Raspberry Pi で PWM制御 【Python】

Raspberry Pi

Raspberry Pi で PWM 制御を行う Python のスクリプトについてまとめておくことにします。
以下の環境で動作確認をしています。
環境:
・ Raspberry Pi 5 (bookworm 以降)
・ Windows パソコン(リモートデスクトップの設定済み)
・ LAN ネットワーク

背景 ~ GPIO の仕様が変更になった!

Raspberry Pi 5 (bookworm) 以降では、パルス幅変調(PWM、pulse width modulation)に関する仕様が変更になっています。
そこで、パルス幅変調を行う Python のスクリプトをまとめ、公開しておくことにします。

PWM 制御が扱えるようになると、たとえば、以下が可能になります。
・ LED の明るさの微調整
・ DC モーターの回転速度、力量・トルクの微調整
・ ヒーター・熱源温度の微調整
・ サーボモーターの制御
・ ビーパーを鳴らす
・ フィードバック制御を行う
・ Raspberry Pi のプログラム上から上記の制御を行う
・ その他

※ スクリプトに示したように PWM制御は、指定したピンの出力(Low/High)を 10ms など周期的に動かすこととし、High (3.3V) とする時間の比率 duty1 を 0% から 100% の範囲で時間的に変化させることで、モーターなどをスムーズに動かすことを可能にする、などといったものです。最近増えてきたブラシレスモーター、ドローン、AI ロボットなどの制御に広く使われています。

手順

① PWM を行うためのフォルダ(例:”gpio1″)とファイル(”GPIO_PWM1.py”)を作成し、以下のスクリプトをコピー&ペーストして保存します。
例: home/pi/gpio1/GPIO_PWM1.py

② Raspberry Pi の GPIO に 動作確認のための回路を接続します。
上記の写真では、以下の接続としています。
・ GPIO03 (GND)、GPIO14 (PWM信号、3.3V)に、それぞれ、黒色、赤色のジャンパケーブルを接続し、ブレッドボードに配線を引き出します。
・ 赤色のジャンパケーブルから電流が、LED、330Ωの抵抗(橙橙茶金)を経由し、黒色のジャンパケーブルに流れるよう接続します。
※ Raspberry Pi のターミナルで pinout コマンドを実行することで、ピン番号・ピン配列を確認できます。
※ LED は足の長い側が +側(赤色ケーブル側)となるよう接続します。

③ ①のスクリプトを実行します。
例: python home/pi/gpio1/GPIO_PWM1.py
※ スクリプトを実行すると、スクリプトで指定した時間(60秒)の間、キーボードの入力を受けつける状態となります。
④ スクリプト実行中に、”u” キーを押すと HIGH となっている期間のパルス幅が増加します。”d” キーを押すとパルス幅が狭くなります。デューティ duty1 の値は、ターミナルに表示されるようにしています。”c” キーを押すとプログラムを終了します。

うまく動いたら

うまく動いたら、スクリプトをアレンジしてみてください。
PWM 出力のピン番号、周波数などを自由に変更できます。
また、GPIO 端子に接続した回路側も変更し、応用してみてください。

詳細は省略しますが、同スクリプトを用いて以下が動いています。
・ 外部バッテリーとトランジスタ使ってDCモーターを回し、Raspberry Pi 側から PWM で回転速度を微調整する
・ ヒーターの温度の微調整を行う

※ なお、冒頭の写真は、LED の明るさを PWM で少しずつ変更したものです。わかりにくいですけれども。。

スクリプトの説明

・ スクリプトの冒頭で import PWMOutputDevice、atexit としているところでパルス幅制御に必要なモジュールをインポートしています。
・ import sys、tty、…、select としているところでは、キーボードのキー入力を受けつけるためのモジュールをインポートしています。キー入力により、パルス幅を最小値 0% から最大値 100% まで変えられるようにします。
・ keypress1() は、キー入力を受けつけて指定した処理をするための関数です。プログラムを停止するコマンド(イベント)が発行されていなければ、キー入力を調べて、”u” (up) であれば、パルス幅を指定した値だけ増加させます。”d” (down) が入力されていれば、パルス幅を指定した値だけ減らします。
また、”c” が入力されていれば、プログラムを停止するコマンドを発行します。
もし、コマンド処理がうまくいかないようなことがあったときは、内部の状態を初期状態に戻しておきます。
・ スクリプトを実行すると、関数を定義したのち、output1 = … とした行以降が実行されます。
・ output1 = PWMOutputDevice(14, …) としたところで、ピン番号と PWM 制御の周波数 frequency, Hz を設定しています。
スクリプトでは、周波数を 100Hz としています。周期でいえば T1 = 1/100 = 10ms としています。
他の GPIO の出力ピンを使いたい場合や、周期・周波数を変更したい場合は、この部分を修正してください。
・ stop1 = … とした行以降で、キー入力を受けつけるためのイベント処理をしています。
・ ch1 は、キー入力で入力された文字です。
・ duty1 = 0.05 は、初期状態でのパルス幅です。この例では、起動時には 5% だけ GPIO14 のピンを HIGH (3.3V) とし、残りの 95% を LOW (0V) とする設定としています。
・ del1 = 0.02 は、”d” または “u” キーを押したときのパルス幅の変化量です。
・ rep1 = 60 は、パルス幅制御の実行回数(≒実行時間)です。長時間、実行したい等の場合はこの値を調整してください。
・ 上記のパラメータの設定後、for ループでパルス幅制御のコマンドを繰り返し実行しています。この間、キーボードの入力を監視し、パルス幅を変更できるようにしています。
・ 末尾でパルス幅を0%としています。for ループの途中で抜けた場合であっても、終了時の外部機器の状態を初期状態に戻すようにします。モーターなどの機器が接続されているとき、制御対象を停止し特定の状態とするようにします。

まとめ

Raspberry Pi (bookworm 以降)でパルス幅変調を行うためのスクリプトについてまとめました。

パルス幅制御が扱えるようになると、Raspberry Pi 側からDCモーターをスムーズに回転させたり、圧電スピーカー・圧電素子やサーボモーターなどの制御も可能になります。
関心のある方は、関連リンクなども参照してみてください。

関連リンク
・ DCモーターを動かす 【Raspberry Pi】 
・ サーボモーターを動かす 【Raspberry Pi】
・ LED を ON/OFF する 【Raspberry Pi & Python】
・ 圧電スピーカーを鳴らす 【Raspberry Pi】
※ こちら↑に、PWM駆動としたときの消費電力を求める計算式、計算例をまとめています。確認したい方は参照してみてください。

スクリプト

PWM 制御を行うスクリプト GPIO_PWM1.py


import time as tm1 
from gpiozero import PWMOutputDevice 
import atexit as at1 
import sys 
import tty 
import threading as th1 
import termios as te1 
import select as sl1 

def keypress1(stop1): 
    global duty1 
    global del1 
    global ch1 
    fi1 = sys.stdin.fileno() 
    old1 = te1.tcgetattr(fi1) 
    try: 
        tty.setcbreak(fi1) 
        while not stop1.is_set(): 
            if sl1.select([sys.stdin], [], [], 0.1)[0]: 
                ch1 = sys.stdin.read(1) 
#               print(ch1) 
                if ch1 == 'c': 
                    stop1.set() 
                elif ch1 == 'u': 
                    duty1 = duty1 + del1 
                    if duty1 > 1.0: 
                        duty1 = 1.0 
                elif ch1 == 'd': 
                    duty1 = duty1 - del1 
                    if duty1 < 0.0: 
                        duty1 = 0.0 
    finally: 
        restore1(fi1, old1) 

def restore1(fi1, settings1): 
    try: 
        te1.tcsetattr(fi1, te1.TCSADRAIN, settings1) 
    except Exception as e1: 
        print(e1) 

output1 = PWMOutputDevice(14, initial_value=0, frequency=100)   # GPIO14, 10ms 
at1.register(lambda: output1.value == 0.0) 

stop1 = th1.Event()                                             # for key events 
thread1 = th1.Thread(target=keypress1, args=(stop1,), daemon=True) 
thread1.start() 
fi1 = sys.stdin.fileno() 
ini1 = te1.tcgetattr(fi1) 

ch1 = ""                                                        # key 
duty1 = 0.05                                                    # duty: 5% 
del1 = 0.02                                                     # 2% 

output1.value = duty1 

rep1 = 60                                                       # 60 times 

try: 
    for i1 in range(rep1): 
        if stop1.is_set(): 
            break 
        output1.value = duty1 
        str1 = str(i1) + " " + str(duty1) + " " + ch1 
        print(str1) 
        tm1.sleep(1)  
#       output1.value = 0       
#       tm1.sleep(1) 
finally: 
    restore1(fi1, ini1) 
    
output1.value = 0.0 
tm1.sleep(1) 

タイトルとURLをコピーしました