Raspberry Pi PicoのファームウェアをSDカードから更新したかったので、stage3のブートローダーを書きました。SDカードを読み書きできる環境があれば、PC無しでPicoのファームウェアを更新して回れるので便利。ただしPicoにはmicro SDカードスロットが備わっているものとする。
SDカードブートローダーの動作
このブートローダーは次のように動作します。
- SDカードをファイルシステム
/sd
にマウントする -
/sd/firmware.bin
とフラッシュのアプリケーション領域とを比較し更新の有無をチェックする - 更新がある場合
/sd/firmware.bin
をフラッシュのアプリケーション領域に書き込む - フラッシュに書き込んだアプリケーション領域を実行する
つまりSDカードのファームウェアを直接実行するわけではなく、フラッシュメモリーにコピーしてから実行しています。
SDカードのマウント
FATファイルシステムとSDカードの操作にはPico用に開発したバーチャルファイルシステム pico-vfs を使用します。
公式のPicoおよびPico WにはSDカードソケットは装備されていないので、Adafruit MicroSD card breakout board+などを接続するか、筆者のようにPicoサイズのmicro SDカード付きプリント基板を貼り付けるなどして追加します。
pico-vfsを使うと、SDカードを接続したSPIのピン配置やボーレートなどを指定してブロックデバイスを定義し、定義したデバイスを使用するFATファイルシステムのマウントポイント(/sd
)を指定するだけで、POSIX互換のファイル操作APIが利用できるようになります。
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()
で比較します。
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()
するだけなので割愛。
アプリケーション領域の実行
フラッシュに書き込まれたアプリケーションの位置を指定して処理をブランチさせます。
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カード配布用ファームウェアとしてビルドできます。
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ではUARTのシリアル通信でファームウェアを更新するブートローダーを作っています。今回ファームウェアを配布する媒体としてSDカードを使用しましたが、無線接続できるRaspberry Pi Pico WならOver-The-Airアップデートも割と簡単に実装できますね。