操作方法を知って使いこなす! pwm コマンドで Pico の PWM の限界を探る
pico-jxgLABO は、USB ケーブル一本でマイコンボード RaspberryPi Pico の様々な機能を試すことができる実験プラットフォームです。
今回の記事では、pico-jxgLABO の pwm
コマンドを使って Pico の PWM 機能の実験をする方法を紹介します。
PWM (Pulse Width Modulation) は、モーターの速度制御や LED の明るさ調整など、様々な用途で使われる技術です。Pico ボードは、GPIO ピンを使って PWM 信号を生成することができますが、その設定はちょっとわかりづらいところがあります。まあ動けばいいかな、とただサンプルプログラムをコピーしてきて動かしている人も多いのではないでしょうか。
pico-jxgLABO を使えば、実際にパラメータを操作してロジックアナライザで波形を観測しながら、PWM の設定や動作を理解することができます。
pico-jxgLABO の書き込み
Pico ボードへの pico-jxgLABO の書き込みと基本的な使い方はこちら。特別なハードウェアは必要なく、Pico や Pico 2 ボードを USB ケーブルで PC に接続するだけで始められます。
この記事で説明する実験を行うには、バージョン 0.2.0
以降の pico-jxgLABO が Pico ボードに書き込まれている必要があります。ターミナルソフトで about-me
コマンドを実行すると pico-jxgLABO のバージョンを確認できます。
L:/>about-me
Program Information
name: pico-jxgLABO
version: 0.2.0
:
:
pwm
コマンドを使った PWM の制御
pwm
コマンドを使って PWM の実験をしていきます。ここでは Pico 2 を使いますが、Pico でも同様に実験できます。
現在の PWM 状態の表示
引数なしで pwm
を実行すると、すべての GPIO の PWM 設定状態が表示されます。
L:/>pwm
GPIO0 func:------
GPIO1 func:------
GPIO2 func:------
GPIO3 func:------
GPIO4 func:------
GPIO5 func:------
GPIO6 func:------
GPIO7 func:------
GPIO8 func:------
GPIO9 func:------
GPIO10 func:------
GPIO11 func:------
GPIO12 func:------
GPIO13 func:------
GPIO14 func:------
GPIO15 func:------
GPIO16 func:------
GPIO17 func:------
GPIO18 func:------
GPIO19 func:------
GPIO20 func:------
GPIO21 func:------
GPIO22 func:------
GPIO23*func:------
GPIO24*func:------
GPIO25*func:------
GPIO26 func:------
GPIO27 func:------
GPIO28 func:------
GPIO29*func:------
まだ PWM の設定は行われていないので、すべての GPIO の func
が ------
となっています。GPIO ピン番号を指定してサブコマンド func:pwm
を実行すると、ファンクションを PWM に設定できます。以下の例では、すべての GPIO ピンを PWM に設定しています。
L:/>pwm 0- func:pwm
GPIO0 func:PWM0 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO1 func:PWM0 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO2 func:PWM1 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO3 func:PWM1 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO4 func:PWM2 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO5 func:PWM2 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO6 func:PWM3 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO7 func:PWM3 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO8 func:PWM4 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO9 func:PWM4 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO10 func:PWM5 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO11 func:PWM5 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO12 func:PWM6 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO13 func:PWM6 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO14 func:PWM7 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO15 func:PWM7 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO16 func:PWM0 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO17 func:PWM0 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO18 func:PWM1 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO19 func:PWM1 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO20 func:PWM2 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO21 func:PWM2 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO22 func:PWM3 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO23*func:------
GPIO24*func:------
GPIO25*func:------
GPIO26 func:PWM5 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO27 func:PWM5 B disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO28 func:PWM6 A disabled freq:2288Hz (clkdiv:1.0 wrap:0xffff) duty:0.000 (level:0x0000) counter:0x0000
GPIO29*func:------
GPIO23, GPIO24, GPIO25, GPIO29 は特殊用途に使われるため、pwm
では操作ができないようになっています。ただし、Pico や Pico 2 の GPIO25 は内蔵 LED に接続されており、手軽に使えて便利なのでオプション -B
(または --builtin-led
) を指定することで操作できるようになっています。
Pico の GPIO はすべて PWM に対応しています。Pico の PWM の機能単位はスライスと呼ばれ、PWM0 から PWM7 までの 8 個のスライスがあります。各スライスは A と B の 2 つのチャネルを持ち、合計で 16 個のチャネルが存在します。
スライスごとに PWM の有効/無効状態、周波数やカウンタの設定ができますが、A と B のチャネルは同じスライスの設定を共有します。つまり、A と B のチャネルは同じ周波数で動作し、カウンタも同期して動きます。同じスライス内では、デューティ比だけがチャネル A と B で個別に設定できます。
それでは、PWM の実験をしていきましょう。以下のコマンドを実行して、ファンクションを初期状態に戻しておいてください。
L:/>pwm 0- func:-
GPIO ピン番号の指定方法
pwm
コマンドの最初の引数には、GPIO ピン番号を指定します。単一の GPIO ピン番号を指定することもできますし、範囲を指定することもできます。例を以下に示します。
コマンド | 説明 |
---|---|
pwm 0 |
GPIO0 の PWM の状態を表示します |
pwm 2,3,8,9 |
GPIO2,3,8,9 の PWM の状態を表示します |
pwm 2-15 |
GPIO2 から GPIO15 までの PWM の状態を表示します |
pwm 8- |
GPIO8 から GPIO29 までの PWM の状態を表示します |
単一のピン操作のためのコマンド pwm0
から pwm29
も用意されています。例えば pwm2
は pwm 2
と同じ意味になります。
LED の明るさを PWM で調整する
まずは、LED の明るさを PWM で調整してみましょう。Pico や Pico 2 の GPIO25 に接続された LED を制御します。Pico W, Pico 2 W の場合は適当な GPIO ピンに LED を接続してください。
以下のコマンドを実行して、GPIO25 の PWM を有効にし、周波数を 100Hz、デューティ比を 0.5 (50%) に設定します。オプション -B
は、GPIO25 の操作を許可するためのものです。
L:/>pwm25 -B func:pwm freq:100 duty:0.5 enable
GPIO25 func:PWM4 B enabled freq:100Hz (clkdiv:23.0 wrap:0xfec0) duty:0.500 (level:0x7f60) counter:0x0bd5
デューティ比に 0 から 1 の間の値を設定すると LED の明るさが変化します。指定されていないオプションは、前回の設定が引き継がれます。
L:/>pwm25 -B duty:0
L:/>pwm25 -B duty:0.2
L:/>pwm25 -B duty:0.4
L:/>pwm25 -B duty:0.6
L:/>pwm25 -B duty:0.8
L:/>pwm25 -B duty:1
PWM 波形の観測
デューティ比は、PWM 信号の high と low の比率を表します。ロジックアナライザを使って波形を観測することで、デューティ比がどのように PWM 信号に反映されるかを確認できます。
以下の手順でコマンドを実行してください。
-
ロジックアナライザを起動して GPIO2, GPIO3, GPIO4, GPIO5 の 4 本のピンの測定を開始します。
L:/>la -p 2-5 enable
-
周波数が 100Hz、デューティ比がそれぞれ 0.2, 0.4, 0.6, 0.8 の PWM 信号を出力します。
L:/>pwm 2-5 func:pwm disable freq:100 counter:0 L:/>pwm2 duty:.2; pwm3 duty:.4; pwm4 duty:.6; pwm5 duty:.8 L:/>pwm 2-5 enable
-
測定した波形を表示します。
L:/>la print
測定結果を以下に示します。シリアルターミナルの画面をキャプチャして横向きに回転しています。
下から、デューティ比 0.2, 0.4, 0.6, 0.8 の PWM 信号が観測できています。デューティ比が大きくなるほど、high の時間が長くなり、波形の幅が広がっていることがわかります。一つの周期は波形の繰り返しパターンの間隔、例えば 0.00usec から 9999.92usec の間ですから、PWM 周期
PWM 周波数の設定方法
pwm
コマンドでは PWM の周波数を変更するために freq
サブコマンドが使えますが、この内部では PWM のクロック分周比 (clkdiv
値) とカウンタのラップアップ値 (wrap
値) の設定が行われています。これらの設定値について以下説明します。
PWM は内部に 16bit のカウンタを持っており、0 (0x0000) から 65535 (0xFFFF) までの値をとります。カウンタは CPU のクロック信号を clkdiv
値で分周した周期で一つずつ増加し、設定された wrap
値に達すると 0 に戻ります。 wrap
値はカウンタがとりうる最大値になるので、Pico のマニュアルでは TOP
とも呼称されています。
clkdiv
値と wrap
値がどのように PWM の周波数に影響するかを理解するため、以下の実験を行います。
clkdiv
値は CPU クロックの分周比を 1.0 から 255.9 [1] までの範囲で設定します。例えば、clkdiv
を 1.0 にすると PWM スライスは CPU クロックと同じ周波数 (1/1.0) で動作し、2.0 にすると CPU クロックの半分 (1/2.0) の周波数で動作します。以下、 wrap
値を最小の 1 に設定して、clkdiv
値を変えてみます。
L:/>pwm0 func:pwm clkdiv:1.0 wrap:1
GPIO0 func:PWM0 A disabled freq:75000000Hz (clkdiv:1.0 wrap:0x0001) duty:0.000 (level:0x0000) counter:0x0000
L:/>pwm0 func:pwm clkdiv:2.0 wrap:1
GPIO0 func:PWM0 A disabled freq:37500000Hz (clkdiv:2.0 wrap:0x0001) duty:0.000 (level:0x0000) counter:0x0000
L:/>pwm0 func:pwm clkdiv:3.0 wrap:1
GPIO0 func:PWM0 A disabled freq:25000000Hz (clkdiv:3.0 wrap:0x0001) duty:0.000 (level:0x0000) counter:0x0000
Pico 2 は CPU クロックが 150MHz で、分周比が 1.0 の場合はそのクロックが直接 PWM の周波数になります。 wrap
値を 1 に設定しましたから、カウンタ値は 0 から始まって 1 になると 0 に戻る、つまり 2 回のカウントで 1 周期となります。PWM に与えられたクロックの 1/2、つまり 75MHz の信号を出力します。
以下、clkdiv
値を 2.0、3.0 と変えていくと、周波数が 1/2, 1/3, ... と変化していくことがわかります。clkdiv
値に対して PWM 信号の周波数は
wrap
値はカウンタの最高値を 1 から 65535 までの範囲で設定します。以下、clkdiv
値を 1.0 に固定して wrap
値を変えていきます。
L:/>pwm0 func:pwm clkdiv:1.0 wrap:1
GPIO0 func:PWM0 A disabled freq:75000000Hz (clkdiv:1.0 wrap:0x0001) duty:0.000 (level:0x0000) counter:0x0000
L:/>pwm0 func:pwm clkdiv:1.0 wrap:2
GPIO0 func:PWM0 A disabled freq:50000000Hz (clkdiv:1.0 wrap:0x0002) duty:0.000 (level:0x0000) counter:0x0000
L:/>pwm0 func:pwm clkdiv:1.0 wrap:3
GPIO0 func:PWM0 A disabled freq:37500000Hz (clkdiv:1.0 wrap:0x0003) duty:0.000 (level:0x0000) counter:0x0000
分周比が 1 ですから、PWM スライスには 150MHz のクロックが供給されています。 wrap
値を 1 に設定すると、カウンタは 0 から 1 までの 2 回のカウントで 1 周期となり、周波数は 75MHz になります。
wrap
値を 2 に設定すると、カウンタは 0, 1, 2 の後に 0 に戻り、1 周期が 3 回のカウントで構成されるため、周波数は 1/3 の 50MHz になります。
wrap
値を 3 に設定すると、カウンタは 0, 1, 2, 3 の後に 0 に戻り、1 周期が 4 回のカウントで構成されるため、周波数は 1/4 の 37.5MHz になります。
つまり、 wrap
値に対して PWM 信号の周波数は
以上、clkdiv
値と wrap
値を組み合わせると、PWM 信号の周波数
最高周波数と最低周波数
clkdiv
値を 1.0、wrap
値を 1 に設定すると、PWM の最高周波数が得られます。Pico 2 の場合、これは level
= 0), 50% (level
= 1), 100% (level
= 2) の 3 種類しか設定できません。
clkdiv
値を 255.9、wrap
値を 65535 に設定すると、PWM の最低周波数が得られます。Pico 2 の場合、これは
PWM デューティ比の設定方法
PWM のデューティ比は、level
値を設定することで制御します。
PWM スライスは、カウンタが 0 になったときに出力を high にします。そこからカウンタ値を増やしていき、level
値になったときに出力を low にします。さらにカウンタ値を増やして wrap
値になると 0 に戻ります。
level
がとる値は 0 から wrap
+ 1 までの範囲です。level
が wrap
+ 1 の場合は、カウンタがその値に達することはないので、出力は常に high になります (デューティ比 100%)。
ここで一つ疑問が生まれます。level
を 0 に設定するとどうなるのでしょうか? カウンタが 0 になったとき、PWM スライスは出力を high にしますが、それは同時に level
に達したことでもあるので low にもしなければいけません。
出力が high になってその直後に low になる、インパルスのような波形が出力される? 実際、そのような出力をしてしまうチップもあり、この現象をグリッチと呼びます。でもこのような信号は制御対象のデバイスの誤動作を引き起こす可能性があって好ましくありません。
Pico ではこのあたりをきちんと考慮していて、level
を 0 に設定すると、出力が常に low になるようにしています (デューティ比 0%)。グリッチが生じる心配はありません。
level
値に対するデューティ比は以下の式で計算されます。
level
がとる値は 0 から wrap
+ 1 までですから、wrap
値が大きいほど level
で設定できるデューティ比の分解能が高くなります。
Phase Correct PWM
通常の PWM はカウンタが 0 から wrap
値に達するまでカウントアップしていきますが、Phase Correct PWM ではカウンタが 0 から wrap
値に達した後、再び 0 に戻るのではなく、wrap
値から 0 に向かってカウントダウンします。これにより、PWM 信号の上昇と下降が対称になり、デューティ比を変化したときでも位相がそろった波形が得られます。
pwm
コマンドでサブコマンド phase-correct:true
を実行すると、Phase Correct PWM が有効になります。
以下の手順で、波形を観測しながら Phase Correct PWM の動作を確認してみましょう。
-
ロジックアナライザを起動して GPIO2, GPIO3, GPIO4, GPIO5 の 4 本のピンの測定を開始します。
L:/>la -p 2-5 enable
-
周波数が 100Hz、デューティ比がそれぞれ 0.2, 0.4, 0.6, 0.8 の PWM 信号を出力します。Phase Correct PWM を有効にするために、サブコマンド
phase-correct:true
を指定します。Phase Correct の設定は周波数に影響するので、freq
サブコマンドよりも前に指定します。L:/>pwm 2-5 func:pwm disable phase-correct:true freq:100 counter:0 L:/>pwm2 duty:.2; pwm3 duty:.4; pwm4 duty:.6; pwm5 duty:.8 L:/>pwm 2-5 enable
-
測定した波形を表示します。
L:/>la print
測定結果を以下に示します。位相の基準点に対して左右対称でパルス幅が変化していることがわかります。
Phase Correct PWM はカウントアップとカウントダウンを行うため、PWM 信号の周波数は通常の PWM の 1/2 になります。Phase Correct を有効にした
C/C++ API との関連
pwm
のサブコマンドに相当する Pico SDK の API は以下の通りです。
サブコマンド | Pico SDK API |
---|---|
func |
gpio_set_function(uint gpio, gpio_function_t fn) |
enable |
pwm_set_enabled(uint slice_num, bool enabled) |
disable |
pwm_set_enabled(uint slice_num, bool enabled) |
freq |
(なし。clkdiv と wrap から計算) |
clkdiv |
pwm_set_clkdiv(uint slice_num, float divider) |
wrap |
pwm_set_wrap(uint slice_num, uint16_t wrap) |
duty |
(なし。level と wrap から計算) |
level |
pwm_set_chan_level(uint slice_num, uint chan, uint16_t level) |
counter |
pwm_set_counter(uint slice_num, uint16_t c) |
phase-correct |
pwm_set_phase_correct(uint slice_num, bool phase_correct) |
pwm
のサブコマンドに相当する pico-jxglib の API は以下の通りです。
サブコマンド | pico-jxglib API |
---|---|
func |
GPIO::set_function(gpio_function_t fn) |
enable |
PWM::set_enabled(bool enabled) |
disable |
PWM::set_enabled(bool enabled) |
freq |
PWM::set_freq(uint32_t freq) |
clkdiv |
PWM::set_clkdiv(float divider) |
wrap |
PWM::set_wrap(uint16_t wrap) |
duty |
PWM::set_chan_duty(float duty) |
level |
PWM::set_chan_level(uint16_t level) |
counter |
PWM::set_counter(uint16_t c) |
phase-correct |
PWM::set_phase_correct(bool phase_correct) |
まとめ
この記事では、pico-jxgLABO を使って Pico の PWM 機能を実験する方法を紹介しました。PWM の設定や動作を理解するために、実際にコマンドを操作し、ロジックアナライザで波形を観測することができました。
Pico の PWM 機能は、モーターの速度制御や LED の明るさ調整など、様々な用途で活用できます。今回の実験を通じて、PWM の基本的な概念や設定方法を理解し、実際のアプリケーションに応用できるようになったと思います。
-
正確には 255 + 1/2 + 1/4 + 1/8 + 1/16 = 255.9375 ↩︎
Discussion