Raspberry Pi Pico で littlefs

Raspberry Pi Pico を使ってラジオのアプリを開発しています。以前は MicroPython を使用していたのですが、最近は Raspberry Pi Pico の pico-sdk で開発するようになりました。Arduino IDE / PlatfoemIO を使用してもいいのですが、マイコンチップの理解度を高めたかったので pico-sdk でやっています。ラジオのアプリケーションでは、受信していた周波数や音量などの設定を保存したいものです。そのほかにも放送局のプリセット情報なども該当します。MicroPython でやってた頃は MicroPython のローカルストレージにそれらの情報を簡単に保存でき、PC の開発ツール thonny ide などからファイル転送などが簡単に行えましたが、pico-sdk でやる場合ローカルストレージのライブラリを組み込む必要があります。

マイコンのローカルストレージ定番は littlefs ということになります。MicroPython でもそうですし、Arduino / PlayformIO 環境でもサポートされています。これを pico-sdk から使用する時にあれこれやったメモ

Windows 側開発環境

私の場合 MSYS2 Mingw64 環境で開発しています。Mingw64 の環境に pico-sdk / picotool / picoprobe / pico-project-generator など Raspberry Pi github で公開されているツールを git clone して pico-sdk の環境変数の設定、また、mingw-w64-x86_64-arm-none-eabi-gcc などのコンパイル環境のインストールなどは済ませて、picotool などが build 出来ることを確認します。

Raspberry Pi 公式で Windows 版開発環境のインストーラが公開されていますが、そんなの公開される前に開発環境構築は終わっていて、今更移行するのが面倒なため Mingw64 でやってる次第です。

Windows 版 vscode を Mingw64 のプロンプトから起動できるよう
PATH 環境変数に /c/Users/ユーザー名/AppData/Local/Programs/Microsoft VS Code を .bashrc に追記。また、
alias vscode='code 1>/dev/null 2>/dev/null'
を .bashrc に追記し vscode というコマンド名でカレントディレクトリを開く状態で起動するようにしています。
$ cd プロジェクトのフォルダー
$ vscode . &

vscode が Mingw64 の環境変数を引き継ぐので、コンパイラパスの設定とかが楽になります。

openocd

https://github.com/openocd-org/openocd

openocd v0.12.0 からは Raspberry Pi Pico もマージされたかして使えるようになっています。Raspberry Pi github の openocd v0.11 は使わなくてもいいと思います。

これも git clone してビルドしてください。

$ git clone https://github.com/openocd-org/openocd.git
$ cd openocd
$ ./bootstrap
$ ./configure --prefix=/usr/local

configure の終了時にサポートされるインターフェースが yes で表示されます。cmsis-dap に yes が付いてれば picoprobe が使用可能になります。Msys2 パッケージの web で hidapi を探して pacman コマンドで入れたらいけるはずです。もちろん libusb なども必要ですので入れます。ここら辺は公式サイトを参考にして下さい。

mklittlefs

マイコンのフラッシュ領域を使用してファイルシステムを構築するライブラリ。
Arduino IDE や PlatformIO では昔からサポートされています。

https://github.com/earlephilhower/mklittlefs

https://github.com/littlefs-project/littlefs

mklittlefs のダウンロード

$ git clone https://github.com/earlephilhower/mklittlefs.git
$ cd mklittlefs
$ git submodule update --init
Submodule 'littlefs' (https://github.com/ARMmbed/littlefs) registered for path 'littlefs'
Cloning into '/home/masahiro/work/mklittlefs/littlefs'...
Submodule path 'littlefs': checked out '6a53d76e90af33f0656333c1db09bd337fa75d23'

$ git log --oneline
6a53d76 (HEAD, tag: v2.5.1) Merge pull request #744 from littlefs-project/fix-fetchmatch-err-path

バージョン 2.5.1 がチェックアウトされますが、最新版を使用したいので master ブランチをチェックアウト
$ git checkout master
$ git log --oneline
f53a0cc (HEAD -> master, tag: v2.9.0, origin/v2, origin/master, origin/HEAD) Merge pull request #929 from littlefs-project/devel

littlefs バージョン 2.9.0 になった。

mklittlefs をビルド

$ cd ..
$ make
g++ -std=gnu++11 -Os -Wall -Wextra -Werror   -Itclap -Iinclude -Ilittlefs -I. -D VERSION=\"0.2.3-64-g6e2fa17\" -D LITTLEFS_VERSION=\"v2.9.0\" -D BUILD_CONFIG=\"\" -D BUILD_CONFIG_NAME=\"-generic\" -D __NO_INLINE__ -D LFS_NAME_MAX=32   -c -o main.o main.cpp
cc -std=gnu99 -Os -Wall -Wextra -Werror   -Itclap -Iinclude -Ilittlefs -I. -D VERSION=\"0.2.3-64-g6e2fa17\" -D LITTLEFS_VERSION=\"v2.9.0\" -D BUILD_CONFIG=\"\" -D BUILD_CONFIG_NAME=\"-generic\" -D __NO_INLINE__ -D LFS_NAME_MAX=32   -c -o littlefs/lfs.o littlefs/lfs.c
cc -std=gnu99 -Os -Wall -Wextra -Werror   -Itclap -Iinclude -Ilittlefs -I. -D VERSION=\"0.2.3-64-g6e2fa17\" -D LITTLEFS_VERSION=\"v2.9.0\" -D BUILD_CONFIG=\"\" -D BUILD_CONFIG_NAME=\"-generic\" -D __NO_INLINE__ -D LFS_NAME_MAX=32   -c -o littlefs/lfs_util.o littlefs/lfs_util.c
g++ main.o littlefs/lfs.o littlefs/lfs_util.o -o mklittlefs
strip mklittlefs
C:\msys64\mingw64\bin\strip.exe: 'mklittlefs': No such file
make: *** [Makefile:96: mklittlefs] Error 1

Error 1 が表示されますが mklittlefs.exe ファイルは出来ています。
エラーの原因は Mingw64 環境ではビルドした実行ファイル名は mklittlefs.exe ですが、Linux 環境でビルドしたら実行ファイル名 mklittlefs です。 .exe が付くため strip コマンドがファイルが見つからないエラーを出すだけです。

実行ファイルからデバッグ情報などを消すだけなので strip コマンドをやらなくても問題ありません。
$ cp mklittlefs.exe /usr/local/bin
などパスの通っている場所に実行ファイルを配置すれば完了です。

注意点としては、mklittlefs コマンドをデフォルトでビルドするとファイル名は 32 文字までです。main.cpp の #define で定義されています。

Raspberry Pi Pico 側に littlefs を組み込む場合、最新の littlefs のファイル名は 255 文字がデフォルトですが mklittle に合わせるためRaspberry Pi Pico 側のビルド環境のコンパイルオプションに -DLFS_NAME_MAX=32 を付けましょう。

CMakeLists.txt だと
add_compile_definitions(LFS_NAME_MAX=32)
を追加することで arm-non-eabi-gcc / arm-none-eabi-g++ 実行時に -DLFS_NAME_MAX=32 が付与されます。

お勧めしませんが lfs.h の #define LFS_NAME_MAX 255 を直接修正する方法もあります。

littlefs のイメージ作成

Raspberry Pi Pico のフラッシュメモリのパラメタは hardware/flash.h で定義されています。

  • FLASH_PAGE_SIZE 256
  • FLASH_SECTOR_SIZE 4096 (4k)
  • FLASH_BLOCK_SIZE 65536 (64k)

Raspberry Pi Pico のフラッシュメモリサイズは 2MB なので、64kB で割ると合計32ブロックあります。実行するプログラムのコードはフラッシュメモリの先頭側から書き込まれますがフラッシュメモリの後ろの方は大抵未使用領域となります。littlefs で確保する領域を 128k (BLOCK 30,31)としました。

$ mkdir temp
$ cd temp
littlefs に書き込むファイルを用意する
$ cd ..
$ mklittlefs -b 4096 -p 256 -s 131072 -c ./temp イメージファイル名
-s 131072 は 128kB を表します。
$ mklittlefs -l イメージファイル名
ファイル名が表示される

これで littlefs のイメージが作成されました。

イメージファイルを Raspberry Pi Pico のフラッシュ領域に書き込む

picotool を使用する場合は Raspberry Pi Pico の BOOTSEL を押しながらリセットすると windows に Rasoberry Pi Pico のストレージが接続されます。

$ picotool load -v -x イメージファイル名 -t bin -o 101e0000

picotool コマンドの load で littlefs のイメージが Raspberry Pi Pico のフラッシュ領域の後ろ 128k 分に書き込まれます。

openocd を使用する場合

$ openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "bindto 0.0.0.0"

openocd を起動します。そして、openocd のプロセスに TeraTerm などから localhost:3333 に接続します。接続が出来たら

Open On-Chip Debugger
>

という表示が出ます。

プロセッサを停止する
> halt
フラッシュメモリの確認(任意)
> flash banks
> flash list
> flash probe 0
> flash probe 1

書き込む領域を消去する
> flash erase_address 0x101e0000 0x20000

イメージファイルを書き込む
> flash write_bank 0 イメージファイル名 0x1e0000

イメージファイルをベリファイする
> flash verify_bank 0 イメージファイル名 0x1e0000

これで littlefs のイメージ書き込みできます。

書き込みとは逆に読み出しは read_bank で読み出します。
> flash read_bank 0 イメージファイル名 0x1e0000 0x20000

ついでに、プログラムも書きたい場合は
> program プロジェクト/build/プログラムファイル名.elf verify reset

Raspberry Pi Pico 側

https://github.com/lurk101/pico-littlefs

を参考にしました。ここのは古い littlefs 用なのでそのまま使えませんが、参考にはなります。Raspberry Pi Pico の littlefs コールバック関数サンプルだけは掲載しないと分からないと思いますので下記に乗せておきます。

/**
 * @file lfs_wrapper.c
*/
#include "pico/stdlib.h"
#include "hardware/flash.h"
#include "hardware/sync.h"
#include "lfs_wrapper.h"
#define LFS_NAME_MAX 32
#include "littlefs/lfs.h"

#define FS_SIZE ( 128 * 1024 )
const char* FS_BASE = (char*)(PICO_FLASH_SIZE_BYTES - FS_SIZE);

lfs_t lfs;
lfs_file_t file;;

static int pico_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void* buffer, lfs_size_t size);
static int pico_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void* buffer, lfs_size_t size);
static int pico_flash_erase(const struct lfs_config *c, lfs_block_t block);
static int pico_flash_sync(const struct lfs_config *c);
static int pico_lock(void);
static int pico_unlock(void);

// configuration of the filesystem is provided by this struct
// for Pico: prog size = 256, block size = 4096, so cache is 8K
// minimum cache = block size, must be multiple
const struct lfs_config pico_cfg = 
{
    // block device operations
    .read = pico_flash_read,
    .prog = pico_flash_prog,
    .erase = pico_flash_erase,
    .sync = pico_flash_sync,
#if LIB_PICO_MULTICORE
    .lock = pico_lock,
    .unlock = pico_unlock,
#endif
    // block device configuration
    .read_size = 1,
    .prog_size = FLASH_PAGE_SIZE,
    .block_size = FLASH_SECTOR_SIZE,
    .block_count = FS_SIZE / FLASH_SECTOR_SIZE,
    .cache_size = FLASH_SECTOR_SIZE / 4,
    .lookahead_size = 32,
    .block_cycles = 500
};

static int pico_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset, void* buffer, lfs_size_t size)
{
    memcpy(buffer, FS_BASE + XIP_BASE + (block * pico_cfg.block_size) + offset, size);
    return LFS_ERR_OK;
}

static int pico_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset, const void *buffer, lfs_size_t size)
{
    uint32_t p = (uint32_t)FS_BASE + (block * pico_cfg.block_size) + offset;
    uint32_t ints = save_and_disable_interrupts();
    flash_range_program(p, (uint8_t *)buffer, size);
    restore_interrupts(ints);
    return LFS_ERR_OK;
}

static int pico_flash_erase(const struct lfs_config *c, lfs_block_t block)
{
    uint32_t p = (uint32_t)FS_BASE + block * pico_cfg.block_size;
    uint32_t ints = save_and_disable_interrupts();
    flash_range_erase(p, pico_cfg.block_size);
    restore_interrupts(ints);
    return LFS_ERR_OK;
}

static int pico_flash_sync(const struct lfs_config *c)
{
    asm("NOP");
    return LFS_ERR_OK;
}

#if LIB_PICO_MULTICORE
static recursive_mutex_t fs_mtx;
static int pico_lock(void) {
    recursive_mutex_enter_blocking(&fs_mtx);
    return LFS_ERR_OK;
}

static int pico_unlock(void) {
    recursive_mutex_exit(&fs_mtx);
    return LFS_ERR_OK;
}
#endif

ls コマンドの例です。どこかから拾ってきました。lfs_ls(“/”); のようにつかいますとシリアルポートにファイル一覧が出ます。

int lfs_ls(const char *path)
{
    lfs_dir_t dir;
    int err = lfs_dir_open(&lfs, &dir, path);
    if (err)
    {
        return err;
    }

    struct lfs_info info;
    while (true)
    {
        int res = lfs_dir_read(&lfs, &dir, &info);
        if (res < 0)
        {
            return res;
        }

        if (res == 0)
        {
            break;
        }

        switch (info.type)
        {
            case LFS_TYPE_REG: printf("reg "); break;
            case LFS_TYPE_DIR: printf("dir "); break;
            default:           printf("?   "); break;
        }

        static const char *prefixes[] = {"", "K", "M", "G"};
        for (int i = sizeof(prefixes)/sizeof(prefixes[0])-1; i >= 0; i--) 
        {
            if (info.size >= (1 << 10*i)-1)
            {
                printf("%*u%sB ", 4-(i != 0), info.size >> 10*i, prefixes[i]);
                break;
            }
        }

        printf("%s\n", info.name);
    }

    err = lfs_dir_close(&lfs, &dir);
    if (err) {
        return err;
    }

    return 0;
}