PicoのNeoPixel(WS2812)のサンプルを動かしてみた

PicoのPIOを使ったサンプルを試すのに、一番手軽なのはNeoPixel(WS2812)のようです。

以前使った8連のNeoPixelがありましたので、これを光らせてみました。また、WS2812をPIOで制御する部分のコードも見てみます。

試したサンプルはPico Python SDKマニュアルの3.9.2にあるものです。
コードはここにあります。

結線ですが、信号を送る線はGP22になっていますので、29番ピンです。あとは3.3V(36番ピン)とGND(黒で示されているピンのどれか)をWS2812に結線します。

このデモはもともと8連のNeoPixel用に書かれているので、そのまま使えます。
動かすとこんな感じになります。

このコードはWS2812ドライバも含めMicroPythonだけで書かれています。
MicroPythonはインラインアセンブラが使えますが、Pico用のMicroPythonはPIOのアセンブラをサポートしており、WS2812のドライバは以下のように定義されています。(コードとしてはRP2040のマニュアルの3.6.2にあるものと等価です。)

@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()

# Create the StateMachine with the ws2812 program, outputting on Pin(22).
sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(22))

# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

最初のデコレータにより、関数ws2812()はPIOアセンブラで処理することが宣言されてます。デコレータにパラメータが4つありますが、意味は以下の通りです。

sideset_init=rp2.PIO.OUT_LOW side-setで使うGPIOは「出力モード、値は0」で初期化
out_shiftdir=rp2.PIO.SHIFT_LEFT シフトレジスタのシフト方向は左シフト
autopull=True シフトレジスタの空きが閾値を超えたら自動でFIFOからシフトレジスタへ読み込む
pull_thresh=24 自動読込が行われる閾値は、データ残量が24ビット以下になった時

WS2812を制御する方法は以前の記事でもちょっと書きましたが、要はパルスのデューティ比の違いで「1」と「0」を識別します。

上のコードのws2812()の中身ですが、最初のT1,T2,T3は定義なので飛ばして、まず「wrap_target()~wrap()」はこの区間をループすることを宣言しています。この処理は実際にはPIOの命令ではなく、PIOへの設定として解釈されます。

次のlabelは定義なので飛ばして、次の「out(x,1).side(0)[T3-1]」の意味は

・送信するデータを1ビット、Xレジスタへ読み込む。
・その際、side-setピンへ0を出力する。
・次の命令を実行する前に「T3-1」サイクル待つ。(-1は命令自身の実行に要するサイクル)

つまり、命令を実行後には

・0が3サイクル分出力され、次に送信するビットはXレジスタに入っている(1 or 0)

ということになります。
次の「jmp(not_x, “do_zero”).side(1)[T1 – 1]」は

・Xが0なら、do_zeroへジャンプする。
・その際(ジャンプしてもしなくても)、side-setピンへ1を出力する。
・T1-1サイクル待つ。

となり、命令を実行後には

・(Xが1だったとき)1が2サイクル分出力され、次に実行する命令はjmp(“bitloop”).side(1)[T2 – 1]
・(Xが0だったとき)1が2サイクル分出力され、次に実行する命令はnop().side(0)[T2 – 1]

となります。なお、PIOにはNOP命令はありませんので、実際にはmov(y, y)で代替されます

そして、Xが1だったときはjmp(“bitloop”).side(1)[T2 – 1]

・bitloopへジャンプする。
・その際、side-setピンへ1を出力する。
・T2-1サイクル待つ。

となりますので、1がさらに5サイクル続き、合計で7サイクル連続します。

Xが0だったときはnop().side(0)[T2 – 1]で、side-setピンは0になり、これが5サイクル続きます。
そのあとwrap()によりbitloopへジャンプします。

従って、全体ではループ1回あたり10サイクルで、

「1」を出力する場合、0001111111
「0」を出力する場合、0001100000

という信号がGPIOに出ていくことになります。クロックが8MHzに設定されていますので、10サイクルあたり1.25μsecで、T1H=0.875μsec、T0H=0.25μsec、T1L=0.375μsec、T0L=1.0μsecとなります。仕様ぴったりの値ではないですが、プラスマイナス0.15μsecは許容されるようですので、T0L以外は仕様の範囲に収まっています。

以下の図は、「1」のコードを出力するパターン(左側)と「0」のコードを出力するパターン(右側)の、命令と実行タイミングを図示したものです。

縦軸が時間軸です。最初の列が命令、2番目の列はside-setで、色は出力ピンの状態(青=L、赤=H)を表します。3番目の列はdelay([]で囲まれた値)指定により、次の命令を実行するまでカウントダウンする様子を表しています。なお、出力ピンの状態が変化するのは命令の実行後なので、命令の先頭から1サイクル遅れます。

このWS2812ドライバでは、ピンへの出力はすべてside-setを使っています。side-setはすべての命令で使用できるので、プログラム中のどのタイミングでもGPIOを操作することができ、便利そうです。

なお、side-setで操作するピンの指定は、SET命令でのGPIOの指定と同様、あらかじめPIOのレジスタで指定されたベースとなるGPIOピンがあり、そこからの相対値で行います。このベースとなるGPIOはSET命令のそれとは独立に指定できます。

ここ以外は普通のMicroPythonのコードです。
LEDの個数は可変になっています。64個で動かしたらこんなふうになりました。

コメント

  1. マイコン小僧だった遠き日 より:

    初めまして、大変参考になり助かっています。ありがとうございます。
    一点気になったので質問させて下さい。

    現状、記事中のタイミングで見ると、殆ど仕様を満たしていません。
    1ビット解釈が違うのでは?と思い疑うと

    「1」を出力する場合、0000111111
    「0」を出力する場合、0001110000

    こうではないか?と。
    疑問部分は「jmp(not_x, “do_zero”) .side(1) [T1 – 1]」の解釈です。

    辻褄合わせで考えているのでなんとも釈然としませんし、心許ない。
    PMと出力の関係が見えてきたので、自分なりにも少しいじくり回してみます。

    PMの概念と構造、コードとの対応が見えてきたのでいろいろ遊べそうです。
    重ねて、ありがとうございます。今後も参考にさせて頂きます。(感謝)

  2. boochow より:

    コメントありがとうございます。

    > 現状、記事中のタイミングで見ると、殆ど仕様を満たしていません。
    > 1ビット解釈が違うのでは?と思い疑うと
    >
    > 「1」を出力する場合、0000111111
    > 「0」を出力する場合、0001110000
    >
    > こうではないか?と。

    はい、おっしゃる意味わかります。
    「1」を出力する方は仕様の範囲内なのですが、「0」を出力する方は、1の出力回数を3回にしてT0H = 0.375μsec、T0L = 0.875μsecというようにしたほうが、仕様の値により近くなりますよね。

    実際、そのようにするのは難しくないはずで、T1, T2, T3が現状では2, 5, 3になっているのを、3, 4, 3に変えれば済むはずです。(今試しに実験してみましたが、このように変えても問題なく動作しました。)

    命令の解釈は間違ってないと思うのですが、モヤモヤとはしますね。

  3. 返信恐れ入ります。

    Qiitaに記事を書いております。(サイト欄に書きました)
    ニコニコ動画に動画をアップもしております。(記事中で恥を晒しております…)

    稚拙なれど主様のお陰でとても理解が深まりました。
    またタイミングの件、なんともスッキリしないけど、現実もそれその通りである事を確認した旨、記事にしました。
    余りシビアでは無い様ですね。

    何はともあれ、ありがとうございました。

  4. boochow より:

    こちらこそありがとうございます。ロジアナの画像、参考になりました。

  5. 追伸。

    再々失礼いたします。
    下記ページを発見しました。

    Light_WS2812ライブラリV2.0–パートI:WS2812を理解する–Timのブログ
    https://cpldcpu.wordpress.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/

    ここに辿り着くに、私のテスト環境(中華ロジアナ)のデコード機能(WS281x)が”動くパターンでも1を認識しない”ので調べていたら発見しました。
    この手のチップが多数あり、どうも各々アバウトな部分があるのがモヤモヤの原因みたいです。
    中にはなかり短めのタイミングの物も有るようで秋月で購入した物だと噛み合わない事も有るようです。

    ご迷惑ばかりで失礼しました。
    本件はここまでに致し、早々にPIOに戻りたいと思います。
    感謝しつつ、お詫びとご多幸、ご発展をお祈りします。(また何処で)

  6. boochow より:

    リンクありがとうございます。なるほど、仕様に対して結構幅があるんですね。仕様よりもノウハウがものを言いそうですね。