#include "spi_nor.h" #include #include /* 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; }