ESP32 の LED_PWM において元クロックを生成する仕組み


はじめに

ESP32 の LED_PWM において、元となるクロックを生成する仕組みについて勉強した結果をまとめています。LED_PWM は、Arduino core for the ESP32 において、LEDC として利用することができます。

LEDC

LEDC は、LED_PWM を用いて PWM 信号を GPIO から出力する関数ライブラリです。以下の様な用途に使われています。
・LED の明るさを制御する
・サーボを制御する
他にも、任意の周波数の信号を出力する場合に使用できます。外付けのハードウェアに向けて安定したクロックを供給したい場合に便利に使えます。

LEDC の使い方

Arduino Core for the ESP32 の Examples の AnalogOut の中に使用例があります(参考文献3)。以下の例は、ある LSI に向けて 2MHz のクロックが必要になった際に作成したコードの骨子です。LEDC のチャネル(0番)、ベース周波数(2MHz)、分解能(5bit=32段階)、出力ピン(GPIO5)、デューティ(8/32=25%)を指定しています。

LEDC.c
const uint8_t  Channel     = 0;
const double   Frequency   = 2e6; // 2MHz
const uint8_t  Resolution  = 5;   // 0-31
const uint8_t  Pin         = 5;
const uint32_t Duty        = 8;
ledcSetup(Channel, Frequency, Resolution);
ledcAttachPin(Pin, Channel);
ledcWrite(Channel, Duty);

素朴な疑問

2MHz の信号において 32 段階の PWM を実現するためには、元となるクロックとして $2\mathrm{MHz}\times32 = 64\mathrm{MHz}$ を生成しておく必要があります。参考文献1 によると、LED_PWM の入力は APB_CLK(80MHz) か REF_Tick(1MHz) のいずれかです。64MHz をどうやって生成するのでしょう。

LED_PWM Divider

参考文献1 の 14.2.2 Timers によると、LED_PWM の元クロックは、Divider で生成され、その動作を決める Div_num レジスタは以下の形式です。

\verb|Div_num| = A \frac{B}{256}

A(10bit) は Div_num の整数部、$\frac{B}{256}$ は分数部(Fractional)です。B は 8bit(0ー255) です。

整数部 A

入力クロック $F_{input}$ を 80MHz とすると、A=1 の場合、$80\mathrm{MHz}\div1=80\mathrm{MHz}$ が整数部の出力となります。A=2 の場合、$80\mathrm{MHz}\div2=40\mathrm{MHz}$ です。整数部だけだと 80MHz と 40MHz の間の細かな周波数は生成できません。後の計算のために整数部が A の場合の出力パルス周期 $T_A$ を周波数の逆数で表しておきます。A+1 の場合も同様です。

T_A=\frac{1}{F_{input}\div A}\\
T_{A+1}=\frac{1}{F_{input}\div(A+1)}\\

分数部 B/256

分数部では、整数部を A とした出力と、整数部を A+1 とした場合の出力とを混合します。混合する割合は、出力パルス 256 個あたり A の出力が (256-B) 個、A+1 の出力が B 個です。B=0 の場合、分数部の出力は全て A の出力となります。B=255 の場合、分数部の出力は A の出力パルスが 1個、A+1 の出力パルスが255個になります。A=1 の場合の 80MHz と、A+1=2 の 40MHz の間を 256 段階で設定できることになります。A の出力パルス周期 $T_A$ を 256-B 個、A+1 の出力パルス周期 $T_{A+1}$ を B個、足し合わせて 256 で割れば、パルス周期の平均値 $T_{output}$ が出ます。平均パルス周期は出力周波数 $F_{output}$ の逆数です。

T_{output}=\frac{T_A(256-B)+T_{A+1}B}{256}=\frac{1}{F_{output}}

A の出力パルスと、A+1 の出力パルスは、均等に散らばる回路になっています。局所的に見るといびつな波形ですが、最終的な PWM 出力では均等で安定した波形が期待できます。

Divider 全体

入力周波数 $F_{input}$ に対して、得られる出力周波数 $F_{output}$ は以下となります。$1\leqq A \leqq 1023,$ $0\leqq B\leqq255$ です。

\frac{256}{F_{output}}=\frac{256-B}{F_{input}\div A}+\frac{B}{F_{input}\div(A+1)}\\

周波数 2MHz で分解能 5bit の PWM 信号に必要な元クロック 64MHz は、A=1, B=64 で得ることができます。

\frac{256}{F_{output}}
=\frac{256-64}{80\mathrm{MHz}\div1} + \frac{64}{80\mathrm{MHz}\div(1+1)}
=\frac{192}{80\mathrm{MHz}} + \frac{64}{40\mathrm{MHz}}
=\frac{192+128}{80\mathrm{MHz}}
=\frac{320}{80\mathrm{MHz}}\\
F_{output}=\frac{80\mathrm{MHz}\times256}{320}=64\mathrm{MHz}

Excelで F-input=80MHz, A=1, B=0..255 をグラフ化してみました。横軸は B(0-255)、縦軸左が周波数(MHz)、縦軸右は周期(μs)です。B の値にしたがって、80MHz から 40MHz まで滑らかに生成できます。

グラフの元Excelデータは以下にあります。Frequency および Resolution の検討に使えます。
LEDC.xlsx

Div_num を生成する

参考文献2 によると、LEDC ではパラメータであるベース周波数 Frequency(~40MHz)、分解能 Resolution(~31bit) から Div_num を計算して ESP32 内部レジスタに設定しています。以下の計算式で分子の 256 が Div_num の分数部に対応しています。

\verb|Div_num|=\frac{\verb|APB_CLK|\times256}{Frequency\times2^{Resolution} } \\
\verb|0x00100|\leqq \verb|Div_num|\leqq\verb|0x3FFFF|

Frequency および Resolution が小さいため $\verb|Div_num|>\verb|0x3FFFF|$ となった場合には、APB_CLK(80MHz) に代えて REF_Tick(1MHz) を選択して計算し直します。それでも $\verb|Div_num|>\verb|0x3FFFF|$ となる場合には、$\verb|Div_num|=\verb|0x3FFFF|$ とします。$\verb|Div_num|<\verb|0x00100|$ となる場合には、$\verb|Div_num|=\verb|0x00100|$ とします。Frequency や Resolution の値により、得られる PWM 信号のベース周波数は指定した Frequency にならない場合があります。

おわりに

LED_PWM には、出力パルスの Up/Down のタイミング、つまり位相を細かく設定したり、ハードウェアで自動的に強弱を連続的に変えたり (automatical fade)、割込みを発生させたりする機能がありますが、LEDC では現時点で未実装です。

参考文献

  1. ESP32 Technical Reference Manual Version 4.1 (2019.11.21)
    latest version: https://www.espressif.com/en/support/download/documents
  2. esp32-hal-ledc.c
    https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-ledc.c
  3. LEDCSoftwareFade.ino
    https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/AnalogOut/LEDCSoftwareFade/LEDCSoftwareFade.ino