今回は、DA変換器を作ってみたいと思います。必要なのは、アナログマルチプレクサと、同じ値の抵抗(×たくさん個)です。
まず、デジタルのマルチプレクサと言うのは、複数の入力信号から1つを選択して出力します。この方向だと、「セレクタ」と呼ばれることもあります。マルチプレクサ=多重化器というのは、逆方向の働きをするものも含まれていて、その場合は1本の入力を複数の出力の内1つだけに通過させる動作となります。
デジタルだと、決まった方向にしか信号が流れないので、一方向の素子となるのですが、アナログのマルチプレクサは、選択されたノードを通して、双方向に電流を流すことができます。これを可能にするのが、以下に示すようなCMOSトランスファゲートという回路になります。
CMOSトランスファゲートとは、NMOSとPMOSを抱き合わせて、同時にON/OFFさせる回路になっています。上図に示したような記号が使われることがあり、アナログスイッチと呼ばれることもあります。
今まで「電気的スイッチ」として使ってきた、NPNトランジスタでは、電流が一方向にしか流せませんでしたが(下図)、アナログスイッチは双方向に電流を流せるのが特徴です。
今回購入したのは、下図の様な、74HC4067を搭載したアナログマルチプレクサモジュール(以下、HC4067モジュール)です。
74HC4067のデータシートを検索する(別ウインドウが開きます)
HC4067モジュールは、アナログスイッチを16個並列に並べて、双方向(16⇔1)にマルチプレクス可能となっています。シルクに見えている信号名で、C0〜C15端子の内1本と、SIG端子が接続されます。C0〜C15より、1本を選択する信号がS0〜S3の4ビットです。EN信号にHIGHを印加すると、SIGとC0〜C15間を切断します。よって、通常は、EN=LOWで使用します。
このHC4067モジュールには、ピンヘッダが付属していませんでした。ブレッドボードで実験をするのに、ピンヘッダなしでは不便なので、手持ちのピンヘッダをはんだ付けして使います。
データシートによると、電源電圧は、2V(MIN)〜6V(MAX)で使えるようなので、3.3Vでも、5Vでも、使用可能です。ただし、各ノードに印可可能な電圧の最大値は、「電源電圧まで」と、なっているので、3.3V電源で使う時に5V信号を接続するのは定格違反となります。
今回は、Arduinoを使って評価するので、電源電圧5Vを供給します。
DA変換器を作るにあたって、中間生成物となる、デジタルポテンシオメータを作ります。
HC4067モジュールは、16本のノードから1本を選択することができるので、同じ値の抵抗を16個用意します。16個の抵抗を直列に接続し、各接続ノードを引き出して、HC4067モジュールの端子に接続すると、デジタルポテンシオメータが完成します(下図)。
抵抗値は任意です。最大値を選択した時に、どの程度の抵抗値が必要か?によって、1個当たりの抵抗値を選ぶことになります。4ビットのアナログマルチプレクサなので、抵抗値は16段階にしか変更できません。デジタル入力DIN3〜0の値に従って、リニアに抵抗値が変化する様に、全て同じ値の抵抗値にしてありますが、用途によっては、何らかの関数に従った抵抗値を使用することも自由(任意)です。
※すぐ後で、DA変換器として使う時に、全16個の直列抵抗を介して電源(Vcc)とGND間に貫通電流が流れるので、適度に大きすぎず、小さすぎもしない電流値になるよう、220Ωを選んであります。220Ω×16個に流れる電流は、約1.4mA(@5V)です。
通常のポテンシオメータ(可変抵抗器)は、過去に何度か使用しています(下図)。
つまみを回すと、抵抗値が変化するので、ボリュームなどの制御に使用しました。しかしこの素子は、人の手によって物理的につまみを回さないと抵抗値を変化させることができません。
デジタルポテンシオメータは、抵抗値を電気的(デジタル)に変更できるのが最大の特徴です。上の回路図で、端子VR1⇔VR2間の抵抗値が、DIN3〜0の値に基づいて16段階(220Ω刻み)に変化します。
用途としては例えば、オペアンプを使った増幅回路の増幅率を電気的に変更したいとか、LPF(ローパスフィルタ)等のカットオフ周波数も電気的に制御することが可能となります。
いざ、回路を実装しようとすると、16個の抵抗を直列に接続して、その各接点から16本の信号を引き出し、HC4067モジュールに繋ぐ……、と、考えただけで疲れてきます。しかし、実際やってみると、ブレッドボードへの実装であれば、そんなに苦労しなくて済むことが判りました。
肝は抵抗の挿し方で、以下図の様に、1列ずらして斜めに抵抗を挿入していくと、この抵抗は直列に接続したことになります。
ブレッドボードの穴(タイポイント)は、片側5個が内部で接続されています。この内、一番中央寄りの1行はHC4067モジュール用に開けておき、残りの4個を使って抵抗を差し込みます。規則的に16個挿入したら、終了です。最後に、肌色にした穴部分にHC4067モジュールのピンヘッダを挿入すると、ほぼ完成です。
実際の様子が、下図です。
抵抗の足を切らずに、長いまま挿入する場合、空中で隣の抵抗の足と接触しないように注意してください。
上写真の様に、170穴ミニブレッドボードを、ギリギリ一杯使い切って、デジタルポテンシオメータを構成することができます。後は、必要な配線をジャンパ線で引き出すだけです。ブレッドボードだと、比較的簡単に回路を構成できました。 → 万能基板に実装しようとすると、大変そうですが……。
デジタルポテンシオメータが完成したら、それをDA変換器として使うことができます。VREFとAGNDに適切な電圧を印加すれば終わりです。前節の回路図を一々描くのは面倒なので、以下図の様に簡略化したシンボルで描くことにします。下図は、VREF=Vcc(5V)、AGND=GNDとした場合の回路図です。
AOUTには、デジタル入力DIN3〜0に従って、VREF〜AGND間を分圧した電位が現れます。
VREFとAGNDに直接電源(例えばVccとGND)を接続するのが基本ですが、例えば、ここに、適切な抵抗を介して、電源に接続すると、DA変換器の出力電圧範囲を調整することも可能となります。
ただし、74HC4067の制約(operating conditions)によって、0≦VREF≦VCC、0≦AGND≦VCCである必要があるのでご注意ください。ここで、VCCは74HC4067の電源電圧です。
最終的にどの様なDA変換器出力を得たいか?によって、バリエーションを選択することが可能です。
この4ビットDA変換器が出力するアナログ電圧の解像度は、VREF=5Vの時、V10=VREF/(2N)≒0.31Vとなります。NはDA変換器のビット数(=4)です。このV10に相当する電圧を、変換精度などの評価の際に、1LSBという単位で表現します。
なお、前節で示したようなR1〜R16=220Ω(一定)の回路を使った場合、DIN=0x0f(フルスケール)に対する出力電圧はVREF−V10となります。
※一般的なNビットDA変換器の場合、デジタル値2Nの時に、VREFが出力されるようになっているため、Nビットの最大数である、デジタル値2N−1の時には、1LSBだけ低い電圧(VREF−V10)までしか出力できないようになっています。
一応これで完成、なのですが、ご覧頂いたように、抵抗分圧が動作原理なので、出力AOUTに電流が流れないことが前提となります。もし、AOUTに電流が流れてしまうと、AOUTの取り出しタップより高い電位側の抵抗と、低い電位側の抵抗に流れる電流の比が変わってしまうため、出力電圧値に狂いが生じてしまいます。
従って、通常は、AOUTに電流を流さぬよう、AOUTを入力インピーダンスの高いオペアンプ(ボルテージフォロア)などで受けて、その出力をDA変換器の出力として使用してください。今回は、上で示した回路図の様に、Arduinoのアナログ入力端子A0に繋ぐだけです。サンプル&ホールド回路のコンデンサにチャージする分の電流しか流れないので、特に対策は不要だと判断しました。
※手持ちのオペアンプLM358はRail-To-Railアンプではないので、DA変換器のVREFと、LM358の電源電圧に同じ5Vを使っても、ボルテージフォロアの出力が3.3V程度までしか上がらないことになります(「#6-4 オペアンプを使って増幅回路を作る」参照)。解決策は、LM358の電源電圧にDA変換器のVREFより十分大きな電圧を使うか、Rail-To-Railオペアンプを使うか、です。
制御すべき信号は、DIN3〜0の4本ですね。それぞれ、digitalWrite()で個別に制御しても良いのですが、お勧めしません。各digitalWrite()には、数μsの時間を要するので、4本すべてが所望の出力に設定されるまで、意図しない電圧がDA変換器から出力されてしまいます。
そこで、DIN3〜0は、ATmega328のポートレジスタに、連続して確保可能なPORT-D(PD7〜4、端子番号D7〜D4)を使って、同時に変化させます。digitalWrite()関数含め、このような用途に対応した関数は見つかりませんので、インラインアセンブラで直接記述します。
※すでに、「#3-1 LCD1602に表示する」で解説済みですので、詳しくはそちらを参照してください。
実験、に移る前に、少し、理想的なDA変換器の入出力特性について説明しておきます(下図)。
上図左側のグラフで、赤線がNビットDA変換器において、N→∞とした場合の理想特性となります。一方、上図右側の拡大図で、階段状の緑線がNビットDA変換器の理想出力です。
よって、このDA変換器でアナログ電圧Vxを出力したい場合には、デジタル値D=round(Vx/V10)を出力すれば良いことが解ります。ただし、round()は四捨五入関数、V10は前の節で説明した、1LSBに相当する電圧値です。
実際には、デジタル値Dに対して出力されるアナログ電圧AOUTが、使用する抵抗値のばらつきなどによって、理想値からずれる(橙色点線)ので、それが変換誤差となります。
ここで、4ビットDA変換器評価用プログラムの完成版をリンクしておきます。
評価用プログラム:test_dac_4b.ino
コンパイルしてArduino UNOへ書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。前節で示した回路図に従って、回路を構成してから実験してください。
実験サンプル:【 】内はシリアルモニタからの送信データ
【daco,0】
OK: daco 0 (0[mV] : 0[mV])
【daco,8】
OK: daco 8 (2500[mV] : 2495[mV])
【daco,15】
OK: daco 15 (4687[mV] : 4682[mV])
表示される電圧値の左側が期待値で右側がAD変換器で読み取った値です。全デジタル値0〜15で試してみましたが、ほぼほぼ、期待通りの出力が得られています。後ほど、ちゃんと変換精度の評価を実施します。
4ビットDA変換器が出力する正弦波(きっとカクカクの階段状)を観測するため、Arduinoをもう1機立ち上げて、そちらでLogiscopeを起動しておきます。筆者はArduino Microを使いました。
実験サンプル:【 】内はLogiscopeからの送信データ
【osc,7,4,5,0】 …… トリガ待ちになる(A0=ADC7 @Microを使用)
実験サンプル:【 】内はシリアルモニタからの送信データ
【sin,100,1】 …… 上のトリガ待ち中に送信すること。
OK: sin 100, 1; 166[us]*60
【sin】コマンドのレスポンスで、セミコロン(;)の後ろの数値は、指定した正弦波を出力するのに、平均166μs間隔で60回、DA変換器の出力を設定できたことを示しています。今後、どの程度の周期でDA変換器出力を変更できるか?の目安にしたいと考えています。
以下の様な波形が観測できました。
予想通りの階段状ですが、正弦波らしき波形を観測することができました。階段の数は、ちゃんと16段に見えます。
電圧軸方向の解像度は、V10≒0.31Vと、だいぶ粗いですが、時間軸方向の解像度も、166μsと粗いので、上の波形以上に綺麗な正弦波を出力するのは、Arduinoでは無理でしょう。……時間軸方向の一番短いステップ幅が、およそ166μsであることから、明らかです。
※どうも、波形取得に使ったMicroの電源電圧Vccがちょっと低いみたいで、4ビットDA変換器のフルスケール(0x0f)出力に対して、5Vまで上がり切っている様な波形に見えていますが、実際には、Vcc−V10までしか上がってません。ちなみに、UNOのanalogRead()で、MicroのVccを測定したら、4.6V程度でした。ちょうど、Vcc−V10の電圧近辺です。手持ちの機体の個体差かも知れませんが、ちょっと低すぎますネ……。
作成した4ビットDA変換器の変換精度を評価しておこうと思います。
測定自体は簡単で、0〜15までデジタル値を振りながら、AOUTの出力電圧をArduinoのAD変換器で測定して、10ビットデジタル値で読み取るだけです。評価用プログラム(test_dac_4b.ino)の【eval】コマンドで実行できるように組み込んであります。
実験サンプル:【 】内はシリアルモニタからの送信データ
【eval】
0,0
1,62
2,126
3,190
4,254
5,319
6,383
7,446
8,511
9,575
10,639
11,703
12,768
13,831
14,896
15,959
OK: eval
出力されるのは、カンマ(,)区切りのデータで、第一数値がデジタル値0〜15、第二数値がanalogRead()の読み出し値(0〜1,023)となっています。これをExcel等の表計算ソフトウェアに貼り付けてデータ処理すると、変換精度を求めることができます。
以下の指標について評価したいと思います。
ここで、デジタル値Dに対する、DA変換器の出力電圧実測値をA[D]、ビット数N、1LSBに相当する電圧値V10=VREF/(2N)、としました。すべて、V10で正規化しているので、単位はLSBです。
以下、測定結果から求めた各指標です。
D | analogRead() | A[mV] | 理想A[mV] | DNL[LSB] | INL[LSB] | AE[LSB] |
0 | 0 | 0 | 0 | 0.000 | 0.500 | |
1 | 62 | 303 | 313 | −0.031 | −0.031 | 0.531 |
2 | 126 | 615 | 625 | 0.000 | −0.031 | 0.531 |
3 | 190 | 928 | 938 | 0.000 | −0.031 | 0.531 |
4 | 254 | 1,240 | 1,250 | 0.000 | −0.031 | 0.531 |
5 | 319 | 1,558 | 1,563 | 0.016 | −0.016 | 0.516 |
6 | 383 | 1,870 | 1,875 | 0.000 | −0.016 | 0.516 |
7 | 446 | 2,178 | 2,188 | −0.016 | −0.031 | 0.531 |
8 | 511 | 2,495 | 2,500 | 0.016 | −0.016 | 0.516 |
9 | 575 | 2,808 | 2,813 | 0.000 | −0.016 | 0.516 |
10 | 639 | 3,120 | 3,125 | 0.000 | −0.016 | 0.516 |
11 | 703 | 3,433 | 3,438 | 0.000 | −0.016 | 0.516 |
12 | 768 | 3,750 | 3,750 | 0.016 | 0.000 | 0.500 |
13 | 831 | 4,058 | 4,063 | −0.016 | −0.016 | 0.516 |
14 | 896 | 4,375 | 4,375 | 0.016 | 0.000 | 0.500 |
15 | 959 | 4,683 | 4,688 | −0.016 | −0.016 | 0.516 |
4ビットと粗いせいもあって、フルスケール(D=15)の出力電圧A[15]=4.68Vに、「んっ?」ってなりますネ……。VREF=5Vに比べてずいぶん低い電圧ですから。
思い出していただくと、理想的DA変換器の出力を、「D=2Nの時にA=VREF」という定義にしてあるので、D=2N−1の時は、A=VREF−V10となるわけです。今、N=4ビットのケースなので、V10=VREF/(2N)≒0.31Vであり、A=5V−0.31V=4.69Vが期待値ですから、これでほぼ、期待通りの値なのです。
今回のお買い物は、以下の通りです。
抵抗分圧で作るDA変換器は、2N個の抵抗を同じ抵抗値で作り込むことが肝になります。12ビットDA変換器ともなると、直列抵抗の数は212=4,096個になりますから、そりゃもう、大変です。