あみらぼ

電子工作がメインの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通信を行う事ができるような基盤を設計して、続きをやりたいところなのですが…。ちょっとたまーに生じる不可解な現象に頭を悩ませています。どう問題を切り分けていこうか考えて時間がかかってるのですが、どうしても解決できなさそうな場合は、次回ブログに詳しく現象を書いてみようかなと思います。(コメントが無くても、自分で状況を文章とかにまとめる事で、なにか解決策を閃くことも結構ある気がするので…)

ESP32でのLCD描画あれこれ

さて、前回、ESP32のDirectPortAccessが遅い事に苦戦しながらも、何とか240x320の全画素書き換えを32fpsで行う事ができました。目標の60fpsには遠く及ばないものの、何とか実用レベルにはなるかも?といったところです。

 

しかし、新たな問題が…、240x320(=150KB)のバッファを2枚用意して、デュアルコアの方法で1枚のバッファをLCDに出力、もう一つのコアでもう一枚のバッファを更新(作成)、というダブルバッファを考えていたのですが、ESP32ではSRAM上に150KB×2のバッファは取れない様子…、ESP32のSRAMは512KBあるものの、通常の設定では少なくとも64KBはキャッシュに使われてたり、512KBのSRAM領域はいくつかに分かれていて、メモリアドレスが連続ではないなどが理由と思われます。

 

んー、これは困りました。更新を1行ずつ順に行うのであれば、シングルバッファでも問題ないかもですが、更新する場所がいろいろ変わる場合や、描画を重ね書きして絵を完成させるような場合は、その作成途中の絵が出力されてしまうのは、ティアリングが出るのと別次元で大きく崩れた絵が出るので、非常にまずいです。LCDへの出力が何行目まで終わったかを確認しながら、書き換えても安全な場所だけを更新する、という方法も、なくはないかもですが、それは描画のプログラミングを著しく困難にさせる上、速度的にもコストが大幅に増して描画できる内容も大きく制約を受けるので、今回考えてる最終的な目標を考えると、シングルバッファは絶対にやりたくないところです。

 

まぁダメもとですが、今使ってるのはESP32-WROVERですのでPSRAM領域が8MBあります!今のところ実際に使えるのは4MBまでとかの制約はあるようですが、少なくとも、300KB程度のバッファを確保するのは余裕です。ただ、PSRAMはSPI-Flashと共有のSPI接続な上に、2つのCPUから読み込みと書き込みを同時に行うので、そこのメモリバンド幅で速度がかなり落ちる事は容易に想像できます。で、ダメもとでPSRAM上にダブルバッファを取って、片方のCPUで書き込み、片方のCPUで読み込み&LCD出力を試してみた結果…、fpsになりました!

いくらなんでも遅すぎませんかね…?PSRAMはP(pseudo:疑似的)SRAMで、内部的にはDRAMらしいので、読み書きタイミングとか、メモリバスの問題とかあるかもですが、それにしても遅すぎる気がします…。試しに、片方のCPUは何もせずに、1つのCPUでPSRAMからの読み込み&LCDへの出力だけを行ってみたところ、23fps程度の速度が出たので、同時アクセス時に特に速度低下が顕著になる様子です…。

で、原因を調べてる中で偶然見つけたのですが、現在のESP32にはPSRAMの読み書きにエラッタ(HWバグ)があるようです!

概要としては、

f:id:amilabo:20200412022406j:plain

  • CPUが特定の順序で外部SRAMにWrite/Readを行った際に予期しない動作となる場合がある

というもので、今回のケースで思いっきりこのエラッタを踏んでいるような気がします。ただ、これに関してはソフトウェアのワークアラウンドがあるようで、プログラムのコンパイル時にこのエラッタで問題が起こらないよう、修正を加えた形でコンパイルがされるようです。確証はありませんが、画乱れ等は起こらない一方で、速度が著しく低下するのは、このワークアラウンドが影響している可能性が高いのではないかと思います。

ちなみに、今年の初め辺りから出荷が開始されているらしい、末尾に「E」がつくバージョンではこのエラッタは無くなった(シリコンウエハレベルでHW的に修正された)ようです。コンパイラの対応がいつ頃されるのか、既にされているのかは不明ですが、「ESP32-WROVER-E」ではいずれこの速度低下は改善されそうです。

この辺の情報って日々更新されると思うので、あえてリンクは貼りませんが、詳細が気になる方は「ESP32 ECO」とかで検索してみると、最新の情報がいろいろわかると思います。

この速度低下の原因を探している中でエラッタを見つけたので順番は前後していますが、以前のブログにESP32の新バージョンの情報を書いておきました。

 

amilabo.hatenablog.com

 

ただ、ESP32-WROVER-Eで速度がある程度改善されたとしても、PSRAM上にダブルバッファでは30fpsもかなり厳しそうなので、他の手を考える必要がありそうです。

デュアルコアをあまり生かせないのは残念ですが、LCDへの転送をもっと早く行えれば、シングルバッファでLCDへの転送が終わってからバッファを更新して、更新が終わったらLCDへの転送、という手も無くはなさそうです。幸い、LCDへの転送中にもう片方のCPUがSRAMを読み書きする分には速度低下は起こらなさそうなので、LCDへの転送中はもう片方のCPUが転送中のバッファを触れないような準備とかを行い、LCDへの転送が終わったら、デュアルコアで大急ぎでバッファを更新、という感じになるでしょうか。

 

いずれにしても、LCDへの転送の高速化は必要そうなので、前回以上に高速化ができないかを試してみました。前回試して上手くいかなかった以下の方法について、

  • 8ビットデータの0に対応する部分とWRピンに対応するビットをGPIO_OUT_W1TCで書き込む(必要なビットを0にすると同時にWRピンもLOWになる)
  • 8ビットデータの1に対応する部分とWRピンに対応するビットをGPIO_OUT_W1TSで書き込む(必要なビットを1にすると同時にWRピンもHIGHになる)

この方法だと、2回のレジスタ書きで済むのですが、LCD側では、WRピンの立ち上がりと同時に8ビットデータを読み込むので、さすがに正しくデータが伝わらなかったわけです。

そこで、WRピンの立ち上がりを遅らせれば、何とかなるかも?という事で、いろいろ試してみました。まず、ESP32の出力とLCDのWRの入力の間に、手元にあったFETをはさんでバッファ回路を作り、信号遅延がいい感じにならないか試してみましたが…、そもそもFETの入力容量600pFの時点で圧倒的に信号が遅れて話にならない感じでした…。遅い遅いとは言ってるものの、メガヘルツレベルでの伝送を行っているので、ほんの少しの容量変化でも大きく影響を受けるようです。いろいろ試した結果、ESP32とLCDのWRの間の接続を60cmくらいの長さのワイヤでびよーんと繋ぐと、何とか絵が乱れず、32fpsから41fpsに高速化する事ができました!ただ、動作は非常に不安定で、1mのワイヤだとダメだったり、60cmでもちょっとくるくる巻いたりすると容量が増えてダメだったり…、といった感じでした。ワイヤの長さではなく、数p~数十pのコンデンサをたくさんの種類買ってきて、ちょうどいい遅延になるような容量を探せば多少は安定するかもしれませんが、波形もひどい事になってるでしょうし、もし動いたとしても、正直、こんな怪しげな方法は絶対採用したくありませんw

 

とゆうか、今回試したことで、今たまたま動いている配線や方法も、ちょっとした事で伝送やその他の不具合が起こるんじゃないかと、いろいろ不安になってきました…。

 

実際、信号がどの程度ちゃんとした矩形波になってるかとか、Duty比はどのくらいになってるかとか、電源はどの程度安定しているのかとか…いろいろ気になってきます。今までも、オシロスコープがあったら…という事はたびたびありましたが、テスターやICレコーダーやテストの工夫等で「間接的に」波形を観測できていたつもりですが、さすがに限界を感じています…。

#後日、結局オシロスコープを買いましたw 超絶便利すぎて、世界が変わりました!

 

さて、長くなってきたので今回はこの辺で。この後、ESP32に関する意外な発見とか、それを使ったアイディアとか、先人の知恵とか、ある程度の妥協とかで、ついに75fpsが出せる見込みが立ちました!

 

次回は、その辺の詳細について書ければと思います。

 

ではでは。

ESP32で240x320のLCDを毎フレ全画素書き換え!

さて、以前にESP32-WROVERで240x320のLCD60fpsでヌルヌル動かすという目標を立て、理論値で最大94fpsまで出せそうと書きましたが、案の定、いろいろ苦戦しました。

 

そもそも、240x320の60fpsって書いても、データの転送レートがいまいちピンとこないので、転送レートを計算してみます。ちなみに、今回使用するLCDはPC等で一般的なRGB8ビットずつと違い、R5ビットG6ビットB5ビットで1画素あたり16ビットカラーとなっております。LCDドライバに内蔵できるメモリ容量などの都合も関係しているのかとは思いますが、個人でマイコンから使用するLCDとしてはまぁ妥当でしょう。一応、R6ビットG6ビットB6ビットの18ビットカラーも設定により使用できるのですが、1画素18ビットはマイコン側でのメモリ管理が面倒そうですし、人間の輝度感覚からしてRedやBlueの諧調を細かくしても、あまり変化はなさそうなので、今回は見送ります。

で、計算すると1フレームの容量は240x320x16=1.2288Mbit(150KByte)です。

これを1秒間に60フレーム転送するので、1.2288x60=73.728Mbpsが、最低限必要な転送レートになります。ESP32が240Mhzで動作しているとはいえ、SPI通信でこの速度を出すのはちょっと厳しそうですね。もしESP側のSPIでこの速度を出せても、LCDモジュール側では早すぎて受け取れないので、SPI接続で60fpsはそもそも理論上からして不可能です。

ただ、今回は8ビット(もしくは16ビット)のパラレル転送を計画しているので、転送する回数としては1/8の9.216M回の転送を1秒間に行えればよい事になります。ESP32が240Mhzで動作するので、一回の転送を38.6cycle以下で行えればいいわけなので、まぁまぁ何とかなりそうな感じです。

 

で、とりあえずダメもとでdigitalWrite()の関数を使ってまずは画面を出すとこまでやってみました。digitalWrite()はいろいろなチェックや計算を経由するので遅いというのは承知の上なので、もしそれで60fpsが出ればラッキーという感じで。で、結果は案の定fps程度…。まぁこれはしょうがないですね。

と、いうわけでDirectPortAccess(DPA)の出番です。名前はなんか大げさですが、やる事は大した事はありません。digitalWrite()の関数を使うかわりに、各ポートのHIGHかLOWかの出力状態を制御しているレジスタに直接値を書き込んでしまうわけです。Atmega328PのArduinoでも、この方法を使って10倍以上は高速化できました。同じことをESP32でもやろうと思います。で、該当レジスタを探したところ、ESP32で最も高速にGPIOの出力を制御するには以下のレジスタを使うのが推奨のようでした。

f:id:amilabo:20200322175101j:plain

ESP32のDPAは情報が少ないようで、このレジスタを使う、という所にたどり着くのも少し苦労しましたが、レジスタ自体にも使い方にちょっとクセがある感じですね。

わかってしまえばどうという事は無いのですが、1つのレジスタで直接0か1かを指定して書き込む事はできないようです。(一応そういうレジスタもありますが、後述の理由により非推奨のようです)

まず、1を書き込みたいポート番号のビットを1にしてそれ以外を0にした32bitの値を作って、それをGPIO_OUT_W1TSに書き込みます。(TSの”S”は”Set"の意味ですね)

次に、0を書き込みたいポート番号のビットを1にしてそれ以外を0にした32bitの値を作って、それをGPIO_OUT_W1TCに書き込みます。(TCの"C"は"Clear"の意味ですね)

最終的に各ポートの出力を特定の状態にしたいのであれば、GPIO_OUT_W1TSを先に書くかGPIO_OUT_W1TCを先に書くかの順番はどちらでも構いません。

なんでこんなことになってるかというと、ESP32はシングルスレッドのみではない事が関係しているようです。GPIOの出力状態を0と1で直接指定するレジスタもありますがそのレジスタを読んで、必要な部分を書き換えてそれ以外の部分はそのままにして書き戻す、という事をすると、読み込んで~書き戻す、の間に他の誰かが同じレジスタを操作していると、それを破壊する事になりますし、逆もまたしかりです。いずれにしても、ReadModifyWriteは余計に時間がかかりそうですし、SPI-Flashがポートを使っている事を考えると、ReadModifyWriteするのはあまりに危険そうなので、ちょっと面倒でも上記の推奨のレジスタを使うしかなさそうです。(後々に調べた結果、この認識はちょっと間違いがありました…)

 

と、いうわけで、何とかこのレジスタを使う方法で実装をしてみましたが、予想以上に速度が出ません。digitalWrite()よりはもちろん早くなりましたが、60fpsどころか、30fpsにもとうてい及びません(絶望のあまり、正確なfpsをメモするのも忘れましたw)。原因は主に以下の3つかと思われます。

  • そもそもレジスタへの書き込みが遅い!
    Atmega328Pと比較して、特にPeripheralレジスタへの書き込みが遅いようで、通常のメモリWriteにかかる時間とかなり差があり一回のレジスタ書き込みにかなりのCycleを要しているようです。
  • GPIOの出力を変化させるのに2回のレジスタ書きが必要!
    上記の通り、推奨の方法では2種類のレジスタにそれぞれ書き込みを行う必要があります。Peripheralレジスタへの書き込みが遅い事と合わせると、1回で書き込みを済ませられない事も速度に十分影響してきます。
  • レジスタに書き込むデータを作るのに時間がかかる!
    Atmega328Pでは、1つのレジスタが8ビットで、かつポート毎にいくつかのレジスタに分かれていたので、書き込みに使うポートを上手いこと選べば、何も考えずに1バイトのデータを直接1つのレジスタに書くだけで済みました。ただ、ESP32ではレジスタが32ビットにまとまってしまっている上に、使えるポートの番号も飛び飛びなので、8ビットのデータを書き込むのに、各ビットを取り出してそれぞれを適切な場所にビットシフトして…、という計算が必要になります。これも、8ビット分となると、速度に十分影響してきます。

これらをどうにかしていかなければならないのですが、1つめはどうしようもなさそうなので、2つ目と3つ目を、まずは少しましになるよう最適化することにしました。

まず、8ビットデータの転送方法として、LCDモジュールのWRというピンがトリガになってるので、順序としては次のようになります。

  • WRピンをGPIO_OUT_W1TCでLOWにする
  • 8ビットデータの1に対応する部分をGPIO_OUT_W1TSで書き込む
  • 8ビットデータの0に対応する部分をGPIO_OUT_W1TCで書き込む
  • WRピンをGPIO_OUT_W1TSでHIGHにする

予想ではESP32が早すぎるので、要所にDelayやNOP等で待ちを入れないとLCDモジュール側でデータを受け取れないと思っていましたが上記の通り、レジスタへの書き込みが非常に遅いので、特に待ちを入れなくても動きました。(というか待ちが必要ないほどESP32が遅すぎる…)

で、LCDモジュール側でデータを受け取るタイミングはWRピンがHIGHになった瞬間なので、WRピンをLOWにするのは、いつでもよさそうという事で、WRピンをLOWにする処理もまとめて以下のように変更しました。

  • 8ビットデータの1に対応する部分をGPIO_OUT_W1TSで書き込む
  • 8ビットデータの0に対応する部分とWRピンに対応するビットをGPIO_OUT_W1TCで書き込む(必要なビットを0にすると同時にWRピンもLOWになる)
  • WRピンをGPIO_OUT_W1TSでHIGHにする

これで、レジスタの書き込みが4回必要だったのが3回で済むようになって、少し早くなりました。(ダメもとでWRをHIGHにする処理もまとめてレジスタの書き込みを2回にしてみましたが、やはりWRピンがHIGHになる瞬間にデータが準備されていないといけないようで、さすがにそれはダメでした)

 

次に、レジスタに書き込む32ビットの値を作るのに時間がかかる方の問題についてですが、1ビットずつ処理せず、まとめてごそっとシフト演算を済ませられないかと連番になってるポート番号が無いか探してみましたが、残念ながら8ビット連番になってるポート番号は無いようです。前半の0~15辺りは、間は挟むものの連番の部分が多いのでそこで処理を減らそうかとも考えましたが、ここで使うポートを決め打ちしてしまうと、のちのち使いたいポートがそことバッティングた時に面倒な事になりそうなので、連番を使った高速化は避ける事にしました。

そこで、あまりスマートとは言えないかもしれませんが、32ビット×256のLookup tableを事前に定数として仕込んでおくことにしました。何をしたかというと、ここでやるのは8ビットのデータを32ビットのデータに変換する事なので、パターンとしては0x00~0xFFの256種類に絞られる事となります。なので、uint32_t table[] ={.......}という感じで256種類全てのパターンで32ビットデータをどうすればよいか事前に計算して定数としておき、転送時にはそのLookup tableの1つを読み込むだけ、という方法をとりました。Lookup tableが4Byte×256=1KByteなので、かなりメモリの無駄遣いは発生しますが、ESP32のSRAMが512KBあるので何とか許容範囲かと思います。(Atmega328PだとSRAMが2KBなので、このLUTだけでSRAM全体の半分も使いますw)

加えて、LUTを使えば、8ビットで使用するポートは飛び飛びでもかまわず、後からでも自由に変えられるというメリットもあります。(さらに、後日書くかもですが、16ビットパラレル転送時には、別の素晴らしいメリットがある場合もあります)

 

これらの最適化で、SRAM上に用意した1フレーム分の画像データ(150KB)の全てを毎フレームLCDモジュールへ転送する処理で、何とか32fpsを達成する事が出来ました!

当初の目標の60fpsでヌルヌルはまだまだ及びませんが、30fpsを超えられた事は実用できるラインとしてかなり大きいです。

ただ、このままでは毎フレーム同じ絵を転送しているだけなので、絵を書き換える処理をしないと、何も転送していないのと変わりません。幸いなことに、ESP32はデュアルコアでSRAM512KBもあるので、150KBのバッファを2枚とって、片方のコアで1枚のバッファをLCDへ転送、もう片方のコアでもう一枚のバッファを作成、次のフレームではそれぞれのコアが使うバッファを入れ替え(フリップ)する、というダブルバッファの方式を採用すれば、何とかなりそうです。

 

と、思っていましたが、ESP32では150KBのバッファ2枚をSRAM上に取ることはできませんでした!少なくとも64KBはキャッシュとして使われている事や、ヒープとして取れる領域に制限があるなどいろいろ理由はあるかと思いますが、もし300KBを取れたとしても、SRAMがあまりにギチギチになってしまうので実用的ではなさそうです…。と、いうわけでこの先も困難が続くわけですが、長くなったので続きは次回

 

実は、この辺の事はしばらく前にやった事なのですが、なかなか記事に書く気が起きず時間が経ってしまいました…。というのも、この辺の話って特に興味のない人とっては、文字ばかりで写真も絵も無くてつまらないのです。もともと、ほぼ需要の無い話ばかりを書いてるブログなのですが、さらに誰にも興味の無いブログになりそうで…。この先も、同様の話が続く予感ですが、まぁ世の中の誰か数人くらいは見てくれるかも、という気持ちで続けていこうと思います!

 

ではでは。

ESP32の新バージョンのチップが登場?

さて、前々回、ESP32-WROVERを使って240x320のLCD60fpsでヌルヌル動かすという目標と共に、計算上の理論値で最大94fpsまで出せそうだし、60fpsならきっと余裕だよね、みたいな事を書きましたが、(案の定)そう上手くはいかず見事に苦戦しています。

 

苦戦した果てに、いろいろ最適化を工夫して、8ビット接続でも30fpsは達成して、16ビット接続のLCDモジュールなら60fpsもなんとか達成できそうな目途がたちました。どうやったかはかなり長くなりそうなので詳細は後日として、そもそも94fpsはかなり夢物語だったんですね。ネット上を見ても、毎フレーム全画素書き換えはせいぜい15fps程度みたいで、30fpsはなかなか厳しく、60fpsはほぼ不可能、といった様子ですね。30fpsで妥協してもいいのですが、他の人ができてないの知ると、逆に意地でも60fps出したくなりますw

 

で、今日はその途中で偶然見つけたESP32に関する興味深い話。ESP32のウエハレベルでの新バージョンが今年のはじめ辺りから登場しているようです。日本語で検索しても、ほとんどその情報は見かけないので、ドキュメント読んで確認した、現バージョンとの違いについて書いておきます。

 

そもそも、チップの更新は今回が初めてではなくて、現在もESP-WROOM32とESP-WROOM32Dが両方流通してたりしますね。モジュールの型番と、内蔵ESPの型番やリビジョンの違いがかなり複雑になっていて、非常にわかりづらいです。今使ってるのも、ここではESP32-WROVERと呼んでいますが正確にはESP32-WROBER-Bですね。従来の製品間でも細かな違いはあるようですが、個人的に一番気になったのは、Rev0のチップには「同じペリフェラルアドレスに連続して書き込むと値が化ける」というエラッタでしょうか。ちょうど、LCDを表示するときなんかに、このエラッタをもろに踏んで原因の究明に苦労した方もいらっしゃるようです。

 

で、今回新しく生産が始まったのは、ESP32-D0WD-V3というチップのようです。公式でも、Recommendedと強調されていることから、今後はWROOMもWROVERもこのチップを搭載したものに統一したいといった感じでしょうか。実際に購入するモジュールの方ですが、このチップが搭載されたモジュールは「E」が付いた型番になるようです。恐らく日本の電子部品屋や通販では、ESP-WROOM-32Eとか、ESP32-WROVER-Eなどという名前で販売されるのではないかと思います。

 

で、肝心の従来チップとの違いは、5つほどの変更点があるようですが、個人的に気になるのは次の2つのエラッタが修正されたことです。(詳細が気になる方は、「ESP32 ECO V3」で検索してみてください。リンクがいつまで有効かわからないので、あえて直リンクは貼りません)

  • CPUが特定の順序で外部SRAMにWrite/Readを行った際に予期しない動作となる場合がある
  • デュアルコア使用時にそれぞれのCPUが異なるアドレス空間を読み込んだ際に予期しない動作となる場合がある

両方とも、いくらでも簡単に踏みそうなエラッタですが、ユーザーが書いたプログラムがこれらのエラッタを踏みそうな場合は、ESP-IDFの方でこれらのエラッタを回避するためのソフトウェアのワークアラウンドが追加されるようなので、特に表向きに明らかなバグが出たりすることは現状でも起こらないようになっているようです。なので、ユーザー側のプログラムは特にチップの更新を意識する必要は無さそうです。ただ、これらのエラッタが修正されることによりV3のチップを使用する場合は、ソフトウェアワークアラウンドが必要なくなるので、実行する処理によってはパフォーマンスが向上する可能性は十分考えられます。

 

確証はありませんが、LCDを60fpsで動かすためのテストをしている中で、極端に速度が落ちる場合などがあり、どうもこれらのエラッタワークアラウンドが関係しているような気がしています。なので、今使ってるESP32-WROVER-Bはテスト用と割り切って、最終的に本番を作る際には、V3チップが搭載されたESP32-WROVER-Eを使った方がよさそうな気がしています。

 

にしても、様々な組み合わせで、ESP32のモジュールの種類が大変な数になってきていますね。(気になる方は、「Espressif Products Ordering Information」で検索してみてください)

  • デュアルコアorシングルコア
  • チップの種類が古い物から順に少なくとも3種類
  • PSRAM無しorPSRAM有り
  • 無線アンテナ内蔵or外付け
  • SPI-Flashサイズが4MB or 8MB or 16MB

上記の選択がそれぞれ独立な上に、これ以外にもいろいろチップの種類があるので、もはや組み合わせ爆発状態ですw

日本で手軽に手に入るのは、SPI-Flashサイズが4MBのモジュールだけのようですが、16MBのモジュールも値段がほぼ変わらないようなので、V3チップの16MB SPI-FlashのWROVERが秋葉原とかで安く買えるようになるとありがたいですね。

 

以上、ESP32の新チップに関して、個人的に調べた情報でした。

需要があるかは全くわかりませんが(恐らく需要は無いとは思いますが)、次回は毎フレLCDピクセル更新を60fpsで行うために考えた工夫なんかを紹介できればと思っています。

 

ではでは。

ESP32でLCDモジュールを使う(2)

さて、前回、購入したLCDシールドのとんでもない仕様により、WROVERを壊しかけましたが、運よく繋ぐ前に気づけた事で、最悪の事態はさけられました。

 

引き続き、LCDシールド上のLCDモジュールをテストしていきます。まず、絵がでないとLCDモジュールを使ってる感じがしないので、atmega328p用のサンプルコードをちょこちょこいじって、ESP32-WROVERから絵を出すところまではなんとかできました。サンプルコードではSDカードから画像を読んでますが、既にLCDシールドとWROVERとの配線が大変な事になってるので、これ以上線を増やさないため、画像はSPIFFSに置いてそこから読み込みました。

f:id:amilabo:20200304110726j:plain

あの配線に繋ぐため、ブレッドボードのどこがWROVERのどこに繋がっているか、目がチカチカしながら何度も確認しつつ接続して、何とか絵を出す事に成功しました。やっと、WROVERを使ってる、って感じになってきましたね。

正直、ここまでこれただけで結構な達成感ですw

 

LCDシールドの仕様上、一般的な画像のRGB24ビットカラーではなく、Red5ビットGreen6ビットBlue5ビットの16ビットカラーなのですが、それほど諧調に違和感もなく、それなりに綺麗に絵が出ました。YUV422なんかで24ビットを16ビットに圧縮する方法があるのは知ってましたが、RGBそれぞれからビットを落とすシンプルな方法もこんな所では使われているんですね。RとBに比べて、Gに1ビット多く割り当ててるのがミソですね、GをYと近似して、人間の知覚にできるだけ影響しないようにしているのでしょう。

 

さて、絵が出て一安心したのもつかの間、ディスプレイの見え方が何か変な事に気づきました。まぁ、安物のディスプレイなので、視野角が狭いのは承知の上だったのですが、見る角度によって、とてつもなく輝度や色合いが変わってしまう角度があるようです。ちょっと写真からは伝わりづらいのですが、画像を載せてみます。

f:id:amilabo:20200306121612j:plain

写真だと、変な筋が見えたりでイマイチに見えますが、肉眼では左2つの画面は、わりと綺麗に見えています。写真でこうなってしまうのは、恐らくRGBのピクセル配列とカメラの解像度との関係でエイリアシングがおきているのが原因で、LCDモジュールの性能とは無関係なはずです。問題は、右側の画像、ほんのちょっと傾けただけで明るさがかなり暗くなり、色もぐちゃぐちゃで酷い見え方になってしまいます。一方で、反対方向に傾けたときは少し画面が明るくなる程度で、正面と同じか、むしろ正面より綺麗に見えるくらいです。上下方向に傾けた場合も試してみましたが、視野角が良好とまではいかないまでも、写真右のような酷い見え方にはなりませんでした。

 

はて?なぜかと気になったので、何とかこのLCDシールドに使われているLCDモジュールの情報が無いかと探し回ったところ、それらしいものを見つけました。ちなみに、「LCDシールド」とか「LCDモジュール」とかの違いで混乱している方もいるかもしれないので少し説明しておきます。まず、LCDの画面そのものには、全ての画素を操作するために1000本近くの端子があり、それを直接はんだ付けしたりすることはできないので、LCDの画面単体で売られていることはまずありません。実際に購入できるのは、そのLCD画面の1000近くの端子に「LCDドライバ」というICが接続されてユーザーが制御するための20~50ほどの端子がFFC(フレキシブルフラットケーブル:ペラペラの中に何十本も線が平行に通ってるケーブル)から出ている物で、それを「LCDモジュール」と呼んでいます。さらに多くの場合、電子工作でFFCを扱うのは難しいのでFFCと基盤が既に接続されていて、さらにその中から特に必要な端子のみを10~20程度出して、LCDモジュールを基盤に固定した「LCD基盤(ボード)」として売られています。さらにその中でもarduinoにそのまま乗せられる基盤を「LCDシールド」と呼んでいるようです。ただ、その辺の呼び方はあいまいで、基板の状態でも「LCDモジュール」と呼んでいる場合もあるようです。

以下は今回購入した「LCDシールド」に乗っている「LCDモジュール」の仕様画像(と思われるもの)です。LCDシールドに出ている端子は15本ほどですが、実際には以下のように37本の端子のFFCLCDシールドの基盤と接続されているという事ですね。 

f:id:amilabo:20200306132219j:plain

 で、この仕様画像から、LCD見え方がなんか変な原因がわかりました。このLCDシールドは明らかに横画面として売られていましたが、実際に乗っているLCDモジュールは、明らかに縦画面なんですね。しかも、上記の画像から「View direction」がはっきりと示されているようです。要するに、このLCDは縦画面でしかも正面より少し傾けた状態で見る事が前提となっていて、それ以外の角度から見る事はほとんど想定されていないんですね。たぶん、昔のガラケーのような感じの縦画面に使うのを想定しているんですね。そう考えると、想定された視野から少しでも外れると極端に見えづらくなるのは、のぞき見を防止する観点ではありがたい事で、あえてそうしているのかもしれません。

 

ただ、個人的にはこのLCDは横画面として使おうと思っていたので、この仕様はかなり致命的です。上下方向に傾けた時の視野角が狭いのはまだしも、左右方向に傾けた時の視野角が全く違って、片方の視野が壊滅的なのはあまりに残念です。ベストポジションで見ようと思ったら、画面を正面より左(実装によっては右)に傾けて見る必要がある、という、なんともヘンテコなものができあがってしまいます。

この件で、改めていきなり本番用のLCDモジュールを買わずに、格安のLCDシールドを練習用として買ったのは正解だったと感じました。「View direction」に注目して改めていろいろなLCDモジュールを探したところ、個人で購入できるようなLCDモジュールのほとんどは同様にView directionがあるようでした。また、横画面のモジュールは少なく、縦画面で上記のように斜め方向から見るのを想定しているものがほとんどのようでした。

 

このような仕様で失敗する人が少しでも減らせればという思いを込めて、まとめておきます。個人で購入できるLCDモジュール(基盤、シールド)のほとんどには、

View directionが存在する!

ということです。想定されたView direction以外の視野角は極端に狭いです。

 

と、いうわけで本番用のLCDモジュールはIPS液晶(実質、ViewDirectionという概念がほぼ無く、どの角度からでも同じように見える)タイプを選択するしかなさそうです。ただ、探してもなかなか2.6インチ前後の小型のLCDでIPS液晶というLCDモジュールは少ない…、もしくは高い…。FFCを扱う事も含めて最後まで完成させられるかまだ確証が持てない時点で購入するかどうかは、なかなか悩ましいところですね…。

 

という所でかなり長くなってしまったので、今回はこの辺で。次回こそは「60fpsで画面をヌルヌル動かす」に向けてのテスト結果を紹介したいと思います。ではでは。

ESP32-WROVERでLCDモジュールを使う!

さて、前回前々回でESP32-WROVERの使えるピンの確認と、(無理やり)自動書き込みができるようになったので、やっと本格的にWROVERを使うためのテストをしていきます。

 

まず、WROVERを使い始めた理由ですが、ゆくゆく作りたいもののために、まずLCDモジュールで画面をヌルヌル表示したい!という目的があったからです。

ただ、突然FFC接続のLCDモジュール単体を買っても、画面が出る前に挫折しそうな気がしたので、まずは練習用として、激安のArduinoシールドとしてのLCDモジュール基盤を買いました。

f:id:amilabo:20200306095735j:plain

  • 2.8インチ320x240 TFT液晶
  • 抵抗膜式タッチスクリーン付き
  • SDカードスロット付き
  • 3.3v用液晶モジュール直結の端子付き
  • 8bitパラレル接続(高速化のため、あえて非SPIを選択)
  • 1200円ぽっきり!

これだけの機能でこの価格であれば、練習用としては十分ですし、横の液晶モジュール直結の端子を使えれば、そのまま本番用にも使えそうです。(後々、様々な理由でこのLCDモジュールは本番用には使えないという事がわかるのですが…)

 

順番が前後しますが、WROVERを買う前に、このLCDシールドを買って、Arduino uno(の互換品)でテストをしていました。サンプルコードでは、画面全体の単色塗りつぶしに1fps程度かかっていましたが、最適化をいろいろして、最終的にarduino unoにシールドとして載せるのと、シリアル通信を行うのを諦めて、PORTBレジスタ1発書きで8ビットを送れるようにしたことで、15fps程度まで高速化する事に成功しました。

更に高速化する余地はまだ少しありそうでしたが、そうした所で、自分としてはarduino uno上ではこのLCDの使い道がありません。arduino側で描画用にLCDの全画素を保持するには、320x240x2=150KBのRAMが必要なのに対して、atmega328pのRAM容量はたったの2KBです。なので、毎フレーム全画素書き換えられても、できる事は、意味のない色やグラデーションを表示する事程度です。atmega328pで使う場合は、あくまで必要な個所を部分的にちょこっとずつ書き換えていく、という使い方になりそうです。そもそも、atmega328pはテストをするために使ったので、使い方がある程度わかったので十分です。そこで、全画素書き換えを実用にするため、WROVERを使い始めた、という次第です。

 

WROVERのSRAM512KBなので、全画素のバッファ(これからはVideoRAM:VRAMと呼ぶことにします)を、SRAM上に十分とれそうです。そのバッファを毎フレームクリア&描画して、LCDモジュールへの全画素転送!、を最低30fps、できたら60fpsで動かす事が当面の目標です。

まずは、理論値の計算ですが、16Mhzのatmega328pで15fps程度出せたので、240MhzのESP32では、単純計算で225fps出せそうです。どちらかというと、ESP32が早すぎてLCDモジュール側の受信速度の方がボトルネックになりそうです。LCDモジュールのドライバICのdatasheetによると、最短書き込みサイクルは66nsのようなので、8ビットパラレル転送だと、限界は約94fpsになります。16ビットパラレル転送の場合はその2倍の約188fpsになりそうです。ESP32側でさすがに16ピンは使いたくないので、8回路DラッチのICなんかを使えば、ピンを1本追加するだけで、16ビットパラレル転送ができそうです。その場合、1フレームの転送にかかる時間は1/188=5.3ms、60fpsを達成するのに必要な時間が、16.6msなので、転送に5.3msかかっても、11.3msは描画する時間に使えますね。8ビットパラレル転送でも6ms程度は描画時間がとれそうですし、VRAMをダブルバッファにして転送と描画をデュアルコアで別々にやれば、どんな場合も描画に16.6ms使えるので、60fpsでヌルヌル動かすの目標は結構余裕そうですね。と、思っていました…。

 

で、実装をを初めていきます。このLCDシールドの魅力的な所は、LCDモジュール直結の端子が用意されているところです。ここに、WROVERの各端子を繋げば、実質LCDモジュールを直接使ったようなテストが可能そうです。

f:id:amilabo:20200306105921j:plain


こんなピンアサインですが、RST端子が出ていません!

まぁ、コレは買う前から調べてわかっていたことで、RST端子は起動時に使う程度で、描画の高速化等とは無関係そうなので、RST端子は無理やりはんだ付けで引き出すか、シールドのピンに繋いでとりあえず使う事にします。

ただ、ここでちょっと不安に思い始めたことがあります。RST端子が無いことから、本当にこの端子を使ったテストがされているのか??ということです。Arduino側からの5Vを、双方向バスバッファのICで3.3Vに変換しているようなので、上記の端子は、LCDモジュールに直結されているだけではなく、バスバッファICの出力にもつながっていそうです。基盤を見ても両面が見えない事やICの裏に隠れた配線が見えないので、なんとかこの基盤らしい回路図が無いかを調べて、それらしいものを見つけました。

 

f:id:amilabo:20200306110924j:plain

悪い予感が的中です。

念のためテスターも使って確認しましたが、上記の端子はやはりLCDモジュールだけではなく、バスバッファの出力にも繋がっています。何がマズいかというと、バスバッファICの出力は入力に何かが繋がっているかいないかに関わらず、0Vか3.3Vが出力されているのです。なので、ここに外部からHIGHやLOWを出力したところで、LCDモジュールにその信号は伝わりません。それどころか、外部からの出力がそのまま3.3Vや0Vと直結される事で、回路がショートした状態となり、大電流が流れ、WROVERが一瞬でお亡くなりになってもおかしくないです。配線する前に調べておいて本当に良かった…。一応、読み込みの際にバスバッファの方向を変えるためにRDとDIRが接続されているので、RDをLOWにしておけばDB0~DB7は何とか使えそうです。一方で、下のバスバッファICのDIRやOE、VCCは固定で結線されてしまっているので、どうしようもありません。なので、CS、RS、WR、WDを使おうと思ったら、このICを剥がして、(0.65mmピッチの端子に)配線を行う必要がありそうです。少なくとも、手はんだで導線をはんだ付けする等は、間違いなく無理ゲーです。

DB0~DB7はWROVERと直結して、CS、RS、WD、RSTはしょうがないのでバスバッファを経由して接続しようかとも思いましたが、先程の計算にあるとおり、DBとWD等は、66nsとかのレベルでの時間の精度が要求されるので、バスバッファによる信号の遅延がかなり影響しそうで、その辺でバグがでると検証などが超絶大変そうなのでやめておいた方がよさそうです。

しかたないので、全端子、arduinoシールドの端子を使ってバスバッファを経由してLCDモジュールと接続する事にしました。バスバッファの遅延も、全端子がほぼ共通の時間で遅延するなら、恐らく問題にならないでしょう、という事で。

f:id:amilabo:20200306113006j:plain

結局、期待していたLCDモジュール直結用の端子は何だったのでしょうか?恐らく、配線はしてあるものの、実際にその端子を使ったテスト等はきっとしていないのではないかと思います。先に書いた通り、この端子を使うと、繋いだ側の機器が死亡する可能性があるので要注意です。WROVER単体なら、500円程度の損害で済むかもしれませんが、またあの配線をやるとなるとうんざりしますし、お高い開発キットなんかだと、そうとうショックを受けて絶望する気がします。

 

とにかく、事故る前に問題を発覚できたのは不幸中の幸いという事で。LCDとWROVERを繋ぐだけの話でかなり長くなってしまったので続きは次回

ESP32の自動書き込みが(無理やり)できるようになった!

前々回、WROVERへの自動書き込みを行いたくて、わざわざシリアル変換モジュールを購入したにもかかわらず、
CTSとDTRは別物!
という、知ってる人なら当たり前の事を知らなかったせいで、泣き寝入りしていましたが、なんとも無理やりな方法ではあるものの、DTR端子の無いシリアル変換モジュールでも、無事にWROVERへの自動書き込みができるようになりました。

どうやったかというと、以前の画像に載っていたarduino pro miniに、RTSの信号を監視させて、適切なタイミングでDTR信号を送るようにする、という、全くスマートではない方法です。
f:id:amilabo:20200302090148j:plain
以前に3個で1000円ちょいで買った5v 16Mhz のarduino pro miniが、ブレッドボード上で使ってなかったので、適当に動きそうなスケッチ書いてみたら、あっさり動きました。
こんな無駄な事にpro miniを使う人はいないと思いますが、もしかしたら、DTR端子の無いシリアル変換モジュールで自動書き込みができずに困っている人の助けになるかもなので、スケッチ載せておきますね。

int RTS_IN = 2; //使うピンは何番でもOK
int DTR_OUT = 4; //使うピンは何番でもOK

void setup() {
  delay(100);
  //事前に、CKDIV8 = 0 (クロック8分周有効)にヒューズビットを設定しておく
  //起動時は16Mhz / 8 = 2Mhzで動いてるので、ここから8Mhzで動くよう変更する
  //このpro miniは5V 16Mhz品だけど、16Mhzで動く瞬間は無いので3.3v動作OK!
  byte save_SREG = SREG;
  cli();
  CLKPR = 0x80; 
  CLKPR = 0x1;  // 8MHZ
  SREG = save_SREG;
  //ここまでで8Mhzへの変更終わり、以降はタイマーやシリアル通信も通常どおり動作可能
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH); //無事動き始めたよーって合図
  pinMode(RTS_IN, INPUT_PULLUP); //書き込み開始のリセットを監視
  pinMode(DTR_OUT, OUTPUT);
  digitalWrite(DTR_OUT, HIGH); //とりあえず平常時はDTRはHIGHの状態にする
  delay(100);
}

void loop() {
  while(digitalRead(RTS_IN) == HIGH); //書き込み開始のリセット待ち
  while(digitalRead(RTS_IN) == LOW); //リセット中はまだDTRはHIGHのまま
  digitalWrite(DTR_OUT, LOW); //リセット終えたらすぐDTRをLOWに変更
  delay(1500); //1.5秒くらい(適当)DTRをLOWにしたまま、これで書き込みモードで起動するはず
  digitalWrite(DTR_OUT, HIGH); //DTRをHIGHに戻す
  while(digitalRead(RTS_IN) == HIGH); //書き込み終了後のリセット待ち
  while(digitalRead(RTS_IN) == LOW); //リセット中もDTRはHIGHのまま
  //リセット後もDTRはHIGHのまま、これで通常モードで起動するはず
}

動作クロックの変更とかありますが、本質はloop()関数内だけです。ただこのままだと、書き込みに関係ないRTS信号を一瞬でも受けてしまうと、ESP32が書き込みモードに入って抜け出せなくなり、再起動が必要になるので、その辺はもう少し工夫した方がいいかもです。あと、基本スリープ状態でRTS_INのFALLINGを検出して割り込み、とかで節電するとよりスマートかもですね。ただ、一度シリアル変換モジュールを繋いでESP32の電源を入れたら、その後は何度も書き込みを繰り返してテストする、というような場合には十分実用かと思います。
atmega328pの8MhzでGPIOを2ピン使うだけで問題無く動いたので、8ピンで50円のATtiny13Aの9.6Mhzでもきっと同じ事ができると思いますし、近々ATtiny13Aを買ってきてそうする予定です。

ちなみに、WROVERからはどんどん脱線していきますが、以前にarduino pro miniを使った時にいろいろ調べて、ネットに載ってなさそうな他の人にも役立ちそうな情報を見つけたのでちょっと書いておきます。
arduino pro mini、及びatmega328pは、5v or 3.3v、16Mhz or 8Mhz、どんなものでも関係なく、データシートの安全領域さえ守っていれば、
基本的に自由な電圧、周波数で使えます。
実際、以前に作った機器では5v 16Mhzのpro miniを、乾電池2本で4Mhzで動作させ、全く問題なく動いています。電圧を下げていっても、1.8v程度でも問題なく動き、1.8vを切った所でBODが作動し、安全に電源を落としてくれている様子です。
なので、(基板上のLDOを使わない、書き込みはISPで行う場合)悩んだらとりあえず5v 16Mhz品のpro miniを買っておいて問題は無い気がします。16Mhz品を8Mhz やそれ以下で使うことはできますが、8Mhz品を発振器変えずにPCのCPUみたいに倍速して16Mhzで使うことは残念ながらできないので。まぁ、大は小を兼ねるといった感じでしょうか?

5v品も3.3v品も、LDOと発振器以外は全く同じ部品の回路なので、やることは、上記のコードのように最初にクロックプリスケーラで任意の周波数になるよう設定するだけです。
ただ、仕様書見た人はわかると思いますが、普通にやると、起動~ブートローダー~ユーザープログラムで周波数変更までの間のけっこうな時間、充電池2本だと2.4vで16Mhz動作させることになるので、それは非常にまずいです。
で、どうするかですが、CKDIV8のヒューズビットを0に設定しておきます。方法はいろいろあるかもですが、boards.txtに、CKDIV8と周波数(と必要に応じてBOD設定)の値だけ変えたオリジナルの定義を追加して、ブートローダーを書き込むのが簡単です。そうする事で、起動時~ブートローダー~ユーザープログラムで周波数変更までの時間、8分の1の周波数で動作するようになります(16Mhz品なら2Mhz、8Mhz品なら1Mhzといった感じです)。
最初、CKDIV8は何か特別な設定で、これを0に設定してしまうと、2Mhzや1Mhz からさらに分周して低い周波数でしか使う事ができないものかと思っていたのですが、そうじゃないんですね。CKDIV8は、単に起動時にクロックプリスケーラの設定を8分周にするかしないかを設定するだけで、その後にユーザーは通常通り自由な周波数に変更する事ができるということみたいです。となると、CKDIV8が本当に超絶便利機能に見えてきます。ブートローダーが8分周で動くので、シリアル経由でプログラムを書き込もうとすると大変かもですが、ISPで書き込めばいいだけで(Arduino uno or 互換機だけで簡単にISP書き込みはできます)、あとはユーザープログラムではdelay等のタイマ関連も期待通りの動作をするので、乾電池2本で余裕で安全に動かせるのは本当に便利です。4Mhz程度で動けばいい場合に、乾電池2本から3.3vに昇圧するなど、愚の骨頂ですね。1点だけ気になってる点としては、高周波の発振器が低電圧でも問題なく動くのか?という点ですが、こちらは調べてもちょっとわかりませんでした。ただ、もともとの設定が発振器はフルスイングではなく低電力で動かしているみたいなので、恐らく大丈夫…だと思っています。

と、いうわけで、今回のケースでも、使っているのは5v 16Mhzのpro miniですが、電源はWROVERと同系統の3.3vからとって、8Mhzで安全に動作させることができています。一応まとめとくと、
Arduino 〇〇は何でも1.8vまで安全に動く!
という事です。(もちろん、ちゃんとした使い方をした場合ですよw)


と、いうあたりで、WROVERとは全く関係ない話で終わってしまいましたが、続きは次回。ではでは。