364 lines
12 KiB
C
364 lines
12 KiB
C
#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;
|
||
}
|