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 制御を行うことができ, 複数のピンを同じチャンネルにまとめて一度に制御することもできる. ESP-IDF 環境では PWM の HIGH スピードモードと LOW スピードモードが選べるが, 基本的に HIGH スピードモードを使っておけばよい.

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  = このチャンネルに紐づけるタイマーの番号, 
     .speed_mode = スピードモード (HIGH と LOW がある)
 };
 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 の明るさは暗くなる.

プロジェクトの準備

サンプルプロジェクトをコピーする.

$ cd ~/esp

$ cp -r esp-idf/examples/get-started/sample_project ./pwm1

$ cd pwm1

プログラムの作成

このサンプルプロジェクトのメインファイルは 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 = 13;    // LED ピン番号
   ledc_timer_t   timer   = 0; // タイマー
   ledc_channel_t channel = 0; // チャンネル番号
   uint32_t       freq = 1000; // 周波数
   uint32_t       duty;        // デューティ比     

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

   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,               // このチャンネルに紐づけるタイマーの番号         
      .speed_mode = LEDC_HIGH_SPEED_MODE // timer mode
   };
   ledc_channel_config(&ledc_channel);  //チャンネルの設定

   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 の持続時間の和が波の周期に対応する.

プロジェクトの準備

サンプルプロジェクトをコピーする.

$ cd ~/esp

$ cp -r esp-idf/examples/get-started/sample_project ./pwm2

$ cd pwm2

プログラムの作成

このサンプルプロジェクトのメインファイルは 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_fade_func_install(0); // 後述の ledc_set_duty_and_update 関数を使うために必要

   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,               // このチャンネルに紐づけるタイマーの番号
      .speed_mode = LEDC_HIGH_SPEED_MODE // timer mode
   };
   ledc_channel_config(&ledc_channel);  //チャンネルの設定

   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 ボタンを同時に押せばよい.