473 lines
17 KiB
C
473 lines
17 KiB
C
#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;
|
||
}
|