460 lines
10 KiB
C
460 lines
10 KiB
C
/*
|
|
* 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 const struct flash_driver_api 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);
|