В этой статья я подготовил обзор широко известной и доступной в продаже микросхемы Flash NAND памяти GD5F1GQ5xExxG. Согласно маркировке, это микросхема энергонезависимой NAND-памяти GigaDevice емкостью 128 МБ с доступом по интерфейсу SPI. Во второй части я расскажу, как установить на эту микросхему LittleFS — отказоустойчивую файловую систему, разработанную специально для микроконтроллеров.

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

Постановка задачи

  1. Написать на C драйвер для микросхемы GD5F1GQ5xExxG. Научиться читать, писать и стирать массивы байтов по абсолютному адресу. Научиться читать регистры микросхемы GD5F1GQ5xExxG, устанавливать защиту на запись. Обеспечить чтение идентификаторов (ID, Unique ID и страницу параметров). Должна быть функция чтения блока по адресу, удаления блока по адресу.

  2. Запустить LittleFS на основе микросхемы GD5F1GQ5xExxG.

  3. Организовать NVRAM на основе LittleFS.

Теория

ECC-память (англ. error-correcting code memory, память с коррекцией ошибок) — тип компьютерной памяти, которая автоматически распознает и исправляет спонтанно возникшие изменения (ошибки) битов памяти.

Выровненный адрес — адрес, который может принимать значения с некоторым периодом начиная с нуля. Периоды выровненных адресов, как правило, это значения степеней двойки — например, 2048.

Third-party — код со стороны. Программный компонент, который вы скачали из интернета.

NVRAM — энергонезависимая память (NV) с произвольным доступом (RAM), по сути, Key Val-Map(-ка). В ней могут храниться любые бинарные данные, ассоциированные со своим ID числом.

Данные — массив hex-байтов с указанной длиной

File — это именованный бинарный массив байтов в памяти. В качества памяти может выступать RAM, ROM (Flash), FRAM, EEPROM, SD-карты, компакт-диски, дискеты, магнитная лента, стекло. Да все что угодно. Что значит именованный? Это значит, что к данным можно обращаться по значению. Пусть это будет натуральное 32-битное число. Так проще. Так как это массив, то, очевидно, рядом с данными также надо хранить и длину этого массива.

Аппаратная часть

Драйвер микросхемы я отлаживал на электронной плате с микроконтроллером STM32F4x. Прошивку собирал из C компилятором GCC. Сборка проекта происходила из написанных вручную Make-скриптов.

В широкую клетку ASIC можно представить на одной картинке.

Кеш-память равна 2 КБ, OPT — 8 КБ. Основная память — 128 МБ. На электрической цепи SPI-NAND может выглядеть так:

Распиновка корпуса микросхемы GD5F1GQ5xExxG простая. Подключение к микроконтроллеру происходит по полнодуплексному проводному синхронному высокоскоростному интерфейсу SPI.

Для соединения микроконтроллера и корпуса WSON8 по SPI потребуется макетная плата с шагом 1.27 мм.

Программная часть

Прежде всего в ПО надо корректно настроить параметры физического интерфейса SPI. Биты выхватываются по положительному перепаду на проводе тактирования. Выдаются по отрицательному перепаду на проводе SO. Передаются в режиме most significant bit first, то есть старшим битом вперед. Первый байт передает 8-битный код инструкции. Байты адреса передаются старшим байтом вперед (big endian).

Размер страницы — 2048 байт. Один блок — 64 страницы, 128 кБ. Стирается память только блоками по 128 кБ.

Параметры

Размер, кБ

Размер, Б

Сколько экземпляров

Сколько бит надо для адреса

Вся микросхема

131072

134217728

1

0

Блок

128

131072

1024

10

--

32

32768

4096

12

Страница

2

2048

65536

16

--

1

1024

131072

17

qword

0.0078125

8

16777216

24

Байт

0.00097656

1

134 217 728

27

Если мы хотим адресовать 128 МБ памяти, то адрес должен быть 27 бит. Если мы хотим адресовать 128 МБ памяти блоками по 2 кБ, то адрес должен быть 16 бит.

Про свой размер говорит сама микросхема, если прочитать ее Parameter Page:

Когда память очищена, там прописаны одни сплошные единицы 0xFF:

Регистры микросхемы GD5F1GQ5UEYIGR

Как и каждый сложный ASIC, микросхема GD5F1GQ5UEYIGR обладает внутренними регистрами, которые отражают мгновенный статус микросхемы. Значения регистров можно прочитать командой Get Features (код операции: 0FH).

Адрес регистра

Название регистра

Битовые поля

A0H

Protection

BRWD, BP, INV, CMP

B0H

Feature

OTP_PRT, OTP_EN, ECC_EN, BPL, QE

C0H

Status

ECCS, P_FAIL, E_FAIL, WEL, OIP

D0H

Feature

DS

F0H

Status

ECCSE, BPS

Как водится, статусные регистры надо периодически опрашивать и предупреждать надсистему о том, что возникли аварии: P_FAIL, E_FAIL и пр.

Как прочитать страницу?

  1. Переместить страницу в кеш (команда 13H). В кеш перемещается сразу вся страница, то есть размером 2048 байт. Страницы адресуются 24-битным адресом. То есть формально так можно адресовать 34359738368 байт, или 32 ГБ. Однако в нашем случае — 128 МБ и фактически задействованы будут только 16 бит.

  2. Чтение страницы происходит не мгновенно. Это требует tRD = 60 мкс. Надо ожидать окончания перемещения массива, читать status-регистр GET FEATURES 0FH и проверять флаг (OIP), пока не закончится процесс перемещения страницы.

  3. Прочитать страницу из кеша с помощью команды 03H или 0BH.

Как прописать страницу?

  1. Выполнить команду Program Load (код операции: 0x02). Аргументами является 12-битный адрес колонки и массив данных, которые надо прописать. Данные сначала помещаются в кеш-память, равную 2048 Б. Данные, выходящие за границу кеша, отбрасываются.

  2. Включить разрешение на запись. Для этого есть отдельная команда WRITE ENABLE (06H).

  3. Выполнить команду PROGRAM EXECUTE (10H). Эта команда инициирует перемещение массива из RAM кеш-памяти в главную энергонезависимую память. Команда получает в аргументах 24-битный адрес. Во время перемещения микросхема переходит в режим занятости на время, равное tPROG — 600 мкс.

  4. Ожидать окончания записи путем периодического чтения командой get features регистра состояния по адресу 0xC0. Сигналом окончания процесса записи является значение 1 в битовом поле OIP (Operation In Progress).

Экспериментально я заметил, что в этой SPI-NAND работает дозапись ячеек флеш-памяти! То есть можно прописать в байт значение 0xFE, затем прописать 0xFC, и в финале прочитается тоже 0xFC. Это позволяет использовать достаточно простые реализации NVRAM.

При этом обратное действие ожидаемо неверно. Если прописать 0xAA, а затем 0xFF, то прочитается то самое старое 0xAA. Это тоже, в общем, хороший признак, а то некоторые флеш-памяти при таких «нелегальных» действиях после чтения выдают вообще случайные значения.

Как стереть NAND-память?

Если чтение и запись NAND-памяти производится страницами, то стирание происходит блоками. Напомню, блок равен 128 кБ — это, между прочим, 64 страницы! Как стереть память:

  1. Отключиться от OTP памяти. Прописать командой set feature в регистре Featue B0H бит OTP_EN=0.

  2. Отменить защиту блока. Для этого командой set features надо прописать битовое поле BP в регистре PROTECTION.

  3. Отправить команду WRITE ENABLE для разрешения изменения содержимого памяти (06H).

  4. Отправить команду BLOCK ERASE (код операции D8H). Указать 24-битный адрес страницы, который потом сам преобразуется в адрес блока.

  5. Процедура стирания не мгновенная. Надо ожидать tBERS (10 мс) до окончания процедуры, опрашивая бит OIP статусного регистра командой GET FEATURES (0FH)

Отладка

SPI память хороша тем, что ее очень просто покрывать модульными тестами.

Удалось прочитать ID микросхемы:

В качестве модульного теста вы можете прочитать Parameter Page и проверить прочитанные значения параметров signature, bytes_per_page, io_capacitance, device_model, manufacturer, jedec_manufacturer_id с ожидаемыми по спецификации значениями:

В тестах следует попробовать включить и отключить OTP-память, режим записи:

Модульный тест на стирание:

Достоинства и недостатки GD5F1GQ5xExxG

+ Много памяти — 128 МБ. За мегабайт получается примерно 6 рублей.

+ Работает дозапись ячеек флеш-памяти. То есть можно прописать в байт значение 0xFE, затем прописать 0xFC, и в финале прочитается тоже 0xFC. Это позволяет использовать достаточно простые реализации NVRAM на такой памяти.

В NAND-флеш есть выбитые ячейки, и это считается нормальным в отгруженных чипах. Поэтому прошивка должна как-то разруливать эту ситуацию программно.

Нет команды стирания всей микросхемы.

Промежуточный результат

Мы написали драйвер для микросхемы GD5F1GQ5xEx. Чем мы обладаем на данном этапе? У нас есть три функции.

bool gd5f1gq5_read_page(const uint8_t num,
                        const uint32_t page_address,
                        uint8_t* const data,
                        const uint32_t size);
bool gd5f1gq5_page_program(const uint8_t num, 
                           const uint32_t address, 
                           const uint8_t* const data, 
                           const uint32_t size);
bool gd5f1gq5_erase_block(const uint8_t num, 
                          const uint32_t page_address);

Модульные тесты показывают, что эти функции работают.

Название функции

Пояснение

gd5f1gq5_read_page

Чтение страницы на 2048 Б

gd5f1gq5_page_program

Запись страницы на 2048 Б

gd5f1gq5_erase_block

Стирание блока 128 кБ

Однако этого все равно мало. Что, если нам потребуется записать по границе между страниц или, еще хуже, стереть данные на границе блоков? Насколько усложняется наш алгоритм вставки произвольных массивов?

Все эти проблемы давно решены при помощи файловых систем. Запуск файловых систем — это классическое решение при работе с сырым API в NAND флеш-памяти.

Запуск LittleFS на SPI-NAND памяти

Файловая система позволит полностью абстрагироваться от физического исполнения энергонезависимого хранилища с его особенностями — и просто из кода на C записывать данные как в обыкновенные файлы. Вы сможете создавать файлы любого размера, пополнять эти файлы новыми данными, удалять файлы по их имени. Все, как мы привыкли делать на ПК, только в микроконтроллере. Это позволит улучшить переносимость микроконтроллерных приложений между разными электронными платами с разной схемотехнической начинкой, так как API файловой системы везде будет одинаковый.

При конфигурации файловой системы надо сразу зарезервировать самый последний блок для модульных тестов. Чтобы прогон модульных тестов не портил файловую систему внутри NAND Flash.

#include "little_fs_config.h"

#include "gd5f1gq5.h"
#include "log.h"
#include "little_fs.h"

#ifdef LFS_NO_MALLOC
// Optional statically allocated read buffer. Must be cache_size.
// By default lfs_malloc is used to allocate this buffer.
static uint8_t StaticFileBuffer[GD5F1GQ5_PAGE_SIZE] = {0};

static uint8_t readBuffer[GD5F1GQ5_PAGE_SIZE] = {0};
static uint8_t progBuffer[GD5F1GQ5_PAGE_SIZE] = {0};
static uint8_t lookaheadBuffer[GD5F1GQ5_PAGE_SIZE] = {0};
#else
#error Using dynamic memory is not recommended in microcontrollers firmware.
#endif

static int nand_read_block(const struct lfs_config* c,
                           lfs_block_t block,
                           lfs_off_t off,
                           void* buffer,
                           lfs_size_t size) {
    int ret = 0;
    LittleFsHandle_t *Node = LittleFsGetNode(1);
    if(Node) {
        LOG_PARN(LITTLE_FS, "Read,Block:%u,OffSet:%u,Size:%u", block, off, size);
        uint32_t page_addr = block * GD5F1GQ5_BLOCK_SIZE + off;
        bool res = gd5f1gq5_read_page(Node->nand_num, 
                                      page_addr, 
                                      (uint8_t*) buffer, size);
        ret = little_fs_res_to_ret(res);
    }
    return ret;
}

static int nand_prog(const struct lfs_config* c,
                     lfs_block_t block,
                     lfs_off_t off,
                     const void* buffer,
                     lfs_size_t size) {
    int ret = LFS_ERR_CORRUPT;
    LOG_NOTICE(LITTLE_FS, "Write,Block:%u,OffSet:%u,Size:%u", block, off, size);
    LittleFsHandle_t* Node = LittleFsGetNode(1);
    if(Node) {
        uint32_t page_addr = block * GD5F1GQ5_BLOCK_SIZE + off;
        bool res = false;
        res = gd5f1gq5_page_program(Node->nand_num, page_addr, (const uint8_t*)buffer, size);
        ret = little_fs_res_to_ret(res);
    }
    return ret;
}

static int nand_block_device_erase(const struct lfs_config* c,
                                   lfs_block_t block) {
    int ret = LFS_ERR_CORRUPT;
    LOG_WARNING(LITTLE_FS, "EraseBlock:%u", block);
    LittleFsHandle_t* Node = LittleFsGetNode(1);
    if(Node) {
        uint32_t page_addr = block * GD5F1GQ5_BLOCK_SIZE;
        bool res = gd5f1gq5_erase_block(Node->nand_num, page_addr);
        ret = little_fs_res_to_ret(res);
    }
    return ret;
}

static int nand_block_device_sync(const struct lfs_config* c) {
    int ret = LFS_ERR_OK;
    LOG_DEBUG(LITTLE_FS, "Sync");
    return ret;
}

static struct lfs_attr attrs={
  .type = 0,  // 8-bit type of attribute, provided by user and used to identify the attribute
  .buffer = NULL, // Pointer to buffer containing the attribute
  .size = 0, // Size of attribute in bytes, limited to LFS_ATTR_MAX
};

const LittleFsConfig_t SECTION_CFG_DATA LittleFsConfig[] = {
        {
          .num = 1,
          .base_address = 0x0,
          .valid = true,
          .nand_num = 1,
          .name = "LittleFsOnGD5F1GQ5",
          // Optional configuration provided during lfs_file_opencfg
          .file_config = {
                // Optional statically allocated file buffer. Must be cache_size.
                // By default lfs_malloc is used to allocate this buffer.
               .buffer= StaticFileBuffer,

                // Optional list of custom attributes related to the file. If the file
                // is opened with read access, these attributes will be read from disk
                // during the open call. If the file is opened with write access, the
                // attributes will be written to disk every file sync or close. This
                // write occurs atomically with update to the file's contents.
                //
                // Custom attributes are uniquely identified by an 8-bit type and limited
                // to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller
                // than the buffer, it will be padded with zeros. If the stored attribute
                // is larger, then it will be silently truncated. If the attribute is not
                // found, it will be created implicitly.

                // Custom attribute structure, used to describe custom attributes
                // committed atomically during file writes.
               .attrs = &attrs,

                // Number of custom attributes in the list
               .attr_count=0,
          },
          .cfg = {
                 // block device operations
              .read = nand_read_block,
              .prog = nand_prog,
              .erase = nand_block_device_erase,
              .sync = nand_block_device_sync,
        #ifdef LFS_NO_MALLOC
              .read_buffer = readBuffer,
              .prog_buffer = progBuffer,
              .lookahead_buffer = lookaheadBuffer,
        #endif
              // block device configuration
              .read_size = GD5F1GQ5_PAGE_SIZE,
              .prog_size = GD5F1GQ5_PAGE_SIZE,
              .cache_size = GD5F1GQ5_PAGE_SIZE,
              .lookahead_size = GD5F1GQ5_PAGE_SIZE,
              .block_cycles = 500,
              .block_size = GD5F1GQ5_BLOCK_SIZE,
              .block_count = GD5F1GQ5_TOTAL_BLOCKS,
        },
    },
};

LittleFsHandle_t LittleFsInstance[] = { { .num = 1, .valid = true, }, };

COMPONENT_GET_CNT(LittleFs, little_fs)

Перед первым использованием API little_fs надо выполнить функции lfs_format и lfs_mount. Затем можно смело создавать, читать, писать и удалять текстовые и бинарные файлы разного размера.

Как можно заметить, в файловой системе в самом деле появились файлы.

Запуск NVRAM

Суть NVRAM в том, что она работает с параметрами. У каждого параметра есть множество свойств — метаданных. Это размер, единицы измерения, физическая величина, размерный множитель и прочее. При наличии файловой системы очень легко организовать NVRAM. Достаточно для каждого параметра создавать отдельный файл, где имя — это ID параметра, а содержимое — это сами данные. Метаданные же, как правило, известны до сборки проекта и хранятся прямо в коде программы.

пример диагностики параметров
Пример диагностики параметров

Иерархия памяти получается такой:

Приложения LittleFS

В LittleFS-файле можно хранить целую прошивку и обновлять ее в bootloader.

В файловой системе вы можете складировать текстовые логи работы каких-либо программных компонентов или даже всей прошивки. Можете сделать своеобразный черный ящик, благо памяти очень много.

В NVRAM можно хранить настройки тактирования, коэффициенты PLL, счетчик запусков, серийный номер, периоды сторожевого таймера, наработки на отказ и прочее. Изменять тактирование во время исполнения прошивки без необходимости пересборки всего проекта. Благодаря NVRAM не придется готовить множество прошивок с разными конфигурациями. Вы просто сделаете одну прошивку с NVRAM и будете подгружать туда специфические параметры для каждого отдельного клиента. Наличие NVRAM — это требование протокола UDS.

Результат

Мы научились пользоваться микросхемой памяти GD5F1GQ5xEx, написали функции, чтобы читать, писать и стирать регионы flash памяти. Также драйвер позволяет просматривать и менять конфигурационные регистры микросхемы GD5F1GQ5, просматривать параметры ASIC. Удалось завести файловую систему на SPI-NAND GD5F1GQ5 памяти. Пройти путь от сырых hex-байтов по физическим адресам до приятных и удобных именованных файлов. В финале удалось организовать NVRAM для хранения параметров.

Как вариант можете еще рассмотреть чип W25M02GV от компании WinBond.

Параметр

GD5F1GQ5xExxG

W25M02GV (W25M02GVZEIG )

Объем памяти, МБ

128

256 

Производитель

GigaDevice

WinBond

Примерная цена, руб.

770

744

Полезные ссылки

Словарь аббревиатур

  • ASIC — Application-Specific Integrated Circuit

  • JEDEC — Joint Electron Device Engineering Council

  • WSON — Plastic Small-outline No-lead Package

  • MSB — Most Significant Bit first

  • WSON — Plastic Small-outline No-lead Package

  • VCC — Voltage at the Common Collector

  • OTP — One-Time Programmable

  • NVB — minimum number of valid blocks

  • WP — Write Protect

  • ECC — error-correcting code

  • SPI — Serial Peripheral Interface

  • DTR — Double Transfer Rate

Комментарии (0)