2016年05月01日 | 公開。 |
前のページでは、Arduino M0のanalogRead関数を高速化するためのAdcBooster関数を紹介しました。このページでは、AdcBooster関数が何をしているかを説明します。
しかし、AdcBooster関数の処理内容を理解するには、まずArduinoのコアライブラリ内でA/D変換器がどのように初期化されているかを理解する必要があります。そのため、コアライブラリ内での初期化処理の説明から始めます。
Arduino M0のA/D変換器の初期化は、コアライブラリ内のwiring.cというファイルで行われています。
参考:私はArduino IDE 1.7.10をC:\arduino-1.7.10というフォルダにインストールしていますが、その場合のwiring.cのフルパスはC:\arduino-1.7.10\hardware\arduino\samd\cores\arduino\wiring.cになります。
wiring.cのinit関数内で、A/D変換器の初期化に関する部分を抜き出すと、次の様になります。
// Initialize Analog Controller // Setting clock GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID( GCM_ADC ) | // Generic Clock ADC GCLK_CLKCTRL_GEN_GCLK0 | // Generic Clock Generator 0 is source GCLK_CLKCTRL_CLKEN ; // Setting CTRLB ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV128 | // Divide Clock by 128. ADC_CTRLB_RESSEL_10BIT; // Result on 10 bits ADC->INPUTCTRL.reg = ADC_INPUTCTRL_MUXNEG_GND | // No Negative input (Internal Ground) ADC_INPUTCTRL_GAIN_DIV2; // Gain setted to 1/2 // Averaging (see table 31-2 p.816 datasheet) ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_2 | // 2 samples ADC_AVGCTRL_ADJRES(0x01ul); // Adjusting result by 1 ADC->SAMPCTRL.reg = 0x3f; ADC->REFCTRL.reg = ADC_REFCTRL_REFSEL_INTVCC1; // Reference intvcc1 [default] ADC->CTRLA.bit.ENABLE = 1; // Enable ADC while( ADC->STATUS.bit.SYNCBUSY == 1 ) { // Waiting for synchroinization }
ここで、
ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV128 | // Divide Clock by 128. ADC_CTRLB_RESSEL_10BIT; // Result on 10 bits
の部分では、48MHzのクロックを128分周してA/D変換器のクロックとし、A/D変換器の分解能は10ビットにする設定を行っています。
また
ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_2 | // 2 samples ADC_AVGCTRL_ADJRES(0x01ul); // Adjusting result by 1
の部分では、2回連続でA/D変換を行い、それらの結果の平均値を返すモードに設定しています。(2回の加算平均処理。なおこの処理は、後のコラムで説明する通り、少し問題がある)
また
ADC->SAMPCTRL.reg = 0x3f;
の部分では、(A/D変換器のクロックで)31.5クロック分のウェイトをA/D変換を行う直前に挿入する設定を行っています。
これらのA/D変換器のクロック周波数の設定、加算平均処理の設定、およびウェイトの挿入がA/D変換を遅くする要因になっているため、これらの設定を後から書き換えるのがAdcBooster関数の役割となります。
前のページのリスト1のAdcBooster関数の内部処理について、いくつかの部分に分けて、説明します。
A/D変換器の設定を変えるには、まずA/D変換器を無効化(disable)してから設定を変え、その後、再度有効化(enable)する必要があります。
そのため、AdcBooster関数の最初で、次の様にA/D変換器を無効化しています。
ADC->CTRLA.bit.ENABLE = 0; // Disable ADC while( ADC->STATUS.bit.SYNCBUSY == 1 ); // Wait for synchronization
またAdcBooster関数の最後では、次の様にA/D変換器を有効化しています。
ADC->CTRLA.bit.ENABLE = 1; // Enable ADC while( ADC->STATUS.bit.SYNCBUSY == 1 ); // Wait for synchronization
wiring.cでは、48MHzのクロックを128分周して、A/D変換器のクロックにする設定にしてありました。すなわち、A/D変換器のクロック周波数は375kHzとなっていました。
しかしSAMD21のデータシートによると、A/D変換器のクロック周波数(fCLK_ADC)の上限は2100kHzとなっています。そこで、fCLK_ADCをもっと上げればA/D変換が高速化できそうだと分かります。
48MHzを32分周した場合にfCLK_ADCが1500kHzになりますから、理屈の上では、ここまでは周波数を上げられそうです。しかしながら、1500kHzで実験すると、入力範囲の上限(3.3V)に近い電圧を入力した場合に、正しくA/D変換が行えませんでした。この現象の原因は、今のところ不明です。
そこで周波数を1段落とし、48MHzを64分周した750kHzに設定すると、正常にA/D変換できるようになりました。
このクロック周波数の設定を行っているのが次の部分です。
ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV64 | // Divide Clock by 64. ADC_CTRLB_RESSEL_10BIT; // Result on 10 bits
wiring.cでは、A/D変換器を2回の加算平均処理を行う設定にしてありますが、この場合、A/D変換の精度が少し上がるのと引き換えに、A/D変換の時間が倍になってしまいます。そこで、AdcBooster関数では、次の様に、加算平均処理をしない設定にしました。
ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 | // 1 sample ADC_AVGCTRL_ADJRES(0x00ul); // Adjusting result by 0
wiring.cでは、A/D変換の前に31.5クロックものウェイトを挿入する設定になっています。A/D変換そのものには6クロック(10ビット変換時)しかかかりませんから、かなり長いウェイトです。
31.5クロックというのは、挿入できるウェイトの上限ですから、あまり効果を検討せずに、一番保守的な設定にしたのだと思います。
このウェイトは、fCLK_ADCを高い周波数に設定した場合に、サンプル&ホールドの時間が不足するのを補うために、おそらく設けられています。(他にもfree runningモードで連続変換を行う場合の、サンプリング周波数の微調整にも使えるはず)
実験したところでは、このウェイトをなくしても、変換精度に悪影響がありませんでした。
そこで、AdcBooster関数では、次の様に、ウェイトを0に設定しています。
ADC->SAMPCTRL.reg = 0x00; // Sampling Time Length = 0
もともとは2回の加算平均処理を行っていたのに、AdcBooster関数では、高速化のために加算平均処理を無効にしました。その結果、どの程度変換誤差が増えるかを、実験により評価しました。
A/D変換の結果をパソコンのテキストファイルに取り込むために、リスト5に示すスケッチを作成しました。
#ifdef ARDUINO_ARCH_SAMD #define ser SerialUSB // Arduino M0/M0 Pro #else #define ser Serial // other than Arduino M0/M0 Pro #endif void AdcBooster() // AdcBooster is a dummy function when running on a machine other than Arduino M0/M0 Pro { #ifdef ARDUINO_ARCH_SAMD ADC->CTRLA.bit.ENABLE = 0; // Disable ADC while( ADC->STATUS.bit.SYNCBUSY == 1 ); // Wait for synchronization ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV64 | // Divide Clock by 64. ADC_CTRLB_RESSEL_10BIT; // Result on 10 bits ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 | // 1 sample ADC_AVGCTRL_ADJRES(0x00ul); // Adjusting result by 0 ADC->SAMPCTRL.reg = 0x00; // Sampling Time Length = 0 ADC->CTRLA.bit.ENABLE = 1; // Enable ADC while( ADC->STATUS.bit.SYNCBUSY == 1 ); // Wait for synchronization #endif } // AdcBooster void setup() { AdcBooster(); while(!ser); ser.begin(9600); } void loop() { ser.println(analogRead(A0)); delay(10); }
リスト5をArduino M0で実行すると、AdcBooster関数を呼び出した上で、A0ピンの電圧をanalogRead関数でA/D変換結果を読み出し、それをUSBシリアル経由でパソコンに送信し続けます。
一定電圧をA0ピンに加え、A/D変換結果をパソコンで処理すれば、数値のばらつきの度合いを調べる事により、A/D変換結果の偶然誤差(非繰り返し性の誤差)の発生の程度が評価できます。
注:この方法では、系統誤差(繰り返し発生する誤差、すなわちゲイン誤差やオフセット誤差、非直線性誤差など)は評価できません。また、加算平均処理には偶然誤差を減らす効果はありますが、系統誤差を減らす効果はありません。
A0ピンに加える電圧は、変換範囲下限の0V付近や、変換範囲上限の3.3V付近は避ける必要があります。これらの電圧に設定すると、A/D変換器が飽和し、誤差が正常に評価できない可能性が高いからです。
今回はA0ピンに加える電圧を発生するのに、I/Oピン一つで読める4X5キーパッドを利用しました。このキーパッドは押すキーによって、色々な電圧を発生できるものです。出力電圧は、電源電圧を抵抗分圧回路により分圧する事により発生します。
商品名 | I/Oピン一つで読める4X5キーパッドキット | |
税抜き小売価格 | 2400円 | |
販売店 | スイッチサイエンス | |
サポートページ | I/Oピン一つで読める4X5キーパッドキットサポートページ |
キーパッドのSW11を押すと、キーパッドの出力に電源電圧の約半分(正確には0.4986倍)の電圧が発生します。10ビットでA/D変換すると510.6(量子化を考慮しない値)の変換結果が返ってくる事が期待できます。
表3に示す様にArduino M0と4X5キーパッドを接続し、SW11を押した状態でリスト5のスケッチを動作させ、A/D変換結果をTeraTermのログ機能を使ってテキストファイルに変換します。
Arduino M0の端子 | 4X5キーパッドの端子 |
---|---|
3.3V (5V系のArduinoで実験する場合は5V端子) |
VCC |
GND | GND |
A0 | OUTPUT |
実行した時のTeraTermの画面を図2に示します。この様に、A/D変換結果が508~512位の範囲でばらついています。
リスト5のsetup関数の最初の行で、AdcBooster関数を呼び出していますが、この行を削除するかコメントアウトすれば、Arduino M0本来のanalogRead関数を用いてA/D変換した結果を評価できます。
AdcBooster関数を使わない場合のTeraTermの画面を図3に示します。A/D変換結果が513~516位の範囲でばらついていますが、図2と比較してばらつきが減ったのかどうかが判断しにくいです。
そこで、A/D変換結果を1000点Excelで読み込み、平均値と標準偏差を計算してみました。計算結果を表4に示します。なお、標準偏差を求めるのにはstdev.s関数を使用しました。
AdcBooster関数の 呼び出しの有無 |
A/D変換値の 平均 |
A/D変換値の 標準偏差 |
---|---|---|
呼び出しあり (加算平均処理なし) |
510.97 | 1.17 |
呼び出しなし (加算平均処理あり) |
515.15 | 0.96 |
AdcBooster関数を呼び出してanalogRead関数を高速化すると、A/D変換結果の標準偏差が0.96から1.17へと、1.22倍に悪化しています。標準偏差は、数値のばらつきの度合いを表していますので、ばらつきが1.22倍になった事になります。
信号理論によれば、誤差が量子化誤差を含めてランダムに発生すると仮定できるとき、2回の加算平均処理により誤差を1/√2倍に低減できます。逆に言えば、2回の加算平均処理をしている場合を基準にすると、加算平均処理をしない場合は誤差が√2倍になります。
表4の結果からは、A/D変換値の標準偏差が1.22倍になっていますので、信号理論により予測される1.41倍(√2倍)ほどには変換誤差が悪化していない事になります。
この結果から、私は高速化のメリットの方が、変換誤差増加のデメリットより大きいと感じます。しかし感じ方には個人差がありますし、A/D変換を何に応用するかにもよりますので、一概には言えないのかも知れません。
参考:余談になりますが、リスト5をArduino Unoで実行した結果、1000回のA/D変換結果の平均値が511.97、標準偏差が0.17となりました。加算平均処理をしなくても、Arduino Unoは圧倒的にA/D変換結果のばらつきが小さくなります。入力した電圧と、A/D変換のコード間の閾値の関係によっても標準偏差の出方は変わってきますから、一つの入力電圧ではなく色々な入力電圧で評価すべきですし、フルスケールの電圧がArduino Unoは5Vと、Arduino M0の3.3Vよりも高い事を考慮する必要もありますが、A/D変換の繰り返し精度(同じ電圧を繰り返し測った時の測定値のばらつきが少なさ)は、Arduino Unoの方がArduino M0よりも優れているといっても良さそうです。
AdcBooster関数の呼び出しの有無によりA/D変換値の平均に差があるのが気になりますが、これに関しては後のコラムで書きます。
Arduino M0は32ビットの高性能マイコンを搭載しているために、昔に開発された8ビットマイコンであるArduino Unoよりも、全ての面で優れていると錯覚してしまいます。しかしながら、何も手を加えない状態では、Arduino UnoよりもA/D変換が低速です。
この記事では、AdcBooster関数を使う事により、Arduino M0のA/D変換の速度を大幅に改善できる事を紹介しました。
また、A/D変換の精度について調べていく内に、Arduino M0は、Arduino Unoよりも、A/D変換の繰り返し精度が悪い(ノイズが多い)事が明らかになりました。この原因についてははっきりとは分りませんが、基板レイアウトに問題があり、ノイズを拾いやすくなっている可能性もあります。
SAMD21マイコンの加算平均処理機能を活用する事により、変換速度を犠牲にすることで、繰り返し精度を向上させる(ノイズを減らす)事もできるのですが、これについては、時間があれば、他の記事で説明したいと思います。
それにしても、2010年に発売になったArduino Unoが、2015年に発売のArduino M0よりも、A/D変換の性能で上回っている事には驚かされます。必ずしも新しいものが優れているとは限らない、一つの例だと思います。
Arduino Unoをはじめとする8ビットArduinoが、最新機種より優れている点は他にもあります。これについては、機会があれば、他の記事で、まとめて紹介したいと思います。
コアライブラリのwiring.c内で、2回の加算平均処理を行うようにA/D変換器の動作モードを設定している事は説明しましたが、これには問題があります。
SAMDのデータシート(Atmel-42181I-SAM L21_Datasheet_Complete-03/2016)の加算平均処理の説明の中で、次の様な注意書きがあります。
Note: To perform the averaging of two or more samples, the Conversion Result Resolution field in the Control B register (CTRLB.RESSEL) must be set to '1'.
訳:2つ以上のサンプルの加算平均を行う場合、Control BレジスタのConversion Result Resolutionフィールド(CTRLB.RESSEL)は1にセットしなければならない。
このCTRLBレジスタのRESSELというフィールドは、A/D変換の精度(ビット数)を表すフィールドです。データシートでこのフィールドの説明の部分を抜き出したのが図4です。この表にも、RESSELフィールドを1に設定すると加算平均モードになると書いてあります。
ところがwiring.cでは、次の様に、10ビットでA/D変換をするモード(CTRLB.RESSEL=2)に設定してしまっています。
ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_2 | // 2 samples ADC_AVGCTRL_ADJRES(0x01ul); // Adjusting result by 1
つまり、データシートの注意書きに従っていないコーディングになっています。
加算平均処理をする場合は、A/D変換を12ビット精度で行い、結果を16ビットで返す事になっているのですが、Arduino M0/M0 Proのコアライブラリでは10ビット精度でA/D変換を行い、それを加算平均しています。
10ビット精度の変換で加算平均処理を行うことは、データシート上は動作が保証されていないのですが、実機で試したところ、うまく動作している様に見えます。なまじ動いてしまうので、不具合が表面化せずに残っているものと思われます。しかしメーカーが動作保証していない以上、何らかの条件でこの問題が表面化する可能性もあります。
表4を見ると、加算平均処理の有無でA/D変換結果の標準偏差が変わるだけではなく、平均値も変わっている事に気が付きます。これは理屈に合わないので、もう少し詳しく調べる事にしました。
表4の実験ではキーパッドのSW11を押した場合についてのみ実験を行いましたが、ここではSW1~20を押した場合全てと、スイッチを押さなかった場合について、AdcBooster関数の呼び出しの有無でA/D変換値の平均にどの様な差が出るかを測定しました。結果を表5に示します。
押したスイッチ | AdcBooster関数を呼び出した場合のA/D変換値の平均 | AdcBooster関数を呼び出さなかった場合のA/D変換値の平均 | キーパッドの出力電圧 [V] |
キーパッドの出力電圧から計算したA/D変換値の理論値 |
---|---|---|---|---|
SW1 | 0.27 (理論値+0.21) |
3.71 (理論値+3.65) |
0.0002 | 0.06 |
SW2 | 24.85 (理論値-0.21) |
28.30 (理論値+3.24) |
0.0820 | 25.06 |
SW3 | 53.22 (理論値-0.29) |
56.69 (理論値+3.18) |
0.1751 | 53.51 |
SW4 | 87.82 (理論値-0.65) |
91.34 (理論値+2.87) |
0.2895 | 88.47 |
SW5 | 132.34 (理論値-0.95) |
136.21 (理論値+2.92) |
0.4362 | 133.29 |
SW6 | 181.36 (理論値-1.07) |
185.09 (理論値+2.66) |
0.597 | 182.43 |
SW7 | 239.78 (理論値-1.32) |
243.80 (理論値+2.70) |
0.789 | 241.10 |
SW8 | 299.60 (理論値-1.70) |
305.09 (理論値+3.79) |
0.986 | 301.30 |
SW9 | 365.29 (理論値-1.41) |
369.36 (理論値+2.66) |
1.200 | 366.70 |
SW10 | 437.33 (理論値-0.57) |
440.97 (理論値+3.07) |
1.433 | 437.90 |
SW11 | 510.97 (理論値-1.18) |
515.15 (理論値+3.00) |
1.676 | 512.15 |
SW12 | 584.70 (理論値-0.49) |
588.75 (理論値+3.56) |
1.915 | 585.19 |
SW13 | 656.45 (理論値+0.37) |
660.48 (理論値+4.40) |
2.147 | 656.08 |
SW14 | 721.74 (理論値-0.04) |
726.34 (理論値+4.56) |
2.362 | 721.78 |
SW15 | 784.54 (理論値-0.80) |
789.43 (理論値+4.09) |
2.570 | 785.34 |
SW16 | 840.32 (理論値-0.33) |
845.41 (理論値+4.76) |
2.751 | 840.65 |
SW17 | 889.94 (理論値-0.83) |
895.35 (理論値+4.58) |
2.915 | 890.77 |
SW18 | 932.73 (理論値-1.12) |
938.22 (理論値+4.37) |
3.056 | 933.85 |
SW19 | 967.06 (理論値-1.63) |
972.81 (理論値+4.12) |
3.170 | 968.69 |
SW20 | 996.72 (理論値-1.92) |
1002.71 (理論値+4.07) |
3.268 | 998.64 |
なし | 1021.25 (理論値-2.44) |
1022.96 (理論値-0.73) |
3.350 | 1023.69 |
注1:キーパッドの出力電圧はDMM(ディジタルマルチメータ)により測定した。また、A/D変換値の理論値は、キーパッドの出力電圧を、Arduino M0の3.3VラインのDMMによる電圧実測値(3.351V)で割り、さらに1024を掛けて求めた。
注2:使用したDMMはLinkmanのLDM-86D。6000カウントのDMMで、使用した600mVレンジおよび6Vレンジの精度は、共に±(0.5%+4カウント)。
注3:SW1を押した場合は、A/D変換器の飽和処理により、A/D変換値の平均が大きく出やすい。またスイッチを押さない場合は、A/D変換器の飽和処理により、A/D変換値の平均が小さく出やすい。よって、これらの条件での測定結果は、参考程度に見る方が良い。
注4:キーパッドの出力電圧を測定し、そこからA/D変換値の理論値を求めたので、この測定ではキーパッドの分圧抵抗の誤差(5%)が、A/D変換値の理論値と実測値との差に影響しない条件で測定を行っている。
表5において、実測したA/D変換値の平均と理論値との差に注目してください。AdcBooster関数を呼び出さない場合は、A/D変換値が理論値より0~2LSBほど低く出ている事が分かります。一方で、AdcBooster関数を呼び出すと、A/D変換値が理論値より2~5LSBほど高く出ている事が分かります。
原因はわからないものの、AdcBooster関数を呼ぶ方が、測定誤差は少ないようです。ただし1台のArduino M0を測定しただけでは、他のArduino M0でもAdcBooster関数を呼ぶ方が測定精度が高いと結論付ける事はできません。ただ、控えめに見ても、AdcBooster関数を呼ぶことによりA/D変換の誤差が増加する心配は、なさそうです。
商品名 | I/Oピン一つで読める4X5キーパッドキット | |
税抜き小売価格 | 2400円 | |
販売店 | スイッチサイエンス | |
サポートページ | I/Oピン一つで読める4X5キーパッドキットサポートページ |
商品名 | I/Oピン一つで読める4X4キーパッドキット | |
税抜き小売価格 | 900円 | |
販売店 | スイッチサイエンス | |
サポートページ | I/Oピン一つで読める4X4キーパッドキットサポートページ |
商品名 | I/Oピン一つで読める4X4キーパッド(完成品) | |
税抜き小売価格 | 1380円 | |
販売店 | スイッチサイエンス | |
サポートページ | I/Oピン一つで読める4X4キーパッド(完成品)サポートページ |