内部発振器を使ったArduino
Arduinoの回路はAtmega328に16MHzの水晶発振子を接続したものが一般的です。しかし、Atmega328は内部発振器も持っています。
独自でArduino互換回路を作る際に、内蔵発振器を使うと、部品点数を減らすことができ、さらに水晶発振子を接続するための2本のピンをGPIOとして使うことができるようになります。
ここまでだと良いことづくしなのですが、内蔵発振器は8MHzで16MHzの水晶発振子と比べると遅いです。また、水晶発振子ではなく、RC発振で実装されているため、精度は水晶発振子に比べるとかなり劣ります。その後差は±10%とされていますが、動作させる電圧や、気温でも大きく変化します。この誤差のために、内部発振器で動作するArduinoは115200bpsでのシリアル通信は非推奨となっています。
これらのメリット・デメリットを踏まえたうえで、内部発振器を使うかどうかを判断する必要があります。
内部発振器の誤差による問題
内部発振器の誤差があると様々な問題が生じます。そのうちで最も大きなものは「外部機器との同期がずれる」というものがあります。特にシリアル通信でこの問題が起きるとArduinoとしての書き込みができなくなってしまい、使い物になりません。
私が、今作っているHACHIBAR(https://inajob.github.io/hachibar/)というゲーム機は内部発振器を使ったArduinoを利用しているのですが、まさにこの問題が起きました。ほとんどの個体は正しくArduinoとしてプログラムを書き込むことができるのですが、一部(今のところ5個中1つ)の個体で書き込みができないのです。115200bpsが非推奨ということで19200bpsまで周波数を下げたのですが、それでも書き込めません。
おかしなAtmega328はICSP経由ではプログラムを書き込むことができたので、一見うまく動いているようなのですが、Arduinoのブートローダを書き込んでもうまくPCからの書き込みができないという状態でした。
始めはAtmega328か、USBシリアル変換ICが壊れているのかと思っていたのですが、個別に挙動を確認するとうまく動いているように見えるので、何か変だな?と思っていました。
内部発振器に誤差があることは知っていたのですが、シリアル通信ができないほどずれているものがあるというのは知らなかったので、しばらくはどうしようもない問題としてほったらかしていました。しかし内部発振器の誤差を調整する方法があることを知り、このおかしな挙動をするAtmega328もArduinoとして使用できるのでは?と思いさらに調査を進めることにしました。
OSCCALによる内部発振器の補正
内部発振器の誤差を調整する方法とはOSCCALというレジスタを利用することです。このレジスタは、起動時にAtmega328のシグネチャバイトに工場出荷時に記録された値が設定されています。
このOSCCALはプログラムから書き込むことができ、それにより内部発振器の周波数をクロックアップしたり、クロックダウンさせることができます。
例えば下のようなコードを書く事で内部発振器で動作するArduinoの速度を標準より早くしたり遅くしたりすることができます。この値を変化させるとシリアル通信ができなくなったり、delayの待ち時間がずれたりと、様々なおかしなことが起きます。
void setup(){
OSCCAL = 100; // 好きな値に設定
}
void loop(){
// Lチカ
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
内部発振器の誤差が大きくてシリアル通信がまともにできないAtmega328でも、このOSCCALを変更することで、通信ができるようになることが想像できました。
OSCCALはリセットで初期値に戻る
しかし、話はそう簡単ではありませんでした。前述したようにOSCCALは起動時にAtmega328のシグネチャバイトに工場出荷時に記録された値が書き込まれるようになっています。
そのため、Arduinoのブートローダが動作しているときのOSCCALをいじる方法がありません。先程の例のようにArduinoのスケッチ内でOSCCALが変更できても何の意味もありません。
ブートローダーを改造する
Arduinoのブートローダーに何かOSCCALを調整する機能があれば良いのでは?と思いちょっと調べたのですが、そういうIssueはちらほら見かけたのですが、そのものずばりの機能が見当たりませんでした。
ということで「ないなら作ればいいじゃない!」ということでブートローダーに手を加えてみることにしました。改造する対象は本家Arduinoではなく、互換ブートローダであるOptiboot(https://github.com/Optiboot/optiboot)にしました。
変更はいたって単純で、makeする際にOSCCAL_VALUEというオプションが設定されている場合は、ブートローダーのmain関数の頭でOSCCALの値を書き換えるというものです。
この改造を行い、かつ適切なOSCCAL_VALUEを設定したブートローダを焼きこむことでおかしな周波数だったボードでも無事シリアル通信ができるようになり、Arduinoとして書き込むことができるようになりました。
補正値の求め方
補正値を正しく求めるために、AnalogWriteの際のPWMの周波数をテスターで読み取って補正しました。
幸い、今回問題になった基板はゲーム機の基板で、良い具合にたくさんのスイッチが搭載されています。そこであるピンでAnalogWriteで128の値を出力し、↑、↓ボタンでOSCCALの値を変更するようなプログラムを書き、そのPWM周波数の変化をテスターで観測し、適切な補正値を求めました。
たまたま購入していた下記テスターが周波数測定の機能を持っていたので役立ちました。
本家にPullRequest
この調査をする中で、結構多くの人が内部発振器で動かすArduinoでのOSCCALの値の変更に悩んでいることがわかりました、そこで、私の変更を本家にPullRequestができるのでは?と思いカジュアルにPullRequestを出してみたところ、無事マージされました!
ArduinoやOptibootには日々お世話になっていたので、良いPullRequestで貢献することができてうれしかったです。
Tips: Dockerでビルドする
私は普段Windowsを使っているのですが、Optibootをビルドするためにいろいろ環境を整えるのが面倒だったのでDockerを利用しました。下記のような感じでビルドすることができたので、環境を用意するのが面倒な人にはお勧めです。
$ cd optiboot/optiboot/bootloaders/optiboot/
$ docker run -v /`pwd`:/src -w //src rubberduck/avr make BAUD_RATE=19200 OSCCAL_VALUE=145 atmega328_pro8
他の解決策
これは外部のRTCを使い、内部発振の誤差を測定し、その誤差を設定するような特殊なブートローダーを書き込むというプロジェクトです。RTCが手元にあるなら、これを使うのがよさそうです。
まとめ
内部発振器で動作するArudinoを使う際には、その誤差に気を付ける必要があります。個体によってはシリアル通信がまともに出来ないことがあります。Arduinoとして書き込み機能ができないほどに誤差がある場合は、ブートローダ内でOSCCAL値を書き換えるなどを行い、クロックを補正する必要があります。
今回のPullRequestによりOptiboot(https://github.com/Optiboot/optiboot)では、この変更が簡単に出来るようになりました。
https://github.com/pkarsy/rcCalibratorもよくできているプロジェクトです。
参考書籍
たまたま買ったこの本にOSCCALについて書かれていたのが非常に参考になりました。Arduinoに慣れてきてAVRのことも気になってきた方にお勧めの一冊です。リファレンスの名の通り、そばに1冊置いておくと安心できそうです。