SNSへはこちら

ESP32で遊んでみる(3) - LED_PWM

いきなりですが、Lチカ(LEDボヤァ)の一環として PWM をやっていきたいと思います。ESP32 の PWM を始めとしたペリフェラルのデジタル端子は、ポートの設定に自由度がありかなり便利です。

この PWM は名前の通り、LED の調光を目的とした機能らしいです。モーター用の PWM は別に存在します。

参考資料とか

設定

ESP-IDF では各種機能・ペリフェラルを動作させるために、専用の構造体を用います。今回は LED_PWM 用の構造体です。
この機能は LEDC と呼ぶらしくて、IDF 上でもこちらの名前が用いられています。

色々やる前に、driver/ledc.h というヘッダをインクルードしておきます。LEDC を動作させるには、LEDC 用タイマーの設定と、PWM ポートのチャネルの設定の2つが必要です。

LEDCタイマーの設定

LEDC には専用のタイマーが実装されていて、これによって波形の生成が可能です。
そもそも LEDC のペリフェラルは2つのブロックに分かれており、High-speed Channel と Low-speed Channel があります。Low-speed はドキュメンテーションを読めば分かると思いますが、Duty を変える時にちょっと設定が面倒ですので、今回は簡単のために High-speed Channel のみ説明します。

また、設定のために使うマクロは同様にドキュメンテーションをご参考にどうぞ。ドキュメンテーションを見ながらこの記事を見ると良いかもです。

ledc_timer_config_t tconfig = {
    .speed_mode = LEDC_HIGH_SPEED_MODE,
    .duty_resolution = LEDC_TIMER_10_BIT,
    .timer_num = LEDC_TIMER_0,
    .freq_hz = 1000,
    .clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&tconfig);

この設定では、タイマーカウントのビット幅を 10bit、High-speed Channel 用タイマーのチャネル0を使用しています。タイマーのチャネルは High-speed と Low-speed でそれぞれ8つあります。
周波数は適当です。クロック源は .clk_cfg で指定していて、まあ面倒ですので LEDC_AUTO_CLK を書くことによって、IDF がいい感じのクロック源を指定してくれます。

今回の例だと カウンタのビット幅10bit, カウンタのオーバーフロー周波数 1000Hz となります。ビット幅とオーバーフロー周波数は独立していてユーザーが決められます。この値によって、PWM パルス幅の精度が決まるわけです。

PWMポートのチャネル設定

上で設定したタイマー条件での波形出力先を指定します。ほとんどの設定項目は素直です。

ledc_channel_config_t cconfig = {
    .gpio_num = 2,
    .speed_mode = LEDC_HIGH_SPEED_MODE,
    .channel = LEDC_CHANNEL_0,
    .intr_type = LEDC_INTR_DISABLE,
    .timer_sel = LEDC_TIMER_0,
    .duty = 1024,
    .hpoint = 0
};
ledc_channel_config(&cconfig);

この設定では IO2 を出力ポートに指定しています。他の番号でも可能ですが、入力専用ポートではエラーが起きますので要注意。
.speed_mode, .timer_sel では先のタイマー設定を指定しています。.channel は「この出力をチャネルxと定義する」という項目です。
.duty はいわゆるデューティー...ではなく、波形が Low→High となるためのしきい値です。ちょっと変数名の言葉選びが下手ですよね。
.hpoint は0としています。これはちょっと難しいのですが、以下で解説しましょう。
また、割り込みについても後の例で説明します。

PWM波形出力のタイミングとhpoint

上のドキュメンテーションでは一切説明がありません。そこで ESP32 Technical Reference Manual を参照します。

この図と文章で PWM 波形出力のタイミングの話がされています。ESP-IDF 向けにまとめると、

  • デフォルトで PWM 出力は Low
  • hpoint になった次のクロックタイミングで High になる
  • 図中の lpoint は hpoint + duty に等しく、この次のクロックタイミングで出力は Low になる
  • カウンタがオーバーフローしても波形は変化しない

...ということで、オーバーフローで電圧波形がリセットされる他のマイコンに比べて、特にこのマイコンはオーバーフロー時のアクションがない(割り込み発生しない)ことが特徴的です。

動かしてみる

では、ここから2つの方法のLEDボヤァを実装してみましょう。

ループで

無限ループによって Duty 値を変えていくという、古典的な方法です。メインのループのみ示します。

    while(1) {
        vTaskDelay(20 / portTICK_PERIOD_MS); // wait for 20msec
        duty -= 16;
        if( duty == 0 ) duty = 1024;
        ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_TIMER_0, duty);
        ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_TIMER_0);
    }

set した後に update することで反映されます。特に説明は不要ですかね。設定も含めたコード全体はこちら

fade関数で

fade という機能を提供する関数があります。この fade が「ボヤァ」という意味なのですが、内部的な割り込みを利用してあげることで、上のループ処理を記述することなく実装可能です。

    ledc_fade_func_install(ESP_INTR_FLAG_LEVEL1);
    ledc_set_fade_with_step(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, 1024, 16, 10);
    while(1) {
        ledc_fade_start(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_WAIT_DONE);
        vTaskDelay(1);
    }

割り込み関数を ledc_fade_func_install で登録する必要がありますが、自分で実装を書く必要はありません。その後 ledc_set_fade_with_step で必要なパラメータを指定すればよいです。
最後に ledc_fade_start を実行することでスタートです。このとき LEDC_FADE_WAIT_DONE を引数に指定すると、ボヤァが1回終わるまでこの関数から返ってきません。それを利用したのがこのコードです。逆に LEDC_FADE_NO_WAIT とすると、呼び出し直後からすぐに返ってきます。
vTaskDelay(1) は ESP32 のウォッチドッグタイマによるリセットが作動しないように入れている待ちです。どうやら無限ループ内ではこれを入れる必要があるっぽい。設定も含めたコード全体はこちら

補足

追加の説明です。

Low→Highと変化する波形の作成

難しいことを考えずに hpoint == 0 の状態でこの PWM を使うと、High→Low と変化する PWM 波形になります。その逆をしてみようということです。

先の例では hpoint を固定していました。すると lopoint は duty そのものになり、これで制御ができていましたよね。
ここで、逆に lpoint をカウンタの最大値に固定してみると、今度は hpoint を動かすことが出来ます。すなわち、タイマーカウンタのオーバーフロー時に Low リセットされる、High→Low へと変化する PWM 波形が出来るということです。やってみましょう。

まず、先程の lpoint を最大値に固定する準備をします。マクロでも使いますかね。

#define LPOINT 1023

更に、初期出力をしないように hpoint == lpoint とします。つまり、hpoint == LPOINT, duty == 0 とすればいいわけです。

    .duty = 0,
    .hpoint = LPOINT

ではここからどうやって動かしていくかですが、ledc_set_duty_with_hpoint という関数を使って工夫します。
この時、以下の数量関係に注意です。

  • hpoint は 設定したいパルス幅(duty とおく)
  • lpoint は hpoint + duty == LPOINT

以上から、hpoint = duty, duty = LPOINT - hpoint とすれば良いとわかります。これを関数に渡すわけです。

    uint32_t duty = 0;
    while(1) {
        vTaskDelay(20 / portTICK_PERIOD_MS);
        duty += 16;
        if( duty == 1024 ) duty = 0;
        ledc_set_duty_with_hpoint(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, duty, LPOINT - duty);
        ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_TIMER_0);
    }

これにて、逆論理の波形が出来ました。

割り込み

このペリフェラルでは割り込みを使用できますが、使い所が限定されています。先程の fade 関数を用いたときのみです。

使用するには、まず割り込みハンドル用の構造体を大域変数として宣言します。

ledc_isr_handle_t handle; // グローバル変数

そして、割り込み関数を書きましょう。

void ledc_fade_isr(void *arg) {
    ets_printf("OK\n");
}

最後に、初期化時に設定してやれば OK です。

ledc_isr_register(ledc_fade_isr, NULL, ESP_INTR_FLAG_EDGE, &handle); // register isr; Only for fade

こうすれば、ledc_fade_start を使用した際に LED が最大値まで光った瞬間に割り込みが入ります。