タイマーとはその名の通りタイマー機能を有したものです。時計やストップウォッチのようなものなど、時間を扱うときに使います。
今回は8bitタイマ/カウンタ0を使用してディレイ関数を作ってみましょう。AVRには_delay_ms()や_delay_us()というディレイ関数が用意されていますが、それをタイマーを使って作ってみようということです。まず最初に、データシートを見てみましょう。
8bitタイマ/カウンタ0の特徴や概要を見てみると、小難しそうなことが書いてあります。勿論すべてを理解するのであれば、内容を端から端まで読まないといけないのですが、今回はディレイ関数を作るだけなので、細かいところは省きましょう。では、どこから見ていけばいいかというと、ズバリレジスタの説明です。
8bitタイマ/カウンタ0用レジスタに挙げられているレジスタは、
の7つです。今回はタイマ割り込みは使用しないので、TIMSK0は使用しません。また、OCR0Bも使わないので、残りの5つのレジスタを使用します。では、各レジスタの説明に移りましょう。
TCCR0A | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
COM0A1 | COM0A0 | COM0B1 | COM0B0 | - | - | WGM01 | WGM00 |
このレジスタは、「タイマ/カウンタ0制御レジスタA」というレジスタであり、名前の通りタイマカウンタを制御するためのレジスタの1つ目です。このレジスタは上記の表のようなビット対応になっています。
まず、7,6bit目のCOM0A1・0と、5,4bit目にあるCOM0B1・0は比較出力選択を行うビットです。簡単に言うと、タイマーが一致した場合に、あるIOピンの出力をON/OFFしますか?というのを選択するビット群です。それぞれ2bitずつあるのは、設定の選択肢が複数あるからです。AとBで別れているのは、OC0AピンとOC0Bピンでそれぞれ独立に出力ピンを設定できるようにしている為です。今回はPWMを出力したい訳ではないので、初期値である標準ポート動作のままにしておきます。従って、COM0A1 = 0,COM0A0 = 0,COM0B1 = 0,COM0B0 = 0とします。
3bit目,2bit目は空欄なのでスルーして、次に1bit,0bit目にあるWGM01,WGM00についてです…が、次のレジスタのWGM02で一緒に説明したいと思います。
TCCR0B | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
FOC0A | FOC0B | - | - | WGM02 | CS02 | CS01 | CS00 |
このレジスタは、「タイマ/カウンタ0制御レジスタB」というレジスタであり、名前の通りタイマカウンタを制御するためのレジスタの2つ目です。このレジスタは上記の表のようなビット対応になっています。
まずは、7bit目,6bit目にあるFOC0A,FOC0Bですが、これらは強制的にタイマをマッチさせるためのbitです。タイマを起動後にしか使いませんし、今回のディレイを自分で作ろう!には使う場面がないので、FOC0A=0,FOC0B=0にしておきます。
3bit目はWGM02です。先程のレジスタ(TCCR0A)で出てきたWGM01,WGM00の説明もここで行います。
このWGMとは、波形生成種別設定を行うビットです。勘の良い方は8種類の設定ができるのでは?と気付くかもしれません。が、実は予約用の空きもあるので実質の設定は6種類です。さて、どのような設定ができるのかと言いますと、大きく分けて2つあります。1つ目はタイマ動作、2つ目はPWM動作(タイマ動作にも成り得ます)です。今回はタイマ動作のみでいいので、そこに重点をおいて設定を考えてみます。
番号 | WGM02 | WGM01 | WGM00 | タイマ/カウンタ動作 | TOP値 | OCR0x更新時 | TOV0設定時 |
0 | 0 | 0 | 0 | 標準動作 | $FF | 即時 | MAX |
1 | 0 | 0 | 1 | 8bit移送基準PWM動作 | $FF | TOP | BOTTOM |
2 | 0 | 1 | 0 | 比較一致タイマ/カウンタ解除(CTC)動作 | OCR0A | 即時 | MAX |
3 | 0 | 1 | 1 | 8bit高速PWM動作 | $FF | BOTTOM | MAX |
4 | 1 | 0 | 0 | (予約) | - | - | - |
5 | 1 | 0 | 1 | 位相基準PWM動作 | OCR0A | TOP | BOTTOM |
6 | 1 | 1 | 0 | (予約) | - | - | - |
7 | 1 | 1 | 1 | 高速PWM動作 | OCR0A | BOTTOM | TOP |
タイマ動作のみをさせる設定は、”標準動作”と”比較一致タイマ/カウンタ解除(CTC)動作”の2つがあります。今回はより細かい設定もできるようにしたいので、”比較一致タイマ/カウンタ解除(CTC)動作”に設定します。従って、WGM02=0,WGM01=1,WGM00=0と設定していきます。
次は、2bit,1bit,0bit目にあるCS02,CS01,CS00についてです。
CS02 | CS01 | CS00 | 意味(分周N) |
0 | 0 | 0 | 停止(タイマ/カウンタ動作停止) |
0 | 0 | 1 | Fclk(分周無し) |
0 | 1 | 0 | Fclk / 8(8分周) |
0 | 1 | 1 | Fclk / 64(64分周) |
1 | 0 | 0 | Fclk / 256(256分周) |
1 | 0 | 1 | Fclk / 1024(1024分周) |
1 | 1 | 0 | T0ピンの下降端 (外部クロック) |
1 | 1 | 1 | T0ピンの上昇端 (外部クロック) |
これは、クロック選択が設定できるビットで、要は分周設定のことです。分周というのは、基準となる周波数を1/n倍(nは整数)にしていくことです。分周を設定することで、低い周波数で動かすことが出来ます。分周の設定は、基準となる周波数や、生成周波数等によって設定する値が変わります。また、このCS[2-0]が0の場合は停止となりますが、CS[2-0]が0以外の値になる、つまり分周設定(外部クロック端設定)されると、タイマ動作が開始となります。分周設定と兼ねてスタート動作も兼ねているのです。従ってCSの設定は後に回したいと思います。
TCNT0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
(MSB) | (LSB) |
このレジスタは、「タイマ/カウンタ0」というレジスタであり、タイマをスタートさせると勝手にカウンタの役割を果たします。後にでてくるTOP値と比較して、一致または超えた場合に、これもまた後に説明する割り込み要求フラグが 立ちます。動作中にユーザーがこのTCNTレジスタをいじくると思いもよらない動作をする場合があるので気を付けてください。
OCR0A | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
(MSB) | (LSB) |
このレジスタは、「タイマ/カウンタ0 比較Aレジスタ」というレジスタであり、先程のTCNT0のカウンタの上限値(TOP値)を設定するレジスタでもあります。PWM設定の場合はデューティー比を決めるレジスタでもあるので、用途によって使用方法が変わります、が、今回は上限値(TOP値)を設定するレジスタって認識で大丈夫です。これも後々設定していきたいと思います。
TIFR0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
- | - | - | - | - | OCF0B | OCF0A | TOV0 |
このレジスタは、「タイマ/カウンタ0割り込み要求フラグレジスタ」というレジスタであり、TCNT0がTOP値($FF,OCR0A,OCR0B)を越えた際にフラグをたてるレジスタです。割り込みなんて大仰な書き方していますが、割り込みハンドラのようなものではありません。ここのレジスタはユーザーが読み取りに行くことで、タイマが完了したのかどうかを判断することが出来ます。また、このフラグを解除する場合は、そのビットの場所に1を書き込むことでクリア出来ます。今回の場合は、OCR0Aのみしか使いませんので、1bit目のOCF0Aを監視することでタイマが終わったかの判断に使います。
さて、大方のレジスタの説明を終えたところで、今回作るディレイ動作をしっかりと考えてみたいと思います。
今回は標準で使えるディレイ関数を自分で作ってみようということなので、
/*TCの初期化関数*/ void initTc0( void ){ TCCR0A = 0b00000010; TCCR0B = 0b00000000; } /*msディレイ関数*/ void delayMs( unsigned short msTime ){ //msTime(ms)の値を変換してTOP値を求める TCNT0 = 0; //TCNTの値を0クリア(保険) OCR0A = "msTimeとクロックからTOP値に変換した値"; TCCR0B = ( TCCR0B & 0b11111000 ) | "CSの設定[0以外]"; //分周設定&タイマ開始 while( !(( TIFR0 >> 1 ) & 1 ) ); //OCF0Aが1になるまでwhileで待機 TIFR0 = 0b00000010; //OCF0Aに1を書き込む(クリア) TCCR0B = ( TCCR0B & 0b11111000 ); //CS[2-0]を0にしてタイマ停止 return; }
というような、引数はミリ秒単位で待ってくれるようなディレイ関数にしていきます(””で囲まれたところはまだ決まっていないでの文字でイメージを説明しています)。
しかし、先程のレジスタの説明とも照らし合わせて考えてほしいのですが、8bitのレジスタしか持っていない8bitタイマが、最小単位を1msと設定した時の最大で測れる時間とはどれだけになるでしょうか?
答えは、255msです。つまり1ミリ秒単位の設定にした場合は、最大255ミリ秒までしかこの関数では測ることが出来ません。delayMs関数は1000や2000といった引数を入れても動くようにしたいですよね。
そこで、計測は1msのみの設定にして、delayMs関数内で待機時間分だけ1msのタイマ処理を繰り返すことで実現したいと思います。つまりプログラムで書くと、
/*TC0の初期化関数*/ void initTc0( void ){ TCCR0A = 0b00000010; TCCR0B = 0b00000000; } /*1ms待つ関数*/ void delayOneMs( void ){ //1(ms)の値を変換してTOP値を求める TCNT0 = 0; //TCNTの値を0クリア(保険) OCR0A = "1msとクロックからTOP値に変換した値"; TCCR0B = ( TCCR0B & 0b11111000 ) | "CSの設定[0以外]"; //分周設定&タイマ開始 while( !(( TIFR0 >> 1 ) & 1 ) ); //OCF0Aが1になるまでwhileで待機 TIFR0 = 0b00000010; //OCF0Aに1を書き込む(クリア) TCCR0B = ( TCCR0B & 0b11111000 ); //CS[2-0]を0にしてタイマ停止 return; } /*ms待つディレイ関数*/ void delayMs( unsigned short msTime ){ unsigned short i; for( i = 0 ; i < msTime ; ++i ){ delayOneMs(); //1ms待機 } return; }
となります。こうすることで、255msが最大値だったものが、変数の上限値(今回はunsigned shortなので65535ms)まで引数として与えることが出来ます。
ディレイ関数の基盤もほぼ出来上がったので、あとは設定を見送っていたレジスタを考えていくだけです。
決めないといけないのはCS[2-0]とOCR0Aです。先程レジスタの説明はしましたが、おそらくあれだけだと何がどうなるか分からないと思います。なのでここからは図を用いてタイマの内部の仕組みを解説し、OCR0AとCSについて理解してもらいます。
先程のCSの説明では「分周設定」と話しましたが、CSの設定では、
を選ぶことが出来ます(詳しくは表2参照)。この分周は一体どのような動作をしているのかというと、
図1のように、clk_tというFclk(基準クロック)から1/n倍(図の場合はn=8)のクロックを作っているのです。この"n"は分周比と呼ばれています。この分周をCS[2-0]の設定で変えることが出来ます。
では実際これで何がしたいのかというと、ずばりディレイ関数で作ろうとしていた「1ms待つ」という部分を作りたいのです。図1で作ったclk_tを用いて、実際に1msを作成したのが図2となります。
Fclkとclk_tは、図1と同じものです、ただ縮尺が違います。タイマがスタートすると、TCNT0は0からカウントアップを始めていきます。カウントアップをするタイミングというのが、先程図1で分周させたclk_tの立ち下りです。TCNT0がOCR0A(TOP値)と同じ値になった時、先程説明したTIFR0のOCF0Aのビットが立ちます。TCNT0が0からOCR0Aになるまでかかる時間の事をタイマ周期と呼び、その逆数をタイマ周波数と呼びます。このタイマ周波数が1000[Hz](=1[ms])になるようにすれば、今回のディレイ関数が完成します。
従って、TCNT0がカウントアップするための時間を決める分周、すなわち"CS[2-0]"と、TCNT0のトップ値を決める"OCR0A"を作りたい周期に合わせた設定をする必要があるのです。
今後もタイマを用いる際に周波数の式を使うことがあると思うので、ここで一度タイマ周波数の式を求めてみます。
まずは先程の図2を読み解きます。1000[Hz]のところをFo[Hz]に置き換えると、clk_t周期でカウントアップするTCNT0が、0→OCR0Aまでカウントアップすると1/Fo(s)かかるということになります。つまり、
1/Fo = ( OCR0A + 1 ) * 1/clk_t …①
という式が得られますね。この、"OCR0A+1"としているのは、0からOCR0Aまでカウント=OCR0A+1となるからです。
また図1を読み解くと、基準クロック[Fclk]を[N]で分周することで、clk_tを作っているため、
clk_t = Fclk / N …②
となることがわかります。この①と②を合わせると、
1/Fo = ( OCR0A + 1 ) / ( Fclk / N ) = N * (OCR0A + 1) / Fclk
となります。逆数を取ってタイマ周波数[Fo]を求める形にすると、
Fo = Fclk / ( N * ( OCR0A + 1 ) )
となっています。これがタイマ周波数を求める式となります。
Fclkはマイコンの基準クロックなので1MHzとなり、Nは分周比で[N = 1,8,64,256,1024]から選択できます(表2を参照)。またOCR0AはTOP値のことですね。タイマの周期は1msに設定する必要があるので、Foは1000となります。従って、
1000 = 1000000 / ( N * ( OCR0A + 1 ) )
変形すると、
OCR0A = ( 1000000 / ( N * 1000 ) ) - 1
となりますね。
ここで注意しないといけないのが、OCR0Aは8bitであるという点です。先程も少し出ましたが、8bitということは0-255までの値でしか設定できません。そこで、分周比Nを丁度良い値に設定することで、OCR0Aを0-255の中に整数で収める必要があります。
分周比Nは、1,8,64,256,1024の中からしか選べないので、それぞれ計算してみます。
[分周…1 ] OCR0A = 1000000 / ( 1 * 1000 ) - 1 = 999; [分周…8 ] OCR0A = 1000000 / ( 8 * 1000 ) - 1 = 124; [分周…64 ] OCR0A = 1000000 / ( 64 * 1000 ) - 1 = 14.65; [分周…256 ] OCR0A = 1000000 / ( 256 * 1000 ) - 1 = 2.90625; [分周…1024] OCR0A = 1000000 / ( 1024 * 1000 ) - 1 = -0.0234375;
OCR0Aは0-255の中に入っていないと設定できないので、この場合は分周8が望ましいと言えます。
分周8の設定は、表2を参照すると、CS02 = 0 , CS01 = 1 , CS00 = 0 となります。また、その際に設定するOCR0Aは124となりますね。
という訳で、完成したプログラムが以下のようになります。
/*TC0の初期化関数*/ void initTc0( void ){ TCCR0A = 0b00000010; TCCR0B = 0b00000000; } /*1ms待つ関数*/ void delayOneMs( void ){ TCNT0 = 0; //TCNTの値を0クリア(保険) OCR0A = 124; //[8分周,1ms] OCR0A = 1000000 / ( 8 * 1000 ) - 1 = 124; TCCR0B = ( TCCR0B & 0b11111000 ) | 0b00000010; //分周設定&タイマ開始 while( !(( TIFR0 >> 1 ) & 1 ) ); //OCF0Aが1になるまでwhileで待機 TIFR0 = 0b00000010; //OCF0Aに1を書き込む(クリア) TCCR0B = ( TCCR0B & 0b11111000 ); //CS[2-0]を0にしてタイマ停止 return; } /*ms待つディレイ関数*/ void delayMs( unsigned short msTime ){ unsigned short i; for( i = 0 ; i < msTime ; ++i ){ delayOneMs(); //1ms待機 } return; }
これで_delay_ms()を使わずにディレイを入れるプログラムが書けますね。
また、今回作ったdelay関数をI/Oのページで紹介している点滅プログラムに適用させると、以下の様になります。
#include "avr/io.h" #define F_CPU 1000000UL #include "util/delay.h" /*TC0の初期化関数*/ void initTc0( void ){ TCCR0A = 0b00000010; TCCR0B = 0b00000000; } /*1ms待つ関数*/ void delayOneMs( void ){ TCNT0 = 0; //TCNTの値を0クリア(保険) OCR0A = 124; //[8分周,1ms] OCR0A = 1000000 / ( 8 * 1000 ) - 1 = 124; TCCR0B = ( TCCR0B & 0b11111000 ) | 0b00000010; //分周設定&タイマ開始 while( !(( TIFR0 >> 1 ) & 1 ) ); //OCF0Aが1になるまでwhileで待機 TIFR0 = 0b00000010; //OCF0Aに1を書き込む(クリア) TCCR0B = ( TCCR0B & 0b11111000 ); //CS[2-0]を0にしてタイマ停止 return; } /*ms待つディレイ関数*/ void delayMs( unsigned short msTime ){ unsigned short i; for( i = 0 ; i < msTime ; ++i ){ delayOneMs(); //1ms待機 } return; } int main( void ){ //初期化 DDRD = 0b00000010; //1bit目を1,その他を0に PORTD = 0b00000000; //全ビットを0に initTc0(); //タイマカウンタ0を初期化 //点滅 while( 1 ){ PORTD = 0b00000010; //1bit目をHIGHに delayMs( 500 ); //500msec wait PORTD = 0b00000000; //1bit目をLOWに delayMs( 500 ); //500msec wait } return 0; }
今回はタイマーを駆使して標準で動かせるディレイ関数と似た動作をするプログラムを作りました。実はこのプログラム、ただのポーリング処理なので無駄極まりない動作をしています。また、重大なことを1つ話し忘れていましたが、
正確な時間では動きません!
えっ…て思われるかもしれませんが、ごめんなさい、動かないんです…
それなりの正確さで動くとは思いますが、このディレイ関数を用いて時計を作ろうとか思わないでください。おそらく1日に数十分くらいずれると思います。
どうして正確に動かないのかというと、
の2点が挙げられます。1つ目はそもそも内部クロックがRCの適当クロックなので、それほど精度が良くありません。2つ目は、端的に言うとプログラムの書き方が悪いということです。
ですが今回は勉強用としてタイマを使ったプログラムを書いたので、それほど難しい処理を入れるわけにはいかなかったんです…
もしやる気があれば、何がいけないのか考えて、より精度の良いディレイ関数を作ってみてください。
質問なども承ってますので、BBSかメールから1報いれてもらえると個別でご対応します。