RISC-V搭載ESP32ボード上の WS2812で手間のかかるLチカ

Espressiff純正のESP32-C3用開発ボード ESP32-C3-DevKitM-1 上のフルカラーLEDで、少し手間のかかるLチカの実験をしてみました。

はじめに

ESP32-C3-DevKitM-1 上のLED(写真の基板の左側・青緑点灯)は、WS2812互換のデジタル通信インタフェースを搭載しています。フルカラーを再現できるメリットのある反面、通常のLEDのように、GPIOのHレベルで点灯、Lレベル出力で消灯といった制御が出来ません。

本開発ボード上のLEDの点灯させるには、R(赤)、G(緑)、B(青)のそれぞれのLED素子の輝度値をデジタル信号で送信する必要があります。本稿では、その制御方法について解説します。

デジタルPWM変調

このLEDに搭載されているWS2812互換デジタル通信インタフェースは、デジタルPWMの変調方式に対応しています。PWMとは、パルス幅変調のことで、入力値に応じたパルス幅を情報として付与する方式です。

下図では、入力値が0のときに幅300nsのパルスを、入力値が1のときに幅600nsのパルスを送信します。

デジタルPWM変調の一例。値=0のときは300nsのパルス幅を値=1のときは600nsのパルス幅を送信する

ここでPWMをご存知の方は、少し疑問を感じたかもしれません。PWMには、例えば256段階などの多値の入力によるデジタル→アナログ変換のような使い方も多く、また、LEDの輝度の調整にも使われているからです。

本稿のLEDでは、PWMをデジタル→アナログ変換用に使うのではなく、WS2812互換のデジタル通信インタフェースとして使う点に注意してください。情報をパルス幅に重畳させる原理が同じPWMであって、使い方は異なります。

なお、周期が一定の場合はPSKと呼びます。WS2812のデータシート記載の図例ではPSKのようにも見えますが、非パルス(Lレベルの)区間の仕様から、デバイスの実装上はPWMとなっていると判断しました。

300nsへの挑戦

幅300nsのパルスを生成するには少なくともGPIOを1.7MHzで制御しなければなりません。GPIO制御による通信インタフェースとしては、高速な方です。とはいえ、1ビットにつき必ず1パルスが含まれるので、パルス幅の精度や伝送速度の精度は、あまり厳しくありません。速度とLED側の受信回路規模の簡略化を両立させた方式と言えるでしょう。

制御するESP32マイコン側も、正確なパルス幅である必要はありません。とはいえ、GPIO制御で300nsのパルス幅を生成するには工夫も必要です。ここでは、パルス幅の待ち時間を空ループで生成し、その空ループの回数は、100回分の空ループが要する所要時間から求めるようにしました。

下図は、PWM波形の測定結果の一例です。普段、使わない高速な信号のため、プローブの調整を失念していました。

PWM波形の一例。普段、使わないタイム・スパンに、プローブの調整を失念するほど高速な波形だった

デジタルPWM変調部のプログラム

下記リストは、引数r、g、bに、それぞれ、赤、緑、青のLED素子の輝度値(0~255)を入力したときに、WS2812互換デジタル通信インタフェース用の送信信号を生成する関数です。THがPWM変調のパルス幅です。TLはパルスのあとの待ち時間です。WS2812では580ns~1000nsの範囲内と定められています。後半のwhile文が、パルス持続時間を生成する空ループ処理部です。

void led(int r,int g,int b){
    _led_reset();
    volatile int TH, TL;
    uint32_t rgb = (g & 0xff) << 16 | (r & 0xff) << 8 | (b & 0xff);
    noInterrupts();
    for(int b=23;b >= 0; b--){
        if(rgb & (1<<b)){
            TH = T1H_num;
            TL = T1L_num;
        }else{
            TH = T0H_num;
            TL = T0L_num;
        }
        digitalWrite(PIN_LED,HIGH);
        while(TH>0) TH--;
        digitalWrite(PIN_LED,LOW);
        while(TL>0) TL--;
    }
    interrupts();
}

空ループの回数を求める事前処理部

上記プログラムの空ループ回数は、プログラム起動時に、測定します。とはいえ、Arduinoのタイマー関数microsだと、1μs単位でしか測れず、600nsを測定するだけの分解能がありません。そこで、目的の空ループを、さらに100回、繰り返すのに要する時間を求め、100で除算しました。

下記リストは、待ち時間(ns)から空ループの回数を求める関数です。

int _led_delay(int ns){
     volatile uint32_t i;
     uint32_t target, counts=0;
     delay(1000);
     noInterrupts();
     delay(1);
     do{
         i = ++counts;
         target = micros() + ns / 10;
         while(i>0) i--;
     }while(micros() < target);
     interrupts();
     return (counts + 50)/100;
 }

もうひと工夫

以上の処理で目的のデジタルPWM変調が得られるはずでしたが、2点の誤算がありました。一点目は、while関数を実行時の遅延時間です。とりあえず、実測値を減算しました。こちらも、前記のように測定して補正する方が良いでしょう。
もう一つの誤算は、WS2812の仕様です。ESP32-C3-DevKitM-1に実装されているSK68XXMINIとWS2812との間に若干の違いがありました。とはいえ、もともと広い仕様なので、ほとんど違いはありませんし、どちらの仕様を適用しても問題なく動作します。
なお、本稿ではPWMと書きましたが、プログラムではPSKっぽくしてみました。オシロスコープをお持ちの方は、そのあたりも確認してみて下さい。

LED(上側)を制御できるようになった

3種類のサンプル・プログラム

この仕組みを利用した3種類のサンプル・プログラムを用意しました。

led01_onoff.ino

LEDのON/OFFを繰り返すLチカ用プログラムです。ちょっとだけ色を付けさせていただきました。プログラムの内容が理解出来たら、色を変更してみて下さい。

https://github.com/bokunimowakaru/esp32c3/tree/master/led_ws2812/led01_onoff

led02_dimmer.ino

LEDの輝度が心臓の鼓動のように明暗を繰り返すプログラムです。周期は、一般的な心拍数に合わせました。プログラムの内容が理解出来たら、少し早くしてみて下さい。自分で改造したプログラムを実行するときは、心拍数も上がるはずです。

https://github.com/bokunimowakaru/esp32c3/tree/master/led_ws2812/led02_dimmer

led03_color.ino

フルカラーLEDの特長を活かし、12色の色相を連続で変化させるプログラムです。同時に、輝度も変化させ、最大輝度のときに30°の倍数の位置になるようにしました。0°の前後(赤紫→赤)の変化がぎこちない気がします。理由をご存知の方は教えていただけないでしょうか?

https://github.com/bokunimowakaru/esp32c3/tree/master/led_ws2812/led03_color

色が変化する

おわりに

WS2812互換の通信インタフェースにLEDの制御をRISC-V搭載の最新ESP32マイコンで行ってみました。また、デジタルPWMやGPIOによる通信インタフェースについても説明しました。手間はかかりますが、フルカラーによる表現力を手に入れた喜びを感じていただければ幸いです。
なお、Adafruit NeoPixel というライブラリを利用しても、同じようなことが出来ます。WS2812で最も使われているライブラリであり、そもそもWS2812を広めたのがAdafruit社だと言っても過言ではないでしょう。

関連記事

by bokunimo.net