You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
10 KiB
459 lines
10 KiB
/* |
|
* Copyright (c) 2024 STMicroelectronics |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#define DT_DRV_COMPAT st_stm32wb0_flash_controller |
|
|
|
#include <zephyr/init.h> |
|
#include <zephyr/kernel.h> |
|
#include <zephyr/device.h> |
|
#include <zephyr/drivers/flash.h> |
|
#include <zephyr/sys/byteorder.h> |
|
#include <zephyr/sys/math_extras.h> |
|
|
|
/* <soc.h> also brings "stm32wb0x_hal_flash.h" |
|
* and "system_stm32wb0x.h", which provide macros |
|
* used by the driver, such as FLASH_PAGE_SIZE or |
|
* _MEMORY_FLASH_SIZE_ respectively. |
|
*/ |
|
#include <soc.h> |
|
#include <stm32_ll_bus.h> |
|
#include <stm32_ll_rcc.h> |
|
#include <stm32_ll_system.h> |
|
|
|
#include <zephyr/logging/log.h> |
|
LOG_MODULE_REGISTER(flash_stm32wb0x, CONFIG_FLASH_LOG_LEVEL); |
|
|
|
/** |
|
* Driver private definitions & assertions |
|
*/ |
|
#define SYSTEM_FLASH_SIZE _MEMORY_FLASH_SIZE_ |
|
#define PAGES_IN_FLASH (SYSTEM_FLASH_SIZE / FLASH_PAGE_SIZE) |
|
|
|
#define WRITE_BLOCK_SIZE \ |
|
DT_PROP(DT_INST(0, soc_nv_flash), write_block_size) |
|
|
|
/* Size of flash words, in bytes (equal to write block size) */ |
|
#define WORD_SIZE WRITE_BLOCK_SIZE |
|
|
|
#define ERASE_BLOCK_SIZE \ |
|
DT_PROP(DT_INST(0, soc_nv_flash), erase_block_size) |
|
|
|
/** |
|
* Driver private structures |
|
*/ |
|
struct flash_wb0x_data { |
|
/** Used to serialize write/erase operations */ |
|
struct k_sem write_lock; |
|
|
|
/** Flash size, in bytes */ |
|
size_t flash_size; |
|
}; |
|
|
|
/** |
|
* Driver private utility functions |
|
*/ |
|
static inline uint32_t read_mem_u32(const uint32_t *ptr) |
|
{ |
|
/** |
|
* Fetch word using sys_get_le32, which performs byte-sized |
|
* reads instead of word-sized. This is important as ptr may |
|
* be unaligned. We also want to use le32 because the data is |
|
* stored in little-endian inside the flash. |
|
*/ |
|
return sys_get_le32((const uint8_t *)ptr); |
|
} |
|
|
|
static inline size_t get_flash_size_in_bytes(void) |
|
{ |
|
/* FLASH.SIZE contains the highest flash address supported |
|
* on this MCU, which is also the number of words in flash |
|
* minus one. |
|
*/ |
|
const uint32_t words_in_flash = |
|
READ_BIT(FLASH->SIZE, FLASH_FLASH_SIZE_FLASH_SIZE) + 1; |
|
|
|
return words_in_flash * WORD_SIZE; |
|
} |
|
|
|
/** |
|
* @brief Returns the associated error to IRQ flags. |
|
* |
|
* @returns a negative error value |
|
*/ |
|
static int error_from_irq_flags(uint32_t flags) |
|
{ |
|
/** |
|
* Only two errors are expected: |
|
* - illegal command |
|
* - command error |
|
*/ |
|
if (flags & FLASH_FLAG_ILLCMD) { |
|
return -EINVAL; |
|
} |
|
|
|
if (flags & FLASH_FLAG_CMDERR) { |
|
return -EIO; |
|
} |
|
|
|
/* |
|
* Unexpected error flag -> "out of domain" |
|
* In practice, this should never be reached. |
|
*/ |
|
return -EDOM; |
|
} |
|
|
|
static bool is_valid_flash_range(const struct device *dev, |
|
off_t offset, uint32_t len) |
|
{ |
|
const struct flash_wb0x_data *data = dev->data; |
|
uint32_t offset_plus_len; |
|
|
|
/* (offset + len) must not overflow */ |
|
return !u32_add_overflow(offset, len, &offset_plus_len) |
|
/* offset must be a valid offset in flash */ |
|
&& IN_RANGE(offset, 0, data->flash_size - 1) |
|
/* (offset + len) must be in [0; flash size] |
|
* because it is equal to the last accessed |
|
* byte in flash plus one (an access of `len` |
|
* bytes starting at `offset` touches bytes |
|
* `offset` to `offset + len` EXCLUDED) |
|
*/ |
|
&& IN_RANGE(offset_plus_len, 0, data->flash_size); |
|
} |
|
|
|
static bool is_writeable_flash_range(const struct device *dev, |
|
off_t offset, uint32_t len) |
|
{ |
|
if ((offset % WRITE_BLOCK_SIZE) != 0 |
|
|| (len % WRITE_BLOCK_SIZE) != 0) { |
|
return false; |
|
} |
|
|
|
return is_valid_flash_range(dev, offset, len); |
|
} |
|
|
|
static bool is_erasable_flash_range(const struct device *dev, |
|
off_t offset, uint32_t len) |
|
{ |
|
if ((offset % ERASE_BLOCK_SIZE) != 0 |
|
|| (len % ERASE_BLOCK_SIZE) != 0) { |
|
return false; |
|
} |
|
|
|
return is_valid_flash_range(dev, offset, len); |
|
} |
|
|
|
/** |
|
* Driver private functions |
|
*/ |
|
|
|
static uint32_t poll_flash_controller(void) |
|
{ |
|
uint32_t flags; |
|
|
|
/* Poll until an interrupt flag is raised */ |
|
do { |
|
flags = FLASH->IRQRAW; |
|
} while (flags == 0); |
|
|
|
/* Acknowledge the flag(s) we have seen */ |
|
FLASH->IRQRAW = flags; |
|
|
|
return flags; |
|
} |
|
|
|
static int execute_flash_command(uint8_t cmd) |
|
{ |
|
uint32_t irq_flags; |
|
|
|
/* Clear all pending interrupt bits */ |
|
FLASH->IRQRAW = FLASH->IRQRAW; |
|
|
|
/* Start command */ |
|
FLASH->COMMAND = cmd; |
|
|
|
/* Wait for CMDSTART */ |
|
irq_flags = poll_flash_controller(); |
|
|
|
/* If command didn't start, an error occurred */ |
|
if (!(irq_flags & FLASH_IT_CMDSTART)) { |
|
return error_from_irq_flags(irq_flags); |
|
} |
|
|
|
/** |
|
* Both CMDSTART and CMDDONE may be set if the command was |
|
* executed fast enough. In this case, we're already done. |
|
* Otherwise, we need to poll again until CMDDONE/error occurs. |
|
*/ |
|
if (!(irq_flags & FLASH_IT_CMDDONE)) { |
|
irq_flags = poll_flash_controller(); |
|
} |
|
|
|
if (!(irq_flags & FLASH_IT_CMDDONE)) { |
|
return error_from_irq_flags(irq_flags); |
|
} else { |
|
return 0; |
|
} |
|
} |
|
|
|
int erase_page_range(uint32_t start_page, uint32_t page_count) |
|
{ |
|
int res = 0; |
|
|
|
__ASSERT_NO_MSG(start_page < PAGES_IN_FLASH); |
|
__ASSERT_NO_MSG((start_page + page_count - 1) < PAGES_IN_FLASH); |
|
|
|
for (uint32_t i = start_page; |
|
i < (start_page + page_count); |
|
i++) { |
|
/* ADDRESS[16:9] = XADR[10:3] (address of page to erase) |
|
* ADDRESS[8:0] = 0 (row & word address, must be 0) |
|
*/ |
|
FLASH->ADDRESS = (i << 9); |
|
|
|
res = execute_flash_command(FLASH_CMD_ERASE_PAGES); |
|
if (res < 0) { |
|
break; |
|
} |
|
} |
|
|
|
return res; |
|
} |
|
|
|
int write_word_range(const void *buf, uint32_t start_word, uint32_t num_words) |
|
{ |
|
/* Special value to load in DATAx registers to skip |
|
* writing corresponding word with BURSTWRITE command. |
|
*/ |
|
const uint32_t BURST_IGNORE_VALUE = 0xFFFFFFFF; |
|
const size_t WORDS_IN_BURST = 4; |
|
uint32_t dst_addr = start_word; |
|
uint32_t remaining = num_words; |
|
/** |
|
* Note that @p buf may not be aligned to 32-bit boundary. |
|
* However, declaring src_ptr as uint32_t* makes the address |
|
* increment by 4 every time we do src_ptr++, which makes it |
|
* behave like the other counters in this function. |
|
*/ |
|
const uint32_t *src_ptr = buf; |
|
int res = 0; |
|
|
|
/** |
|
* Write to flash is performed as a 3 step process: |
|
* - write single words using WRITE commands until the write |
|
* write address is aligned to flash quadword boundary |
|
* |
|
* - after write address is aligned to quadword, we can use |
|
* the BURSTWRITE commands to write 4 words at a time |
|
* |
|
* - once less than 4 words remain to write, a last BURSTWRITE |
|
* is used with unneeded DATAx registers filled with 0xFFFFFFFF |
|
* (this makes BURSTWRITE ignore write to these addresses) |
|
*/ |
|
|
|
/* (1) Align to quadword boundary with WRITE commands */ |
|
while (remaining > 0 && (dst_addr % WORDS_IN_BURST) != 0) { |
|
FLASH->ADDRESS = dst_addr; |
|
FLASH->DATA0 = read_mem_u32(src_ptr); |
|
|
|
res = execute_flash_command(FLASH_CMD_WRITE); |
|
if (res < 0) { |
|
return res; |
|
} |
|
|
|
src_ptr++; |
|
dst_addr++; |
|
remaining--; |
|
} |
|
|
|
/* (2) Write bursts of quadwords */ |
|
while (remaining >= WORDS_IN_BURST) { |
|
__ASSERT_NO_MSG((dst_addr % WORDS_IN_BURST) == 0); |
|
|
|
FLASH->ADDRESS = dst_addr; |
|
FLASH->DATA0 = read_mem_u32(src_ptr + 0); |
|
FLASH->DATA1 = read_mem_u32(src_ptr + 1); |
|
FLASH->DATA2 = read_mem_u32(src_ptr + 2); |
|
FLASH->DATA3 = read_mem_u32(src_ptr + 3); |
|
|
|
res = execute_flash_command(FLASH_CMD_BURSTWRITE); |
|
if (res < 0) { |
|
return res; |
|
} |
|
|
|
src_ptr += WORDS_IN_BURST; |
|
dst_addr += WORDS_IN_BURST; |
|
remaining -= WORDS_IN_BURST; |
|
} |
|
|
|
/* (3) Write trailing (between 1 and 3 words) */ |
|
if (remaining > 0) { |
|
__ASSERT_NO_MSG(remaining < WORDS_IN_BURST); |
|
__ASSERT_NO_MSG((dst_addr % WORDS_IN_BURST) == 0); |
|
|
|
FLASH->ADDRESS = dst_addr; |
|
FLASH->DATA0 = read_mem_u32(src_ptr + 0); |
|
|
|
FLASH->DATA1 = (remaining >= 2) |
|
? read_mem_u32(src_ptr + 1) |
|
: BURST_IGNORE_VALUE; |
|
|
|
FLASH->DATA2 = (remaining == 3) |
|
? read_mem_u32(src_ptr + 2) |
|
: BURST_IGNORE_VALUE; |
|
|
|
FLASH->DATA3 = BURST_IGNORE_VALUE; |
|
|
|
remaining = 0; |
|
|
|
res = execute_flash_command(FLASH_CMD_BURSTWRITE); |
|
} |
|
|
|
return res; |
|
} |
|
|
|
/** |
|
* Driver subsystem API implementation |
|
*/ |
|
int flash_wb0x_read(const struct device *dev, off_t offset, |
|
void *buffer, size_t len) |
|
{ |
|
if (!len) { |
|
return 0; |
|
} |
|
|
|
if (!is_valid_flash_range(dev, offset, len)) { |
|
return -EINVAL; |
|
} |
|
|
|
const uint8_t *flash_base = ((void *)DT_REG_ADDR(DT_INST(0, st_stm32_nv_flash))); |
|
|
|
memcpy(buffer, flash_base + (uint32_t)offset, len); |
|
|
|
return 0; |
|
} |
|
|
|
int flash_wb0x_write(const struct device *dev, off_t offset, |
|
const void *buffer, size_t len) |
|
{ |
|
struct flash_wb0x_data *data = dev->data; |
|
int res; |
|
|
|
if (!len) { |
|
return 0; |
|
} |
|
|
|
if (!is_writeable_flash_range(dev, offset, len)) { |
|
return -EINVAL; |
|
} |
|
|
|
/* Acquire driver lock */ |
|
res = k_sem_take(&data->write_lock, K_NO_WAIT); |
|
if (res < 0) { |
|
return res; |
|
} |
|
|
|
const uint32_t start_word = (uint32_t)offset / WORD_SIZE; |
|
const uint32_t num_words = len / WORD_SIZE; |
|
|
|
res = write_word_range(buffer, start_word, num_words); |
|
|
|
/* Release driver lock */ |
|
k_sem_give(&data->write_lock); |
|
|
|
return res; |
|
} |
|
|
|
int flash_wb0x_erase(const struct device *dev, off_t offset, size_t size) |
|
{ |
|
struct flash_wb0x_data *data = dev->data; |
|
int res; |
|
|
|
if (!size) { |
|
return 0; |
|
} |
|
|
|
if (!is_erasable_flash_range(dev, offset, size)) { |
|
return -EINVAL; |
|
} |
|
|
|
/* Acquire driver lock */ |
|
res = k_sem_take(&data->write_lock, K_NO_WAIT); |
|
if (res < 0) { |
|
return res; |
|
} |
|
|
|
const uint32_t start_page = (uint32_t)offset / ERASE_BLOCK_SIZE; |
|
const uint32_t page_count = size / ERASE_BLOCK_SIZE; |
|
|
|
res = erase_page_range(start_page, page_count); |
|
|
|
/* Release driver lock */ |
|
k_sem_give(&data->write_lock); |
|
|
|
return res; |
|
} |
|
|
|
const struct flash_parameters *flash_wb0x_get_parameters( |
|
const struct device *dev) |
|
{ |
|
static const struct flash_parameters fp = { |
|
.write_block_size = WRITE_BLOCK_SIZE, |
|
.erase_value = 0xff, |
|
}; |
|
|
|
return &fp; |
|
} |
|
|
|
#if defined(CONFIG_FLASH_PAGE_LAYOUT) |
|
void flash_wb0x_pages_layout(const struct device *dev, |
|
const struct flash_pages_layout **layout, |
|
size_t *layout_size) |
|
{ |
|
/** |
|
* STM32WB0 flash: single bank, 2KiB pages |
|
* (the number of pages depends on MCU) |
|
*/ |
|
static const struct flash_pages_layout fpl[] = {{ |
|
.pages_count = PAGES_IN_FLASH, |
|
.pages_size = FLASH_PAGE_SIZE |
|
}}; |
|
|
|
*layout = fpl; |
|
*layout_size = ARRAY_SIZE(fpl); |
|
} |
|
#endif /* CONFIG_FLASH_PAGE_LAYOUT */ |
|
|
|
static DEVICE_API(flash, flash_wb0x_api) = { |
|
.erase = flash_wb0x_erase, |
|
.write = flash_wb0x_write, |
|
.read = flash_wb0x_read, |
|
.get_parameters = flash_wb0x_get_parameters, |
|
#ifdef CONFIG_FLASH_PAGE_LAYOUT |
|
.page_layout = flash_wb0x_pages_layout, |
|
#endif |
|
/* extended operations not supported */ |
|
}; |
|
|
|
int stm32wb0x_flash_init(const struct device *dev) |
|
{ |
|
struct flash_wb0x_data *data = dev->data; |
|
|
|
k_sem_init(&data->write_lock, 1, 1); |
|
|
|
data->flash_size = get_flash_size_in_bytes(); |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
* Driver device instantiation |
|
*/ |
|
static struct flash_wb0x_data wb0x_flash_drv_data; |
|
|
|
DEVICE_DT_INST_DEFINE(0, stm32wb0x_flash_init, NULL, |
|
&wb0x_flash_drv_data, NULL, POST_KERNEL, |
|
CONFIG_FLASH_INIT_PRIORITY, &flash_wb0x_api);
|
|
|