【Arduino工作〜発展編】AVR-RustでAtmega328pの割り込み処理を試そう!
※ 当ページには【広告/PR】を含む場合があります。
2021/11/10

今回はAVR-RustでAVRマイコンの
「割り込み」
単純な機能を使うだけなら、メインルーチンだけでことが済むので、割り込み処理(サブルーチン)をさほど理解せずともなんとなく動作するプログラムが出来てしまします。
ただAVRマイコンのみならず、ある程度プログラムの規模が大きくなってきたときには、割り込み処理を使うことが必須になってきます。
マイコンの持つ能力を余すこと無く応用した完成品を作成したいなら、じっくりと割り込みの裏側の理解をしていきましょう。
なおこの記事は、
RustでArduinoライクに使えるものを作ってみる
大体この記事もそろそろ応用的な内容に差し掛かってきまして、基礎的な内容から大分遠くなってきました。
途中、既知として前回までで利用したテクニックを使っている箇所が出てきますが、ここでは詳しい解説はしませんので、以下のリンクで知りたい内容があればそちらでお願いします。
AVRマイコン向けのRustプログラムのビルドや書き込み方法に関しては以下の記事をご覧ください。
AVRマイコンのクロック周波数の設定の話は、以下の前回の記事の
またAVRマイコンのUSARTの設定の話は、以下の前回の記事パートの
PWM出力とタイマーの使い方に関しては、以下の記事で説明しています。
では以降で、AVRマイコンの割り込みをRustでどう作り込んでいくか見ていきましょう。
AVRの割り込みと割り込みベクタ
まず初めに知っておくことは、AVRマイコンにおける処理の考え方の大前提として、同時に一つの処理しか実行されないということです。
指定した時間だけ待機するdelay関数も、マイコンが
何も処理しないままじっと待っている
決められたクロック回数分だけ何もしない処理
何でもかんでもメインルーチンにマイコンにやらせたい動作を定義して、そのタイミングを全てdelayで調整する...というやり方は不可能ではないですが、欲しい動作が増えたり複雑化していくほどdelayだけでは困難になります。
このため、ハードウェアからの特定のトリガーイベントを元に、予め定義しておいたサブルーチンを実行する仕組みを、
「割り込み」
割り込みのトリガーイベントの種類は特別なアドレス値で定義・管理されており、
「割り込みベクタ」
※Cライブラリ(io.h等)に収録されている定義名。なおRustでは利用しません。
一般的に、
割り込み処理
1. 割り込み機能を有する周辺回路から割り込み要求(IRQ; Interrupt Re-Quest)が発行
2. 実行予定にあるメモリアドレスを一旦スタックに格納
3. 現行で実行中の命令を完了
4. 割り込みルーチン(ISR; Interrupt Service Routine)へ制御が移行
5. 割り込みルーチンの処理が完了後、メインルーチンで項目2で格納していたプロセスに復帰
実際の割り込み処理は、多数の状態レジスタやIOレジスタなどが複雑に非同期動作する場合がほとんどで、言うほど単純なものではなく、複雑なプロセスが走っています。
タイマー2を使った割り込み
Atmega328pにはタイマーが3つあり、タイマーごとに多少、機能とレジスタ構造が違います。
ちなみにタイマー0を使っても同じですが、タイマーによって出力ピンが異なるのを留意しながら利用してください。
タイマー2のレジスタ操作
タイマー2の割り込みには、先程の割り込みベクタの表にまとめたように、
比較A一致
比較B一致
カウンターオーバフロー
この内今回は
比較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}$$は以下のような式で表します。
Eq. (1)
例えばシステムクロック数$$f
OCR2A = 49
なお高速PWMモードの存在するAtmegaシリーズのAVRマイコン以外ではこちらの手法が基本的なPWM波形生成方法がとなっています。
すこし話がPWMに逸れましたので、割り込みの話に戻します。
タイマーをCTCモードで利用すると、カウンタがTOP値に一致すると同時に、比較一致の割り込み要求をトリガーさせるようにできます。
この割り込み処理を許可するためには、まず
TCCR2A
COM2A1-COM2A0-COM2B1-COM2B0-無し-無し-WGM21-WGM20
設定例:
00(OC2Aポート切断)-00(OC2Bポート切断)-00-10(☆)
まずは、
COM2A1-COM2A0
COM2B1-COM2B0
出力しないばあいには
00
01
また1-0ビット目に
WGM21-WGM20
TCCR2B
TCCR2Bレジスタが以下のような構成になっています。
FOC2A-FOC2B-無し-無し-WGM22-CS22-CS21-CS20
設定例:
00-00-0(☆)-111
7-6ビット目の
FOC2A-FOC2B
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のモード選択ビットの設定は以下の表の通りです。
今回のようにCTCモードにしたい場合には、
WGM22-WGM21-WGM20
010
10
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
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)
#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"
extern "avr-interrupt"
プログラムの実装
ここからは一定間隔(ここでは8秒間隔)ごとに、タイマー1から発生させたPWMのデューティー比を、タイマー2の割り込みから切り替えるためのプログラムを試します。

なお、マイコンの組込開発は通常のOS有りのアプリケーションプログラム開発と違い、コンソール出力を手軽に見ながら試行錯誤するようなやり方が出来ません。
割り込みの許可/禁止
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ブロック
//ルーチン間で共有される変数
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データシート等で確認しましょう。