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
@@ -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 */
@@ -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 */
@@ -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;
}
@@ -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;
}