Raspberry Pi PicoのBOOTSELボタンを使ってMDX playerを制御する

[Raspberry Pi Pico Audio PackでX68000のMDXファイルを再生する](https://qiita.com/yunkya2/items/da423a2bee0266fbec44) でMDXファイルが再生できるようになったので、フラッシュに複数のファイルを置いて更に再生制御もできるようにしてみます。

ところが、[Display Pack](https://shop.pimoroni.com/products/pico-display-pack)は液晶の他にボタンも付いていてユーザからの入力に使える一方で、[Audio Pack](https://shop.pimoroni.com/products/pico-audio-pack)には一つもボタンがないのでこういうことができません(基板上のスペースもGPIOピンも余っているので、ボタンくらい付けておいてほしかった…)。
そこで、Picoに付いている唯一のボタン、フラッシュ書き込み時にしか使わないBOOTSELをなんとかユーザからの入力に使えないかと試してみました。

# BOOTSELボタンの読み出し

Picoの[リセット時の処理](https://qiita.com/yunkya2/items/5f0f5ebb0f26a52805d9)ではブートROM内でBOOTSELボタンの状態を読みだしてフラッシュ書き込みモードにするかどうかを選択しているので、通常のアプリからでも読み出す方法はあるはずです。ROMのソースコードからGPIO_HI_IN(0xd0000008)を読めばいいことは分かるので、gdbで直接覗いてみます。

```
(gdb) x/x 0xd0000008
0xd0000008:     0x00000002   <== 押す
(gdb) x/x 0xd0000008
0xd0000008:     0x00000002   <== 離す
```

…読めません。ROMはここを見てるのに何故? と、ブートROMを抜けて通常動作してる状態では、フラッシュはXIPモードで制御されているため、BOOTSELが繋がっているCSピンもそのために使われていて自由に読み出すことができないのでした。そこで、GPIOのfunction selectの設定を変更します。GPIO_QSPI_SS_CTRL(0xd0000008)レジスタを書き換えます。

```
(gdb) set {int}0x4001800c=5   <== SIOモードに変更
(gdb) x/x 0xd0000008
0xd0000008:     0x00000000    <== 押す
(gdb) x/x 0xd0000008
0xd0000008:     0x00000002    <== 離す
```

見事ボタンの状態が読めました。ただ、XIPモードから変更してしまうと、フラッシュ上のプログラムを直接実行することができなくなります。なので、読み出しのためのコードはSRAM上に配置して、なおかつ読み出し中に割り込みが発生するとフラッシュ上に飛んでしまうので割り込みを禁止して…と考えていたら、SDKのサンプルにBOOTSELボタンを読み出すコードがあったことに気付きました。最初からこっちを見ればよかった。

```pico-examples/picoboard/button/button.c
bool __no_inline_not_in_flash_func(get_bootsel_button)() {
    const uint CS_PIN_INDEX = 1;

    // Must disable interrupts, as interrupt handlers may be in flash, and we
    // are about to temporarily disable flash access!
    uint32_t flags = save_and_disable_interrupts();

    // Set chip select to Hi-Z
    hw_write_masked(&ioqspi_hw->io[CS_PIN_INDEX].ctrl,
                    GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
                    IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);

    // Note we can't call into any sleep functions in flash right now
    for (volatile int i = 0; i < 1000; ++i);

    // The HI GPIO registers in SIO can observe and control the 6 QSPI pins.
    // Note the button pulls the pin *low* when pressed.
    bool button_state = !(sio_hw->gpio_hi_in & (1u << CS_PIN_INDEX));

    // Need to restore the state of chip select, else we are going to have a
    // bad time when we return to code in flash!
    hw_write_masked(&ioqspi_hw->io[CS_PIN_INDEX].ctrl,
                    GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
                    IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);

    restore_interrupts(flags);

    return button_state;
}
```

考えていた通り、割り込みを禁止してGPIOのモードを一時変更して、ボタンの状態を読んだ後に元に戻しています。関数の`__no_inline_not_in_flash_func(get_bootsel_button)()`という定義は、この関数をフラッシュ上に置かない(SRAM上に置く)という指定になります[^1]。

[^1]: 実は、RP2040のもう一つのCPUを動かしている場合はこの処理だけでは不十分です。ボタンを読んでいる間にも別のCPUが動いてフラッシュから命令コードを読み込んでいるかも知れないからです。おそらく、別のCPUには一度割り込みでもかけてSRAM上のハンドラで待機させておく、といった処理が必要になるのではないかと思います。

# MDXプレイヤーの対応

さて、BOOTSELボタンが読めることが分かったので、[pico-mdx](https://github.com/yunkya2/pico-mdx)もこのボタンで制御できるようにします。

https://github.com/yunkya2/pico-mdx

以前のバージョンでは再生するファイルをCMakefile.txtに1つだけ指定できましたが、複数ファイルの再生に対応できるようにしました。pico-mdx/mdxlist.txtというファイルにMDXファイルのファイル名を1行に一つずつ指定します。

```mdxlist.txt
#   mdxlist.txt
#

#   PDX file search path
%.
#%<path-to-pdx-file>

#   MDX file names
test.mdx
```

対応するPDXファイルは、MDXファイル内の指定を読んで検索するようにしています。MDXファイルと同じパスにあればそれを使い、ない場合には"%"で始まる行を検索パスとしてそちらから探しに行きます。

ビルド、インストール、起動すると再生開始待ちの状態になるので、BOOTSELボタンを1回押すと最初のファイルから再生が始まります。再生中は以下のボタン操作で制御できます。

| Action |               | Operation      |
|--------|---------------|----------------|
| *     | (シングルクリック) | 次の曲へ     |
| * *   | (ダブルクリック) | 前の曲へ |
| * * * | (トリプルクリック) | 再生停止  |
| ==    | (ボタンを押したまま)    | 音量を下げる    |
| * ==  | (シングルクリック後にボタン押したまま) | 音量を上げる |

ボタン1つなのでちょっと複雑ですが、音楽プレイヤーとして一通りの操作ができるようになりました。

# 実行

再生時の動画をtwitterに上げました。

https://twitter.com/yunkya2/status/1373283345900077074

Picoの2MBのフラッシュメモリは、普通に音楽データを置くには小さすぎますがMDX+PDXファイルであれば数10曲でも余裕で入ります。モバイルバッテリーとイヤホンを付ければ、普通にポータブルプレイヤーとして使えますね。