まえがき
Arduino UNO R4はRTCを内蔵してるものの、X’talを搭載せずに、マイコン内蔵のRC発振器を使っているため、ものすごく精度が悪い。「悪い」というレベルを通り越して、RTCとしてまったく使い物にならない。何しろ、1分で1秒くらい進むのだから。
ということは、CPUクロックも同じように精度が悪いのではなかろうか?だとしたら、delay()やmillis()なども相当精度が悪いだろう。ということで、調べてみようかと。
簡単な実験
一番簡単なのはLチカ。1秒ごとの点滅に合わせてストップウォッチをスタートさせれば、見る見るうちにズレていくだろう(秒のカウントアップとLEDの点滅のタイミングが合わなくなるに違いない)。
ところが、実際にやってみると、予想に反してちっもズレない。そのまま15分くらい放置してLEDの点滅状況を見てみたけど、ストップウオッチの秒カウントアップととズレていない。「一周回ってまた合った」ということも考えられるけれど、観察を続けてもズレている状況は確認できなかった。
では、もう少し詳しく見てみようと思い、PWM出力を使って1MHzを出力させる機能を付け加えて、それを周波数カウンタで見てみた。

1000.008kHzなので、+0.0008%ということか。ppmなら+8ppm(で合ってるかな)。かなりいいんじゃないか?
周波数カウンタは秋月のキットのもの。GPSを使って校正しているので、それなりに合っていると思う。
ソースコードはこちら。生成AIに作らせた。
/*
* Arduino UNO R4用 PWM出力+Lチカサンプル(pwm.h使用)
* D11ピンから指定周波数のPWM信号を出力し、内蔵LEDを1秒周期で点滅(0.2秒点灯/0.8秒消灯)
*/
#include "pwm.h"
#define PWM_FREQ 1000000.0f // 設定可能範囲: 約1Hz~数MHz
const int LED = 13;
const int PWM_PIN = 11; // D11ピン
PwmOut pwm(PWM_PIN);
void setup() {
pinMode(LED, OUTPUT);
pwm.begin(PWM_FREQ, 50.0f); // 周波数(Hz), デューティ比(%)
}
void loop() {
digitalWrite(LED, HIGH);
delay(200);
digitalWrite(LED, LOW);
delay(800);
}
CPUクロックを測る
こちらの記事によれは、Arduino UNO R4はIOピンにCPUクロックを出力させることができるそうだ。
マイコンチップのRA4M1にはいくつかのクロックソースを内蔵しており、レジスタの設定によって出力させるクロック源と分周比を指定できるとのこと。
CPUクロック
ということで、一番速いクロックである48MHzを出力させて、それを周波数カウンタで見てみた。

ついでに、2分周させてもみた。

やはり、どちらも約+8ppm。PWM出力の場合と同じ。というか、違っていたら困るけど。
外部電源の場合(CPUクロック)
上の測定では、USBから電源を供給していたのだけど、電源の供給方法によってクロックの精度が変わるとの記事が目にとまった。
ということで、再現実験。外部電源を接続して、USBケーブルを抜く。

周波数がだいぶ上がった。そればかりでなく変動も大きくなった。

だいたい、この48009~48016kHz程度の範囲でフラフラしている。悪い方の数値を取れば、+3%強、クロックが速くなった。← 間違い。+3%ではなくて、+0.3%。
なお、USBケーブルを外すのがミソ。参考元の記事(のコメント)によれば、USBクロックに同期させて補正していのではないかとのこと。したがって、外部電源供給であっても、USBケーブルが繋がっていればその補正が働く。補正のタイミングとの兼ね合いで、測定結果がふらついて見えるのかなぁ?周波数カウンタのゲート時間は1秒なので、細かいジッタは平均されてこういう数値としては見えないだろうから。
RTC用クロック
もう一つついでに、RTCで使われるLOCOを分周比1に設定して見てみる。

これは、本来は32.786kHzが期待される。しかしながら、33.350kHzと、大幅に高い周波数。+1.7%。なるほど、RTCが1分で1秒以上進むはずだ。
なお、このときはUSBケーブルを接続した状態。それでもこんなに周波数がズレているということは、LOCOはUSBクロックによる補正はされないのだろう。
まとめ
「クロックの周波数精度は結構いい。ただし、USBケーブルをつないでいる場合」と言うところか?いや、これを「いい」と言っていいのかどうか疑問だけど。
USBケーブルを繋いでいない場合は、今回の私の測定では 約+3% 約0.3% だったが、参考にさせてもらった記事では約-0.2%だったようなので、個体差が大きそう。
ちょっと別の視点から。例えば音楽の場合。平均律では1オクターブ(周波数2倍)を12音に均等に割っている。この12等分したもの(2の12乗根)、すなわち、半音は周波数で言えば1.059倍ということになる。
\(2^{\frac{1}{12}}=1.05946\)
つまり、半音差の周波数差は、わずか6%弱。CPUクロックが3%ズレてしまうと、これを元に作った周波数では1/4音くらいズレてしまう。こんなにもズレてしまったら音楽向けの用途には無理。USBケーブルを使ってUSBクロックへの同期を期待する手もあるかもしれないが、「それは装置としてはちょっと違うんじゃないの?」というか。それに、ジッタが大きいようなので、常にビブラートがかかった状態になりそうだし。
このまとめを書きながら、ふと思い立って最初のLチカを外部電源供給で動かしてみたところ、案の定、ストップウォッチの秒のカウントアップとLED点滅のタイミングがじわじわとズレていった。1分も経てばストップウォッチのカウントアップがだいぶおいていかれる(「LED点滅が速い」が正しい解釈だけど)。
やっぱり、特段の事情があるのでなければ、UNO R3を使うのが無難だなぁ。
【追加実験】
ソースコード
CPUクロックを測定したときのコード。参考にさせたもらったものとほぼ変わらないが、備忘録として掲載しておく。変えたのは、クロック源をHOCO、分周比を1、それと、シリアルの初期化待ちを3000にしたことだけ(手元のUNO R4だとこれくらい待たないとシリアルモニタに出力されない)。
// Arduino UNO R4 のクロックを IOピンに出して周波数を測定
// http://radiopench.blog96.fc2.com/blog-category-65-4.html
// 20240214_UnoR4ClockOutTest
// D11ピン(P109)にCLKOUT信号を出す
char cbuff[10]; // 文字列操作バッファ
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println();
Serial.println("Start !");
// CKCORを設定(クロックソースと分周比を設定)
R_SYSTEM->PRCR = 0xA501; // レジスタプロテクト解除
R_SYSTEM->CKOCR = 0; // CKOCRの全ビットクリア
R_SYSTEM->CKOCR_b.CKOSEL = 0b000; // HOCO:000, MOCO:001, LOCO:010, MOSC:011, SOSC:100
R_SYSTEM->CKOCR_b.CKODIV = 0b000; // 1分周:000, 001, 010, 011, 100, 101, 110, 128分周:111
R_SYSTEM->CKOCR_b.CKOEN = 1; // クロックアウト許可
R_SYSTEM->PRCR = 0xA500; // レジスタを再プロテクト
viewCKOCR();
// D11ポートの設定(CKOUTが出力出来るように設定)
R_PMISC->PWPR_b.B0WI = 0; // 書き込みプロテクトを、
R_PMISC->PWPR_b.PFSWE = 1; // 外す
R_PFS->PORT[1].PIN[9].PmnPFS = 0; // 念のために設定をリセット
R_PFS->PORT[1].PIN[9].PmnPFS_b.PDR = 1; // D11(P109)を出力に設定
R_PFS->PORT[1].PIN[9].PmnPFS_b.PSEL = 0b01001; // CLKOUTを選択
R_PFS->PORT[1].PIN[9].PmnPFS_b.PMR = 1; // 周辺機能をON
R_PMISC->PWPR_b.PFSWE = 0; // 書き込みプロテクトを、
R_PMISC->PWPR_b.B0WI = 1; // 掛ける
viewD11(); // ちゃんと設定出来たか確認
}
void loop() {
}
void viewCKOCR() { // CKOCRの内容を出力
sprintf(cbuff, "CKOCR = 0x%02x", R_SYSTEM->CKOCR);
Serial.println(cbuff);
}
void viewD11() { // D11のポート設定を出力
sprintf(cbuff, "D11(P109) = 0x%08lx", R_PFS->PORT[1].PIN[9].PmnPFS);
Serial.println(cbuff);
}
コメント