PWM (ESP-IDF 環境 + C 言語)

プログラムの書き方

PWM を使うと LOW (0) と HIGH (1) が交互に変化するデジタルな波形を作ることができる. PWM 波形の要素として,周波数とデューティー比がある.

デューティー比を変化させることで, 実効電圧を変化させることができる.

PWM 波形を作るために,マイコンにはタイマーとチャンネルという概念があり, タイマーに対して 1 周期を何ビットで表現するか (= 分解能) を設定する必要がある.

タイマーの分解能とデューティー比の関係は以下の図のようになっており, プログラム中ではデューティー比は LOW(0) と HIGH(1) の比率 (%) ではなく, 分解能に応じたカウント値で与えるのが一般的である.

なお, ESP32 のクロックは 80 MHz = 12.5 μs で動作するため, 分解能をそれよりも小さくすることはできない. 逆に言うと, タイマの分解能によって表現できる最大波長が決まる.

  • 1 bit ならば 1 / (12.5e-6 * 2) = 40 MHz
  • 8 bit ならば 1 / (12.5e-6 * 256) = 312.5 kHz
  • 13 bit ならば 1 / (12.5e-6 * 8192) = 9765 Hz

ESP32 は最大で 16 チャンネル分の PWM 制御を行うことができ, 複数のピンを同じチャンネルにまとめて一度に制御することもできる.

PWM の関数については, ESP-IDF Programming GuideAPI ReferenceLED Control (LEDC) を参照して欲しい.

以下に最低限使う必要のある関数を挙げる.

  • 周期・周波数決める「タイマー」と,そのタイマーを使ってパルス幅を決める「チャンネル」の 2 つを定義しないといけない.
    • タイマーを切り替えることで,例えば DC モータとサーボモータのそれぞれに対して異なる周波数のデジタル波形を与えることができる.
    • 複数の機器に同じチャンネルを割り当てることもできる.
  • デューティー比と周波数を変更するときは,ピン番号ではなく,チャンネル番号を指定する.

    //ヘッダファイルの読み込み
    #include "driver/ledc.h" 
    
    //タイマーの設定
    ledc_timer_config_t ledc_timer = {
         .timer_num = タイマーの番号,
         .duty_resolution = 分解能,
         .freq_hz = 周波数,
         .speed_mode = スピードモード (HIGH と LOW がある)
     };
     ledc_timer_config(&ledc_timer);
    
     //チャンネルの設定. 
     ledc_channel_config_t ledc_channel = {
         .channel    = チャンネル番号,
         .duty       = デューティ比,
         .gpio_num   = PWM として使うピン番号,
         .timer_sel  = このチャンネルに紐づけるタイマーの番号, 
     };
     ledc_channel_config(&ledc_channel);
    
    //duty 比の変更
    ledc_set_duty_update( スピードモード, チャンネル, デューティー比, hpoint )
    
    //周波数の変更
    ledc_fade_func_install(0); //ledc_set_duty_and_update 関数を使うために必要. 初期化で 1 回だけ行う.
    ledc_set_duty_and_update(スピードモード, チャンネル, デューティー比, hpoint);

例題 1: 電圧変化 (LED の明るさを変えるプログラム)

PWM を用いると,実効的な電圧を調整することができる. duty 比 50% とすると,LED にかかる電圧が半分となり,LED の明るさは暗くなる.

プロジェクトの準備

サンプルプロジェクトをコピーする.Linux を使う場合はターミナルで以下のように打鍵する.

$ cd ~/esp

$ idf.py create-project pwm

    Executing action: create-project
    The project was created in /home/sugiyama/esp/pwm

$ cd pwm

一方で,Windows の場合は PowerShell で以下のように打鍵する.

> cd C:\esp\v5.5.4

> idf.py create-project pwm

    Executing action: create-project
    The project was created in C:\esp\v5.5.4\pwm

> cd .\pwm\

プログラムの作成

このサンプルプロジェクトのメインファイルは main ディレクトリ以下の blink.c であるので, そのファイルを編集する.エディタとして,vi, emacs, gedit, code (VS code) などが使える. code (VS code) を使う場合は以下のように打鍵すると良い

Linux の場合

$ code main/pwm.c

Windwos の場合

> code.cmd .\main\pwm.c

pwm.c を以下のように書き換えて保存する.

#include <stdio.h>
#include "driver/ledc.h"        //このヘッダファイルが必要
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void app_main(void)
{
   gpio_num_t     pin = 13;    // LED ピン番号
   ledc_timer_t   timer   = 0; // タイマー
   ledc_channel_t channel = 0; // チャンネル番号
   uint32_t       freq = 1000; // 周波数
   uint32_t       duty;        // デューティ比     

   ledc_timer_config_t ledc_timer = {
      .timer_num  = timer,                  //タイマーの番号
      .duty_resolution = LEDC_TIMER_8_BIT, // resolution of PWM duty. 8bit (0-255) に設定.
      .freq_hz    = freq,                     // frequency of PWM signal
      .speed_mode = LEDC_HIGH_SPEED_MODE   // timer mode
   };
   ledc_timer_config(&ledc_timer);        //タイマーの設定

   ledc_channel_config_t ledc_channel = {
      .channel    = channel,             // チャンネル番号
      .duty       = 0,                   // 初期のデューティ比
      .gpio_num   = pin,                 // ピン番号
      .timer_sel  = timer,               // このチャンネルに紐づけるタイマーの番号
   };
   ledc_channel_config(&ledc_channel);  //チャンネルの設定

   ledc_fade_func_install(0); // 後述の ledc_set_duty_and_update 関数を使うために必要

   int i = 0;
   while (1) {
      duty = (i * 50) % 250;  //duty 比を周期的に変化.0, 10, 200, ..., 500, 0, 100, ...

      // LED に対するデューティ比を変更.ピン番号ではなくチャンネルを指定
      ledc_set_duty_and_update(LEDC_HIGH_SPEED_MODE, channel, duty, 0);

      //待ち.1秒
      vTaskDelay(1000 / portTICK_PERIOD_MS);

      //ループを回す
      i += 1;
   }
}

ビルドとマイコンへの書き込み

idf.py build コマンドを実行する.

$ idf.py build

マイコンに書き込むのは idf.py flash コマンド, 標準出力を表示するのは idf.py monitor コマンドである. まとめて idf.py flash monitor としても良い.

$ idf.py flash monitor

実習ボードの LED の 1 つ目が明るさが変化しながら点灯することが確認できるであろう.

monitor を終了するのは Ctrl-] である.

例題2: 周波数変化 (圧電ブザーを鳴らすプログラム)

音は波であるため, 音を出したい時には電圧の HIGH と LOW を繰り返すことで, プログラム的に下記に示すような特定の周波数を持つ波 (矩形波) を作れば良い. HIGH と LOW の持続時間の和が波の周期に対応する.

プログラムの作成

このサンプルプロジェクトのメインファイルは main ディレクトリ以下の main.c であるので, そのファイルを編集する.エディタとして,vi, emacs, gedit, code などが使える.

#include <stdio.h>
#include "driver/ledc.h"  //このヘッダファイルが必要
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void app_main(void)
{
   gpio_num_t     pin = 15;    //ブザーのピン番号
   ledc_timer_t   timer   = 1; //タイマー
   ledc_channel_t channel = 1; //チャンネル
   uint32_t       freq = 500;  // 周波数
   uint32_t       duty = 128;  // デューティ比 50% に相当

   ledc_timer_config_t ledc_timer = {
      .timer_num  = timer,                 //タイマーの番号
      .duty_resolution = LEDC_TIMER_8_BIT, // resolution of PWM duty. 8bit (0-255) に設定.
      .freq_hz    = freq,                  // frequency of PWM signal
      .speed_mode = LEDC_HIGH_SPEED_MODE   // timer mode
   };
   ledc_timer_config(&ledc_timer);        //タイマーの設定

   ledc_channel_config_t ledc_channel = {
      .channel    = channel,             // チャンネルの番号
      .duty       = duty,                // 初期のデューティ比
      .gpio_num   = pin,                 // ピン番号
      .timer_sel  = timer,               // このチャンネルに紐づけるタイマーの番号
   };
   ledc_channel_config(&ledc_channel);  //チャンネルの設定

   ledc_fade_func_install(0); // 後述の ledc_set_duty_and_update 関数を使うために必要

   while (1) {
      //音を出すために,duty比を50% に.
      duty = 128;
      ledc_set_duty_and_update(LEDC_HIGH_SPEED_MODE, channel, duty, 0);

      //ド
      freq = 262;
      ledc_set_freq(LEDC_HIGH_SPEED_MODE, timer, freq);
      vTaskDelay(1000 / portTICK_PERIOD_MS);

      // レ
      freq = 294;
      ledc_set_freq(LEDC_HIGH_SPEED_MODE, timer, freq);
      vTaskDelay(1000 / portTICK_PERIOD_MS);

      // ミ
      freq = 330;
      ledc_set_freq(LEDC_HIGH_SPEED_MODE, timer, freq);
      vTaskDelay(1000 / portTICK_PERIOD_MS);

      //音を止める.duty比をゼロに.
      duty = 0;
      ledc_set_duty_and_update(LEDC_HIGH_SPEED_MODE, channel, duty, 0);
      vTaskDelay(1000 / portTICK_PERIOD_MS);
   }
}

ビルドとマイコンへの書き込み

idf.py build コマンドを実行する.

$ idf.py build

マイコンに書き込むのは idf.py flash コマンド, 標準出力を表示するのは idf.py monitor コマンドである. まとめて idf.py flash monitor としても良い.

$ idf.py flash monitor

実習ボードの LED の 1 つ目が明るさが変化しながら点灯することが確認できるであろう.

monitor を終了するのは Ctrl-] である.
音を止めるには,ボード上の BOOT ボタンと EN ボタンを同時に押せばよい.

レポート課題 3: PWM 波形 + 平均電圧の観測

実施方法

[1] 目的:

  • マイコンの PWM 機能を使って作成した電圧の波形をオシロスコープで測定し,プログラム中で指定した周波数・デューティー比との関係を理解する.

[2] 使用する機器:

  • マイコンボード
    • ボード上の LED で使っているピンをオシロスコープに接続すること
  • オシロスコープ
  • ブレッドボート
  • ジャンパーケーブル
  • 抵抗
  • コンデンサー

[3] 機器の接続方法

[4] プログラム:

  • 以下のプログラムの穴埋め (4 箇所, <穴1>~<穴4>) を行ってプログラムを完成させなさい.但し,LED は左端以外のものを選択すること.

    #include <stdio.h>
    #include "driver/ledc.h"
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    
    void app_main(void)
    {
      int i;
      gpio_num_t pin[4]     = { <穴1> };      //ピン番号
      uint32_t ch[4]        = { 0, 1, 2, 3 }; //チャンネル番号
    
      ledc_timer_t timer[2] = {0, 1};    // タイマー2 つ.
      uint32_t freq[2]      = {<穴2>};   // 周波数 (Hz).タイマー毎に設定
      uint32_t resolv[2]    = {<穴3>};   // 解像度. 8 bit 以外にすること.タイマー毎に設定
      uint32_t duty[4]      = { <穴4> }; // デューティ比.4 つ設定
    
      //タイマー設定.LED 2 つに対して 1 つのタイマーを適用する
      for (i = 0; i < 2; i++) {
         ledc_timer_config_t ledc_timer = {
            .timer_num  = timer[i],             //タイマーの番号
            .duty_resolution = resolv[i],       // 解像度
            .freq_hz    = freq[i],              // frequency of PWM signal
            .speed_mode = LEDC_HIGH_SPEED_MODE  // timer mode
         };
         ledc_timer_config(&ledc_timer);        //タイマーの設定
      }
    
      //チャンネル設定. 各 LED に異なるチャンネルを割り当てる
      for (i = 0; i < 4; i++) {
         ledc_channel_config_t ledc_channel = {
            .channel    = ch[i],               // チャンネル番号
            .duty       = 0,                   // 初期のデューティ比
            .gpio_num   = pin[i],              // ピン番号
            .timer_sel  = timer[i/2],          // このチャンネルに紐づけるタイマーの番号
         };
         ledc_channel_config(&ledc_channel);  //チャンネルの設定
      }
    
      ledc_fade_func_install(0);        // 後述の ledc_set_duty_and_update 関数を使うために必要
    
      // LED を PWM で点灯.
      for (i = 0; i < 4; i++) {
         //LED を紐づけられた各チャネルに指定されたデューティー比を設定
         ledc_set_duty_and_update(LEDC_HIGH_SPEED_MODE, ch[i], duty[i], 0);
      }
    }

[5] オシロスコープでの観察・スケッチ

  • デューティー比の異なる 4 つの LED について,PWM の周期とパルス幅をオシロスコープから読み取る.さらにフィルター(平滑)回路を通したときに得られる平均電圧もオシロスコープから読み取る.

  • どれか 1 つのケースについて,電圧波形を観測・スケッチする.
    • スケッチにはグラフ用紙を使用することとし,単位等を正確に記述すること.

オシロスコープの使い方

1. プローブの接続
   2 本のプローブを使って以下の通り接続する
   1-1. CH1(チャンネル1・黄色): 平滑化回路の出力(コンデンサの+側など、電圧が平均化された部分)に測定針を繋ぐ
   1-2. CH2(チャンネル2・水色): マイコン基板の PWM 出力ピン(平滑化回路に入る前の元の信号)に測定針を繋ぐ
   1-3. GND(黒いワニ口クリップ): CH1 と CH2 の両方のクリップを、マイコン基板の GND に繋ぐ。

2. オシロスコープの初期設定(画面の整理)
   前の人の設定が残っていると測定に失敗するため,必ず工場出荷時の状態にリセットする.
   2-1. パネル右上の [DEFAULT SETUP] ボタンを押す.      
   2-2. パネル中央の [CH2 MENU] ボタンを押す(水色の線が追加される)
   2-3. 画面に不要なメニューが出ている場合は、[Menu Off] ボタン(画面右下)を押して消す.      
   2-4. CH1 の波形(黄色の線)と CH2 の波形(水色の線)だけが表示されていることを確認する.

3. スケール(縦軸・横軸)の調整
   波形を画面に収めるために設定する.
   3-1. 縦軸(電圧)の設定:
      ・CH1 と CH2 の [SCALE] ツマミ(大きい方)を回し,画面左下の表示を 「CH1 1.00V/div」(1目盛りが1V)にする.
      ・CH1 と CH2 の [POSITION] ツマミ(小さい方)を回し,黄色の線を画面の真ん中の横線に合わせる
       (ここが 0 V の基準になる)
   3-2. 横軸(時間)の設定:
      ・HORIZONTAL(水平)エリアの [SEC/DIV] ツマミを回し,画面中央下の表示を 「50.0μs/div」 または
        「100μs/div」にし,波形が画面に2〜3個(2〜3周期分)入るように時間を調整する.

4. トリガ(波形を止める)の設定
   波形が右や左に流れるのを防ぐための設定を行う.必ずCH2(PWM信号)を基準にして止めること.
   4-1. TRIGGER(トリガ)エリアの [TRIG MENU] ボタンを押す.画面右側にメニューが出る.
   4-2. メニュー横のボタンを押して以下のように設定する.
      ・タイプ:エッジ
      ・ソース:CH2 (重要!)
      ・スロープ:立ち上がり
   4-3. [TRIG LEVEL] ツマミを回して画面右端にある水色の小さな矢印(トリガレベルの目印)を
        真ん中より少し上 1.5 V 付近にあわせる

5. 測定の実行(シングルショット撮影)
   5-1. 接続するピンを取り換えながら PWM 波形と平均電圧を観察する.

注意事項

ブレッドボードの表面の穴は内部で下図の緑枠で示されるような形で結線されている.

横方向の穴は内部で接続されており,同じブロックにある横方向の 5 個の穴 (例えば,1 番の A~E)のどれかに部品の足やワイヤを挿すと, 同じブロックの他の穴と自動的に接続される. これに対して,縦方向には裏面で接続されていないため,自動的には接続されない.

ブレッドボードの左右にある赤い線と青い 線は,それぞれ電源を接続するためのものであり,これらは縦方向に長く繋がっている.通常, 赤い線は電源の+側,青い線は電源のグラウンドに接続する.

レポートのまとめ方

以下の内容を記述すること.

  • 実験の目的
  • 実験方法
    • 利用する機器の一覧
    • 機器の接続方法
      • オシロスコープをマイコンボードのどのピンに接続したか明記すること
  • 実験設定
    • 機器 (オシロスコープ) の設定
    • プログラムの穴埋め部に何を入れたかまとめて示すこと.以下のような表形式で示すこと(表には表番号を付すこと).
      • 書き方の例:「作成したプログラムは実験手順書に書かれているサンプルプログラムを元にした.サンプルプログラム中の<穴1>~<穴4>に入れた数字を表 X に示す.」
      • プログラム全部をレポートで示す必要はありません.

穴1穴2穴3穴4
ケース113200LEDC_TIMER_7_BIT50
ケース214200LEDC_TIMER_7_BIT100
ケース327300LEDC_TIMER_10_BIT300
ケース433300LEDC_TIMER_10_BIT400

注: 適当に数字を入れてます.同じにする必要はありません.穴2, 穴3 は共通になるはずです

  • 実験結果1
    • オシロスコープで得られた実際の周期・周波数・パルス幅・フィルター(平滑)回路を通したときに得られる平均電圧をオシロスコープから読み取り,それを記録する.
    • 表には単位を付けるのを忘れないこと!

周期パルス幅平均電圧の測定値(V)
ケース1
ケース2
ケース3
ケース4

  • 実験結果2
    • どれか 1 つのケースを選び,オシロスコープで得られた電圧波形のスケッチを示すこと.
  • 議論
    • ケース 1 ~ 4 の実験設定を元に計算を行い,以下の表の形式にまとめる.
    • 周期とパルス幅について,プログラムで与えた実験設定から計算して得た値と,実験結果 1 で得た値を比較し,実験設定と実験結果が整合的になっているか論じなさい.
    • 実験結果 1 で得た平均電圧の測定値が,「実験設定から計算したデューティー比 × 定格電圧 (3.3V) 」と整合的になっているか論じなさい.

プロラムで与えた周波数(Hz)プログラムで与えた解像度(bit)プログラムで与えたデューティー比(カウント値)プログラムで与えた数値から計算した周期(ms)プログラムで与えた数値から計算したパルス幅(ms)プログラムで与えた数値から計算したデューティー比 (%)計算したデューティー比(%)と定格電圧 3.3 V の掛け算で得た平均電圧
ケース1
ケース2
ケース3
ケース4