Add embedded storage drivers and extend firmware metadata
This commit is contained in:
10
c/spi-nor/CMakeLists.txt
Normal file
10
c/spi-nor/CMakeLists.txt
Normal 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)
|
||||
72
c/spi-nor/Diagnostics/Inc/spi_nor_command_service.h
Normal file
72
c/spi-nor/Diagnostics/Inc/spi_nor_command_service.h
Normal 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 */
|
||||
73
c/spi-nor/Diagnostics/Inc/spi_nor_read_service.h
Normal file
73
c/spi-nor/Diagnostics/Inc/spi_nor_read_service.h
Normal 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 */
|
||||
165
c/spi-nor/Diagnostics/Src/spi_nor_command_service.c
Normal file
165
c/spi-nor/Diagnostics/Src/spi_nor_command_service.c
Normal 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;
|
||||
}
|
||||
59
c/spi-nor/Diagnostics/Src/spi_nor_read_service.c
Normal file
59
c/spi-nor/Diagnostics/Src/spi_nor_read_service.c
Normal 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
140
c/spi-nor/Inc/spi_nor.h
Normal 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
123
c/spi-nor/PORTING.md
Normal 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
113
c/spi-nor/README.md
Normal 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
472
c/spi-nor/Src/spi_nor.c
Normal 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;
|
||||
}
|
||||
363
c/spi-nor/Tests/test_spi_nor.c
Normal file
363
c/spi-nor/Tests/test_spi_nor.c
Normal 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;
|
||||
}
|
||||
166
c/spi-nor/Tests/test_spi_nor_command_service.c
Normal file
166
c/spi-nor/Tests/test_spi_nor_command_service.c
Normal 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;
|
||||
}
|
||||
74
c/spi-nor/Tests/test_spi_nor_read_service.c
Normal file
74
c/spi-nor/Tests/test_spi_nor_read_service.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user