651 lines
15 KiB
C
651 lines
15 KiB
C
/*
|
|
* Copyright (c) 2019 Vestas Wind Systems A/S
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @file
|
|
* @brief Driver for Atmel AT24 I2C and Atmel AT25 SPI EEPROMs.
|
|
*/
|
|
|
|
#include <drivers/eeprom.h>
|
|
#include <drivers/gpio.h>
|
|
#include <drivers/i2c.h>
|
|
#include <drivers/spi.h>
|
|
#include <sys/byteorder.h>
|
|
#include <zephyr.h>
|
|
|
|
#define LOG_LEVEL CONFIG_EEPROM_LOG_LEVEL
|
|
#include <logging/log.h>
|
|
LOG_MODULE_REGISTER(eeprom_at2x);
|
|
|
|
/* AT25 instruction set */
|
|
#define EEPROM_AT25_WRSR 0x01U /* Write STATUS register */
|
|
#define EEPROM_AT25_WRITE 0x02U /* Write data to memory array */
|
|
#define EEPROM_AT25_READ 0x03U /* Read data from memory array */
|
|
#define EEPROM_AT25_WRDI 0x04U /* Reset the write enable latch */
|
|
#define EEPROM_AT25_RDSR 0x05U /* Read STATUS register */
|
|
#define EEPROM_AT25_WREN 0x06U /* Set the write enable latch */
|
|
|
|
/* AT25 status register bits */
|
|
#define EEPROM_AT25_STATUS_WIP BIT(0) /* Write-In-Process (RO) */
|
|
#define EEPROM_AT25_STATUS_WEL BIT(1) /* Write Enable Latch (RO) */
|
|
#define EEPROM_AT25_STATUS_BP0 BIT(2) /* Block Protection 0 (RW) */
|
|
#define EEPROM_AT25_STATUS_BP1 BIT(3) /* Block Protection 1 (RW) */
|
|
|
|
struct eeprom_at2x_config {
|
|
union {
|
|
#ifdef CONFIG_EEPROM_AT24
|
|
struct i2c_dt_spec i2c;
|
|
#endif /* CONFIG_EEPROM_AT24 */
|
|
#ifdef CONFIG_EEPROM_AT25
|
|
struct spi_dt_spec spi;
|
|
#endif /* CONFIG_EEPROM_AT25 */
|
|
} bus;
|
|
struct gpio_dt_spec wp_gpio;
|
|
size_t size;
|
|
size_t pagesize;
|
|
uint8_t addr_width;
|
|
bool readonly;
|
|
uint16_t timeout;
|
|
bool (*bus_is_ready)(const struct device *dev);
|
|
eeprom_api_read read_fn;
|
|
eeprom_api_write write_fn;
|
|
};
|
|
|
|
struct eeprom_at2x_data {
|
|
struct k_mutex lock;
|
|
};
|
|
|
|
static inline int eeprom_at2x_write_protect(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
if (!config->wp_gpio.port) {
|
|
return 0;
|
|
}
|
|
|
|
return gpio_pin_set_dt(&config->wp_gpio, 1);
|
|
}
|
|
|
|
static inline int eeprom_at2x_write_enable(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
if (!config->wp_gpio.port) {
|
|
return 0;
|
|
}
|
|
|
|
return gpio_pin_set_dt(&config->wp_gpio, 0);
|
|
}
|
|
|
|
static int eeprom_at2x_read(const struct device *dev, off_t offset, void *buf,
|
|
size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
struct eeprom_at2x_data *data = dev->data;
|
|
uint8_t *pbuf = buf;
|
|
int ret;
|
|
|
|
if (!len) {
|
|
return 0;
|
|
}
|
|
|
|
if ((offset + len) > config->size) {
|
|
LOG_WRN("attempt to read past device boundary");
|
|
return -EINVAL;
|
|
}
|
|
|
|
k_mutex_lock(&data->lock, K_FOREVER);
|
|
while (len) {
|
|
ret = config->read_fn(dev, offset, pbuf, len);
|
|
if (ret < 0) {
|
|
LOG_ERR("failed to read EEPROM (err %d)", ret);
|
|
k_mutex_unlock(&data->lock);
|
|
return ret;
|
|
}
|
|
|
|
pbuf += ret;
|
|
offset += ret;
|
|
len -= ret;
|
|
}
|
|
|
|
k_mutex_unlock(&data->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static size_t eeprom_at2x_limit_write_count(const struct device *dev,
|
|
off_t offset,
|
|
size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
size_t count = len;
|
|
off_t page_boundary;
|
|
|
|
/* We can at most write one page at a time */
|
|
if (count > config->pagesize) {
|
|
count = config->pagesize;
|
|
}
|
|
|
|
/* Writes can not cross a page boundary */
|
|
page_boundary = ROUND_UP(offset + 1, config->pagesize);
|
|
if (offset + count > page_boundary) {
|
|
count = page_boundary - offset;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static int eeprom_at2x_write(const struct device *dev, off_t offset,
|
|
const void *buf,
|
|
size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
struct eeprom_at2x_data *data = dev->data;
|
|
const uint8_t *pbuf = buf;
|
|
int ret;
|
|
|
|
if (config->readonly) {
|
|
LOG_WRN("attempt to write to read-only device");
|
|
return -EACCES;
|
|
}
|
|
|
|
if (!len) {
|
|
return 0;
|
|
}
|
|
|
|
if ((offset + len) > config->size) {
|
|
LOG_WRN("attempt to write past device boundary");
|
|
return -EINVAL;
|
|
}
|
|
|
|
k_mutex_lock(&data->lock, K_FOREVER);
|
|
|
|
ret = eeprom_at2x_write_enable(dev);
|
|
if (ret) {
|
|
LOG_ERR("failed to write-enable EEPROM (err %d)", ret);
|
|
k_mutex_unlock(&data->lock);
|
|
return ret;
|
|
}
|
|
|
|
while (len) {
|
|
ret = config->write_fn(dev, offset, pbuf, len);
|
|
if (ret < 0) {
|
|
LOG_ERR("failed to write to EEPROM (err %d)", ret);
|
|
eeprom_at2x_write_protect(dev);
|
|
k_mutex_unlock(&data->lock);
|
|
return ret;
|
|
}
|
|
|
|
pbuf += ret;
|
|
offset += ret;
|
|
len -= ret;
|
|
}
|
|
|
|
ret = eeprom_at2x_write_protect(dev);
|
|
if (ret) {
|
|
LOG_ERR("failed to write-protect EEPROM (err %d)", ret);
|
|
}
|
|
|
|
k_mutex_unlock(&data->lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static size_t eeprom_at2x_size(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
return config->size;
|
|
}
|
|
|
|
#ifdef CONFIG_EEPROM_AT24
|
|
|
|
static bool eeprom_at24_bus_is_ready(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
return device_is_ready(config->bus.i2c.bus);
|
|
}
|
|
|
|
/**
|
|
* @brief translate an offset to a device address / offset pair
|
|
*
|
|
* It allows to address several devices as a continuous memory region
|
|
* but also to address higher part of eeprom for chips
|
|
* with more than 2^(addr_width) adressable word.
|
|
*/
|
|
static uint16_t eeprom_at24_translate_offset(const struct device *dev,
|
|
off_t *offset)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
const uint16_t addr_incr = *offset >> config->addr_width;
|
|
*offset &= BIT_MASK(config->addr_width);
|
|
|
|
return config->bus.i2c.addr + addr_incr;
|
|
}
|
|
|
|
static size_t eeprom_at24_adjust_read_count(const struct device *dev,
|
|
off_t offset, size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
const size_t remainder = BIT(config->addr_width) - offset;
|
|
|
|
if (len > remainder) {
|
|
len = remainder;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static int eeprom_at24_read(const struct device *dev, off_t offset, void *buf,
|
|
size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
int64_t timeout;
|
|
uint8_t addr[2];
|
|
uint16_t bus_addr;
|
|
int err;
|
|
|
|
bus_addr = eeprom_at24_translate_offset(dev, &offset);
|
|
|
|
if (config->addr_width == 16) {
|
|
sys_put_be16(offset, addr);
|
|
} else {
|
|
addr[0] = offset & BIT_MASK(8);
|
|
}
|
|
|
|
len = eeprom_at24_adjust_read_count(dev, offset, len);
|
|
|
|
/*
|
|
* A write cycle may be in progress so reads must be attempted
|
|
* until the current write cycle should be completed.
|
|
*/
|
|
timeout = k_uptime_get() + config->timeout;
|
|
while (1) {
|
|
int64_t now = k_uptime_get();
|
|
err = i2c_write_read(config->bus.i2c.bus, bus_addr,
|
|
addr, config->addr_width / 8,
|
|
buf, len);
|
|
if (!err || now > timeout) {
|
|
break;
|
|
}
|
|
k_sleep(K_MSEC(1));
|
|
}
|
|
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static int eeprom_at24_write(const struct device *dev, off_t offset,
|
|
const void *buf, size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
int count = eeprom_at2x_limit_write_count(dev, offset, len);
|
|
uint8_t block[config->addr_width / 8 + count];
|
|
int64_t timeout;
|
|
uint16_t bus_addr;
|
|
int i = 0;
|
|
int err;
|
|
|
|
bus_addr = eeprom_at24_translate_offset(dev, &offset);
|
|
|
|
/*
|
|
* Not all I2C EEPROMs support repeated start so the the
|
|
* address (offset) and data (buf) must be provided in one
|
|
* write transaction (block).
|
|
*/
|
|
if (config->addr_width == 16) {
|
|
block[i++] = offset >> 8;
|
|
}
|
|
block[i++] = offset;
|
|
memcpy(&block[i], buf, count);
|
|
|
|
/*
|
|
* A write cycle may already be in progress so writes must be
|
|
* attempted until the previous write cycle should be
|
|
* completed.
|
|
*/
|
|
timeout = k_uptime_get() + config->timeout;
|
|
while (1) {
|
|
int64_t now = k_uptime_get();
|
|
err = i2c_write(config->bus.i2c.bus, block, sizeof(block),
|
|
bus_addr);
|
|
if (!err || now > timeout) {
|
|
break;
|
|
}
|
|
k_sleep(K_MSEC(1));
|
|
}
|
|
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
#endif /* CONFIG_EEPROM_AT24 */
|
|
|
|
#ifdef CONFIG_EEPROM_AT25
|
|
|
|
static bool eeprom_at25_bus_is_ready(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
|
|
return spi_is_ready(&config->bus.spi);
|
|
}
|
|
|
|
static int eeprom_at25_rdsr(const struct device *dev, uint8_t *status)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
uint8_t rdsr[2] = { EEPROM_AT25_RDSR, 0 };
|
|
uint8_t sr[2];
|
|
int err;
|
|
const struct spi_buf tx_buf = {
|
|
.buf = rdsr,
|
|
.len = sizeof(rdsr),
|
|
};
|
|
const struct spi_buf_set tx = {
|
|
.buffers = &tx_buf,
|
|
.count = 1,
|
|
};
|
|
const struct spi_buf rx_buf = {
|
|
.buf = sr,
|
|
.len = sizeof(sr),
|
|
};
|
|
const struct spi_buf_set rx = {
|
|
.buffers = &rx_buf,
|
|
.count = 1,
|
|
};
|
|
|
|
err = spi_transceive_dt(&config->bus.spi, &tx, &rx);
|
|
if (!err) {
|
|
*status = sr[1];
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int eeprom_at25_wait_for_idle(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
int64_t timeout;
|
|
uint8_t status;
|
|
int err;
|
|
|
|
timeout = k_uptime_get() + config->timeout;
|
|
while (1) {
|
|
int64_t now = k_uptime_get();
|
|
err = eeprom_at25_rdsr(dev, &status);
|
|
if (err) {
|
|
LOG_ERR("Could not read status register (err %d)", err);
|
|
return err;
|
|
}
|
|
|
|
if (!(status & EEPROM_AT25_STATUS_WIP)) {
|
|
return 0;
|
|
}
|
|
if (now > timeout) {
|
|
break;
|
|
}
|
|
k_sleep(K_MSEC(1));
|
|
}
|
|
|
|
return -EBUSY;
|
|
}
|
|
|
|
static int eeprom_at25_read(const struct device *dev, off_t offset, void *buf,
|
|
size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
struct eeprom_at2x_data *data = dev->data;
|
|
size_t cmd_len = 1 + config->addr_width / 8;
|
|
uint8_t cmd[4] = { EEPROM_AT25_READ, 0, 0, 0 };
|
|
uint8_t *paddr;
|
|
int err;
|
|
const struct spi_buf tx_buf = {
|
|
.buf = cmd,
|
|
.len = cmd_len,
|
|
};
|
|
const struct spi_buf_set tx = {
|
|
.buffers = &tx_buf,
|
|
.count = 1,
|
|
};
|
|
const struct spi_buf rx_bufs[2] = {
|
|
{
|
|
.buf = NULL,
|
|
.len = cmd_len,
|
|
},
|
|
{
|
|
.buf = buf,
|
|
.len = len,
|
|
},
|
|
};
|
|
const struct spi_buf_set rx = {
|
|
.buffers = rx_bufs,
|
|
.count = ARRAY_SIZE(rx_bufs),
|
|
};
|
|
|
|
if (!len) {
|
|
return 0;
|
|
}
|
|
|
|
if ((offset + len) > config->size) {
|
|
LOG_WRN("attempt to read past device boundary");
|
|
return -EINVAL;
|
|
}
|
|
|
|
paddr = &cmd[1];
|
|
switch (config->addr_width) {
|
|
case 24:
|
|
*paddr++ = offset >> 16;
|
|
__fallthrough;
|
|
case 16:
|
|
*paddr++ = offset >> 8;
|
|
__fallthrough;
|
|
case 8:
|
|
*paddr++ = offset;
|
|
break;
|
|
default:
|
|
__ASSERT(0, "invalid address width");
|
|
}
|
|
|
|
err = eeprom_at25_wait_for_idle(dev);
|
|
if (err) {
|
|
LOG_ERR("EEPROM idle wait failed (err %d)", err);
|
|
k_mutex_unlock(&data->lock);
|
|
return err;
|
|
}
|
|
|
|
err = spi_transceive_dt(&config->bus.spi, &tx, &rx);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static int eeprom_at25_wren(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
uint8_t cmd = EEPROM_AT25_WREN;
|
|
const struct spi_buf tx_buf = {
|
|
.buf = &cmd,
|
|
.len = 1,
|
|
};
|
|
const struct spi_buf_set tx = {
|
|
.buffers = &tx_buf,
|
|
.count = 1,
|
|
};
|
|
|
|
return spi_write_dt(&config->bus.spi, &tx);
|
|
}
|
|
|
|
static int eeprom_at25_write(const struct device *dev, off_t offset,
|
|
const void *buf, size_t len)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
int count = eeprom_at2x_limit_write_count(dev, offset, len);
|
|
uint8_t cmd[4] = { EEPROM_AT25_WRITE, 0, 0, 0 };
|
|
size_t cmd_len = 1 + config->addr_width / 8;
|
|
uint8_t *paddr;
|
|
int err;
|
|
const struct spi_buf tx_bufs[2] = {
|
|
{
|
|
.buf = cmd,
|
|
.len = cmd_len,
|
|
},
|
|
{
|
|
.buf = (void *)buf,
|
|
.len = count,
|
|
},
|
|
};
|
|
const struct spi_buf_set tx = {
|
|
.buffers = tx_bufs,
|
|
.count = ARRAY_SIZE(tx_bufs),
|
|
};
|
|
|
|
paddr = &cmd[1];
|
|
switch (config->addr_width) {
|
|
case 24:
|
|
*paddr++ = offset >> 16;
|
|
__fallthrough;
|
|
case 16:
|
|
*paddr++ = offset >> 8;
|
|
__fallthrough;
|
|
case 8:
|
|
*paddr++ = offset;
|
|
break;
|
|
default:
|
|
__ASSERT(0, "invalid address width");
|
|
}
|
|
|
|
err = eeprom_at25_wait_for_idle(dev);
|
|
if (err) {
|
|
LOG_ERR("EEPROM idle wait failed (err %d)", err);
|
|
return err;
|
|
}
|
|
|
|
err = eeprom_at25_wren(dev);
|
|
if (err) {
|
|
LOG_ERR("failed to disable write protection (err %d)", err);
|
|
return err;
|
|
}
|
|
|
|
err = spi_transceive_dt(&config->bus.spi, &tx, NULL);
|
|
if (err) {
|
|
return err;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
#endif /* CONFIG_EEPROM_AT25 */
|
|
|
|
static int eeprom_at2x_init(const struct device *dev)
|
|
{
|
|
const struct eeprom_at2x_config *config = dev->config;
|
|
struct eeprom_at2x_data *data = dev->data;
|
|
int err;
|
|
|
|
k_mutex_init(&data->lock);
|
|
|
|
if (!config->bus_is_ready(dev)) {
|
|
LOG_ERR("parent bus device not ready");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config->wp_gpio.port) {
|
|
if (!device_is_ready(config->wp_gpio.port)) {
|
|
LOG_ERR("wp gpio device not ready");
|
|
return -EINVAL;
|
|
}
|
|
|
|
err = gpio_pin_configure_dt(&config->wp_gpio, GPIO_OUTPUT_ACTIVE);
|
|
if (err) {
|
|
LOG_ERR("failed to configure WP GPIO pin (err %d)", err);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct eeprom_driver_api eeprom_at2x_api = {
|
|
.read = eeprom_at2x_read,
|
|
.write = eeprom_at2x_write,
|
|
.size = eeprom_at2x_size,
|
|
};
|
|
|
|
#define ASSERT_AT24_ADDR_W_VALID(w) \
|
|
BUILD_ASSERT(w == 8U || w == 16U, \
|
|
"Unsupported address width")
|
|
|
|
#define ASSERT_AT25_ADDR_W_VALID(w) \
|
|
BUILD_ASSERT(w == 8U || w == 16U || w == 24U, \
|
|
"Unsupported address width")
|
|
|
|
#define ASSERT_PAGESIZE_IS_POWER_OF_2(page) \
|
|
BUILD_ASSERT((page != 0U) && ((page & (page - 1)) == 0U), \
|
|
"Page size is not a power of two")
|
|
|
|
#define ASSERT_SIZE_PAGESIZE_VALID(size, page) \
|
|
BUILD_ASSERT(size % page == 0U, \
|
|
"Size is not an integer multiple of page size")
|
|
|
|
#define INST_DT_AT2X(inst, t) DT_INST(inst, atmel_at##t)
|
|
|
|
#define EEPROM_AT24_BUS(n, t) \
|
|
{ .i2c = I2C_DT_SPEC_GET(INST_DT_AT2X(n, t)) }
|
|
|
|
#define EEPROM_AT25_BUS(n, t) \
|
|
{ .spi = SPI_DT_SPEC_GET(INST_DT_AT2X(n, t), \
|
|
SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | \
|
|
SPI_WORD_SET(8), 0) }
|
|
|
|
#define EEPROM_AT2X_DEVICE(n, t) \
|
|
ASSERT_PAGESIZE_IS_POWER_OF_2(DT_PROP(INST_DT_AT2X(n, t), pagesize)); \
|
|
ASSERT_SIZE_PAGESIZE_VALID(DT_PROP(INST_DT_AT2X(n, t), size), \
|
|
DT_PROP(INST_DT_AT2X(n, t), pagesize)); \
|
|
ASSERT_AT##t##_ADDR_W_VALID(DT_PROP(INST_DT_AT2X(n, t), \
|
|
address_width)); \
|
|
static const struct eeprom_at2x_config eeprom_at##t##_config_##n = { \
|
|
.bus = EEPROM_AT##t##_BUS(n, t), \
|
|
.wp_gpio = GPIO_DT_SPEC_GET_OR(INST_DT_AT2X(n, t), wp_gpios, {0}), \
|
|
.size = DT_PROP(INST_DT_AT2X(n, t), size), \
|
|
.pagesize = DT_PROP(INST_DT_AT2X(n, t), pagesize), \
|
|
.addr_width = DT_PROP(INST_DT_AT2X(n, t), address_width), \
|
|
.readonly = DT_PROP(INST_DT_AT2X(n, t), read_only), \
|
|
.timeout = DT_PROP(INST_DT_AT2X(n, t), timeout), \
|
|
.bus_is_ready = eeprom_at##t##_bus_is_ready, \
|
|
.read_fn = eeprom_at##t##_read, \
|
|
.write_fn = eeprom_at##t##_write, \
|
|
}; \
|
|
static struct eeprom_at2x_data eeprom_at##t##_data_##n; \
|
|
DEVICE_DT_DEFINE(INST_DT_AT2X(n, t), &eeprom_at2x_init, \
|
|
NULL, &eeprom_at##t##_data_##n, \
|
|
&eeprom_at##t##_config_##n, POST_KERNEL, \
|
|
CONFIG_EEPROM_INIT_PRIORITY, \
|
|
&eeprom_at2x_api)
|
|
|
|
#define EEPROM_AT24_DEVICE(n) EEPROM_AT2X_DEVICE(n, 24)
|
|
#define EEPROM_AT25_DEVICE(n) EEPROM_AT2X_DEVICE(n, 25)
|
|
|
|
#define CALL_WITH_ARG(arg, expr) expr(arg);
|
|
|
|
#define INST_DT_AT2X_FOREACH(t, inst_expr) \
|
|
UTIL_LISTIFY(DT_NUM_INST_STATUS_OKAY(atmel_at##t), \
|
|
CALL_WITH_ARG, inst_expr)
|
|
|
|
#ifdef CONFIG_EEPROM_AT24
|
|
INST_DT_AT2X_FOREACH(24, EEPROM_AT24_DEVICE);
|
|
#endif
|
|
|
|
#ifdef CONFIG_EEPROM_AT25
|
|
INST_DT_AT2X_FOREACH(25, EEPROM_AT25_DEVICE);
|
|
#endif
|