「AD変換なんて、analogRead()関数で一発」、そうなんですよ、普通はね。
でも、最近使い始めたATtiny2313というMCU(下図)には、AD変換器が搭載されていないので、analogRead()関数は使えないんですね。
CdSセルを使って明るさを検知したり、サーミスタを使って温度を検知したり、何かと使い所のあるAD変換器なので、ATtiny2313でも使いたくなるケースはきっとあると思われます。
以前、UNOの外付け回路としてAD変換器を作った記事がありまして(以下記事を参照)、ATtiny2313にAD変換器が無いのなら、自分で作るという選択肢があるんですよ。
「#8-2 逐次比較型AD変換器を作る」
「#9-2 ΔΣ型AD変換器を作る」
ただ、上の2つの記事で作った外付けの回路はそこそこ規模が大きくて、さすがにそこまでしてATtiny2313でAD変換したいか?って言う事なんです。もう少しお手軽に使えるなら良いのだけど。
そこで今回のテーマですが、ATtiny2313と、その外付けに抵抗×1個、コンデンサ(容量)×1個だけを接続して、AD変換器を実現してみようと思います。
今回は結論を先に言ってしまいます。からくりの種明かしは、その後解説します。
下図がATtiny2313を使って、外付けにAD変換器の部品を作り込んだ回路図となります。
まあ、これでAD変換器を構成することが出来るのですが、それにはソフトウェアでの制御を追加する必要があります。以降、原理からソフトウェアによる制御まで、解説してみたいと思います。
以下図がATtiny2313にAD変換器を構築した時のブレッドボード実装例です。
今回作成するのは逐次比較(SAR)型AD変換器です。原理について詳細は「#8-2 逐次比較型AD変換器を作る」をご覧ください。
以下図が基本的な回路構成です。
必要な回路は、サンプル&ホールド(S&H)回路、比較回路、DA変換器、逐次比較レジスタSAR、です。
以上の様に、PWM型DA変換器とアナログ比較ユニットを使ったAD変換器となります。
ここでも先に結論からご覧に入れます。以下がATtiny2313用のAD変換コードです。
ISR (TIMER1_OVF_vect)
{
// TOI1 interrupt service routine
// check counter
if (TCNT1L>acmp_MAXC)
return;
// analog comparison
acmp_posi=(ACSR&0x20)? true: false;
acmp_ncnt++;
}
int adc4_read (void)
{
// read from 4 bit sar-adc
// return: 4 bit digital value
int idx;
int anlg; // SAR
int alft; // binary search: alft>argt
int argt;
const int N=4; // ADC bits
// clear TC1
noInterrupts ();
TCNT1=0;
interrupts ();
// binary search
alft=(1<<N);
argt=0;
anlg=(alft + argt)/2;
for (idx=0; idx<N; idx++)
{
// dac output
noInterrupts ();
OCR1A=anlg<<(8 - N);
interrupts ();
// stabilization time
acmp_ncnt=-adc4_WAIT;
while (acmp_ncnt<0)
;
// SAR
if (!acmp_posi)
alft=anlg;
else
argt=anlg;
anlg=(alft + argt)/2;
}
// stop pwm
noInterrupts ();
OCR1A=0;
interrupts ();
return (anlg);
}
void setup (void)
{
// initialize
pinMode (pin_AINP, INPUT);
pinMode (pin_AINN, INPUT);
pinMode (pin_DACS, OUTPUT);
// analog comparator setting
ACSR=0x00;
DIDR=0x03;
// TC1 setting
// OC1A pwm output, pwm phase correct 8 bit mode 1, fcpu/1, TOI1 enable
TCCR1A=0x80 + 0x01;
TCCR1B=0x00 + 0x01;
noInterrupts ();
OCR1A=0;
TIMSK |= 0x80;
interrupts ();
}
コード部分の抜粋です。実際に使う時には、いくつかグローバル変数や定数宣言などの部分も必要になりますが、基本的に上コード部分を最終的なプログラム(tiny_adc_4b.ino)から探してコピー&ペーストすれば、ATtiny2313でAD変換を実行することが可能となります。
ご覧の通りそれほど規模の大きなコードではないので、メモリの少ないATtiny2313(Flash 4Kバイト、RAM 128バイト)でも、十分使い物になると思います。
※TOI1割り込みは、AD変換中にしか使わないので、AD変換前後で割り込みを許可/禁止してもOKです。
以下、ポイントとなる部分について個々に解説を加えます。
PWM型DA変換器のアナログ出力は、CR充放電に起因する波形となるのでギザギザです(下図青線)。
PWM周期に同期して、いつも一定のタイミングで比較を実行しないと、精度を向上できません。
PWMタイマ(TC1)のオーバフロウ(TOI1)割り込みでサンプリングすると、以下図の様に、PWM HIGH期間のちょうど真ん中あたりでアナログ比較結果を取り込むことが出来ます。
それが前節で示したコードの「ISR (TIMER1_OVF_vect) { 〜 }」部分です。
AD変換器の変換時間を高速化しようとすると、PWM型DA変換器の出力安定時間を短くする必要があります。出力安定時間はLPFの時定数CRで決まるので、時定数はなるべく小さいことが求められます。PWM周期Tpwmは時定数CRとほぼ一致させて使うので、時定数を小さくする → PWM周期を短くする、ことにつながります。
ATtiny2313 @8MHzの処理能力を考えておかないと、割り込み処理の負荷がMCUの処理速度を超えてしまって、ハングアップしてしまう恐れがあります。
そこで、以下の様なテストコード(#ifdef USE_TINY_SERIAL)を挿入して、TOI1割り込みサービスルーチン(ISR)内の処理時間を計測してみました。
ISR (TIMER1_OVF_vect)
{
// TOI1 interrupt service routine
: (通常処理、省略)
:
#ifdef USE_TINY_SERIAL
dbg_sum += TCNT1;
dbg_cnt++;
#endif // USE_TINY_SERIAL
}
※「#13-10 ATtiny2313とシリアルモニタ」で整備したtinyシリアル関数群とシリアルモニタを使って、計測結果を吸い上げました。
計測の結果、TC1で約41カウント(平均値)程の時間を要していることが判りました。TC1はクロックfcpu/1(fcpu=8MHz)でカウントアップしていますので、41カウント≒5.1μsです。PWM周期(=TOI1割り込み周期)は、255×2=510カウントなので、TOI1 ISRの負荷率は41/510≒8%と、許容範囲でしょう。
※もっと割り込み処理の負荷率が上がっても良いアプリケーションであれば、PWM周期とそれに合わせてPWM型DA変換器の時定数を短くして、変換時間を短縮することもできます。AD変換時間については次節に解説があります。
次回、作成したAD変換器の変換精度も評価しますので、過去に「#8-2 逐次比較型AD変換器を作る」で作成したAD変換器と比較可能な様に、4ビットAD変換器(adc4_read()関数)を作りたいと思います。
二分探索(バイナリサーチ)で探索するので、4ビットAD変換器であれば比較回数は4回となります。原理と処理内容についてはこちらで解説済みです。比較する度に基準電圧となるPWM型DA変換器出力を変化する必要があるため、都度、出力安定時間を待つ必要が生じます。
PWM型DA変換器の出力は、時定数CR時間経過後、最終到達電圧の63.2%位置まで到達します。N×CR時間経過後であれば、K=1−(1−63.2%)N[%]位置まで到達します。これがアナログ比較の基準電圧となるので、K→100%に限りなく近づけることが精度向上の絶対条件です。
とは言っても、1回の変換に何秒もかかる様では使い物にならないので、変換時間との取捨選択です。
N | K |
1 | 63.20% |
2 | 86.46% |
3 | 95.02% |
4 | 98.17% |
5 | 99.33% |
6 | 99.75% |
7 | 99.91% |
8 | 99.97% |
上表の通り、Nがある程度大きくなるとKは飽和してくるので、適切なNで留め置くのが吉です。
PWM型DA変換器では、出力電圧を変更することは、PWMデューティを変更することに帰着します(下コード)。PWMデューティはOCR1Aレジスタで設定するのですが、PWMが周期内で崩れてしまわない様に、OCR1Aレジスタの変更値が動作(PWM周期)に反映されるのは、カウンタが最大値(255)に到達したタイミングとなります。
// dac output
noInterrupts ();
OCR1A=anlg<<(8 - NN);
interrupts ();
従って、Nとしては7を選択し、OCR1Aレジスタの変更が反映されるまでの最大1周期を加算して、DA変換器出力安定時間(adc4_WAIT)としました(↓)。
const int8 adc4_WAIT=8; // adc4: dac wait time [TOI1 times]
// stabilization time
acmp_ncnt=-adc4_WAIT;
while (acmp_ncnt<0)
;
※adc4_WAITの計算式:adc4_WAIT=N×C2×R1/Tpwm+1。時定数C2×R1をPWM周期Tpwmとぴったり合わせずとも、ある程度はこの計算式で合わせ込むことが可能です。
Tpwm≒64μsですので、1回のDA変換出力安定時間はTpwm×adc4_WAIT=512μsです。4ビットAD変換を実施すると、都合4回DA変換器出力を変化させるので、1回のAD変換時間は512μs×4回≒2msとなります。
UNOのanalogRead()関数が約100μs、「#8-2 逐次比較型AD変換器を作る」で作った4ビットAD変換器の変換時間は約400μsでしたので、それらに比べるとちょっと遅いですが、この設計値で進めます。サンプル&ホールド回路が無いので、変換時間中のアナログ量(AIN)変化が無いことが、精度を確保する要件となります。
TOI1割り込み処理内でのアナログ比較サンプリングが前提なので、割り込みを禁止した状態では変換できません。即ち、他のISR内やnoInterrupts()中は使用不可です。……と言うか、使ってしまうと、adc4_read()関数内のinterrupts()関数(上コード参照)によって割り込みが有効化されてしまうため、意図しない動作が発生します。
話の流れとしてはフライングになってしまいますが、この時点で先行評価を実行したところ、絶対精度(AE)の低下が見られました(下図)。
良い感じで綺麗な特性になっているのですが、時々、ノイズの様にポコポコと、−2LSBを超えるような絶対精度(AE)の低下箇所が見られました。
時々、バイチャンスで精度が低下することから、何らかの競合によってアナログ比較タイミングがずれていると推測しました。ATTinyCoreでは、今回設定したTOI1割り込み以外にもmillis()関数等の時間を刻むためにシステムタイマ(TC0)のオーバフロウ(TOI0)割り込みが発生しています。
TOI0割り込みが発生した直後にTOI1割り込みが発生しても、先発のTOI0割り込み処理が終わるまで、TOI1割り込みは保留されるので、アナログ比較タイミングがずれてしまい、精度が低下したのでしょう。
ただ、AD変換中にTOI0割り込みを禁止するだけでは完全には対策できず、AEの低下点が残ってしまいました(発生頻度は減少したので、一因であったことは間違いないみたいです)。どうやら、TOI0割り込み以外にも、TOI1割り込みをディスターブする事象があるらしいです。
そこで追加したのが、TOI1 ISR先頭の「TCNT1L>acmp_MAXC」処理です(↓)。
ISR (TIMER1_OVF_vect)
{
// TOI1 interrupt service routine
// check counter
if (TCNT1L>acmp_MAXC)
return;
// analog comparison
acmp_posi=(ACSR&0x20)? true: false;
acmp_ncnt++;
}
前の節でTOI1 ISRの負荷率を計測した時、ISR内の処理に要する時間が、TC1カウント数で約41カウントでした。もし、ISR処理の先頭でTC1のカウンタTCNT1Lがacmp_MAXC=45カウントを超えていた場合、何らかの処理でディスターブされてサンプリングタイミングを逃してしまった時とみなし、「割り込みが無かったこと」にしてreturnする処理としました。
const uint8 acmp_MAXC=45; // acmp: maximum TCNT1 for TOI1
TC1カウントクロックなど、条件を変更した場合には、再測定して調整が必要なことに留意してください。
この処理を追加した効果については、次回、変換精度評価する時にご確認ください。
4ビットAD変換コードは、前の節で抜粋して示しました。ここでリンクするプログラムには、AD変換器の変換精度を評価するため、ホストとなるArduino(例えばUNO)とI2Cで通信するコードなどが追加されています。
完成版プログラム:tiny_adc_4b.ino
I2Cを使うためにWireライブラリをリンクしてあるので、手元のコンパイル結果では、Flash 1,254バイト、RAM 56バイトとなりました。Wireライブラリの物量を除くと、adc4_read()関数自体は大したコードサイズではないはずです。外部回路もLPF(CR回路)だけなので、意外と使い物になるのではないでしょうか?
回路もプログラムも完成しましたので「終了」なのですが、やっぱり変換精度を評価しておかないと終われません。次回、変換精度を評価して、完結します。