警告・免責事項
執筆者は組み込み系エンジニアではなく、趣味で学んだことを記事にしているため、拙い部分があるかとは思いますがご了承ください。専門分野外のことに取り組んだ奮闘記だと捉えていただくのが良いかと思います。本記事の情報をもとにした開発、製作、運用などの結果について、いかなる責任も負いません。(例: Raspberry Pi PicoやLEDが壊れたり発火するなど)
Programmable I/O とは
Raspberry Pi Pico(以降、Picoと記載します)には特別な機能があります。
Programmable I/O(略してPIO)です。
公式の説明は「Raspberry Pi Pico C/C++ SDK」の"Chapter 3. Using programmable I/O (PIO)"を参照いただくとして、これは一体なんなのか、私のざっくりイメージを紹介しましょう。
シリアル通信やパラレル通信をプログラムで自由自在に作り出せるものです。この世にまだ存在しないアダプターを自作できると言っても良いと思います。
Picoに標準で搭載されているシリアルポートの他にPIOを使って新たなシリアルポートを増やすこともできます。
また、Picoと他の機器とで双方向の通信をするのではなく、ロータリーエンコーダーなどの信号を読み取るだけで、他の機器と対話しないものもPIOで作れます。
しかも、Picoの2つのcpuコアとは別の、PIO専用の処理装置があります。(こんな凄そうな装置がついていてPicoのお値段は本当に4ドルで良いの!? と思ってしまいます。)
このPIO専用の処理装置を使えば、たとえばシリアル通信の処理を行う際に、Picoのcpuの処理時間を奪うことなく他の機器とシリアル通信ができるようです。
Picoのcpuコアでもソフトウェアの処理でシリアル通信は実現可能かとは思いますが、
cpuコアは他に処理すべき仕事があることもあります。
ゆえにPIOはそのようなタイムセンシティブな処理に向いていると言えるでしょう。
PicoがあればUARTやI2Cの端子がないパソコン・マイコンでも、アダプターとして活用することでいろいろなモジュールと通信ができるはずです。
これだけではあまり魅力的に感じられない方も多いと思います。
もうすこし発展させて考えてみましょう。
というより私が初めてPIOを知ったきっかけを紹介しましょう。
機械学習で、(USBではない)昔のゲームコントローラーを操作するAIを作りたいと考えていました。
先駆者は居ないかとググったところ、
TensorKartRealHWというものが見つかりました。
これはなにか概要を説明しましょう。
もともとはTensorflowを使ってマリオカート64を操縦するプロジェクトが2017年1月頃にネット上で公開されたのですが、それはPCのエミュレーター上で稼働するマリオカート64でした。
kevin Hughes氏がブログで公開しています。
https://www.kevinhughes.ca/blog/tensor-kart
それを受けてか、stacksmashing氏はNINTENDO64(以降N64と記載します)実機でそれをやりたかったそうです。
そうして生まれたのがTensorKartRealHWというプロジェクトです。
N64実機のゲーム画面をキャプチャボードでPCに映像を取り込み、PC上でTensorflowを動かし、映像というInputに対するOutputとしてN64コントローラを操作します。
AIから実機のコントローラー操作はどうやっているのか気になり、情報を追いかけると、コントローラ端子のシリアル通信をPicoのPIOで実現していることがわかりました。
┌─────┐ ┌──────────────────┐ ┌─────────────────┐
│ ├──────────────► capture board ├────► │
│ N64 │ └──────────────────┘ │ PC │
│ │ ┌───────┐ │ (TensorFlow) │
└──▲──┘ │ │ │ │
│ │ Pico │ └──────┬──────────┘
│ │ (PIO) │ │
└────────┤ ◄───────────────────────────────┘
└───────┘ Emulate N64Controller via PIO
(USB to N64 Controller Adaptor)
N64コンでできるということはあらゆるゲームコントローラーもPIOの実装さえできれば通信ができるような気もしてきます。
私は世代として興味のあるものはファミコンやスーファミ、デュアルショック(DS)、DS2などです。(DS3からはUSB通信になったのでわざわざPIOを使わなくても良いかと思います。)
もうすこし他の分野にも使えないか考えてみましょうか。
少し調べてみたところVGAも再現可能なようです。
NTSCといったコンポジット出力も可能なようです。
pico-extrasのリポジトリにライブラリがあります。
PicoでのVGAのサンプルコードについては
https://github.com/raspberrypi/pico-playground#scanout-video
で公開されているようです。
rp2040のdatasheetに、対応できるI/O standardについて記載がありました。
• 8080 and 6800 parallel bus
• I2C
• 3-pin I2S
• SDIO
• SPI, DSPI, QSPI
• UART
• DVI or VGA (via resistor DAC)
また、hardware-design-with-rp2040-JP.pdfも参考になりそうです。
もちろん、ライブラリをつかわず自前実装する場合はVGAの仕様を理解する必要があるかとは思います。
他の分野も考えてみましょう。
GameBoy(以降、ゲームボーイやGBと記載)の通信ケーブルもPIOで再現できるのではないでしょうか?
と思ってググってみると、もう作ってる人がいました。
https://www.youtube.com/watch?v=KtHu693wE9o
こちらはGBのテトリスをオンライン化したといったもののようです。
これは当時ネットで記事にもなっていたのでみたことある方もいらっしゃるのではないでしょうか。
私も当時このネット記事を目にしていたのですが、まさかPicoを使っているとはその時は思っていませんでした。
さらに驚いたのですが、作者はTensorKartRealHWと同じstacksmashing氏なようです。
同氏はPicoに関するものを作ることが多いようです。最近はXで写真のみ公開されているTPM Snifferというものに取り掛かっているようで、私は詳細な発表がされることを楽しみにしています。
また、「モバイルアダプタGB」という、ゲームボーイをPHSで通信する製品が存在しますが、それをPicoでエミュレートする試みも他の有志の方の活動として存在するようです。
脱線しましたが話を元に戻しましょう。
これまで見てきたようにPicoのPIOがあればそういった古のガジェットとも通信ができますし、もちろんUARTやSPI、I2Cの通信もできるわけです。(当然ながらPIOコードでの実装は必要ですが...。)
そしてPicoは(ある程度の手間が必要ですが)インターネットにもつなげることができます。
つまり、古のガジェットがPicoを経由してインターネットを介して世界中の人々やマシンと対話することができるのです。
夢が広がるではないですか。
さて、ここまで考えてProgrammable I/O (PIO)を学ぶ価値を感じられた方はぜひPIOの世界の入り口をくぐってみましょう。
下記では、公式のチュートリアル的な位置付けになっているWS2812(NeoPixel)について取り上げます。
Getting Startedのpdfを読む
picoを初めて触られる方は公式のドキュメントである"Getting started with Raspberry Pi Pico" のPDFから読み進めるのが良いかもしれません。
開発環境のセットアップ手順について書かれていますし、サンプルコードのビルド方法やデバッグ方法についても書かれています。注意点としては、 Raspberry Pi 4 などでの開発が前提の書き方をされているので、 Raspberry Pi 4 を使わないで開発する場合は手順などに戸惑うかもしれません。
Raspberry Pi Pico C/C++ SDK のpdfを読む
公式ドキュメントの"Software Development"のPDFには"Raspberry Pi Pico C/C++ SDK"と"Raspberry Pi Pico Python SDK"がありますが、私は前者しか見てないので前者で説明を続けます。
rp2040のdatasheetを読む
公式ドキュメントの"RP2040 Datasheet"のPDF
"Chapter 3. PIO" はState Machineが4つあることやInstructionスロットが32スロットあること、XとYのスクラッチレジスタがあること、IRQは0と1があることなどがわかる資料であり、こちらも読んでおくことをおすすめします。
WS2812とは
LEDストリップやLEDテープを聞いたことはあるでしょうか?
自作PC界隈ではRGBテープ(全てのLEDを同時に同じ色で光らせるもの)やアドレサブルRGBテープ(ARGBテープ)(一つ一つのLEDの色を同時に別の色で光らせれるもの)がそれにあたるかと思います。自作キーボード界隈でも使っている方はいらっしゃるようなので、ご存知の方もいらっしゃるかもしれません。ARGBテープは実は中身はWS2812だったということも多いのではと思います。
(私はNOMU30という自作キーボードキットを買って組み立てたことがあり、基板にLEDテープ用の端子が用意されているのを見たことがあります。しかし、当時は使い方がよくわからなかったのでテープを装着せずに放置していました。どのLEDテープを買えば良いかも分かりませんでしたし。。)
LEDテープを文章で説明すると、シリコンのような柔らかさを持つ、テープ状のものにフルカラーのLEDがいくつも搭載されているものです。しかも、テープを途中で切断してもプラナリアのように切断したLEDテープは単独で利用可能です。(※ 切断してもよいポイントがあります。LEDそのものを切断するのではなく、配線のところが切断可能になっています。)
WS2812を採用したLEDテープは、たった3本の配線で複数の異なるLEDを同時にさまざまな色に光らせることができるようです。いったいどのように実現しているのでしょうか?
↑家に転がっていたLEDテープの写真
(写真中央部分にある導線があるところがハサミでカット可能な部分)
※写真はWS2812のものではありません!
3本の配線は
- GND
- DATA
- V
です。3本の端子(ケーブル)のうち、LEDを何色で光らせるかの色情報はどのケーブルで伝えるかを考察してみましょう。GNDやVのケーブルで色情報を伝達できるとは思えません。つまり、いろいろな色の情報を伝達するにはDATAの配線一本で済ませる必要がありそうです。
Raspberry Pi Pico C/C++ SDKのpdfを見ると、やはりDATAケーブルはシリアル通信で色情報を送信してLEDを制御するもののようです。
シリアル通信でRGB(もしくはRGBW)のそれぞれの色情報を送信し、受け取った側のLED(WS2812のLEDはシリアル通信で送られてきた色情報を理解できる機能を搭載している)でその色情報を元に色を発光させます。
つまり、DATAケーブルにおいてWS2812用のシリアル通信を自前で用意することができれば、どんなマイコンでもPicoでもWS2812を扱うことができるというわけです。
Raspberry Pi Pico C/C++ SDKのPDFではPIOでWS2812を扱うサンプルプログラムを取り上げています。 本記事でも簡単に取り上げます。
クロック分周とは
PIOは125MHzの周波数で動作するそうです。
しかし、ディジタル通信をする際にもっと低い周波数で通信したい状況があります。
たとえばUART serialは10-3000kHzです。
困りました。PIOは125MHzの周波数なので、UART serial通信をするには高すぎる周波数です。
そんな困った時でも解決策があります。
クロック分周で、もともとの周波数からもっと低い周波数を用意することができます。
(私の専門外の用語を説明するので、あくまで私の説明で正確な説明ではないと思います。ご了承ください。)
PIOの125MHzから、たとえば2分割すると、
125/2=62.5MHzをつくれます。
6分割すると、
125/6=20.833...MHzです。
6分割した時の1クロックは
1000000000(ns)/20833333.3333=48.0000000001nsです。
今回はWS2812を制御したいので、WS2812のシリアル通信で使用される周波数はいくつなんだ?という疑問があります。
WS2812のビットレートは800kbpsなようです。
0または1を表現するのに後述するT1+T2+T3のクロックサイクル数が必要なので、
ビットレートx1bitのサイクル数で使用する周波数が求まります。
ただ、何分割すれば良いかが重要なので、周波数がいくつかは今回導出しませんでした。
使用したい周波数 = PIOの周波数 / 何分割すれば良いか
何分割すれば良いか = PIOの周波数 / 使用したい周波数
何分割すれば良いか = PIOの周波数 / (ビットレート * 1bitあたりのクロックサイクル数)
float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit);
WS2812での0と1の定義
※何秒HighなのかLowなのかは現時点では気にしなくてOKです。気になる方はWS2812のデータシートをご参照ください。
- 0はこうです
- 1はこうです
この信号をPIOのプログラムで作り出します。
数式とT1,T2,T3の対応
pico-examplesのソースコードでT1,T2,T3といった3種類の時間的な区切り、定義がなされています。
T1,T2,T3がそれぞれ信号のどの部分に当たるのかは次の図をご覧ください。
.define public T1 2
.define public T2 5
.define public T3 3
数値はHighの信号もしくはLowの信号をキープする時間の長さです。
各命令の最後に[数値]を書くと、その数値分だけサイクル数を遅延します。その命令も1サイクルとして時間がかかるのでT1だけ遅延したい時は[T1-1]と記載します。
私が既にソースを理解した上で説明をしてしまうのですが、
データの0を表現したい時は、
T1がHighでT2,T3がLOWのときです。
データの1を表現したい時は、
T1がHighでT2もHigh。T3がLOWの時です。
アバウトに言ってしまうと、1を表現したい時はHighの時間を0の時よりも長くすれば良い訳です。
(※どれだけの長さにすべきかはws2812のデータシートを参照して判断してください)
これをpioのアセンブラコード、pioasmで表現すれば良いわけです。
サンプルコードのアセンブラを見ていきます。
bitloop:
out x, 1 side 0 [T3 - 1] ; Side-set still takes place when instruction stalls
jmp !x do_zero side 1 [T1 - 1] ; Branch on the bit we shifted out. Positive pulse
do_one:
jmp bitloop side 1 [T2 - 1] ; Continue driving high, for a long pulse
do_zero:
nop side 0 [T2 - 1] ; Or drive low, for a short pulse
正直、初見では何やってるかわからないと思います。私はわかりませんでした。
(サマレンの主人公のように)俯瞰して全体のフロー、プログラムの分岐を見ていくと、
bitloopラベルの処理をした後に、do_oneラベルへ行くか、
do_zeroラベルの処理へ飛ぶか、といった構造になっていることが分かります。
つまり、0または1のデータを表現する際に共通の処理をbitloopラベルにある命令で行い、その後0のデータを表現したい時はdo_zeroラベルへ飛び、1のデータを表現したい時はdo_oneラベルの処理を(fallthroughで)行うコードであると推測が立てれます。
これをデータ0,データ1の図に当てはめることができるか考えてみます。
(なお、Raspberry Pi Pico C/C++ SDKのpdfの解説を読むとT1,T2,T3それぞれを-1しているのは、pioasmの1命令実行時に消費するサイクル数だけ時間が経過するので、その分遅延する必要がないので差し引いているとのことです)
その結果がこちらの図です。
↑T1の区間についてです。
↑T2の区間についてです
↑T2の区間についてです
↑T3の区間についてです
これでサンプルコードのpioasm部分は理解できたかと思います。
(pioasmの各命令の説明は省略していますので、適宜、公式ドキュメントなどご参照ください。)
0と1の信号の作り出し方はこれでわかりました。
あとはシリアル通信で転送するデータとして、どのようにRGBの情報をどのようなフォーマットで用意するかです。
このソースについてはcのコードに書かれています。
LEDの個数も定義されていますね。
#define NUM_PIXELS 150
RGBの情報をどのようなフォーマットで用意するか
ですが、仕様を理解する時間が確保できず、アドカレの日付がきてしまいました。
RGBの情報フォーマット以外のソースコードに納得したところで実際に実機で動かすことにしてみました。
実機で動かす
- ビルド時の注意
- LEDの個数をソースで指定します。
今回、私はWS2812のLEDテープが手元になかったので、テキトーにネットでポチったWS2812互換のLEDが正方形にならんだものをポチりました。600円くらいだったと思います。
配線がよくわからなかったのですが、
https://github.com/raspberrypi/pico-examples/blob/eca13acf57916a0bd5961028314006983894fc84/pio/ws2812/ws2812.c#L22
に
// default to pin 2 if the board doesn't have a default WS2812 pin defined
#define WS2812_PIN 2
とあります。
picoのsdkにある関数sm_config_set_out_pins()の第二引数out_baseを2にしたとき、このピンはgpio 2番に対応するようです。
GPIO 2番とws2812のDATAピンを接続します。DATAピンはシリアル通信をするケーブルですね。
BOOTSELモードでpicoとPCをUSB接続し、UF2ファイルをドラッグ&ドロップでプログラムを書き込みます。
軽く調べたところこのサンプルコードの場合は抵抗がなくとも、ws2812は利用可能なようです。今回、抵抗は咬ませませんでしたが無事動いているようです。しばらく動かしていますが燃えていません。(過電流・家電圧の場合はLEDは燃えると思うので。。)
良い子は私の真似をせず、ちゃんと電圧電流がデータシートを参照して定格を守りましょう。
実際に動作している様子
疑問
サンプルコードを実機で動かしたところで、そもそもT1,T2,T3の値はどこからきたものなのか疑問に思いました。
おそらくデータシートを参照しながら周波数かクロック分周の値を計算すると1サイクルがxμsであることがわかるので、そこから数値を導出するのだと思います。
自分で計算して求めるのも面倒なので(おぃ)、ググって計算してる方がいないか調べました。
するとMICAH JOEL DUBINKO氏のブログ記事が見つかりました。
MICAHLOGIC QUERYOPTICON | Why almost all Raspberry Pi Pico (RP2040) Neopixel code is wrong
ブログによると、T1,T2,T3の値はWS2812のデータシートを参照すると最適な値ではないようです。
最適どころか、定格の下限ギリギリを攻めるような値になってしまっており、
Dubinko氏がブログで主張しているように、少しでもずれが生じればデータシートに沿わない使い方になってしまい、適切な挙動をしないように思えます。
しかし、先ほど実機で動かしたように、正しく動いてそうです。動いているのは、データシートから外れても動作するという、WS2812を作ったエンジニアの寛大さによる恩恵に依存しているのでしょう。
全てのWS2812ライクの製品で動くとは言えないとおもうので、動かない製品も出てくると思います。
終わりに
PIO入門でかなり初歩的と思われる題材を体験しました。
今後はここからさらに発展して応用的なものを触っていきたいと思います。
(今年の私の次のアドカレ記事はPIO以外のネタを扱います)
作ってから気づきましたが、自作PCパーツで売られているスタンドアローンのARGBコントローラーと同じものですね。RGBのLEDテープやARGBのLEDテープやらAura RGBやらなんぞやと当時疑問ばかりでしたが、一度自分の手でWS2812と対話するものをつくってみると、仕組みや仕様など、よりクリアに理解できたような気がします。
今となってはNOMU30についていたLEDテープ用の端子も使いこなせそうな肌感はあります。
picoをお持ちの方はws2812互換のLEDテープと組み合わせて日常生活をゲーミングな光で彩ってみてはいかがでしょうか。