Raspberry Piのクロックマネージャ – ハードウェアPWMドライバをMicroPythonで書く(前編)


前回、Raspberry PiのPWMをベアメタルプログラミングで操作しました。
いつもだとこれをベアメタル版MicroPythonにPWMクラスとして追加するのですが、今回は1クッション置いて、まずはMicroPythonでPWMクラスを書いてみます。

前回示したように、MicroPythonでRasberry Piのペリフェラル制御レジスタを操作することができます。これはmem32[addr]というメソッドを使っています。mem32[addr]はアドレスaddrに32ビットのデータを読み書きします。

SPIやI2Cなどはデバイスとのデータ通信を伴うので、C言語でそれなりに高速な実装をしておくほうが望ましいですが、PWMはレジスタに値を設定するだけで利用できますので、MicroPythonでも実装できます。(PWMの機能にはデータをFIFOに書いて連続的にPWM変換するものもありますが、こちらはC言語実装が望ましいので今回はパスします。)

クロックマネージャの設定

PWMの搬送波として使用するクロックは、クロック管理用のレジスタで指定します。
このあたりはマニュアルでも網羅的には解説されていないのですが、以下のページが詳しいです。

Avian’s Blog: Notes on the general-purpose clock on BCM2835

クロックマネージャの主な指定項目は、

信号源:オシレータ(19.2MHz)/PLL A(0Hz)/PLL C(1GHz)/PLL D(500MHz)/HDMI(216MHz)
プリスケーラ:整数部12bit、小数部12bitの24bit固定小数点
MASHフィルタ:なし/1段/2段/3段

の3つです。以下説明していきます。

クロックの信号源

クロックの信号源は上記5つおよびGND(クロックが供給されない)から選択します。
それぞれの設定値と周波数は以下のページに載っています。

GPCLK at Raspberry Pi GPIO Pinout

オシレータ(19.2MHz)はRaspberry Piのボード上の水晶発振子の信号です。この信号がPLLで逓倍されて、より高い周波数が生成され、CPUやHDMIのクロックに利用されています。

PLLの設定を変更すれば他の周波数も生成できそうですが、システムでも利用されている信号なので、変更すると影響が大きいと思われます。

プリスケーラの設定

クロック信号源からのクロックはプリスケーラに送られます。プリスケーラは入力クロックを分周し、より低い周波数のクロックを生成します。信号源以上の周波数は生成することができません。

試した限りでは、生成できる最大の周波数は信号源の周波数の1/2になるようです。
プリスケーラを1に設定すると、信号を生成してくれませんでした。

また、プリスケーラは12ビットですので、(信号源の周波数/4096)が生成できるPWMのクロック周波数の下限になります。
(これは「PWMクロック」の周波数であり、後述するようにPWM信号の周波数はこのさらに整数分の1になります。)

このことから、プリスケール後のクロックの周波数は

オシレータ:4.6KHz~9.6MHz
PLL C:244.1KHz~500MHz
PLL D:122KHz~250MHz
HDMI:52.7KHz~108MHz

の範囲で指定できることになります。

プリスケーラは固定小数点値が指定できます。
たとえば19.2MHzを1/9.6して2MHzの信号を作るには、プリスケーラを

整数部:9
小数部:2458(4096 * 0.6 = 2457.6なので)

と設定すればいいことになります。

しかし、実際にはそう都合よくスケーリングできるわけではなく、1/9の分周とと1/10の分周の間を行ったり来たりすることによって、「平均的に」1/9.6を実現します。

さらに、そう都合よく平均値が実現できるわけではなく、どうしても誤差が生じます。
その誤差を補正するのが3番目のパラメータ「MASHフィルタ」です。

MASHフィルタの設定

MASHフィルタについては、正直、理論的なところは私も理解していないのですが、多段のフィルタで「前の段で生じた誤差(ノイズ)成分を次の段で補正する」という機能のようです。

つまり、上記の例で言えば1/9の分周とと1/10の分周でカバーし切れなかった分の誤差を、「1/8の分周と1/9の分周」および「1/10の分周と1/11の分周」の間をさらに行ったり来たりすることによって補正するわけです。

この結果、多段にすればするほど個々の波の周波数の変動は大きくなるものの、「平均すると」所望の周波数に近い結果が得られるようになります。
マニュアルにも解説がありますが、

MASH=0:周波数は小数部を無視した結果になる
MASH=1:最大周波数は19.2MHz/整数部、最小周波数は19.2MHz/(整数部 + 1)
MASH=2:最大周波数は19.2MHz/(整数部 – 1)、最小周波数は19.2MHz/(整数部 + 2)
MASH=3:最大周波数は19.2MHz/(整数部 – 3)、最小周波数は19.2MHz/(整数部 + 4)

となります。上記の2MHzの信号を生成する場合で計算してみると、

MASH=0:2.133 MHz
MASH=1:1.92 MHz ~ 2.133 MHz
MASH=2:1.745 MHz ~ 2.4 MHz
MASH=3:1.477 MHz ~ 3.2 MHz

ということになります。
後述しますが、実際にPWMで波形を生成する場合、1つの波形で数百~数千クロックを使用するのが一般的です。従って、上記のように大きく変動があっても、PWMの1つの波の中で平均化されるので、最終的な波形への影響は小さいと思われます。

では、MASHの値をどのように選べばいいかというと、0は誤差が大きすぎますが、1~3では

・PWMの1波形あたりのクロックが数クロック程度なら、MASH=1
・それより大きい場合は、分散と平均のどちらを優先するかでcase by case

ということになるかと思います。

段数が大きくなるほど(平均周波数としての)周波数は指定された周波数に近くなります。
その一方、プリスケーラの値が小さい場合、MASHを多段にするとクロックの変動幅が大変大きくなります。

極端な例として、プリスケーラの設定値が3のときに、MASH=3を指定したら分周のカウントは0~7の範囲でぶれることになります。
0や1は実際には指定できませんし、7になったら出力の周波数は想定周波数の半分近くまで落ち込むことになります。

以下に、MASHの値を変化させるとクロック変動がどう変わるか、オシロの画面をキャプチャした動画を示します。
信号源はオシレータ(19.2MHz)、プリスケーラは1/9.6(前述の設定)、PWMをrange = 2、data = 1(次回後述しますが、1クロックが出力「1」、次の1クロックが出力「0」となる設定です)で出力させています。

MASH=0とMASH=1では周波数が変化する(MASH=0は誤差が大きい)こと、MASHの値が大きくなると周波数の微視的な変動が大きくなることが見て取れると思います。

MicroPythonでの実装

PWM用のクロックに関して、上記の設定をMicroPythonで書くとこんな感じになります。

0x201010a0、0x201010a4がレジスタのアドレスです。これらのレジスタに値を書きこむときは、上位8ビットが必ず0x5aでなければなりません。
これは、誤って間違った値を書き込まれないようにするための「パスワード」らしいです。

使い方は、例えば

PWM_clock_config(CLK_OSC, 192)

とすると、19.2MHzのクロックを1/192して100KHzのクロックを生成します。

次回はこのクロックを使ってPWMを行う部分を解説します。

コメント