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

View File

@@ -0,0 +1,363 @@
#include "spi_nor.h"
#include <stdio.h>
#include <string.h>
/* Host mock хранит максимальную W25Q128 целиком и не использует STM32 HAL. */
#define MOCK_CAPACITY (16UL * 1024UL * 1024UL)
#define MOCK_SECTOR_SIZE 4096UL
#define MOCK_COMMAND_READ 0x03U
#define MOCK_COMMAND_WRITE 0x02U
#define MOCK_COMMAND_ERASE 0x20U
#define MOCK_COMMAND_STATUS 0x05U
#define MOCK_COMMAND_WREN 0x06U
#define MOCK_COMMAND_JEDEC 0x9FU
/* Состояние fake SPI моделирует команды, CS, ошибки и readback-порчу. */
typedef struct
{
uint8_t memory[MOCK_CAPACITY];
uint8_t jedec[3];
uint8_t opcode;
uint8_t chip_selected;
uint8_t write_enabled;
uint8_t fail_io;
uint8_t busy_forever;
uint8_t corrupt_program;
uint8_t corrupt_erase;
uint32_t address;
uint32_t tick_ms;
uint32_t page_program_count;
uint32_t erase_count;
} mock_spi_t;
/* Счётчики формируют короткий самостоятельный test runner без framework. */
static unsigned int tests_run;
static unsigned int tests_failed;
/* CHECK сохраняет имя строки и позволяет выполнить остальные сценарии. */
#define CHECK(condition) TestCheck((condition), #condition, __LINE__)
/* Регистрирует одно утверждение и печатает только диагностическую ошибку. */
static void TestCheck(int condition, const char *expression, int line)
{
++tests_run;
if (!condition) {
++tests_failed;
(void)printf("FAIL line %d: %s\n", line, expression);
}
}
/* Инициализация задаёт erased-состояние и JEDEC W25Q128 по умолчанию. */
static void MockInit(mock_spi_t *mock)
{
memset(mock, 0, sizeof(*mock));
memset(mock->memory, 0xFF, sizeof(mock->memory));
mock->jedec[0] = 0xEFU;
mock->jedec[1] = 0x40U;
mock->jedec[2] = 0x18U;
}
/* Декодирует три адресных байта команды в физическое 24-битное смещение. */
static uint32_t MockAddress(const uint8_t *tx)
{
return ((uint32_t)tx[1] << 16U) |
((uint32_t)tx[2] << 8U) |
(uint32_t)tx[3];
}
/* Fake transfer исполняет ровно тот callback-контракт, который видит ядро. */
static spi_nor_io_status_t MockTransfer(void *context, const uint8_t *tx,
uint8_t *rx, size_t size,
uint32_t timeout_ms)
{
mock_spi_t *mock = context;
size_t index;
(void)timeout_ms;
if ((mock->fail_io != 0U) || (mock->chip_selected == 0U)) {
return SPI_NOR_IO_ERROR;
}
if (tx != NULL) {
if ((mock->opcode == MOCK_COMMAND_WRITE) && (size != 4U)) {
if (mock->write_enabled == 0U) {
return SPI_NOR_IO_ERROR;
}
for (index = 0U; index < size; ++index) {
mock->memory[mock->address + index] &= tx[index];
}
if ((mock->corrupt_program != 0U) && (size != 0U)) {
mock->memory[mock->address] ^= 0x01U;
}
mock->address += (uint32_t)size;
mock->write_enabled = 0U;
++mock->page_program_count;
return SPI_NOR_IO_OK;
}
mock->opcode = tx[0];
if ((size == 4U) && ((mock->opcode == MOCK_COMMAND_READ) ||
(mock->opcode == MOCK_COMMAND_WRITE) ||
(mock->opcode == MOCK_COMMAND_ERASE))) {
mock->address = MockAddress(tx);
}
if (mock->opcode == MOCK_COMMAND_WREN) {
mock->write_enabled = 1U;
} else if (mock->opcode == MOCK_COMMAND_ERASE) {
if (mock->write_enabled == 0U) {
return SPI_NOR_IO_ERROR;
}
memset(&mock->memory[mock->address], 0xFF, MOCK_SECTOR_SIZE);
if (mock->corrupt_erase != 0U) {
mock->memory[mock->address + 7U] = 0x00U;
}
mock->write_enabled = 0U;
++mock->erase_count;
}
return SPI_NOR_IO_OK;
}
if (rx == NULL) {
return SPI_NOR_IO_ERROR;
}
if (mock->opcode == MOCK_COMMAND_JEDEC) {
memcpy(rx, mock->jedec, size);
} else if (mock->opcode == MOCK_COMMAND_STATUS) {
memset(rx, (mock->busy_forever != 0U) ? 1 : 0, size);
} else if (mock->opcode == MOCK_COMMAND_READ) {
memcpy(rx, &mock->memory[mock->address], size);
mock->address += (uint32_t)size;
} else {
memset(rx, 0xFF, size);
}
return SPI_NOR_IO_OK;
}
/* Mock CS фиксирует активность и сбрасывает opcode на границе транзакции. */
static void MockChipSelect(void *context, uint8_t active)
{
mock_spi_t *mock = context;
mock->chip_selected = active;
if (active != 0U) {
mock->opcode = 0U;
}
}
/* Mock tick управляется delay callback и делает timeout детерминированным. */
static uint32_t MockTick(void *context)
{
mock_spi_t *mock = context;
return mock->tick_ms;
}
/* Mock delay не ждёт реальное время, а продвигает виртуальные миллисекунды. */
static void MockDelay(void *context, uint32_t delay_ms)
{
mock_spi_t *mock = context;
mock->tick_ms += delay_ms;
}
/* Создаёт IO contract без HAL, GPIO-регистров и глобальных handles. */
static spi_nor_io_t MockIo(mock_spi_t *mock)
{
spi_nor_io_t io;
memset(&io, 0, sizeof(io));
io.context = mock;
io.transfer = MockTransfer;
io.chip_select = MockChipSelect;
io.tick_ms = MockTick;
io.delay_ms = MockDelay;
return io;
}
/* Успешный probe должен определить каждую заявленную модель и геометрию. */
static void TestSupportedJedec(void)
{
const uint8_t ids[3][3] = {
{0xEFU, 0x40U, 0x17U},
{0xEFU, 0x40U, 0x18U},
{0xBFU, 0x25U, 0x41U}
};
const uint32_t capacities[3] = {
8UL * 1024UL * 1024UL,
16UL * 1024UL * 1024UL,
2UL * 1024UL * 1024UL
};
unsigned int index;
for (index = 0U; index < 3U; ++index) {
static mock_spi_t mock;
spi_nor_t device;
spi_nor_info_t info;
spi_nor_io_t io;
MockInit(&mock);
memcpy(mock.jedec, ids[index], sizeof(mock.jedec));
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
CHECK(spi_nor_get_info(&device, &info) == SPI_NOR_OK);
CHECK(info.capacity_bytes == capacities[index]);
CHECK(mock.chip_selected == 0U);
}
}
/* Unknown и пустая шина должны различаться, сохраняя raw JEDEC. */
static void TestProbeFailures(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
uint8_t raw[3];
MockInit(&mock);
mock.jedec[0] = 0x12U;
mock.jedec[1] = 0x34U;
mock.jedec[2] = 0x56U;
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_E_UNSUPPORTED);
CHECK(spi_nor_get_last_jedec(&device, raw) == SPI_NOR_OK);
CHECK(memcmp(raw, mock.jedec, sizeof(raw)) == 0);
memset(mock.jedec, 0xFF, sizeof(mock.jedec));
CHECK(spi_nor_probe(&device) == SPI_NOR_E_NOT_FOUND);
}
/* Program через границу страницы обязан создать два chunks и точный readback. */
static void TestProgramAndRead(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
uint8_t source[20];
uint8_t result[20];
size_t index;
MockInit(&mock);
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
for (index = 0U; index < sizeof(source); ++index) {
source[index] = (uint8_t)(index + 1U);
}
CHECK(spi_nor_program(&device, 250U, source, sizeof(source)) == SPI_NOR_OK);
CHECK(mock.page_program_count == 2U);
memset(result, 0, sizeof(result));
CHECK(spi_nor_read(&device, 250U, result, sizeof(result)) == SPI_NOR_OK);
CHECK(memcmp(source, result, sizeof(source)) == 0);
CHECK(spi_nor_read(&device, MOCK_CAPACITY - 2U, result, 4U) ==
SPI_NOR_E_BOUNDS);
}
/* SST25 использует безопасную последовательность из отдельных Byte Program. */
static void TestSstByteProgram(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
const uint8_t source[3] = {0x11U, 0x22U, 0x33U};
MockInit(&mock);
mock.jedec[0] = 0xBFU;
mock.jedec[1] = 0x25U;
mock.jedec[2] = 0x41U;
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
CHECK(spi_nor_program(&device, 0U, source, sizeof(source)) == SPI_NOR_OK);
CHECK(mock.page_program_count == 3U);
}
/* Readback-порча program должна возвращаться отдельной verify-ошибкой. */
static void TestProgramVerifyFailure(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
const uint8_t source[2] = {0x12U, 0x34U};
MockInit(&mock);
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
mock.corrupt_program = 1U;
CHECK(spi_nor_program(&device, 0U, source, sizeof(source)) ==
SPI_NOR_E_VERIFY);
CHECK(mock.chip_selected == 0U);
}
/* Erase проверяет alignment, весь сектор и readback-порчу. */
static void TestErase(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
MockInit(&mock);
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
mock.memory[9U] = 0x00U;
CHECK(spi_nor_erase(&device, 1U, MOCK_SECTOR_SIZE) ==
SPI_NOR_E_ALIGNMENT);
CHECK(spi_nor_erase(&device, 0U, MOCK_SECTOR_SIZE) == SPI_NOR_OK);
CHECK(mock.memory[9U] == 0xFFU);
mock.corrupt_erase = 1U;
CHECK(spi_nor_erase(&device, 0U, MOCK_SECTOR_SIZE) == SPI_NOR_E_VERIFY);
}
/* Вечный BUSY использует виртуальный tick и заканчивается timeout, не зависая. */
static void TestTimeout(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
spi_nor_config_t config = spi_nor_default_config();
const uint8_t value = 0x55U;
MockInit(&mock);
io = MockIo(&mock);
config.program_timeout_ms = 3U;
CHECK(spi_nor_init(&device, &io, &config) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
mock.busy_forever = 1U;
CHECK(spi_nor_program(&device, 0U, &value, 1U) == SPI_NOR_E_TIMEOUT);
CHECK(mock.tick_ms >= 3U);
}
/* Ошибка обмена обязана поднять CS и не оставлять контекст permanently busy. */
static void TestIoFailureReleasesState(void)
{
static mock_spi_t mock;
spi_nor_t device;
spi_nor_io_t io;
uint8_t value;
MockInit(&mock);
io = MockIo(&mock);
CHECK(spi_nor_init(&device, &io, NULL) == SPI_NOR_OK);
CHECK(spi_nor_probe(&device) == SPI_NOR_OK);
mock.fail_io = 1U;
CHECK(spi_nor_read(&device, 0U, &value, 1U) == SPI_NOR_E_IO);
CHECK(mock.chip_selected == 0U);
mock.fail_io = 0U;
CHECK(spi_nor_read(&device, 0U, &value, 1U) == SPI_NOR_OK);
}
/* Main выполняет все host-сценарии и возвращает ненулевой код при сбое. */
int main(void)
{
TestSupportedJedec();
TestProbeFailures();
TestProgramAndRead();
TestSstByteProgram();
TestProgramVerifyFailure();
TestErase();
TestTimeout();
TestIoFailureReleasesState();
(void)printf("SPI NOR host mock: %u checks, %u failures\n",
tests_run, tests_failed);
return (tests_failed == 0U) ? 0 : 1;
}