zephyr/drivers/flash/spi_flash_at45.c
Andrzej Głąbek 0b573a1f38 drivers: spi_flash_at45: Fix erasing of first two sectors
Most available AT45 flash chips have their first two sectors shorter
than the consecutive ones. Usually, the first sector is marked as 0a
and has its size equal to eight pages (one block) and the second one
(usually 0b) is the complement to the size of a regular sector.
This commits modifies the driver so that erasing of these first two
sectors is performed correctly. This modified behavior is configurable
with a new DT property so that it is still possible to also use legacy
AT45 chips that do not feature such sector split. Such legacy chips
usually also do not support the chip erase and sector erase commands,
so two more DT properties are introduced to cover that.

Signed-off-by: Andrzej Głąbek <andrzej.glabek@nordicsemi.no>
2023-05-09 12:22:15 +02:00

733 lines
18 KiB
C

/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/drivers/flash.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(spi_flash_at45, CONFIG_FLASH_LOG_LEVEL);
#define DT_DRV_COMPAT atmel_at45
/* AT45 commands used by this driver: */
/* - Continuous Array Read (Low Power Mode) */
#define CMD_READ 0x01
/* - Main Memory Byte/Page Program through Buffer 1 without Built-In Erase */
#define CMD_WRITE 0x02
/* - Read-Modify-Write */
#define CMD_MODIFY 0x58
/* - Manufacturer and Device ID Read */
#define CMD_READ_ID 0x9F
/* - Status Register Read */
#define CMD_READ_STATUS 0xD7
/* - Chip Erase */
#define CMD_CHIP_ERASE { 0xC7, 0x94, 0x80, 0x9A }
/* - Sector Erase */
#define CMD_SECTOR_ERASE 0x7C
/* - Block Erase */
#define CMD_BLOCK_ERASE 0x50
/* - Page Erase */
#define CMD_PAGE_ERASE 0x81
/* - Deep Power-Down */
#define CMD_ENTER_DPD 0xB9
/* - Resume from Deep Power-Down */
#define CMD_EXIT_DPD 0xAB
/* - Ultra-Deep Power-Down */
#define CMD_ENTER_UDPD 0x79
/* - Buffer and Page Size Configuration, "Power of 2" binary page size */
#define CMD_BINARY_PAGE_SIZE { 0x3D, 0x2A, 0x80, 0xA6 }
#define STATUS_REG_LSB_RDY_BUSY_BIT 0x80
#define STATUS_REG_LSB_PAGE_SIZE_BIT 0x01
#define INST_HAS_WP_OR(inst) DT_INST_NODE_HAS_PROP(inst, wp_gpios) ||
#define ANY_INST_HAS_WP_GPIOS DT_INST_FOREACH_STATUS_OKAY(INST_HAS_WP_OR) 0
#define INST_HAS_RESET_OR(inst) DT_INST_NODE_HAS_PROP(inst, reset_gpios) ||
#define ANY_INST_HAS_RESET_GPIOS DT_INST_FOREACH_STATUS_OKAY(INST_HAS_RESET_OR) 0
#define DEF_BUF_SET(_name, _buf_array) \
const struct spi_buf_set _name = { \
.buffers = _buf_array, \
.count = ARRAY_SIZE(_buf_array), \
}
struct spi_flash_at45_data {
struct k_sem lock;
};
struct spi_flash_at45_config {
struct spi_dt_spec bus;
#if ANY_INST_HAS_RESET_GPIOS
const struct gpio_dt_spec *reset;
#endif
#if ANY_INST_HAS_WP_GPIOS
const struct gpio_dt_spec *wp;
#endif
#if defined(CONFIG_FLASH_PAGE_LAYOUT)
struct flash_pages_layout pages_layout;
#endif
uint32_t chip_size;
uint32_t sector_size;
uint32_t sector_0a_size;
uint16_t block_size;
uint16_t page_size;
bool no_chip_erase;
bool no_sector_erase;
uint16_t t_enter_dpd; /* in microseconds */
uint16_t t_exit_dpd; /* in microseconds */
bool use_udpd;
uint8_t jedec_id[3];
};
static const struct flash_parameters flash_at45_parameters = {
.write_block_size = 1,
.erase_value = 0xff,
};
static void acquire(const struct device *dev)
{
struct spi_flash_at45_data *data = dev->data;
k_sem_take(&data->lock, K_FOREVER);
}
static void release(const struct device *dev)
{
struct spi_flash_at45_data *data = dev->data;
k_sem_give(&data->lock);
}
static int check_jedec_id(const struct device *dev)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
uint8_t const *expected_id = cfg->jedec_id;
uint8_t read_id[sizeof(cfg->jedec_id)];
const uint8_t opcode = CMD_READ_ID;
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&opcode,
.len = sizeof(opcode),
}
};
const struct spi_buf rx_buf[] = {
{
.len = sizeof(opcode),
},
{
.buf = read_id,
.len = sizeof(read_id),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
DEF_BUF_SET(rx_buf_set, rx_buf);
err = spi_transceive_dt(&cfg->bus, &tx_buf_set, &rx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
return -EIO;
}
if (memcmp(expected_id, read_id, sizeof(read_id)) != 0) {
LOG_ERR("Wrong JEDEC ID: %02X %02X %02X, "
"expected: %02X %02X %02X",
read_id[0], read_id[1], read_id[2],
expected_id[0], expected_id[1], expected_id[2]);
return -ENODEV;
}
return 0;
}
/*
* Reads 2-byte Status Register:
* - Byte 0 to LSB
* - Byte 1 to MSB
* of the pointed parameter.
*/
static int read_status_register(const struct device *dev, uint16_t *status)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
const uint8_t opcode = CMD_READ_STATUS;
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&opcode,
.len = sizeof(opcode),
}
};
const struct spi_buf rx_buf[] = {
{
.len = sizeof(opcode),
},
{
.buf = status,
.len = sizeof(uint16_t),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
DEF_BUF_SET(rx_buf_set, rx_buf);
err = spi_transceive_dt(&cfg->bus, &tx_buf_set, &rx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
return -EIO;
}
*status = sys_le16_to_cpu(*status);
return 0;
}
static int wait_until_ready(const struct device *dev)
{
int err;
uint16_t status;
do {
err = read_status_register(dev, &status);
} while (err == 0 && !(status & STATUS_REG_LSB_RDY_BUSY_BIT));
return err;
}
static int configure_page_size(const struct device *dev)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
uint16_t status;
uint8_t const conf_binary_page_size[] = CMD_BINARY_PAGE_SIZE;
const struct spi_buf tx_buf[] = {
{
.buf = (void *)conf_binary_page_size,
.len = sizeof(conf_binary_page_size),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
err = read_status_register(dev, &status);
if (err != 0) {
return err;
}
/* If the device is already configured for "power of 2" binary
* page size, there is nothing more to do.
*/
if (status & STATUS_REG_LSB_PAGE_SIZE_BIT) {
return 0;
}
err = spi_write_dt(&cfg->bus, &tx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
} else {
err = wait_until_ready(dev);
}
return (err != 0) ? -EIO : 0;
}
static bool is_valid_request(off_t addr, size_t size, size_t chip_size)
{
return (addr >= 0 && (addr + size) <= chip_size);
}
static int spi_flash_at45_read(const struct device *dev, off_t offset,
void *data, size_t len)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
if (!is_valid_request(offset, len, cfg->chip_size)) {
return -ENODEV;
}
uint8_t const op_and_addr[] = {
CMD_READ,
(offset >> 16) & 0xFF,
(offset >> 8) & 0xFF,
(offset >> 0) & 0xFF,
};
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&op_and_addr,
.len = sizeof(op_and_addr),
}
};
const struct spi_buf rx_buf[] = {
{
.len = sizeof(op_and_addr),
},
{
.buf = data,
.len = len,
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
DEF_BUF_SET(rx_buf_set, rx_buf);
acquire(dev);
err = spi_transceive_dt(&cfg->bus, &tx_buf_set, &rx_buf_set);
release(dev);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
}
return (err != 0) ? -EIO : 0;
}
static int perform_write(const struct device *dev, off_t offset,
const void *data, size_t len)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
uint8_t const op_and_addr[] = {
IS_ENABLED(CONFIG_SPI_FLASH_AT45_USE_READ_MODIFY_WRITE)
? CMD_MODIFY
: CMD_WRITE,
(offset >> 16) & 0xFF,
(offset >> 8) & 0xFF,
(offset >> 0) & 0xFF,
};
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&op_and_addr,
.len = sizeof(op_and_addr),
},
{
.buf = (void *)data,
.len = len,
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
err = spi_write_dt(&cfg->bus, &tx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
} else {
err = wait_until_ready(dev);
}
return (err != 0) ? -EIO : 0;
}
static int spi_flash_at45_write(const struct device *dev, off_t offset,
const void *data, size_t len)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err = 0;
if (!is_valid_request(offset, len, cfg->chip_size)) {
return -ENODEV;
}
acquire(dev);
#if ANY_INST_HAS_WP_GPIOS
if (cfg->wp) {
gpio_pin_set_dt(cfg->wp, 0);
}
#endif
while (len) {
size_t chunk_len = len;
off_t current_page_start =
offset - (offset & (cfg->page_size - 1));
off_t current_page_end = current_page_start + cfg->page_size;
if (chunk_len > (current_page_end - offset)) {
chunk_len = (current_page_end - offset);
}
err = perform_write(dev, offset, data, chunk_len);
if (err != 0) {
break;
}
data = (uint8_t *)data + chunk_len;
offset += chunk_len;
len -= chunk_len;
}
#if ANY_INST_HAS_WP_GPIOS
if (cfg->wp) {
gpio_pin_set_dt(cfg->wp, 1);
}
#endif
release(dev);
return err;
}
static int perform_chip_erase(const struct device *dev)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
uint8_t const chip_erase_cmd[] = CMD_CHIP_ERASE;
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&chip_erase_cmd,
.len = sizeof(chip_erase_cmd),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
err = spi_write_dt(&cfg->bus, &tx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
} else {
err = wait_until_ready(dev);
}
return (err != 0) ? -EIO : 0;
}
static bool is_erase_possible(size_t entity_size,
off_t offset, size_t requested_size)
{
return (requested_size >= entity_size &&
(offset & (entity_size - 1)) == 0);
}
static int perform_erase_op(const struct device *dev, uint8_t opcode,
off_t offset)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err;
uint8_t const op_and_addr[] = {
opcode,
(offset >> 16) & 0xFF,
(offset >> 8) & 0xFF,
(offset >> 0) & 0xFF,
};
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&op_and_addr,
.len = sizeof(op_and_addr),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
err = spi_write_dt(&cfg->bus, &tx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
} else {
err = wait_until_ready(dev);
}
return (err != 0) ? -EIO : 0;
}
static int spi_flash_at45_erase(const struct device *dev, off_t offset,
size_t size)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err = 0;
if (!is_valid_request(offset, size, cfg->chip_size)) {
return -ENODEV;
}
/* Diagnose region errors before starting to erase. */
if (((offset % cfg->page_size) != 0)
|| ((size % cfg->page_size) != 0)) {
return -EINVAL;
}
acquire(dev);
#if ANY_INST_HAS_WP_GPIOS
if (cfg->wp) {
gpio_pin_set_dt(cfg->wp, 0);
}
#endif
if (!cfg->no_chip_erase && size == cfg->chip_size) {
err = perform_chip_erase(dev);
} else {
while (size) {
size_t sector_size = cfg->sector_size;
if (cfg->sector_0a_size) {
if (offset < cfg->sector_0a_size) {
sector_size = cfg->sector_0a_size;
} else if (offset < cfg->sector_size) {
/* Sector 0b. Calculate its size. */
sector_size -= cfg->sector_0a_size;
}
}
if (!cfg->no_sector_erase &&
is_erase_possible(sector_size,
offset, size)) {
err = perform_erase_op(dev, CMD_SECTOR_ERASE,
offset);
offset += sector_size;
size -= sector_size;
} else if (is_erase_possible(cfg->block_size,
offset, size)) {
err = perform_erase_op(dev, CMD_BLOCK_ERASE,
offset);
offset += cfg->block_size;
size -= cfg->block_size;
} else if (is_erase_possible(cfg->page_size,
offset, size)) {
err = perform_erase_op(dev, CMD_PAGE_ERASE,
offset);
offset += cfg->page_size;
size -= cfg->page_size;
} else {
LOG_ERR("Unsupported erase request: "
"size %zu at 0x%lx",
size, (long)offset);
err = -EINVAL;
}
if (err != 0) {
break;
}
}
}
#if ANY_INST_HAS_WP_GPIOS
if (cfg->wp) {
gpio_pin_set_dt(cfg->wp, 1);
}
#endif
release(dev);
return err;
}
#if defined(CONFIG_FLASH_PAGE_LAYOUT)
static void spi_flash_at45_pages_layout(const struct device *dev,
const struct flash_pages_layout **layout,
size_t *layout_size)
{
const struct spi_flash_at45_config *cfg = dev->config;
*layout = &cfg->pages_layout;
*layout_size = 1;
}
#endif /* IS_ENABLED(CONFIG_FLASH_PAGE_LAYOUT) */
static int power_down_op(const struct device *dev, uint8_t opcode,
uint32_t delay)
{
const struct spi_flash_at45_config *cfg = dev->config;
int err = 0;
const struct spi_buf tx_buf[] = {
{
.buf = (void *)&opcode,
.len = sizeof(opcode),
}
};
DEF_BUF_SET(tx_buf_set, tx_buf);
err = spi_write_dt(&cfg->bus, &tx_buf_set);
if (err != 0) {
LOG_ERR("SPI transaction failed with code: %d/%u",
err, __LINE__);
return -EIO;
}
k_busy_wait(delay);
return 0;
}
static int spi_flash_at45_init(const struct device *dev)
{
const struct spi_flash_at45_config *dev_config = dev->config;
int err;
if (!spi_is_ready_dt(&dev_config->bus)) {
LOG_ERR("SPI bus %s not ready", dev_config->bus.bus->name);
return -ENODEV;
}
#if ANY_INST_HAS_RESET_GPIOS
if (dev_config->reset) {
if (!device_is_ready(dev_config->reset->port)) {
LOG_ERR("Reset pin not ready");
return -ENODEV;
}
if (gpio_pin_configure_dt(dev_config->reset, GPIO_OUTPUT_ACTIVE)) {
LOG_ERR("Couldn't configure reset pin");
return -ENODEV;
}
gpio_pin_set_dt(dev_config->reset, 0);
}
#endif
#if ANY_INST_HAS_WP_GPIOS
if (dev_config->wp) {
if (!device_is_ready(dev_config->wp->port)) {
LOG_ERR("Write protect pin not ready");
return -ENODEV;
}
if (gpio_pin_configure_dt(dev_config->wp, GPIO_OUTPUT_ACTIVE)) {
LOG_ERR("Couldn't configure write protect pin");
return -ENODEV;
}
}
#endif
acquire(dev);
/* Just in case the chip was in the Deep (or Ultra-Deep) Power-Down
* mode, issue the command to bring it back to normal operation.
* Exiting from the Ultra-Deep mode requires only that the CS line
* is asserted for a certain time, so issuing the Resume from Deep
* Power-Down command will work in both cases.
*/
power_down_op(dev, CMD_EXIT_DPD, dev_config->t_exit_dpd);
err = check_jedec_id(dev);
if (err == 0) {
err = configure_page_size(dev);
}
release(dev);
return err;
}
#if defined(CONFIG_PM_DEVICE)
static int spi_flash_at45_pm_action(const struct device *dev,
enum pm_device_action action)
{
const struct spi_flash_at45_config *dev_config = dev->config;
switch (action) {
case PM_DEVICE_ACTION_RESUME:
acquire(dev);
power_down_op(dev, CMD_EXIT_DPD, dev_config->t_exit_dpd);
release(dev);
break;
case PM_DEVICE_ACTION_SUSPEND:
acquire(dev);
power_down_op(dev,
dev_config->use_udpd ? CMD_ENTER_UDPD : CMD_ENTER_DPD,
dev_config->t_enter_dpd);
release(dev);
break;
default:
return -ENOTSUP;
}
return 0;
}
#endif /* IS_ENABLED(CONFIG_PM_DEVICE) */
static const struct flash_parameters *
flash_at45_get_parameters(const struct device *dev)
{
ARG_UNUSED(dev);
return &flash_at45_parameters;
}
static const struct flash_driver_api spi_flash_at45_api = {
.read = spi_flash_at45_read,
.write = spi_flash_at45_write,
.erase = spi_flash_at45_erase,
.get_parameters = flash_at45_get_parameters,
#if defined(CONFIG_FLASH_PAGE_LAYOUT)
.page_layout = spi_flash_at45_pages_layout,
#endif
};
#define INST_HAS_RESET_GPIO(idx) \
DT_INST_NODE_HAS_PROP(idx, reset_gpios)
#define INST_RESET_GPIO_SPEC(idx) \
IF_ENABLED(INST_HAS_RESET_GPIO(idx), \
(static const struct gpio_dt_spec reset_##idx = \
GPIO_DT_SPEC_INST_GET(idx, reset_gpios);))
#define INST_HAS_WP_GPIO(idx) \
DT_INST_NODE_HAS_PROP(idx, wp_gpios)
#define INST_WP_GPIO_SPEC(idx) \
IF_ENABLED(INST_HAS_WP_GPIO(idx), \
(static const struct gpio_dt_spec wp_##idx = \
GPIO_DT_SPEC_INST_GET(idx, wp_gpios);))
#define SPI_FLASH_AT45_INST(idx) \
enum { \
INST_##idx##_BYTES = (DT_INST_PROP(idx, size) / 8), \
INST_##idx##_PAGES = (INST_##idx##_BYTES / \
DT_INST_PROP(idx, page_size)), \
}; \
static struct spi_flash_at45_data inst_##idx##_data = { \
.lock = Z_SEM_INITIALIZER(inst_##idx##_data.lock, 1, 1), \
}; \
INST_RESET_GPIO_SPEC(idx) \
INST_WP_GPIO_SPEC(idx) \
static const struct spi_flash_at45_config inst_##idx##_config = { \
.bus = SPI_DT_SPEC_INST_GET( \
idx, SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | \
SPI_WORD_SET(8), 0), \
IF_ENABLED(INST_HAS_RESET_GPIO(idx), \
(.reset = &reset_##idx,)) \
IF_ENABLED(INST_HAS_WP_GPIO(idx), \
(.wp = &wp_##idx,)) \
IF_ENABLED(CONFIG_FLASH_PAGE_LAYOUT, ( \
.pages_layout = { \
.pages_count = INST_##idx##_PAGES, \
.pages_size = DT_INST_PROP(idx, page_size), \
},)) \
.chip_size = INST_##idx##_BYTES, \
.sector_size = DT_INST_PROP(idx, sector_size), \
.sector_0a_size = DT_INST_PROP(idx, sector_0a_pages) \
* DT_INST_PROP(idx, page_size), \
.block_size = DT_INST_PROP(idx, block_size), \
.page_size = DT_INST_PROP(idx, page_size), \
.no_chip_erase = DT_INST_PROP(idx, no_chip_erase), \
.no_sector_erase = DT_INST_PROP(idx, no_sector_erase), \
.t_enter_dpd = DIV_ROUND_UP( \
DT_INST_PROP(idx, enter_dpd_delay), \
NSEC_PER_USEC), \
.t_exit_dpd = DIV_ROUND_UP( \
DT_INST_PROP(idx, exit_dpd_delay), \
NSEC_PER_USEC), \
.use_udpd = DT_INST_PROP(idx, use_udpd), \
.jedec_id = DT_INST_PROP(idx, jedec_id), \
}; \
IF_ENABLED(CONFIG_FLASH_PAGE_LAYOUT, ( \
BUILD_ASSERT( \
(INST_##idx##_PAGES * DT_INST_PROP(idx, page_size)) \
== INST_##idx##_BYTES, \
"Page size specified for instance " #idx " of " \
"atmel,at45 is not compatible with its " \
"total size");)) \
\
PM_DEVICE_DT_INST_DEFINE(idx, spi_flash_at45_pm_action); \
\
DEVICE_DT_INST_DEFINE(idx, \
spi_flash_at45_init, PM_DEVICE_DT_INST_GET(idx), \
&inst_##idx##_data, &inst_##idx##_config, \
POST_KERNEL, CONFIG_SPI_FLASH_AT45_INIT_PRIORITY, \
&spi_flash_at45_api);
DT_INST_FOREACH_STATUS_OKAY(SPI_FLASH_AT45_INST)