Использование LIttleFS совместно с параллельной NAND памятью

Обычно для удобства работы с различными носителями информации используются различные файловые системы. Основное удобство заключается в абстрагировании от особенностей физического исполнения носителя ( как размер страницы, блока и т.д) и сосредоточиться на работе с полезной нам информацией. В данной статье пойдет речь о параллельной NAND Flash память и файловой системе LittleFS.

До недавнего времени для встроенных систем на основе микроконтроллеров и предназначенных для работы с NAND чипами мне была известна, только Yaffs. Про эту файловую систему есть отличная серия статей. Основной недостаток Yaffs, на мой взгляд, это высокие требования к объему ОЗУ. По этой причине мне даже не удавалось ее протестировать, т.к. у меня нет плат у которых на борту были бы SDRAM и NAND память. LittleFS лишена, по крайней мере, этого недостатка  и мне удалось ее запустить ее и немного протестировать.

Заявленные характеристики LittleFS:

  • специально разработана для работы с блочными устройствами памяти
  • устойчивость к пропаданию питания
  • динамический контроль износа
  • работа с ограниченным объемом ОЗУ/ПЗУ.

Подготовка «железа».

Для тестов буду использовать проект под плату Waveshare и модуль с установленной памятью K9F1G08UOE объемом 128 Мб. Для вывода отладочной информации используется UART3. CubeMX сгенерировал основу проекта, для доступа к флешь памяти использую интерфейс FSMC со следующими параметрами:

Настройки FSMC

Здесь нечего описывать, но пару слов все же сказать стоит. С пересчетом таймингов указанных в настройках FSMC в реальные единицы времени я не стал заморачиваться, а подобрал опытным путем. Эти тайминги актуальны для тактовой частоты шины AHB 168 МГц. Также в проекте необходимо настроить пин управления чип селектом микросхемы памяти (CS) в моем случаи это PD13. Вся его настройка — это выход с постоянным уровнем логического нуля.

Есть еще одна особенность характерная именно для моей комбинации устройств это выходы ALE/CLE. Дело в том, что они перепутаны местами, эту проблему можно легко обойти софтовым способом. Нужно отредактировать дефайны в файле stm32f4xx_hal_nand.h таким образом:

#define CMD_AREA                   ((uint32_t)(1U<<17U))  /* A16 = CLE high */
#define ADDR_AREA                  ((uint32_t)(1U<<16U))  /* A17 = ALE high */

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

Для проверки, функционирования памяти, можно запустить функцию test_nand_flash(). Там происходит стирание блока, чтение по нулевому адресу, проверка, что все данные равны 0xFF. Следующий шаг — проверка запись/чтение четырех страниц. Если тест проходит без ошибок, то можно считать, что флешь память работает корректно.

Имплементация LittleFS и простой тест.

Когда функции чтения/записи и стирания флешь памяти отлажены, можно приступить к имплементации LittleFS. Делается это очень просто, мы просто должны заполнить структуру lfs_config. Подробное описание ее полей есть в файле lsf.h. Названия полей говорят сами за себя, поэтому опишу самое основное.

В моем тестовом проекте заполнение структуры lfs_config происходит в функции io_fs_init() в файле io_fs.c.

int io_fs_init(void)
{
    uint32_t page_size = io_nand_get_page_size();

    _lfs_config.read_size   = page_size;
    _lfs_config.prog_size   = page_size;
    
    _lfs_config.block_size  = io_nand_get_block_size() * page_size;
    _lfs_config.block_count = io_nand_get_block_number();
    
    _lfs_config.lookahead_size = page_size;
    _lfs_config.cache_size     = page_size;
    
    _lfs_config.read_buffer = _rd;
    _lfs_config.prog_buffer = _wr;

    _lfs_config.read   = _fs_flash_read;
    _lfs_config.prog   = _fs_flash_prog;
    _lfs_config.erase  = _fs_flash_erase;
    _lfs_config.sync   = _fs_flash_sync;

    return 0;
}

read/prog size — минимальное кол-во байт на чтение/запись, должно быть больше или равно размеру странице.

block_size — размер блока в байтах. Блок это минимальный объем флешь памяти, который возможно стереть.

block_count — общее кол-во блоков флешь памяти т.е. вообще всего с учетом кол-ва плейнов (Plane).

lookahed_size — размер буфера, необходимого для хранения данных о блоках, в которые можно разместить данные. Должен быть кратным числу 4.

cache_size — размер буфер под кэш. Увеличение этого буфер приводит к увеличению производительности LittleFS, за счет уменьшения кол-ва обращений к носителю. Должен быть кратен read/prog size.

read/prog_buffer —  опциональные буферы для чтения/записи. Удобно их создавать  для отладки т.к. видно содержимое страницы. Должны быть равны cache_size.

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

int _fs_flash_prog(  const struct lfs_config *cfg, lfs_block_t block,
                    lfs_off_t off, const void *buffer, lfs_size_t size)
{

    assert(off  % cfg->prog_size == 0);
    assert(size % cfg->prog_size == 0);
    assert(block < cfg->block_count);  

    uint32_t addr = block * io_nand_get_block_size() + off / io_nand_get_page_size();
    io_nand_write_8b(addr, (uint8_t*) buffer, size);
  
    return 0;
}

Особенностью этой функции можно считать лишь пересчет адреса блока  и смещения в адрес совместимый с функцией io_nand_write_8b(uint32_t addr, uint8_t *buffer, uint32_t size, uint32_t offst). Данная функция использует линейную адресацию станиц флешь памяти. Например, addr == 168 будет равен Plane == 0 | Block ==2 | Page == 40 .  Если еще необходимо смещение в байтах от начала страницы, то нужно отправить аргумент offst.

В функцию _fs_flash_prog() LittleFS будет отправлять номер блока и смещение в байтах, причем off будет всегда кратно размеру страницы, т.е. это по сути её адрес. Поэтому нам просто нужно block и off привести к линейной страничной адресации.

Теперь когда мы заполнили структуру lfs_config можно попробовать запустить пример.

    io_fs_file file;
    // инциализация файловой системы
    volatile int32_t err = io_fs_init();
    
    err = io_fs_mount();
    if (err < 0) // если есть ошибка монтирования    
    {
        // то произвести форматирование
        printf("Format...\r\n");
        err = io_fs_format();
        err = io_fs_mount();
    }
    
    // прочитать текущее значение кол-ва загрузок
    uint32_t boot_count = 0;
    err = io_fs_file_open(&file, "boot_count", IO_FS_O_RDWR | IO_FS_O_CREAT);
    err = io_fs_file_read(&file, &boot_count, sizeof(boot_count));

    // обновить счетчик загрузок
    boot_count += 1;
    // перейти в начало файла
    err = io_fs_file_rewind(&file);
    // произвести запись
    err = io_fs_file_write(&file, &boot_count, sizeof(boot_count));

    // закрыть файл, окончательно производится дозапись на носитель
    err = io_fs_file_close(&file);

    // размонтировать файловую систему
    err = io_fs_unmount();

    // печать значения счетчика включений
    printf("boot_count: %d\n", boot_count);

Подключившись терминалом к UART3 можно увидеть значение счетчика включений. Для его инкремента достаточно перезагрузить плату. Если счетчик адекватно инкрементируется и в терминал не выводится никаких других сообщений, то можно себя поздравить LittleFS успешно запущена.

Тест скорости.

Пора произвести измерения скорости чтения/записи, а также, как дополнительный тест проверить работу файловой системы с относительно большим файлом. Сам тест находится в main.c, а дефайны определяющие его поведение в самом начале файла. В данном тесте проводится время записи, после форматирования, повторная запись с самого начала файла, а также измерение времени записи. Временные интервалы я измерял с помощью осциллографа, размер исследуемого файла 64 кБ.

Результаты:

  • первая запись 101,4 мс
  • повторная запись 101 мс
  • чтение 16,15 мс

Как видно первая и повторная запись занимает одинаковое время.

Ниже приведены соответствующие осциллограммы.

Первая запись

Повторная запись

Чтение

 

 

 

 

 

 

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

Тестовый проект

LttleFS