From 7b187d68ca1080003ea8a57457a787a2cf310800 Mon Sep 17 00:00:00 2001 From: Liam Ogletree Date: Wed, 9 Jul 2025 12:11:16 -0500 Subject: [PATCH] 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. This driver implements flash_erase, but flash_write also implicitly erases. Tested writing to and reading/erasing from an AT25XV021A device across page boundaries with varying lengths (less than and greater than page size). Tested chip erase functions. 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 | 968 +++++++++++++++++++++++++ dts/bindings/mtd/atmel,at25xv021a.yaml | 53 ++ 5 files changed, 1040 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 5de788d80a84d..24268e72fc69c 100644 --- a/drivers/flash/CMakeLists.txt +++ b/drivers/flash/CMakeLists.txt @@ -68,6 +68,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 758a43287a20f..2003b3a3173d6 100644 --- a/drivers/flash/Kconfig +++ b/drivers/flash/Kconfig @@ -166,6 +166,7 @@ config FLASH_INIT_PRIORITY # zephyr-keep-sorted-start 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..5d225748cccb9 --- /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..154ed8ebaf9af --- /dev/null +++ b/drivers/flash/spi_flash_at25xv021a.c @@ -0,0 +1,968 @@ +/* + * 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 GET_PAGE_SIZE(inst) DT_PROP(inst, page_size) +#define DEV_PAGE_SIZE (DT_FOREACH_STATUS_OKAY(atmel_at25xv021a, GET_PAGE_SIZE)) + +#define WP_GPIOS(inst) DT_NODE_HAS_PROP(inst, wp_gpios) || +#define HAS_WP_GPIOS (DT_FOREACH_STATUS_OKAY(atmel_at25xv021a, WP_GPIOS) 0) + +enum flash_at25xv021a_timeout { + DEV_TIMEOUT, + DEV_TIMEOUT_CHIP_ERASE, +}; + +struct flash_at25xv021a_config { + struct spi_dt_spec spi; +#if HAS_WP_GPIOS + struct gpio_dt_spec wp_gpio; +#endif /* HAS_WP_GPIOS */ +#if defined(CONFIG_FLASH_PAGE_LAYOUT) + struct flash_pages_layout pages_layout; +#endif /* defined(CONFIG_FLASH_PAGE_LAYOUT) */ + uint8_t jedec_id[3]; + size_t size; + size_t page_size; + size_t addr_width; + int64_t timeout[2]; + bool read_only; + bool ultra_deep_sleep; +}; + +struct flash_at25xv021a_data { + struct k_mutex lock; +}; + +static const struct flash_parameters flash_at25xv021a_parameters = { + .write_block_size = 1, + .erase_value = 0xff, +}; + +static int flash_at25xv021a_read_status(const struct device *dev, uint8_t *status) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[2] = {DEV_READ_SR, DEV_DUMMY_BYTE}; + uint8_t sr[2]; + 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, + enum flash_at25xv021a_timeout timeout_option) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + int64_t now, timeout; + uint8_t status; + + timeout = config->timeout[timeout_option] + k_uptime_get(); + + for (;;) { + now = k_uptime_get(); + + err = flash_at25xv021a_read_status(dev, &status); + if (err != 0) { + return err; + } + + if (!(status & DEV_SR_BUSY)) { + return 0; + } + + if (now > timeout) { + break; + } + + k_msleep(1); + } + + LOG_ERR("timed out waiting for %s to idle", dev->name); + return -EBUSY; +} + +static int flash_at25xv021a_spi_write(const struct device *dev, const struct spi_dt_spec *spi, + const struct spi_buf_set *tx) +{ + int err; + + err = flash_at25xv021a_wait_for_idle(dev, DEV_TIMEOUT); + if (err != 0) { + return err; + } + + err = spi_write_dt(spi, tx); + if (err != 0) { + LOG_ERR("unable to write to %s\n", dev->name); + } + + return err; +} + +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; + + err = flash_at25xv021a_wait_for_idle(dev, DEV_TIMEOUT); + if (err != 0) { + return err; + } + + err = spi_transceive_dt(spi, tx, rx); + if (err != 0) { + LOG_ERR("unable to read from %s\n", dev->name); + } + + return err; +} + +static int flash_at25xv021a_check_status(const struct device *dev, uint8_t mask) +{ + int err; + uint8_t status; + + err = flash_at25xv021a_wait_for_idle(dev, DEV_TIMEOUT); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_read_status(dev, &status); + if (err != 0) { + return err; + } + + return status & mask; +} + +static int flash_at25xv021a_write_enable(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[1] = {DEV_WRITE_ENABLE}; + 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); + if (err != 0) { + LOG_ERR("unable to enable writes on %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_hardware_lock(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[2] = {DEV_WRITE_SR, DEV_HW_LOCK}; + 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, DEV_TIMEOUT); + if (err != 0) { + return err; + } + +#if HAS_WP_GPIOS + err = gpio_pin_configure_dt(&config->wp_gpio, GPIO_OUTPUT_ACTIVE); + if (err != 0) { + LOG_ERR("unable to set WP GPIO"); + return err; + } +#endif /* HAS_WP_GPIOS */ + + err = !flash_at25xv021a_check_status(dev, DEV_SR_SPRL); + if (err != 0) { + LOG_ERR("unable to lock hardware"); + } + + return err; +} + +static int flash_at25xv021a_hardware_unlock(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[2] = {DEV_WRITE_SR, DEV_HW_UNLOCK}; + 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, DEV_TIMEOUT); + if (err != 0) { + return err; + } + +#if HAS_WP_GPIOS + err = gpio_pin_configure_dt(&config->wp_gpio, GPIO_OUTPUT_INACTIVE); + if (err != 0) { + LOG_ERR("unable to set WP GPIO"); + return err; + } +#endif /* HAS_WP_GPIOS */ + + 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); + if (err != 0) { + LOG_ERR("unable to unlock hardware"); + } + + return err; +} + +static int flash_at25xv021a_global_protection(const struct device *dev, uint8_t protection_cmd) +{ + int err; + uint8_t expected; + 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; + } + + expected = (protection_cmd == DEV_GLOBAL_PROTECT) ? DEV_SR_SWP : 0; + err = !(flash_at25xv021a_check_status(dev, DEV_SR_SWP) == expected); + if (err != 0) { + LOG_ERR("unable to update global protection"); + } + + return err; +} + +static int flash_at25xv021a_software_protection(const struct device *dev, off_t page, + uint8_t protection_cmd) +{ + int err = 0; + 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; + } + + if (protection_cmd == DEV_PROTECT) { + err = !flash_at25xv021a_check_status(dev, DEV_SR_SWP); + } else { + err = (flash_at25xv021a_check_status(dev, DEV_SR_SWP) == DEV_SR_SWP); + } + + if (err != 0) { + LOG_ERR("failed to update software protection for %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_device_info(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[1] = {DEV_READ_DEVICE_INFO}; + uint8_t info[3]; + 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; + + if (len == 0) { + LOG_WRN("attempted to read 0 bytes from %s", dev->name); + return 0; + } + + k_mutex_lock(&data->lock, K_FOREVER); + + err = flash_at25xv021a_read_internal(dev, offset, buf, len); + if (err != 0) { + return err; + } + + k_mutex_unlock(&data->lock); + + return err; +} + +static int flash_at25xv021a_erase_internal(const struct device *dev, off_t addr) +{ + int err; + 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; + } + + err = flash_at25xv021a_check_status(dev, DEV_SR_EPE); + if (err != 0) { + LOG_ERR("unable to erase from %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_write_internal(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + int err; + 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); + if (err != 0) { + LOG_ERR("failed to program %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_write(const struct device *dev, off_t offset, const void *buf, + size_t len) +{ + int err, head = 0; + off_t page_addr, page_start; + size_t write_len; + uint8_t copy[DEV_PAGE_SIZE]; + const struct flash_at25xv021a_config *config = dev->config; + struct flash_at25xv021a_data *data = dev->data; + + if (config->read_only) { + LOG_ERR("attempted to write to read-only device %s", dev->name); + return -EINVAL; + } + + if (len == 0) { + LOG_WRN("attempted to write 0 bytes to %s", dev->name); + return 0; + } + + page_addr = offset / (off_t)config->page_size; + page_start = ROUND_DOWN(offset, config->page_size); + offset -= page_start; + + k_mutex_lock(&data->lock, K_FOREVER); + + while (len != 0) { + write_len = MIN(len, config->page_size - offset); + + if (write_len < config->page_size) { + err = flash_at25xv021a_read_internal(dev, page_start, (void *)copy, + config->page_size); + if (err != 0) { + break; + } + } + + err = flash_at25xv021a_software_protection(dev, page_start, DEV_UNPROTECT); + if (err != 0) { + break; + } + + err = flash_at25xv021a_erase_internal(dev, page_addr); + if (err != 0) { + break; + } + + if (write_len < config->page_size) { + strncpy(©[offset], (const void *)((const uint8_t *)buf + head), + write_len); + + err = flash_at25xv021a_write_internal(dev, page_start, (void *)copy, + config->page_size); + } else { + err = flash_at25xv021a_write_internal( + dev, page_start, (const void *)((const uint8_t *)buf + head), + config->page_size); + } + + if (err != 0) { + break; + } + + err = flash_at25xv021a_software_protection(dev, page_start, DEV_PROTECT); + if (err != 0) { + break; + } + + head += write_len; + len -= write_len; + page_addr += 1; + page_start += config->page_size; + offset = 0; + } + + k_mutex_unlock(&data->lock); + + return err; +} + +static int flash_at25xv021a_chip_erase(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[1] = {DEV_CHIP_ERASE}; + 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, DEV_TIMEOUT_CHIP_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); + if (err != 0) { + LOG_ERR("failed to erase %s", dev->name); + } + + return err; +} + +static int flash_at25xv021a_process_erase(const struct device *dev, off_t page_addr, + off_t page_start, off_t offset, size_t size, + size_t write_len, uint8_t *copy) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + + err = flash_at25xv021a_software_protection(dev, page_start, DEV_UNPROTECT); + if (err != 0) { + return err; + } + + if (write_len < config->page_size) { + err = flash_at25xv021a_read_internal(dev, page_start, (void *)copy, + config->page_size); + if (err != 0) { + return err; + } + } + + err = flash_at25xv021a_erase_internal(dev, page_addr); + if (err != 0) { + return err; + } + + if (write_len < config->page_size) { + if (offset != 0) { + err = flash_at25xv021a_write_internal(dev, page_start, (void *)copy, + offset); + } else { + err = flash_at25xv021a_write_internal(dev, page_start + size, + (void *)©[size], + config->page_size - size); + } + + if (err != 0) { + return err; + } + } + + err = flash_at25xv021a_software_protection(dev, page_start, DEV_PROTECT); + + return err; +} + +static int flash_at25xv021a_erase(const struct device *dev, off_t offset, size_t size) +{ + int err = 0; + off_t page_addr, page_start; + size_t write_len; + uint8_t copy[DEV_PAGE_SIZE]; + const struct flash_at25xv021a_config *config = dev->config; + struct flash_at25xv021a_data *data = dev->data; + + if (config->read_only) { + LOG_ERR("attempted to erase from read-only device %s", dev->name); + return -EINVAL; + } + + if (size == 0) { + LOG_WRN("attempted to erase 0 bytes from %s", dev->name); + return 0; + } + + k_mutex_lock(&data->lock, K_FOREVER); + + if (size >= config->size) { + err = flash_at25xv021a_chip_erase(dev); + k_mutex_unlock(&data->lock); + return err; + } + + page_addr = offset / (off_t)config->page_size; + page_start = ROUND_DOWN(offset, config->page_size); + offset -= page_start; + + while (size != 0) { + write_len = MIN(size, config->page_size - offset); + + err = flash_at25xv021a_process_erase(dev, page_addr, page_start, offset, size, + write_len, copy); + if (err != 0) { + break; + } + + page_addr += 1; + page_start += config->page_size; + size -= write_len; + offset = 0; + } + + k_mutex_unlock(&data->lock); + + return err; +} + +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) +{ + ARG_UNUSED(dev); + + return &flash_at25xv021a_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; + const struct flash_at25xv021a_config *config = dev->config; + uint8_t cmd[1] = {DEV_RESUME}; + 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); + + err = flash_at25xv021a_device_info(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, DEV_TIMEOUT_CHIP_ERASE); + if (err != 0) { + return err; + } + + err = flash_at25xv021a_spi_write(dev, &config->spi, &tx); + + return err; +} + +static int flash_at25xv021a_pm_action(const struct device *dev, enum pm_device_action action) +{ + int err; + + switch (action) { + case PM_DEVICE_ACTION_RESUME: + err = flash_at25xv021a_resume(dev); + break; + case PM_DEVICE_ACTION_SUSPEND: + err = flash_at25xv021a_suspend(dev); + break; + case PM_DEVICE_ACTION_TURN_OFF: + __fallthrough; + case PM_DEVICE_ACTION_TURN_ON: + __fallthrough; + default: + return -ENOTSUP; + } + + return err; +} +#endif /* CONFIG_PM_DEVICE */ + +static int flash_at25xv021a_hardware_init(const struct device *dev) +{ + int err; + + 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_SWP) == DEV_SR_SWP); + if (err != 0) { + LOG_ERR("unable to initialize hardware"); + } + + return err; +} + +static int flash_at25xv021a_init(const struct device *dev) +{ + int err; + const struct flash_at25xv021a_config *config = dev->config; + struct flash_at25xv021a_data *data = dev->data; + + if (!device_is_ready(config->spi.bus)) { + LOG_ERR("spi bus device 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_device_info(dev); + if (err != 0) { + LOG_ERR("unable to verify device information"); + return err; + } + +#if HAS_WP_GPIOS + 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 /* HAS_WP_GPIOS */ + + err = flash_at25xv021a_hardware_init(dev); + if (err != 0) { + return err; + } + + err = k_mutex_init(&data->lock); + if (err != 0) { + LOG_ERR("unable to initialize mutex"); + } + + return err; +} + +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 non-negative") + +#define ASSERT_PAGE_SIZE(pg) \ + BUILD_ASSERT((pg != 0U) && ((pg & (pg - 1)) == 0U), "Page size must be a power of 2") + +#define SPI_OP (SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8)) + +#define WP_GPIOS_IF_PROVIDED(inst) \ + IF_ENABLED(DT_NODE_HAS_PROP(inst, wp_gpios), \ + (.wp_gpio = GPIO_DT_SPEC_GET(inst, wp_gpios),)) + +#define PAGES_LAYOUT_IF_PROVIDED(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)); \ + \ + static const struct flash_at25xv021a_config flash_at25xv021a_config_##inst = { \ + .spi = SPI_DT_SPEC_GET(DT_INST(inst, atmel_at25xv021a), SPI_OP, 0), \ + WP_GPIOS_IF_PROVIDED(DT_INST(inst, atmel_at25xv021a)) \ + PAGES_LAYOUT_IF_PROVIDED(inst) \ + .jedec_id = DT_INST_PROP(inst, jedec_id), \ + .size = DT_INST_PROP(inst, size), \ + .page_size = DT_INST_PROP(inst, page_size), \ + .timeout = DT_INST_PROP(inst, timeout), \ + .ultra_deep_sleep = DT_INST_PROP(inst, ultra_deep_sleep), \ + .read_only = DT_INST_PROP(inst, read_only), \ + }; \ + \ + static struct flash_at25xv021a_data flash_at25xv021a_data_##inst; \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, flash_at25xv021a_pm_action); \ + \ + DEVICE_DT_DEFINE(DT_INST(inst, atmel_at25xv021a), 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..09be7fc61fc45 --- /dev/null +++ b/dts/bindings/mtd/atmel,at25xv021a.yaml @@ -0,0 +1,53 @@ +# 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 + required: true + description: | + JEDEC ID as manufacturer ID (1 byte) and device ID (2 bytes), e.g., + jedec-id = [1f 43 01]; + + size: + type: int + required: true + description: Flash capacity in bits. + + page-size: + type: int + required: true + description: Flash page size in bits. + + timeout: + type: array + default: [10, 4000] + description: Timeouts for read/write and erase operations in milliseconds. + + 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.