From 126e946fcbb8876f995d43a6d6d47b73c04c1a5a Mon Sep 17 00:00:00 2001 From: Liam Ogletree Date: Wed, 9 Jul 2025 12:11:16 -0500 Subject: [PATCH 1/2] drivers: flash: Add support for Atmel AT25 SPI flash variant The AT25XV021A variant is a flash variant of Atmel's AT25 family that adds extra protections, requiring additional writes to the device to program or erase data. This commit adds a flash driver for AT25XV021A devices instead of modifying (1) the existing AT45 SPI flash driver or (2) the existing AT24/25 EEPROM driver because this variant poses fundamental changes that affect all aspects of the driver. Notably, - AT25XV021A includes a second status register, and the format and functions of the existing status register is changed from the existing drivers. - AT25XV021A requires executing page or chip erase commands before writing, making it incompatible with the existing AT24/25 EEPROM driver. - AT25XV021A adds a software protection layer that requires extra writes before executing program or erase commands. Tested writing to and erasing from an AT25XV021A device. Tested reading from an AT25XV021A device across page boundaries. Tested chip erase function. Tested driver initialization from varying initial hardware states. Signed-off-by: Liam Ogletree --- drivers/flash/CMakeLists.txt | 1 + drivers/flash/Kconfig | 1 + drivers/flash/Kconfig.at25xv021a | 17 + drivers/flash/spi_flash_at25xv021a.c | 993 +++++++++++++++++++++++++ dts/bindings/mtd/atmel,at25xv021a.yaml | 60 ++ 5 files changed, 1072 insertions(+) create mode 100644 drivers/flash/Kconfig.at25xv021a create mode 100644 drivers/flash/spi_flash_at25xv021a.c create mode 100644 dts/bindings/mtd/atmel,at25xv021a.yaml diff --git a/drivers/flash/CMakeLists.txt b/drivers/flash/CMakeLists.txt index 95493b13e10bd..a94b1fb7d8adf 100644 --- a/drivers/flash/CMakeLists.txt +++ b/drivers/flash/CMakeLists.txt @@ -73,6 +73,7 @@ zephyr_library_sources_ifdef(CONFIG_SOC_FLASH_SILABS_SIWX91X soc_flash_silabs_si zephyr_library_sources_ifdef(CONFIG_SOC_FLASH_SMARTBOND flash_smartbond.c) zephyr_library_sources_ifdef(CONFIG_SOC_FLASH_TELINK_B91 soc_flash_b91.c) zephyr_library_sources_ifdef(CONFIG_SOC_FLASH_XMC4XXX soc_flash_xmc4xxx.c) +zephyr_library_sources_ifdef(CONFIG_SPI_FLASH_AT25XV021A spi_flash_at25xv021a.c) zephyr_library_sources_ifdef(CONFIG_SPI_FLASH_AT45 spi_flash_at45.c) zephyr_library_sources_ifdef(CONFIG_SPI_NOR spi_nor.c) # zephyr-keep-sorted-stop diff --git a/drivers/flash/Kconfig b/drivers/flash/Kconfig index f3eaeade2257e..d76d9deaf9920 100644 --- a/drivers/flash/Kconfig +++ b/drivers/flash/Kconfig @@ -167,6 +167,7 @@ config FLASH_INIT_PRIORITY source "drivers/flash/Kconfig.adi_max32_spixf" source "drivers/flash/Kconfig.ambiq" source "drivers/flash/Kconfig.andes" +source "drivers/flash/Kconfig.at25xv021a" source "drivers/flash/Kconfig.at45" source "drivers/flash/Kconfig.b91" source "drivers/flash/Kconfig.cadence_nand" diff --git a/drivers/flash/Kconfig.at25xv021a b/drivers/flash/Kconfig.at25xv021a new file mode 100644 index 0000000000000..01b7b1f2fdf31 --- /dev/null +++ b/drivers/flash/Kconfig.at25xv021a @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +config SPI_FLASH_AT25XV021A + bool "Atmel AT25 SPI flash variant" + default y + depends on DT_HAS_ATMEL_AT25XV021A_ENABLED + select FLASH_HAS_DRIVER_ENABLED + select FLASH_HAS_PAGE_LAYOUT + select FLASH_HAS_EXPLICIT_ERASE + select SPI + help + Enable support for AT25XV021A SPI flash variants. + + Vendors that have manufactured such variants include: + - Renesas Electronics + - Adesto Technologies diff --git a/drivers/flash/spi_flash_at25xv021a.c b/drivers/flash/spi_flash_at25xv021a.c new file mode 100644 index 0000000000000..07b6da95e90b0 --- /dev/null +++ b/drivers/flash/spi_flash_at25xv021a.c @@ -0,0 +1,993 @@ +/* + * Copyright (c) 2025 Cirrus Logic, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Driver for AT25XV021A SPI flash devices, a variant of Atmel's AT25 family + */ + +#define DT_DRV_COMPAT atmel_at25xv021a + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(spi_flash_at25xv021a, CONFIG_FLASH_LOG_LEVEL); + +/* AT25XV021A opcodes */ +#define DEV_READ (0x0b) +#define DEV_PAGE_ERASE (0x81) +#define DEV_CHIP_ERASE (0x60) +#define DEV_WRITE (0x02) +#define DEV_WRITE_ENABLE (0x06) +#define DEV_PROTECT (0x36) +#define DEV_UNPROTECT (0x39) +#define DEV_READ_SR (0x05) +#define DEV_WRITE_SR (0x01) +#define DEV_READ_DEVICE_INFO (0x9f) +#define DEV_DEEP_SLEEP (0xb9) +#define DEV_ULTRA_DEEP_SLEEP (0x79) +#define DEV_RESUME (0xab) + +/* AT25XV021A driver instruction set */ +#define DEV_DUMMY_BYTE (0x00) +#define DEV_HW_LOCK (0xf8) +#define DEV_HW_UNLOCK (0x00) +#define DEV_GLOBAL_PROTECT (0x7f) +#define DEV_GLOBAL_UNPROTECT (0x00) + +/* AT25XV021A status register masks */ +#define DEV_SR_BUSY (1U << 0) +#define DEV_SR_WEL (1U << 1) +#define DEV_SR_SWP (3U << 2) +#define DEV_SR_WPP (1U << 4) +#define DEV_SR_EPE (1U << 5) +#define DEV_SR_SPRL (1U << 7) + +#define ANY_DEV_WRITEABLE !DT_ALL_INST_HAS_BOOL_STATUS_OKAY(read_only) +#define ANY_DEV_HAS_WP_GPIO DT_ANY_INST_HAS_PROP_STATUS_OKAY(wp_gpios) + +struct flash_at25xv021a_config { + struct spi_dt_spec spi; +#if ANY_DEV_HAS_WP_GPIO + struct gpio_dt_spec wp_gpio; +#endif /* ANY_DEV_HAS_WP_GPIO */ +#if defined(CONFIG_FLASH_PAGE_LAYOUT) + struct flash_pages_layout pages_layout; +#endif /* defined(CONFIG_FLASH_PAGE_LAYOUT) */ + struct flash_parameters parameters; + uint8_t jedec_id[3]; + size_t size; + k_timeout_t timeout; + bool read_only; + bool ultra_deep_sleep; +#if ANY_DEV_WRITEABLE + size_t page_size; + k_timeout_t timeout_erase; +#endif /* ANY_DEV_WRITEABLE */ +}; + +struct flash_at25xv021a_data { + struct k_mutex lock; +}; + +static int flash_at25xv021a_read_status(const struct device *dev, uint8_t *status) +{ + int err; + uint8_t sr[2]; + uint8_t cmd[2] = {DEV_READ_SR, DEV_DUMMY_BYTE}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + const struct spi_buf rx_buf = {.buf = sr, .len = ARRAY_SIZE(sr)}; + const struct spi_buf_set rx = {.buffers = &rx_buf, .count = 1}; + + err = spi_transceive_dt(&config->spi, &tx, &rx); + if (err < 0) { + LOG_ERR("unable to read status register from %s", dev->name); + return err; + } + + *status = sr[1]; + + return err; +} + +static int flash_at25xv021a_wait_for_idle(const struct device *dev, k_timeout_t timeout) +{ + int err; + uint8_t status; + k_timepoint_t end = sys_timepoint_calc(timeout); + + while (!sys_timepoint_expired(end)) { + err = flash_at25xv021a_read_status(dev, &status); + if (err != 0) { + return err; + } + + if ((status & DEV_SR_BUSY) == 0) { + return 0; + } + + k_msleep(1); + } + + LOG_ERR("timed out waiting for %s to idle", dev->name); + return -EBUSY; +} + +static int flash_at25xv021a_spi_transceive(const struct device *dev, const struct spi_dt_spec *spi, + const struct spi_buf_set *tx, + const struct spi_buf_set *rx) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + + err = flash_at25xv021a_wait_for_idle(dev, config->timeout); + if (err != 0) { + return err; + } + + err = spi_transceive_dt(spi, tx, rx); + if (err < 0) { + LOG_ERR("unable to read from %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_verify_device(const struct device *dev) +{ + int err; + uint8_t info[3]; + uint8_t cmd[1] = {DEV_READ_DEVICE_INFO}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + const struct spi_buf rx_bufs[2] = { + {.buf = NULL, .len = ARRAY_SIZE(cmd)}, + {.buf = info, .len = ARRAY_SIZE(info)}, + }; + const struct spi_buf_set rx = {.buffers = rx_bufs, .count = ARRAY_SIZE(rx_bufs)}; + + err = flash_at25xv021a_spi_transceive(dev, &config->spi, &tx, &rx); + if (err != 0) { + return err; + } + + if ((info[0] != config->jedec_id[0]) || (info[1] != config->jedec_id[1]) || + (info[2] != config->jedec_id[2])) { + return -ENODEV; + } + + return err; +} + +static int flash_at25xv021a_read_internal(const struct device *dev, off_t offset, void *buf, + size_t len) +{ + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[5] = { + DEV_READ, + FIELD_GET(GENMASK(23, 16), offset), + FIELD_GET(GENMASK(15, 8), offset), + FIELD_GET(GENMASK(7, 0), offset), + DEV_DUMMY_BYTE, + }; + const struct spi_buf tx_buf = {.buf = cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = { + .buffers = &tx_buf, + .count = 1, + }; + const struct spi_buf rx_bufs[2] = { + {.buf = NULL, .len = ARRAY_SIZE(cmd)}, + {.buf = buf, .len = len}, + }; + const struct spi_buf_set rx = {.buffers = rx_bufs, .count = ARRAY_SIZE(rx_bufs)}; + + return flash_at25xv021a_spi_transceive(dev, &config->spi, &tx, &rx); +} + +static int flash_at25xv021a_read(const struct device *dev, off_t offset, void *buf, size_t len) +{ + int err; + struct flash_at25xv021a_data *data = dev->data; + const struct flash_at25xv021a_config *config = dev->config; + + if (len == 0) { + LOG_DBG("attempted to read 0 bytes from %s", dev->name); + return 0; + } + + if (len > config->size) { + LOG_ERR("attempted to read more than device %s size: %u", dev->name, config->size); + return -EINVAL; + } + + k_mutex_lock(&data->lock, K_FOREVER); + + err = flash_at25xv021a_read_internal(dev, offset, buf, len); + + k_mutex_unlock(&data->lock); + + return err; +} + +#if ANY_DEV_WRITEABLE +static int flash_at25xv021a_check_status(const struct device *dev, uint8_t mask, uint8_t *status) +{ + int err; + uint8_t temp_status; + const struct flash_at25xv021a_config *config = dev->config; + + err = flash_at25xv021a_wait_for_idle(dev, config->timeout); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_read_status(dev, &temp_status); + if (err != 0) { + return err; + } + + *status = (temp_status & mask); + + return err; +} + +static int flash_at25xv021a_spi_write(const struct device *dev, const struct spi_dt_spec *spi, + const struct spi_buf_set *tx) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + + err = flash_at25xv021a_wait_for_idle(dev, config->timeout); + if (err != 0) { + return err; + } + + err = spi_write_dt(spi, tx); + if (err < 0) { + LOG_ERR("unable to write to %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_write_enable(const struct device *dev) +{ + int err; + uint8_t status; + uint8_t cmd[1] = {DEV_WRITE_ENABLE}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_WEL, &status); + if (err != 0) { + return err; + } + + if (status != DEV_SR_WEL) { + LOG_ERR("unable to enable writes on %s", dev->name); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_hardware_lock(const struct device *dev) +{ + int err; + uint8_t status; + uint8_t cmd[2] = {DEV_WRITE_SR, DEV_HW_LOCK}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + /* Ensure device is idle before configuring WP pin. */ + err = flash_at25xv021a_wait_for_idle(dev, config->timeout); + if (err != 0) { + return err; + } + +#if ANY_DEV_HAS_WP_GPIO + err = gpio_pin_configure_dt(&config->wp_gpio, GPIO_OUTPUT_ACTIVE); + if (err < 0) { + LOG_ERR("unable to set WP GPIO"); + return err; + } +#endif /* ANY_DEV_HAS_WP_GPIOS */ + + err = flash_at25xv021a_check_status(dev, DEV_SR_SPRL, &status); + if (err != 0) { + return err; + } + + if (status != DEV_SR_SPRL) { + LOG_ERR("unable to lock hardware"); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_hardware_unlock(const struct device *dev) +{ + int err; + uint8_t status; + uint8_t cmd[2] = {DEV_WRITE_SR, DEV_HW_UNLOCK}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + /* Ensure device is idle before configuring WP pin. */ + err = flash_at25xv021a_wait_for_idle(dev, config->timeout); + if (err != 0) { + return err; + } + +#if ANY_DEV_HAS_WP_GPIO + err = gpio_pin_configure_dt(&config->wp_gpio, GPIO_OUTPUT_INACTIVE); + if (err < 0) { + LOG_ERR("unable to set WP GPIO"); + return err; + } +#endif /* ANY_DEV_HAS_WP_GPIO */ + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_SPRL, &status); + if (err != 0) { + return err; + } + + if (status == DEV_SR_SPRL) { + LOG_ERR("unable to unlock hardware"); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_global_protection(const struct device *dev, uint8_t protection_cmd) +{ + int err; + uint8_t expected_status, status; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[2] = {DEV_WRITE_SR, protection_cmd}; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_hardware_unlock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_hardware_lock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_SWP, &status); + if (err != 0) { + return err; + } + + expected_status = (protection_cmd == DEV_GLOBAL_PROTECT) ? DEV_SR_SWP : 0; + if (status != expected_status) { + LOG_ERR("unable to update global protection"); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_software_protection(const struct device *dev, off_t page, + uint8_t protection_cmd) +{ + int err; + uint8_t status; + uint8_t unexpected_status; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[4] = { + protection_cmd, + FIELD_GET(GENMASK(23, 16), page), + FIELD_GET(GENMASK(15, 8), page), + FIELD_GET(GENMASK(7, 0), page), + }; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_hardware_unlock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_hardware_lock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_SWP, &status); + if (err != 0) { + return err; + } + + unexpected_status = (protection_cmd == DEV_PROTECT) ? 0 : DEV_SR_SWP; + if (status == unexpected_status) { + LOG_ERR("failed to update software protection for %s", dev->name); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_hardware_init(const struct device *dev) +{ + int err; + uint8_t status; + + err = flash_at25xv021a_hardware_unlock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_global_protection(dev, DEV_GLOBAL_PROTECT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_hardware_lock(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, (DEV_SR_SPRL | DEV_SR_SWP), &status); + if (err != 0) { + return err; + } + + if (status != (DEV_SR_SPRL | DEV_SR_SWP)) { + LOG_ERR("unable to initialize hardware"); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_write_internal(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + int err; + uint8_t status; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[4] = { + DEV_WRITE, + FIELD_GET(GENMASK(23, 16), offset), + FIELD_GET(GENMASK(15, 8), offset), + FIELD_GET(GENMASK(7, 0), offset), + }; + const struct spi_buf tx_bufs[2] = { + {.buf = cmd, .len = ARRAY_SIZE(cmd)}, + {.buf = (void *)buf, .len = len}, + }; + const struct spi_buf_set tx = {.buffers = tx_bufs, .count = ARRAY_SIZE(tx_bufs)}; + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_EPE, &status); + if (err != 0) { + return err; + } + + if (status != 0) { + LOG_ERR("failed to program %s", dev->name); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_process_write(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + off_t page_start = ROUND_DOWN(offset, config->page_size); + + err = flash_at25xv021a_software_protection(dev, page_start, DEV_UNPROTECT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_write_internal(dev, offset, buf, len); + if (err != 0) { + return err; + } + + return flash_at25xv021a_software_protection(dev, page_start, DEV_PROTECT); +} + +static int flash_at25xv021a_chip_erase(const struct device *dev) +{ + int err; + uint8_t status; + uint8_t cmd[1] = {DEV_CHIP_ERASE}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_global_protection(dev, DEV_GLOBAL_UNPROTECT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + /* Need to wait extra time for chip erase. */ + err = flash_at25xv021a_wait_for_idle(dev, config->timeout_erase); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_global_protection(dev, DEV_GLOBAL_PROTECT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_EPE, &status); + if (err != 0) { + return err; + } + + if (status != 0) { + LOG_ERR("failed to erase %s", dev->name); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_erase_internal(const struct device *dev, uint32_t addr) +{ + int err; + uint8_t status; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[4] = { + DEV_PAGE_ERASE, + FIELD_GET(GENMASK(9, 8), addr), + FIELD_GET(GENMASK(7, 0), addr), + DEV_DUMMY_BYTE, + }; + const struct spi_buf tx_buf = {.buf = &cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1}; + + err = flash_at25xv021a_write_enable(dev); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + if (err != 0) { + return err; + } + + /* Page erase operations can take up to 20 milliseconds. */ + err = flash_at25xv021a_wait_for_idle(dev, config->timeout_erase); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_EPE, &status); + if (err != 0) { + return err; + } + + if (status != 0) { + LOG_ERR("unable to erase from %s", dev->name); + return -EIO; + } + + return err; +} + +static int flash_at25xv021a_process_erase(const struct device *dev, off_t *offset) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint32_t page_addr = *offset / config->page_size; + + err = flash_at25xv021a_software_protection(dev, *offset, DEV_UNPROTECT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_erase_internal(dev, page_addr); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_software_protection(dev, *offset, DEV_PROTECT); + if (err != 0) { + return err; + } + + *offset += config->page_size; + + return err; +} + +static int flash_at25xv021a_write(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + int err; + struct flash_at25xv021a_data *data = dev->data; + const struct flash_at25xv021a_config *config = dev->config; + + if (config->read_only) { + LOG_ERR("attempted to write to read-only device %s", dev->name); + return -EINVAL; + } + + if (len == 0) { + LOG_DBG("attempted to write 0 bytes to %s", dev->name); + return 0; + } + + if (len > config->page_size) { + LOG_ERR("attempted to write more than page size in one write operation"); + return -EINVAL; + } + + k_mutex_lock(&data->lock, K_FOREVER); + + err = flash_at25xv021a_process_write(dev, offset, buf, len); + if (err != 0) { + LOG_ERR("unable to complete write operation for %s", dev->name); + } + + k_mutex_unlock(&data->lock); + + return err; +} + +static int flash_at25xv021a_erase(const struct device *dev, off_t offset, size_t size) +{ + int err = 0; + struct flash_at25xv021a_data *data = dev->data; + const struct flash_at25xv021a_config *config = dev->config; + + if (config->read_only) { + LOG_ERR("attempted to erase from read-only device %s", dev->name); + return -EINVAL; + } + + if (size == 0) { + LOG_DBG("attempted to erase 0 bytes from %s", dev->name); + return 0; + } + + if (offset % config->page_size != 0 || size % config->page_size != 0) { + LOG_ERR("offset and/or size is not aligned to page size in %s erase", dev->name); + return -EINVAL; + } + + if (offset + size > config->size) { + LOG_ERR("attempted to erase beyond %s size boundary: %u", dev->name, config->size); + return -EINVAL; + } + + k_mutex_lock(&data->lock, K_FOREVER); + + if (offset == 0 && size == config->size) { + err = flash_at25xv021a_chip_erase(dev); + k_mutex_unlock(&data->lock); + return err; + } + + while (size != 0) { + + err = flash_at25xv021a_process_erase(dev, &offset); + if (err != 0) { + LOG_ERR("unable to complete erase operation for %s", dev->name); + break; + } + + size -= config->page_size; + } + + k_mutex_unlock(&data->lock); + + return err; +} +#else +static int flash_at25xv021a_write(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + LOG_ERR("attempted to write to read-only device %s", dev->name); + + return -EINVAL; +} + +static int flash_at25xv021a_erase(const struct device *dev, off_t offset, size_t size) +{ + LOG_ERR("attempted to erase from read-only device %s", dev->name); + + return -EINVAL; +} +#endif /* ANY_DEV_WRITEABLE */ + +static int flash_at25xv021a_get_size(const struct device *dev, uint64_t *size) +{ + const struct flash_at25xv021a_config *config = dev->config; + + *size = (uint64_t)config->size; + + return 0; +} + +static const struct flash_parameters *flash_at25xv021a_get_parameters(const struct device *dev) +{ + const struct flash_at25xv021a_config *config = dev->config; + + return &config->parameters; +} + +#if defined(CONFIG_FLASH_PAGE_LAYOUT) +static void flash_at25xv021a_pages_layout(const struct device *dev, + const struct flash_pages_layout **layout, + size_t *layout_size) +{ + const struct flash_at25xv021a_config *config = dev->config; + + *layout = &config->pages_layout; + *layout_size = 1; +} +#endif /* defined(CONFIG_FLASH_PAGE_LAYOUT) */ + +#ifdef CONFIG_PM_DEVICE +static int flash_at25xv021a_resume(const struct device *dev) +{ + int err; + uint8_t cmd[1] = {DEV_RESUME}; + const struct flash_at25xv021a_config *config = dev->config; + const struct spi_buf tx_bufs = {.buf = cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_bufs, .count = 1}; + + /* If in ultra deep sleep mode, we can send any command. Don't check return status. */ + (void)spi_write_dt(&config->spi, &tx); + + /* Device takes a minimum of 70 microseconds to exit ultra deep sleep mode. */ + k_msleep(1); + + err = flash_at25xv021a_verify_device(dev); + if (err != 0) { + LOG_ERR("failed to resume %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_suspend(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[1] = {config->ultra_deep_sleep ? DEV_ULTRA_DEEP_SLEEP : DEV_DEEP_SLEEP}; + const struct spi_buf tx_bufs = {.buf = cmd, .len = ARRAY_SIZE(cmd)}; + const struct spi_buf_set tx = {.buffers = &tx_bufs, .count = 1}; + + /* Longer timeout in case suspend is called during a chip erase operation. */ + err = flash_at25xv021a_wait_for_idle(dev, config->timeout_erase); + if (err != 0) { + return err; + } + + return flash_at25xv021a_spi_write(dev, &config->spi, &tx); +} + +static int flash_at25xv021a_pm_action(const struct device *dev, enum pm_device_action action) +{ + switch (action) { + case PM_DEVICE_ACTION_RESUME: + return flash_at25xv021a_resume(dev); + case PM_DEVICE_ACTION_SUSPEND: + return flash_at25xv021a_suspend(dev); + case PM_DEVICE_ACTION_TURN_OFF: + __fallthrough; + case PM_DEVICE_ACTION_TURN_ON: + __fallthrough; + default: + break; + } + + return -ENOTSUP; +} +#endif /* CONFIG_PM_DEVICE */ + +static int flash_at25xv021a_init(const struct device *dev) +{ + int err; + struct flash_at25xv021a_data *data = dev->data; + const struct flash_at25xv021a_config *config = dev->config; + + err = k_mutex_init(&data->lock); + if (err != 0) { + LOG_ERR("unable to initialize mutex"); + return err; + } + + if (!device_is_ready(config->spi.bus)) { + LOG_ERR("spi bus is not ready"); + return -ENODEV; + } + +#ifdef CONFIG_PM_DEVICE + /* Resume if device was previously suspended. */ + err = flash_at25xv021a_resume(dev); + if (err != 0) { + return err; + } +#endif /* CONFIG_PM_DEVICE */ + + err = flash_at25xv021a_verify_device(dev); + if (err != 0) { + LOG_ERR("unable to verify device information"); + return err; + } + +#if ANY_DEV_WRITEABLE && ANY_DEV_HAS_WP_GPIO + if (!device_is_ready(config->wp_gpio.port)) { + LOG_ERR("device controlling WP GPIO is not ready"); + return -ENODEV; + } + + if (!gpio_is_ready_dt(&config->wp_gpio)) { + LOG_ERR("WP GPIO is not ready"); + return -ENODEV; + } +#endif /* ANY_DEV_WRITEABLE && ANY_DEV_HAS_WP_GPIO */ + +#if ANY_DEV_WRITEABLE + return flash_at25xv021a_hardware_init(dev); +#else + return 0; +#endif /* ANY_DEV_WRITEABLE */ +} + +static DEVICE_API(flash, spi_flash_at25xv021a_api) = { + .read = flash_at25xv021a_read, + .write = flash_at25xv021a_write, + .erase = flash_at25xv021a_erase, + .get_size = flash_at25xv021a_get_size, + .get_parameters = flash_at25xv021a_get_parameters, +#if defined(CONFIG_FLASH_PAGE_LAYOUT) + .page_layout = flash_at25xv021a_pages_layout, +#endif +}; + +#define ASSERT_SIZE(sz) BUILD_ASSERT(sz > 0, "Size must be positive") + +#define ASSERT_PAGE_SIZE(pg) \ + BUILD_ASSERT(IS_POWER_OF_TWO(pg), "Page size must be positive and a power of 2") + +#define ASSERT_TIMEOUTS(timeout, timeout_erase) \ + BUILD_ASSERT((timeout > 0) && (timeout_erase > 0), "Timeouts must be positive") + +#define SPI_OP (SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8)) + +#define PAGE_SIZE_IF_WRITEABLE(inst) \ + COND_CODE_0(DT_INST_NODE_PROP_OR(inst, read_only, false), \ + (.page_size = DT_INST_PROP(inst, page_size),), \ + (EMPTY)) + +#define TIMEOUT_ERASE_IF_WRITEABLE(inst) \ + COND_CODE_0(DT_INST_NODE_PROP_OR(inst, read_only, false), \ + (.timeout_erase = K_MSEC(DT_INST_PROP(inst, timeout_erase)),), \ + (EMPTY)) + +#define WP_GPIO_IF_PROVIDED(inst) \ + IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, wp_gpios), \ + (.wp_gpio = GPIO_DT_SPEC_INST_GET(inst, wp_gpios),)) + +#define PAGES_LAYOUT_IF_ENABLED(inst) \ + IF_ENABLED(CONFIG_FLASH_PAGE_LAYOUT, (.pages_layout = { \ + .pages_count = DT_INST_PROP(inst, size) / DT_INST_PROP(inst, page_size),\ + .pages_size = DT_INST_PROP(inst, page_size), \ + },)) + +#define SPI_FLASH_AT25XV021A_DEFINE(inst) \ + \ + ASSERT_SIZE(DT_INST_PROP(inst, size)); \ + ASSERT_PAGE_SIZE(DT_INST_PROP(inst, page_size)); \ + ASSERT_TIMEOUTS(DT_INST_PROP(inst, timeout), DT_INST_PROP(inst, timeout_erase)); \ + \ + static const struct flash_at25xv021a_config flash_at25xv021a_config_##inst = { \ + .spi = SPI_DT_SPEC_INST_GET(inst, SPI_OP, 0), \ + .jedec_id = DT_INST_PROP(inst, jedec_id), \ + .size = DT_INST_PROP(inst, size), \ + .timeout = K_MSEC(DT_INST_PROP(inst, timeout)), \ + .read_only = DT_INST_PROP(inst, read_only), \ + .ultra_deep_sleep = DT_INST_PROP(inst, ultra_deep_sleep), \ + .parameters = \ + { \ + .write_block_size = DT_INST_PROP(inst, page_size), \ + .erase_value = 0xff, \ + }, \ + WP_GPIO_IF_PROVIDED(inst) PAGES_LAYOUT_IF_ENABLED(inst) \ + PAGE_SIZE_IF_WRITEABLE(inst) TIMEOUT_ERASE_IF_WRITEABLE(inst)}; \ + \ + static struct flash_at25xv021a_data flash_at25xv021a_data_##inst; \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, flash_at25xv021a_pm_action); \ + \ + DEVICE_DT_INST_DEFINE(inst, flash_at25xv021a_init, PM_DEVICE_DT_INST_GET(inst), \ + &flash_at25xv021a_data_##inst, &flash_at25xv021a_config_##inst, \ + POST_KERNEL, CONFIG_FLASH_INIT_PRIORITY, &spi_flash_at25xv021a_api); + +DT_INST_FOREACH_STATUS_OKAY(SPI_FLASH_AT25XV021A_DEFINE) diff --git a/dts/bindings/mtd/atmel,at25xv021a.yaml b/dts/bindings/mtd/atmel,at25xv021a.yaml new file mode 100644 index 0000000000000..a3e7cf45fc8a7 --- /dev/null +++ b/dts/bindings/mtd/atmel,at25xv021a.yaml @@ -0,0 +1,60 @@ +# Copyright (c) 2025 Cirrus Logic, Inc. +# SPDX-License-Identifier: Apache-2.0 + +description: | + AT25XV021A SPI flash variants + + Example datasheet: https://www.renesas.com/en/document/dst/at25xv021a-datasheet + + The AT25XV021A variants add sector protect and unprotect features, which + requires additional steps to program or erase data from the device when compared + to other devices in the AT25 family. + +compatible: "atmel,at25xv021a" + +include: "spi-device.yaml" + +properties: + jedec-id: + type: uint8-array + default: [0x1f, 0x43, 0x01] + description: JEDEC Standard ID + + size: + type: int + required: true + description: Flash capacity in bytes. + + page-size: + type: int + required: true + description: Flash page size in bytes. + + timeout: + type: int + default: 3 + description: | + Timeout for read/write operations in milliseconds. Per the referenced + datasheet, page program operations can take up to 2.5 milliseconds. + + timeout-erase: + type: int + default: 4000 + description: | + Timeout for erase operations (including chip erase) in milliseconds. + Per the referenced datasheet, chip erase can take up to 4 seconds. + + read-only: + type: boolean + description: Set flash device to be read-only. + + ultra-deep-sleep: + type: boolean + description: Go into ultra deep sleep mode instead of deep sleep mode. + + wp-gpios: + type: phandle-array + description: | + The WP pin of AT25XV021A is active low. + If connected directly the MCU pin should be configured + as active low. From 3c7e8c69e9479a30492ab2af14d33522a3432e1e Mon Sep 17 00:00:00 2001 From: Liam Ogletree Date: Mon, 14 Jul 2025 11:19:17 -0500 Subject: [PATCH 2/2] tests: drivers: Add AT25XV021A flash device to test cases Dummy values mirror at25 and spi-nor implementation. Signed-off-by: Liam Ogletree --- tests/drivers/build_all/flash/spi.dtsi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/drivers/build_all/flash/spi.dtsi b/tests/drivers/build_all/flash/spi.dtsi index e2c36f8180cf8..197baa25e460d 100644 --- a/tests/drivers/build_all/flash/spi.dtsi +++ b/tests/drivers/build_all/flash/spi.dtsi @@ -24,3 +24,15 @@ spi-nor@1 { size = <1048576>; jedec-id = [00 11 22]; }; + +at25xv021a@2 { + compatible = "atmel,at25xv021a"; + reg = <0x2>; + status = "okay"; + spi-max-frequency = <5000000>; + jedec-id = [00 11 22]; + size = <262144>; + page-size = <1>; + timeout = <1>; + timeout-erase = <1>; +};