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

10
c/spi-nor/CMakeLists.txt Normal file
View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.13)
project(spi_nor C)
add_library(spi_nor STATIC Src/spi_nor.c)
target_include_directories(spi_nor PUBLIC Inc)
set_target_properties(spi_nor PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED YES)
enable_testing()
add_executable(test_spi_nor Tests/test_spi_nor.c)
target_link_libraries(test_spi_nor PRIVATE spi_nor)
add_test(NAME test_spi_nor COMMAND test_spi_nor)

View File

@@ -0,0 +1,72 @@
#ifndef SPI_NOR_COMMAND_SERVICE_H
#define SPI_NOR_COMMAND_SERVICE_H
/*
* Переносимый read-only сервис коротких сервисных команд SPI NOR.
*
* Модуль не знает о HAL, Modbus, HTTP и GUI. Он принимает уже распознанный
* spi_nor_t, применяет собственный allowlist и выполняет ровно одну законченную
* транзакцию. Изменяющие команды намеренно отсутствуют в публичном API.
*/
#include "spi_nor.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Четырёх байт достаточно для opcode и 24-битного адреса READ 0x03. */
#define SPI_NOR_COMMAND_SERVICE_MAX_TX_BYTES 4U
#define SPI_NOR_COMMAND_SERVICE_MAX_RX_BYTES 64U
typedef enum
{
SPI_NOR_COMMAND_SERVICE_IDLE = 0,
SPI_NOR_COMMAND_SERVICE_BUSY,
SPI_NOR_COMMAND_SERVICE_OK,
SPI_NOR_COMMAND_SERVICE_INVALID,
SPI_NOR_COMMAND_SERVICE_BLOCKED,
SPI_NOR_COMMAND_SERVICE_UNAVAILABLE,
SPI_NOR_COMMAND_SERVICE_IO_ERROR,
SPI_NOR_COMMAND_SERVICE_TIMEOUT
} spi_nor_command_service_status_t;
typedef struct
{
uint16_t sequence;
uint8_t tx_length;
uint8_t rx_length;
uint8_t tx[SPI_NOR_COMMAND_SERVICE_MAX_TX_BYTES];
} spi_nor_command_service_request_t;
typedef struct
{
spi_nor_command_service_status_t status;
uint16_t sequence;
uint8_t tx_length;
uint8_t rx_length;
uint8_t tx_echo[SPI_NOR_COMMAND_SERVICE_MAX_TX_BYTES];
uint8_t rx[SPI_NOR_COMMAND_SERVICE_MAX_RX_BYTES];
} spi_nor_command_service_response_t;
/*
* Allowlist общий для W25Q64/W25Q128 и SST25VF016B:
* 0x9F JEDEC ID, 0x05 status register 1, 0x03 обычное чтение данных.
*/
spi_nor_command_service_status_t spi_nor_command_service_validate(
const spi_nor_t *device,
const spi_nor_command_service_request_t *request);
/* Process очищает старый ответ и выполняет не более одной SPI-транзакции. */
spi_nor_command_service_status_t spi_nor_command_service_process(
spi_nor_t *device,
const spi_nor_command_service_request_t *request,
spi_nor_command_service_response_t *response);
#ifdef __cplusplus
}
#endif
#endif /* SPI_NOR_COMMAND_SERVICE_H */

View File

@@ -0,0 +1,73 @@
#ifndef SPI_NOR_READ_SERVICE_H
#define SPI_NOR_READ_SERVICE_H
/*
* Ограниченный read-only сервис диагностики SPI NOR.
*
* Сервис отделён от HAL, Modbus и приложения. Владелец передаёт только ёмкость
* и callback чтения, поэтому portable-ядро SpiNor не получает зависимостей GUI.
*/
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Один маленький пакет ограничивает длительность SPI и Modbus main loop. */
#define SPI_NOR_READ_SERVICE_MAX_BYTES 64U
typedef enum
{
SPI_NOR_READ_SERVICE_IDLE = 0,
SPI_NOR_READ_SERVICE_BUSY,
SPI_NOR_READ_SERVICE_OK,
SPI_NOR_READ_SERVICE_INVALID,
SPI_NOR_READ_SERVICE_UNAVAILABLE,
SPI_NOR_READ_SERVICE_IO_ERROR
} spi_nor_read_service_status_t;
/* Callback адаптера не раскрывает драйверу контекст конкретной платформы. */
typedef spi_nor_read_service_status_t (*spi_nor_read_service_read_fn)(
void *context, uint32_t address, void *data, size_t size);
typedef struct
{
void *context;
spi_nor_read_service_read_fn read;
uint32_t capacity_bytes;
} spi_nor_read_service_t;
typedef struct
{
uint32_t address;
uint16_t length;
uint16_t sequence;
} spi_nor_read_service_request_t;
typedef struct
{
spi_nor_read_service_status_t status;
uint32_t address;
uint16_t length;
uint16_t sequence;
uint8_t data[SPI_NOR_READ_SERVICE_MAX_BYTES];
} spi_nor_read_service_response_t;
/* Init проверяет callback и ёмкость, но сам не обращается к SPI. */
spi_nor_read_service_status_t spi_nor_read_service_init(
spi_nor_read_service_t *service, void *context,
spi_nor_read_service_read_fn read, uint32_t capacity_bytes);
/* Process всегда очищает старые данные до проверки нового запроса. */
spi_nor_read_service_status_t spi_nor_read_service_process(
const spi_nor_read_service_t *service,
const spi_nor_read_service_request_t *request,
spi_nor_read_service_response_t *response);
#ifdef __cplusplus
}
#endif
#endif /* SPI_NOR_READ_SERVICE_H */

View File

@@ -0,0 +1,165 @@
#include "spi_nor_command_service.h"
#include <string.h>
/* Только эти три opcode документированы одинаково для всех поддержанных NOR. */
#define SPI_NOR_DIAG_READ_DATA 0x03U
#define SPI_NOR_DIAG_READ_STATUS_1 0x05U
#define SPI_NOR_DIAG_READ_JEDEC_ID 0x9FU
/* HAL-коды переводятся в стабильные статусы диагностического протокола. */
static spi_nor_command_service_status_t map_io_status(
spi_nor_io_status_t status)
{
if (status == SPI_NOR_IO_OK) {
return SPI_NOR_COMMAND_SERVICE_OK;
}
if (status == SPI_NOR_IO_TIMEOUT) {
return SPI_NOR_COMMAND_SERVICE_TIMEOUT;
}
return SPI_NOR_COMMAND_SERVICE_IO_ERROR;
}
/* READ использует 24-битный big-endian адрес, как основное ядро SpiNor. */
static uint32_t read_address(const uint8_t tx[4])
{
return ((uint32_t)tx[1] << 16U) |
((uint32_t)tx[2] << 8U) |
(uint32_t)tx[3];
}
spi_nor_command_service_status_t spi_nor_command_service_validate(
const spi_nor_t *device,
const spi_nor_command_service_request_t *request)
{
uint8_t opcode;
if ((device == NULL) || (request == NULL) ||
(device->initialized == 0U) || (device->detected == 0U) ||
(device->io.transfer == NULL) || (device->io.chip_select == NULL)) {
return SPI_NOR_COMMAND_SERVICE_UNAVAILABLE;
}
if ((request->tx_length == 0U) ||
(request->tx_length > SPI_NOR_COMMAND_SERVICE_MAX_TX_BYTES) ||
(request->rx_length == 0U) ||
(request->rx_length > SPI_NOR_COMMAND_SERVICE_MAX_RX_BYTES)) {
return SPI_NOR_COMMAND_SERVICE_INVALID;
}
opcode = request->tx[0];
/* JEDEC возвращает ровно manufacturer/type/capacity для определения модели. */
if (opcode == SPI_NOR_DIAG_READ_JEDEC_ID) {
return ((request->tx_length == 1U) && (request->rx_length == 3U)) ?
SPI_NOR_COMMAND_SERVICE_OK : SPI_NOR_COMMAND_SERVICE_INVALID;
}
/* SR1 — единственный общий и однозначно read-only status всех трёх NOR. */
if (opcode == SPI_NOR_DIAG_READ_STATUS_1) {
return ((request->tx_length == 1U) && (request->rx_length == 1U)) ?
SPI_NOR_COMMAND_SERVICE_OK : SPI_NOR_COMMAND_SERVICE_INVALID;
}
/* Обычный READ допускает произвольный bounded RX, но требует весь адрес. */
if (opcode == SPI_NOR_DIAG_READ_DATA) {
uint32_t address;
if (request->tx_length != 4U) {
return SPI_NOR_COMMAND_SERVICE_INVALID;
}
address = read_address(request->tx);
if ((address >= device->info.capacity_bytes) ||
((uint32_t)request->rx_length >
(device->info.capacity_bytes - address))) {
return SPI_NOR_COMMAND_SERVICE_INVALID;
}
return SPI_NOR_COMMAND_SERVICE_OK;
}
/*
* Fail-closed default блокирует WREN, program/erase, status/protection
* writes, power-down/wake, reset и любой неизвестный opcode. Будущий
* изменяющий режим должен быть отдельным API, а не расширением allowlist.
*/
return SPI_NOR_COMMAND_SERVICE_BLOCKED;
}
spi_nor_command_service_status_t spi_nor_command_service_process(
spi_nor_t *device,
const spi_nor_command_service_request_t *request,
spi_nor_command_service_response_t *response)
{
spi_nor_command_service_status_t status;
spi_nor_io_status_t io_status;
uint8_t locked = 0U;
if (response == NULL) {
return SPI_NOR_COMMAND_SERVICE_INVALID;
}
/* Очистка запрещает повторное использование старого RX после любого отказа. */
memset(response, 0, sizeof(*response));
if (request != NULL) {
response->sequence = request->sequence;
response->tx_length = request->tx_length;
if (request->tx_length <= SPI_NOR_COMMAND_SERVICE_MAX_TX_BYTES) {
memcpy(response->tx_echo, request->tx, request->tx_length);
}
}
status = spi_nor_command_service_validate(device, request);
if (status != SPI_NOR_COMMAND_SERVICE_OK) {
response->status = status;
return status;
}
if (device->busy != 0U) {
response->status = SPI_NOR_COMMAND_SERVICE_BUSY;
return response->status;
}
if (device->io.lock != NULL) {
/* Mutex относится ко всей CS-транзакции, а не к отдельному transfer. */
io_status = device->io.lock(device->io.context,
device->config.lock_timeout_ms);
if (io_status != SPI_NOR_IO_OK) {
response->status = (io_status == SPI_NOR_IO_TIMEOUT) ?
SPI_NOR_COMMAND_SERVICE_TIMEOUT :
SPI_NOR_COMMAND_SERVICE_BUSY;
return response->status;
}
locked = 1U;
}
if (device->busy != 0U) {
if ((locked != 0U) && (device->io.unlock != NULL)) {
device->io.unlock(device->io.context);
}
response->status = SPI_NOR_COMMAND_SERVICE_BUSY;
return response->status;
}
/* Busy устанавливается после mutex и снимается до unlock основного устройства. */
device->busy = 1U;
response->status = SPI_NOR_COMMAND_SERVICE_BUSY;
/* CS освобождается безусловно после TX либо RX ошибки/тайм-аута. */
device->io.chip_select(device->io.context, 1U);
io_status = device->io.transfer(device->io.context, request->tx, NULL,
request->tx_length,
device->config.io_timeout_ms);
if (io_status == SPI_NOR_IO_OK) {
io_status = device->io.transfer(device->io.context, NULL, response->rx,
request->rx_length,
device->config.io_timeout_ms);
}
device->io.chip_select(device->io.context, 0U);
device->busy = 0U;
if ((locked != 0U) && (device->io.unlock != NULL)) {
device->io.unlock(device->io.context);
}
status = map_io_status(io_status);
if (status != SPI_NOR_COMMAND_SERVICE_OK) {
/* Частичный RX не должен выглядеть достоверным после физической ошибки. */
memset(response->rx, 0, sizeof(response->rx));
response->rx_length = 0U;
response->status = status;
return status;
}
/* RX length подтверждается только когда оба transfer завершились успешно. */
response->rx_length = request->rx_length;
response->status = SPI_NOR_COMMAND_SERVICE_OK;
return response->status;
}

View File

@@ -0,0 +1,59 @@
#include "spi_nor_read_service.h"
#include <string.h>
/* Диагностический слой намеренно не содержит program/erase callbacks. */
spi_nor_read_service_status_t spi_nor_read_service_init(
spi_nor_read_service_t *service, void *context,
spi_nor_read_service_read_fn read, uint32_t capacity_bytes)
{
if ((service == NULL) || (read == NULL) || (capacity_bytes == 0U)) {
return SPI_NOR_READ_SERVICE_INVALID;
}
service->context = context;
service->read = read;
service->capacity_bytes = capacity_bytes;
return SPI_NOR_READ_SERVICE_OK;
}
/* Вычитание после проверки address исключает uint32 overflow address+length. */
spi_nor_read_service_status_t spi_nor_read_service_process(
const spi_nor_read_service_t *service,
const spi_nor_read_service_request_t *request,
spi_nor_read_service_response_t *response)
{
spi_nor_read_service_status_t status;
if (response == NULL) {
return SPI_NOR_READ_SERVICE_INVALID;
}
memset(response, 0, sizeof(*response));
if ((service == NULL) || (request == NULL) || (service->read == NULL) ||
(service->capacity_bytes == 0U)) {
response->status = SPI_NOR_READ_SERVICE_UNAVAILABLE;
return response->status;
}
response->sequence = request->sequence;
response->address = request->address;
if ((request->length == 0U) ||
(request->length > SPI_NOR_READ_SERVICE_MAX_BYTES) ||
(request->address >= service->capacity_bytes) ||
((uint32_t)request->length >
(service->capacity_bytes - request->address))) {
response->status = SPI_NOR_READ_SERVICE_INVALID;
return response->status;
}
response->status = SPI_NOR_READ_SERVICE_BUSY;
/* Callback остаётся единственной точкой физического чтения платформы. */
status = service->read(service->context, request->address,
response->data, request->length);
if (status != SPI_NOR_READ_SERVICE_OK) {
/* После SPI-ошибки вызывающий не получает частично заполненный буфер. */
memset(response->data, 0, sizeof(response->data));
response->status = status;
return response->status;
}
response->length = request->length;
response->status = SPI_NOR_READ_SERVICE_OK;
return response->status;
}

140
c/spi-nor/Inc/spi_nor.h Normal file
View File

@@ -0,0 +1,140 @@
#ifndef PORTABLE_SPI_NOR_H
#define PORTABLE_SPI_NOR_H
/*
* Переносимое ядро SPI NOR.
*
* Ядро знает только стандартные SPI-команды и callbacks платформы. Оно не
* включает HAL, регистры MCU, GPIO проекта, Modbus или код приложения. Каждый
* экземпляр хранит собственное состояние и поэтому не использует глобальные
* изменяемые переменные.
*/
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Поддерживаемые микросхемы ограничены устройствами с 24-битной адресацией. */
typedef enum
{
SPI_NOR_MODEL_NONE = 0,
SPI_NOR_MODEL_W25Q64,
SPI_NOR_MODEL_W25Q128,
SPI_NOR_MODEL_SST25VF016B
} spi_nor_model_t;
/* Ошибки протокола не зависят от кодов конкретного HAL. */
typedef enum
{
SPI_NOR_OK = 0,
SPI_NOR_E_ARGUMENT = -1,
SPI_NOR_E_IO = -2,
SPI_NOR_E_TIMEOUT = -3,
SPI_NOR_E_NOT_FOUND = -4,
SPI_NOR_E_UNSUPPORTED = -5,
SPI_NOR_E_BOUNDS = -6,
SPI_NOR_E_ALIGNMENT = -7,
SPI_NOR_E_VERIFY = -8,
SPI_NOR_E_BUSY = -9
} spi_nor_status_t;
/* Callback SPI возвращает только переносимый результат физического обмена. */
typedef enum
{
SPI_NOR_IO_OK = 0,
SPI_NOR_IO_ERROR = -1,
SPI_NOR_IO_TIMEOUT = -2
} spi_nor_io_status_t;
/* Геометрия и JEDEC доступны приложению только как копируемая диагностика. */
typedef struct
{
spi_nor_model_t model;
uint8_t manufacturer_id;
uint8_t memory_type;
uint8_t capacity_code;
uint32_t capacity_bytes;
uint32_t sector_size;
uint16_t page_size;
} spi_nor_info_t;
/*
* Контракт платформы описывает одну SPI-шину и один сигнал CS.
*
* transfer выполняется при уже активном CS. NULL в tx означает передачу
* dummy-байтов, NULL в rx — игнорирование принятого потока. lock/unlock
* необязательны; RTOS-порт может ими сериализовать полную read/program/erase
* операцию, а не отдельный SPI пакет.
*/
typedef struct
{
void *context;
spi_nor_io_status_t (*transfer)(void *context, const uint8_t *tx,
uint8_t *rx, size_t size,
uint32_t timeout_ms);
void (*chip_select)(void *context, uint8_t active);
uint32_t (*tick_ms)(void *context);
void (*delay_ms)(void *context, uint32_t delay_ms);
spi_nor_io_status_t (*lock)(void *context, uint32_t timeout_ms);
void (*unlock)(void *context);
} spi_nor_io_t;
/* Тайм-ауты вынесены в конфигурацию для медленных шин и разных RTOS. */
typedef struct
{
uint32_t io_timeout_ms;
uint32_t lock_timeout_ms;
uint32_t program_timeout_ms;
uint32_t erase_timeout_ms;
uint32_t ready_poll_delay_ms;
} spi_nor_config_t;
/* Контекст полностью принадлежит вызывающему коду и не требует malloc. */
typedef struct
{
spi_nor_io_t io;
spi_nor_config_t config;
spi_nor_info_t info;
uint8_t initialized;
uint8_t detected;
uint8_t busy;
} spi_nor_t;
/* Возвращает документированные безопасные тайм-ауты библиотеки. */
spi_nor_config_t spi_nor_default_config(void);
/* Инициализация только копирует callbacks и не обращается к физической шине. */
spi_nor_status_t spi_nor_init(spi_nor_t *device, const spi_nor_io_t *io,
const spi_nor_config_t *config);
/* Probe будит память, читает JEDEC и разрешает работу известной геометрии. */
spi_nor_status_t spi_nor_probe(spi_nor_t *device);
/* Операции используют физические смещения микросхемы, начиная с нуля. */
spi_nor_status_t spi_nor_read(spi_nor_t *device, uint32_t address,
void *data, size_t size);
spi_nor_status_t spi_nor_program(spi_nor_t *device, uint32_t address,
const void *data, size_t size);
spi_nor_status_t spi_nor_erase(spi_nor_t *device, uint32_t address,
size_t size);
/* Информация выдаётся только после успешного распознавания поддержанной модели. */
spi_nor_status_t spi_nor_get_info(const spi_nor_t *device,
spi_nor_info_t *info);
/* Последний JEDEC доступен и для различения пустой шины и неизвестной модели. */
spi_nor_status_t spi_nor_get_last_jedec(const spi_nor_t *device,
uint8_t jedec[3]);
/* Read-only диагностика возвращает status register 1 под общей блокировкой. */
spi_nor_status_t spi_nor_read_status_register(spi_nor_t *device,
uint8_t *status);
#ifdef __cplusplus
}
#endif
#endif /* PORTABLE_SPI_NOR_H */

123
c/spi-nor/PORTING.md Normal file
View File

@@ -0,0 +1,123 @@
# Портирование SPI NOR на другую платформу
## 1. Добавить переносимое ядро
В сборку новой цели включите `Src/spi_nor.c`, а `Inc` добавьте в include path.
Это единственные обязательные файлы. Они используют только C99-заголовки
`stddef.h`, `stdint.h` и `string.h`.
## 2. Реализовать platform callbacks
Создайте собственный контекст шины и заполните `spi_nor_io_t`:
- `transfer` — полный синхронный SPI-обмен при уже активном CS;
- `chip_select` — `active=1` опускает CS, `active=0` поднимает его;
- `tick_ms` — монотонный 32-битный счётчик миллисекунд;
- `delay_ms` — задержка выхода из power-down и пауза BUSY polling;
- `lock/unlock` — необязательная пара для RTOS, оба либо заданы, либо `NULL`.
`transfer` должен поддерживать следующие варианты:
| `tx` | `rx` | Действие |
|---|---|---|
| не `NULL` | `NULL` | передать command или payload |
| `NULL` | не `NULL` | передавать dummy и сохранить входящие байты |
| не `NULL` | не `NULL` | full-duplex обмен, допустим для порта |
Callback не должен самостоятельно переключать CS. Ядро может вызвать transfer
дважды под одним CS: сначала command, затем data. Ошибка должна возвращаться как
`SPI_NOR_IO_ERROR` или `SPI_NOR_IO_TIMEOUT`.
## 3. Настроить SPI
Для W25Q/SST25 используйте master, 8 бит, MSB first и режим, разрешённый
datasheet. Частота не должна превышать предел микросхемы и возможности разводки.
CS перед `spi_nor_init()` должен быть настроен output push-pull и находиться в 1.
Проверьте питание, общую землю и отсутствие конфликта MISO с другими CS.
## 4. Инициализировать и выполнить probe
```c
spi_nor_t device;
spi_nor_io_t io = MakeMyPlatformIo();
spi_nor_config_t config = spi_nor_default_config();
config.erase_timeout_ms = 10000U; /* Пример для медленной микросхемы. */
if (spi_nor_init(&device, &io, &config) != SPI_NOR_OK) {
HandleConfigurationError();
}
if (spi_nor_probe(&device) != SPI_NOR_OK) {
HandleAbsentOrUnsupportedFlash();
}
```
До успешного probe read/program/erase отклоняются. Для диагностики неизвестной
памяти вызовите `spi_nor_get_last_jedec()` после неуспешного probe.
## 5. Подключить FlashStorage при необходимости
Добавьте в сборку:
- `Libraries/FlashStorage/Src/flash_storage.c`;
- `Libraries/FlashStorage/Adapter/SPI_NOR/Src/flash_storage_spi_nor_adapter.c`;
- include paths `FlashStorage/Inc` и `FlashStorage/Adapter/SPI_NOR/Inc`.
После probe создайте `flash_storage_spi_nor_adapter_t`, вызовите
`flash_storage_spi_nor_adapter_init()` и передайте `flash_storage_spi_nor_ops`
в `flash_storage_init()` или `eeprom_store_init()`. Layout должен лежать внутри
фактической `capacity_bytes` и быть выровнен по 4096 байтам.
## 6. STM32 HAL
Для другого STM32 можно использовать существующий порт, если семейство имеет
совместимый HAL API. Подключите `spi_nor_stm32f4_hal.c`, замените HAL include на
заголовок нужного семейства и переименуйте порт. Имена конкретных `hspi`, GPIO
и pin передавайте из приложения; не добавляйте их как `extern` в библиотеку.
Если используется DMA или RTOS, простой STM32F4 порт следует заменить:
- transfer обязан дождаться завершения DMA до возврата;
- lock/unlock должны использовать mutex, не semaphore из ISR;
- mutex должен быть общим для всех устройств на одной SPI-шине;
- callback delay должен освобождать CPU через RTOS delay, если это допустимо.
## 7. Приёмочные проверки
1. На пустой шине получить `SPI_NOR_E_NOT_FOUND` и CS=1.
2. Прочитать ожидаемый JEDEC.
3. Проверить чтение первого и последнего адреса.
4. В тестовом секторе выполнить erase и полный verify `0xFF`.
5. Записать данные через границу страницы и сравнить readback.
6. Имитировать SPI error/timeout и убедиться, что CS поднят.
7. Проверить отказ для диапазона за концом памяти и невыровненного erase.
8. При RTOS параллельно запустить два клиента и проверить mutex-анализатором.
Для текущего репозитория host mock запускается через
`python -m unittest tests.test_spi_nor_host` и не требует STM32 HAL.
## 8. Перенос read-only command service
Если нужен сервисный просмотр команд, добавьте в сборку
`Diagnostics/Src/spi_nor_command_service.c` и include path `Diagnostics/Inc`.
Передавайте ему уже успешно probed `spi_nor_t`; отдельный HAL-порт не нужен:
```c
spi_nor_command_service_request_t request = {0};
spi_nor_command_service_response_t response;
request.sequence = 1U;
request.tx_length = 1U;
request.rx_length = 3U;
request.tx[0] = 0x9FU;
(void)spi_nor_command_service_process(&device, &request, &response);
```
Не расширяйте существующий allowlist ради записи. Если продукту когда-нибудь
потребуются изменяющие операции, они должны иметь отдельный API, threat model,
защиту диапазонов и аппаратные тесты. Текущий сервис не содержит compile-time
или runtime переключателя, снимающего блокировку.
Проверьте mock-тестом своей платформы: TX/RX порядок под одним CS, освобождение
CS при timeout каждого transfer, общий mutex SPI bus, строгие максимумы 4/64 и
отказ для `06`, `02`, всех erase, status/protection writes, power/reset и
неизвестных opcode.

113
c/spi-nor/README.md Normal file
View File

@@ -0,0 +1,113 @@
# Переносимая библиотека SPI NOR
`SpiNor` — самостоятельный синхронный драйвер последовательной NOR Flash. Он
не зависит от STM32, HAL, `main.c`, GPIO проекта, Modbus, GUI и FlashStorage.
Память для контекста предоставляет приложение; `malloc` и глобальное изменяемое
состояние не используются.
## Поддержанные микросхемы
| Модель | JEDEC | Ёмкость | Program | Erase |
|---|---:|---:|---:|---:|
| W25Q64 | `EF 40 17` | 8 МиБ | страницы до 256 байт | 4 КиБ |
| W25Q128 | `EF 40 18` | 16 МиБ | страницы до 256 байт | 4 КиБ |
| SST25VF016B | `BF 25 41` | 2 МиБ | безопасный Byte Program | 4 КиБ |
Ядро использует команды с 24-битным адресом и намеренно не принимает
микросхемы больше 16 МиБ. Неизвестный JEDEC возвращает
`SPI_NOR_E_UNSUPPORTED`; пустая шина `00 00 00` или `FF FF FF` —
`SPI_NOR_E_NOT_FOUND`.
## Структура
- `Inc/spi_nor.h` — публичные типы, callbacks, конфигурация и API;
- `Src/spi_nor.c` — переносимый протокол, JEDEC, bounds, тайм-ауты и verify;
- `Port/STM32F4_HAL` — единственное место с зависимостью от STM32F4 HAL;
- `Examples/portable_init.c` — минимальная интеграция без имён текущей платы;
- `PORTING.md` — пошаговый перенос на другой STM32 или другой MCU;
- `Tests` — mock-шина и сценарии без HAL и реального оборудования.
- `Diagnostics` — два bounded read-only сервиса без HAL/Modbus/GUI;
`spi_nor_read_service` читает физический диапазон через callback, а
`spi_nor_command_service` выполняет одну разрешённую сервисную транзакцию.
Адаптер журнала находится отдельно в
`Libraries/FlashStorage/Adapter/SPI_NOR`: FlashStorage не проникает в ядро
SPI NOR, а SPI NOR не знает формат журнала.
Диагностический сервис принимает только capacity и callback чтения, ограничивает
один пакет 64 байт и не имеет `program`/`erase` API. Приложение может связать
его с Modbus, не добавляя GUI или прикладные зависимости в ядро `SpiNor`.
### Сервис коротких SPI-команд
`spi_nor_command_service` принимает `spi_nor_t`, request с `sequence`, TX до
4 байт и RX до 64 байт, а response возвращает status, echo TX и RX. Владельцем
контекста остаётся приложение; сервис не выделяет память и не хранит историю.
Allowlist одинаков для всех трёх поддержанных моделей:
| Opcode | Строгая форма | Причина допуска |
|---:|---|---|
| `9F` | TX 1, RX 3 | JEDEC ID |
| `05` | TX 1, RX 1 | status register 1 |
| `03` | TX 4 (`03 A2 A1 A0`), RX 1–64 | обычное 24-битное чтение с bounds |
Любой другой opcode возвращает `BLOCKED`. В частности, запрещены WREN/WRDI,
program, все erase, status/protection writes, power-down/release, reset и
неизвестные команды. Это fail-closed read-only API, а не универсальный SPI
terminal. Изменяющий сервисный режим не требуется и в библиотеку не включён.
Одна операция имеет порядок `CS active -> TX -> dummy RX -> CS inactive`.
После ошибки или тайм-аута TX/RX сервис безусловно освобождает CS и очищает
частичный RX. Он использует тот же `busy` и необязательные `lock/unlock`, что
основной экземпляр `spi_nor_t`.
## Публичный API
```c
spi_nor_io_t io;
spi_nor_t flash;
/* Порт приложения заполняет callbacks и собственный io.context. */
FillPlatformCallbacks(&io);
if (spi_nor_init(&flash, &io, NULL) == SPI_NOR_OK &&
spi_nor_probe(&flash) == SPI_NOR_OK) {
uint8_t bytes[16];
(void)spi_nor_read(&flash, 0U, bytes, sizeof(bytes));
}
```
`spi_nor_program()` автоматически делит поток по страницам и проверяет каждый
chunk обратным чтением. `spi_nor_erase()` принимает только полные выровненные
4-КиБ секторы и проверяет каждый стёртый байт на `0xFF`.
## Электрическое подключение
Нужны `SCK`, `MOSI`, `MISO`, отдельный active-low `CS`, питание и земля.
Питание и допустимую частоту следует брать из datasheet конкретной модели.
Для текущей платы используются SPI2: `PB10/SCK`, `PC3/MOSI`, `PC2/MISO` и
`PE3/CS`, но эти имена находятся только в `app_storage.c`.
## Ограничения выполнения
- API синхронный и не предназначен для ISR;
- erase может блокировать вызывающий поток до заданного тайм-аута;
- один `spi_nor_t` не допускает рекурсивных операций;
- при RTOS callbacks `lock/unlock` должны защищать всю составную операцию;
- общий SPI bus требует согласованного mutex и корректного управления CS всех
подключённых устройств;
- DMA callback нельзя считать завершённым до фактического окончания обмена;
- библиотека не меняет SPI mode и clock: это обязанность платформенного порта.
## Гарантии ошибок
При любой ошибке обмена CS возвращается в неактивное состояние. Ошибка program
или erase не маскируется: результат считается успешным только после ожидания
BUSY и полного readback verify. Повторять изменяющую операцию автоматически
библиотека не будет, потому что политика повторов принадлежит приложению.
## Shared source
Canonical source: `templates/c/spi-nor`. Used by `home/climate`; its old paths are compatibility includes. Board-specific ports remain in the application. Change this library, not the forwarding files.

472
c/spi-nor/Src/spi_nor.c Normal file
View 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;
}

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;
}

View File

@@ -0,0 +1,166 @@
#include "spi_nor_command_service.h"
#include <stdio.h>
#include <string.h>
typedef struct
{
/* Счётчики доказывают единственную пару assert/release для каждого вызова. */
uint8_t cs_active;
uint8_t cs_asserts;
uint8_t cs_releases;
uint8_t transfers;
spi_nor_io_status_t fail_on_transfer;
uint8_t fail_transfer_number;
} mock_bus_t;
static unsigned checks;
static unsigned failures;
/* Каждый CHECK увеличивает общий счётчик, чтобы тест не мог пройти пустым. */
#define CHECK(value) do { checks++; if (!(value)) { failures++; } } while (0)
/* Mock различает TX и dummy RX, сохраняя реальный порядок одной транзакции. */
static spi_nor_io_status_t mock_transfer(void *context, const uint8_t *tx,
uint8_t *rx, size_t size,
uint32_t timeout_ms)
{
mock_bus_t *bus = context;
size_t index;
(void)timeout_ms;
/* Номер transfer позволяет отдельно сломать TX и последующий dummy RX. */
bus->transfers++;
if ((bus->fail_transfer_number != 0U) &&
(bus->transfers == bus->fail_transfer_number)) {
return bus->fail_on_transfer;
}
if ((tx == NULL) && (rx != NULL)) {
for (index = 0U; index < size; index++) {
rx[index] = (uint8_t)(0xA0U + index);
}
}
return SPI_NOR_IO_OK;
}
static void mock_cs(void *context, uint8_t active)
{
mock_bus_t *bus = context;
/* Последнее состояние проверяется после каждого error/timeout сценария. */
bus->cs_active = active;
if (active != 0U) {
bus->cs_asserts++;
} else {
bus->cs_releases++;
}
}
static void reset_bus(mock_bus_t *bus)
{
/* Сценарии не наследуют счётчики и forced error предыдущего вызова. */
memset(bus, 0, sizeof(*bus));
}
int main(void)
{
mock_bus_t bus;
spi_nor_t device;
spi_nor_command_service_request_t request;
spi_nor_command_service_response_t response;
static const uint8_t blocked[] = {
0x06U, 0x02U, 0x20U, 0x52U, 0xD8U, 0xC7U, 0x60U,
0x01U, 0x31U, 0x11U, 0x36U, 0x39U, 0xB9U, 0xABU,
0x66U, 0x99U, 0x04U, 0xFFU
};
size_t index;
/* Mock device считается уже успешно probed поддержанной SPI NOR. */
memset(&device, 0, sizeof(device));
reset_bus(&bus);
device.io.context = &bus;
device.io.transfer = mock_transfer;
device.io.chip_select = mock_cs;
device.config.io_timeout_ms = 10U;
device.config.lock_timeout_ms = 10U;
device.info.capacity_bytes = 256U;
device.initialized = 1U;
device.detected = 1U;
memset(&request, 0, sizeof(request));
request.sequence = 7U;
request.tx_length = 1U;
request.rx_length = 3U;
request.tx[0] = 0x9FU;
/* JEDEC проверяет данные, sequence, echo и физический порядок transfers. */
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_OK);
CHECK(response.sequence == 7U && response.tx_echo[0] == 0x9FU);
CHECK(response.rx_length == 3U && response.rx[0] == 0xA0U &&
response.rx[2] == 0xA2U);
CHECK(bus.cs_asserts == 1U && bus.cs_releases == 1U &&
bus.cs_active == 0U && bus.transfers == 2U);
/* Status и последний физический byte разрешены строгими shape/bounds. */
reset_bus(&bus);
request.tx[0] = 0x05U;
request.rx_length = 1U;
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_OK);
request.tx[0] = 0x03U;
request.tx[1] = 0U;
request.tx[2] = 0U;
request.tx[3] = 0xFFU;
request.tx_length = 4U;
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_OK);
request.rx_length = 2U;
/* Последний адрес плюс два байта обязан отклониться до активации CS. */
CHECK(spi_nor_command_service_validate(&device, &request) ==
SPI_NOR_COMMAND_SERVICE_INVALID);
/* Весь mutating/state-changing набор и неизвестный opcode fail-closed. */
request.tx_length = 1U;
request.rx_length = 1U;
/* Список включает все опасные семейства opcode и произвольный unknown FF. */
for (index = 0U; index < sizeof(blocked); index++) {
request.tx[0] = blocked[index];
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_BLOCKED);
CHECK(bus.cs_active == 0U);
}
/* Неверные длины не обращаются к шине. */
reset_bus(&bus);
request.tx[0] = 0x9FU;
request.tx_length = 1U;
request.rx_length = SPI_NOR_COMMAND_SERVICE_MAX_RX_BYTES + 1U;
/* Проверяем не только статус, но и полное отсутствие физического I/O. */
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_INVALID);
CHECK(bus.transfers == 0U && bus.cs_asserts == 0U);
/* CS обязан стать inactive при ошибке TX, ошибке RX и timeout RX. */
request.rx_length = 3U;
/* Одинаковая гарантия CS применяется к первому и второму transfer. */
for (index = 1U; index <= 2U; index++) {
reset_bus(&bus);
bus.fail_transfer_number = (uint8_t)index;
bus.fail_on_transfer = SPI_NOR_IO_ERROR;
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_IO_ERROR);
CHECK(bus.cs_releases == 1U && bus.cs_active == 0U &&
response.rx_length == 0U);
}
reset_bus(&bus);
bus.fail_transfer_number = 2U;
bus.fail_on_transfer = SPI_NOR_IO_TIMEOUT;
/* Timeout имеет отдельный статус, но ту же очистку RX и освобождение CS. */
CHECK(spi_nor_command_service_process(&device, &request, &response) ==
SPI_NOR_COMMAND_SERVICE_TIMEOUT);
CHECK(bus.cs_releases == 1U && bus.cs_active == 0U);
(void)printf("SPI NOR command service: %u checks, %u failures\n",
checks, failures);
return failures == 0U ? 0 : 1;
}

View File

@@ -0,0 +1,74 @@
#include "spi_nor_read_service.h"
#include <stdio.h>
#include <string.h>
static unsigned checks;
static unsigned failures;
static uint8_t image[256];
static spi_nor_read_service_status_t forced_status = SPI_NOR_READ_SERVICE_OK;
#define CHECK(value) do { checks++; if (!(value)) { failures++; } } while (0)
/* Mock копирует детерминированный образ и умеет вернуть физическую ошибку. */
static spi_nor_read_service_status_t mock_read(void *context, uint32_t address,
void *data, size_t size)
{
(void)context;
if (forced_status != SPI_NOR_READ_SERVICE_OK) {
memset(data, 0xA5, size);
return forced_status;
}
memcpy(data, &image[address], size);
return SPI_NOR_READ_SERVICE_OK;
}
int main(void)
{
spi_nor_read_service_t service;
spi_nor_read_service_request_t request;
spi_nor_read_service_response_t response;
unsigned index;
for (index = 0U; index < sizeof(image); index++) {
image[index] = (uint8_t)index;
}
CHECK(spi_nor_read_service_init(&service, NULL, mock_read,
sizeof(image)) == SPI_NOR_READ_SERVICE_OK);
request.address = 0U;
request.length = 1U;
request.sequence = 7U;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_OK);
CHECK(response.data[0] == 0U && response.sequence == 7U);
/* Последний физический байт является допустимой включительной границей. */
request.address = sizeof(image) - 1U;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_OK);
CHECK(response.data[0] == 0xFFU);
request.address = sizeof(image);
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_INVALID);
request.address = sizeof(image) - 32U;
request.length = 33U;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_INVALID);
request.address = 0U;
request.length = 0U;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_INVALID);
request.length = SPI_NOR_READ_SERVICE_MAX_BYTES + 1U;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_INVALID);
request.length = 8U;
forced_status = SPI_NOR_READ_SERVICE_IO_ERROR;
CHECK(spi_nor_read_service_process(&service, &request, &response) ==
SPI_NOR_READ_SERVICE_IO_ERROR);
CHECK(response.length == 0U && response.data[0] == 0U);
(void)printf("SPI NOR read service: %u checks, %u failures\n", checks, failures);
return failures == 0U ? 0 : 1;
}