トランジスタ技術2022年4月号に掲載されていた積分型ADC (Analog to Digital Converter)の記事を見て、面白そうだったので実際にブレッドボードで作ってみました。
初めに言っておきますが、今回作るADCはマイコンのADCの代わりになるようなものではなく、原理を理解して楽しむようなものです。
回路はほぼすべてトラ技のものをそのまま使用させてもらっていますが、文章を読んでもすぐに理解できなかった点などを咀嚼してこの記事を書くことにしました。
概略
今回の回路の構成はこのような感じになっています。
一定電圧を積分するとその出力は一次関数となり、それとアナログ入力を比較します。
積分結果がアナログ入力を上回ったタイミングでカウンタのクロックを停止させます。クロックの周期と T を調整することで(4bitなら 周期=T/16)、 t/T がカウンタの値として出力されます。
積分結果が直線なので t/T はアナログ入力に比例します。したがって、これでアナログからデジタルに変換できることが分かります。
次は各ブロックの回路を見ていきます。
積分回路
オペアンプの積分回路について数式を用いて説明します。オペアンプの入力と出力の間にコンデンサを入れると積分回路となります。
そもそもオペアンプは、2つの入力の差を増幅して出力する素子です。式にすると
V_{out} = A(V_{+} - V_{-})
となりますが、増幅率Aは10000など非常に大きいので
V_{+} - V_{-} = 0
とみなせます。これはバーチャルショートやイマジナリーショートと呼ばれていて、オペアンプ回路の基本となります。
いま、コンデンサに蓄えられた電荷Qと流れる電流Iを図のように定めると、(オペアンプの入力インピーダンスは大きいのでオペアンプの入力端子には電流は流れません)
Q = C(V_{out} - V_{-})\\ I = \cfrac{dQ}{dt}
となりますが、
V_{-} = V_{+} = 0
を用いると、
Q = CV_{out}\\ I = C\cfrac{dV_{out}}{dt}
となります。また、
-V_{in} = RI = RC\cfrac{dV_{out}}{dt}
(符号に注意)より
\cfrac{dV_{out}}{dt} = -\cfrac{V_{in}}{RC}
となり、両辺をtで積分すると、
V_{out} = -\cfrac{V_{in}}{RC}t
となります。(ただしt=0 でV_{out} = 0 とする)
V_{in}に負の一定電圧をかけてやることで概略で示したように時間に比例して増加する出力が得られます。
なお当然ですが出力はオペアンプにかける電源電圧の範囲に限られます。
リセットするにはコンデンサを短絡すればよいです。
比較回路
積分結果とアナログ入力を比較して0か1を出力します。
コンパレータICというのもありますが、オペアンプに周辺回路を何も付けなければそのままコンパレータになります。
先ほどのバーチャルショートの話と混同しないように注意が必要ですが、バーチャルショートはオペアンプの出力から入力に負のフィードバックが掛かっている場合に成立します。
コンパレータといってもこのままだと出力は1か-1(負電源)なので、負の出力を0にするリミッタ回路を追加します。
オペアンプの右の部分がリミッタ回路です。抵抗とショットキーバリアダイオードからできています。
ショットキーバリアダイオードは通常のダイオードよりも順電圧降下の小さいダイオードです。
積分結果がアナログ入力よりも小さいとき、オペアンプの出力は1(具体的には約4V)となります。このときダイオードには逆方向に電圧が掛かるので、電流は流れずそのまま1が出力されます。
逆の場合はオペアンプの出力は-1(約-3V)となり、ダイオードには順方向に電圧が掛かることになります。
GNDからダイオード、電流制限抵抗を通ってオペアンプに電流が吸い込まれるので、出力電圧はGNDからショットキーバリアダイオードの順電圧降下分だけ下がった電圧(約-0.3V)となります。
原理的に0になるわけではではありませんが、後に出てくるアナログスイッチの入力電圧は -0.5V ~ VDD+0.5V なのでこれで大丈夫です。
カウンタ回路
4bitのバイナリカウンタを使ってtを測ります。
カウンタのビット数がADCの分解能にそのまま対応します。
カウンタICも当然あるのですが、今回はDフリップフロップを組み合わせて作ります。
こんなに簡単な回路でカウンタになるのってちょっとびっくりしませんか?私はびっくりしました。
それはさておき、動作を説明します。左が下位で右が上位となっています。
2進数の数字が1ずつ増えていく様子を思い浮かべてほしいのですが、1の位は毎回1と0が入れ替わりますよね。そのため最下位のフリップフロップのDはQに繋がっていて、クロックが入るごとに反転します。
2つ目以降は全て同じ回路になっていて、DはQに、CKは1つ下の位のQに繋がっています。
10進数だと考えやすいと思いますが、一般の記数法において、ある位の数字が変化するのは1つ下の位が桁あふれしたときで、その変化は+1です。
特に2進数においては、桁あふれ=1→0、変化=反転にほかなりません。
下の位が1→0と変化するとき、Qは0→1と変化しますからこれをCKに入力していて、その時の変化は反転なのでDは自身のQに繋がっているというわけです。
上図では省略していますが、もちろんQにはLEDを付けて結果を出力させます。
また、各フリップフロップにリセット端子があるのでそれも接続します。
電源回路
オペアンプの動作には正負の両電源が必要になります。
今回は角形電池の9Vを真ん中を基準にして±4.5Vとして使用することにしました。
真ん中の電圧は抵抗2つで簡単に作れますが、そのままだと負荷によって電圧が変わってしまうので電源としては使えません。
そこで、またオペアンプの出番です。次のようなボルテージフォロワと呼ばれる回路で、負荷があっても電圧を一定に保ちます。
オペアンプの出力と反転入力を直結することで、バーチャルショートが成立し、
V_{out} = V_{-} = V{+}となります。
これをGNDとして使い、もともとの電池の両極は正負の電源として使います。
あえて明示的に書いていますが、オペアンプの電源は電池です。
全体回路図
以上の回路をまとめると全体の回路図はこんな感じです。
上段左が電源部で、中段は左から定電圧生成部、積分部、比較部となっていて、下段がカウンタ、出力部です。
上段右はICの電源とかなのであまり関係ないです。
ここまでで紹介していない部品として、アナログスイッチがあります。
これは右図のピン番号で説明すると、13番がHだと1と2が導通して、Lだと非導通になります。
この例だとDO0をHにするとコンデンサを短絡して放電することが出来ます。
少し見にくいですが、太字になっているアナログ入力、DO0~DO2が外部入力です。
DO0, DO1はリセット信号で、DO2はクロックです。
トラ技ではADALM2000というUSB計測器を使用してこれらの信号を制御しているのですが、当然そんなものは持っていないのでPICマイコンで制御したいと思います。プログラムは最後に掲載します。
定数計算
積分回路は R=100kΩ, C=1μF として、クロックは100Hzだとします。
アナログ入力の入力範囲を0V~4Vとしたときの積分する定電圧を求めましょう。
カウンタは4bitなので、クロック16周期で積分値が4Vになるようにすればよいことになります。
V_{out} = -\cfrac{V_{in}}{RC}tに V_{out}=4, R=100*10^{3}, C=1*10^{-6}, t=16/100 を代入すると、
V_{in} = -2.5V となります。これを可変抵抗とボルテージフォロワで作ります。
実際はオペアンプが電源電圧いっぱいまでは出力できなかったりするので、積分電圧や入力電圧を微調整します。
動作テスト
今回使用したパーツは以下の通りです。
- オペアンプ TL082L-D08-T x2
- フリップフロップ TC74HC74AP x2
- アナログスイッチ TC74HC4066AP
- ショットキーバリアダイオード SD103A
- 積層セラミックコンデンサ 1uF
- LED、抵抗、可変抵抗
- 9Vアルカリ電池
- PIC16F18313
パーツは全て秋月電子で揃えました。
ブレッドボードで組んでみたのがこちらです。ごちゃごちゃしてますね。
右上の電源ラインにアルミ電解コンデンサが入っているのが分かると思いますが、これがないと何故か動作が不安定になってちゃんと動きませんでした…。
実際に動作させてる動画がこちらです。
ピンセットで回している可変抵抗が入力で、LEDが出力です。およそ1秒ごとにAD変換を行っていて、可変抵抗を回すと出力結果もそれに応じて変化するのが確認できました。
変換のたびに結果が若干違うので精度は4ビットすら怪しいですが、概ね増減の傾向は正しいので良しとしましょう。
PICのプログラムも一応載せておきます。
▼クリックで表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#pragma config FEXTOSC = OFF // FEXTOSC External Oscillator mode Selection bits (Oscillator not enabled) #pragma config RSTOSC = HFINT1 // Power-up default value for COSC bits (HFINTOSC (1MHz)) #pragma config CLKOUTEN = OFF // Clock Out Enable bit (CLKOUT function is disabled; I/O or oscillator function on OSC2) #pragma config CSWEN = ON // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed) #pragma config FCMEN = ON // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is enabled) #pragma config MCLRE = ON // Master Clear Enable bit (MCLR/VPP pin function is MCLR; Weak pull-up enabled ) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config WDTE = OFF // Watchdog Timer Enable bits (WDT disabled; SWDTEN is ignored) #pragma config LPBOREN = OFF // Low-power BOR enable bit (ULPBOR disabled) #pragma config BOREN = ON // Brown-out Reset Enable bits (Brown-out Reset enabled, SBOREN bit ignored) #pragma config BORV = LOW // Brown-out Reset Voltage selection bit (Brown-out voltage (Vbor) set to 2.45V) #pragma config PPS1WAY = ON // PPSLOCK bit One-Way Set Enable bit (The PPSLOCK bit can be cleared and set only once; PPS registers remain locked after one clear/set cycle) #pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a Reset) #pragma config DEBUG = OFF // Debugger enable bit (Background debugger disabled) #pragma config WRT = OFF // User NVM self-write protection bits (Write protection off) #pragma config LVP = ON // Low Voltage Programming Enable bit (Low voltage programming enabled. MCLR/VPP pin function is MCLR. MCLRE configuration bit is ignored.) #pragma config CP = OFF // User NVM Program Memory Code Protection bit (User NVM code protection disabled) #pragma config CPD = OFF // Data NVM Memory Code Protection bit (Data NVM code protection disabled) #include <xc.h> #define _XTAL_FREQ 1000000 __interrupt isr(){ GIE = 0; if(TMR0IF){ TMR0IF = 0; static char ra2 = 0; RA2 = ra2 = 1-ra2; } GIE = 1; } void Init(){ TRISA = 0b00011000; WPUA = 0b00011000; ANSELA = 0; PORTA = 0; T0CON0 = 0b10000100; T0CON1 = 0b01000000; TMR0IE = 1; PEIE = 1; GIE = 1; } void main(void) { Init(); char ra2 = 0; while(1){ //リセット T0CON0bits.T0EN = 0; //クロック停止 RA0 = 1; //コンデンサ放電 RA1 = 0; //カウンタリセット __delay_ms(200); TMR0L = 0; //クロックリセット T0CON0bits.T0EN = 1; //クロック開始 RA0 = 0; //コンデンサ充電 RA1 = 1; //カウンタリセット解除 __delay_ms(800); } } |
まとめ
今回はトラ技の記事を参考に積分型ADCを作ってみました。
私はオペアンプやアナログ回路の経験があまりないので、実際に作ってみて程よい勉強になったと思います。
また何か面白そうな回路があれば実際に作ってみたいと思います。
今回の記事は以上となります。ご覧いただきありがとうございました。