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;
}

Raspberry Pi Pico で Si4735 DSP ラジオ製作

最近は ATMEGA328P 終了の兆しが見えてきて、もうちょっとフラッシュやメモリの大きいマイコンに移行したいと思っていました。5V ならまだ Arduino MEGA2560 とかあって GPIO もたくさんあるので使っていく予定ですが、それ以外は Raspberry Pi Pico 中心でいいかなと思い始めるようになりお試しで使ってみました。前から ESP32 とかも持っているのですが ESP-IDF に慣れないのと、ESP-IDF は x.x のリリースされたバージョンに対しても更新をかけてきます。commit 番号によっては動かなくなったりもします。 Raspberry Pi Pico は現在 RP2040 チップだけなので SDK のリリースもシンプルです。ビルドも cmake のインタフェースなので理解できました。また Raspberry Pi Pico は MicroPython も動作するので、C/C++ で書くほどの物でない場合は MicroPython を使用する手も有りで、SDK の持つ基本的なGPIO操作やハイレベルAPIもポーティングされているので、とっかかりは MicroPython から始めるのいいと思いました。

Raspberry Pi Pico の基本をやるとき思いついたのが Si4735 DSP ラジオです。過去 ATMEGA328P にキャラクタ液晶つけて動かしたことがあったのですが、これなら i2c が動けばラジオチップは動くので、ラジオチップを動かすところから始めました。

試作用基板作成

ブレッドボードでやり始めたのですが LEDチカチカ程度とは違い配線が面倒。ということで Raspberry Pi Pico 用に GPIO ピンをピンヘッダに出す基板を作成。Raspberry Pi Pico が2個乗っかているのは一個は動作用でもう一つは PicoProbe という C/C++ で使うデバッガです。MicroPython では使用しません。ピンヘッダを3列出しているので、本来の信号線を配線するのに加えて Analog Discovery のロジックスコープもつけられます。これをMDF板に貼り付けて液晶とTTGOラジオといわれる基板も付けました。

開発環境選定

開発環境は最初から MicroPython に決めていました。初めてなので C/C++ SDK に慣れていなかったためです。また、ラジオの周波数表示などを液晶に出すため、グラフィック環境も欲しいということでググっていたら LVGL というグラフィックライブラリをみつけました。これも本家は C で書かれていますが、lv_micropython という MicroPython に LVGL がバインドされたものがあったので、これなら使えそうと思いました。gitgub の https://github.com/lvgl/lv_micropython にリポジトリがあります。

ファームウエアは ubuntu / Raspberry Pi OSなど Linux の環境であればリポジトリに書いてある手順通りにやれば作成できます。MSYS2 MinGW64 では MicroPython のクロス環境ビルド途中で、Windows のコマンドライン長限界を超えるものがあってエラーになります。make の変数を与えれば分割ビルドできそうなのですが、面倒なので やっていません。

プロトタイプ開発

python IDE の thonny を使って MicroPython のコードを書いていきます。GPIO/SPI/i2c などの使い方は MicroPython のドキュメント https://micropython-docs-ja.readthedocs.io/ja/latest/ に、LVGL グラフィックスライブラリは https://docs.lvgl.io/master/index.html にあります。

少しコーディングしては RUN させるというトライ&トライです。その結果フォントの組み込みなどの方法も理解出来て写真のようにいろいろなウイジェットが使えるようになりました。

  • 周波数をテンキーから入力して選局
  • 表示されいる周波数からステップチューニング
  • AM・FMなどバンドスキャン
  • 選局リストから放送局をタップすると選局

が主な機能です。

本番用基板作成

ここまで出来たら残るは本番用基板です。今回は ILI9341 の液晶に大きさを合わせました。上から液晶・ラジオ基板・マイクロスピーカーを貼り付ける基板の3枚です。

基板は何回か作り直ししましたが、それっぽい感じに仕上がりました。

回路図やアプリケーション本体などは、https://github.com/ngc6589/Si4735_Radio_RaspberryPiPico にて公開しています。

Raspberry Pi Pico I/O 拡張基板

普段使用するマイコンは、Arduino UNO(ATMEGA328P) か Arduino MEGA2560( ATMEGA2560) を良く使っています。最近はマイコンに接続するモジュールも 3.3V のが多いので、Raspberry Pi Pico を使えるようにとお勉強始めたところです。

ESP32 も持っているのですが、ESP32-S(XTensa LX7) ESP32-C(RISC-V) の新製品が技適取って十分出回っていればこちらに行ったかもです。ESP32 の WROVER-E が技適あるので、これでもいいのですが。無線モジュール要らないなら今のところ Raspberry Pi Pico で十分かな。というところで、Pico を使い始めた次第です。

ちょこっと動かすだけなら、Raspberry Pi Pico に秋月の細ピンヘッダつければブレッドボードに差しても左右2列取れますので十分です。

最初はブレッドボードで始めたのですが、やっぱごちゃるなぁということで基板を作成。2×20 ピンヘッダに IO ピンを出しているので、ロジックアナライザの線を刺すこともできますし、SPI や I2C でバス共有するの予備端子としても使えます。

今は、こんな感じで使用しています。基板手前の Raspberry Pi Pico のソケットは Pico Probe デバッグのソケットです。現在 Micropython 勉強中なので openocd + gdb-multiarch は使用していないため手前の Pico Probe は外しています。

基板を10枚作ったので余りは頒布にのせました(売り切り終了)

Raspberry Pi Pico I/O 拡張基板(キット) | 頒布物のページ (em9system.com)

https://github.com/ngc6589/Raspberry-Pi-Pico_IO_Expansion-1