RPi.GPIOを用いたPWM出力
ラズパイでPWM出力といえばRPi.GPIOを使ってこんなコードを書くかと思います。
import time
import sys
import RPi.GPIO as GPIO
def main():
BZPIN = 19
GPIO.setmode(GPIO.BCM)
GPIO.setup(BZPIN , GPIO.OUT)
BZ = GPIO.PWM(BZPIN, 440)
BZ.start(50)
time.sleep(10)
BZ.cleanup(BZPIN)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt :
print("!!Exit!!")
sys.exit()
このプログラムでは、19番ピンに接続したブザーから440Hz(ラ)の音が流れることが期待できるわけですが、
なんか変ですね。その原因はこちらの波形を見ればわかります。100Hzを出力した場合の波形ですが、波形が乱れて残像が出ています(ジッタが出ている)。
こちらのフォーラムの投稿によると、RPi.GPIOのPWMはソフトウェアPWMを使用しており、OSやPythonの処理時間の乱れの影響を受けるようです。逆にきれいな音を鳴らすためにはハードウェアPWMを用いてソフトの処理遅れの影響を排除する必要があります。
ハードウェアPWM
ラズパイのチップセット(bcm2711)には、データシート8章に記載がある通り、クロックからハード的にPWMを生成する回路が存在します。
この回路のレジスタに設定を書き込むと、自動的にPWMの波形が生成されるので、ジッタが発生しません。
ただし、ハードウェアPWMを利用できるのは、GPIO12/18もしくはGPIO13/19だけで、しかもGPIO12⇔GPIO18や、GPIO13⇔GPIO19で違うPWM波形を出すことはできません。(12⇔13、18⇔19は独立したPWM波形を生成可能)
PythonでこのハードウェアPWMを生成する方法を2つ紹介します。
方法1:rpi-hardware-pwm
このライブラリは、ハードウェアPWMを生成する専用のライブラリです。
インストール
rpi-hardware-pwmはハードウェアPWMを出すためのライブラリです。まずはインストールします。
sudo pip install rpi-hardware-pwm
externally-managed-environment エラーが出る場合があります。
sudo pip install --break-system-packages rpi-hardware-pwm
なんて治安の悪いことをしましたが、気にする人は仮想環境を使うなりしてもらえればよいかと思います。
設定
このライブラリでは事前にPWMを出す端子を決めて/boot/config.txt
に記載しておく必要があります。Channel0に設定したPWMをGPIO18から、Channel1に設定したPWMをGPIO19から出力する場合は以下の記載をすればOKです。
[all]
## 中略 ##
dtoverlay=pwm-2chan
GPIO12、GPIO13から出す場合はこうなります。
[all]
## 中略 ##d
toverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
実行結果
import time
import sys
from rpi_hardware_pwm import HardwarePWM
def main():
BZPIN = 19
BZ = HardwarePWM(pwm_channel=0, hz=100)
BZ.start(50)
time.sleep(100)
BZ.stop()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt :
print("!!Exit!!")
sys.exit()
波形を見るとジッタは一切出ていません。
1kHzを設定した波形も全く問題ありません。(RPi.GPIOで1kHzはぶれすぎて計測が困難でした)
方法2:pigpio
こちらは、RPi.GPIOの置き換えを念頭においたライブラリです。
デーモンがバックグラウンドで動作すること、ハードウェアPWMをサポートしていないポートでもある程度(例:5μs)の精度が実現出来るようです。
インストール方法
pip install pigpio
こちらも、適宜仮想環境を使うほうが良いかと思われます。
また、事前にデーモンの実行が必要です。
sudo pigpiod
本格的に使うなら、自動実行させても良いでしょう。
sudo systemctl enable pigpiod
sudo systemctl start pigpiod
実行結果
このコードで動作を確認しました。
import time
import sys
import pigpio
def main():
BZPIN = 19
BZPIN2 = 20
DUTY = 0.5
gpio = pigpio.pi()
# BZPINから1kHz, Duty=50%の波形を出力
gpio.hardware_PWM(BZPIN, 1000, int(DUTY *10000))
# BZPIN2から1kHz, Duty=50%の波形を出力
gpio.set_PWM_frequency(BZ2PIN, 1000)
gpio.set_PWM_dutycycle(BZ2PIN, int(DUTY * 255))
time.sleep(100)
gpio.stop()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt :
print("!!Exit!!")
sys.exit()
bcm2711には搭載されているハードウェアPWMが使えるBZPIN(GPIO19)は、波形を見るとジッタは一切出ていません。このことから、ハードウェアPWMを使用できていることがわかります。
本来PWMを出力できないBZPIN2(GPIO20)ですが、こちらも波形に問題ありません。
(なお、daemonの受け付けられる最小の周期が決まっているので、設定した周波数を正確に出せるわけでは無いようです。例:440Hz=400Hz)
また、同時にRPi.GPIOを用いて他の端子のLEDを点灯・消灯させましたが、通常通り動作しました。
感想
RPi.GPIOは確かに手軽ですが、リアルタイム性を求める動作には向かないことがわかりました。LEDやDCモータをPWM制御する分には問題なくても、音を鳴らしたりステッピングモータを回したりすると影響が無視できないです。
また、ハードウェアPWMを用いるため、rpi_hardware_pwmを使用しましたが、こちらは設定が少々面倒で、特にPWMを出力する端子を変更するためには再起動が必要でした。ただ、プログラミングはし易いように感じました(周波数やDutyはそのまま入れればOK)。
一方、pigpioは、ハードウェアPWMだけでなくリアルタイム制御に使える機能(今回は試していないですが入力割り込みなど)が多く、設定も楽(デーモンを起動するだけ)でしたが、引数に若干くせがあるように感じました。
また、どちらもRPi.GPIOを同時に用いても問題ない(ピンの重複はNG)ので、リアルタイム制御がいらないデバイスは、過去に書いたプログラムをがんばって書き直す必要がないのが嬉しいです。