fiord Advent Calendar 2025 2 日目の内容となります。
前日の記事 でタイマーレジスターを含めた複数の方法で L チカを実装しました。タイマーレジスタ―がどのように動作しているかについては説明を省いたため、この記事で解説を行います。
この方法では、Arduino が搭載しているマイコンの種類によって操作方法が変わります。今回は Arduino UNO R3 を対象に、ATmega328P を前提とします。
割り込みの種類
下記の割り込みが ATmega328P にはあります(データシートより抜粋)
| Vector No. | プログラムアドレス | 割り込みベクタ名(AVR記号) | 割り込み要因 |
|---|---|---|---|
| 1 | 0x0000 | RESET | リセット(外部ピン、電源ON、ブラウンアウトリセット、ウォッチドッグ) |
| 2 | 0x0002 | INT0 | 外部割り込みリクエスト 0 |
| 3 | 0x0004 | INT1 | 外部割り込みリクエスト 1 |
| 4 | 0x0006 | PCINT0 | ピンチェンジ割り込みリクエスト 0(PBポート) |
| 5 | 0x0008 | PCINT1 | ピンチェンジ割り込みリクエスト 1(PCポート) |
| 6 | 0x000A | PCINT2 | ピンチェンジ割り込みリクエスト 2(PDポート) |
| 7 | 0x000C | WDT | ウォッチドッグ・タイムアウト割り込み |
| 8 | 0x000E | TIMER2 COMPA | Timer/Counter2 比較一致A |
| 9 | 0x0010 | TIMER2 COMPB | Timer/Counter2 比較一致B |
| 10 | 0x0012 | TIMER2 OVF | Timer/Counter2 オーバーフロー |
| 11 | 0x0014 | TIMER1 CAPT | Timer/Counter1 キャプチャイベント |
| 12 | 0x0016 | TIMER1 COMPA | Timer/Counter1 比較一致A |
| 13 | 0x0018 | TIMER1 COMPB | Timer/Counter1 比較一致B |
| 14 | 0x001A | TIMER1 OVF | Timer/Counter1 オーバーフロー |
| 15 | 0x001C | TIMER0 COMPA | Timer/Counter0 比較一致A |
| 16 | 0x001E | TIMER0 COMPB | Timer/Counter0 比較一致B |
| 17 | 0x0020 | TIMER0 OVF | Timer/Counter0 オーバーフロー |
| 18 | 0x0022 | SPI, STC | SPI 転送完了 |
| 19 | 0x0024 | USART, RX | USART 受信完了 |
| 20 | 0x0026 | USART, UDRE | USART データレジスタ空(送信可能) |
| 21 | 0x0028 | USART, TX | USART 送信完了 |
| 22 | 0x002A | ADC | ADC 変換完了 |
| 23 | 0x002C | EE READY | EEPROM 書き込み/読み込み準備完了 |
| 24 | 0x002E | ANALOG COMP | アナログコンパレータ出力変化 |
| 25 | 0x0030 | TWI | 2線式シリアルインターフェース(I²C) |
| 26 | 0x0032 | SPM READY | ストアプログラムメモリ準備完了(ブートローダ用) |
このうち、今回は 0x000E から 0x0020 までの範囲がタイマー割り込みとして利用可能な種類となります。Timer/Counter に種類があるので、それぞれどのように利用されているかも見てみましょう。
タイマーの種類
| Timer | bit 幅 | PWM で対応するピン | デフォルトの用途 |
|---|---|---|---|
| Timer0 | 8bit | 5, 6 |
millis()、delay() などで利用 |
| Timer1 | 16bit | 9, 10 | Servo ライブラリで利用 |
| Timer2 | 8bit | 3, 11 |
tone() が利用 |
ATmega328P は 16MHz で動作します。「分周」という概念について後述しますが、8bit カウンターだと最大でも約 16ms に 1 度は割り込みを起こす必要があります。一方で、16bit では非常に多様なタイミングで割り込みを起こすことが出来ることから、基本的には Timer1 の利用をすることが望ましいと個人的には考えています。
以降は Timer1 を利用することを前提に話を進めます。
Timer1 に関連するレジスタについて
| レジスタ | ビット | 役割 |
|---|---|---|
| TCCR1A | COM1A1:0, COM1B1:0 | PWM出力ピンの動作(割り込みだけなら 00 でOK) |
| WGM11:10 | 波形生成モードの下位2bit | |
| TCCR1B | ICNC1 | 入力キャプチャノイズキャンセラ |
| ICES1 | 入力キャプチャエッジ選択 | |
| WGM13:12 | 波形生成モードの上位2bit | |
| CS12:10 | クロック選択(プリスケーラ設定) | |
| TCCR1C | FOC1A/B | 強制出力比較(通常不要) |
| TCNT1 (16bit) | 現在のカウンタ値(自動でインクリメント) | |
| OCR1A / OCR1B | 比較一致値(ここに値を入れる) | |
| TIMSK1 | OCIE1A/B | 比較一致割り込みA/B 有効化 |
| ICIE1 | 入力キャプチャ割り込み | |
| TOIE1 | オーバーフロー割り込み | |
| TIFR1 | 割り込みフラグ(自動or手動でクリア) |
「ビット」の欄の : の前後の数値は配列の添え字の範囲で、例えば CS12:10 には CS12、CS11、CS10 が内包されています。
この表から、例えば TCCR1B は ICNC1、ICES1、WGM13:12、CS12:10 の 4 つのパラメータを持っていることが分かります。WGM12 を有効化(1 に変更)するには、下記のようにすればよいです。
TCCR1B |= (1 << WGM12);
一方で、 CS12 を無効化(0 に変更)するには、下記のように変更すればよいです。
TCCR1B &= ~(1 << CS12);
波形生成モード
WGM13:10 の値の内容により、どのように割り込みを起こすかの処理方法に違いがあります。
原則として、マイコンのクロックと共に内部でカウンターの数値が上がっていき、特定のタイミングで割り込みが発生します。
| WGM13 | WGM12 | WGM11 | WGM10 | モード名 | カウント範囲 | 割り込み発生タイミング | 特徴 |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | Normal | 0 ~ 65,535 | 65,535 → 0 の時 | Timer0 はこれで動作 |
| 0 | 0 | 0 | 1 | PWM, Phase Correct, 8-bit | 0→255→0 | 0/255 到達時 | |
| 0 | 0 | 1 | 0 | PWM, Phase Correct, 9-bit | 0→511→0 | 0/511 到達時 | |
| 0 | 0 | 1 | 1 | PWM, Phase Correct, 10-bit | 0→1,023→0 | 0/1,023 到達時 | |
| 0 | 1 | 0 | 0 | CTC | 0 ~ OCR1A
|
OCR1A 到達時 |
非常に汎用性が高い |
| 0 | 1 | 0 | 1 | Fast PWM, 8-bit | 0 ~ 255 | 255 到達時 | |
| 0 | 1 | 1 | 0 | Fast PWM, 9-bit | 0 ~ 511 | 511 到達時 | |
| 0 | 1 | 1 | 1 | Fast PWM, 10-bit | 0 ~ 1,023 | 1,023 到達時 | |
| 1 | 0 | 0 | 0 | PWM, Phase and Frequency Correct | 0→ICR1→0 |
0 | モーターで利用されることがある |
| 1 | 0 | 0 | 1 | PWM, Phase and Frequency Correct | 0→OCR1A→0 |
0 | モーターで利用されることがある |
| 1 | 0 | 1 | 0 | PWM, Phase Correct | 0→ICR1→0 |
0/ICR1 到達時 ` |
モーターで利用されることがある |
| 1 | 0 | 1 | 1 | PWM, Phase Correct | 0→OCR1A→0 |
0/OCR1A 到達時 ` |
モーターで利用されることがある |
| 1 | 1 | 0 | 0 | CTC | 0 ~ ICR1
|
ICR 到達時 |
OCR1A をパルス幅に利用可能な点が魅力 |
| 1 | 1 | 0 | 1 | (Reserved) | |||
| 1 | 1 | 1 | 0 | Fast PWM | 0 ~ ICR1
|
ICR1 到達時 |
analogWrite で利用 |
| 1 | 1 | 1 | 1 | Fast PWM | 0 ~ OCR1A
|
OCR1A 到達時 |
1,000Hz 以上の高速 PWM |
基本的には wGM13:10 が 0b0100 の CTCモードを利用することが一般的です。
TCCR1A = 0; // COM1A/COM1B は 0で良いです。そうでない場合 9/10 ピンへの出力が行われます。
TCCR1B = (1 << WGM12); // 他の設定は後述
分周比について
CTC で利用する OCR1A は 16-bit、つまり 65,535 までカウントが出来ます。
しかし、16MHz でカウントされるので、約 4ms でカウント上限に到達してしまいます。4ms より大きな周期で割り込みが行いたい際に 分周比(Prescaler) というものを利用します。
分周比は特定のクロック数毎にカウントを 1 上昇させる、というものです。
| CS12 | CS11 | CS10 | 分周比 |
|---|---|---|---|
| 0 | 0 | 1 | 1 |
| 0 | 1 | 0 | 8 |
| 0 | 1 | 1 | 64 |
| 1 | 0 | 0 | 256 |
| 1 | 0 | 1 | 1,024 |
例えば分周比が 64 の場合、64 クロック毎、つまり 16MHz/64 = 250kHz でカウンターが上昇します。OCR1A の値の範囲とこの分周比により、非常に多様な周期での割り込みが出来るようになります。
実践編
では、実際に 50ms 毎(20 Hz)に割り込みを発生させてみましょう。各分周比での必要カウント数は下記のようになります。
| 分周比 | 計算式 | 必要カウント数 |
|---|---|---|
| 1 | 16MHz/(Prescaler: 1)/20Hz | 800,000 |
| 8 | 16MHz/(Prescaler: 8)/20Hz | 100,000 |
| 64 | 16MHz/(Prescaler: 64)/20Hz | 12,500 |
| 256 | 16MHz/(Prescaler: 256)/20Hz | 3,125 |
| 1024 | 16MHz/(Prescaler: 1024)/20Hz | 781.25 |
ここで、分周比 1/8 は OCR1A の上限を超えてしまうため利用できません。一方で、1,024 も必要カウント数が小数になってしまいました。
782 カウントにすると、782*1024/16MHz=50.048ms 秒単位での割り込みとなります。これは正確な割り込みにならないため避けるべきでしょう。
従って 64 もしくは 256 を利用することが望ましいです。
実装
実装に入っていくのですが、ここでは分周比 256 を利用します。
重要な点として、「必要カウント数」には OCR1A → 0となるステップ数を含むため、値を 1 減らしておきましょう。
void setup() {
noInterrupts(); // 割り込み出来ないように
// 初期化
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// prescaler: 256, 必要カウント数: 3125
OCR1A = 3124; // 16MHz / 256 / (3124 + 1) = 20 Hz
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS12); // prescaler 256
TIMSK1 |= (1 << OCIE1A); // OCR1A との比較一致割り込みを有効化
interrupts(); // 割り込みスタート
}
まとめ
タイマー割り込みを元に、Arduino の裏側にあるマイコンの機能を直接利用する、という感覚を少し身に着けてもらえたら嬉しいです。
また、書いていて感じた本末転倒なことではございますが、結局のところデータシートを読みながら理解、実装を行う形は避けられないと感じています。この記事でも記載はしたものの、「ここまで触れると長すぎる記事になってしまう」として避けた内容が多いです。故に一部中途半端に感じられる箇所が多いと思います。
現代は生成 AI を用いてデータシート上から必要な内容をピックアップすることも出来ますので、是非データシートを読み込んでみることをオススメします。