/* * 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 #include #include #include #include #include #define LOG_LEVEL CONFIG_EEPROM_LOG_LEVEL #include 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) */ #define HAS_WP_OR(id) DT_NODE_HAS_PROP(id, wp_gpios) || #define ANY_INST_HAS_WP_GPIOS (DT_FOREACH_STATUS_OKAY(atmel_at24, HAS_WP_OR) \ DT_FOREACH_STATUS_OKAY(atmel_at25, HAS_WP_OR) 0) 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; #if ANY_INST_HAS_WP_GPIOS struct gpio_dt_spec wp_gpio; #endif /* ANY_INST_HAS_WP_GPIOS */ 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; }; #if ANY_INST_HAS_WP_GPIOS 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); } #endif /* ANY_INST_HAS_WP_GPIOS */ 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); #if ANY_INST_HAS_WP_GPIOS 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; } #endif /* ANY_INST_HAS_WP_GPIOS */ while (len) { ret = config->write_fn(dev, offset, pbuf, len); if (ret < 0) { LOG_ERR("failed to write to EEPROM (err %d)", ret); #if ANY_INST_HAS_WP_GPIOS eeprom_at2x_write_protect(dev); #endif /* ANY_INST_HAS_WP_GPIOS */ k_mutex_unlock(&data->lock); return ret; } pbuf += ret; offset += ret; len -= ret; } #if ANY_INST_HAS_WP_GPIOS ret = eeprom_at2x_write_protect(dev); if (ret) { LOG_ERR("failed to write-protect EEPROM (err %d)", ret); } #else ret = 0; #endif /* ANY_INST_HAS_WP_GPIOS */ 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; k_mutex_init(&data->lock); if (!config->bus_is_ready(dev)) { LOG_ERR("parent bus device not ready"); return -EINVAL; } #if ANY_INST_HAS_WP_GPIOS if (config->wp_gpio.port) { int err; 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; } } #endif /* ANY_INST_HAS_WP_GPIOS */ 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_WP_GPIOS(id) \ IF_ENABLED(DT_NODE_HAS_PROP(id, wp_gpios), \ (.wp_gpio = GPIO_DT_SPEC_GET(id, wp_gpios),)) #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), \ EEPROM_AT2X_WP_GPIOS(INST_DT_AT2X(n, t)) \ .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) \ 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