Raspberry PiのPWM – ハードウェアPWMドライバをMicroPythonで書く(後編)


前編はこちら

Raspberry Piは、ハードウェアPWMを2系統持っています。クロック源は共有されていますので、同期して動かすことができます。オーディオ出力の左チャネルと右チャネルの出力はこの2系統のPWMが使われています。

前編では、PWMに供給するクロックについて説明しました。後編はPWMの設定を解説します。

PWMの設定

Raspberry PiのPWMはいろいろと細かい設定があるのですが、サーボの制御等で使用するのに必要な項目は以下の3つです。

range:PWMの1周期を構成するクロック個数
data:PWMで「1」を出力するクロック個数
モード:Mark/SpaceまたはPWM

上の図はクロックとrangeとdataの関係を示したものです。
クロックパルスが1つ来るたびにカウントが1ずつ上がっていきます。
カウントがrangeに達するとリセットされて0に戻ります。
従って、rangeはPWM出力波形の1周期の長さを表します。
また同時に、PWMの解像度(段階の数)をも表しています。

PWM出力は、カウントがdata以下ならば1、dataより大きければ0です。
ただし、これはMark/Spaceモードの場合で、PWMモードの場合は1と0をできるだけ高速に切り替えるように動作します。
Mark/SpaceモードでもPWMモードでも、1周期内で出力が1になる時間の総量は同じなのですが、固まって出現するか分散して出現するかが異なります。
デューティ比を連続的に変化させたときのPWM出力信号の違いをオシロで観測した動画を以下に載せておきます。

GPIOの設定

PWMをGPIOピンに出力するためには、設定が別途必要です。
オーディオジャックが付いているRaspberry Piでは、PWM出力はオーディオ出力となっており、GPIO40,45が使われています。
Raspberry Pi Zeroではこのピンは拡張ポートに出力されていませんが、代わりにGPIO18,19またはGPIO12,13が使えます。

これらのピンにRCからなるローパスフィルタを接続すれば、オーディオ出力として使うこともできます。以下のページに解説があります。

Pi Zero PWM Audio | Adding Basic Audio Ouput to Raspberry Pi Zero | Adafruit Learning System

PWMをGPIOピンに出力するには、ピンのモードを切り替える(ピンにAlternate Functionを設定する)必要があります。
このあたりは以前以下の記事で書きました。

Raspberry Pi Zero WでベアメタルLチカ – 楽しくやろう。

マニュアルによると、PWM出力できるGPIOピンと設定するAlternate Functionは以下のようになっています。
PWMは2系統ありますが、ピンによって使用できるPWMは決まっています。

MicroPythonで、PWMに設定したいピン番号を与えると設定を行ってPWMのIDを返す関数は以下のようになります。
設定できた場合はPWMのIDとして0か1、設定できなかった場合は-1を返します。

PWMクラスの実装

マニュアルによると、PWM関連のレジスタは以下のようになっています。
PWMは2系統ありますが、range(RNG1, RNG2)とdata(DAT1, DAT2)以外のレジスタは共用になっています。
FIF1も、1と付いてはいますが共用です。このレジスタに書き込んだデータはFIFOバッファに追加されますが、バッファも共用です。2チャンネルで使用する場合は、各チャンネルは偶数番目と奇数番目のデータを受け取ります。

以前書いたように、ベースアドレスはマニュアルには掲載されていませんが、エラッタによると0x7E20C000です。
ベアメタルでは0x2020C000となります。
また、0xC、0x1Cからの4バイトは使われていないようです。

PWMチャンネルごとの初期化設定には、既に紹介したMark/Spaceの他、

・アクティブ/インアクティブ
・1と0の出力を反転するか(polarity)
・無信号時は0を出力するか1を出力するか(silence bit)
・FIFOの利用有無
・FIFOを利用する場合、データが空なら最後のデータを再度使用するか否か
・PWMとして利用するかシリアライザとして使用するか

といったフラグがあります。
シリアライザとは、データをクロックに合わせて1bitずつ出力する機能で、出力ビットパターンを完全に制御することができます。
silence bitは、PWM使用時は0だと通常、1だとPWMの出力が常に1になるようです。(実際の出力はpolarityの値に依存します。)

PWM出力の指定は前述の通りrangeとdataで行うのが基本です。
また、周波数については前編で紹介したように、クロック源とプリスケーラの設定で制御します。

しかし、MicroPythonの他のプラットフォームでは、周波数やデューティ比を直接指定するAPIがあります。

Quick reference for the pyboard — MicroPython 1.9.4 documentation

7. Pulse Width Modulation — MicroPython 1.9.4 documentation

Quick reference for the WiPy — MicroPython 1.9.4 documentation

そこで、簡易的なAPIとして

・クロック源=OSC
・range=960

を固定し、周波数をプリスケーラの設定で、デューティ比をdataの設定で行うメソッドを用意することにしました。

・freq(f):周波数をf Hzに指定(最大10KHz)
・duty(d):デューティ比を0..960の範囲で指定(480で50%)

rangeが960だと、サーボの制御には若干精度が悪い(range=2000程度が望ましい)ですが、その代わり周波数は最大10KHzまでの矩形波を出力できます。

実装を載せておきます。

使い方はこんな感じです。

動作させると、本記事の冒頭のGIFアニメのような出力が得られます。(このアニメはQGiferで作成しました。)

コメント

  1. himuka より:

    ラズパイ(pi3model B)でサーボモータをハードウエアPWMで
    動作制御にトライしています。サーボモーターはtower pro のSG92Rです。
    下記のプログラムで一動作は回転しますが、連続動作に四苦八苦しています。
    例えば90度→180度→0度の動作、さらに無限動作のプログラミングに
    苦労しています。
    while True:あるいはfor range int
    など挑戦していますがなかなか思う通り動きません。
    何かヒントお願いいたします。

    import wiringpi as GPIO
    servo_pin = 18

    set_degree =180
    print(set_degree)

    GPIO.wiringPiSetupGpio()

    GPIO.pinMode(servo_pin, 2)
    GPIO.pwmSetMode(0)

    GPIO.pwmSetRange(1024)

    GPIO.pwmSetClock(375)

    move_deg = int((9.5*set_degree/180 + 2.5)*(1024/100))
    GPIO.pwmWrite(servo_pin, move_deg)

  2. boochow より:

    こんにちは、ご質問ありがとうございます。

    連続動作ということですが、プログラムはサーボが所定の角度に達するまで待ってはくれませんので、PWMに値を設定した後、

    time.sleep(待ち時間)

    でサーボが回りきるまで待ってはどうでしょうか。待ち時間の単位は秒です。
    また、先頭行に

    import time

    が必要です。