Raspberry Pi Pico実機上でユニットテストを実行する

手軽に使えるRaspberry Pi Picoの手軽さを維持しつつ、テストの使い心地(とても重要)のバランスを見極めるのがちょっと悩ましくて楽しかったので共有。

今回は永続化した循環バッファ(リングバッファ、サーキュラーバッファ)をPicoのオンボードフラッシュメモリに保持して、センサーデータを一定期間のあいだ蓄積するサンプルを書いていました。Picoオンボードの温度計を1秒間隔で記録して追記します。蓄積した最古から最新のセンサーデータを昇順の時系列データとして標準出力に出力します。

https://github.com/oyama/pico-persistent-circular-buffer


```$ screen /dev/tty.usbmodem21302 115200
--------ASCENDING TIME SERIES
timestamp=30.3,temperature=29.95
timestamp=31.4,temperature=29.95
timestamp=32.5,temperature=29.95
timestamp=33.6,temperature=29.95
timestamp=34.7,temperature=30.42
timestamp=35.8,temperature=29.95
timestamp=36.8,temperature=29.95
timestamp=37.9,temperature=30.42
timestamp=39.0,temperature=29.95
--------ASCENDING TIME SERIES
timestamp=31.4,temperature=29.95
timestamp=32.5,temperature=29.95
timestamp=33.6,temperature=29.95
timestamp=34.7,temperature=30.42
timestamp=35.8,temperature=29.95
timestamp=36.8,temperature=29.95
timestamp=37.9,temperature=30.42
timestamp=39.0,temperature=29.95
timestamp=40.1,temperature=29.48
```
BOOTSELボタンを押してるあいだは最新から最古の降順で時系列データを出力します。
```$ screen /dev/tty.usbmodem21302 115200
--------ASCENDING TIME SERIES
timestamp=150.2,temperature=30.42
timestamp=151.3,temperature=29.95
timestamp=152.4,temperature=30.42
timestamp=153.5,temperature=30.42
timestamp=154.5,temperature=29.95
timestamp=155.6,temperature=29.01
timestamp=156.7,temperature=30.42
timestamp=157.8,temperature=30.42
timestamp=158.9,temperature=30.42
--------DESCENDING TIME SERIES
timestamp=160.0,temperature=29.95
timestamp=158.9,temperature=30.42
timestamp=157.8,temperature=30.42
timestamp=156.7,temperature=30.42
timestamp=155.6,temperature=29.01
timestamp=154.5,temperature=29.95
timestamp=153.5,temperature=30.42
timestamp=152.4,temperature=30.42
timestamp=151.3,temperature=29.95
```

APIはシンプルに _バッファの作成_、_アイテム追加_、バッファをトラバースする _カーソルのopen_, _カーソルのアイテム取得_ の4種類だけ。アイテムは任意サイズのデータを登録できます。
```
void cb_create(cb_t *cb, uint32_t address, size_t length, size_t item_size,
               timestamp_extractor_t get_timestamp, bool force_initialize);
void cb_append(cb_t *cb, const void *data, size_t size);
void cb_open_cursor(cb_t *cb, cb_cursor_t *cursor, cb_cursor_order_t order);
bool cb_get_next(cb_cursor_t *cursor, void *data);
```

二次記憶装置でうごく循環バッファを書いたことがなくていろいろ手間取ったのですが、これは網羅的に自動テストできる環境じゃないとやってられないなーと感じて用意することにしました。

## Raspberry Pi Debug Probe

PicoのBOOTSELボタンを押しながらUSBに挿して、*.uf2ファイルをPicoのUSBメモリにドロップして...の手順は手軽なんですが、回数を重ねてくるとやってられなくなります。俺はmicro USBコネクタを抜き差しするマシンだうぉぉーん。そこでARM Serial Wire Debug (SWD)を使えるよう、Raspberry Pi Debug Probe[^1]をPicoと開発マシン(macos)に接続します。

![Raspberry Pi Debuf Probe](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108691/cffb2129-7cb0-4c0b-70bc-17f239117b4e.jpeg)

macosの開発マシンはHomebrewでクロスコンパイルする環境を用意しておきます。これでコマンドラインからプログラムの書き込み・再起動を自動化できます。接続や設定はPico Probeの商品ページ[^1]とGetting started with Pico[^2]のとおりです。接続といってもSWDのスルーホールにピンを差し込むだけなんですが。
![SWDピンの接続](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108691/39211e19-5c2f-687a-e23e-9a711afa5979.jpeg)
刺すだけ。

プログラムの書き込み・再起動はコマンド一つ。
```bash
$ openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg \
          -c "adapter speed 5000" -c "program my-project.elf verify reset exit"
```

ビルドするプログラムは`CMakeLists.txt`でUSBへの標準出力を有効にしておきます。
```CMakeLists.txt
pico_enable_stdio_usb(my-project 1)

```

ちなみにDebug ProbeとPicoを開発マシンにUSB接続すると、ttyデバイス2つが現れます。
```bash
$ ls  /dev/tty.usbmodem* 
/dev/tty.usbmodem1101  /dev/tty.usbmodem21302
```
どっちがProbeでどっちがPicoかは分かりません。ボーレート 115,200bpsでそれぞれに繋いでみて確かめましょう。
```bash
$ screen /dev/tty.usbmodem21302 115200
```
ちなみにPicoの標準出力を開発マシンに繋ぐにあたり、

- pico_enable_stdio_usb()している場合(Pico -(USB)-> macos)
- Raspberry Pi Debug ProbeとPicoのUARTを接続している場合(Pico -(UART)-> Debug Probe -(USB)-> macos)

と2パターンあり得ますが、前者のUSB直は手軽で良いのですが、Picoを再起動した際にttyが切れてしまうので、後者の方がテストには都合が良いです。

## テストコードの記述

テストコードはプロジェクトに`tests`ディレクトリを追加します。
```bash
$ ls tests
CMakeLists.txt test_create.c test_restore.c
main.c test_append.c test_cursor.c tests.h
```
個々のテストを`test_*.c`ファイルに記述します。
プロジェクト最上位の`CMakeLists.txt`に`tests`ディレクトリの情報を追加します。
```CMakeLists.txt
add_subdirectory(tests EXCLUDE_FROM_ALL)
```

`EXCLUDE_FROM_ALL` オプションを付与して、プロジェクト本体のビルドには含まないようにします。`tests/CMakeLists.txt`はこんな感じ

```tests/CMakeLists.txt
set(CMAKE_BUILD_TYPE Debug)

add_executable(tests
  ../circular_buffer.c
  main.c
  test_create.c
  test_append.c
  test_cursor.c
  test_restore.c
)

target_link_libraries(tests
  hardware_adc
  hardware_flash
  hardware_sync
  pico_stdlib
)

target_include_directories(tests
  PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}/../include
)
pico_add_extra_outputs(tests)
pico_enable_stdio_usb(tests 1)
```

`tests/main.c` はテストのエンドポイントになるコードです。test_*.cと一緒に一つのRaspberry Pi Pico用ファームウェア`tests.elf`としてビルドします。テストのお作法については割愛。プロジェクトで作成したコードのAPIを操作して期待する状態になるか `assert()` して最後まで実行できたら`printf("ok")`を出力するだけです。
pico-sdkの場合、`tests/CMakeLists.txt`に
```tests/CMakeLists.txt
set(CMAKE_BUILD_TYPE Debug)
```
を記述するか、cmake実行時に
```
$ cmake -DCMAKE_BUILD_TYPE=Debug ..
```
しないと`assert()`が有効にならないので注意。

## カスタムターゲットの追加

あとは`openocd`でテストプログラムを書き込んで標準出力を眺めるだけ...なのですが、openocdのコマンドライン引数は長すぎて覚えられません。なのでユニットテスト用の`tests/CMakeLists.txt`にカスタムターゲット`run_tests`を追加します。

```tests/CMakeLists.txt
add_custom_target(run_tests
  COMMAND ${OPENOCD} -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "program tests.elf verify reset exit"
  DEPENDS tests
)
```
`CMakeLists.txt`を編集したので、ビルド環境を作り直しましょう
```bash
$ rm -rf build
$ mkdir build
$ cd build
$ cmake ..
```
これであとは
```bash
$ make run_tests
```
するだけでテストをビルド・実行できるようになりました。今度こそテスト結果を眺めだけ。

```$ screen /dev/tty.usbmodem21302 115200
Start all tests
create ...................ok
append ...................ok
cursor ...................ok
restore ..................ok
All tests are ok
```

標準出力を垂れ流しているだけで、終了コードなどはみていないので、CIで自動化して...とかはできません。必要ならttyの入力を監視するプログラムを走らせればいけそうですね。実際のテストコードはGithubのリポジトリを参照して下さい。特別なものは何も入れていません。

テストに失敗すると、
```$ screen /dev/tty.usbmodem21302 115200
Start all tests
create ...................ok
append ...................ok
cursor ...................assertion "item.timestamp == (uint64_t)(0 + i)" failed: file "src/github.com/oyama/pico-persistent-circular-buffer/tests/test_cursor.c", line 42, function: test_cursor_descending
```
このような出力で失敗箇所がわかります。

[^1]: [Raspberry Pi Debug Probe](https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html)
[^2]: [Getting started with Pico](https://datasheets.raspberrypi.com/pico/getting-started-with-pico-JP.pdf)


~~ちなみに永続化循環バッファの実装は落ち度があって、フラッシュメモリの単一領域をerase/programしています。なのでeraseした瞬間にPicoが電源を失うとデータもぜんぶ失います。かわいいですね。~~ よりロバストでWear-Leveling機能付きなCircular Bufferを実装しました。
フラッシュメモリはデータの更新に消去・書き込みの2ステップが必要で、消去は4096バイト単位、書き込みは256バイト単位など非対称?なのが悩ましいところです。いにしえのHDDではなくフラッシュメモリに最適なデータ構造と操作アルゴリズムの勉強をしないとなーと強く思った次第です。