ESP32で円形POV(バーサライタ)を作る②(内蔵フラッシュメモリを使った画像表示編)

バーサライタ

前回に引き続き、今回は画像変換やESP32のプログラムの解説になるのだが、後に続く解説の基礎を担う大事な回となっているので一読の価値あり。
前回はこちら

画像変換

変換の概要

円形POVでは縦x横の画像を半径x分解能の画像へと変形する必要があります。半径はLEDの数、1周の分解能は(360度÷表示する角度)となりますが、分解能については一番外側がLEDのサイズよりも小さいことだと考えているので、直径x3.14÷LEDのサイズ、が最大値だと思います。

今回は90mmの基板に30個の2mmサイズLEDを載せたものを回すため、半径は30(個)、分解能は[円周]÷[LEDサイズ]=[90(mm)x2x3.14]÷[2(mm)]で283とします。

画像の変換にはPythonを使用しています。変換の方法は、一辺がLEDの数x2の正方形に画像を縮小してから、回転角度に沿ってそれぞれのピクセルの色を取得するというものです。もう少し画像の劣化を防ぐ方法のアイデアはありますが、一番シンプルなので今回はこの方式を採用しました。

※画像の劣化を防ぐアイデア…画像のサイズ縮小を行わず、回転角度に沿って扇形状内の全ての画素の色を取得して足す。全ての画素の色を取得後、扇形状内に含まれる画素数で色を割る。

Pythonプログラム

画像変換プログラムについての補足

ガンマ補正(9~11行目)

画像の色をそのままLEDで表示すると、明るめの色が白飛びしてしまうのでガンマ補正という方法で明るさを抑えています。普通の画像変換ではガンマ補正は±1.0ぐらいまでなのですが、LEDで本来の色を再現しようとすると5.0ぐらいの強烈な値を設定する必要がありました。(この値は各々調整してください)

円中心と外側の明るさ補正(64~66行目)

円形POVは円の中心に近いほどLEDの回転速度が遅くなるため、中心ほど明るくなります。外側と中心の明るさを揃えるため、中心からの距離が近くなるにつれて明るさを下げています。

出力されるファイル

このプログラムで出力されるファイルには以下のものがあります。保存先は画像のあったフォルダに「forPOV」フォルダが作成され、その中に保存されます。

・[画像名]BGR.c…RGB各8bitのChar配列を3つずつ並べた24bitカラーの配列を出力します。配列の並びとしてはB,G,Rの順です。内蔵フラッシュメモリに書き込む画像データは基本的にこれを使います。

・[画像名]32.c…RGB24bitカラーをunsigned long(32bit:0x8+Bx8+Gx8+Rx8)の配列として並べたものを出力します。再現される色はBGR.cと同じですが、ファイルサイズが32bit/24bit =4/3倍になるので、フラッシュメモリに複数枚のデータを保存するときには不向きです。

・[画像名]16.c…BBBBBGGGGGGRRRRRの並びの16bitカラーをunsigned short(16bit)の配列として出力します。24bitカラーよりも色の再現は悪くなりますが、データサイズは節約できます。

・[画像名].bmp…円形POV用に変形したBitmap画像を出力します(つまりは変換の概要で説明した縦長の画像)。カラーは24bitカラーです。画像ファイルなので出力結果を見ることができます。また、SDカードからデータを読み込む場合にはこの形式で保存します。

・[画像名].bin…24bitカラーのデータをバイナリファイルで保存しています。今回は使っていませんが、16bitカラーのファイルでSDカードに保存する場合など、将来的な参照のために記載しています。

実際に変換した結果

この画像を変換しました。ファイル種類としては透過PNGとなり、透過部分は黒として判定されます。

元画像
変換後画像

ESP32用プログラム

今回はESP32の内蔵フラッシュメモリに画像データを書き込む方法をとります。フラッシュメモリは電源を切っても内容が消えないので主にプログラムの保管先となっていますが、ある程度の大きさのデータを入れておくこともできます(容量はESP32の種類によって変わりますが2~16MB程度です)。フラッシュメモリへの書き込み方法としてはいくつかの方法がありますが、.cのファイルを追加する方法を採用しています。

画像データの追加方法

Pythonで出力した.cファイルをArduinoエディタ上にドラッグ・アンド・ドロップして追加します。また、プログラムで「#include “MFTLogo_sqRGB.c”」というふうに記載します。
Pythonで出力した画像データのダウンロードはコチラ

こんな感じにタブに追加できればOK

プログラム(Arduinoで記載)

プログラムの流れ

  1. ホールセンサの入力を割込みで検出する(79行)
  2. ホールセンサの入力の時間から回転の一周の時間、一周を分解能で割った時間(LEDアレイに書き込む間隔:drawTime)を算出、LEDへの書き込み(drawFlag)を有効にする(43~53行)
  3. (Loop内)drawFlagがTrueの時にLEDアレイの書き込みを開始する(101行)
  4. While(1)で無限ループにする(116行)
  5. 回転の始点の角度が0度以外の場合、画像の途中の行からの描画になるので、描画する行(h)に回転の始点の角度(startDeg)から算出した開始行を代入する(106~107行)
  6. 現在の時間≧前に書き込んだ時の時間+drawTime=nextDrawTimeになるまで待つ(118行)
  7. フラッシュメモリから変数配列(dataBGR)に画像データの1行分だけコピーする(136行)
  8. LEDアレイに送信するためのデータ配列へと加工して変数配列(sendData)に保存する(139~143行)
  9. SPIでLEDアレイにデータを送信する(180行)
  10. 描画する行(h)に1を足す
  11. ⑤~⑨を繰り返す。hが開始行になったらWhileループを抜ける

プログラムの補足

  • 13~15行:GPIOピンの設定を行います。SPIを使う場合、dataPin,ClockPinの設定は不要です。逆にSPIを使わない場合、dataPin,ClockPinはG0~31のどれかに設定できます(ESP32のプログラムや動作に影響のあるピンを除く)。
  • 17行:SPIを使うか、GPIOのアップダウンで通信を行うかの設定です。詳しくは後述しますがSPIは最大80MHz、GPIOは最大10MHzで通信できます。
  • 20~22行:POVの構造によって値を変更する場所です。startDegはホールセンサが(時計で言う)12時の方向にある時が0度、4時の方向にある時が120度です。なお、LEDの回転方向は時計回りになるようにしてください。
  • 25行:LEDの明るさで、LEDの通信の時に使います。0~31の値を設定できますが、あまり明るすぎると電流量が跳ね上がります。
  • 29行:画像の形式を変更できます。0が通常のBGRx8bitで、1が32bit(BGRと画質は一緒)、2が16bit(BGRよりも画質が落ちるが、データサイズとデータの読み込みが速い)
  • 37、83~93行:ESP32の割り込みの特性なのかなんなのかわからないのですが、時々ホールセンサの入力を2回分カウントしているようで、そのときには回転速度がめっちゃ上がっていると判定されてしまうので、しきい値を設けています。20000usだと50回転/秒なため、こんなに速くは回さないよねという値にします。(ちなみに使用したホールセンサはシュミットトリガ的な機能があるので、センサーとしてフォトリフレクタを使った時のようにシュミットトリガICを入れる必要はありません。実際にシュミットトリガICを経由して割り込み信号に入れましたが結果は同じでした。)
回転時間の計測。明らかに短い時間での判定がおきている
  • 66行:SPIの周波数を設定します。SPIは最大80MHzまで設定できるようですが、HD107S LEDの通信速度の最大が(データシート上では)30MHzなのでsetFrequencyは30,000,000としています(ただ、実物で確認すると40MHzまでいけそうです)。ESP32は他のピンをハードSPIにできるという情報もありますが未検証です(その場合は最大40MHzとのこと)。
  • 83~93行:SPIで送信すると、信号を送信してから次の信号を送信するまでに間が空きます。そのため、最小時間で信号を送信するとなるとLEDの数分一気に送る必要があるため、sendDataという配列を設けています。構造としては、[0x00が4つ][輝度,G,B,R][0xffが5つ]という構成です。最後の0xffが5つなのは、最後の通信は([LEDの数]+ 14)/16を0xffにすると決められているため、LEDが66~81個までの時は5回以上0xffを流す必要があります。82個以上では6回以上流す必要があります。(なのでLED10個では2回で良いとは思いますが念のため…)
  • 100行目:この行を有効にするとホールセンサの入力に関わらず、LEDへの描画を繰り返すことになります。そのためLEDアレイを回転しないでLEDが光るかどうかを確認する時に有効にします。
  • 201~219行:SPIを使用しない、GPIOのオンオフで行う場合の通信になります。レジスタの値を直接変えることで10MHzで通信できますが、G0~G31の値を一気に変えるため他にG0~G31の中に出力ピンがある場合は誤作動を起こす可能性があるため気をつける必要があります。逆に一気にGPIOを変更できることから、複数のLEDアレイと通信することもできるため4つ以上のLEDアレイに対して通信する場合、こちらの方が有利になります。ESP32のGPIOの高速化についてはこちらの記事を参照ください。

実際の見た目

シリアル通信出力と時間のハナシ

シリアル通信の出力は1周回るまでの【回転周期[us]:青】と、【準備時間[us]:赤】【フラッシュから変数配列へのコピー時間[us]:緑】【LEDアレイへの送信時間[us]:オレンジ】の合計を送信します。

【回転周期[us]】<【準備時間[us]】+【フラッシュから変数配列へのコピー時間[us]】+【LEDアレイへの送信時間[us]】となることが重要です。

シリアルモニタ(SPI使用時)
シリアルプロッタ(SPI使用時)

これを見ると【準備時間[us]:赤】【フラッシュから変数配列へのコピー時間[us]:緑】は無視できるレベルで、【LEDアレイへの送信時間[us]:オレンジ】が最も時間のかかる項目となっています。コピー時間は例えば16bitカラーにすることで短縮できますが、LEDアレイへの送信時間はLEDの通信速度がmax30MHzになっている以上早めることはできず、SPIで1列のLEDアレイに信号を送ることを捨て、パラレルに複数のLEDアレイへ信号を送ることを検討する必要があります。
【フラッシュから変数配列へのコピー時間[us]】と【LEDアレイへの送信時間[us]】の時間はLEDアレイの数に比例し、送信する間隔は分解能に反比例するので、全体でかかる時間は概ねLEDアレイの数の2乗に比例して大きくなります(面積が増えるからね)。1周にかかる時間はSPI使用時・LEDアレイ30個で約15ms位かかっているのでLEDアレイを60個にすると60msぐらいかかり1000/60=毎秒16.7回転より遅く回さないと描画が追いつかなくなります。

次回

次回はマルチコア及びVSPIを使った、SDカードから画像を読み込んでPOVに表示する方法について説明します。

コメント

タイトルとURLをコピーしました