Raspberry PiのMicroPythonでNeoPixelを制御Part2


前回は、Raspberry PiのハードウェアPWMのシリアライザ機能を使って、5つのNeoPixelを点灯させました。
これは、PWMを止めてからFIFOにデータを書き込み、PWMを再開させて連続したデータを送り出すというものでした。

これだと、連続して送信できるデータはFIFOのサイズに制約されます。
しかし、FIFOはバッファとして働きますので、送信中にFIFOに連続的にデータを流し込んでやれば、より多くのNeoPixelを点灯させることができます。
ただ、NeoPixelのデータは0.8MHz~1MHzのPWM信号なので、このスピードでデータを流し込むのはMicroPythonでは難しく、Cで実装する必要があります。

そこで、PWMクラスに新しくfifo_writeというメソッドを追加してみました。これはbytearrayでデータ列を受け取り、FIFOに書き込むメソッドです。
タイムアウトしない限り、FIFOが満杯のときは空きができるまで待ち、与えたbytearrayを全てFIFOに書き出します。

・・・という目論見だったのですが、なかなかうまく動きませんでした。
テストには、以前購入した64連のNeoPixelを使いました。

どうも、FIFOのサイズを超えたデータ量を送るあたりから、表示がおかしくなります。
FIFOは32bit×16個、NeoPixel用のデータを1個送信するのに96bit消費するので、NeoPixelの6個目でFIFOのサイズを超えます。
このとき起こる現象は

1)表示が更新されず、その後PWMをオフにすると全点等してしまう
2)表示が更新されるが、送ったデータの最後の部分が表示されない

の2つでした。

1)については、ロジアナで見てみると、同じデータを延々と送信しています。
このとき、リピート機能(FIFOが空になったら最後のデータを繰り返し送信する)はオフにしてるにも関わらず、リピート機能が働いているように見えます。

2)については、データの長さによらず、最後のほうだけが表示されないので、あえて余分なデータを送ってやると表示されたりします。ロジアナで見ていても信号が出ていないので、NeoPixel側の問題ではなさそうです。

対策ですが、試行錯誤の部分は飛ばして、結論だけ書くと

1)2つのPWMを両方FIFOを使用する設定にする
2)データを送った後、ダミーのデータを送ってFIFOのデータを吐き出させる

というバッドノウハウで動作させることができました。

1)について、マニュアルには何も書かれていないのですが、どうも2チャンネルあるPWMの片方だけでFIFOを利用すると、FIFOのサイズを超えてデータを送る際のFIFOの読み出し動作がおかしくなるようです。

具体的には、マニュアルにはFIFOが空になったときに、FIFOの最後のデータを出力し続けるか、出力を停止するかをCTLレジスタのRPTL1、RPTL2ビットで制御できると記載されていますが、これが動作してくれません。
動作しない条件はよく分からないのですが、出力停止を指定していても、最後のデータを出力し続ける状態になってしまいます。
こうなると、データを止めるには、PWMを明示的に停止(PWENビットをオフ)する必要があります。

逆に、この現象を生じさせない十分条件は「PWMを2チャンネルともFIFOを使用する設定にする」ことのようです。
そうするとFIFOに2倍のデータを与えないといけないのですが、ここらへんはC言語での実装のほうでカバーすることにしました。

2)については実装としては単に追加で0x00を1KBくらい送信するだけです。このデータはNeoPixelには解釈できないデータですので、NeoPixelを光らせる上では、あまり実害はありません。不可解なのは、FIFOのサイズ(256バイト)よりも大きな、1KBくらいのデータがシステム内部のどこかに滞留していてるように見えることです。データ自体の欠落はないので、どこかに保存されていたことは確かです。

PWMクラスに追加したメソッドは、

PWM.fifo_active(active)
PWM.fifo_write(buf1, buf2, timeout=1000)

の二つです。

fifo_active()はPWMの全チャネルを同時にイネーブル/ディスエーブルします。1つのFIFOを共有しているので、PWMは同期して動作させないとFIFOが空になるタイミングがずれてしまいます。MicroPythonで1チャネルずつ制御していては時間のずれが大きすぎるので、Cで実装しています。

fifo_write()は、buf1とbuf2を交互に(32bitずつ)FIFOに書き込みます。buf2は省略可能で、その場合はbuf1のみを書き込みます。また、buf1とbuf2のサイズが異なる場合は、短いほうのバッファは最後の32bit分のデータを繰り返し使用します。
timeoutの単位はマイクロ秒で、PWMの出力ビットレートを考慮してユーザが与える必要があります。

これを使って8×8のNeoPixelで虹色をスクロールするデモを作ってみました。
(sleepの値を大きくすると分かりますが、1行ではなく1ピクセルずつスクロールしています。)

今回は、NeoPixelにデータを送信する前にリセット信号を送るようにしました。
リセット信号は、50μsec以上の”L”です。

逆に言うと、信号線の通常の状態はプルアップなのですね。
プルアップから矩形波を送るには、いったん信号線をLに落とす必要がありますので、データ送信前に必ずリセット信号を付けるようにしました。


(2018/11/26追記:以下のツイートによると、最近の製品では280μsec以上という仕様に変更になっているそうです。)

また、PWMの設定で信号線の極性を反転し、データが0のときHが出力されるようにしました。
これに合わせて、NeoPixel用のPWMデータを生成するbyte2pwm()もデータを反転させています。

なお、クロックは前回の19.2MHz/6だと動作が安定しなかったので、少し遅くして19.2MHz/7 = 2.74MHzとしました。
シリアライザの1bitあたり0.3646us、NeoPixelの1bitあたりでは0.685MHz(1.46us)になります。

byte2pwm()を使ったデータ変換は少々重く、送信の度に毎回行うとフレームレートが上がりません。
そのため、あらかじめ変換したデータをmemoryviewで部分的に読み出して使うようにしました。

memoryviewは、書式上はスライスと同じなのですが、データを複製しません。
そのため、メモリ消費が少なく(memoryviewオブジェクトのみ)て済み、オーバーヘッドも小さくて済みます。

最後の無限ループの中で、実際にシリアライザに送信するデータを組み立てています。
マジックナンバーがいろいろ入っていますが、

64個のNeoPixel = 24bit * 64 * (1bitあたりPWMで4bit必要) = 6144bit = 768bytes

が実際に送りたいデータです。

一方、アニメーションとしては、6色(1色あたり2列)のデータをスクロールさせます。1列8個ですから、画面データは6色×2列×8個x3バイト=288バイト、加えて切れ目無くスクロールするためにバッファとして1画面(64×3=192バイト)分余計にデータを用意します。

これをPWMのデータに変換したものをbufに格納します。
スクロールの一周分のデータは、PWMでは4倍のサイズ(1152バイト)になります。

bufから1画面分(768バイト)のデータをmemoryviewで取り出し、先頭にリセット信号用のデータ(reset)、末尾にデータ追い出し用のダミーデータ(tail)を付加してFIFOに書き込んでいます。
tailの長さ996バイトは、正常に動作する量をカット&トライで決めました。

コメント