「#8-2 逐次比較型AD変換器を作る」の記事で、逐次比較(SAR)型AD変換器を手作りしました。今回は、ΔΣ型AD変換器の手作りに挑戦です。
本来、全ての回路を外部回路で作成しようとすると、サンプリングF/Fやクロック、デジタルフィルタ回路などを構成する必要があるのですが、それは、部品も足りないし、しんどいので、一部をArduinoのポート機能と、ソフトウェア処理で代替えして構成します。
今回も、実用にするというよりは、原理の学習用と言う位置づけです。
「とは?」って、解説できるほど詳しいわけでは無いので、教科書程度の知識で実験を進めることをあらかじめ明記しておきます。ブロック図は以下の様になります。
逐次比較型AD変換器の場合には、NビットAD変換器を作るために、NビットのDA変換器が必要でした。ΔΣ型では、DA変換器は1ビットでOKです。1ビットなので、DA変換器出力は0Vまたは5Vの2択となり、通常のデジタルポートで十分となります。
まず、入力されたアナログ電圧と、1ビットDA変換器出力を比較します。ソフトウェアでもそうですが、比較 ⇒ 減算して正/負を確かめること、ですので、減算回路でアナログ電圧の引き算を実行します。数学では、増分・差分の事をデルタ(Δ)と呼ぶことが多いので、ここの減算回路の事をΔ回路と呼んでいます。
減算した結果は、1ビットDA変換器出力との差分 → 誤差なので、誤差を積分回路で蓄積して行きます。同様に、ここがΣ回路と呼ばれます。
誤差(Δ回路出力)を蓄積(Σ回路出力)した結果を、VREF/2と比較して、VREF/2より大きければ1、小さければ0を出力する二値化回路を通して、デジタル値を取り出すことが可能です。
出力されたデジタル値は、その瞬間の1ビットAD変換値となります。これを、遅延回路(F/F)でラッチして、次の一周期間に1ビットDA変換器から出力するアナログ値とします。以上で、一連のループが完成です。
※コンピュータグラフィックスの減色処理と言うのがあって、今でもプリンタなどに用いられている手法なのだけど、その中に「誤差拡散法」と言うのがあります。ΔΣ型AD変換器のアルゴリズムは、誤差拡散法に似ています。
出力の"1"(または"0")の密度が入力アナログ電圧値に対応します。"1"の密度が高い("0"の密度が低い)とアナログ電圧が高く、"1"の密度が低い("0"の密度が高い)とアナログ電圧が低い、と言った具合です。
通常は、出力信号列にデジタルフィルタを通すことで、Nビットのデジタル値を取り出します。
このAD変換器の特徴は、(教科書からの受け売りですが、)以下の通りです。
今回の実装では、サンプリング&DA変換器出力への反映部分がArduinoのソフトウェア処理でかつ、割り込みによる制御と、非常に遅いため、そもそもの利点が全く生かせないことには、留意しておいてください。仕組みの理解と動作原理についての学習用と言う位置づけなので、割り切ります。
前節で示したブロック図の各要素について、具体的に落とし込みながら、ΔΣ型AD変換器の目標仕様を以下の通りまとめておきます。
次の節では、回路の構成に必要なオペアンプの演算回路について考察を進めます。
オペアンプの回路動作(入出力電圧の関係式)を考察するに当たって、具体的な回路が必要なので、ちょっとフライングですが、今回の回路を下図に示します。
後ほど、この回路を使って、動作を確認するので、LTspiceでエントリしてあります。
図中、上部左側の回路が加算回路です。抵抗R1を流れる電流をI1、R2を流れる電流をI2、Rfを流れる電流をIfと置き、回路図中の記号を使って関係式を立てて、解いていきます。
Vbをバイアス(あるいは0値)として、信号成分(Vdac−Vb)と(AIN−Vb)が加算され、かつ、符号が反転する、という関係式が導出されました。また、この回路は単純な加算回路ではなくて、加算対象の入力電圧に、各々、係数Rf/R1またはRf/R2がかかります。今回は、Rf/R1=Rf/R2=1/2に設定して、加算結果がオーバフロウしないようにして使います。
図中、上部右側の回路が積分回路です。抵抗Rsを流れる電流をIsと置き、回路図中の記号を使って関係式を立てて、解いていきます。
Vbをバイアスとして、信号成分(Vdlt−Vb)が積分され、かつ、符号反転する、と言う関係式が導出されました。
この式は、Vdltが一定と仮定して、Vsgm=Vbからスタートすると、積分し続けてCf×Rs時間が経過時、Vsgm=Vb−Vdltに到達することを示しています。よって、Cf×Rsの定数は非常に重要で、大きすぎると中々電圧が収束しないし、小さすぎるとすぐに振り切ってオーバフロウしてしまいます。
今回は、一定時間毎にサンプリングするので、サンプリング周期と時定数Cf×Rfの値を揃えました。具体的には、サンプリング周期を200μsに設定する(予定、現段階では(1^^;)。後ほど考察して、決定します)ので、Cf×Rs=200μsとなるように、Cf=0.1μF、Rs=2kΩとしました。完成版プログラム(test_adc_ds.ino)のサンプリングレートを変更した場合には、積分回路の回路定数(Cf×Rs)もセットで変更してください。
筆者の環境では、アナログ回路のデバッグを実機で実施するのはかなり厳しいので、まずはLTspiceでシミュレーションして、動作を確認しておきます。先ほど提示済みですが、改めて、シミュレーション用回路を以下に示します。
ポイントは、次に述べる通りです。
では、シミュレーションしてみましょう。
上段の緑線がデジタル出力(DO)です。この時点では、1ビットのデジタル出力なので、"1"の密度が入力アナログ値を表しています。下段入力電圧(AIN)の高/低と、DO="1"の密度を見比べてみてください。
下段の緑色正弦波形が入力アナログ電圧(AIN)で、この値をAD変換している様子を示しています。青線はΔ回路の出力(Vdlt)、赤線がΣ回路の出力Vsgmです。……「どこがどう」と、具体的には説明しにくいのですが、期待通りに動作しています(1^^;)っ。
※デジタル出力DOとアナログ入力AINの関係は、入出力が逆転してはいますが、昔「#5-6 正弦波を生成する」で使った、PWMと正弦波出力の関係に似ていると思いませんか?恐らく、DOをローパスフィルタ(LPF、CR回路)に通すと、充放電のギザギザ波形になりますが、正弦波が再現されると思われます。
動作が確認できたので、今度は、Arduino UNOを意識した、実装回路を起こしておきます(下図)。
ポイントは以下の通りです。
また、「#8-2 逐次比較型AD変換器を作る」と同様に、12ビットDA変換器(MCP4725)を使って、ΔΣ型AD変換器の精度評価を実施しますので、以下ネットリストの通り、MCP4725を接続してください。
Arduino UNO | 接続 | MCP4725 | 接続 | ΔΣ型AD変換器 |
5V | → | VCC | ||
GND | ⇔ | GND | ||
A4(SDA) | ⇔ | SDA | ||
A5(SCL) | ⇔ | SCL | ||
OUT | → | AIN |
※MCP4725は、変換精度評価用です。ΔΣ型AD変換器の一部ではありません。
ΔΣ型AD変換器の一部は、手持ちの部品を使って外部回路として構成するのが困難なため、UNOのI/Oポートとソフトウェアを使って構成します。Arduinoで実現する必要のある回路は、サンプリング回路、1ビットDA変換器、デジタルフィルタの3つです。
以下、各ブロックごとに解説を進めます。
サンプリングは本来、適切なクロックとF/Fでサンプリングするので、ソフトウェアで実現するにも、一定期間毎にサンプリングしてください。そのため、8ビットタイマ(TC2)の割り込みを使って、一定期間毎に割り込みを発生させ、割り込みサービスルーチン(ISR)内で、必要な処理を実行することにします。
TC2のレジスタ設定を以下に示します。
レジスタ | アドレス | 設定値 | 備考 |
TCCR2A | 0x00b0 | 0x03 | fast pwm mode (OCR2A),OC2A/B=normal port |
TCCR2B | 0x00b1 | 0x0b | fast pwm mode (OCR2A),fcpu/32 |
OCR2A | 0x00b3 | 0x63 |
サンプリング周期に従って設定 サンプリング周期=(0x63+1)×2μs=200μs |
TIMSK2 | 0x0070 | 0x01 | TOIE2 enable |
データのサンプリングは、通常のデジタル信号なので、普通にdigitalRead()します。
サンプリングしたデータは、そのままだと、その瞬間の1ビットAD変換値でしかありません。NビットのAD変換値として読み出すためには、一定期間、1ビットAD変換値を読み取って、デジタルフィルタにかける必要があります。後ほど説明する、今回採用したデジタルフィルタでは、NビットのAD変換値を求めるのに2N個のサンプリングデータを保持する必要があります。
従って、2N個のバッファに、各サンプリング値(0または1)を保持することとします。今回は、N=4を想定しているので、バッファ数は16個で済みますが、N=8ビットなら256個、10ビットなら1,024個のバッファが必要となります。ただし、バッファは1個当たり1ビットで良いので、少々複雑なビット演算が必要となりますが、ビット単位のリングバッファ制御によって、記憶域を削減しています。
以下、ビット単位リングバッファ処理コードの一部抜粋です。
const int adc_BITS=4; // adc: data bits
const int adc_MAX1=(1<<adc_BITS); // adc: ring buffer size (bit)
const int adc_MAX2=(adc_MAX1 + 7)/8; // adc: ring buffer size (byte)
static int adc_ptr0; // adc: pointer to ring buffer (count)
static uint8 adc_ptr1; // adc: pointer to ring buffer (bit)
static int adc_ptr2; // adc: pointer to ring buffer (byte)
static uint8 adc_ring[adc_MAX2]; // adc: ring buffer
================================
// 最古値読み出し
oldo=(adc_ring[adc_ptr2]&adc_ptr1)? HIGH: LOW;
================================
// 最新値書き込み
adc_ring[adc_ptr2] ^= adc_ptr1;
================================
// update pointers
adc_ptr0++;
if (adc_ptr0>=adc_MAX1)
{
adc_ptr0=0;
adc_ptr1=0x01;
adc_ptr2=0;
}
else
{
adc_ptr1<<=1;
if (adc_ptr1==0)
{
adc_ptr1=0x01;
adc_ptr2++;
}
}
サンプリングレートを上げ過ぎると、ISRの処理中に次の割り込みフラグが立ってしまい、処理が間に合わなくなってしまう場合が想定されます。そのようなケースでは正しいAD変換値は取得できないので、ISRを抜ける前に割り込みフラグをチェックして、もし、フラグが立っていた場合には、エラーフラグを立てます。
// check interrupt flag
if (TIFR2&0x01)
{
// ERROR: interrupt interval too short
adc_err=ErrAdc;
}
#ifdef DEBUG_ISR
if (dbg_tcnt2<TCNT2)
dbg_tcnt2=TCNT2;
#endif // DEBUG_ISR
最後のDEBUG_ISRは、ISR内の処理時間を知るためのデバッグ用コードです。割り込みがかかると、TC2のカウンタTCNT2は0にリセットされて、カウントを続けます。ISRを抜ける時にTCNT2を参照すると、ISR内の処理にかかった時間を知ることができます。TC2は、fcpu/32のクロックでカウントする設定にしたので、1カウント当たり2μsです。
後ほど実験する時に、ISR実行時間の実力値を測定してみます。
目標仕様の所で述べた通り、Δ回路の減算回路が加算回路になっているので、1ビットDA変換器出力の符号を反転して出力する必要があります。1ビットDA変換器の「符号反転」とは、単に、0 → 1/1 → 0と反転して出力するだけのことです。
1ビットDA変換器出力を変化すると、その変化がΔ回路 → Σ回路 → 二値化回路と伝搬して行く時間が必要なので、ISRの先頭で、出力データのサンプリング直後にDA変換器出力を変化させ、少しでも伝搬時間を稼ぎます。
// sampling and 1 bit dac control
cmpo=digitalRead (pin_CMPO);
digitalWrite (pin_DACO,(cmpo ^ 0x01));
cmpoはint型なので、0x01とEOR(^)演算することで、0/1の反転を実行しています。
サンプリングの所で解説したリングバッファ値を使って、デジタルフィルタを構成し、NビットのAD変換値を計算します。
以下、デジタルフィルタ処理部分のコードとなります。
// update adc data
oldo=(adc_ring[adc_ptr2]&adc_ptr1)? HIGH: LOW;
if (cmpo!=oldo)
{
adc_cnt1=adc_cnt1 - oldo + cmpo;
adc_ring[adc_ptr2] ^= adc_ptr1;
}
adc_data=(adc_data + adc_cnt1)/2;
このAD変換器は、リングバッファの段数を増やしさえすれば、原理的には何ビットにでもすることができます。ただし、ビット数を増やすと、その分、リングバッファにデータが蓄積されるまでの時間が伸びるため、入力アナログ値の変化に対する応答時間が長くなります。従って実用上は、解像度(ビット数)と変換スピードの取捨選択で、適切な落とし所に落ち着かせる必要があるでしょう。
この特徴から、ΔΣ型AD変換器は一般的に、入力信号をセレクトしながら複数チャンネルのAD変換を行う用途には適しません。
今回は、「#8-2 逐次比較型AD変換器を作る」で作った4ビット逐次比較型AD変換器と比較してみようと思っていますので、ビット数N=4を想定しています。
※リングバッファに必要となるメモリと、入力アナログ電圧変化に対する応答時間が許されるなら、N=10ビットなどに設定する事も可能なコードにしてあります。解像度を上げたとて、変換精度がついて来ないので、無意味かもしれませんが……。
最後に、AD変換データ(デジタル値)を読み出す関数について解説しておきます。
変換データ処理(デジタルフィルタ等)は、全て割り込みサービスルーチン(ISR)内で終了しているため、基本的には、グローバル変数adc_dataにアクセスすれば、いつでも、その時点のAD変換データを読み出すことが可能です。
しかし、adc_dataは、ISR内で非同期に更新されるint型(16ビット)変数なので、8ビットMCUであるATmega328が、上位データバイトと下位データバイトにアクセスする間で、割り込みが発生してadc_dataが更新されてしまうと、読み出しデータが不整合を起こしてしまいます。
この問題を回避するため、AD変換データを読み出すadc_read()関数は、一時、割り込みを禁止して変換データ格納変数adc_dataにアクセスしてください。
int adc_read (void)
{
// read from N bit ds-adc
// return: N bit digital value
int adat;
// disable interrupt to read adc_data and adc_err
noInterrupts ();
if (adc_err!=ErrNo)
{
// ERROR:
adat=adc_err;
adc_err=ErrNo;
}
else
{
// success:
adat=adc_data;
}
interrupts ();
return (adat);
}
N=4で固定なら、adc_dataをint型では無く、unit8型などにしておくと、割り込み禁止せずに読み出し可能だったのですが、N>8にも設定可能なコードにしてあるため、上の様な処理が必要となります。……無駄な労力だったか?
ソフトウェア/ハードウェアが整いましたので、いよいよ実験です。まずは、完成したプログラムをリンクしておきます。
完成プログラム:test_adc_ds.ino
コンパイルしてUNOに書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。
実験中の様子を以下図に示します。
実験サンプル:【 】内はシリアルモニタからの送信データ
【daco,0】
OK: DAC=0(0[mV]) ADC=0(0[mV]) 0.000[LSB]
【daco,2048】
OK: DAC=2048(2500[mV]) ADC=7(2187[mV]) -1.001[LSB]
【daco,4095】
OK: DAC=4095(4998[mV]) ADC=15(4687[mV]) -0.995[LSB]
3点ほど、適切に電圧を発生して、変換させてみました。これだけ見ると、一応、AD変換器として機能しているようです。後ほど、ちゃんとした変換精度を評価してみます。
【who】
DBG: tcnt2=13 /100
OK: test_adc_ds ver.1.00
DEBUG_ISRを#defineしてあれば、"DBG:"の行が表示されます。数値は、ISRを抜ける時点のTCNT2カウンタ値と、スラッシュ(/)の後ろの数値は、OCR2Aの値、すなわち、割り込み周期です。TCNT2<<OCR2A(十分小さい)なら、OKです。
TC2のカウントクロックはfcpu/32=500kHzを設定してあるので、1カウントの周期は2μsとなります。TCNT2=13と言うことは、ISR処理が13×2μs=26μsかかっているということです。割り込み以外の処理にも、CPUタイムを確保する必要があるのですが、それを無視して目いっぱい頑張れば、割り込み周期を50μs程度までは上げられる(上限)と推定できます。
今回は、以下に実行する精度評価の結果、サンプリングレートを上げ過ぎると、逆に変換精度が低下する現象が見られたため、サンプリングレートを200μsに留めてあります。LM358のスピードがあんまり速くないので、それが原因だろうと推察しています。
※ただ、アナログ演算回路3段(Δ回路 → Σ回路 → 二値化回路)分であれば、100μsもあったら十分な気がします。何か他に原因があるのかもしれません……。
変換精度を評価して行きましょう。「#8-2 逐次比較型AD変換器を作る」で実施したのと同様な、AD変換器特性を評価してみます。AD変換器特性の評価指標についてはこちらの解説をご参照ください。
【eval】
0,0
1,0
2,0
3,0
4,0
5,0
:
: (中略)
:
4091,15
4092,15
4093,15
4094,15
4095,15
OK: eval,4
シリアルモニタのデータをExcelにコピー&ペーストして、グラフ化してみます。以下図に、測定した全4,096点の測定データと、その一次回帰直線のグラフを示します。
横軸に入力電圧x×5V/4,096、縦軸に変換結果のデジタル値D[x]をプロットしてあります。
ぱっと見、良さそうです。
Excelに引かせた一次回帰直線(赤破線)の数式(上図グラフ中に表記)において、縦軸D=0との交点(Vofs)とD=2N−1=15との交点(Vful)を求めると、以下となります。
Vofs=0.0739/3.0012≒0.025V
Vful=(15+0.0739)/3.0012≒5.023V
ここからオフセット誤差(OE)とフルスケール誤差(FE)を計算可能です。後ほどまとめて示します。
先に、残りの指標を求めてしまいます。まず、絶対精度(AE)ですが、4,096点の測定データと理想特性の偏差(誤差)ですので、上の一次回帰直線を引かせた時のデータで表計算して求めます。結果のグラフを以下図に示します。
横軸は、評価に使用した12ビットDA変換器の出力電圧(x)[V]、縦軸は絶対精度(AE)[LSB]です。
最初のグラフでは気が付きませんでしたが、入力アナログ電圧が3.5Vを超えた辺りで、絶対誤差が大きくなっていたようです。原因は不明です。絶対値が最大となる最大誤差はAE=−1.668LSBでした。
DNLとINLも求めておきます。
D [LSB] |
A[D] [V] |
DNL [LSB] |
INL [LSB] |
0 | 0.000 | 0.000 | |
1 | 0.238 | −0.238 | −0.238 |
2 | 0.553 | 0.008 | −0.230 |
3 | 0.870 | 0.016 | −0.215 |
4 | 1.184 | 0.004 | −0.211 |
5 | 1.495 | −0.004 | −0.215 |
6 | 1.825 | 0.055 | −0.160 |
7 | 2.108 | −0.094 | −0.254 |
8 | 2.480 | 0.191 | −0.063 |
9 | 2.756 | −0.117 | −0.180 |
10 | 3.092 | 0.074 | −0.105 |
11 | 3.440 | 0.113 | 0.008 |
12 | 3.898 | 0.465 | 0.473 |
13 | 4.272 | 0.199 | 0.672 |
14 | 4.584 | −0.004 | 0.668 |
15 | 4.849 | −0.152 | 0.516 |
絶対精度(AE)が大きくなった3.5Vを超えた辺りに相当する、D=12のDNLと、それに伴ってD≧12のINLに大きな誤差が出ています。そこまでは、だいぶ良い感じだったのですけどね……。
全ての評価指標の算出が終わりましたので、以下にまとめておきます。「#8-2 逐次比較型AD変換器を作る」で評価した、4ビット逐次比較型AD変換器の指標と並べて比較します。
指標 | ΔΣ型 | 逐次比較型 | 備考 |
オフセット誤差(OE) | 0.080LSB | 0.525LSB |
この指標のみ、ΔΣ型の方が 高精度な結果となった。 |
フルスケール誤差(FE) | 1.074LSB | 0.683LSB | |
微分非直線性誤差(DNL) |
-0.238 〜 0.465LSB |
−0.113 〜 0.141LSB |
DNL>−1なので、単調増加かつ、 ミッシングコードは存在しない。 |
積分非直線性誤差(INL) | 0.672LSB | 0.242LSB | |
絶対精度(AE) | −1.668LSB | −1.238LSB |
全体的に、逐次比較型よりも、ΔΣ型の方が、精度が低いという結果になってしまいました。本来は逆なのですけどね……(1^^;)。
原因としては、以下の様な事が考えられます。
原因の予想はできても、原因を特定したり、対策したりできるようなデバッグ環境や技能は、筆者にはありません。まあ、アナログ素人の筆者が作ったΔΣ型AD変換器では、ここ止まりでしょう\(1^👅^)/。
4ビットDA変換器から始まった「アナログユニットを手作りしてみよう」シリーズは今回で終了です。
アナログ回路の学習用ですので、通して眺めてみてください。