Python × Pico 電子工作 | #04 PWMで明るさを変えよう

前回までで LED の ON/OFF(デジタル出力)とボタン入力を学びました。ただし LED は 「点灯」「消灯」 の二択しか作れていません。今回はその中間、「うっすら光らせる」「だんだん明るくする」を実現する PWM(Pulse Width Modulation) を扱います。

  • デューティ比で明るさが決まる仕組み
  • pico_pwm_write(固定値)と pico_pwm_write_val(変数を差し込める)の使い分け
  • for ループでデューティ比を 0→100% に変えるフェードイン と呼吸 LED

PycoBlocks(MicroPythonモード) を開き、左カテゴリ「Pico 電子工作 / 出力」のPWM出力ブロックが今回の主役です。配線は #02 で組んだ「LED + 330Ω」のままで OK なので、新しい部品を追加せずにすぐ試せます。

PWMで明るさを変えよう

PWMとは:明るさは「ONの時間割合」で決まる

PWM(Pulse Width Modulation, パルス幅変調)は、GPIO ピンを 非常に速く ON/OFF させて、その ON 時間の割合 を変えることで「中間の明るさ」を作る技術です。Pico 標準では 1 秒間に約 1,900 回(≒ 1.9kHz)の速さで ON/OFF します。目では追えない速さなので、人間には 滑らかな明るさの変化 として見えます。

デューティ比(duty cycle)は、1 周期のうち HIGH の時間が占める割合のことです。0% なら完全消灯、100% なら最大の明るさ、50% ならその中間、というように直感的な指定ができます。下の波形図で、デューティ比と明るさの関係を見てみましょう。

PWM デューティ比 25% / 50% / 75% の波形と LED の明るさの対応図
橙で塗った部分が HIGH(ON)の時間。1 周期に占める ON 時間の割合(デューティ比)が大きいほど、平均的に流れる電流が増えて LED が明るく見える。0% は完全消灯、100% は点灯(led.value(1))と同じ最大の明るさ。
デューティ比1周期の波形LED の見え方
0%ずっと LOW完全消灯
25%少しだけ HIGHかすかに光る
50%半分 HIGH中くらいの明るさ
75%大部分 HIGH明るい
100%ずっと HIGH最大の明るさ(点灯と同じ)

ステップ1:PWMでLEDを薄く光らせよう

配線は #02 と同じです。GP15 → 330Ω 抵抗 → LED → GND の経路で OK。「点灯」ブロックの代わりに 「PWM出力 ピン GP15 デューティ 50%」 ブロックを置きます。

GP15 → 330Ω → LED → GND の回路図
回路は #02 とまったく同じ。ピンが出す信号が「ずっと HIGH」から「高速で ON/OFF する PWM 信号」に変わるだけで、部品も配線も変えなくてよい。
GP15 に LED と 330Ω 抵抗を配線した実体配線図
GP15 → 330Ω → LED → GND。#02 と同じ配線をそのまま使えます。(実体配線図は Fritzing の部品画像をもとに作図・CC BY-SA 3.0)
GP15 にデューティ比 50% で PWM 出力するブロック
ブロック構成:ずっと繰り返す → PWM出力 GP15 デューティ 50% → 0.5 秒待つ。
from machine import Pin, PWM
import utime
pwm = PWM(Pin(15))
while True:
    pwm.duty_u16(32768)   # 50% = 65535 × 50 / 100 ≒ 32768
    utime.sleep(0.5)

duty_u16() は MicroPython の PWM API で、0〜65535(16bit 符号なし整数) でデューティを指定します。PycoBlocks のブロックは 0〜100% を内部で 0〜65535 にスケールしてくれるので、人間にわかりやすい % 表記のまま使えます。

💡 試してみよう

ブロックのデューティ値を 0 → 25 → 50 → 75 → 100 と変えて、LED の明るさが段階的に変わることを目で確認しましょう。0% は完全消灯、100% は led.value(1) と同じ最大の明るさです。

ステップ2:フェードイン点灯

デューティ値を 0% から 100% まで少しずつ増やす と、LED が「だんだん明るくなる」フェードイン演出が作れます。ここで活躍するのが 「変数を差し込める」 PWM 出力ブロック(pico_pwm_write_val)です。デューティ部分が はめ込み式 になっていて、for ループの繰り返し変数 d をそのまま入れられます。

for ループで PWM デューティを 0→100 まで動かすブロック
ブロック構成:変数 d を 0 から 100 まで 5 ずつ繰り返す → PWM出力 GP15 デューティ [d] % → 0.05 秒待つ。
from machine import Pin, PWM
import utime
pwm = PWM(Pin(15))
while True:
    for d in range(0, 100, 5):
        pwm.duty_u16(int(max(0, min(100, d)) * 65535 / 100))
        utime.sleep(0.05)

range(0, 100, 5) なので d は0, 5, 10, 15, ..., 95 の 20 段階を取ります。0.05 秒 × 20 = 約 1 秒 で 0% から (ほぼ) 100% まで上がりきり、その後 while True で先頭に戻って一気に 0% に落ち、また 0% から明るくなる…を繰り返します。

📝 明るさの上がり方が気になる?──Weber–Fechner の法則

人間の目は 暗いところでの変化に敏感、明るいところでの変化に鈍感 という性質(Weber–Fechner の法則)があるため、デューティを一定間隔で上げると「最初は急に明るくなり、後半はあまり変化しない」ように見えます。より自然に見せたい場合は d を 2 乗するなど指数カーブで動かすと改善できます。

ステップ3(コラム):なぜちらつかない?──PWM 周波数の話

PWM は「ON / OFF を繰り返している」のに、なぜ LED が ちらつかず連続点灯に見える のでしょうか?答えは 切り替えの速さ(周波数) にあります。

MicroPython の PWM(Pin(...)) 既定周波数は 約 1,907 Hz(≒ 1.9 kHz、1 秒間に約 1,900 回切り替え)です。人間の目が「ちらつき」として認識できる限界は概ね 60Hz 前後 と言われており、それを 30 倍以上上回る速さなので連続点灯に見えるわけです。

周波数を下げてみると?pwm.freq(20) のように 20Hz まで落とすと、目に見えて点滅するようになります(試すと面白いです)。周波数を上げると?数 kHz〜数十 kHz の範囲ではモーターやサーボ、ブザーの制御によく使われます。周波数とデューティ比は 独立に決められる 2 つの軸 だと覚えておきましょう。

演習課題

課題3-4-1:デューティ比からHIGH時間を計算してみよう

PWM 周波数が 1,000 Hz(1 周期 = 1 ms)の信号を出すとき、デューティ比 30% / 50% / 80% のそれぞれで、1 周期のうち HIGH に保たれる時間(ミリ秒)はいくつになるでしょうか?電卓を使わずに計算してみよう。

▶ 模範解答と解説を見る
# 答え:
# 1 周期 = 1 / 1000 秒 = 1 ms
#
# デューティ 30%  →  HIGH 時間 = 1 ms × 0.30 = 0.30 ms (300 μs)
# デューティ 50%  →  HIGH 時間 = 1 ms × 0.50 = 0.50 ms (500 μs)
# デューティ 80%  →  HIGH 時間 = 1 ms × 0.80 = 0.80 ms (800 μs)
#
# 残りの時間が LOW。例えば 30% なら 0.70 ms が LOW。
# 平均電圧(≒ LED の明るさ)は 3.3V × デューティ比 で近似できる。
#   30% → 約 0.99V 相当の明るさ
#   50% → 約 1.65V 相当の明るさ
#   80% → 約 2.64V 相当の明るさ

解説: 計算のコツ:「1 周期の時間 × デューティ比 = HIGH 時間」というシンプルな掛け算です。デューティ比を小数で扱う(30% = 0.30)と式が読みやすくなります。周波数を変えても 「比率」は同じ なので、明るさは保たれます。(例:500Hz でも 1000Hz でも、デューティ 50% なら同じ明るさ)発展:LED に流れる電流も同じ式で平均値を求められます。I = (3.3V − 2.0V) / 330Ω ≒ 約 4 mA。デューティ 50% なら平均電流は約 2 mA。

課題3-4-2:呼吸LEDを作ろう

ステップ2 の「だんだん明るくなる」を改造して、0% → 100% → 0% を行ったり来たり させることで、「ゆっくり呼吸している」ような 演出を作ろう。配線はステップ1 と同じ(GP15 + 330Ω + LED)です。

▶ 模範解答と解説を見る

ブロックの組み合わせ例:

呼吸LED:明るく→暗く→明るくを繰り返すブロック
ブロック構成:ずっと → for 0→100 step 5(明るく)→ for 100→0 step -5(暗く)。
from machine import Pin, PWM
import utime
pwm = PWM(Pin(15))
while True:
    for d in range(0, 100, 5):           # 明るくしていく
        pwm.duty_u16(int(max(0, min(100, d)) * 65535 / 100))
        utime.sleep(0.03)
    for d in range(100, 0, -5):          # 暗くしていく
        pwm.duty_u16(int(max(0, min(100, d)) * 65535 / 100))
        utime.sleep(0.03)

解説: ポイントは 2 つの for ループを並べる こと。1 本目は step 5 で 0→100、2 本目は step -5 で 100→0 まで戻します。utime.sleep(0.03) なので 20 ステップ × 2 = 40 ステップで約 1.2 秒、つまり 1.2 秒で 1 呼吸 のリズムになります。発展:sleep の値を 0.05、0.1 と変えるとゆっくり呼吸、0.01 にすると速い呼吸になります。深呼吸(5 秒で 1 呼吸)にしたい場合は 0.125 秒 程度が目安です。

課題3-4-3:2つのLEDを交互にフェードさせよう

GP14 と GP15 にそれぞれ LED(330Ω 抵抗付き)を配線し、片方が明るくなるともう片方が暗くなる「シーソー」のような演出を作ろう。

課題3-4-3 配線図:GP14 / GP15 にそれぞれ LED
GP14 と GP15 にそれぞれ 330Ω 抵抗 + LED を配線。#02 課題と同じ配線。
▶ 模範解答と解説を見る

ブロックの組み合わせ例:

課題3-4-3 解答:2つのLEDを交互にフェード
ブロック構成:for 0→100(GP15=d / GP14=100-d)→ for 100→0(GP15=d / GP14=100-d)。
from machine import Pin, PWM
import utime
pwm14 = PWM(Pin(14))
pwm15 = PWM(Pin(15))
while True:
    for d in range(0, 100, 5):           # GP15 が明るく、GP14 が暗く
        pwm15.duty_u16(int(max(0, min(100, d)) * 65535 / 100))
        pwm14.duty_u16(int(max(0, min(100, 100 - d)) * 65535 / 100))
        utime.sleep(0.03)
    for d in range(100, 0, -5):          # GP15 が暗く、GP14 が明るく
        pwm15.duty_u16(int(max(0, min(100, d)) * 65535 / 100))
        pwm14.duty_u16(int(max(0, min(100, 100 - d)) * 65535 / 100))
        utime.sleep(0.03)

解説: 鍵は 100 - d という計算ブロック です。d が 0 なら GP14 のデューティは 100%、d が 100 なら GP14 のデューティは 0%、というように 足して必ず 100% になる 関係になります。片方が明るくなれば、もう片方は同じ割合だけ暗くなる「シーソー」が成立します。発展:3 つの LED で RGB の色合成にも応用できます。GP13 / GP14 / GP15 にそれぞれ赤・緑・青の LED を繋ぎ、デューティ比を独立に動かすと無限の色を作れます。(実際の RGB LED モジュールは内部で 3 つの LED をまとめたものです)

まとめ

  • PWM は GPIO を高速 ON/OFF させ、HIGH 時間の割合(デューティ比) で平均出力を変える技術。
  • MicroPython では PWM(Pin(N)).duty_u16(値) で指定。値は 0〜65535(16bit)だが、PycoBlocks では 0〜100% で書ける。
  • pico_pwm_write は固定値、pico_pwm_write_val は 変数や式を差し込める。フェード演出はこちらを使う。
  • 既定周波数は約 1.9kHz なので人間の目にはちらつかない。pwm.freq(...) で変更できる。
  • for ループでデューティを動かせば フェードイン・呼吸 LED が作れる。

次の記事ではアナログ 入力(ADC)を扱います。可変抵抗(ボリューム)の回転量を 0〜65535 の数値で読み取り、それをそのまま PWM のデューティ比に渡せば「つまみで明るさを変えられる LED」が完成します。

トップへ戻る