Как стать автором
Обновить

Очередной драйвер SPI флэшек… Но уже с кэшем и «нормальным» api

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров7.7K

Прочитал на Хабре кучу простых, и даже очень, статеек на тему программирования микроконтроллеров, тоже решил добавить что-то простое, понятное, но чуть более полезное.

Да простят меня бывалые разработчики за сей труд, да и хейтеры пусть прощают тоже?

О чём пойдет речь

Будем размышлять и пытаться писать быстрый драйвер флешки и при этом попробуем сэкономить её ресурс при перезаписях.

Предыстория и проблемы возможных реализаций

Понадобилась SPI флэшка для хранения данных, которыми оперирует встраиваемое ПО микроконтроллера. Типовая задача, если не хватает места внутри основного чипа.

Объем памяти нужен был небольшой, но приличный, что-то в районе 8 мегабайт.

Как и все нормальные программисты, во избежание изобретения велосипеда, полез искать в сети (github, gitlab) в итоге ничего не приглянулось.

Основные проблемы найденных вариантов:

  1. Все реализации предоставляют только базовые функции записи и чтения по страницам и секторам.

  2. Нет реализаций, когда нужно считать или записать страницы, диапазон которых разбросан на два или более секторов, а некоторые не позволяют даже с несколькими страницами в одном секторе провзаимодействовать.

  3. Кривая по тем или иным причинам реализация.

  4. Реализация в лоб. Необходимо ещё свою обертку писать с нормальным взаимодействием.

  5. Плохой стиль/архитектура:

    1. extern везде, где нужно и не нужно

    2. Куча макросов, где можно обойтись без них, ещё и получить бóльший профит.

    3. Вытаскивание наружу того, что должно оставаться внутри модуля, — торчащие глобальные переменные и функции, которые используются только внутри и никак не описаны в файле заголовка.

Приведу несколько примеров того, что не понравилось.

Пример 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

Просто ужас:

  1. Инициализация портов микроконтроллера внутри библиотеки.

  2. Макросы-команды памяти в заголовочном файле.

  3. Невнятное описание функций.

  4. Торчащие наружу переменные и константы.

Примерные чувства
Примерные чувства

А теперь чего хотелось:

  1. Простой и понятный API. По сути нужен не драйвер микросхемы, а менеджер памяти с драйвером (и рыбку съесть, и … костью не подавиться).

  2. Возможность легко использовать с другими библиотеками типа FatFs и usb msc. Следовательно нужны функции:

    1. Чтения и записи страниц, и не обязательно внутри одного сектора

    2. Получение статуса памяти (драйвера памяти) - понадобится для выше стоящих библиотек

    3. Получение информации о размере, также будет нужно.

  3. Возможность легко расширить для использования других похожих микросхем, что есть почти во всех реализациях, но без лишнего кода и действий.

  4. Какие-нибудь элементы синхронизации при использовании RTOS: семафоры или мьютексы.

  5. Без насильственных действий над памятью.

  6. Быстрым чтением и записью. Быстрее и ещё быстрее.

Приступаем к велосипедостроению

Приведу сразу весь заголовочный файл. Он не содержит ничего лишнего вроде списка команд, регистров или ещё какого-то мусора, только то, что нужно непосредственно пользователю (имеется ввиду программист).

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 байт). Читать можно сколько угодно, но нам это и не важно.

Следовательно нужен буфер размером с сектор.

Типовой алгоритм должен выглядеть следующим образом, это есть в одном из примеров:

  1. Читаем весь сектор

  2. Модифицируем буфер

  3. Стираем сектор

  4. Записываем буфер в сектор.

Что здесь плохо?

Пример: хотим мы записать 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. В итоге имеем следующее выражение.

x = 8r + 8w + 1e

Итого без использования кэша получаем время равное 6x (зависит от количества страниц), с кэшем - 2x (от количества секторов).

Неплохо, да?

Может показаться, что, улучшив алгоритм без кэша, чтобы писать сразу блок страниц внутри сектора, мы приблизимся к тем же затратам, что и с кэшем - 2x. Можно, однако это лишь один из вариантов использования.

Вариант 2. Внутри одного сектора.

В таком случае временны́е затраты без кэша будут равны Nx, c кэшем - 1x.

Ожидаю полного тест памяти без кэша.
Ожидаю полного тест памяти без кэша.

В худшем случае, когда пишем каждый раз в новый сектор, алгоритм с кэшированием по затрачиваемому времени будет равен алгоритму без кэша. Так почему бы его не использовать раз уже выделили временный буфер под программу перезаписи данных в микросхеме?

Недоработки.

Внимательный читатель наверняка заметил, что упор сделан на запись данных, а чтение стало ещё медленнее. Даже если нам нужна одна страница из сектора, приходится читать весь сектор целиком, что не очень хорошо.

Это действительно так. Признаюсь, осознание того, как это сделать красиво, пришло только во время набора этого текста.

Есть куда расти.

Вместо выводов

  1. Реализованный API прост и понятен даже без комментариев (моё личное мнение)

  2. Из модуля наружу ничего не торчит (макросы, типы, переменные и функции)

  3. Сохранен ресурс микросхемы по максимуму (с некоторыми оговорками)

  4. Скорость взаимодействия увеличена до приемлемого максимума.

    Но есть нюанс ?: при отправке команд разрешения записи, стираний и самой записи необходимо после поднятия чип селекта (CS) подождать некоторое время перед следующей командой, такой как запрос статуса занятости. Пока это реализовано с помощью HAL_Delay. Знаю, что это тупо, учитывая скорость общения равной 18 мегабитам, но пока не удалось придумать нормальную реализацию.

    Хотя была мысль без прижатого CS вхолостую прогнать несколько байтов по шине SPI, тем самым получив небольшую задержку, но это работало не стабильно, возможно стоит увеличить посылку.

    Возможно кто-то подскажет как обойти эту проблему.

  5. Код не зависит от других библиотек типа HAL и тому подобных. За одним исключением: задержка, которую можно сделать иначе, хочу в это верить.

В принципе, на этом всё, приготовился к критике и вопросам.

Весь код можно посмотреть здесь https://gitlab.com/devprodest/m25q64_mem_mng

Описание стилистики оставим на следующий раз, если будет интересно узнать очередное мнение очередного чувака из этих ваших интернетов.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Попробовать описать свои мысли по архитектурно-стилевому оформлению сишного кода?
83.64% Пиши46
16.36% Нет, полно уже таких9
Проголосовали 55 пользователей. Воздержались 7 пользователей.
Теги:
Хабы:
Всего голосов 19: ↑18 и ↓1+22
Комментарии33

Публикации

Истории

Работа

Программист С
37 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань