単体LSI、ATmega328P-PUを使って、Arduinoっぽいボード(命名:328duino)を手作りしてみた記事(「#9-5 ブレッドボードにArduinoっぽい物」)で、部品数削減のために使った「内蔵RC発振器」がありました。この発振器、ユーザ使用状態で校正して使用することができるようになっています。
実はこれって意外に面白い機能で、自分の使用条件に合わせて再校正して使ったり、Arduinoを動作させながら動的に動作周波数を変えてみたり、等の応用が可能です。
と、言う事で、ATmega328データシートを参照しながら、内蔵RC発振器の仕様調査からスタートします。
ATmega328には、内蔵RC発振器が搭載されています。その名称から察するに、恐らく、「#8-9 発振器でLEDを自動点滅する」で作成した発振器みたいなものが搭載されているのでしょう。ATmega328の内蔵RC発振器は、OSCCALレジスタに設定した値に従って、周波数を変更できるようになっています。
データシート巻末に載っている、TYP特性グラフ(以下図、引用)によると、校正可能周波数範囲は4〜13[MHz]程度です。
※ATmega328データシートから引用
グラフは、横軸にOSCCALレジスタの設定値0x00〜0xffを取り、縦軸にその時の発振周波数をプロットしています。温度は3温度分のプロットがありますが、電圧条件なども書かれていないし、そもそも、掲載されているのは典型的な1チップの特性でしょうから、このグラフから応用できる情報は少ないです。
今欲しいのは、どこにあるのかわからない典型的なチップの特性グラフではなく、目の前にあるATmega328P-PUの特性グラフなのですから。今回の目的は、手元のATmega328P-PUで、上図のようなグラフを作成することです。
仮に、上図のようなグラフを描くことができると、所望の周波数(縦軸)で発振させたい時に、OSCCALにどの値(横軸)を設定すれば良いか?を知ることができるようになります。
工場出荷時、8.0MHz±10%となるOSCCALの値を測定して、その測定結果を「キャリブレーションビット」に書き込んであります。キャリブレーションビットは、普通のレジスタとは違って、CPU命令ではアクセスできません。「#9-4 ATmega328のヒューズ書き込み」で使ったシリアルプログラミングモードなどを使うと、ヒューズビットなどと同様にして読み出すことができます(ユーザは、キャリブレーションビットには書き込みできません)。
キャリブレーションビットに記録されたデータが、リセット中自動的にOSCCALレジスタに転送され、リセット解除直後は、校正された8.0MHzで発振しています。OSCCALレジスタは、普通にユーザが書き換え可能なレジスタなので、値を書き換えることで、異なる周波数で発振させることも可能です。
LSIには「製造ばらつき」がありますので、チップによって、8.0MHzに設定するためのOSCCAL値は異なります。
製造ばらつき以外にも、周波数の変動が起こり得ます。工場出荷時、8.0MHz @3V,25℃で校正されているのですが、データシートのTYP特性を眺めてみると、温度や電圧などの条件によって、3〜4[%]程の変動がある様です。8.0MHzが7.7〜8.3[MHz]くらいまで変動する、ってことです。
工場出荷時の校正条件である3V,25℃ですが、温度は良いとして、普段は5Vで使うことが多いので、少なくとも、3 → 5[V]の分の偏差は存在します。
従って、通常使用条件(5V,室温)で、最適な値となるように校正して使うのが理想です。これが目的の1つめ。
ちょっと「クロックアップ」と言うのとは違う気がしますが(定格周波数内なので)……、今回の目的の2つ目は、内蔵RC発振器の周波数を〜14MHz程度に設定して高速動作させたい、と言う事にあります。
この時、留意しておくべきことは、「#9-4 ATmega328のヒューズ書き込み」でも指摘した通り、フラッシュやEEPROMの消去・書き込み時に、内蔵RC発振器を使っているので、消去・書き込みを実行する時には、8.8MHz以上に上げてはならない、と規定されていることです。
8MHzで起動後、自分でOSCCALを設定して14MHz程度にクロックアップして実行、EEPROMの書き込み実行時のみ、一時的に元の周波数(8MHz)に戻して書き込む、と言った用途を想定しています。
以上、まとめると、今回の目的は以下の二点にあります。
どちらの目的も、手元のATmega328P-PUに関して、発振周波数データを測定し、上で示した様なグラフを作成することに帰着します。前回作成した、328duino(第二形態)(下図)の内蔵RC発振器を校正したいと思います。
目的を果たすためには、内蔵RC発振器の周波数を測定しなければなりません。本節では、クロック周波数の測定方法について考察します。
まず、328duino(第二形態)を使うことで、MCUが内蔵RC発振器の出力クロックで動作するようになります。CPUもですが、周辺のタイマやUARTなど、全てが内蔵RC発振器で動作します。
しかし仮に、システムクロック自体を外部端子に出力(ヒューズLのCKOUTビット)したとしても、8MHzのクロックは速すぎて、STM32+Logiscopeでも、波形を拾いきれません。止むを得ないので、クロックそのものを測定するのはあきらめて、タイマのPWM出力周波数を測定することで、間接的にクロック周波数を測定しようと考えました。
PWMを出力するタイマには、8ビットタイマ(TC2)を使います。TC2を以下の様に設定して、PWMを出力しておきます。
レジスタ | アドレス | 設定値 | 備考 |
TCCR2A | 0x00b0 | 0x10 | normal mode 0,OC2B=toggle |
TCCR2B | 0x00b1 | 0x02 | normal mode 0,fcpu/8 |
OCR2B | 0x00b4 | 0xff | デューティ50%、周期2×256カウント |
上の設定では、CPUのクロック周波数をfcpu’とした時、PWMの周期は、Tpwm’=2×256/(fcpu’/8)となります。例えば、fcpu’=8MHzの場合、Tpwm’=512μsです。
PWMを端子割り込みなどで受けて、前回割り込みと今回割り込みの間の時間を計測すると、PWM周期を知ることができますが、割り込みでは正確な時間を計測するのは困難なので、16ビットタイマ(TC1)のインプットキャプチャ機能を使います。
レジスタ名 | アドレス | 設定値 | 説明 |
TCCR1A | 0x0080 | 0x00 | normal mode 0,OC1A/B=normal port |
TCCR1B | 0x0081 | 0x41 |
normal mode 0,fcpu/1, ICP1 noise canceler disable,ICP1 rising edge |
TIMSK1 | 0x006f | 0x20 | インプットキャプチャ割り込み有効(ICIE1) |
一番高い精度でPWM周期を測定するため、TC1を最速クロックfcpu/1でカウントします。ICP1端子の立ち上がりエッジでインプットキャプチャを実行し、同時に割り込みを起こします。
インプットキャプチャと言うのは、ICP1端子の立ち上がりエッジを検出した時のカウンタ値TCNT1レジスタを、ICR1レジスタに転送(取り込み)する機能です。タイマ(TC1)はそのままカウントを続けるので、前回の取り込み値と、今回の取り込み値を引き算すると、その差分がPWMの周期となります。
TC1はfcpu/1でカウントしているので、求めた<差分値>/(fcpu/1)が、PWM周期Tpwmです。
前節のターゲット側で出力したPWMの周期Tpwm’と、ホスト側で計測したPWM周期Tpwmが等しいと置いて、ターゲット側のクロック周波数fcpu’を求めます。ここで改めて、ターゲット側周波数fcpu’とホスト側周波数fcpuと置きます。
ターゲット側:Tpwm’=2×256/(fcpu’/8) …… 前節
ホスト側:Tpwm=<差分値>/(fcpu/1) …… ホストで計測した周期
ここで、Tpwm=Tpwm’としてfcpu’について解くと、
fcpu’=2×256×8×fcpu/<差分値>
ホスト側Arduino UNOの水晶発振子fcpu=16MHzが神様となります。万一、「UNOの水晶発振子が15.4MHzにずれてました〜」なんてなったらアウトですが、信じるしかありません(1^−^;)。
※さすがに、そこまでずれていることは無いでしょうけど、0.5%ずれていて16MHz → 15.92MHzになっていたとしたら、計測したfcpu’にも、0.5%の誤差が伝搬することには留意。
今回、内蔵RC発振器を校正される側の328duinoをターゲット、PWM周期から周波数を計測する側のArduino UNOをホストと呼ぶことにします。
前節で考察したように、ターゲット(328duino)がTC2を使ってPWMを出力し、それをホスト(UNO)がTC1のインプットキャプチャで受けてターゲットのクロック周波数を計測します。上で示したグラフのデータを人手で取得するのは大変面倒なので、全てプログラムで自動取得できるようにしたいと思います。
そのためには、ホスト側からターゲット側へ、「OSCCALにこの値を設定してくれ」と、命令を飛ばす必要があります。ターゲット側は、OSCCALの値を変更しながら動作するので、どんどん動作周波数が変わります。従って、非同期シリアル(UART)の様な非同期の通信は成立しません。
クロック同期の通信手段(I2CまたはSPI)を使う必要があるのですが、ここでは、マスタ/スレーブ共、使い易いI2Cを選択します。やり取りすべきデータは、ターゲット側のOSCCALレジスタ設定値だけですので、以下の様に決めます。
ホストはI2C通信を使って、ターゲット側のOSCCALレジスタ値を0x00 → 0xffまで振りながら、随時PWM周期を測定し、各OSCCALレジスタ値に対応するクロック周波数を自動取得します。
※I2Cの使い方については、「逆引き索引」から、I2Cに関する過去記事をご参照ください。
今回は、ホスト側とターゲット側にそれぞれ連携するソフトウェアが必要ですが、2本のプログラムを別々に作るのは面倒なので、1本にまとめてしまいました。I/OポートD4をINPUT_PULLUPに設定しておいて、起動時のD4端子状態によって、ホスト/ターゲットを切り分け、内部で場合分けして動作するように作り込みました。
pinMode (pin_HOSTSEL, INPUT_PULLUP);
host_target=(digitalRead (pin_HOSTSEL)==LOW)? true: false;
if (host_target)
{
// host: ホスト側の処理内容を記述
}
else
{
// target: ターゲット側の処理内容を記述
}
D4端子を放置すると、INPUT_PULLUPのお陰でターゲット側だと判定されるので、ホスト側に使用するArduino UNOだけ、D4端子とGNDをジャンパで接続して、D4=LOWに落として使います。
インプットキャプチャ割り込みのサービスルーチン(ISR)内処理を、以下に解説しておきます。ISR内でPWM周期からターゲットの周波数を算出し、移動平均も取っています。以下、コードの下に説明文を挿入しながら解説します。
ISR (TIMER1_CAPT_vect)
{
// ICI1 interrupt service routine
uint16 cnt;
static uint16 cnt_prev;
TC1インプットキャプチャ割り込みのISR宣言です。
// measure frequency of pwm
cnt=ICR1;
割り込みの最初に、取り込んだカウンタ値(ICR1レジスタ)を変数cntに取り込んでおきます。
ftgt=uint16 ((F_CPU/1000.*frq_PERI*frq_DIV)/(cnt - cnt_prev) + 0.5);
前回の割り込みで取り込んだカウンタ値cnt_prevと、今回のカウンタ値cntを減算してPWM周期を求め、そこからターゲットのクロック周波数ftgtを計算します。ftgtは、単位kHzの整数型で計算しています。
cnt_prev=cnt;
今回のカウンタ値cntをcnt_prevにコピーして、次回の割り込みに備えます。
// moving average
ftgt_sum=ftgt_sum - frq_buf[frq_ptr] + ftgt;
クロック周波数ftgtが時間変動する場合に備えて、移動平均を取っておきます。
frq_buf[frq_ptr]=ftgt;
frq_MAX段のリングバッファに、過去frq_MAX(=128)個分の測定値を蓄えています。1段当たり2バイトなので、256バイトのRAMを使用しています。まだ、RAM容量に余裕があるので、もう少し広い範囲の移動平均を取るように設定する事は可能です。
frq_ptr++;
if (frq_ptr>=frq_MAX)
{
frq_ptr=0;
frq_ptr_ovfl=true;
}
}
リングバッファのポインタfrq_ptrを更新して、ISRを抜けます。
完成したプログラムをリンクしておきます。
計測プログラム:cali_rcosc.ino
Arduino UNOと、328duino(第二形態)に、それぞれコンパイルして書き込んでおいてください。シリアルモニタを開くと、以下のコマンドを受け付けるようになっています。
ターゲット側からも実行可能なコマンドが用意してありますが、基本的に、ターゲット側(328duino)を操作する必要は無いので、ホスト側(UNO)のシリアルモニタだけを開いてください。
準備が整いました。実験して行きましょう。まず、ホスト(UNO)と、ターゲット(328duino)を、以下の様に結線します。
ホスト Arduino UNO |
接続 |
ターゲット 328duino |
|
5V | → | Vcc | |
GND | ⇔ | GND | |
D8(ICP1) | ← | 5 | PD3_OC2B_D3 |
A4(SDA) | ⇔ | 27 | PC4_SDA_A4 |
A5(SCL) | ⇔ | 28 | PC5_SCL_A5 |
D4 | GND | ||
× | 6 | PD4_D4 |
※I2Cのプルアップは、Wireライブラリが設定した、A4,A5端子のプルアップ機能を期待。
ここでは5Vの校正データを取得しますので、UNOから328duinoへ5V電源を供給します。もし、別な電源電圧で校正したい場合には、328duinoへ所望の電源を供給し、UNOとの通信線全てにL/Sを挿入してください。
以下、実験中の様子です。
ホスト(Arduino UNO)側のシリアルモニタで実験します。
実験サンプル:【 】内はシリアルモニタからの送信データ
【rdoc】
OK: target OSCCAL=0x9c
ターゲット(328duino)のキャリブレーションビット値、0x9cが返ってきました。
【show,8】
OK: show 8 (1000[ms])
OUT: freq=,7965,7968
OUT: freq=,7962,7967
OUT: freq=,7961,7965
OUT: freq=,7960,7962
OUT: freq=,7962,7961
OUT: freq=,7974,7963
OUT: freq=,7970,7967
OUT: freq=,7961,7969
計測した周波数が1,000ms毎に8回、シリアルモニタへ表示されました。1つ目の数値が表示時点最新の計測データ、2つ目の数値は最新frq_MAX(=128、デフォルト)回分の移動平均値です。Excelなどに貼り付ける時の利便性を鑑み、カンマ(,)区切りになっています。
う〜ん、ずいぶんと時間変動があるみたいですね。しかも、かなり激しい。実験(校正)を進める前に、プログラムを含めた計測システム(系)の妥当性を確認しておく必要がありそうです。
まずは、時間変動を統計的に確認しておきましょう。
実験サンプル:【 】内はシリアルモニタからの送信データ
【show,1024,1】
OK: show 1024 (1[ms])
OUT: freq=,7956,7955
OUT: freq=,7954,7955
:
: (中略)
:
OUT: freq=,7955,7961
OUT: freq=,7960,7960
1,024回の測定値を取得しました。このデータをExcelに貼り付けて、「データ ⇒ データ分析 ⇒ ヒストグラム」を実行してみました。移動平均(第二数値)では無くて、毎回の測定結果である、第一数値の方です。
いくつか、出現しやすい周波数が飛び出しているようですが、まあ、正規分布っぽいですね(物理現象ですから)。
統計量:中央値=7963、平均=7962.57、標準偏差σ=6.44
±3×σ≒±19kHzの範囲内に約99.7%の出現値が含まれますので、ざっと、±0.24%(≒19/7962.57×100%)の時間変動がある様です。
もしかすると、内蔵RC発振器という装置が、「こういうもの」なのかもしれないので、水晶発振子で動作する別なArduinoをターゲットとして、水晶発振子の時間変動データを取得してみます。
一旦、328duinoを取り外し、代わりに手持ちのPro Miniを、ターゲットとして上表と同じ様に接続しました。
実験サンプル:【 】内はシリアルモニタからの送信データ
【show,1024,1】
OK: show 1024 (1[ms])
OUT: freq=,15980,15978
OUT: freq=,15980,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15980,15978
OUT: freq=,15977,15978
:
: (中略)
:
OUT: freq=,15980,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
OUT: freq=,15977,15978
こちらは、ヒストグラムを描くまでもありませんでした。1,024点データを取得してみましたが、2つの値(15,980kHzと15,977kHz)しか出現しませんでした。2つの差=3kHzも、PWM周期の測定に使用したタイマ(TC1)の1カウント分の差でしかないことを確認済みです。
非同期のPWMを取り込んで計測しているので、タイミング次第で、1カウント分の増減は必ず発生します。
水晶発振子って、すごいんですね。ピッタリ同じ周波数で発振し続けているみたいです。時間変動は見られません。平均値は15,978kHzで、期待値16,000kHzに対しては、0.15%程の偏差が見られます。まあ、UNOとPro Mini、どっちの水晶発振子が、どっち側にどのくらい変動しているのか?は、わかりかねますが……。
と、言う事で、測定系は正しいようです。内蔵RC発振器は、「水晶発振子に比べると、周波数の時間変動が大きい」ことがわかりました。データシートにも、「…8.0MHz (3V,25℃)…」と、記載されていたので、有効数字2桁程度なのでしょう。
内蔵RC発振器の周波数測定結果を有効数字2桁で丸めると、8.0MHzが計測できていますので、仕様通りの結果が観測できた、と言って良いでしょう。
測定系のデバッグが終わったところで、もう一度、ターゲットに328duinoを接続し直し、いよいよ、校正データを取得してみます。
実験サンプル:【 】内はシリアルモニタからの送信データ
【eval】
0,4679
1,4702
2,4719
3,4751
:
: (中略)
:
252,14515
253,14687
254,14826
255,15000
OK: eval
このデータをExcelに貼り付けて描かせたグラフが以下図です。
横軸にOSCCALレジスタ設定値、縦軸にその時の周波数kHzをプロットしています。Vcc=5V,室温での測定結果です。
意外と綺麗に、データシートのTYP特性グラフと同様なグラフが描けました(1^−^)。以下雑感です。
残念ながら、16MHzまでは届きませんでしたが、最大15MHzまで内蔵RC発振器で動作させることができそうです。
念押しですが、この校正データは、チップ(個体)によって異なります。皆様の手元にあるATmega328に、本ページの測定結果を適用しても、同じ周波数にはなりません。チップ毎に校正データを取得する必要があります。
クロックは、同期設計においては「神の振る指揮棒」みたいなもので、周波数は一定であることが前提で動作しています。そのクロックの周波数に時間変動が生じると、どうなるのでしょうか?
あんまり、良いことは無い印象です。でも、EMI(Electro Magnetic Interference)ノイズに関しては、肯定的に働くかもしれません。
EMIとは、電子機器の動作によって発生する電磁波ノイズのことです。「右ねじの法則」でご承知の通り、電流が流れると、その方向に右ねじをねじ込むような磁界が発生、磁界が電界、電界が磁界、……と繰り返すことで電磁波が発生するのがメカニズムです。
同期設計では、クロックの立ち上がりエッジでチップ内の回路が一斉に動作するため、その瞬間に大きな電流が流れます。結果、クロック周波数に起因するEMIノイズのピークが大きく観測されてしまうことになります。
回路が動作すれば電流が流れます。よって、EMIノイズが発生するのは避けられません。着目すべきは、「クロック」による一定時間毎のEMIノイズが、クロック周波数一点に集中することです。水晶発振子の実験で分かった様に、綺麗に決まった周波数で発振し続ける水晶発振子では、その周波数にすべてのEMIノイズが集中して放射されるため、EMIノイズのピークが高くなってしまいます。
一方、クロック周波数に時間変動がある場合、各クロック周波数で発生したEMIノイズのピークが分散されて低くなり、結果としてEMIノイズが小さくなることが期待できます(イメージ図↓)。
これを積極的に実施したのが、SSCG(Spread Spectrum Clock Generator)という方式です。
本来のSSCGは、例えば、センタスプレッド(指定周波数を中心に、上下に変動する)やダウンスプレッド(指定周波数から低周波数側に変動する)が選べたり、0.2〜数%くらいまでの変動幅を持っていたりするようです。それに多分、原理上、周波数の時間変動は正規分布より、一定確率分布の方がEMIノイズの分散に寄与できるはずです。
※正規分布だと、結局、中心周波数近辺にピークが集中して、EMIノイズレベルが高くなってしまうためです。
ATmega328P-PUの内蔵RC発振器は、当然、SSCGを実施した物ではなくて、ただ単に回路特性上、時間変動を持ってしまっただけ、なのですけど……(しっかり正規分布だったし)。実験による計測結果より、「センタスプレッドの±0.24%」くらいには思って良いのかも?? …… イヤ、嘘。ゴメンナサイ、言い過ぎましたm(1__)m。
使用上の注意事項をまとめておきます。
実用化するには、もう少し工夫が要りそうですね。検討してみます(1^−^)/✨。