Add embedded storage drivers and extend firmware metadata

This commit is contained in:
2026-09-27 01:44:59 +03:00
parent 795a1279b1
commit 2ad29e7ffd
52 changed files with 5801 additions and 3 deletions

472
c/spi-nor/Src/spi_nor.c Normal file
View File

@@ -0,0 +1,472 @@
#include "spi_nor.h"
#include <string.h>
/* Стандартные команды одинаковы для проверенных W25Q и SST25. */
#define SPI_NOR_COMMAND_WRITE_ENABLE 0x06U
#define SPI_NOR_COMMAND_READ_STATUS 0x05U
#define SPI_NOR_COMMAND_READ_DATA 0x03U
#define SPI_NOR_COMMAND_PAGE_PROGRAM 0x02U
#define SPI_NOR_COMMAND_SECTOR_ERASE 0x20U
#define SPI_NOR_COMMAND_READ_JEDEC_ID 0x9FU
#define SPI_NOR_COMMAND_RELEASE_POWER_DOWN 0xABU
#define SPI_NOR_STATUS_BUSY 0x01U
#define SPI_NOR_SECTOR_SIZE 4096UL
#define SPI_NOR_MAX_PAGE_SIZE 256U
#define SPI_NOR_VERIFY_CHUNK_SIZE 32U
#define SPI_NOR_24BIT_CAPACITY_LIMIT 0x01000000UL
/* Преобразует результат физического обмена в ошибку протокольного слоя. */
static spi_nor_status_t map_io_status(spi_nor_io_status_t status)
{
if (status == SPI_NOR_IO_OK) {
return SPI_NOR_OK;
}
if (status == SPI_NOR_IO_TIMEOUT) {
return SPI_NOR_E_TIMEOUT;
}
return SPI_NOR_E_IO;
}
/* Захватывает весь составной вызов, чтобы другой поток не вклинился после WREN. */
static spi_nor_status_t begin_operation(spi_nor_t *device)
{
spi_nor_io_status_t lock_status;
if ((device == NULL) || (device->initialized == 0U)) {
return SPI_NOR_E_ARGUMENT;
}
/* Рекурсивный вызов отклоняется до mutex, чтобы не ждать самого себя. */
if (device->busy != 0U) {
return SPI_NOR_E_BUSY;
}
if (device->io.lock != NULL) {
lock_status = device->io.lock(device->io.context,
device->config.lock_timeout_ms);
if (lock_status != SPI_NOR_IO_OK) {
return (lock_status == SPI_NOR_IO_TIMEOUT) ?
SPI_NOR_E_TIMEOUT : SPI_NOR_E_BUSY;
}
}
if (device->busy != 0U) {
if (device->io.unlock != NULL) {
device->io.unlock(device->io.context);
}
return SPI_NOR_E_BUSY;
}
device->busy = 1U;
return SPI_NOR_OK;
}
/* Всегда освобождает как локальный флаг, так и необязательный mutex платформы. */
static void end_operation(spi_nor_t *device)
{
device->busy = 0U;
if (device->io.unlock != NULL) {
device->io.unlock(device->io.context);
}
}
/* Обмен выполняется только при активном CS, а ошибка не оставляет CS в нуле. */
static spi_nor_status_t transaction(spi_nor_t *device,
const uint8_t *command,
size_t command_size,
const uint8_t *tx_data,
uint8_t *rx_data,
size_t data_size)
{
spi_nor_io_status_t io_status;
device->io.chip_select(device->io.context, 1U);
io_status = device->io.transfer(device->io.context, command, NULL,
command_size,
device->config.io_timeout_ms);
if ((io_status == SPI_NOR_IO_OK) && (data_size != 0U)) {
io_status = device->io.transfer(device->io.context, tx_data, rx_data,
data_size,
device->config.io_timeout_ms);
}
device->io.chip_select(device->io.context, 0U);
return map_io_status(io_status);
}
/* Формирует opcode и 24-битный big-endian адрес независимо от endian CPU. */
static void make_address_command(uint8_t command[4], uint8_t opcode,
uint32_t address)
{
command[0] = opcode;
command[1] = (uint8_t)(address >> 16U);
command[2] = (uint8_t)(address >> 8U);
command[3] = (uint8_t)address;
}
/* Проверяет весь полуоткрытый диапазон без переполнения address + size. */
static spi_nor_status_t validate_range(const spi_nor_t *device,
uint32_t address,
size_t size)
{
if ((device->detected == 0U) || (size == 0U)) {
return SPI_NOR_E_ARGUMENT;
}
if ((address >= device->info.capacity_bytes) ||
(size > (size_t)(device->info.capacity_bytes - address)) ||
(device->info.capacity_bytes > SPI_NOR_24BIT_CAPACITY_LIMIT)) {
return SPI_NOR_E_BOUNDS;
}
return SPI_NOR_OK;
}
/* Читает регистр состояния в отдельной законченной SPI-транзакции. */
static spi_nor_status_t read_status(spi_nor_t *device, uint8_t *status)
{
uint8_t command = SPI_NOR_COMMAND_READ_STATUS;
return transaction(device, &command, 1U, NULL, status, 1U);
}
/* Публичное чтение статуса не допускает пересечения с program/erase/read. */
spi_nor_status_t spi_nor_read_status_register(spi_nor_t *device,
uint8_t *status)
{
spi_nor_status_t result;
if ((device == NULL) || (status == NULL) || (device->detected == 0U)) {
return SPI_NOR_E_ARGUMENT;
}
result = begin_operation(device);
if (result != SPI_NOR_OK) {
return result;
}
result = read_status(device, status);
end_operation(device);
return result;
}
/* Ожидает снятия BUSY с wrap-safe арифметикой tick и конечным тайм-аутом. */
static spi_nor_status_t wait_ready(spi_nor_t *device, uint32_t timeout_ms)
{
uint32_t started_ms = device->io.tick_ms(device->io.context);
uint8_t status = SPI_NOR_STATUS_BUSY;
spi_nor_status_t result;
while ((status & SPI_NOR_STATUS_BUSY) != 0U) {
result = read_status(device, &status);
if (result != SPI_NOR_OK) {
return result;
}
if ((status & SPI_NOR_STATUS_BUSY) == 0U) {
return SPI_NOR_OK;
}
if ((uint32_t)(device->io.tick_ms(device->io.context) - started_ms) >=
timeout_ms) {
return SPI_NOR_E_TIMEOUT;
}
if (device->config.ready_poll_delay_ms != 0U) {
device->io.delay_ms(device->io.context,
device->config.ready_poll_delay_ms);
}
}
return SPI_NOR_OK;
}
/* Перед каждой изменяющей командой устанавливает volatile Write Enable Latch. */
static spi_nor_status_t write_enable(spi_nor_t *device)
{
uint8_t command = SPI_NOR_COMMAND_WRITE_ENABLE;
return transaction(device, &command, 1U, NULL, NULL, 0U);
}
/* Внутреннее чтение не захватывает mutex повторно и используется verify-кодом. */
static spi_nor_status_t raw_read(spi_nor_t *device, uint32_t address,
void *data, size_t size)
{
uint8_t command[4];
make_address_command(command, SPI_NOR_COMMAND_READ_DATA, address);
return transaction(device, command, sizeof(command), NULL, data, size);
}
/* Программирует один page/byte chunk и немедленно проверяет его чтением. */
static spi_nor_status_t program_chunk(spi_nor_t *device,
uint32_t address,
const uint8_t *data,
size_t size)
{
uint8_t command[4];
uint8_t verify[SPI_NOR_MAX_PAGE_SIZE];
spi_nor_status_t result;
result = write_enable(device);
if (result != SPI_NOR_OK) {
return result;
}
make_address_command(command, SPI_NOR_COMMAND_PAGE_PROGRAM, address);
result = transaction(device, command, sizeof(command), data, NULL, size);
if (result != SPI_NOR_OK) {
return result;
}
result = wait_ready(device, device->config.program_timeout_ms);
if (result != SPI_NOR_OK) {
return result;
}
result = raw_read(device, address, verify, size);
if (result != SPI_NOR_OK) {
return result;
}
return (memcmp(verify, data, size) == 0) ?
SPI_NOR_OK : SPI_NOR_E_VERIFY;
}
/* Проверяет каждый байт сектора после erase, а не только первый word. */
static spi_nor_status_t verify_erased_sector(spi_nor_t *device,
uint32_t address)
{
uint8_t verify[SPI_NOR_VERIFY_CHUNK_SIZE];
uint32_t offset;
size_t index;
spi_nor_status_t result;
for (offset = 0U; offset < SPI_NOR_SECTOR_SIZE;
offset += sizeof(verify)) {
result = raw_read(device, address + offset, verify, sizeof(verify));
if (result != SPI_NOR_OK) {
return result;
}
for (index = 0U; index < sizeof(verify); ++index) {
if (verify[index] != 0xFFU) {
return SPI_NOR_E_VERIFY;
}
}
}
return SPI_NOR_OK;
}
/* Сопоставляет только проверенные JEDEC; неизвестную ёмкость не угадывает. */
static spi_nor_status_t identify_device(spi_nor_t *device)
{
const uint8_t manufacturer = device->info.manufacturer_id;
const uint8_t memory_type = device->info.memory_type;
const uint8_t capacity = device->info.capacity_code;
device->info.sector_size = SPI_NOR_SECTOR_SIZE;
if ((manufacturer == 0xEFU) && (memory_type == 0x40U) &&
(capacity == 0x17U)) {
device->info.model = SPI_NOR_MODEL_W25Q64;
device->info.capacity_bytes = 8UL * 1024UL * 1024UL;
device->info.page_size = 256U;
return SPI_NOR_OK;
}
if ((manufacturer == 0xEFU) && (memory_type == 0x40U) &&
(capacity == 0x18U)) {
device->info.model = SPI_NOR_MODEL_W25Q128;
device->info.capacity_bytes = 16UL * 1024UL * 1024UL;
device->info.page_size = 256U;
return SPI_NOR_OK;
}
if ((manufacturer == 0xBFU) && (memory_type == 0x25U) &&
(capacity == 0x41U)) {
device->info.model = SPI_NOR_MODEL_SST25VF016B;
device->info.capacity_bytes = 2UL * 1024UL * 1024UL;
/* Byte Program 0x02 не требует SST AAI sequencing. */
device->info.page_size = 1U;
return SPI_NOR_OK;
}
if (((manufacturer == 0x00U) && (memory_type == 0x00U) &&
(capacity == 0x00U)) ||
((manufacturer == 0xFFU) && (memory_type == 0xFFU) &&
(capacity == 0xFFU))) {
return SPI_NOR_E_NOT_FOUND;
}
return SPI_NOR_E_UNSUPPORTED;
}
/* Публичные defaults сохраняют тайм-ауты исходного HAL-драйвера. */
spi_nor_config_t spi_nor_default_config(void)
{
spi_nor_config_t config;
config.io_timeout_ms = 100U;
config.lock_timeout_ms = 100U;
config.program_timeout_ms = 1000U;
config.erase_timeout_ms = 5000U;
config.ready_poll_delay_ms = 1U;
return config;
}
/* Проверяет обязательные callbacks и сохраняет независимую копию контракта. */
spi_nor_status_t spi_nor_init(spi_nor_t *device, const spi_nor_io_t *io,
const spi_nor_config_t *config)
{
if ((device == NULL) || (io == NULL) || (io->transfer == NULL) ||
(io->chip_select == NULL) || (io->tick_ms == NULL) ||
(io->delay_ms == NULL) ||
((io->lock == NULL) != (io->unlock == NULL))) {
return SPI_NOR_E_ARGUMENT;
}
memset(device, 0, sizeof(*device));
device->io = *io;
device->config = (config != NULL) ? *config : spi_nor_default_config();
if ((device->config.io_timeout_ms == 0U) ||
(device->config.program_timeout_ms == 0U) ||
(device->config.erase_timeout_ms == 0U)) {
memset(device, 0, sizeof(*device));
return SPI_NOR_E_ARGUMENT;
}
device->io.chip_select(device->io.context, 0U);
device->initialized = 1U;
return SPI_NOR_OK;
}
/* Будит устройство и сохраняет raw JEDEC даже при неизвестной модели. */
spi_nor_status_t spi_nor_probe(spi_nor_t *device)
{
uint8_t command = SPI_NOR_COMMAND_RELEASE_POWER_DOWN;
uint8_t jedec[3] = {0U, 0U, 0U};
spi_nor_status_t result;
result = begin_operation(device);
if (result != SPI_NOR_OK) {
return result;
}
device->detected = 0U;
memset(&device->info, 0, sizeof(device->info));
result = transaction(device, &command, 1U, NULL, NULL, 0U);
if (result == SPI_NOR_OK) {
device->io.delay_ms(device->io.context, 1U);
command = SPI_NOR_COMMAND_READ_JEDEC_ID;
result = transaction(device, &command, 1U, NULL, jedec,
sizeof(jedec));
}
if (result == SPI_NOR_OK) {
device->info.manufacturer_id = jedec[0];
device->info.memory_type = jedec[1];
device->info.capacity_code = jedec[2];
result = identify_device(device);
if (result == SPI_NOR_OK) {
device->detected = 1U;
}
}
end_operation(device);
return result;
}
/* Чтение проверяет адрес до активации CS и не использует внутренний RAM-кэш. */
spi_nor_status_t spi_nor_read(spi_nor_t *device, uint32_t address,
void *data, size_t size)
{
spi_nor_status_t result;
if (data == NULL) {
return SPI_NOR_E_ARGUMENT;
}
result = begin_operation(device);
if (result != SPI_NOR_OK) {
return result;
}
result = validate_range(device, address, size);
if (result == SPI_NOR_OK) {
result = raw_read(device, address, data, size);
}
end_operation(device);
return result;
}
/* Запись разбивается по границам страниц; SST получает byte-program chunks. */
spi_nor_status_t spi_nor_program(spi_nor_t *device, uint32_t address,
const void *data, size_t size)
{
const uint8_t *source = data;
size_t remaining = size;
spi_nor_status_t result;
if (data == NULL) {
return SPI_NOR_E_ARGUMENT;
}
result = begin_operation(device);
if (result != SPI_NOR_OK) {
return result;
}
result = validate_range(device, address, size);
while ((result == SPI_NOR_OK) && (remaining != 0U)) {
size_t page_remaining = device->info.page_size -
(address % device->info.page_size);
size_t chunk = (remaining < page_remaining) ?
remaining : page_remaining;
result = program_chunk(device, address, source, chunk);
address += (uint32_t)chunk;
source += chunk;
remaining -= chunk;
}
end_operation(device);
return result;
}
/* Erase принимает только целые выровненные 4-КиБ секторы в пределах памяти. */
spi_nor_status_t spi_nor_erase(spi_nor_t *device, uint32_t address,
size_t size)
{
size_t remaining = size;
spi_nor_status_t result;
result = begin_operation(device);
if (result != SPI_NOR_OK) {
return result;
}
if (((address % SPI_NOR_SECTOR_SIZE) != 0U) ||
((size % SPI_NOR_SECTOR_SIZE) != 0U)) {
result = SPI_NOR_E_ALIGNMENT;
} else {
result = validate_range(device, address, size);
}
while ((result == SPI_NOR_OK) && (remaining != 0U)) {
uint8_t command[4];
result = write_enable(device);
if (result == SPI_NOR_OK) {
make_address_command(command, SPI_NOR_COMMAND_SECTOR_ERASE,
address);
result = transaction(device, command, sizeof(command), NULL,
NULL, 0U);
}
if (result == SPI_NOR_OK) {
result = wait_ready(device, device->config.erase_timeout_ms);
}
if (result == SPI_NOR_OK) {
result = verify_erased_sector(device, address);
}
address += SPI_NOR_SECTOR_SIZE;
remaining -= SPI_NOR_SECTOR_SIZE;
}
end_operation(device);
return result;
}
/* Копия info не позволяет внешнему коду менять геометрию активного контекста. */
spi_nor_status_t spi_nor_get_info(const spi_nor_t *device,
spi_nor_info_t *info)
{
if ((device == NULL) || (info == NULL)) {
return SPI_NOR_E_ARGUMENT;
}
if (device->detected == 0U) {
return SPI_NOR_E_NOT_FOUND;
}
*info = device->info;
return SPI_NOR_OK;
}
/* Raw JEDEC остаётся диагностически доступен после NOT_FOUND/UNSUPPORTED. */
spi_nor_status_t spi_nor_get_last_jedec(const spi_nor_t *device,
uint8_t jedec[3])
{
if ((device == NULL) || (jedec == NULL) ||
(device->initialized == 0U)) {
return SPI_NOR_E_ARGUMENT;
}
jedec[0] = device->info.manufacturer_id;
jedec[1] = device->info.memory_type;
jedec[2] = device->info.capacity_code;
return SPI_NOR_OK;
}