公開日:2020/03/14
title.png

9th Season


#9-9 328duinoをArduino IDEに組み込む

 今回の目標は、「#9-5 ブレッドボードにArduinoっぽい物」で作成した328duino(第二形態)をArduino IDEに組み込むことです。わざわざArduino IDEに組み込まなくても、「Pro or ProMini (3.3V,8MHz)」を選択すれば、プログラム(スケッチ)の書き込みはできるし、「必要ないんじゃない?」。ですよね……、でも、「#9-7 内蔵RC発振器を校正する」で匂わせた様に、最終目標は「内蔵RC発振器を使い、8MHzを超える周波数で動作させたい」なので、この野望を実現するにはArduino IDEに組み込む必要があります。

※今回の作業を踏むと、野望(最終目標)が実現されます。

前提条件・準備

 まず、前提条件を確認しておきましょう。この条件がクリアできていないなら、この先に読み進む意味がありません。

 1)2)は必要事項、ですので、3)の条件に当てはまる方は、1)2)をクリア後、先に進んでください。
 スコープは第二形態なので、以降、このページでは、328duino(第二形態)を単に328duinoと記します。

内蔵RC発振器の周波数を変更するライブラリ

 OSCCALレジスタを設定する事による動的周波数変更に対応するため、ライブラリ(328duino.h)を作ります。

 Arduino IDEのコンパイルでは、F_CPUというマクロが定義されていて、このマクロにCPUのクロック周波数が単位Hzで格納されています。例えばSerialライブラリを、以下の様に初期化した場合。

  Serial.begin (9600);

 シリアルの通信速度が9,600baudになるように調整するため、F_CPUマクロの値が使われています。millis()関数やmicros()関数などが返す値も同様に、F_CPUで換算されています。
 F_CPUマクロは、Arduino IDEのboards.txtを編集することで、変更可能なのですが、その方法については、後ほど節を設けて説明します。

 ここでは一旦、F_CPUマクロに、任意の周波数を設定できた、と仮定しましょう。
 内蔵RC発振器は、OSCCALレジスタを設定する事により、4〜14[MHz]程度の範囲で任意の周波数を設定する事ができます。例えば、14MHzで動作させたい時に、F_CPU=14,000,000Hzと、設定したとします。ところが、リセット解除直後は、工場出荷時の8MHzで動作しています。

  void setup (void)
  {
    Serial.begin (9600);
    Serial.println ("RDY");
  }


 すると、上のコードは失敗します。F_CPU=14MHzが設定されてコンパイルされたため、クロック周波数が14MHzである前提で、9,600baudになるような速度で通信しようとするのに、実際のクロック周波数が8MHzだからです。

 そこで、OSCCALの値を変更することで任意のクロック周波数に設定する、setSysClock()関数(仮)を定義します。

  void setup (void)
  {
    setSysClock (F_CPU/1000000);
    Serial.begin (9600);
    Serial.println ("RDY");
  }


 これで、Serialライブラリの通信速度を設定する前に、クロック周波数をF_CPUに設定する事ができました。コンパイル時のF_CPUと、実際の動作周波数が一致するため、Serialライブラリだけでなく、millis()関数やmicros()関数なども正常に動作させることができます。

 ここで、ちょっと面倒なのは、このプログラムをUNOでコンパイルして実行する場合です。UNOでは、setSysClock()関数は必要ありませんし、逆に実行してしまうと、F_CPU(=16MHz)に合わせて、意図しない周波数にOSCCALが設定されてしまう恐れもあります。

※内蔵RC発振器の周波数は、フラッシュやEEPROMの消去・書き込みに使用されているので、不必要にOSCCALの値を変更すべきではありません。

 そもそも、328duinoで実行しないコードも含めて、「おまじない」でsetSysClock(F_CPU/1000000);を挿入しておくというのも、難儀な話です。

A328duinoクラスライブラリ

 そこで、A328duinoクラスを定義して、これをstaticにインスタンスしておきます。

  static A328duino A328d;

 こうすることで、コンパイルされたプログラムのsetup()関数を実行する前に、A328duinoクラスのインスタンスが初期化されることとなり、結果として、A328duinoクラスのコンストラクタが実行されます。

  A328duino::A328duino (void)
  {
    // A328duino constructor
    setSysClock (F_CPU/1000000);
  }


 上のコードはイメージですが、コンストラクタ内で、setSysClock()を実行して、F_CPUに相当する周波数になるようにOSCCALレジスタを設定しておくと、setup()関数に到達した時点で既に、クロック周波数がF_CPUと一致していますので、後は何も考えず、いつも通りコードを記述しておけばOKです。

  #include <328duino.h>
  void setup (void)
  {
    Serial.begin (9600);
    Serial.println ("RDY");
  }


 唯一、必要となるのは、328duino.hの#includeだけです。このヘッダファイルは、Arduino IDEで328duinoを選択した場合以外では、悪さをしないようにしてありますので、ユーザは、UNO等で実行する場合にも、#include部分をコメントしたり削除したりする必要はありません。
 なお、クロック周波数を設定するsetSysClock()は、関数ではなく、A328duinoクラスのメソッドとして定義しておきます。

OSCCAL校正テーブルが必要

 この場合、使用する328duinoの個体毎に必ず必要となるのが、クロック周波数fcpuとOSCCALレジスタ設定値の対応表(校正テーブル)です。個体毎に必要ですので、皆様のお手元にあるATmega328を使い、「#9-7 内蔵RC発振器を校正する」に従って、校正テーブルを計測しなければなりません。

 校正テーブルを328duinoクラスに適用する方法は、以下の2種類を用意しました。

 欠点はどっちもどっち、な感じですが、複数のATmega328を使う場合には、2)の解しかありません。

ライブラリの書き換え

 ライブラリの書き換えを選択する場合、まず、ライブラリをインストールします。
 インストールディレクトリ(通常は、<My Documents>\Arduino\libraries\328duino\src)の328duino.cppをテキストエディタで開きます。

  static uint16_t f_osccal_0[ ]=
  {
    // fcpu vs. OSCCAL value table
    // contents: (fcpu[MHz]<<8) + OSCCAL
    //0x089d,  // 8MHz
    //0x0ac6,  // 10MHz
    //0x0ce3,  // 12MHz
    //0x0ef8,  // 14MHz
    //0x0fff,  // 15MHz
    0x0000  // end of table
  };


 先頭付近に、上の様な空のテーブル定義(サンプルをコメントしてある)がありますので、ご自身の校正テーブルを上書きしてください。テーブル値は16ビット符号無整数で、上位8ビットに周波数MHz、下位8ビットにOSCCALレジスタの設定値を記載します。
 周波数は、MHz単位の整数であり、1〜15[MHz]の範囲を指定可能とします(それ以外の値は設定禁止、リザーブ)。
 テーブルは任意のデータ数を指定可能ですので、最後のデータを0x0000としてください。

EEPROMへ校正テーブルを記録

 EEPROMへデータを書き込むには、「#6-8 ATmegaのEEPROMを使ってみる」で作成したプログラム(test_eeprom.ino)が使用可能です。
 書き込みたいATmega328(328duino)へ、test_eeprom.inoをコンパイル&書き込み、シリアルモニタを開いてください。この時点では、「Pro or Pro Mini (3.3V,8MHz)」を使って、コンパイル&書き込んでください。

  書き込みサンプル:【 】内はシリアルモニタからの送信データ
  【wr,000,08,9d,0a,c6,0c,e3,0e,f8,0f,ff,00】
  OK+ EEPROM[0x0]=0x8
  OK+ EEPROM[0x1]=0x9D
  OK+ EEPROM[0x2]=0xA
  OK+ EEPROM[0x3]=0xC6
  OK+ EEPROM[0x4]=0xC
  OK+ EEPROM[0x5]=0xE3
  OK+ EEPROM[0x6]=0xE
  OK+ EEPROM[0x7]=0xF8
  OK+ EEPROM[0x8]=0xF
  OK+ EEPROM[0x9]=0xFF
  OK+ EEPROM[0xA]=0x0
  OK: wrote 11 data

  【dump,0,16】
  OK+  +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f
  0000: 08 9d 0a c6 0c e3 0e f8 0f ff 00 ff ff ff ff ff
  OK: dump 0x0000 ~ 16


 EEPROMへの書き込みを実施しますので、必ず、内蔵RC発振器が8MHz以下となる周波数で実行してください。上のサンプルでは、EEPROMの0x000番地から、校正テーブルを書き込みましたが、必要に応じて、空いているアドレスに変更してください。
 書き込む値は、前節で解説した、f_osccal_0[ ]への記載値と同じ「16ビット符号無整数で、上位8ビットに周波数MHz、下位8ビットにOSCCALレジスタの設定値」です。ビッグエンディアンで書き込みます。上位8ビットデータ(周波数)が0x00または0xffとなったところで、校正テーブルの末尾と認識します。

 次に、EEPROM上の校正テーブルアドレスを328duinoクラスに知らせるため、ユーザ定義マクロを設定する必要があります。手順は以下の通りです。

328duinoクラスライブラリ仕様

 328duinoクラスライブラリの仕様を以下に示します。

 ライブラリは、以下リンクからダウンロードして、インストールしてください。

  328duinoクラスライブラリ:328duino.zip

 インストール方法は以下の通りです。

※後日、328duinoライブラリを328duino(第一形態)クロックアップ版に対応しました。本ページの説明文は変更していませんが、ダウンロードファイル(328duino.zip)は最新版に差し替わっています。機能拡張分の説明については「#10-6 328duinoをクロックアップ(完結編)」を参照してください。

boards.txtに追加する

 Arduino IDEで、328duinoのクロック周波数を選択可能とするため、boards.txtという設定ファイルに追記します。以下の手順で追記してください。

 解る範囲で、boards.txtに追記した設定項目を説明しておきます。

 以上で、Arduino IDEへの組み込みは終了です。「ツールメニュー ⇒ ボード ⇒ 328duino」で、動作周波数を選択してプログラムをコンパイル&書き込みすれば、マクロF_CPUに設定された状態で実行されます。

 コアライブラリは、F_CPUマクロで調整して動作するコードになっているので、クロック周波数を12MHzなど中途半端な値に設定しても、問題なく動作するはずです。F_CPUマクロを使用していないライブラリや関数、プログラムの一部は、16MHzないし8MHzの動作周波数を期待(前提と)していると考えられ、動作しないケースがあります。
 筆者のプログラムでも、HsswSerialクラスライブラリ等はF_CPUを使っているのでOKですが、speak_th.inoなど直接タイマレジスタに定数を書き込んでしまっている様なプログラムは、PWM周期や割り込み周期がずれてしまいます。

※我が事ながら……、UNOをはじめATmega系のArduinoは、16MHzを前提としてプログラムを組んでしまっていたので、ダメですね。F_CPUを使ってコーディングしないと……<(1x_x)>。

 なお、ブートローダにoptibootを設定しましたので、boards.txtの組み込みが終わったら「#9-4 ATmega328のヒューズ書き込み」に従って、ブートローダを再書き込みしておいてください。書き込むブートローダ(ボード)は、「328duino」を選択してください。
 ブートローダを書き込むと、上で設定したヒューズも同時に書き込まれます。「#9-5 ブレッドボードにArduinoっぽい物」では、ブートローダの書き込み後にser_prog.inoを使って、手動で内蔵RC発振器を選択するヒューズ値を書き込みましたが、必要なくなりました。

テスト

 では、いくつかテストしてみましょう。Arduino IDEで「ツールメニュー ⇒ ボード ⇒ 328duino」を選択して、クロック周波数は>RC 8MHzを選択しておいてください。8MHzだと、Pro Mini @3.3V,8MHzと同じ事なので、動いて当然です。筆者は、RC 14MHzでテストをしました。

Lチカ(Blink)

シリアルモニタのテスト

Logiscopeのテスト

 ここからは、Olive+でlogiscope.olvを実行して、波形取得などをテストします。Arduino IDEを閉じるか、シリアルモニタを閉じてから、logiscope.olvを起動してください。波形を取得するので、予め、328duinoのPD2_D2端子と、PD3_OC2B_D3端子をジャンパ線で接続しておいてください(ピン配置はこちら)。

  実験サンプル:【 】内はLogiscopeからの送信データ
  【ana,3,96】
  OK: analogWrite(3, 96)

  【trg,2,r,10000】

ar99_pwmng.png

 さて、D3端子にanalogWrite()関数でデューティ96/255のPWM波形を出力し、その波形をD3端子に接続したD2端子から取得した波形が上図です。

 analogWrite()関数の出力PWMは、カウントクロックがfcpu/64で周期が510カウントなので、fcpu=14MHzではおよそ2,331μsの周期なのですが、上図で周期を計測してみると、約2,041μsと、だいぶずれていました。念のため、STM32でLogiscopeを起動して、STM32から328duinoのD3端子を観測したところ、期待通りのPWMが観測できました。

※筆者が最初に実行した時は、上の様になりました。この後、328duinoクラスライブラリで対応したので、皆様の手元では、正しい結果が観測できるはずです。

 どういうことか?そう。analogWrite()は、正しいPWMを出力していることが、STM32からの観測でわかりましたので、328duinoのLogiscopeが変です。Logiscopeでは、信号の変化時間をmicros()関数で計測しているので、恐らく、micros()関数がF_CPU=14MHzの時に期待しない動作をしていると推測しました。

 これは、millis()関数とmicros()関数の実装(ソースコード)を確認しておく必要があります。

millis()関数とmicros()関数

 コアライブラリのソースを確認してみました。millis()関数もmicros()関数もマクロF_CPUを使っているので、一見良さそうに見えます。しかし、よく吟味してみると、F_CPU[MHz]が2冪数であることを前提としたコードになっていて、2冪数以外の周波数では、誤差が生じていました。

 まず、millis()関数の調査結果を以下表に示します。

F_CPU[MHz] MILLIS_INC FRACT_INC 理想millis millis()関数 error[%]
8 2.048 6.000 2.048 2.048 0.000
10 1.638 79.800 1.638 1.632 −0.391
12 1.365 45.667 1.365 1.360 −0.391
14 1.170 21.286 1.170 1.168 −0.195
15 1.092 11.533 1.092 1.088 −0.391
16 1.024 3.000 1.024 1.024 0.000

 millis()関数が返すミリ秒単位の時間は、8ビットタイマ(TC0)のオーバフロウ割り込みISRでカウントされていて、その中で使用しているのが、MILLIS_INCとFRACT_INCと言う、整数型の定数です。上表では小数点以下まで計算して表示してありますが、コアライブラリでは整数演算なので、小数点以下の数値は切り捨てられています。
 MILLIS_INCとFRACT_INCは、F_CPUを使って計算しているのですが、F_CPUが2冪数MHzであることを前提としたコードになっているため、F_CPU=8MHzや16MHzでは誤差が無いのですが、今回の様にF_CPU=14MHz等の半端な数値に設定してあると、誤差(error)が生じています。

※他に、この解析で判明したことは、上表のMILLIS_INCがmillis()関数の解像度になっていることです。16MHzでは約1msの解像度がありますが、8MHzでは約2msが解像度なので、millis()=2を観測した次に観測可能なのは、millis()=4です。

 MILLIS_INCの小数点以下の部分を、FRACT_INCで回収するコードになっているので、MILLIS_INCの小数点以下が整数型に切り捨てられても、誤差は生じません。FRACT_INCの小数点以下が、整数型に切り捨てられてしまうことで誤差を生じています。

 こちらは、コアライブラリ内ISR処理コードの修正が必要なため、改修は断念しました。ただ、誤差は最大でも0.391%と、大きくはない値なので、この程度の誤差が生じることを前提として、使用可能だと判断しました。

※F_CPU=12MHzの時、millis()関数で1時間(3,600,000ms)を計測すると、約14秒(0.391%)の誤差が生じます。


 次は、micros()関数の調査結果に進みます(以下表)。

F_CPU[MHz] 理想micros micros()関数 error1[%]   _micros()関数 error2[%]
8 8.000 8.000 0.000   8.000 0.000
10 6.400 6.000 −6.250 6.375 −0.391
12 5.333 5.000 −6.250 5.313 −0.391
14 4.571 4.000 −12.500 4.563 −0.195
15 4.267 4.000 −6.250 4.250 −0.391
16 4.000 4.000 0.000 4.000 0.000

 micros()関数も、演算に使用する係数部分を整数に切り捨てて計算しています。millis()関数で言うところのFRACT_INCに当たるような、割り切れない小数点以下の数値を回収する機構が無いため、理想的な場合のmicrosと比べて大きな誤差(error1)が生じてしまいます。1,000μs計測しただけで、60〜120[μs]の誤差が出るという結果ですので、さすがに看過できません。👊

※本来、millis()−micros()/1,000の結果は、一定の値となるべきですが、millis()とmicros()の誤差が異なるため、millis()とmicros()はどんどんずれて行きます。

 そこで、micros()関数を改定して_micros()関数を作ることとします。とは言っても、マイクロ秒オーダの電子制御に使用する_micros()関数に、float演算などの重たい処理をさせるのは避けるべきですので、演算に使う係数部分を8ビットの固定小数値(=4ビット符号無整数.4ビット小数)にして、演算を高速化しました。

※処理の詳細については、328duinoクラスライブラリのソースをご覧ください。8ビット固定小数の掛け算ですが、実は定数のため、GCCの最適化がバリバリ効きまくったみたいで、見た目に比べてかなり高速です。

 結果、上表右側に記した様に、millis()関数と同程度の誤差(error2)まで低下することができました。これなら実用に耐えられるでしょう。
 328duino.hをインクルードすると、「#defnie micros() _micros()」が張られているので、micros()関数の呼び出しに対して、改定版_micros()関数が呼ばれる様になっています。改定版_micros()関数は、少し大目に演算が必要なため、オリジナルmicros()関数に比べると処理速度は遅くなります。

※処理速度の低下(処理時間の増加)は×1〜2倍強程度なので、こちらも許容範囲と考えています。

 なお、改定版_micros()関数をmicros()関数にオーバライドするマクロは、328duino.hをインクルードしたファイル内でのみ有効なため、その他の外部ライブラリ内でmicros()関数を使っている箇所は、誤差の大きなオリジナルmicros()関数が使われてしまうことに留意してください。

 328duinoクラスライブラリを改定後、Logiscopeで取り直したPWM波形が以下図です。

ar99_pwmok.png

 周期約2,328μs程度と、期待(約2,331μs)通りの周期を計測できました。

その他雑談

 「#9-7 内蔵RC発振器を校正する」でも指摘した様に、EEPROMやFlashメモリに対して、消去・書き込みを実行する場合には、クロック周波数を8MHzに戻す必要があります。

  A328d.setSysClock (A328dSscRstrFactry);
  // :
  // : EEPROMへの書き込みコード
  // :
  A328d.setSysClock (F_CPU/1000000);


※読み出しは、クロックアップしたままの周波数でOKです。

 その他、クロック周波数をダイナミックに変更する必要のあるアプリケーションにおいて、シリアルモニタをデバッグに使用したい場合は、以下の様にすると、何とか、使用可能です。

  fcpu_old=A328d.setSysClock (A328dSscQueryCur);
  A328d.setSysClock (fcpu_new);
  Serial.begin (SERIAL_BAUD*(F_CPU/1000000)/fcpu_new);
  // :
  // : クロック周波数fcpu_newでの処理コード
  // :
  A328d.setSysClock (fcpu_old);
  Serial.begin (SERIAL_BAUD*(F_CPU/1000000)/fcpu_old);


 クロックを切り替えた瞬間、あるいはその途中で、送受信が起きるとコケますが、送受信していない状態で上の様に切り替えると、破綻せずにシリアルモニタの通信は追従します。デバッグ情報の送信のみに使うのであれば、問題なく使用可能だと思われます。
 非同期にシリアルモニタから受信してしまう用途では、切り替えた瞬間のデータを取りこぼすか、文字化けを起こしますので、デバッグ表示の送信くらいにしか使えないと思います。


 動的なクロック切り替えには、まだ課題があるように思いますが、内蔵RC発振器を使って、8MHz以上で動作する環境としては、まあ、合格レベルに到達したのではないでしょうか?しかし、コアライブラリが2冪周波数を前提としていたとは……(16MHzの次は32MHzですか?)。


btn_arduino.png

btn_prev.png btn_next.png

home