Raspberry Pi PicoのファームウェアをSDカードから起動する

Raspberry Pi PicoのファームウェアをSDカードから更新したかったので、stage3のブートローダーを書きました。SDカードを読み書きできる環境があれば、PC無しでPicoのファームウェアを更新して回れるので便利。ただしPicoにはmicro SDカードスロットが備わっているものとする。

https://github.com/oyama/pico-sdcard-boot


## SDカードブートローダーの動作

このブートローダーは次のように動作します。

1. SDカードをファイルシステム `/sd`にマウントする
2. `/sd/firmware.bin`とフラッシュのアプリケーション領域とを比較し更新の有無をチェックする
3. 更新がある場合`/sd/firmware.bin`をフラッシュのアプリケーション領域に書き込む
4. フラッシュに書き込んだアプリケーション領域を実行する

つまりSDカードのファームウェアを直接実行するわけではなく、フラッシュメモリーにコピーしてから実行しています。

### SDカードのマウント

FATファイルシステムとSDカードの操作にはPico用に開発したバーチャルファイルシステム _pico-vfs_ を使用します。

https://github.com/oyama/pico-vfs

公式のPicoおよびPico WにはSDカードソケットは装備されていないので、[Adafruit MicroSD card breakout board+](https://www.adafruit.com/product/254)などを接続するか、筆者のようにPicoサイズのmicro SDカード付きプリント基板を貼り付けるなどして追加します。

https://qiita.com/0yama/items/972c77a9e63ff9d9c742

https://qiita.com/0yama/items/df7265d6f785c404c5db


pico-vfsを使うと、SDカードを接続したSPIのピン配置やボーレートなどを指定してブロックデバイスを定義し、定義したデバイスを使用するFATファイルシステムのマウントポイント(`/sd`)を指定するだけで、POSIX互換のファイル操作APIが利用できるようになります。
```c
blockdevice_t *sd = blockdevice_sd_create(spi0,
                                          PICO_DEFAULT_SPI_TX_PIN,
                                          PICO_DEFAULT_SPI_RX_PIN,
                                          PICO_DEFAULT_SPI_SCK_PIN,
                                          PICO_DEFAULT_SPI_CSN_PIN,
                                          125000000 / 2 / 4,  // 15.6MHz
                                          true);  // enable CRC check
filesystem_t *fat = filesystem_fat_create();
fs_mount("/sd", fat, sd);
```
### ファームウェアの更新検知と書き込み

ファームウェア`/sd/firmware.bin`を`fopen()`し、フラッシュに展開ずみの既存のファームウェアと`memcmp()`で比較します。
```c
static bool is_same_existing_program(FILE *fp) {
    uint8_t buffer[FLASH_SECTOR_SIZE] = {0};
    size_t program_size = 0;
    size_t len = 0;
    while ((len = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
        uint8_t *flash = (uint8_t *)(XIP_BASE + SD_BOOT_FLASH_OFFSET + program_size);
        if (memcmp(buffer, flash, len) != 0)
            return false;
        program_size += len;
    }
    return true;
}
```
`SD_BOOT_FLASH_OFFSET`がブートローダーを避けるための、256KBのオフセットです。
書き込みは、普通に`flash_range_erase()`して`flash_range_program()`するだけなので割愛。

### アプリケーション領域の実行

フラッシュに書き込まれたアプリケーションの位置を指定して処理をブランチさせます。
```c
void launch_application_from(uint32_t *app_location) {
    // https://vanhunteradams.com/Pico/Bootloader/Bootloader.html
    uint32_t *new_vector_table = app_location;
    volatile uint32_t *vtor = (uint32_t *)(PPB_BASE + M0PLUS_VTOR_OFFSET);
    *vtor = (uint32_t)new_vector_table;
    asm volatile (
        "msr msp, %0\n"
        "bx %1\n"
        :
        : "r" (new_vector_table[0]), "r" (new_vector_table[1])
        : );
}
```

## SDカードブート用アプリケーションのビルドとデプロイ

ブートローダーからロードされるファームウェアは通常のRaspberry Pi Pico用ファームウェアと同じ書き方をしますが、CMakeLists.txtで`enable_sdcard_app(TARGET)`を指定することで、SDカード配布用ファームウェアとしてビルドできます。

```CMakeLists.txt
cmake_minimum_required(VERSION 3.13...3.27)
include(vendor/pico_sdk_import.cmake)
add_subdirectory(pico-sdcard-boot)

project(hello)
set(FAMILY rp2040)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()

add_executable(hello main.c)
target_link_libraries(hello PRIVATE pico_stdlib)
pico_enable_stdio_usb(hello 1)
pico_add_extra_outputs(hello)

enable_sdcard_app(hello)
```
`enable_sdcard_app()`関数は、SDカード配布ファームウェア用のリンカースクリプトを使用する手続きをカプセル化したものです。通常のリンカースクリプトと異なり、フラッシュの先頭から256KBオフセットした位置にファームウェアを配置するよう指示しています。また通常のstage2ブートローダーを省略しています。

開発からデプロイまでの手順を俯瞰すると、開発時は通常のPico用ファームウェアと同じ手順でデバッグ・動作確認を行います。実装が固まりデプロイする段になったならば`enable_sdcard_app()`を有効にしてデプロイ用`.bin`ファイルをビルドする流れになります。
ビルドされる`.bin`ファイルをSDカードのルートディレクトリに`firmware.bin`の名前で配置します。ファームウェアを含むSDカードを装着した状態でPicoを起動すれば、ブートローダーが新しいファームウェアをフラッシュにコピーして実行してくれます。

## SDカードでファームウェアを更新する意義

筐体や設置場所の事情でPCとUSB接続が難しい場合や、開発者ではないエンドユーザーにファームウェア更新を依頼する場面を想定しています。通常の「BOOTSELボタンを押しながらPCにUSB接続して.uf2ファイルをドロップ」も十分シンプルな手順なのですが、これをもう一段シンプルな「SDカードをソケットに差し込んでスイッチオン」にできます。
実装にあたって参考にした[Custom Serial Bootloader for the RP2040](https://vanhunteradams.com/Pico/Bootloader/Bootloader.html)ではUARTのシリアル通信でファームウェアを更新するブートローダーを作っています。今回ファームウェアを配布する媒体としてSDカードを使用しましたが、無線接続できるRaspberry Pi Pico WならOver-The-Airアップデートも割と簡単に実装できますね。