Xiao ESP32S3 でPWM信号発生テスト

2025.4.14 2024.2.13 Coskx Lab  

1 はじめに

Xiao ESP32S3でPWM信号を発生します。


(1)PWM信号

PWM(Pulse Width Modulation)信号というのは,短いパルス信号を一定周期で発し,モータなどのONOFF制御を高速に行うためのものです。
しかもONの時間とOFFの時間の比率を自由に変更できるようになっています。
参考 PWM波形の例 (https://www.mbtechworks.com/projects/raspberry-pi-pwm.html)


パルス周期はPWM周期(PWM period)とも呼ばれ,その逆数はPWM周波数(PWM frequency)と呼ばれます。
また「パルス幅/パルス周期」の値はデューティー比(duty ratio, duty cycle)と呼ばれます。

(2)ESP32S3がPWM信号を発生する仕組み

PWM信号の生成ではESP32S3の指定したピンに対して,プログラムで手工業的にHとLを交互に出力する作業をしているわけではありません。
EPS32にはPWM発生回路(PWMモジュール)があります。
プログラムはこのPWM発生回路(PWMモジュール)に対してPWM周期やパルス幅を与えて,PWM波形を発生します。
PWMモジュールがPWM波形を発生しただけではまだ外部にPWM信号は見えません。
EPS32は多くのGPIOピン(GPIOは,General-purpose input/output,汎用入出力)を持っています。
GPIOピンは入力に使われるのか出力に使われるのかさえ決まっていない多用途ピンです。
選定したGPIOピンを出力用途に設定し,PWMモジュールが生成しているPWM信号をそのピンから出力するように設定します。
こうして初めてあるピンからPWM信号が出力されるようになります。

(3)プログラムが行うこと(詳細)

  1. ある1つの出力ピンを指定し,出力用途だと設定します。
    動作テストでは,ESP32S3のピンD0をPWM出力として使ってみます。それ以外のピンについてもプログラムをピンの指定を変更しながら試すことが出来ます。
  2. EPS32S3にはPWMモジュール(PWMチャンネル)は0から7までの8個ありますが,その中の1つを使います。
    どのチャンネルを使用するかは指示しなくても良いようになっています。
    (ESP32:16個,ESP32-S2・ESP32-S3:8個,ESP32-C3・ESP32-C6・ESP32-H2:6個)
  3. PWM周波数とPWMモジュールが内部で用いるカウンタのビット数を設定します。
    カウンタのビット数を多くするとデューティー比を細かく指定することができるようになりますが,高いPWM周波数を使うことができなくなります。
  4. 指定した出力ピンからPWM信号を出すように結びつけます。
  5. 希望するデューティー比を先に指定したPWMモジュール(PWMチャンネル)に与えます。
    これは何回でも与え直すことができます。

(4)Arduinoライブラリが更新され,使用関数が変更

Arduino ESP32 coreのバージョンが2.Xから3.0になり,
ledcSetup(), ledcAttachPin() は廃止され,ledcAttachChannel()またはledcAttach()を使うことになりました。
https://github.com/espressif/arduino-esp32/blob/master/docs/en/migration_guides/2.x_to_3.0.rst#id4

2 使用環境

3 PWMの動作テスト用プログラム

PWM波形を生成しますが,生成するデューティー比は離散的で
0/256, 1/256, 10/256, 64/256, 128/256, 192/256, 255/256, 256/256, 257/256
とします。(257/256は動作チェックのためです)
PCからのシリアル通信で,LFを与えるたびにデューティー比を増加させ,最大になったら元に戻ります。
その他の設定値は次の通りです。
 ・PWM生成に使用するビット数: 8bit
 ・PWM周波数:2000Hz
 ・PWM出力に使用するPINの名前:D0

PWMに関連する関数名はledc○○○○○になっています。
最初に開発されたときに,LEDの明るさ制御に使われたようで,LED_Controlの名残と思われます。

  PWMの動作テスト用プログラム本体

//PWMTest01.ino//
//Xiao ESP32S3//

const uint8_t nBits_forPWM = 8; // PWMに使用するビット数 n=1~14[bit]
const double PWM_Frequency = 2000.0;   // PWM周波数 [Hz]

/*
//PWM周波数 80000000.0Hz(80MHz)  nビット使用のとき 
//MaxFrequency[Hz]: 80000000/2^(n+1)
//MinFrequency[Hz]: MaxFrequency/2^10

bit数 n resolution(2^n) 2^(n+1)  MaxFrequency[Hz] MinFrequency[Hz]
    16    65536        131072      610.3515625       0.596046448
    14    16384         32768     2441.40625         2.384185791
    12     4096          8192     9765.625           9.536743164
     8      256           512   156250             152.5878906
     4       16             32 2500000            2441.40625
     1        2             4 20000000           19531.25
*/

const uint8_t PWM_PIN = D0; // PWM出力に使用するGPIO PIN番号
//const uint8_t PWM_PIN = LED_BUILTIN;         // the PWM pin the LED is attached to //led = 21
                                               // これを使うとLEDONOFFが負論理になっている

const int PWM_Values[] = {0, 1, 2, 10, 64, 128, 192, 254, 255, 256, 257}; //デューティ
                         //MaxDuty=2^n  DutyRatio = Duty/MaxDuty
const int nValues = sizeof(PWM_Values)/sizeof(int);

int ValueIndex = 0;

void setup() {
  Serial.begin(115200);
  delay(100);
  //while(!Serial);
  Serial.println("\n\n\n*** PWM Test ***");
  delay(100);

  //PWM信号発生ピンを出力モードにする
  pinMode(PWM_PIN, OUTPUT); 

  //PWM出力ピンと周波数,分解能の設定
  bool result = ledcAttach(PWM_PIN, PWM_Frequency, nBits_forPWM);
  if (result) Serial.println("ledc is ready.");
  else Serial.println("ledc failed.");

  Serial.printf("duty ratio = %d/256\n", PWM_Values[ValueIndex]);
  ledcWrite(PWM_PIN, PWM_Values[ValueIndex]);
}

void loop() {
  if (Serial.available() > 0) {
    // read the incoming byte:
    Serial.read();
    ValueIndex++;
    if (nValues<=ValueIndex) ValueIndex = 0;
    Serial.printf("duty ratio = %d/256\n", PWM_Values[ValueIndex]);
    ledcWrite(PWM_PIN, PWM_Values[ValueIndex]);
  }
}

4 配線

PWM信号の出力は「D0」です。 この信号をオシロスコープで観察するために,次のように配線しました。


5 PWMのための設定値の制限

PWM信号の生成は内部のカウンタが用いられています。そのためカウンタを何ビット使うかを設定する必要があります。
カウンタのビット数が決まると,最大PWM周波数・最小PWM周波数が決まり,デューティー指令値(PWM指令値)の範囲が定まります。
デューティー指令値(PWM指令値)を指示すると,デューティー比が決まります。

1)PWMに使用するビット数 n = 1~14 [bit]
  ただしESP32S3

2)PWM周波数に設定できる最大周波数,最小周波数は, n とソースクロックに依存します。
  ソースクロックが80MHzの場合
  最大PWM周波数 MaxFrequency = 80000000.0 ÷ 2n+1[Hz]
  最小PWM周波数 MinFrequency = 80000000.0 ÷ 2n+11[Hz]
  例
bit数 nresolution 2nMaxFrequency[Hz]MinFrequency[Hz]
14163842441.406252.384185791
1240969765.6259.536743164
8256156250152.5878906
41625000002441.40625
122000000019531.25

  実測
  ESP32S3で実行時に設定できる最大・最小周波数を調べてみました。
  周波数設定値は指示できますが,実際の周波数設定値は演算誤差があると考えられます。
  bit数 nが小さいときに最大・最小周波数設定値に意味はないかもしれません。
bit数 nMaxFrequency[Hz]MinFrequency[Hz]
1424463
12978410
8156555153
425048922442
12003913819532

3)デューティー指令値(PWM指令値) v の範囲は n に依存します。
  0 ≦ v ≦ MaxValue
  MaxValue = 2n = resolution

4)デューティー比
  dutyratio = v ÷ MaxValue

例 n=12のとき
  設定できる最大PWM周波数9765.625Hz,最小PWM周波数9.536743164Hzなので,例えばPWM周波数を5000Hzに設定できます
  設定できるデューティー指令値(PWM指令値)は0から4096になります。
  デューティー指令値を1024にするとデューティー比は25%になります。
  デューティー指令値を2048にするとデューティー比は50%になります。


6 オシロスコープ測定結果

PWM発生に使用するビット数を8に設定してPWM値を0から1,2,..254,255,256,257までテストしました。
(8ビットでは0から255までしか表せないはずですが,257まで試しました。)

わかったこと

PWM値想定デューティ比(パルス幅)実測デューティ比(パルス幅)
00/256 (0%)0 (0%)
11/256 (0.4%)約2μsec (0.4%)
22/256 (0.8%)約4μsec (0.8%)
1010/256 (4.0%)約20μsec (4.0%)
254254/256 (99.2%)約496μsec (99.2%)
255255/256 (99.6%)常にH (100%) ←変だ
256256/256 (100%)常にH (100%)
257257/256 (100.4%)約2μsec (0.4%)

PWM値で示されるデューティ比パルスが生成されました
PWM値が256のときに100%のHが主力されました。
ただし,PWM値が255のときにも100%のHが主力されました。

PWM値が255のときの異常な動作は,ライブラリのプログラムにより引き起こされていることがわかりました。
C:\Users\[ユーザ名]\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.2.0\cores\esp32\esp32-hal-ledc.c
に次のような記述があります。

bool ledcWrite(uint8_t pin, uint32_t duty) {
  ledc_channel_handle_t *bus = (ledc_channel_handle_t *)perimanGetPinBus(pin, ESP32_BUS_TYPE_LEDC);
  if (bus != NULL) {

    uint8_t group = (bus->channel / 8), channel = (bus->channel % 8);

    //Fixing if all bits in resolution is set = LEDC FULL ON
    uint32_t max_duty = (1 << bus->channel_resolution) - 1;

    if ((duty == max_duty) && (max_duty != 1)) {
      duty = max_duty + 1;
    }

    ledc_set_duty(group, channel, duty);
    ledc_update_duty(group, channel);

    return true;
  }
  return false;
}

256/256のデューティ比を与えるとPWM発生器が常時Hを出力することになっていることはわかっています。
なぜかmax_dutyを255に設定していて,dutyがmax_dutyに等しいとき,すなわちduty=255のときにduty = max_duty + 1 (=256) にしています。
このような作業は不要です。
このファイルのオリジナルは別名で確保して,問題部分をカットし,次のようにして,PWM値が255のときに99.6%のHが出力されるようになりました。

bool ledcWrite(uint8_t pin, uint32_t duty) {
  ledc_channel_handle_t *bus = (ledc_channel_handle_t *)perimanGetPinBus(pin, ESP32_BUS_TYPE_LEDC);
  if (bus != NULL) {

    uint8_t group = (bus->channel / 8), channel = (bus->channel % 8);

    //Fixing if all bits in resolution is set = LEDC FULL ON
    uint32_t max_duty = (1 << bus->channel_resolution) - 1;

    //if ((duty == max_duty) && (max_duty != 1)) {
    //  duty = max_duty + 1;
    //}

    ledc_set_duty(group, channel, duty);
    ledc_update_duty(group, channel);

    return true;
  }
  return false;
}

PWM値想定デューティ比(パルス幅)実測デューティ比(パルス幅)
254254/256 (99.2%)約496μsec (99.2%)
255255/256 (99.6%)約498μsec (99.6%)←正常
256256/256 (100%)常にH (100%)

この部分は不思議ですが,多くのArduinoマイコンのPWM生成ハードウエアに対応するようになっているため,多数派に従った造りになっているのかもしれません。
ただし,ESP32-WROOM-32Eにおいても,この修正は有効であったため,使用CPUの動作を見ながら対応するのがよさそうです。

以下は実際の様子です。
1) PWM_Frequency = 2000.0Hz, PWM_Value = 128, PWM ratio=50%


2V/div 1ms/div

設定どおり,PWM周波数2000Hz
PWM周期0.5msec,500μsec
dutyRatio50%で動作している

2) PWM_Frequency = 2000.0Hz, PWM_Value = 1, PWM ratio=1/256


1μs/div
設定どおり,PWM周波数2000Hz
PWM周期0.5msec,500μsec
dutyRatio1/256なので
パルス幅は500μsec/256=1.953125μsecのはず
実測値は1.94μsec(オシロの自動計測)

3) PWM_Frequency = 2000.0Hz, PWM_Value = 0, PWM ratio=0/256


すべてL信号

4) PWM_Frequency = 2000.0Hz, PWM_Value = 1, PWM ratio=1/256


1/256幅の短いパルスが見える

5) PWM_Frequency = 2000.0Hz, PWM_Value = 10, PWM ratio=10/256


10/256幅の短いパルスが見える(見やすい)

6) PWM_Frequency = 2000.0Hz, PWM_Value = 64, PWM ratio=64/256=1/4


25%幅のパルスが見える

7) PWM_Frequency = 2000.0Hz, PWM_Value = 128, PWM ratio=128/256=1/2


50%幅のパルスが見える

8) PWM_Frequency = 2000.0Hz, PWM_Value = 192, PWM ratio=192/256=3/4


75%幅のパルスが見える

9) PWM_Frequency = 2000.0Hz, PWM_Value = 255, PWM ratio=255/256


255/256幅のパルスが見える
(1/256だけ短くお休み)

10) PWM_Frequency = 2000.0Hz, PWM_Value = 256, PWM ratio=256/256


全期間にわたってH信号が見える
通常デジタル機器は8bitを扱うなら0から255 (28-1)までしか対応できないのに, 親切な設計となっています

7 まとめ

ピン割り当て,PWMモジュール(PWMチャンネル)割り当てを行いPWM信号を生成しました。
ライブラリファイルを一部手直しして,意図したデューティー比のPWM波形を生成することができました。
信号観察結果も予想通りでした。