AruduinoでInline Assemblyをやってみる<割り込み処理編>

前回の記事では、Lチカをインラインアセンブリで実現しました。

ArduinoでInline Assemblerを使ってみる<Lチカ編> | ぼくのマイコン開発のメモ (tekuteku-embedded.xyz)

ここでは、どうせなら割り込み処理までやろうとおもいます。こんな感じの機能の実現を目指してみます。

そして、これが結果です。こんな感じで動きました。

ポチとおすとLEDストップ。再度おすと、動き出すみたいな感じです。

LEDの点灯、消灯そして、Delay処理は前の記事でやったそのままですが、ボタンを押すと、LEDの動作がストップするような場合には、割り込み処理を新たにいれる必要があります。ここでは、それをインラインアセンブリで実現します。

ATmega328の割り込み処理って?

仕様書によるとATmega328pは下のような割り込み処理がサポートされてます。

こんな感じで、リセットボタン押した時の処理、タイマーがオバーフローした時の処理など、色々な用途に応じて、割り込み処理がベクタ形式で管理されてるようです。

今回やりたいことはボタン押すと割り込み処理が実行されるというものなので、PCINT0~PCINT2の処理が使えそうです。

備考にピン変化0~2群割り込み要求ってあります。群ってなんだよ?というとPCTINT0~7までが0群。PCTINT8~14までが1群。PCINT16~23までが2群になっていて、それらが、それぞれのデジタル入出力ポートと紐づいています。

つまり、タクトSWはいま、#2(PD2)に繋げてるので、PD2は下図からだと、PCINT18が割り込み信号として割り当てられていることが分かります。なので、所属グループは、ピン変化割り込み2群(直訳っぽい日本語;)になります。

タクトSWをON/OFFした時の割り込みベクタは、PCINT2を使うんだってことです。

割り込みのベクタが何かわかったらあとは簡単で、コード上でこうするだけです。

#include <Arduino.h>  
ISR(PCINT2_vect) // 割り込みベクタ名に_vectをくっつけるだけ
{
  //割り込み処理の内容をここに書く
}  

これで、タクトSWをON/OFFしたら、動作していたプログラムが中断され、この関数が呼ばれます。

SW入力の割り込み処理を有効化する

実際には、関数を有効化するためには、事前に割り込み許可を2つのレジスタに書いとく必要があります。一つは、0~2群とかいってた、割り込みピングループごとの割り込み許可PCICRと、グループ内の各デジタルピン毎の割り込み許可PCMSK2になります。下が仕様書の抜粋です。

これがグループ毎の許可
これがピン毎の許可

指定のBitをセットするだけですね。その時のコードは、下のようにしました。stというオペコードを使って、レジスタを設定してます。

#define PCMSK_BTN (1<<PCINT18) //割り込みピン
#define PCICR_BTN (1<<PCIE2)   //ピングループ

asm volatile(
	"ldi r24, %1	\n" //r24にPCICR用のデータを格納
	"st Z, r24		\n" //PCICRに書き込み
	"ldi r24, %2	\n" //r24にPCMSK用のデータを格納
	"st Y, r24		\n" //PCMSK2に書き込み
	: : "z"(_SFR_MEM_ADDR(PCICR)),"M"(PCICR_BTN),"M"(PCMSK_BTN),
    "y"(_SFR_MEM_ADDR(PCMSK2))
);

割り込み処理時のCPUの動き

これまで、割り込み処理の際にCPUがどう処理するかはあまり意識を払ってきませんでしたが、実際は、割り込みがはいると、下のイラストの順に動作しています。

CPUはフラッシュメモリに書かれた処理の内容を順番に呼んでいくことで、プログラムを実行しています。今、CPUは0x△△△△のアドレスのコードを実行しています。何もなければ、その次のアドレスが読み込まれ、(分岐があれば、分岐先のアドレスが)処理が続くはずですが、この時、ボタンが押されると、CPUは、割り込みベクタを参照して、割り込み処理の内容が書かれているアドレスまでJumpすることになります。

このとき、中断したプログラムは割り込み処理が終わるまで、データが退避されます。

具体的には、割り込み信号が入ると、プログラムの実行に必要な汎用レジスタとSREGの情報をデータメモリ上にスタックします。そして、割り込み処理が実施され、終了したら、スタックしていた情報を読込直して、中断したプログラムを再開するという感じになります。

絵の中で、PUSH、POPと書きましが、これが割り込み時にデータを退避させたり、元に戻したりするオペコードになります。例えば、今回のプログラムでは、こんな感じでデータを退避しました。

ISR(PCINT2_vect,ISR_NAKED) //割り込み処理
{
	asm volatile
	(  // プロローグ
      // 使っていたレジスタを退避させる
		"push r17		\n"
		"push r18		\n"
		"push r19		\n"
		"push r20		\n"
      // SREGも退避させる
		"in r20, __SREG__    \n"
		"push r20				\n"
      
      // この間に割り込み処理の内容を書く
      // エピローグ
		"pop r20				\n"
		"out __SREG__, r20		\n"
		"pop r20				\n"
		"pop r19				\n"
		"pop r18				\n"
		"pop r17				\n"
		
		"reti					\n"
	);

コードをみれば推測はできますが、POPとPUSHを連続で使用する場合、データは下のようなイメージで使用します。

なお、割り込み処理が終わったらretではなくretiで戻るので注意が必要です。

ちなみに、ISRの関数を使って割り込み処理を呼びだす場合には、このPUSHとPOPの処理は実は要らないです。

ISR(PCINT2_vect,ISR_NAKED)ではなく、ISR(PCINT2_vect)とすれば、

API側が勝手にプロローグとエピローグ処理を代行してくれます。

今回は、勉強するのが主目的なので、あえて、手動で書くことに挑戦しました。もちろん、フルでアセンブリ言語で記述する場合には、このような処理はマストになります。

LEDを順番に点灯する処理について

実は、上の内容で今回の目的は達成できたので、ここからは助長なんですが、一応、どうやって、光らす処理を書いたか、忘れないようにまとめときます。

LEDの順番は冒頭のイラストの通りです。LEDいっぱいつけたかったので、PBとPDの二つのポートを使ってます。

下がLEDを順番に光らすロジック部分。ポートの初期化とかは別途必要です。

#include <Arduino.h>
//タイマ設定、前の記事とおなじ
#define CLOCK_MHZ 16UL
#define DELAY_LENGTH_MS 60UL
#define TICKS_1COUNT 5
uint32_t volatile DELAY_TICK =CLOCK_MHZ*1000*DELAY_LENGTH_MS/TICKS_1COUNT;

asm volatile
	(
	"rst:			\n" //すべてのLEDを点灯させたらここに戻る
	"out %1, 0		\n" 
	"ldi r17, 1		\n" //LED出力制御用 初期値をセット(PB0=high)

	"PB_loop:	\n" //PORTB制御用のループ PORTBはPB0~PB5のLED6個
	"wait:		\n" // SWで割り込みがはいるたびに
	"sbic %1, 3	\n" // PD3をhigh(ON)とlow(OFF)に切り替えてる
	"jmp wait	\n" // ONの時はループにいれて、LEDを止める		

	"out %0, r17	\n" //PORTBの指定BITのLEDを点灯
	"rcall delay	\n" //ちょっとDELAYサブルーチン呼ぶ
	"lsl r17	      \n" //lsl は1ビット左シフト 点灯するLEDをずらす 

	"sbrs r17, 6	\n" //LEDはBit5までしかない、
                     //bit6までいったら、スキップしてループ抜ける
	"jmp PB_loop	\n" //ループ継続の場合

	"cbi %0, 5	  \n" //PORTBのbit5をclearして消す
	"ldi r17, 0x80	\n" //初期値セット PD7点灯
	"PD_loop:		\n" //PORTD制御用のループ

	"wait2:			\n" //上と同じ、SW ONでLED停止させる 
	"sbic %1, 3		\n"
	"jmp wait2		\n"

	"out %1, r17		\n"
	"rcall delay		\n"
	"lsr r17		      \n"
	"sbrs r17, 3		\n"
	"jmp PD_loop		\n"
	"cbi %1, 4		   \n" //

	"jmp rst		\n" //全部のLEDが光ったらもとに戻る

	"delay:		\n" //delay関数 前の記事と全く同じ
	"lds r18, %2		\n" // タイマ用のTICK数(32bitのうち24bitを使用)
	"lds r19, %2+1		\n" 
	"lds r20, %2+2		\n"

	"0	:	        \n"
	"subi r18, 1  \n" 
	"sbci r19, 0  \n" 
	"sbci r20, 0  \n" 
	"brcc 0b		  \n" 
	"ret			  \n"

	: :"I"(_SFR_IO_ADDR(PORTB)),"I"(_SFR_IO_ADDR(PORTD)),"p"(&DELAY_TICK)
	:"r17","r18","r19","r20"

	);

割り込み処理は以下の通りです。

ISR(PCINT2_vect,ISR_NAKED)
{
	asm volatile
	(
		"push r17		\n"
		"push r18		\n"
		"push r19		\n"
		"push r20		\n"

		"in r20, __SREG__   	\n"
		"push r20		\n"
      /////// ここからが割り込み処理 ////////
		"sbis	%1,2	\n" //SWがつながってるピンがLOWなら何もしない
		"jmp end		\n" //High -> LOWの割り込みは処理しない
                      //(↑ ボタン押して放すとき)
		"sbic %0,3		\n" //ボタンが押された時,PD3をHighなら
		"jmp LEDOff		\n" //Lowにして、LowならHighにする
		"sbi %0,3		\n"
		"jmp end	    	\n"

		"LEDOff :		\n"
		"cbi %0,3		\n"
                /////////////////////////////////////
		"end:			\n"
		"pop r20		\n"
		"out __SREG__, r20	\n"
		"pop r20		\n"
		"pop r19		\n"
		"pop r18		\n"
		"pop r17		\n"
		"reti			\n"
		::"I"(_SFR_IO_ADDR(PORTD)),"I"(_SFR_IO_ADDR(PIND))
	);
}

以上です、こんなのでも動画のとおり、それなりに期待通りに動いています。SWのチャタがあると、誤作動する場合もありますが…

これで一応は、割り込み処理まで実装することができました。

アセンブリで割り込み🙌

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です