第10回

AD変換


アナログ値からデジタル値に変換します。
以前ポートのところで入力ピンを扱いましたが、PINでは0と1しか判別出来ません。
センサでいうとスイッチのON/OFFしか判別できないということです。
しかし世の中の情報は多くがアナログです。
AD変換により扱えるセンサの幅が広がります。

可変抵抗

例えばアナログの情報としてボリュームが挙げられます。
音量や照度を調節するつまみを想像してください。
実際は可変抵抗の利用です。
可変抵抗の回路記号はこんな感じです。(http://www.asahi-net.or.jp/~qq3y-nkdo/ocl/feet/KairoA3/KA_005.html
bの記号がまさに可変抵抗の内部を表しています。
両端間の抵抗値は一定で真ん中の端子と端との間が可変となります。
ただ抵抗値を変えたいだけなら真ん中と端の2端子で十分なわけです。(そういう用途の為にaの記号を用います。)
では何のために3端子あるかというと、今回のような用途のためです。
2つの端のうち片方をグラウンドに、もう片方を基準電圧につなぎ、真ん中の端子をADCにつなぐと0から基準電圧までの任意の電圧をADCにかけることが出来ます。
回転に応じてADCにかかる電圧が変わるわけです。
(今回使用するのは半固定抵抗といいます。
頻繁に値を変えたい時に用いるのは可変抵抗で、回転させやすい形状で耐久性が高いです。
それに比べて半固定抵抗は一度抵抗を決めたらほとんど動かさない時に使います。
安価ですが耐久性はそれほどありません。)
取り敢えず半固定抵抗を用いて手動でLEDの明るさを変えてみてください。
電位が0~5Vに変化したため明るさが変わりましたね。
マイコンではこの電位をAD変換します。

レジスタの説明

では変換の様子を見ていきましょう。
0Vを0に、基準電圧(今回は5V)を1023とします。
ADCピンにつながった電位を10bitの精度でデジタル値に変換すると下の表のようになります。
ADCピンの電位 0V(最低値) 1V 2V 3V 4V 5V(最大値)
変換されるデジタル値 0 204 409 613 818 1023
つまりADCピンに2.5Vがかかると、これは5Vの半分の電圧なので、内部では1023の半分である512に変換されるわけです。
Atmega88では最大1024分割ですが、この「どれだけ細かく分割するか(分解する能力)」を分解能といいます。
分解能が高ければ高いほどアナログ値に近い近似が出来るというわけです。

AD変換に使うレジスタはのADMUX,ADCSRA,ADCの3つです。
ADCはデジタル値に変換された値が格納されるだけなのでこちらから書き換える必要はありません。
まず前の2つから見ていきましょう。
7(bit) 6 5 4 3 2 1 0
ADMUX 0
ADCSRA 0 0 0
①…基準電圧を設定します。今回はAVCCを使うので01ですね。ARFFの電圧を使うことも出来ますし、内部で生成される1.1Vの電圧を使用することもできます。
②…1だと左寄せ、0だと右寄せ。これは後で説明します。
③…接続先を選択します。Atmega88にはADCピン0~5の6つが搭載されています。
④…AD変換を使用するので1にします。0なら普通の入出力ピンになります。
⑤…AD変換を開始する時に1にします。変換が終わると0に戻ります。
⑥…(?)AD変換を行う速度の設定です。AD変換には13クロックかかり、周期は50~200kHzにした方が良いらしいので、今回は16分周にします。
この切替の後動作が安定するまで待つ必要があるらしいです。。

右寄せ

さて、後で説明するといった右寄せ・左寄せについて書いていきます。
単純な話、AD変換の分解能を右寄せで1024(10bit)、左寄せは256(8bit)に落とすことが出来ます。
右寄せはともかく左寄せでどのように分解能を8bitにするのでしょうか。
ADCレジスタは上位8bitのADCHと下位8bitのADCLの2つから構成されています。
ここで,左寄せにした際のADCHの値を見てみましょう。
ここには,分解能8bitのデジタル値が入るようになるわけです。
例として,501という数字を見てみましょう。
10ケタの二進数で書くと0111110101です。
ADCH
7 6 5 4 3 2 1 0
0 1 1 1 1 1 0 1
ADCL
7 6 5 4 3 2 1 0
0 1 0 0 0 0 0 0
ここでADCHのみに着目すると、1111101=125となります。
125/256ということで,分解能が8bitになっていることが分かるでしょうか。
ここで、502のときを考えてみましょう。この時、変わるのはADCLの6bit目だけです。つまり、ADCHの値は全く変わりません。
同様に502、503においてもADCHの値は変わらず125となるので、精度は分解能10bitの時の4分の1となっています。
このように、10bitの分解能を簡単に8bitに落とすことができるわけです。
10bitの分解能で知りたい時は0でADCの値を見て、8bitの時は1でADCHの値を見る、と覚えてください。

レジスタの説明が一通り終わったところで、半固定抵抗でLEDの明るさを変えるプログラムを見てみましょう。
  #include <avr/io.h>
  #define F_CPU 1000000
  #include <util/delay.h>
  int main(void)
  {
    //ポート設定の初期化
     DDRC = 0b00000000; //PORTCを入力設定
     DDRD = 0b11111111; //PORTDを出力設定
     PORTC = 0b00000000; //内部プルアップ無効
     PORTD = 0b00000000; //出力をLに
     //タイマー設定の初期化
     TCCR0A = 0b10000011; //OC0Aを高速PWM動作で0からOCR0AまでHに
     TCCR0B = 0b00000010; //8分周高速PWM動作
     //ADC設定の初期化
     ADMUX = 0b01100000; //AVCCを基準、左寄せ、ADC0(PC0)に接続
     ADCSRA = 0b10000100; //変換器を有効、16分周(1MHz/16=62.5kHz)AD変換には13クロックかかるため.
     while(1)
     {
        //AD変換を行う
       ADCSRA = ADCSRA | 0b01000000; //AD変換器起動
         while(ADCSRA & 0b01000000) ; //AD変換完了待ち
         OCR0A = ADCH;//デューティー比はAD変換の値をそのまま出力
                       //ADCHはグローバル変数であるため,どこからでも参照可
     }
  }
AD変換を行っているadc関数について。
2行とも論理演算子によるマスク処理が行われています。
"|"は論理和、"&"は論理積でした。詳しいことは第7回ポートのところに書かれています。
このwhile文は1の間(AD変換途中)はループしつづけ、0になる(AD変換完了)とループから抜けて次に進むという仕組みになっています。
また、OCSRAにADCHを入れています。
前述の通り今回は左揃えなのでADCHには8bitのデジタル値が入っています。
OCR0Aも8bitタイマなので、値をそのまま入れることができます。

プログラムの書き方について

関数の使い方

最後に,C言語の関数の使い方について説明します.
関数と言えば、f(x)=2x-1とかが頭に浮かぶと思います。
C言語における関数もこれをもっと広くとらえたようなものです。
因みに、今までプログラムの本文を書いてきたmain()というのも、main関数という関数です。
では例として、与えられたint型の数字の絶対値を返す関数を考えてみましょう。
簡単にプログラムを書くとこんな感じになります。
  int num=-5;//numに代入されている値を絶対値に変換
  if(num<0)//もしnumが0より小さければ
  num=-1*num;//numの符号を反転
               //num>=0の時は何もしなくていい.
では,これを関数の形で書き直してみます.
  int abs(int num)//関数の宣言
  {
     if(num<0)//もしnumが0より小さければ
      num=-1*num;//numの符号を反転
      return(num);//numを返す
  }
一行目は、関数の宣言です。ここでは、関数の名前をabsとして宣言しました。
初めのintは、今回作る関数absの型です。最終的にint型の数で返すので、intで宣言しました。
この返す値の事を返り値(戻り値)といいます。
次のかっこ()内は、引数の型です。この関数を使う際に、絶対値に変換したい値はこの引数として宣言されます。
(引数がint型なので、それを絶対値に変換した値もint型となるため、先ほどの関数の型(返り値の型)もint型にしました。)
さて、これで関数の宣言は終わりました。次の中かっこ{}内に関数の内容を書いていきます。
numという変数は関数の宣言と同時に引数として宣言しているので、新たに作る必要はありません。
最後のreturnは変数を返す時に用います。これにより、関数の結果にnumが代入されます。
では、実際にmain関数でabs関数を呼び出してみましょう。こんな風に書きます。
   int main(void)
  {
     int num=-5;//numという変数を宣言し,-5で初期化
      num=abs(num);//numが絶対値に変換された後,再度代入
                     //よって,numの値は5となる.
  }
このようにして、一度宣言した関数は使うことができます。ただし、関数を宣言する位置は関数を使用する位置よりも先に書いておく必要があります。
宣言さえ先にしておけば、関数の内容は後でも構いません。(これをプロトタイプ宣言といいます。)
さて、先ほどの関数と使用例において、疑問に思った人がいるかもしれません。
numって変数が二つ出てきてないか?とか、abs関数の中でnumに絶対値が代入されてるし、main関数の中で代入し直す必要なくね?とかですね。
実は変数は、宣言した関数の中でしか使えません。その関数を出ると、その変数は消えてしまいます。これを、ローカル変数といいます。
よって、main関数の中で宣言したnumという変数と、abs関数の中で宣言したnumという変数は、まったく別の変数です。
したがって、同じ名前の変数が二つ出てきても別の変数なのでまったく問題なく、同様に別の変数なので、abs関数の中のnumに絶対値が入っていたとしても、それをmain関数のnumに代入しないと意味がないわけです。
こういう処理が面倒くさいときは、グローバル変数を使います。
  int num=-5;//関数の外で変数を宣言
  void abs(void)//関数の宣言
  {
     if(num<0)//もしnumが0より小さければ
     num=-1*num;//numの符号を反転
  }
  int main(void)
  {
     abs(num);//numが絶対値に変換された後,再度代入
                 //よって,numの値は5となる.
  }
先ほどと異なり、numという変数がどの関数にも入っていないところで宣言しています。
これをグローバル変数といい、どの関数からでも使うことができます。
さて、main関数を見てください。abs(num);と、代入の形になっていません。
これは、abs関数の中でグローバル変数であるnumの値を変えているため、関数を呼び出すだけで代入が終わっているからです。
そのため、abs関数には返り値も引数もなく、「何もない、空(から)」という意味のvoidで宣言しています。
返す値がないため、returnもありません。
このように、グローバル変数はとても便利に見えます。
しかし、逆に言えばこの変数はどこからでも変更できてしまうという弱点を持っているともいえます。
不用意に使いすぎると、気付かない間に数値が変わっているといった事態を招くため、多用は推奨されません。

ライブラリ

関数は自作することも多いですが、良く使われる関数は同じような関数と一緒にライブラリというところにまとめられています。
今回作成した絶対値を返す関数も勿論存在し、stdlib.hという中に入っています。
プログラムの頭でincludeすることで、そのライブラリにある関数を呼び出せるようになります。
さて、includeという文字に見覚えがあるでしょうか。前回までのプログラムの最初のおまじないを見てください。
  #include <util/delay.h>
と、こんな風に書いてありますね。
この中にdelay関数が宣言されていて、これをincludeすることでdelay_ms()という関数を使うことができるようになるわけです。
先ほどのstdlib.hの他に、三角関数や指数関数などがまとめられたmath.hや、文字などを扱う際に便利な関数がまとめられたctype.hなど、様々なライブラリ関数が存在します。

先ほどのプログラムを関数を使うと下のようになります。
  #include <avr/io.h>
  #define F_CPU 1000000
  #include <util/delay.h>
  void port_init(void)//ポート設定の初期化
  {
      DDRC = 0b00000000; //PORTCを入力設定
      DDRD = 0b11111111; //PORTDを出力設定
      PORTC = 0b00000000; //内部プルアップ無効
      PORTD = 0b00000000; //出力をLに
  }
  void timer_init(void)//タイマー設定の初期化
  {
      TCCR0A = 0b10000011; //OC0Aを高速PWM動作で0からOCR0AまでHに
      TCCR0B = 0b00000010; //8分周高速PWM動作
  }
  void adc_init(void)//ADC設定の初期化
  {
      ADMUX = 0b01100000; //AVCCを基準、左寄せ、ADC0(PC0)に接続
      ADCSRA = 0b10000100; //変換器を有効、16分周(1MHz/16=62.5kHz)AD変換には13クロックかかるため.62.5kHz/13=約5kHz
     _delay_ms(5); //分周器の動作安定待ち
  }
  void adc(void)//AD変換を行う関数
  {
      ADCSRA = ADCSRA | 0b01000000; //AD変換器起動
      while(ADCSRA & 0b01000000) ; //AD変換完了待ち
  }
  int main(void)
  {
     port_init();//ポート設定の初期化
     timer_init();//タイマー設定の初期化
     adc_init();//ADC設定の初期化
     while(1)
     {
         adc(); //AD変換を行う
         OCR0A = ADCH; //デューティー比はAD変換の値をそのまま出力
                     //ADCHはグローバル変数であるため,どこからでも参照可
     }
  }
main文の中が大分スキッリしたのが分かるでしょうか。

課題

お暇があれば。
1.PC0にかかる電圧が2.5Vを超えるとLED点灯、2.5V以下ならLEDが消灯するというプログラム。
ただし、ADCの値を右寄せとする(10bitのデジタル値に変換する)。
ヒント:ADCには10bitの値が入っています。(ADCの中でADCHとADCLは勝手に結合されています。)
2.PC0にかかる電圧によってLEDの明るさを3段階に変えるプログラム。ADCは右寄せで構いません。
出来ればif文を使わずに書いてみてください。
3.(やや難)adc_connect(char num)という、引数の値に対応したポートにおいてAD変換(右寄せ)を行う関数を作成してください。
引数は0~5までとする。(引数が0ならPC0、引数が1ならPC1といったような感じ)
ヒント:ADMUXレジスタの下位4bitと接続先ポート番号は1:1で対応している。マスク処理。
4.PC0とPC1の2か所に半固定抵抗を繋ぎ、OC0AとOC0BにLEDを繋ぐ。
PC0の入力値に対応してOC0AのLEDの明るさを変え、PC1の入力値に対応してOC0BのLEDの明るさを変えるプログラム。
(今回やった内容をPC1,OC0Bでも行う)
ヒント:同時に2か所のAD変換はできないので、一つずつを交互に。
最終更新:2015年09月07日 16:11
添付ファイル