Rust + ProMicro (ATmega32u4) で全割り込み許可を有効にしただけでハングアップのような現象が起こるのに対処する

ふとしです。

あいかわらず Rust で組み込みをいじくっています。そんなある日 ProMicro で割り込みを有効にすると、単純な LED 点滅アプリケーションが停まってしまうような現象が発生しました。

最終的にわかった対処法は単純でしたが、原因がわかるまでひどく苦労しましたし、せっかくなので日記にします。

(なお Arduino IDE を使う場合はデフォルトで全割り込み許可が有効になりますが、後に説明する理由から問題も起こりません。)

対処法

原因はブートローダーにより余分なレジスターが設定される局面があり、それが回り回ってアプリケーションのリセットを呼ぶからです。

対処法は 2 つあります。

1. アプリケーションのアップロード後 USB を抜き差しする

パワーオンリセットを発生させレジスターを綺麗にします。

(手間ですね)

2. 全割り込み許可 (SREG:I) を設定にする前に USB 全体許可 (USBCON:USBE) を解除する

抜き差しは手間なので、原因となるレジスターを動的に綺麗にして原因を排除してから割り込みを有効にします。

#[no_mangle]
pub extern "C" fn main() {
    let cp = avr_device::atmega32u4::Peripherals::take().unwrap();

    cp.USB_DEVICE.usbcon.modify(|_, w| w.usbe().clear_bit());
    // interrupt::enable は SREG:I を設定する
    unsafe { interrupt::enable() };

    loop {
    }
}

さて、対処として上に付け加えることは、この日記内にはもうありません。

以下は調査方法など雑多な記述です。

原因の調査

単純な LED 点滅もしないのは自分の書き方がまちがっているのではないかと思い、状態が変化する LED 点滅アプリケーションに書き直しました。

#[no_mangle]
pub extern "C" fn main() {
    let mut delay = Delay::<MHz16>::new();
    let dp = atmega32u4_hal::atmega32u4::Peripherals::take().unwrap();

    let mut pd = dp.PORTD.split();
    let mut led = pd.pd5.into_output(&mut pd.ddr);

    let mut on = true;

    let mut flash = || {
        if on {
            led.set_low().unwrap();
        } else {
            led.set_high().unwrap();
        }
        on = !on;
    };

    for _ in 0..10 {
        flash();
        delay.delay_ms(50u8)
    }

    loop {
        flash();
        delay.delay_ms(100u8)
    }
}

最初に早く点滅して、その後ゆっくり点滅します。これの各所に

unsafe { interrupt::enable() };

を置いて様子を見ました。

先頭に置く

先頭に置いた場合、LED は点滅せず、ただ点灯したままになりました。

アプリケーションは固まってしまっているようにみえます。

最初の点滅の後に置く

早く点滅した後……そのまま早く点滅した状態が続きました。

このことからアプリケーションが固まっているのではなく、先頭に戻っていることがわかりました。

原因の調査 2

しかし、なぜ interrupt::enable() だけでアプリケーションがリセットされてしまうのかがわかりません。そこで arv-dump を用いて、コンパイラが実際に生成しているバイナリからアセンブリを観ることにしました。

executable をアセンブリにする

avr-objdump -S target/atmega32u4/release/blink.elf > tmp/rust.dmp

これにより各所にコメントとして Rust のコードが挿入されたアセンブリファイルが得られます。Rust のコードが邪魔な場合は Cargo.tomldebugfalse にしておくと、プレーンなアセンブリが得られます。


target/atmega32u4/release/blink.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0:	0c 94 56 00 	jmp	0xac	; 0xac <__ctors_end>
   4:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
   8:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
   c:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
  10:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
略

jmp 0 をさがす

アセンブリはほとんど知りませんが jmp 0 で無理矢理リセットする手法を知っていました。リセットのような現象が起こっているので、まずそれを検索してみると、ありました。

000000e6 <__bad_interrupt>:
  e6:	0c 94 00 00 	jmp	0	; 0x0 <__vectors>

__bad_interrupt とは

__bad_interrupt は未使用の割り込みのジャンプ先としてコンパイラが指定します。

現時点ではどの割り込み用関数も定義していないので、全ての割り込みのジャンプ先として指定していました。

00000000 <__vectors>:
   0:	0c 94 56 00 	jmp	0xac	; 0xac <__ctors_end>
   4:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
   8:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
   c:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
  10:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>
  中略
  a8:	0c 94 73 00 	jmp	0xe6	; 0xe6 <__bad_interrupt>

Atmega32u4 の説明書は、全割り込み許可に加えてそれぞれの割り込みを個別に許可しないと割り込みは有効にならないと説明しています。したがって、どの割り込みも有効にしていない現時点では、これは問題がないはずです。

全部の割り込み処理を用意してみる

個別に有効にしないと有効にならないはずなのに、と思いながら全ての割り込み用の関数を定義します。

#[avr_device::interrupt(atmega32u4)]
fn RESET() {}
#[avr_device::interrupt(atmega32u4)]
fn INT0() {}
#[avr_device::interrupt(atmega32u4)]
fn INT1() {}
#[avr_device::interrupt(atmega32u4)]
fn INT2() {}
#[avr_device::interrupt(atmega32u4)]
fn INT3() {}
#[avr_device::interrupt(atmega32u4)]
fn INT6() {}
#[avr_device::interrupt(atmega32u4)]
fn PCINT0() {}
#[avr_device::interrupt(atmega32u4)]
fn USB_GEN() {}
#[avr_device::interrupt(atmega32u4)]
fn USB_COM() {}
#[avr_device::interrupt(atmega32u4)]
fn WDT() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER1_CAPT() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER1_COMPA() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER1_COMPB() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER1_COMPC() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER1_OVF() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER0_COMPA() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER0_COMPB() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER0_OVF() {}
#[avr_device::interrupt(atmega32u4)]
fn SPI_STC() {}
#[avr_device::interrupt(atmega32u4)]
fn USART1_RX() {}
#[avr_device::interrupt(atmega32u4)]
fn USART1_UDRE() {}
#[avr_device::interrupt(atmega32u4)]
fn USART1_TX() {}
#[avr_device::interrupt(atmega32u4)]
fn ANALOG_COMP() {}
#[avr_device::interrupt(atmega32u4)]
fn ADC() {}
#[avr_device::interrupt(atmega32u4)]
fn EE_READY() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER3_CAPT() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER3_COMPA() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER3_COMPB() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER3_COMPC() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER3_OVF() {}
#[avr_device::interrupt(atmega32u4)]
fn TWI() {}
#[avr_device::interrupt(atmega32u4)]
fn SPM_READY() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER4_COMPA() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER4_COMPB() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER4_COMPD() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER4_OVF() {}
#[avr_device::interrupt(atmega32u4)]
fn TIMER4_FPF() {}

これにより割り込みには __bad_interrupt 以外がジャンプ先として定義されます。

00000000 <__vectors>:
   0:	0c 94 56 00 	jmp	0xac	; 0xac <__ctors_end>
   4:	0c 94 5c 01 	jmp	0x2b8	; 0x2b8 <__vector_0>
   8:	0c 94 5c 01 	jmp	0x2b8	; 0x2b8 <__vector_0>
   c:	0c 94 5c 01 	jmp	0x2b8	; 0x2b8 <__vector_0>
  10:	0c 94 5c 01 	jmp	0x2b8	; 0x2b8 <__vector_0>
  中略
  a8:	0c 94 5c 01 	jmp	0x2b8	; 0x2b8 <__vector_0>

今回の場合は処理内容が完全に同じなので、同じ処理がジャンプ先になっています。

000002ae <__vector_0>:
 2ae:	0f 92       	push	r0
 2b0:	1f 92       	push	r1
 2b2:	0f b6       	in	r0, 0x3f	; 63
 2b4:	0f 92       	push	r0
 2b6:	00 24       	eor	r0, r0
 2b8:	0f 90       	pop	r0
 2ba:	0f be       	out	0x3f, r0	; 63
 2bc:	1f 90       	pop	r1
 2be:	0f 90       	pop	r0
 2c0:	18 95       	reti

ジャンプ先は reti という割り込みから抜ける命令で終わる処理になっており、正常に割り込みが扱われることがわかります。(意味のなさそうな処理は発生していますが……)

これを転送すると、LED の点滅は早い -> 遅いと遷移するようになりました。やったぜ。これにより、有効にならないはずの割り込みのいずれかが有効になっているせいで __bad_interrupt を呼んで jmp 0 が発生し、アプリケーションが異常動作しているように見えることがわかりました。

動いたのでどれが効いたか探す

二分探査的に削除していくと以下が残りました。

#[avr_device::interrupt(atmega32u4)]
fn USB_GEN() {}

これがあればとりあえず動作はします。

原因の調査 3

しかしダミー関数を用意する上の対処には問題があります。

不必要な関数が必要になるというコードの見た目だけならまだ問題はないのですが、不必要な割り込みが発生しているため、割り込み有効後の遅い方のループが 100ms ではなくおおよそ 1500ms 程度のループになっていまいました。これでは実用に耐えません。

そこで本来は有効になっていないはずの USB 全割り込み許可 (USBE) をクリアしました。

cp.USB_DEVICE.usbcon.modify(|_, w| w.usbe().clear_bit());

すると、これにより割り込み用の関数がなくとも正常動作し、ループの速度も期待通りのものになりました。一見落着です。

これは対処法 2 の方です。

原因の調査 4

ふと思い立って、リセットピンやアプリケーションのアップロードでのリセットではなく、USB 端子を抜き差しする本当のパワーオンリセットを試したところ、動的なレジスタークリアは必要なくなりました。

これは対処法 1 です。

原因は

リセットピンのリセットではブートローダーが DFU (Device Firmware Update) モードにし、USB 経由でアプリケーションをアップロードできるようになります。

ProMicro のブートローダーは他のアプリケーションと同じ扱いなので、他のアプリケーションと同じように USB 全割り込み許可を設定しなければ USB を使えません。そのために設定されたレジスターがそのままで DFU が終了してしまっているため、設定した覚えのない割り込みが発生し、__bad_interrupt によって異常動作が発生しているのだと推測しています。

パワーオンリセットでは DFU モードに入らないため USB 全割り込み許可も設定されず、異常動作も起こらないので、だいたい合っているのではないかなと思っています。

(adafruit/Caterina-Bootloader では USB の有効化・無効化にあたる関数の USB_Init USB_Detach が、おそらく既に hex 化されており、追えませんでした。なので、推測で終了しています)

Arduino IDE では

Arduino IDE ではデフォルトで全割り込みを有効にしますが、USB デバイスとして認識されるように USB 割り込みを適切に処理しています。したがって __bad_interrupt へのジャンプはありませんし、ループ速度の低下のような現象は起こらないようです。

(Arduino IDE はほとんど使っていないのでよくわかりません)

なお Arduino IDE が自動で有効にした USB はリセットボタンなしでの DFU モードへの遷移に使いますが、そのあれこれはまた別の機会に。

おわりに

原因がわかってしまえば対処も簡単でした。

しかし「有効にするまで有効にならない」がそうではなくなってしまう局面があると気づくまで、大分時間がかかっていましました。

「有効にするまで有効にならない」はデバイスのパワーオンリセットの状態の説明としては完全に正しい一方で、ブートローダーの DFU 後には副作用が残った状態でアプリケーションが開始されるというなんともわかりにくい問題でした。