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 Guide の API Reference の LED 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 の明るさは暗くなる.
プロジェクトの準備
サンプルプロジェクトをコピーする.
$ 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, // このチャンネルに紐づけるタイマーの番号
};
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 つ目が明るさが変化しながら点灯することが確認できるであろう.
例題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, // このチャンネルに紐づけるタイマーの番号
};
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 つ目が明るさが変化しながら点灯することが確認できるであろう.
レポート課題2: 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 = 0; // タイマー uint32_t freq = <穴2>; // 周波数 (Hz). uint32_t resolv = <穴3>; // 解像度. 8 bit 以外にすること. uint32_t duty[4] = { <穴4> }; // デューティ比.4 つ設定 ledc_fade_func_install(0); // 後述の ledc_set_duty_and_update 関数を使うために必要 //タイマー設定.全ての LED に同じタイマーを適用する ledc_timer_config_t ledc_timer = { .timer_num = timer, //タイマーの番号 .duty_resolution = resolv, // 解像度 .freq_hz = freq, // 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, // このチャンネルに紐づけるタイマーの番号 }; ledc_channel_config(&ledc_channel); //チャンネルの設定 } // LED を PWM で点灯. for (i = 0; i < 4; i++) { //LED を紐づけられた各チャネルに指定されたデューティー比を設定 ledc_set_duty_and_update(LEDC_HIGH_SPEED_MODE, ch[i], duty[i], 0); } }
[5] オシロスコープでの観測・スケッチ
- オシロスコープの使い方はマニュアルを見ること.
- p.9~ 基本的な使い方
- p.25~ 測定例
- 後述のオシロスコープの画面のスクリーンショットのように,2 チャンネルを上下に分けて表示すること.
- 測定開始 → マニュアル の測定例 (p.25~) ではオートセットアップを使うよう書かれていますが,AUTOSETだと予期しない設定になる可能性もあるので,電源を入れたら DEFAULTSETUP (工場出荷時設定) を押して下さい.CH1 MENU, CH2 MENU を押すとそれぞれの波形が表示されます.
- 波形が横に流れていく場合はトリガレベルを調整してください (マニュアル p.13)
- 画面の右端にトリガレベル用の矢印が出ているので,TRIG LEVELツマミで真ん中より上に上げる.
- 縦軸:1 目盛りを 1 V に設定すること (マニュアル p.12)
- 横軸:波形が 2 つ程度入るよう設定すること (マニュアル p.12-13)
- 上下 2 分割:Position つまみを調節する.CH1 の GND を縦軸の原点に,CH2 の GND を縦軸の真ん中に合わせること (マニュアル p.12)
- デューティー比を 4 通り変化させ,それぞれのケースでの電波波形をオシロスコープで測定する.それぞれのケースにおいて,フィルター(平滑)回路を通したときに得られる平均電圧をオシロスコープから読み取りなさい.
- デューティー比(プログラムの<穴2>) として異なる値を入力すること.
- 分解能(プログラムの<穴3>) を変えても構わない.

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

注意事項
ブレッドボードの表面の穴は内部で下図の緑枠で示されるような形で結線されている.
横方向の穴は内部で接続されており,同じブロックにある横方向の 5 個の穴 (例えば,1 番の A~E)のどれかに部品の足やワイヤを挿すと, 同じブロックの他の穴と自動的に接続される. これに対して,縦方向には裏面で接続されていないため,自動的には接続されない.
ブレッドボードの左右にある赤い線と青い 線は,それぞれ電源を接続するためのものであり,これらは縦方向に長く繋がっている.通常, 赤い線は電源の+側,青い線は電源のグラウンドに接続する.

レポートのまとめ方
以下の内容を記述すること.
- 実験の目的
- 実験方法
- 利用する機器の一覧
- 機器の接続方法
- オシロスコープをマイコンボードのどのピンに接続したか明記すること
- 実験設定
- 機器 (オシロスコープ) の設定
- プログラムの穴埋め部に何を入れたかまとめて示すこと.以下のような表形式で示すこと(表には表番号を付すこと).
- 書き方の例:「作成したプログラムは実験手順書に書かれているサンプルプログラムを元にした.サンプルプログラム中の<穴1>~<穴4>に入れた数字を表 X に示す.」
- プログラム全部をレポートで示す必要はありません.
| 穴1 | 穴2 | 穴3 | 穴4 | |
|---|---|---|---|---|
| ケース1 | 13 | 300 | LEDC_TIMER_10_BIT | 100 |
| ケース2 | 14 | 300 | LEDC_TIMER_10_BIT | 200 |
| ケース3 | 27 | 300 | LEDC_TIMER_10_BIT | 300 |
| ケース4 | 33 | 300 | LEDC_TIMER_10_BIT | 400 |
注: 適当に数字を入れてます.同じにする必要はありません.穴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 |
- 発展問題
- 設定した解像度 (穴3) とデューティー比 (カウント値) (穴4) に関する以下の問いに答えなさい
- 解像度を 6, 10, 12 bit に設定したとき,デューティー比 (カウント値) として取りうる値の範囲 (<穴4> として取りうる値の範囲) をそれぞれ答えなさい.
- 解像度を8 bit に設定したときに得られる平均電圧の有効数字が凡そいくつになるか説明しなさい.
- 設定した解像度 (穴3) とデューティー比 (カウント値) (穴4) に関する以下の問いに答えなさい