第8回

タイマー

主にタイマー機能を用いたPWM動作について説明します。
前回触れたポートの入出力からマイコンのピンを0V、あるいは5Vに出来るようになりました。
PWMを使えばピンの出力状態を見かけ上0~5Vの間に設定することが出来ます。
つまりLEDの明るさやモーターの速度を変えることが可能になります。

タイマーについて

PWMの前に、タイマーのことを少々。
言葉からも想像できますが、時間を計る機能がマイコンに内蔵されています。

どのように時間を計るか

まずマイコンにはクロックが決められています。
これは既にこれまでのプログラムの中に登場しています。
  #define F_CPU 1000000
1秒間に1000000回、1μ秒単位で時間を計れると考えてください。
このクロックを数えて時間を計ります。1カウントで1μ秒ですね。
ただクロックを0,1...と無制限に数えることは出来ません。
Atmega88には8bitタイマと16bitタイマの2種類が用意されています。
2^8=256、2^16=65536カウントが上限になり、それを超えると再び0からカウントが始まります。
例えば8bitタイマをの動きは下のようになります。

PWMについて

中間の出力状態を考える

0と1(5V)で構成されているデジタルの世界でその中間の値をどのように作り出すか。
例えば2.5Vを出力することを考えてみます。
図のように1周期を1Vと0Vの半々にして高速で繰り返せば、見かけは2.5Vになります。

Duty比

1周期に対して5V(High)の割合のことをDuty比といいます。
8bitタイマであれば256カウントのうち、128カウント分をHighにすればDuty比50%になります。
Atmega88ではOCRレジスタというものを用いてDuty比を決定します。
出力波形は次のように作られます。
まず先程の8bitタイマを遠目に見ると、下のようなグラフに見えます。
これを形状からのこぎり波といいます。
次にy=127(0から数えて128カウント目は127)に線を引きます。
ここでy=127よりyの値が等しいか小さいときはHigh、それよりも大きい時はLowを出力するとします。
すると下のようなグラフになります。
どうでしょうか。
途中で引いた境界線がOCRレジスタです。Duty比はこれで決まります。

delay関数との違い

先程出力間隔が非常に短ければ疑似的に中間出力が可能であることを説明しました。
LEDを暗めにしてみましょう。
(追記:Duty比40%だと大して暗くなりません。OCR0Aを10くらいまで下げてください。)
#include <avr/io.h>
#define F_CPU 1000000
#include <util/delay.h>

int main(void){
    DDRD  = 0b11111111;//PDをすべて出力設定
     PORTD = 0b00000000;//PDをすべてLに

     TCCR0A=0b10000011;//OCR0Aを8ビット高速PWM動作
     TCCR0B=0b00000001;//前置分周なし    

     OCR0A =101;//Duty比約40% 
}
出力幅はタイマだけでなくdelay関数を用いても表現することが出来ます。
#include <avr/io.h>
#define F_CPU 1000000
#include <util/delay.h>

int main(void){
   DDRD  = 0b11111111;//PDをすべて出力設定
    PORTD = 0b00000000;//PDをすべてLに
    while(1){
      PORTD=0b11000000;//PD6,7をHに
       _delay_ms(4);//4ミリ秒待つ
       PORTD=0b10000000;//PD6をLに
       _delay_ms(6);//6ミリ秒待つ
    }
}    
同じ明るさになったかと思います。
この2つは何が違うのでしょうか。ピンの状態を指定するか、動作を指定するかの違いです。
タイマによるPWM出力は、OCRによってピンの出力の状態を指定します。ポート出力と似たようなものです。
一方delay関数はその間なにもしない、直前までの状態を保持するという動作の関数です。
つまりdelay関数で波形を作るとdelay関数に入っている間は他の命令が出来ません。
また、delay関数は変数を代入出来ません。
その点OCRは変数を代入出来るのでDuty比を好きな時に変更できます。

Atmega88のレジスタ

Atmega88には8bitタイマが2つ、16bitタイマが1つ備わっています。
各タイマにタイマピンが二つずつ、計6つ用意されています。OC0A~OC2Bがそれにあたります。
具体例にタイマ0を使います。データシートのP64を見てください。
TCCR0A、TCCR0Bの2つのレジスタを用意します。
内容を見ていきましょう。主に下の3つです。
レジスタ ビット 内容が無いよう
1 COM0A1~0 7,6(TCCR0A) OC0Aピンの状態を決定(標準ポートor比較一致出力)
2 WGM02~00 3(TCCR0B),1,0(TCCR0A) タイマ動作の決定(CTC動作、高速PWM動作、位相基準PWM動作)
3 CS02~00 2,1,0(TCCR0B) プリスケーラ(分周期)の設定
表の上から見ていきましょう。
1.COM0A1~0
OC0Aピンの状態を決定します。00(標準ポート動作)にするとポートピン(PD6)として使用する時に使います。
01はCTC動作の時にしか使いません?
10、11(比較一致出力)にするとOCRピンから波形を出力します。
10は非反転動作といい、0からOCR0AまでをHigh、残りをLowで出力します。上の図の波形は非反転動作です。
11は反転動作です。非反転動作のHighとLowが逆転した形になります。
ここで注意です。ポートを出力設定にしておきましょう。(DDR,PORTの話)
2.WGM02~00
タイマ動作を決定します。CTC動作、高速PWM動作、位相基準PWM動作の3つがあります。
どのような動作、波形を作るかはP60~の図を見るのが分かりやすいと思います。
3.CS02~00
分周期を設定します。1カウントの幅を変更できます。
8bitタイマは256カウント、つまり256μ秒しか計れません。例えば010(8分周)にすると1カウント8μ秒となります。
分周器は1024分周まであるので8bitタイマでは最大1024×256=262144カウント、約260m秒まで計れます。
精度は落ちるようですが。。

今回はOC0Aピンを比較一致出力で非反転動作、高速PWM動作、分周なしに設定してみましょう。
                       //76543210
  TCCR0A = 0b10000011;
  TCCR0B = 0b00000001;
上のようになったでしょうか。
次にOCR0AをいじってDuty比を変えて、LEDをグラデーションさせるプログラムを書いてみましょう。
#include <avr/io.h>
#define F_CPU 1000000
#include <util/delay.h>

int main(void){
    DDRD  = 0b11111111;//PDをすべて出力設定
     PORTD = 0b00000000;//PDをすべてLに

     TCCR0A=0b10000011;//OCR0Aを8ビット高速PWM動作
     TCCR0B=0b00000001;//前置分周なし    

     unsigned char i=0;//iを正のchar型で宣言(0で初期化)

     while(1){
      OCR0A=i;//OCR0Aのiカウント分Highに
       _delay_ms(5);//5ミリ秒待つ
       i++;//iを1増加(インクリメント)
     }   
}
これをdelay関数で書くのは骨ですね。

サーボモータの動作について

サーボモータは約20m秒周期に対して1.0~2.0m秒がHighとなるような信号を送ると一定の角度を維持することが出来ます。
例えば基準位置(0度)にする信号、1.5m秒をHighにした信号は下のようになります。
この出力波形もPWMで作ることが出来ます。
20msは20000μ秒ですから8bitタイマで分周器をかけるよりも16bitタイマを使った方がよさそうです。
16bitタイマにはカウントの上限値を決めることが出来ます。それにはICRレジスタを用います。
データシートのP83を見てください。(一番上の表の下から2番目)
実際にサーボを動かしてみましょう。
#include <avr/io.h>
#define F_CPU 1000000
#include <util/delay.h>

int main(void)
{
    DDRB = 0b00000010;    //PB1を出力設定に
    PORTB = 0b11111101;    //ポートBの内部プルアップを有効
    TCCR1A = 0b10000010;    //OC1Aを高速PWM動作で0からOCR1AまでHに
    TCCR1B = 0b00011001;    //分周なし高速PWM動作
    ICR1 = 19999;            //1周期20ms
   OCR1A=1499;              //1.5ms(0°)
 
   char push = 0;    //スイッチが押されていない間は0,押されている間は1
   while(1)
   {
       if(PINB==0b11111110)//PB0がLowなら
        {
          if(push==0)//それまでボタンが押されていなければ
            {
               OCR1A+=500;
               if(OCR1A>2000)
               {
                   OCR1A=999;
               }
          }
          push=1;    //スイッチが押されたことを記憶
        }
       else if(PINB==0b11111111)
       {
          push=0;    //スイッチが離されたことを記憶
        }
   }
}

課題

お暇であれば。
1.スイッチのON/OFFでモーターの速度が切り替えてください。(Duty比は任せます)
2.前述でLEDのグラデーションが出てきました。更に滑らかな変化にしていきましょう。
人の目は明るさをDuty比の1/3乗に比例するように認識するらしいです。
Duty比が0~255の間で0,1,8,27,64...と変化すればよいわけです。
関数で書くとOCRレジスタ=x^3/a(aは定数)です。aが大きいほど滑らかなグラデーションが出来ます。
ここで問題ですがOCRレジスタには少数は入れられません。どうしましょう。
整数型では小数点は切り捨てられます。強引に計算式をぶち込んでやると値は整数になります。
グラデーションが出来たら、明るくなる方だけでなく暗くなる方のグラデーションも作ってみてください。
3.OC0Aだけでなく、OCR0Bピンも使ってみましょう。
OC0Aが明るくなる時、OC0Bが暗くなるような動作を作ってください。
グラデーションは綺麗でなくて構いません。
最終更新:2015年09月07日 05:51