あみらぼ

電子工作がメインのDIYもの作り雑記

ESP32-WROVERで320x240のLCD全画素書き換え120fpsを達成した話

前回、ESP32-WROVERで320x240のLCDの全画素書き換えを、32fps出せたと書きましたが、その後、前々から考えてたアイディアを実装するため、PCBを発注して、プログラムの最適化も行ったところ、120fpsを出す事に成功しました!

 

まず、ESP32のDirectPortAccessについて、多数のピンの出力状態を同時に変えるには、GPIO_OUT_W1TCとGPIO_OUT_W1TSを使った2回のレジスタ書き込みが必要、という点がありましたが、GPIO_OUT_REGレジスタが、本当に使えないものなのか、いろいろ実験して調べてみました。

GPIO_OUT_REGはATmega328なんかと同様の、素直なDirectPortAccessを行えるレジスタで、一回の書き込みでほぼ全てのピンの状態を任意に変更できます。ただ、スレッドセーフじゃないので使用は非推奨とのことです。

要するに、GPIO_OUT_REGは書き換えたくないピンの状態も強制的に変えてしまうので、書き換えたくないピンがある場合は、ReadModifyWriteをしないといけなくて、それがスレッドセーフじゃないので非推奨という事です。

 

ESP32は、内部で特定の番号のピンを使って常にSPI-Flashと通信を行っているので、GPIO_OUT_REGを使うとSPI-Flashの通信をめちゃくちゃにしてしまいそうなので、GPIO_OUT_REGを使うのは論外と思っていたのですが…、ダメもとで使ってみたところ、特に問題もなく、あっさり動いちゃいました。

どうやら、GPIO_OUT_REGで書き換えられるのは、PinMode()でOUTPUTに指定したピンのみで、HWSPI等で使ってるピンは、たとえ出力のピンでもGPIO_OUT_REGからは影響を受けないようです。

試しに、VSPIを使いながら、GPIO_OUT_REGの書き込みを行ったり等の実験をしましたが、やはりVSPIは特に影響を受けませんでした。そもそも、VSPIに使用しているピンはdigitalWrite()を使っても状態を変える事は出来ないようです。

今まで、ESP32のGPIO Matrixがよくわかっていなかったのですが、ここまでの実験とか、ライブラリのコード読んだりとかで、あーそういう事かと何となくわかった気がします。

そもそもESP32はGPIO Matrixというものを一段挟むため、GPIOに対してDirectPortAccess行える方法は無くて、DirectPortAccessに近い事が行える、って感じなんですね。SPI通信に比べて、DirectPortAccessがかなり遅い事の理由がよくわかってきました。

 

逆に、ここまでの話で、GPIO Matrixの事とか、スレッドセーフじゃない事とかをわかって使うなら、GPIO_OUT_REGを使うのは、十分にアリなんじゃないかと思えてきました。

GPIO_OUT_REGを使う事で、今まで一回の8ビット転送にレジスタ書き込みが最低3回必要だったのが、2回で行えるようになり、転送を30%以上高速化することができました!

 

さらに、非常に初歩的な事なのですが、今まではサンプルコード通り、8ビット転送を1回、もしくはuint16_tを引数にとって8ビット転送を2回、行う関数を実装していたのですが、この関数をやめて、転送のforループ内に直接、8ビット転送を行うコードを書いたところ、8ビット転送をキッチリ10Mhzで行えるようになりました!多分可読性とかを考えると、インライン関数なんかを使うのが正しいと思われますが、その辺のコーディングのお作法とかは正直苦手です…w

オシロスコープで確認して本当にキッチリ10Mhzの波形がでたので、APBクロックの観点からも、恐らくこれがESP32のDirectPortAccess(っぽいもの)の最高速度な気がします。(もしかしたら、アセンブラを使ったりよりコードを最適化する事で、20Mhz出る可能性もあるかもですが…)

ここまでで、理論値で80Mbps(65fps)の転送を行えるようになり、実際に、60fps超えでの全画素書き換えを達成しました

 

これで、320x240のLCDを全画素60fpsで書き換える!という当初の目標は一応達成できたわけですが、LCDモジュールによっては偉大な先人が既にSPIで60fps書き換えを行っているようで、しかもSPIの方法はDMAで転送を行えて転送中もCPUのコアを他の事に使えるため、正直今回のこの方法は何のメリットもありません!(10Mhzなので配線パターンとかノイズに強いとかはちょっとメリットかもですが…)というわけで、転送をより高速化するべく、前々から試してみたかった、16bitパラレル転送に挑戦してみる事にしました。

 

以前書きましたが、FPCの「LCDモジュール」には数十本の端子があり、そのうちの最低限の端子を出したものが「LCDシールド」や「LCD基盤」として売られています。8ビット転送に対応したFPCのLCDモジュールは、たいてい16ビット転送にも対応していて1枚数百円程度の安価で入手もそれほど難しくはありません。ただ問題は、LCDモジュールの端子のピッチと本数の条件を満たすFPC/FFCコネクタや変換基盤の入手が非常に難しく、専用の基盤を自作するなり発注するなりして用意しなくてはいけない点です。

また、ESP32-WROOMやESP32-WROVERは、SPI-Flashが使っていて実質使えない端子があったり、入力専用の端子があったりで、見た目より実際に出力に使える端子が少なく、16ビットのパラレル転送を行おうとするとそのままではピンの数が足りない、という問題もあります。

 

これらの問題を解決するため、以前から興味があったPCBの設計と発注を行ってみました!送料をケチったのとコロナの影響で、届くのが大幅に遅れ、届いた時には過去の自分の設計を見直すところから始まりましたが…。ちなみに、ピンの数が足りない問題は、8ビットDラッチを使う事で解決しました。転送毎に書き換える16本+WR1本の計17本はLCDモジュールと直結し、その他の1フレームに1回程度の頻度でしか書き換えない端子はDラッチ経由で接続する事で、速度にはほぼ影響しないという設計です。

f:id:amilabo:20200724204359j:plain

今回の目的に使う部分は真ん中で、左右はスペースが余ったので、いろいろ試しのパターンを入れてみた感じです。

ど真ん中にあるのが、Dラッチを実装する箇所で、下がFPCのLCDモジュールを実装する箇所です。端子間隔が1mmピッチのLCDモジュールと0.7mmピッチのLCDモジュールの、2種類のモジュールが手元にあって、両方でテストをしたかったので、両方に対応するため、パターンが少し複雑になっています。あと、タッチパネル対応のLCDなので、16bit転送とタッチパネル読みを共存させるための集合抵抗を実装できるパターンも作っておきました。

左側が、スイッチとかLDOとかを実装するかもと思って作ったパターンで、右側が、今後の参考にと、いろんなサイズの穴径とランド径のパターンを作ってみました。あと、ESP32-WROVERの実装部分には、使える端子を全部出して手軽にはんだ付けできるようにしてみました。白線の部分で切り取ると、非常にコンパクトで全端子が使えて、ブレッドボードの両端も使えるESP32-WROVERのピッチ変換基盤になるという寸法です。(一応PCBアンテナの部分は両面とも銅箔が一切無いようにはしてありますが、公式の推奨パターンを見ると、この作り方は、ちょっとお行儀がよくないみたいですね)。基盤の発注とかはせずに、ユニバーサル基盤で自作の機器を作りたくなったら、ここを切り取って直接コンパクトに実装できるかなと思ってます。(おそらくいないと思いますが、同じ基盤が数枚余ってるので、ランド径のサンプル部分が欲しいとか、WROVERの変換ボードとして欲しいとかいう方がいらっしゃいましたら、お譲りします。)

 

f:id:amilabo:20200724210535j:plain

必要なパーツを実装したところ。1か所、DラッチのOEの端子にミスがあって一瞬絶望しましたが、運よくパターンカットとはんだ付けで回避できました。

 

と、いうわけで、16ビットパラレル転送の実装ですが…。以前、8ビットパラレル転送をする際に、速度を出すために、uint8_tの値からレジスタに書き込むべきuint32_tの値を計算無しで一発で引くために、uint32_t lut[256]という感じで256要素のLookup tableを作り、事前に値を仕込んでおくという方法をとりました。これをそのまま16ビット転送にすると…、uint32_t lut[65536]で、Lookup tableのサイズが256KB!となり、とても実用的ではありません。かといって、uint16_tからuint32_tの変換を毎回ちまちまやっていたのでは、16ビット転送の効果が薄れるどころか、逆に8ビットより速度が落ちるかもしれません。

 

そこで、65536色から256色を選んだカラーパレットを用いて、画面バッファを8bit/pixelで作るという、昔ながらのカラーパレット作戦を取ることにしました。(実は、これが一番やってみたかった事で、後述しますがいろいろメリットもあります)

1度の画面に同時に256色までしか出す事はできなくなりますが、ずっと固定の256色ではなく、カラーパレットなので、表示内容によっては実質16ビットカラーとほぼ変わらない表示を出す事もできます。

なにより、この方法を取る事で、Lookup tableが8ビット転送の時と同じ、uint32_t lut[256]ですむという事と合わせて、速度低下も一切なく、8ビット転送のキッチリ2倍の速度が出る事になります。8bitカラーから16bitカラーへの変換と、uint16_tから適切なuint32_tを求める計算の両方が、Lookup table一回引くだけで行えるというのが、なんとも気持ちがいいです。(そう思うのは自分だけですかねw)

 

というわけで、これらを実装した結果、理論値が16bit×10Mhz=160Mbps、フレームレートが130.2fpsの転送を行えるようになりました!実測では123~125fps程度の転送速度となり、LCDモジュール側のリフレッシュレートを120fpsに設定することで、安定して120fpsで全画素書き換えを行う事に成功しました!(ついでに、LCD基盤にはあまり出ていないTearingEffectのピンも使えたので、LCDモジュールのリフレッシュレートとしっかり同期を取って、Tearingも一切起こらないようにできました!)

 

 

また、以前に書いた問題で、ESP32はSRAM512KBあるけど、320×240のバッファ2枚はSRAM上に取れないという問題がありましたが、8bit/pixelになった事で、320×240のバッファ2枚をそれぞれ連続領域のメモリでSRAM上に取ることができました!

 

120fpsは出ましたが、やはり1枚数百円のLCDの応答速度が貧弱なのか、正直60fpsと比べて劇的にヌルヌル見えるかといわれると、何とも言えません。実際には、60fps辺りで使うのかなと思いますが、最大で120fpsまで出るので、80fpsにするとか、可変fpsにするとか、そういう使い方もいいかなーと思います。

 

最後に、この方法のメリットとデメリットをまとめておきます。

メリット

・最大120fpsまで出せる

・Dラッチにより、LCDへの転送を行っていない間は、SPI通信するなりボタン入力読み取るなり、ほとんど全部のピンを自由に使える

SRAM上にダブルバッファを取れるので、描画のプログラムが簡単になる

・8bit/pixelなので、描画等の演算にかかる負荷が少なくなる

・事前に8bit/pixelでデータを作成しておけば、データをSDカードやPSRAMから読んだりする時に、転送時間が半分で済む

・画像バッファの内容は変えずに、カラーパレットのみ変更する事で、超低負荷で表示内容を変えたりフラッシュのエフェクトを出したりの、昔のファミコンとかでやってたような面白い工夫ができる??

デメリット

・1画面に同時に256色までしか出せない

・カラーパレットのために1KBのメモリの確保が必要

・16ビット転送のため繋がなければならない線が増えて、配線が複雑になる

・カラーパレットなので、隣同士の色をブレンドしたり等の画像処理が困難

LCDへの転送中はCPUのコア1つが完全にその処理に制約される。(例えば60fpsの場合、CPUリソース全体の最低4分の1は転送に制約される)

 

こんなところですかね。良い面も悪い面もありますが、自分としてはダブルバッファが取れる事とか、画像データの転送が半分で済むとかが結構メリットに感じるので…。あらためてケースに入れる事や、他のボタン入力やSPI通信を行う事ができるような基盤を設計して、続きをやりたいところなのですが…。ちょっとたまーに生じる不可解な現象に頭を悩ませています。どう問題を切り分けていこうか考えて時間がかかってるのですが、どうしても解決できなさそうな場合は、次回ブログに詳しく現象を書いてみようかなと思います。(コメントが無くても、自分で状況を文章とかにまとめる事で、なにか解決策を閃くことも結構ある気がするので…)