Files
templates/c/spi-nor/Tests/test_spi_nor.c

364 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}