Прочитал на Хабре кучу простых, и даже очень, статеек на тему программирования микроконтроллеров, тоже решил добавить что-то простое, понятное, но чуть более полезное.
Да простят меня бывалые разработчики за сей труд, да и хейтеры пусть прощают тоже?
О чём пойдет речь
Будем размышлять и пытаться писать быстрый драйвер флешки и при этом попробуем сэкономить её ресурс при перезаписях.
Предыстория и проблемы возможных реализаций
Понадобилась SPI флэшка для хранения данных, которыми оперирует встраиваемое ПО микроконтроллера. Типовая задача, если не хватает места внутри основного чипа.
Объем памяти нужен был небольшой, но приличный, что-то в районе 8 мегабайт.
Как и все нормальные программисты, во избежание изобретения велосипеда, полез искать в сети (github, gitlab) в итоге ничего не приглянулось.
Основные проблемы найденных вариантов:
Все реализации предоставляют только базовые функции записи и чтения по страницам и секторам.
Нет реализаций, когда нужно считать или записать страницы, диапазон которых разбросан на два или более секторов, а некоторые не позволяют даже с несколькими страницами в одном секторе провзаимодействовать.
Кривая по тем или иным причинам реализация.
Реализация в лоб. Необходимо ещё свою обертку писать с нормальным взаимодействием.
Плохой стиль/архитектура:
extern везде, где нужно и не нужно
Куча макросов, где можно обойтись без них, ещё и получить бóльший профит.
Вытаскивание наружу того, что должно оставаться внутри модуля, — торчащие глобальные переменные и функции, которые используются только внутри и никак не описаны в файле заголовка.
Приведу несколько примеров того, что не понравилось.
Пример 1 - с описанием и критикой
https://github.com/nimaltd/w25qxx
//...
extern w25qxx_t w25qxx;
bool W25qxx_Init(void);
void W25qxx_EraseChip(void);
void W25qxx_EraseSector(uint32_t SectorAddr);
void W25qxx_EraseBlock(uint32_t BlockAddr);
uint32_t W25qxx_PageToSector(uint32_t PageAddress);
uint32_t W25qxx_PageToBlock(uint32_t PageAddress);
uint32_t W25qxx_SectorToBlock(uint32_t SectorAddress);
uint32_t W25qxx_SectorToPage(uint32_t SectorAddress);
uint32_t W25qxx_BlockToPage(uint32_t BlockAddress);
bool W25qxx_IsEmptyPage(uint32_t Page_Address, uint32_t OffsetInByte, uint32_t NumByteToCheck_up_to_PageSize);
bool W25qxx_IsEmptySector(uint32_t Sector_Address, uint32_t OffsetInByte, uint32_t NumByteToCheck_up_to_SectorSize);
bool W25qxx_IsEmptyBlock(uint32_t Block_Address, uint32_t OffsetInByte, uint32_t NumByteToCheck_up_to_BlockSize);
void W25qxx_WriteByte(uint8_t pBuffer, uint32_t Bytes_Address);
void W25qxx_WritePage(uint8_t *pBuffer, uint32_t Page_Address, uint32_t OffsetInByte, uint32_t NumByteToWrite_up_to_PageSize);
void W25qxx_WriteSector(uint8_t *pBuffer, uint32_t Sector_Address, uint32_t OffsetInByte, uint32_t NumByteToWrite_up_to_SectorSize);
void W25qxx_WriteBlock(uint8_t *pBuffer, uint32_t Block_Address, uint32_t OffsetInByte, uint32_t NumByteToWrite_up_to_BlockSize);
void W25qxx_ReadByte(uint8_t *pBuffer, uint32_t Bytes_Address);
void W25qxx_ReadBytes(uint8_t *pBuffer, uint32_t ReadAddr, uint32_t NumByteToRead);
void W25qxx_ReadPage(uint8_t *pBuffer, uint32_t Page_Address, uint32_t OffsetInByte, uint32_t NumByteToRead_up_to_PageSize);
void W25qxx_ReadSector(uint8_t *pBuffer, uint32_t Sector_Address, uint32_t OffsetInByte, uint32_t NumByteToRead_up_to_SectorSize);
void W25qxx_ReadBlock(uint8_t *pBuffer, uint32_t Block_Address, uint32_t OffsetInByte, uint32_t NumByteToRead_up_to_BlockSize);
Куча ненужных функций, в тупую реализован datasheet. Из всего множества функций можно выделить лишь несколько необходимых. Но тех, которые реально нужны нет.
А теперь очень длинный кусок кода, который я уже сократил, но он всё ещё слишком большой.
switch (id & 0x000000FF)
{
case 0x20: // w25q512
w25qxx.ID = W25Q512;
w25qxx.BlockCount = 1024;
break;
case 0x19: // w25q256
w25qxx.ID = W25Q256;
w25qxx.BlockCount = 512;
break;
case 0x18: // w25q128
w25qxx.ID = W25Q128;
w25qxx.BlockCount = 256;
break;
case 0x17: // w25q64
w25qxx.ID = W25Q64;
w25qxx.BlockCount = 128;
break;
case 0x16: // w25q32
w25qxx.ID = W25Q32;
w25qxx.BlockCount = 64;
break;
case 0x15: // w25q16
w25qxx.ID = W25Q16;
w25qxx.BlockCount = 32;
break;
case 0x14: // w25q80
w25qxx.ID = W25Q80;
w25qxx.BlockCount = 16;
break;
case 0x13: // w25q40
w25qxx.ID = W25Q40;
w25qxx.BlockCount = 8;
break;
case 0x12: // w25q20
w25qxx.ID = W25Q20;
w25qxx.BlockCount = 4;
break;
case 0x11: // w25q10
w25qxx.ID = W25Q10;
w25qxx.BlockCount = 2;
break;
default:
w25qxx.Lock = 0;
return false;
}
w25qxx.PageSize = 256;
w25qxx.SectorSize = 0x1000;
w25qxx.SectorCount = w25qxx.BlockCount * 16;
w25qxx.PageCount = (w25qxx.SectorCount * w25qxx.SectorSize) / w25qxx.PageSize;
w25qxx.BlockSize = w25qxx.SectorSize * 16;
w25qxx.CapacityInKiloByte = (w25qxx.SectorCount * w25qxx.SectorSize) / 1024;
Чтобы добавить или убрать какие-то микросхемы, которые никогда в жизни не попадут в устройство, нужно пролистать до 170 строки, вставить или удалить ветви switch, а потом не забыть проверить дальнейший код.
Пример 2 - чуть более лояльная критика, но это не точно
https://github.com/iammingge/Driver_W25Qxx
Те же самые проблемы с API. Но уже есть возможность вернуть ошибку при выполнении операций. Да и то не самым элегантным способом.
Список доступных функций
//.. это 250 строка до этого куча макросов бесполезных макросов.
uint16_t W25Qxx_ID_Manufacturer(void);
uint32_t W25Qxx_ID_JEDEC(void);
uint64_t W25Qxx_ID_Unique(void);
void W25Qxx_Reset(void);
void W25Qxx_PowerEnable(void);
void W25Qxx_PowerDisable(void);
void W25Qxx_VolatileSR_WriteEnable(void);
void W25Qxx_WriteEnable(void);
void W25Qxx_WriteDisable(void);
void W25Qxx_4ByteMode(void);
void W25Qxx_3ByteMode(void);
void W25Qxx_Suspend(void);
void W25Qxx_Resume(void);
void W25Qxx_ReadExtendedRegister(void);
void W25Qxx_WriteExtendedRegister(uint8_t ExtendedAddr);
void W25Qxx_ReadStatusRegister(uint8_t Select_SR_1_2_3);
void W25Qxx_WriteStatusRegister(uint8_t Select_SR_1_2_3, uint8_t Data);
void W25Qxx_VolatileSR_WriteStatusRegister(uint8_t Select_SR_1_2_3, uint8_t Data);
uint8_t W25Qxx_RBit_WEL(void);
uint8_t W25Qxx_RBit_BUSY(void);
uint8_t W25Qxx_RBit_SUS(void);
uint8_t W25Qxx_RBit_ADS(void);
void W25Qxx_WBit_SRP(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_TB(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_CMP(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_QE(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_SRL(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_WPS(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_DRV(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_BP(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_LB(W25Qxx_SRM srm, uint8_t bit);
void W25Qxx_WBit_ADP(uint8_t bit);
uint8_t W25Qxx_ReadStatus(void);
void W25Qxx_isStatus(uint8_t Select_Status, uint32_t timeout, W25Qxx_ERR *err);
void W25Qxx_Global_UnLock(W25Qxx_ERR *err);
void W25Qxx_Global_Locked(W25Qxx_ERR *err);
void W25Qxx_Individual_UnLock(uint32_t ByteAddr, W25Qxx_ERR *err);
void W25Qxx_Individual_Locked(uint32_t ByteAddr, W25Qxx_ERR *err);
void W25Qxx_Erase_Chip(W25Qxx_ERR *err);
void W25Qxx_Erase_Block64(uint32_t Block64Addr, W25Qxx_ERR *err);
void W25Qxx_Erase_Block32(uint32_t Block32Addr, W25Qxx_ERR *err);
void W25Qxx_Erase_Sector(uint32_t SectorAddr, W25Qxx_ERR *err);
void W25Qxx_Erase_Security(uint32_t SectorAddr, W25Qxx_ERR *err);
void W25Qxx_Read(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToRead, W25Qxx_ERR *err);
void W25Qxx_Read_Security(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToRead, W25Qxx_ERR *err);
void W25Qxx_Read_SFDP(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToRead, W25Qxx_ERR *err);
void W25Qxx_DIR_Program_Page(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToWrite, W25Qxx_ERR *err);
void W25Qxx_DIR_Program(uint8_t *pBuffer, uint32_t ByteAddr, uint32_t NumByteToWrite, W25Qxx_ERR *err);
void W25Qxx_DIR_Program_Security(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToWrite, W25Qxx_ERR *err);
void W25Qxx_Program(uint8_t *pBuffer, uint32_t ByteAddr, uint32_t NumByteToWrite, W25Qxx_ERR *err);
void W25Qxx_Program_Security(uint8_t *pBuffer, uint32_t ByteAddr, uint16_t NumByteToWrite, W25Qxx_ERR *err);
W25QXX_EXT W25Qxx_t W25Qxx;
void W25Qxx_QueryChip(W25Qxx_ERR *err);
void W25Qxx_config(W25Qxx_ERR *err);
void W25Qxx_SusResum_Erase_Sector(uint32_t SectorAddr, W25Qxx_ERR *err);
А теперь по стилистике.
Приведу небольшой кусочек в заголовочном файле.
Описаны команды, которые может принимать микросхема. Объясните, для чего их нужно вытаскивать из модуля, для чего они именно макросами, а не в виде перечисления.
Зачем здесь данные о самой микросхеме, если они нигде в этом API не используются, а нужны только для внутренних вычислений?
Так много вопросов и так мало ответов...
#define W25Q_CMD_WEN 0x06
#define W25Q_CMD_VOLATILESREN 0x50
#define W25Q_CMD_WDEN 0x04
#define W25Q_CMD_POWEREN 0xAB
#define W25Q_CMD_MANUFACTURER 0x90
#define W25Q_CMD_JEDECID 0x9F
#define W25Q_CMD_UNIQUEID 0x4B
#define W25Q_CMD_READ 0x03
#define W25Q_CMD_4BREAD 0x13
#define W25Q_CMD_FASTREAD 0x0B
#define W25Q_CMD_4BFASTREAD 0x0C
/**
* @brief W25Qxx SIZE
*/
#define W25Qxx_PAGESIZE 0x00100
#define W25Qxx_PAGEPOWER 0x08
#define W25Qxx_SECTORSIZE 0x01000
#define W25Qxx_SECTORPOWER 0x0C
#define W25Qxx_BLOCKSIZE 0x10000
#define W25Qxx_BLOCKPOWER 0x10
#define W25Qxx_SECTURITYSIZE 0x00300
От себя маленькое замечание: все структуры и перечисления, на основе которых делаются типы, нужно именовать, а не просто так. Вот пример, как плохо.
typedef enum
{
UNKNOWN = 0x000000,
W25X05 = 0xEFFF10,
W25X10 = 0xEFFF11,
...
} W25Qxx_CHIP;
Если отредактировать первую строку и вписать туда имя перечисления, пусть даже W25Qxx_CHIP
, появится возможность делать неполное описание этого типа там, где он вдруг понадобится, а не подключать весь заголовочный файл. Даже тупо подсветка в редакторе кода симпатичнее.
typedef enum W25Qxx_CHIP W25Qxx_CHIP;
Правда, это отдельная тема...
У этого кода есть и хорошие черты, например, запись данных реализована интереснее, чем в остальных: можно писать массивом страниц, автоматизировано стирание сектора.
Пример 3 - ну совсем коротко
https://github.com/zoosmand/Winbond-W25Qxx-EEPROM-SPI-with-DMA-on-STM32
Просто ужас:
Инициализация портов микроконтроллера внутри библиотеки.
Макросы-команды памяти в заголовочном файле.
Невнятное описание функций.
Торчащие наружу переменные и константы.
А теперь чего хотелось:
Простой и понятный API. По сути нужен не драйвер микросхемы, а менеджер памяти с драйвером (и рыбку съесть, и … костью не подавиться).
Возможность легко использовать с другими библиотеками типа FatFs и usb msc. Следовательно нужны функции:
Чтения и записи страниц, и не обязательно внутри одного сектора
Получение статуса памяти (драйвера памяти) - понадобится для выше стоящих библиотек
Получение информации о размере, также будет нужно.
Возможность легко расширить для использования других похожих микросхем, что есть почти во всех реализациях, но без лишнего кода и действий.
Какие-нибудь элементы синхронизации при использовании RTOS: семафоры или мьютексы.
Без насильственных действий над памятью.
Быстрым чтением и записью. Быстрее и ещё быстрее.
Приступаем к велосипедостроению
Приведу сразу весь заголовочный файл. Он не содержит ничего лишнего вроде списка команд, регистров или ещё какого-то мусора, только то, что нужно непосредственно пользователю (имеется ввиду программист).
mem.h
/**
************************************************************************************************
* @file mem.h
* @copyright Copyright (C) 2023
************************************************************************************************
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
/* ---------------------------------------------------------------------------------------------------------*/
typedef enum mem_status_t
{
MEM_OK = 0, // 0: Successful
MEM_ERROR, // 1: R/W Error
MEM_WRPRT, // 2: Write Protected
MEM_NOTRDY, // 3: Not Ready / Busy
MEM_PARERR // 4: Invalid Parameter
} mem_status_t;
typedef enum mem_ioctl_cmd_t
{
MEM_IOCTL_CTRL_SYNC = 0, // Complete pending write process
MEM_IOCTL_PAGES_COUNT, // Get page size
MEM_IOCTL_PAGE_SIZE, // Get page size
MEM_IOCTL_SECTOR_SIZE, // Get erase block size
MEM_IOCTL_BURN, // Write chache to memory
} mem_ioctl_cmd_t;
/* ---------------------------------------------------------------------------------------------------------*/
mem_status_t mem_init();
mem_status_t mem_status();
mem_status_t mem_read_page(uint8_t* buff, uint32_t page, size_t count);
mem_status_t mem_write_page(uint8_t* buff, uint32_t page, size_t count);
mem_status_t mem_ioctl(mem_ioctl_cmd_t cmd, void* data);
mem_status_t mem_deinit();
/* ---------------------------------------------------------------------------------------------------------*/
Интерфейс есть, пора перейти к его реализации. Но для начала стоит разобраться, как всё это должно работать.
Важное замечание. Данная память устроена так, что, если хочешь изменить один байт, нужно переписывать весь сектор. Неожиданно, да? На самом деле нет. Однако...
Стирать можно минимум сектор (4 килобайта), писать - максимум страницу (256 байт). Читать можно сколько угодно, но нам это и не важно.
Следовательно нужен буфер размером с сектор.
Типовой алгоритм должен выглядеть следующим образом, это есть в одном из примеров:
Читаем весь сектор
Модифицируем буфер
Стираем сектор
Записываем буфер в сектор.
Что здесь плохо?
Пример: хотим мы записать 10 страниц с помощью этого алгоритма и будем записывать их не сразу, а по одной, не важно подряд или в хаотичном порядке. В итоге мы 10 раз сотрём и перезапишем сектор. Неслабо, да?
Как сделать лучше?
Для начала можно начать писать сразу массивом страниц: 1 операция стирания и несколько операций записи, что уже ускорит работу и облегчит жизнь микросхеме.
Также можно попробовать кэшировать изменения. Переписывать сектор только если он меняется: работали с одним сектором, перешли к работе с другим сектором, при переходе провели запись данных.
В итоге линейный алгоритм приобретает чуть большую сложность за счет ветвления.
Для уменьшения количества перезаписей необходимо ввести флаг был ли модифицирован кэш. В итоге в ветке "ДА" блок с записью пойдет через ещё одно условие.
Но и здесь есть свои минусы: если всё время работать с одним и тем же сектором и никогда его не сохранять, то при обрыве питания устройства можно потерять все данные, что намодифицировали. Чтобы этого избежать, необходимо периодически выполнять запись кэша в память. Как часто - ХЗ, это компромисс между насилием и страхом потери.
Разберём небольшой пример.
На изображении показано, что записываемые данные должны располагаться в 4..7 и 0,1 страницах первого и второго секторов соответственно. Необходимо сначала обработать страницы первого сектора, затем второго, а по пути сделать одну перезапись и ещё одну уже потом.
Функции чтения и записи почти идентичны. Взглянем на их код.
mem_status_t mem_read_page(uint8_t* buff, uint32_t page, size_t count)
{
mem_mux_take();
mem_status_t status = mem_operation(buff, page, count, false);
mem_mux_give();
return status;
}
mem_status_t mem_write_page(uint8_t* buff, uint32_t page, size_t count)
{
mem_mux_take();
mem_status_t status = mem_operation(buff, page, count, true);
mem_mux_give();
return status;
}
Функции mem_mux_take()
и mem_mux_give()
захватывают и освобождают объект синхронизации. Так как это не везде нужно объявил их как weak
, в реализации по умолчанию они пустые, а при сборке кода будут выпилены.
void __attribute__((weak)) mem_mux_create() {}
void __attribute__((weak)) mem_mux_take() {}
void __attribute__((weak)) mem_mux_give() {}
Основная функция, которая оперирует с данными, представлена ниже:
static mem_status_t mem_operation(uint8_t* buf, uint32_t page, size_t count, bool write)
{
if (mem_is_init == 0) return MEM_ERROR;
if ((page + count) > (dev->flash_size / dev->page_size) || (buf == NULL)) return MEM_PARERR;
mem_status_t status = MEM_OK;
int32_t sectorn = page / dev->pages_in_sector; // number operating sector
for (size_t npage = 0; npage < count && status == MEM_OK;)
{
// if sector has changed
if (current_sector != sectorn) status = mem_update_cache(sectorn);
// page number in sector
uint32_t pn_in_sector = page & (dev->pages_in_sector - 1);
// pages count for operation
uint32_t pagec_op = (count - npage);
pagec_op = (pn_in_sector + pagec_op) > dev->pages_in_sector //
? (dev->pages_in_sector - pn_in_sector) //
: pagec_op;
size_t cahce_n = dev->page_size * pn_in_sector; // byte number of cahce
size_t buff_n = dev->page_size * npage; // byte number of buffer
size_t data_size = dev->page_size * pagec_op; // bytes quantity for operation
if (write)
{
sector_modify = true;
memcpy(&mem_cache[ cahce_n ], &buf[ buff_n ], data_size);
}
else
{
memcpy(&buf[ buff_n ], &mem_cache[ cahce_n ], data_size); //
}
// pages of next sector if nessary
page += pagec_op;
npage += pagec_op;
sectorn++;
}
return status;
}
Заодно посмотрим и на функцию работы с кэшем. Это единственный буфер который используется в коде.
static volatile int32_t current_sector = -1;
static volatile bool sector_modify = false;
static uint8_t mem_cache[ 1024 * 4 ] = {0};
static mem_status_t mem_update_cache(int32_t sector)
{
mem_status_t status = MEM_OK;
if (sector_modify && (current_sector != -1))
{
status = mem_erase_sector(current_sector * dev->sector_size);
for (size_t i = 0; i < dev->pages_in_sector && status == MEM_OK; i++)
{
uint32_t page_offset = i * dev->page_size;
if (mem_write_data((current_sector * dev->sector_size) + page_offset, // address
mem_cache + page_offset, // data
dev->page_size) != MEM_OK)
{
status = MEM_ERROR;
break;
}
}
sector_modify = false;
}
if (status == MEM_OK && (current_sector != sector))
{
current_sector = sector;
status = mem_read_data(sector * dev->sector_size, mem_cache, dev->sector_size);
}
return status;
}
Функции для непосредственного взаимодействия с микросхемой памяти (чтение и запись) также довольно тривиальны и почти как у всех.
Адрес разворачивается типичной для ARM инструкцией REV, которую можно заменить на 4 строки с раскладыванием переменной адреса в массив по байтам в нужном порядке или цепочкой сдвигов и дизъюнкций.
static mem_status_t mem_read_data(uint32_t addr, uint8_t* data, size_t len)
{
if (addr > dev->flash_size || data == NULL) { return MEM_PARERR; }
mem_status_t status = MEM_OK;
spi_cs_activate();
{
uint32_t cmd = __REV(addr) | MEM_CMD_READ_DATA;
if (spi_tx(&cmd, sizeof(cmd)) != SPI_OK || // send address
spi_rx(data, len) != SPI_OK) // read data
{
status = MEM_ERROR;
}
}
spi_cs_deactivate();
return status;
}
static mem_status_t mem_write_data(uint32_t addr, uint8_t* data, size_t len)
{
if (addr > dev->flash_size || data == NULL || len > 256) { return MEM_PARERR; }
// needed before write
mem_status_t status = mem_send_cmd(MEM_CMD_WRITE_EN);
spi_cs_activate();
{
uint32_t cmd = __REV(addr) | MEM_CMD_WRITE_PAGE;
if (status != MEM_OK || // write enable
spi_tx(&cmd, sizeof(cmd)) != SPI_OK || // send address
spi_tx(data, len) != SPI_OK) // write data
{
status = MEM_ERROR;
}
}
spi_cs_deactivate();
status = mem_wait_rdy(dev->timeout);
return status;
}
Добавление альтернативных микросхем.
Нужно лишь вписать инициализированную структуру в массив, остальное выполниться автоматически:
static const mem_dev_t dev_table[] = {
// ZD25Q64B
{
.manufact_id = 0xBA,
.device_id = 0x16,
.capacity_id = 0x17,
.memory_id = 0x32,
//
.flash_size = 0x800000,
.page_size = 0x100,
.sector_size = 0x1000,
.pages_in_sector = 16,
.timeout = 100,
},
};
Тип этой структуры определен в том же файле "mem.c", что исключает возможность просачивания типа во внешнюю среду.
typedef struct mem_dev_t
{
uint8_t manufact_id; // Manufacturer ID
uint8_t device_id; // Device ID
uint8_t capacity_id; // Capacity ID
uint8_t memory_id; // Memory ID
//
uint32_t flash_size; // Total size, bytes
uint16_t page_size; // Min program size, bytes
uint16_t sector_size; // Min erase size, bytes
uint16_t pages_in_sector; // Pages quantity in sector, pieces
uint16_t timeout; // Operation timeout, ms
} mem_dev_t;
А что на счет скорости и всего такого?
Для начала предлагаю рассмотреть пример с рисунка выше, где записывался массив данных:
2 сектора
В каждом секторе по 8 страниц
Данные начинается с 4 страницы первого сектора, а заканчиваются в 1 странице второго сектора.
В итоге полное количество страниц, с которыми оперируем, будет равно 6 (4 в первом + 2 во втором).
Вариант 1. По одной странице за раз.
На запись одной страницы выходит 8 циклов записи (w) и 1 цикл стирания (e), циклы чтения(r) почти бесплатны, но посчитаем тоже - 8. В итоге имеем следующее выражение.
Итого без использования кэша получаем время равное 6x (зависит от количества страниц), с кэшем - 2x (от количества секторов).
Неплохо, да?
Может показаться, что, улучшив алгоритм без кэша, чтобы писать сразу блок страниц внутри сектора, мы приблизимся к тем же затратам, что и с кэшем - 2x. Можно, однако это лишь один из вариантов использования.
Вариант 2. Внутри одного сектора.
В таком случае временны́е затраты без кэша будут равны Nx, c кэшем - 1x.
В худшем случае, когда пишем каждый раз в новый сектор, алгоритм с кэшированием по затрачиваемому времени будет равен алгоритму без кэша. Так почему бы его не использовать раз уже выделили временный буфер под программу перезаписи данных в микросхеме?
Недоработки.
Внимательный читатель наверняка заметил, что упор сделан на запись данных, а чтение стало ещё медленнее. Даже если нам нужна одна страница из сектора, приходится читать весь сектор целиком, что не очень хорошо.
Это действительно так. Признаюсь, осознание того, как это сделать красиво, пришло только во время набора этого текста.
Есть куда расти.
Вместо выводов
Реализованный API прост и понятен даже без комментариев (моё личное мнение)
Из модуля наружу ничего не торчит (макросы, типы, переменные и функции)
Сохранен ресурс микросхемы по максимуму (с некоторыми оговорками)
Скорость взаимодействия увеличена до приемлемого максимума.
Но есть нюанс ?: при отправке команд разрешения записи, стираний и самой записи необходимо после поднятия чип селекта (CS) подождать некоторое время перед следующей командой, такой как запрос статуса занятости. Пока это реализовано с помощью
HAL_Delay
. Знаю, что это тупо, учитывая скорость общения равной 18 мегабитам, но пока не удалось придумать нормальную реализацию.Хотя была мысль без прижатого CS вхолостую прогнать несколько байтов по шине SPI, тем самым получив небольшую задержку, но это работало не стабильно, возможно стоит увеличить посылку.
Возможно кто-то подскажет как обойти эту проблему.
Код не зависит от других библиотек типа HAL и тому подобных. За одним исключением: задержка, которую можно сделать иначе, хочу в это верить.
В принципе, на этом всё, приготовился к критике и вопросам.
Весь код можно посмотреть здесь https://gitlab.com/devprodest/m25q64_mem_mng
Описание стилистики оставим на следующий раз, если будет интересно узнать очередное мнение очередного чувака из этих ваших интернетов.