今回はAVR-RustでAVRマイコンの
「割り込み」 をどう実現するか具体例を取り上げて説明していきます。
単純な機能を使うだけなら、メインルーチンだけでことが済むので、割り込み処理(サブルーチン)をさほど理解せずともなんとなく動作するプログラムが出来てしまします。
ただAVRマイコンのみならず、ある程度プログラムの規模が大きくなってきたときには、割り込み処理を使うことが必須になってきます。
マイコンの持つ能力を余すこと無く応用した完成品を作成したいなら、じっくりと割り込みの裏側の理解をしていきましょう。
なおこの記事は、
RustでArduinoライクに使えるものを作ってみる ことをテーマに複数回の記事に渡って連載しているところの第5回目です。
大体この記事もそろそろ応用的な内容に差し掛かってきまして、基礎的な内容から大分遠くなってきました。
途中、既知として前回までで利用したテクニックを使っている箇所が出てきますが、ここでは詳しい解説はしませんので、以下のリンクで知りたい内容があればそちらでお願いします。
AVRマイコン向けのRustプログラムのビルドや書き込み方法に関しては以下の記事をご覧ください。


合同会社タコスキングダム|蛸壺の技術ブログ
[Arduino工作〜発展編] RustでATmega328pのプログラムをビルドしてみる

RustからどのようにAVRマイコン用のプログラムを使っていくのか考えていきます。




AVRマイコンのクロック周波数の設定の話は、以下の前回の記事の
「AVR-Rustライブラリを使ったAtmega328pの外部発振器の設定について」 をご覧ください。
またAVRマイコンのUSARTの設定の話は、以下の前回の記事パートの
「AVR-Rustライブラリを使ったAtmega328pのUSARTの設定について」 をご覧ください。
PWM出力とタイマーの使い方に関しては、以下の記事で説明しています。

合同会社タコスキングダム|蛸壺の技術ブログ
【Arduino工作〜発展編】AVR-RustでAtmega328pからPWM波形を出力したい!

AVR-Rustライブラリを使ってAtmega328pの機能からPWM波形を出力するための方法を紹介します。



では以降で、AVRマイコンの割り込みをRustでどう作り込んでいくか見ていきましょう。


AVRの割り込みと割り込みベクタ



まず初めに知っておくことは、AVRマイコンにおける処理の考え方の大前提として、同時に一つの処理しか実行されないということです。
指定した時間だけ待機するdelay関数も、マイコンが
何も処理しないままじっと待っている わけではなく、きっちりと 決められたクロック回数分だけ何もしない処理 を全力で回しています。
何でもかんでもメインルーチンにマイコンにやらせたい動作を定義して、そのタイミングを全てdelayで調整する...というやり方は不可能ではないですが、欲しい動作が増えたり複雑化していくほどdelayだけでは困難になります。
このため、ハードウェアからの特定のトリガーイベントを元に、予め定義しておいたサブルーチンを実行する仕組みを、
「割り込み」 と呼んでいます。
割り込みのトリガーイベントの種類は特別なアドレス値で定義・管理されており、
「割り込みベクタ」 として主に以下の表のように決められています。


ベクタ順序 ベクタ名※ トリガー元デバイス 備考
0 無し 電源などリセット全般 電源ON・WDT・BOD等の各種リセット動作
1 INT0_vect INT0ピン 外部割り込み要求0
2 INT1_vect INT1ピン 外部割り込み要求1
3 PCINT0_vect PCINT0ピン 入力信号変化時割り込み要求0
4 PCINT1_vect PCINT1ピン 入力信号変化時割り込み要求1
5 PCINT2_vect PCINT2ピン 入力信号変化時割り込み要求2
6 WDT_vect WDT ウオッチドック完了
7 TIMER2 COMPA vect タイマー2/比較器A タイマー2比較A一致
8 TIMER2 COMPB vect タイマー2/比較器B タイマー2比較B一致
9 TIMER2 OVF vect タイマー2 タイマー2オーバフロー
10 TIMER1 CAPT vect タイマー1 タイマー1入力キャプチャ発生
11 TIMER1 COMPA vect タイマー1/比較器A タイマー1比較A一致
12 TIMER1 COMPB vect タイマー1/比較器B タイマー1比較B一致
13 TIMER1 OVF vect タイマー1 タイマー1オーバフロー
14 TIMER0 COMPA vect タイマー0/比較器A タイマー0比較A一致
15 TIMER0 COMPB vect タイマー0/比較器B タイマー0比較B一致
16 TIMER0 OVF vect タイマー0 タイマー0オーバフロー
17 SPI STC vect SPI SPIシリアル送信完了
18 USART RX vect USART USARTシリアル受信完了
19 USART UDRE vect USART USARTデータレジスタ空き
20 USART TX vect USART USARTシリアル送信完了
21 ADC_vect ADC AD変換完了
22 EE READY vect EEPROM EEPROM操作準備完了
23 ANALOG COMP vect ANA_COMP アナログ比較完了
24 TWI_vect TWI 2線シリアル通信状態変化時
25 SPM READY vect SPM SPM命令準備完了


※Cライブラリ(io.h等)に収録されている定義名。なおRustでは利用しません。
一般的に、
割り込み処理 とは次のようなプロセスで流れます。

            1. 割り込み機能を有する周辺回路から割り込み要求(IRQ; Interrupt Re-Quest)が発行
2. 実行予定にあるメモリアドレスを一旦スタックに格納
3. 現行で実行中の命令を完了
4. 割り込みルーチン(ISR; Interrupt Service Routine)へ制御が移行
5. 割り込みルーチンの処理が完了後、メインルーチンで項目2で格納していたプロセスに復帰

        

実際の割り込み処理は、多数の状態レジスタやIOレジスタなどが複雑に非同期動作する場合がほとんどで、言うほど単純なものではなく、複雑なプロセスが走っています。


タイマー2を使った割り込み



Atmega328pにはタイマーが3つあり、タイマーごとに多少、機能とレジスタ構造が違います。

以前の記事 でタイマーモードやレジスタの概要は説明しましたが、今回はPWM波形の発生源としてタイマー1を使うため、タイマー2を割り込み制御に利用します。
ちなみにタイマー0を使っても同じですが、タイマーによって出力ピンが異なるのを留意しながら利用してください。

タイマー2のレジスタ操作



タイマー2の割り込みには、先程の割り込みベクタの表にまとめたように、
比較A一致比較B一致カウンターオーバフロー の3種類が利用できます。
この内今回は
比較A一致 だけにフォーカスして説明します。
割り込み要求の使い方を一つ覚えれば後は同じ要領で、他の割り込みも使えるようになると思います。

TCCR2A・TCCR2Bレジスタ



比較A一致の割り込みをトリガーするためには、ここではタイマー2の比較器AをCTCモードに設定してみます。
CTCモードは高速PWMモードとは異なり、ここでの例ではタイマーから得られるノコギリ波(正しくは階段形状)のTOP値をOCR2Aで決めることができます。
このタイマー2の比較器Aの出力ピン(PB3ピン;OC2A)からPWM波形を得ることもできます。

合同会社タコスキングダム|蛸壺の技術ブログ


CTCモードで生成されるPWM波形は、カウンターがOCR2Aの値と一致する際にゼロリセットされ、そのタイミングでHIGHとLOWがトグルされるような出力になります。
なお、このPWM波形をPB3ピンから外部に出力するためには、当該の方向レジスタDDRBのビットを出力(1)に切り替えて、TCCR2Aレジスタの
COM2A1-COM2A0 ビットを 01 に設定することで取り出せるようになります。
今回はタイマー2を割り込み処理の発生元にするだけですので、このPWM波形は利用することはありません。
ちなみにCTCモードで得られるPWM波の周波数$$f_\mathrm{PWM}^\mathrm{CTC}$$は以下のような式で表します。

fPWMCTC=fCLK2N(1+OCR2A) f_\mathrm{PWM}^\mathrm{CTC} = \frac{f_\mathrm{CLK}}{2N (1 + \mathrm{OCR2A})} Eq. (1)


例えばシステムクロック数$$f
\mathrm{CLK} = 8\ \mathrm{[MHz]}$$、分周数$$N = 128$$、`OCR2A = 49`だった場合、$$fOCR2A = 49 }^\mathrm{CTC} = 625\ \mathrm{[Hz]}$$のPWM波形になります。
なお高速PWMモードの存在するAtmegaシリーズのAVRマイコン以外ではこちらの手法が基本的なPWM波形生成方法がとなっています。
すこし話がPWMに逸れましたので、割り込みの話に戻します。
タイマーをCTCモードで利用すると、カウンタがTOP値に一致すると同時に、比較一致の割り込み要求をトリガーさせるようにできます。
この割り込み処理を許可するためには、まず
TCCR2A レジスタを設定する必要があります。

            COM2A1-COM2A0-COM2B1-COM2B0-無し-無し-WGM21-WGM20

設定例:
    00(OC2Aポート切断)-00(OC2Bポート切断)-00-10(☆)

        

まずは、
COM2A1-COM2A0COM2B1-COM2B0 の各ビットですが先程も述べたようにPWM波形を外部に対応のポートから出力するかどうかを設定します。
出力しないばあいには
00 、する場合には 01 にします。
また1-0ビット目に
WGM21-WGM20 がありますが、これを理解するには TCCR2B レジスタも加味する必要が有ります。
TCCR2Bレジスタが以下のような構成になっています。

            FOC2A-FOC2B-無し-無し-WGM22-CS22-CS21-CS20

設定例:
    00-00-0(☆)-111

        

7-6ビット目の
FOC2A-FOC2B は非PWMモードの際に、このビットに1を書き込むと、強制的に即座に比較一致を行うことができます。 この機能は使いどころがないので基本 00 とします。
分周数は
CS22-CS21-CS20 で定義し、以下のようなビットで分周数を設定します。

            CS22-CS21-CS20:
    000...タイマー停止
    001...分周無し(=1)
    010...1/8
    011...1/32
    100...1/64
    101...1/128
    110...1/256
    111...1/1024

        

タイマーのモード設定はTCCR2A/Bの2つのレジスタにまたがって存在している
WGM22-WGM21-WGM20 のビットに設定しなければなりません。
タイマー2のモード選択ビットの設定は以下の表の通りです。

WGM22-WGM21-WGM20 モード名 TOP値 OCR2更新タイミング TOVタイミング
000 標準 0xFF 即時 0xFF
001 8bit位相標準PWM 0xFF 0xFF 0x00
010 CTC OCR2A 即時 0xFF
011 8bit高速PWM 0xFF 0x00 0xFF
101 位相標準PWM OCR2A OCR2A 0x00
111 高速標準PWM OCR2A 0x00 OCR2A


今回のようにCTCモードにしたい場合には、
WGM22-WGM21-WGM20010 に設定するので、TCCR2Aレジスタの1-0ビット目を 10 に、TCCR2Bレジスタの3ビット目を 0 にします。
CTCモードの割り込み周期はOCR2Aに一致した直後にトリガーされるため、例えば主クロック周波数8MHz、分周数1024、OCR2Aを100とすると、割り込み間隔は
0.125us(@8MHz) * 1024 * 100 = 12.8ms で計算できます。


TIMSK2レジスタ



そのままだと割り込みトリガー動作に許可がないので、割り込みルーチン上で何も起こりません。
今回の割り込みトリガー元であるタイマー2に割り込み処理の許可を与える場合には、
TIMSK2 レジスタを設定する必要があります。

            無-無-無-無-無-OCIE2B-OCIE2A-TOIE2

設定例:
    00000-010(比較A一致割り込み有効)

        

2ビット目を1にすると、タイマー2の比較B一致の割り込み要求を許可します。
同様に1ビット目を1にすると、タイマー2の比較A一致の割り込み要求を許可します。
0ビット目を1にすると、タイマー2のカウンターオーバフロー割り込み要求を許可します。
今回は比較A一致割り込みだけを使うので、
OCIE2A ビットのみ1を設定します。


RustでISRマクロが使えない...サブルーチンどうする?



Rustでの割り込みルーチンを実装する前に、C言語との違いを考えておきましょう。
C言語組込で割り込み処理を作成する場合、ISRマクロで目的の割り込みを指定して、サブルーチンの処理を記述するやり方が一般的かと思います。

            ISR(<割り込みベクタの識別名>) {
    //...サブルーチン処理
}

        

何もしないとRustでこの割り込みルーチンにあたるマクロは未実装です。
そのままCのISRマクロにライブラリリンクしてくれる親切な仕組みがないので、割り込み処理が走りません。
Rustでも割り込み処理を行うようにするためには、C言語のISRマクロの中身を覗いて、裏でプリプロセッサがやっていることを理解する必要があります。

先程のパート のように、マイコンの種類によって違いはありますが io.h 等で定義されています。
例えば、タイマー2のオーバフロー割り込みである
TIMER2_OVF_vect は、以下のように定義されるものです。

            #define TIMER2_OVF_vect_num   9
#define TIMER2_OVF_vect   _VECTOR(9)   /* Timer/Counter2 Overflow */

        

そこで更に
_VECTOR(N) へ定義が渡されています。
この
_VECTOR(N)sfr_defs.h に定義されていて、

            #define _VECTOR(N) __vector_ ## N

        

というような関数マクロです。
ということは、最終的にC言語で
ISR(TIMER2_OVF_vect) と書くと、プリプロセッサによって __vector_9 とアッセンプラ展開することになるようです。
ということで、C言語では複数のヘッダファイルで回りくどく割り込みルーチンの定義がなされているわけですが、RustのFFIを利用して直接割り込みベクタのオリジナルのシンボルを叩きます。

            #[no_mangle]
pub unsafe extern "avr-interrupt" fn __vector_N() {
    //...サブルーチン処理
}

        

ここでの
N にあたる割り込みベクタ順序は、 先程の割り込みベクタ表 での順番に割り振られている番号です。
AVRマイコンでも多少異なるかも知れないので、データシート等を良く確認して使う必要があります。

こちら でも議論されているように、 extern "C" で_ vector Nを呼び出すと場合によっては別のアドレス値を読み込むときがあり、 extern "avr-interrupt" できちんとライブラリ指定するのがより正しいとされています。


プログラムの実装



ここからは一定間隔(ここでは8秒間隔)ごとに、タイマー1から発生させたPWMのデューティー比を、タイマー2の割り込みから切り替えるためのプログラムを試します。

合同会社タコスキングダム|蛸壺の技術ブログ


なお、マイコンの組込開発は通常のOS有りのアプリケーションプログラム開発と違い、コンソール出力を手軽に見ながら試行錯誤するようなやり方が出来ません。

前回解説していた ように、先にUSART機能でマイコンと相互通信出来るように機能を作成しておくと、マイコンの実機内部で発生しているもバグも見つけやすくなります。

割り込みの許可/禁止



C言語でのプログラミングにおいても、割り込みをスタートさせる際には、メインルーチンで
sei() 関数で割り込み許可する必要がありました。
これはRustに関しても同じですが、Cの
sei() 関数の代わりに、アセンブラ命令(ニーモニック)の SEI を以下のように直接叩きます。

            #![feature(llvm_asm)]
//...

pub extern "C" fn main() {
    //...
    //👇割り込み処理を許可
    unsafe { llvm_asm!("sei"); }
    //...

        

逆に割り込み処理を禁止に戻したいのであれば、
llvm_asm!("cli"); を使います。

メインルーチンとサブルーチン間のデータ共有



C言語であれば、メインルーチン外(main関数)のスコープの外で(volatileを付けた)変数を宣言することで、その変数を介してサブルーチンとのデータの受け渡しが出来ました。

            #include <avr/interrupt.h>

//ルーチン間で共有される変数
volatile unsigned char COUNT = 0;

ISR(HOGE_vect) {
    //サブルーチンで変数COUNTを操作...
}

int main(void) {
    sei();
    //メインルーチンでも変数COUNTを操作...
}

        

Rustでは変数拘束のライフタイム管理の概念がより重要ですので、単純にメインルーチン外に変数を出しただけではコンパイルエラーになります。
そこでグローバル変数としてデータを共有したい場合、
static mut で変数を拘束宣言しておき、この変数を使う場合には unsafeブロック 内で使用することでC言語との補完が可能です。

            //ルーチン間で共有される変数
static mut COUNT: u8 = 0;

#[no_mangle]
pub unsafe extern "avr-interrupt" fn __vector_N() {
    unsafe {
        //サブルーチンで変数COUNTを操作...
    }
}

#[no_mangle]
pub extern "C" fn main() {
    unsafe { llvm_asm!("sei"); }
    unsafe {
        //メインルーチンでも変数COUNTを操作...
    }
}

        

Rustでプログラミング



以上のポイントを踏まえて、今回の割り込み機能をテストするためのプログラムを以下のソースコードをビルドして生成します。


            #![feature(llvm_asm, lang_items, unwind_attributes)]
#![feature(start, abi_avr_interrupt)]
#![no_std]
#![no_main]

extern crate avr_delay;
extern crate avr_std_stub;
extern crate avrd;

use avr_delay::{delay};
use core::ptr::write_volatile;

use avrd::atmega328p::{DDRB, DDRC};
use avrd::atmega328p::{CLKPR};

//👇タイマー1制御レジスタ
use avrd::atmega328p::{TCCR1A, TCCR1B, OCR1A, ICR1};

//👇タイマー2制御レジスタ
use avrd::atmega328p::{TCCR2A, TCCR2B, OCR2A, TIMSK2};

//👇USART関連レジスタ
use avrd::atmega328p::{UCSR0A, UCSR0B, UCSR0C, UDR0, UBRR0};

static mut COUNT: u8 = 0;
static mut OLD_COUNT: u8 = 0;

#[no_mangle]
#[start]
pub extern "C" fn main() {
    clock_init();
    usart_init();
    pwm_init();
    interrupt_init();

    //👇PB1ピン(OC1A), PB3(OC2A)ピンを出力
    unsafe { write_volatile(DDRB, 0b00001010); }

    //👇割り込み処理を許可
    unsafe { llvm_asm!("sei"); }

    loop {
        delay(2000000); //👈1s@8Mhz
        unsafe {
            //👇一秒ごとにカウンターを回す
            cycle_shift(&mut COUNT);
        }
    }
}

#[no_mangle]
fn clock_init() {
    unsafe {
        write_volatile(CLKPR, 0b10000000);
        //👇主クロック周波数16MHzを2分周し、8MHz駆動にする
        write_volatile(CLKPR, 0b00000001);
    }
}

#[no_mangle]
fn pwm_init(){
    unsafe {
        write_volatile(TCCR1A, 0b10000010);
        write_volatile(TCCR1B, 0b00011011);
        write_volatile(ICR1, 65535);
        //👇Duty比: 0.5
        write_volatile(OCR1A, 32767);
    }
}

#[no_mangle]
fn interrupt_init(){
    unsafe {
        //👇タイマ2はCTCモードで、比較A一致で割り込みトリガー
        write_volatile(TCCR2A, 0b00000010);
        //👇1024分周
        write_volatile(TCCR2B, 0b00000111);
        //👇割込み間隔: 0.125us(@8MHz) * 1024 * 100 = 12.8ms
        write_volatile(OCR2A, 100);
        //👇比較A一致割り込み有効
        write_volatile(TIMSK2, 0b0000010);
    }
}

//👇割り込み処理の確認用のUSART設定
#[no_mangle]
fn usart_init() {
    unsafe {
        write_volatile(UBRR0, 51);//9600bps
        write_volatile(UCSR0B, 0b00011000);
        write_volatile(UCSR0C, 0b00000110);
    }
}

//👇タイマー2の比較A割り込みルーチン
#[no_mangle]
pub unsafe extern "avr-interrupt" fn __vector_7() {
    unsafe {
        if OLD_COUNT ^ COUNT != 0 {
            //👇デバッグ用にカウンターをUSARTのTxより送信して、シリアルでモニタリング可
            transmit(COUNT);
            //👇8秒おきにタイマー1のPWM波形のDuty比を変化
            if COUNT == 0 {
                //👇Duty比: 約0.92
                write_volatile(OCR1A, 60000);
            }
            if COUNT == 7 {
                //👇Duty比: 約0.08
                write_volatile(OCR1A, 5000);
            }
            OLD_COUNT = COUNT;
        }
    }
}

//👇循環整数(0~15)の生成
#[no_mangle]
fn cycle_shift(x: &mut u8) {
    *x += 1;
    *x &= (1<<4) - 1;
}

#[no_mangle]
fn transmit(byte: u8) {
    while !ready_to_transmit() {}
    unsafe {
        write_volatile(UDR0, byte);
    }
}

#[no_mangle]
fn ready_to_transmit() -> bool {
    unsafe {
        (*UCSR0A & (1 << 5)) > 0
    }
}

        

動作確認



先程のRustソースコードをビルドしたものをAtmega328pに書き込みます。
ブレッドボード上に部品を配置して、PB1ピン(OC1A)から出力されるタイマー1のPWM波のデューティー比が変化しているかを確認してみましょう。

合同会社タコスキングダム|蛸壺の技術ブログ


PWMの周波数はそのまま、デューティー比を定間隔で変化させているので、LEDの点滅時間が短い状態と長い状態が一定時間ごとに入れ替わることが分かります。


まとめ



今回はAVRマイコンの割り込み機能全般をRustで使う方法を紹介していきました。
割り込み要求のタイミングには色々とバリエーションがあり、使いたい場合にはその都度適切なトリガー元モジュールのレジスタ周辺のビットを良く理解する必要があります。 困った時はAVRデータシート等で確認しましょう。

参考

ATmega48A/PA/88A/PA/168A/PA/328/P データシート