RaspberryPiのPythonによるPWM出力について

# [RPi.GPIO](https://pypi.org/project/RPi.GPIO/)を用いたPWM出力
ラズパイでPWM出力といえばRPi.GPIOを使ってこんなコードを書くかと思います。

```python
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(ラ)の音が流れることが期待できるわけですが、

<iframe width="560" height="315" src="https://www.youtube.com/embed/BCh7YRylR2s?si=wGudMUrltWQqZCzA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

なんか変ですね。その原因はこちらの波形を見ればわかります。100Hzを出力した場合の波形ですが、波形が乱れて残像が出ています(ジッタが出ている)。
![TEK00001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/e5b9ab83-5f3a-0192-115e-804b9ce45268.png)
[こちらのフォーラムの投稿](https://forums.raspberrypi.com/viewtopic.php?t=194839)によると、RPi.GPIOのPWMはソフトウェアPWMを使用しており、OSやPythonの処理時間の乱れの影響を受けるようです。逆にきれいな音を鳴らすためにはハードウェアPWMを用いてソフトの処理遅れの影響を排除する必要があります。

# ハードウェアPWM
ラズパイのチップセット(bcm2711)には、[データシート](https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf)8章に記載がある通り、クロックからハード的にPWMを生成する回路が存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/09694bb6-fd80-35c1-ea13-f2bc10ff34a7.png)

この回路のレジスタに設定を書き込むと、自動的にPWMの波形が生成されるので、ジッタが発生しません。
ただし、ハードウェアPWMを利用できるのは、GPIO12/18もしくはGPIO13/19だけで、しかもGPIO12⇔GPIO18や、GPIO13⇔GPIO19で違うPWM波形を出すことはできません。（12⇔13、18⇔19は独立したPWM波形を生成可能）
PythonでこのハードウェアPWMを生成する方法を2つ紹介します。

## 方法1:[rpi-hardware-pwm](https://pypi.org/project/rpi-hardware-pwm/)
このライブラリは、ハードウェアPWMを生成する専用のライブラリです。
### インストール
rpi-hardware-pwmはハードウェアPWMを出すためのライブラリです。まずはインストールします。
```shell
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です。
```conf
[all]
## 中略 ##
dtoverlay=pwm-2chan
```
GPIO12、GPIO13から出す場合はこうなります。
```conf
[all]
## 中略 ##d
toverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
```

### 実行結果
```python
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()
```
波形を見るとジッタは一切出ていません。

![TEK00002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/aff8b987-47a2-53e6-a59d-f1c2509ce097.png)

1kHzを設定した波形も全く問題ありません。(RPi.GPIOで1kHzはぶれすぎて計測が困難でした）
![TEK00004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/1dbb799f-f4f9-3f1b-81ac-2163890cde55.png)


## 方法2:[pigpio](https://pypi.org/project/pigpio/)
こちらは、RPi.GPIOの置き換えを念頭においたライブラリです。

[デーモンがバックグラウンドで動作すること、ハードウェアPWMをサポートしていないポートでもある程度(例：5μs)の精度が実現出来るようです。](http://abyz.me.uk/rpi/pigpio/python.html#set_PWM_frequency)

### インストール方法

```shell
pip install pigpio
```
こちらも、適宜仮想環境を使うほうが良いかと思われます。
また、事前にデーモンの実行が必要です。
```shell
sudo pigpiod
```
本格的に使うなら、自動実行させても良いでしょう。
```shell
sudo systemctl enable pigpiod
sudo systemctl start pigpiod
```


### 実行結果
このコードで動作を確認しました。

```python
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を使用できていることがわかります。
![TEK00005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/88ea5256-c870-883b-4b50-567a44242aae.png)

本来PWMを出力できないBZPIN2(GPIO20)ですが、こちらも波形に問題ありません。
(なお、daemonの受け付けられる最小の周期が決まっているので、設定した周波数を正確に出せるわけでは無いようです。例：440Hz=400Hz)
![TEK00003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269597/e5d787e9-4dfb-aac2-59f2-94dfd5bee270.png)

また、同時にRPi.GPIOを用いて他の端子のLEDを点灯・消灯させましたが、通常通り動作しました。

# 感想
RPi.GPIOは確かに手軽ですが、リアルタイム性を求める動作には向かないことがわかりました。LEDやDCモータをPWM制御する分には問題なくても、音を鳴らしたりステッピングモータを回したりすると影響が無視できないです。
また、ハードウェアPWMを用いるため、rpi_hardware_pwmを使用しましたが、こちらは設定が少々面倒で、特にPWMを出力する端子を変更するためには再起動が必要でした。ただ、プログラミングはし易いように感じました（周波数やDutyはそのまま入れればOK）。
一方、pigpioは、ハードウェアPWMだけでなくリアルタイム制御に使える機能(今回は試していないですが入力割り込みなど)が多く、設定も楽(デーモンを起動するだけ)でしたが、引数に若干くせがあるように感じました。
また、どちらもRPi.GPIOを同時に用いても問題ない(ピンの重複はNG）ので、リアルタイム制御がいらないデバイスは、過去に書いたプログラムをがんばって書き直す必要がないのが嬉しいです。