Add embedded storage drivers and extend firmware metadata
This commit is contained in:
472
c/spi-nor/Src/spi_nor.c
Normal file
472
c/spi-nor/Src/spi_nor.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user