Timerを理解する(Timer1メイン)

Timer1をレジスタを制御する方法で使えればかなり便利なのではないかとおもい、利用法を調べてみました。また、ステッピングモータの角速度を割り込み処理でうまいこと制御したいので調べました。
ライブラリによる利用は
http://mangoooou.blogspot.jp/2017/09/timer1.html
にまとめました(わかりにくいですが)
ライブラリを読んでわかったことを書いてます(わかりにくいですが)
http://mangoooou.blogspot.jp/2018/03/timer1.html

まず、参考にしたブログです
http://usicolog.nomaki.jp/engineering/avr/avrPWM.html
http://startelc.com/AVR/Avr_100timrMemo.html
https://sites.google.com/site/qeewiki/books/avr-guide/timers-on-the-atmega328
https://avr.jp/user/DS/PDF/mega328P.pdf (データシートが日本語訳されてる!)

つぎに、レジスタ名などが省略されて表記されているためにわかりにくいので
予想もはいってますがフルネームを書こうと思います
TCCR          : Timer/Counter Control Register     ->モードの設定、分周比の設定、etc
TCNT             : Timer/Counter Register (CNT = counter)  ->現在のカウンターの値を保存
TIMR(TIMSK) :  Timer Interrupt Mask Register      ->割り込み許可
TIFR              :  Timer Interrupt Flag Register   ->割り込みフラッグ
OCR              :  Output Compare Register            ->比較する値
ICR               :  Input Capture Register              ->TCNTのキャプチャレジスタ

bit 名
TOV  : Timer Overflow
TOIE   : Timer Overflow Interrupt Enable
COM    :おそらく Compare match Mode
WGM :Waveform Generator Mode
OCF  : Output Compare Flag(コンペアマッチすると立つフラッグ)

pin名
T1 :PD5 ->pin5
T0   :PD4 ->pin4
OC0A : PD6 ->pin6
OC0B : PD5 ->pin5
OC1A:PB1 -> pin9
OC1B:PB2->pin10



次に、Arduinoをカウンターとして使用してみたいと思います。
#include<avr/io.h>
をインクルードすれば、レジスタ名、bit名がそのまま利用できます。(下のプログラムではなぜか、うまく表示されていない)
デフォルトのレジスタの値を読んでみます(Timer0 0とO の区別に注意)
#include


void setup() {
  Serial.begin(9600);
  //TCCR1A=0x13;
  Serial.print("TCCR0A   ");
  Serial.println(TCCR0A,BIN);
  Serial.print("TCCR0B   ");
  Serial.println(TCCR0B,BIN);
  Serial.print("OCR0A      ");
  Serial.println(OCR0A   ,BIN);
  Serial.print("OCR0B      ");
  Serial.println(OCR0B   ,BIN);
  Serial.print("TCNT0   ");
  Serial.println(TCNT0,BIN);
  Serial.print("TIMSK0   ");
  Serial.println(TIMSK0,BIN);
  Serial.print("TIFR0     ");
  Serial.println(TIFR0,BIN);
  
}

void loop() {
  // put your main code here, to run repeatedly:

}




得られる結果としては
TCCR0A   11       ->高速PWM(TOP:0xFF)、波形出力なし、
TCCR0B   11       ->分周比1/64
OCR0A      0                      ->比較値は0x00
OCR0B      0
TCNT0   1111111             ->カウンタはずっとこの値で変動しませんでした
TIMSK0   1                       ->TOIE0のみ有効(overflow割り込み)
TIFR0     110                     ->TOV:0,OCF0A:1,OCF0B:1

TCNTがなぜずっと変動しなかったのかわかりません。
->(追記)原因がわかりました。リセットから読むまでの時間は同じであるため常に同じ値を示すようです。Serial.println(TCNT0,BIN);の前にdelay以外(timer0を使用するため)の命令を入れると変動します。

デフォルトで高速PWMなのはPWMを使用することが多いからでしょう。ステッピングモータは周波数変動でduty比変動ではないのでPWMについては検証しません。


Timer0でT0から手動でクロックを送りカウンタの値を読み込んでみます。
モードを「標準」、CSを 111 にすることで立ち上がりでカウントアップしてくれます。回路として、pin4にスイッチをつけます。
#include<avr/io.h>
void setup(){
TCCR0A=0x00;
TCCR0B=0x07;
Serial.begin(9600);
}
void loop(){
Serial.println(TCNT0,HEX);
}
でカウンターになります。
標準モードでは0x01から0xFFまで変動し、0xffの後0x00に戻ります。
TIFR0の挙動は面白く、0xff->0x00->0x01(ここで0x03になります)

TCCR0A=0x02;
TCCR0B=0x07;
に変えると、CTCモードになります。
TCNTが1週目は0x01->0xff->0x00で変動しなくなります。これはOCRが0x00に設定されているためです。

OCR0A=0x68; と設定すると
1週目から0x01->0x68->0x00(OCF0A(コンペアマッチAのフラッグ)はここで1になる)->0x68 ... と変動していきます.
#include
#include

void setup() {
  Serial.begin(9600);
  TCCR0A=0x02;
  TCCR0B=0x07;
  OCR0A=0x68;
}

void loop() {
  Serial.print(TCNT0,HEX);
  Serial.print("  ");
  Serial.println(TIFR0,BIN);
  if(Serial.available()>0){
    int k=Serial.read();
    if(k=='j'){
      OCR0A=0x32;    
    }
  }

}

としてカウンタが0x32以上で’j’を送り、OCR0Aを変更した際の挙動を確認すると
カウンタはリセットされず0xFFまでカウントアップし0x00に戻り、0x32まで行って0x00に戻るという感じになりました。
をみてわかるとおりTCNTとOCRが 「=」の時にTOP(->リセット)が送られます。
よってOCR以上にTCNTが合ったとしても、リセットは送られないため0xFFまでカウントアップされる事になります。

COM0A,COM0B をいじることで、直接出力にすることができるみたいです。
OC0A : PD6 ->pin6
OC0B : PD5 ->pin5

からわかるとおり、pinModeの設定は必要そうです。
pinMode(6,OUTPUT);
TCCR0A=0xc2;  //COM0A -> 11 :コンペアマッチでHIGH
にするとTIFR0のOCF0Aが1の時pin6がHIGHになります。ずっとHIGH。おそらくflagを下ろさないといけないのでしょう。->(追記)フラッグは1を書き込むことで0にすることができます。下ろしたところ、pin6は変化しませんでした。おそらく、CTCモードでは「トグル」以外は意味がないでしょう。なぜなら、「11:コンペアマッチでHIGH」はおそらく、OCR以下ではHIGHということだと思われるからです。初動でpin6がLOWなのはコンペアマッチするまではデフォルトのままであるからでしょう。

TCCR0A=0x42;  //COM0A -> 01 :コンペアマッチでトグル
に変更するとOCRで設定した値をTCNTが超えた瞬間、出力(pin6)が反転します。ただし、OCFR0Aは1のまま。

TCCR0B の FOC0Aを1にセットするとOC0A の出力が変動します。FOC0Aは強制的にコンペアマッチが発生したことにしてくれます。割り込みは発生しません。また、自動的に0に戻ります。

TCNTはTCCR0Bを変更した際にリセットはかかりません。


内部クロックの周波数を検証してみましょう。
分周比1/1024、カウント数OCR0Aに0xff を入れたので255+1 = 256 (0もカウントに使われるため)、システムクロック16MHz
1/16us *1024*256 = 16.384 ms

#include
#include

void setup() {
  Serial.begin(9600);
  TCCR0A=0x42;
  TCCR0B=0x05;
  OCR0A=0xFF;
  pinMode(6,OUTPUT);
}

void loop() {
  //Serial.print(TCNT0,HEX);
  //Serial.print("  ");
  //Serial.println(TIFR0,BIN);
  //TIFR0=0x02;
  if(Serial.available()>0){
    int k=Serial.read();
    if(k=='j'){
      TCCR0B=0x87;  
      Serial.println(TCCR0B,HEX);
      while(true){
      
      }
    }
  }

}



このようなプログラムの結果をロジアナで測定してみると
トグルがかかるまでが計算時間なので、目的通りトグルしていることが確認できます。
OCR0Aに+1するところはOCE0Aに0x06とか小さい値を入れると確認できます。


次に割り込みを入れてみましょう。デフォルトではOverflowのTOIEのみ有効になってます。

 まず、全割り込み許可という名前の通り全割り込みをOK or Neglect するレジスタがあります。
 次にTIMSK (Timer Interrupt MaSK Register)に
TOIE (TOp Interrupt Enable) : TOV(Top OVerflow Interrupt)を許可するbit
OCIE :OCF( Output Compare Flag(コンペアマッチすると立つフラッグ))の許可
という2つの許可があります。
 全割り込み許可->TIMSKの変更で割り込みがはいるようになります(どっちを先にすべきはわかりません)。

 全割り込み許可は
#include<avr/interrupt.h>
をincludeしたあと、
sei(); //set interrupt で全割り込み許可
cli(); //clear interrupt で全割り込み無視
で決められます。
 
 はいった割り込みは割り込みベクタがあるので
ISR(ベクタ名){

}
でISR内の処理が実行されます。
TIMER1_COMPA_vect : OCR1Aとの一致
TIMER1_COMPB_vec     :   OCR1Bとの一致


2.Timer1について

Timer0との大きな違いは、バグか仕様かOCR1A,Bの書き換えがモード設定をする前にできないということです。(TCCR1A,B,Cを書いた後は問題ないです)
最初に
OCR1AH = 0x06;
  OCR1AL = 0x56;
  OCR1BH =0x45;
  OCR1BL =0x3D;

  TCCR1A = 0x40;
  TCCR1B = 0x0D; // CTC OCR , 1/1024
  TCCR1C = 0x00;
  TIMSK1 = 0x06;
このようにセットアップすると
TCCR1A   1000000
TCCR1B   1101
TCCR1C   0
OCR1AL      0
OCR1AH      0
OCR1BL      0
OCR1BH      0
TCNT1L   11000011
TCNT1H   1001100
TIMSK1   110
TIFR1     100000
こんな感じにレジスタの内容が変わります。
OCR1が書き換わってないのが明らかです。また、このまま時間が経つとTCNTが最大値までカウントアップされるのですが、0x0000に変化したときOCRと一致するためCTCモードでは0x0000に戻され、を繰り返し、TCNTが変化しなくなります。

サンプルプログラム(OCRが書けない)
#include
#include
ISR(TIMER1_COMPA_vect){
  
}
ISR(TIMER1_COMPB_vect){
  
}


void setup() {
  
  Serial.begin(115200);
  OCR1AH = 0x06;
  OCR1AL = 0x56;
  OCR1BH =0x45;
  OCR1BL =0x3D;
  
  TCCR1A = 0x40;
  TCCR1B = 0x0D; // CTC OCR , 1/1024
  TCCR1C = 0x00;
  TIMSK1 = 0x06;

  /*
  OCR1AH =0x45;
  OCR1AL =0x3D;
  
  OCR1BH =0x45;
  OCR1BL =0x3D;


  TCCR1A = 0x40;
  TCCR1B = 0x0D; // CTC OCR , EX input
  TCCR1C = 0x00;
*/
  
  pinMode(5,INPUT);
  sei();
}

void loop() {
  /*
  //Serial.print("TCNT1L   ");
  Serial.print(TCNT1L,BIN);
  Serial.print("   ");
  Serial.println(TCNT1H,BIN);
  */
  Serial.print("TCCR1A   ");
  Serial.println(TCCR1A,BIN);
  Serial.print("TCCR1B   ");
  Serial.println(TCCR1B,BIN);
  Serial.print("TCCR1C   ");
  Serial.println(TCCR1C,BIN);
  
  byte sai = OCR1AL;
  byte kai = OCR1AH;
  Serial.print("OCR1AL      ");
  Serial.println(sai   ,BIN);
  Serial.print("OCR1AH      ");
  Serial.println(kai   ,BIN);
  Serial.print("OCR1BL      ");
  Serial.println(OCR1BL   ,BIN);
  Serial.print("OCR1BH      ");
  Serial.println(OCR1BH   ,BIN);
  Serial.print("TCNT1L   ");
  Serial.println(TCNT1L,BIN);
  Serial.print("TCNT1H   ");
  Serial.println(TCNT1H,BIN);
  Serial.print("TIMSK1   ");
  Serial.println(TIMSK1,BIN);
  Serial.print("TIFR1     ");
  Serial.println(TIFR1,BIN);
  
}


includeのみ正しく表示されないので
#include<avr/io.h>
#include<avr/interrupt.h>
に書き換えてください。

Timer1は2バイトのカウンタを持つため、Timer0,2と違い、OCR,TCNTの書き方、読み込み方に順序があります。なぜなら、Atmega328pは1バイトのデータバスで動くため、ど2byteを同じタイミングで読むことはソフト的には不可能であるからです。そのため、ハード的に上位バイトに一時レジスタがついています。下位バイトのアクセスが一時レジスタの読み込み、書き込みになります。注意点としては一時レジスタがOCR,TCNT etcそれぞれに1つではなく、共通の1つのレジスタであることです。
書き込む際は 上位->下位 の順に書き。
読み込む際は 下位->上位 の順に読みます。

書いてないサイトもありますが、TCCR1Cが存在します。これは、強制的にコンペアマッチが発生したとさせるためのレジスタです。CTCでトグルにセットしておくと意味を持ちます。
サイト2からレジスタマップです
レジスタで書くとプログラムの可読性が低くなるのできついですが、私にはこの方法しか知りません。

外部pin入力の変化でTCNT(Timer Counter)を直接Captureする(ICR(Input Capture Register)に保存する)機能が存在します。私は使わないと思うので調べませんが、便利かもしれません。

略語が多すぎて読みにくいかもしれないですが、機能としては難しくないので頑張って見てください。

3/28 追記
Timer1のTCNTは初期値を0x0000に設定しておかないと、
OCRBが0x0002、トグルとした場合
TCNTの最初の状態が0x0003となってしまった場合パルスが反転して思った挙動にならないので注意が必要です。
OCR割り込みが起こるのは一致した瞬間ではなく、一致を確認した次のカウントで入ります。例えばOCR1Aを0x0008にした場合、0x0000にカウンタが変化した瞬間に割り込みが入ります。OCR1Bを0x0004にした場合、0x0005にカウンタが変化した瞬間に割り込みが入ります。

#include
#include
ISR(TIMER1_COMPA_vect) {

}
ISR(TIMER1_COMPB_vect) {
  TCCR1C=0x80;
}


void setup() {

  Serial.begin(9600);
  cli();
  TCCR1A = 0x40;
  TCCR1B = 0x0F; // CTC OCR , EX input
  TCCR1C = 0x00;

  OCR1AH = 0x00;
  OCR1AL = 0x08;

  OCR1BH = 0x00;
  OCR1BL = 0x02;




  pinMode(5, INPUT);
  pinMode(9,OUTPUT);
  TCNT1H=0x00;
  TCNT1L=0x00;
  TIMSK1 = 0x06;
  sei();
}

void loop() {
  byte saL = TCNT1L;
  byte kaH = TCNT1H;
  int  sai = saL | (kaH << 8);
  if(sai==2){
    TCNT1H=0x00;
    TCNT1L=0x00;
  }
  Serial.println(sai);
}
このプログラムでpin5にスイッチ、pin9にLEDをつけて実験すると
カウンタが2になるとif分によりカウンタは0に戻されます。OCR1Bは2なのでこの割り込みが入っているかを確かめるための実験です。
 実験結果としては、カウンタは0->1->0->1 ...を繰り返しました。LEDはずっと消灯したままです。つまり0x02の時に割り込みフラグとかいろいろはないということです。

3/30 追記
トグルに設定した状態は初期状態が不安定なので初期状態をしっかり設定してあげる必要がありそうです。
次の2つのプログラムは初期状態が逆になってます。割り込みがなくても反転するのでcli();に意味はありません。

#include
#include
ISR(TIMER1_COMPA_vect) {

}
ISR(TIMER1_COMPB_vect) {
  TCCR1C = 0x80;
}


void setup() {

  Serial.begin(9600);
  cli();
  TCCR1A = 0x40;
  TCCR1B = 0x0D; // CTC OCR , EX input
  TCCR1C = 0x00;

  OCR1AH = 0x00;
  OCR1AL = 0x08;

  OCR1BH = 0x00;
  OCR1BL = 0x02;
  for (int i = 0; i <= 90; i++) {
    Serial.println("waiting to change pin9");
  }


  TCCR1B = 0x0F; // CTC OCR , EX input
  pinMode(5, INPUT);
  pinMode(9, OUTPUT);
  TCNT1H = 0x00;
  TCNT1L = 0x00;
  TIMSK1 = 0x06;
  sei();
}

void loop() {
  byte saL = TCNT1L;
  byte kaH = TCNT1H;
  int  sai = saL | (kaH << 8);
  Serial.print(sai);
  int kai = PINB & _BV(1); //pin9読み込み
  Serial.print("   ");
  Serial.println(kai);
}

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#include
#include
ISR(TIMER1_COMPA_vect) {

}
ISR(TIMER1_COMPB_vect) {
  TCCR1C=0x80;
}


void setup() {

  Serial.begin(9600);
  cli();
  TCCR1A = 0x40;
  TCCR1B = 0x0F; // CTC OCR , EX input
  TCCR1C = 0x00;

  OCR1AH = 0x00;
  OCR1AL = 0x08;

  OCR1BH = 0x00;
  OCR1BL = 0x02;




  pinMode(5, INPUT);
  pinMode(9,OUTPUT);
  TCNT1H=0x00;
  TCNT1L=0x00;
  TIMSK1 = 0x06;
  sei();
}

void loop() {
  byte saL = TCNT1L;
  byte kaH = TCNT1H;
  int  sai = saL | (kaH << 8);
  Serial.print(sai);
  int kai=PINB & _BV(1); //pin9読み込み
  Serial.print("   ");
  Serial.println(kai);
}



ちがいとしては、前者はsetup時に一度1/1024で勝手にカウンタが動くようにした後時間を空けています。そうすることで
カウンタ  pin9の状態(前者)   pin9の状態(後者)
0     LOW           HIGH
1     LOW           HIGH
2     LOW           HIGH
3     HIGH          LOW
4     HIGH          LOW
5     HIGH          LOW
6     HIGH          LOW
7     HIGH          LOW
8     HIGH          LOW
0     HIGH          LOW
0からはまた繰り返す
というふうになります。
そのため、トグルとして運用するには初期段階または、動作中にpin9の状態を読み(出力状態でも読み込むことができる)、状態に合わせて、FOCを使い(TCCR1C=0x80;)強制的に状態を合わせておかないと反転波形が発生する可能性があります。

さらに調べるとよくわからなくなってきました。

#include
#include
ISR(TIMER1_COMPA_vect) {

}
ISR(TIMER1_COMPB_vect) {
  TCCR1C = 0x80;
}


void setup() {

  Serial.begin(9600);
  cli();
  TCCR1A = 0x40;
  TCCR1B = 0x0D; // CTC OCR , EX input
  TCCR1C = 0x00;

  OCR1AH = 0x00;
  OCR1AL = 0x08;

  OCR1BH = 0x00;
  OCR1BL = 0x02;
  for (int i = 0; i <= 90; i++) {
    Serial.println("waiting to change pin9");
  }


  TCCR1B = 0x0F; // CTC OCR , EX input
  pinMode(5, INPUT);
  pinMode(9, OUTPUT);
  int kai = PINB & _BV(1); //pin9読み込み
  if(kai==2){//HIGH
    TCCR1C = 0x80; //強制LOW
  }
  TCNT1H = 0x00;
  TCNT1L = 0x00;
  TIMSK1 = 0x06;
  sei();
  int kai2 = PINB & _BV(1); //pin9読み込み
  Serial.println("wait");
  int kai3 = PINB & _BV(1); //pin9読み込み
  Serial.print(kai);
  Serial.print("  kai ");
  Serial.print(kai2);
  Serial.print("  kai ");
  Serial.println(kai3);
  if(kai2==2){
    Serial.println("forced");
  }
}

void loop() {
  byte saL = TCNT1L;
  byte kaH = TCNT1H;
  int  sai = saL | (kaH << 8);
  Serial.print(sai);
  int kai = PINB & _BV(1); //pin9読み込み
  Serial.print("   ");
  Serial.println(kai);
}
このようなプログラムでどこでpin9の出力が変化するかを調べると
kai->0(OFF)
kai2->0(OFF)
kai3->2(ON)
と時間経過によって出力が変化しています。(出力が反映されるのに時間がかかる可能性もあり)
TCNTの書き込みが比較一致を生成しないと見られる文があったのでよくわからなくなりました。(TCNTを0にしてるのでそこでOCRAに引っかかってる?と仮定した場合。追記:引っかかりません)
(引用_日本語化atmega328pのdatasheet p.88より)
20.10.2. TCNT1書き込みによる比較一致妨害
TCNT1への全てのCPU書き込みは、例えタイマ/カウンタが停止されていても、次のタイマ/カウンタ クロック周期で起こるどんな比較一致をも
妨げます。この特質はタイマ/カウンタ クロックが許可されている時に、割り込みを起動することなく、TCNT1と同じ値に初期化されることを
OCR1xに許します。

sei();を移動したところkaiの反応も変化しました。
上記の原因がわかりました。カウンタ一致割り込みは
OCRと一致ー>フラッグが立つー>割り込みが入る
というわけで、全割り込み許可前にフラッグが立っていると、全割り込みが許可された瞬間割り込み処理が実行されるため、上記のエラーが起きていました。(現在は一致していなくても、以前にフラグが立ってしまっていると実行されるのがポイント)
よって、対処としては
割り込み許可前にflagをおろしておく または
状態に合わせて無理やり初期化しておくの2つがあるでしょう。
後者ではsei();の後pin9を読み状態に合わせて変化させてみましたが、ダメでした。
  sei();
  kai2 = PINB & _BV(1); //pin9読み込み
  if(kai2==2){
    Serial.println("forced");
    TCCR1C = 0x80;
  }
おそらく,pin9の変化に時間が多少かかるのでしょう。
前者ではうまくいきました。
TIFR1 = 0x06;
sei();
とすると、OCRA、B両方フラグをおろせます。これが一番よいでしょう。

Timer1の設定の順序は
(全割り込み禁止:初期状態で禁止されてるかも)->TCCR(モード、CS)ー>OCR->TIMSK(割り込み許可)ー>TCNT初期化ー>TIFR(フラグレジスタ)の初期化ー>全割り込み許可
の順がよいと思います。

次にOCRの書き換えが割り込みを発生させるかどうかを検証します。
TCNTが7 OCRAが8の時 OCRを7に書き換えると次のクロックでTCNTが0になります。(TCNTが8の時にOCRを7に変えると0xFFFFになるまでTCNTが0x00に戻ってきません)

コメント