割り込みを体験する #2

1つ前の記事では、タイマを使用しての割り込みのための事前処理、及び、割り込み処理ルーチンに関して取り上げました。

今回は、CPUリセットから初期化関数の実行と割り込みベクタから割り込み処理ルーチンまでに関して触れたいきたいと思います。



まずはじめに、割り込みに関して簡単におさらいしておきます。

割り込み=例外

CPUにとって割り込みとは、今現在行っている処理をまさに割り込まれる、予測不可能な例外的事象です。
このCPUコアに対して処理を中断する要求を例外と呼びます。
例外発生源はCPUの内部・外部を問いません。


割り込み例外は設定によって禁止(無視)することも出来ますが、禁止していない、またはそうできない例外の場合、CPUコアは実行中の処理を中断し、例外の種類別に処理を移すべき場所へ処理を移します。例外種類別に定義された場所の事を例外ベクタとよび、例外ベクタ一覧を例外ベクタテーブルと呼びます。


割り込みも例外の一部であり、例外ベクタテーブルにも割り込み処理ベクタが定義されています。

では

実際のコードを見ていきます。今回はCPUがリセットした状態からのスタートアップを行う、ローレベルのコードからになります。

#include "io.h"
/* extern symbols on ld-script */
	.extern __etext
	.extern __init_values_start
	.extern __init_values_end
	.extern __bss_start
	.extern __bss_end
	.extern __init_stack
/* extern symbols on *.c */
	.extern isr_timer
	.extern main

	.global start

	.text
	.code 32
	.align 4

io.hはレジスタのアドレスを定義したヘッダファイルです。中身に関しては省略します。
extern擬似命令は、リンカスクリプトで定義されたシンボルと、C言語で定義されたシンボルの解決を行っています。
global擬似命令は、startラベルのアドレスを公開しています。(リンカスクリプトで参照)
そして、コード領域指示、32bit長コード、4バイト整列指定です。

上記の擬似命令の後は、例外ベクタの設定です。
ARMは、デフォルトで例外ベクタの開始が0番地のため*1、コードの先頭にベクタテーブルを記述します。
ARMのベクタテーブルは以下のようになっています。

例外の種類 CPUモード 要因
1 reset Supervisor CPUに対してreset信号が通知された
2 未定義命令 Undefined 未定義命令が発行された
3 ソフトウェア割り込み(SWI) Supervisor SWI命令が発行された
4 プリフェッチボート Abort 命令をメモリからフェッチ(取り込み)しようとした際、非整列などが要因でAbortが起こった際に発生
5 データアボート Abort データを命令をメモリからロード/ストアしようとした際、非整列などが要因でAbortが起こった際に発生
6 IRQ IRQ 割り込みが発生
7 FIQ FIQ 高速割り込みが発生

FIQベクタを除く各ベクタは4バイトしかないため、分岐命令を置くのが一般的です。
以下のようになります。

start:
	b  reset
	b  undefined_instruction_exception
	b  software_interrupt_exception
	b  prefetch_abort_exception
	b  data_abort_exception
	b  reserved_exception
	b  interrupt_exception
	b  fast_interrupt_exception

今回のタイマ割り込みはIRQとして例外を発生させます。
従ってinterrupt_exceptionに実際の割り込み処理を定義します。
が、その前に、必須のreset例外の時の処理を定義します。先ず各CPUモード時のスタックの設定を行います。
今回はモードとしてIRQとSupervisor(割り込み中以外の状態)のみの設定とします。

reset:
	/* r1=stack size/r0=stack origin */
	mov r1, #0x1000
	ldr r0, =__init_stack

	/* Initialize IRQ mode stack */
	mov r0, #(0x12|0xc0)
	msr cpsr_c, r0
	mov sp, r0

	/* Initialize SV mode stack */
	mov r0, #(0x13|0xc0)
	msr cpsr_c, r0
	sub r0, r0, r1
	mov sp, r0

cpsr_cはコアのステータスレジスタのモード指定bit以外のbitを触らないようにするためのアセンブリマクロ(?)です。
__init_stackはリンカスクリプトで設定されたスタックの先頭アドレスです。
IRQモード時のスタックには先頭を設定し、SVモードは4KB先の位置を先頭に設定しました。

続いて、BSS領域、Data領域を初期化します。

/* 	Clear BSS */
	ldr r0, =__bss_start
	ldr r1, =__bss_end
	mov r2, #0
1:	cmp r0, r1
	beq	2f
	strb r2, [r0], #1
	b 1b
2:	

/* 	Initialize data */
	ldr r0, =__etext
	ldr r1, =__init_values_start
	ldr r2, =__init_values_end
1:	cmp r1, r2
	beq	2f
	ldrb r3, [r0], #1
	strb r3, [r1], #1
	b 1b
2:	

リンカスクリプトで定義された各領域のアドレスを参照して初期化を行っています。

更に、最低限*2の周辺モジュールの初期化を行い、main関数にJumpします。

/* Initialize Peripherals.  */
	ldr r0, =WDTCON			/* Disable wdt. */
	mov r1, #0
	str r1, [r0]

	ldr r0, =INTMSK 		/* ALL interrupt Masking */
	mov r1, #0xffffffff
	str r1, [r0]

	ldr r0, =INTSUBMSK		/* ALL Sub-interrupt Masking */
	ldr r1, =0x3ff
	str r1, [r0]

	ldr r0,=ULCON0			/* UART0 Setting  */  
	ldr r1,=0x3         
	strb r1, [r0]

/* 	Jump to main */
	ldr r0, =main 
	mov pc, r0

ここまでがreset例外の処理になります。

続いて、例外ベクタからの分岐先の処理を定義します。
先にIRQ例外以外の例外*3です。紙面上の都合で何もしません*4

undefined_instruction_exception:
software_interrupt_exception:
prefetch_abort_exception:
data_abort_exception:
reserved_exception:
fast_interrupt_exception:
	b	.

最後に、IRQ例外時の処理です。

interrupt_exception:
/* save registers */
	stmfd sp!, {r0-r12, lr}
	mrs r0, spsr
	stmfd sp!, {r0}

/* disable irq */
	mrs r0,cpsr
	orr r0,r0, #0x80
	msr cpsr_c, r0 

/* call isr */
	ldr r0, =isr_timer
	mov lr, pc	
	mov pc, r0	

/* enable irq */
	mrs r0,cpsr
	bic r0,r0, #0x80
	msr cpsr_c, r0 
	
/* restore registers */
	ldmfd sp!, {r0}
	msr spsr_c, r0
	ldmfd sp!, {r0-r12, lr}

/* return */
	subs pc, r14, #4
	nop
	nop
	nop

IRQ例外が発生した瞬間にCPUはIRQモードになり、スタックポインタが切り替わります。
切り替わったスタックポインタに対して、sp(r13)、pc(r15)以外の全てのレジスタ、更にspsrをpushします。
spsrはIRQが発生した瞬間にcpsrの内容が保存される特殊なレジスタです。
例外処理から復帰した瞬間にcpsrがspsrから復元され、結果、割り込まれた状態のCPUモードに復帰し、スタックポインタも同様に元に戻ります。

最後に、先のスタートアップコードから呼び出されるmain関数が定義されたC言語のコードは以下のようになります。

#include "io.h"

void init_timer(void); // timer.c
  
volatile static char sig = 'M';

void put(char c)
{
	write_reg32(UTXH0, c);
}

void main(void)
{
	put(sig);
	init_timer();

	// Enable IRQ
	asm volatile (
		"mrs r0,cpsr \n\t"		\
		"bic r0,r0, #0x80 \n\t" \
		"msr cpsr_c, r0 \n\t");

	// Busy loop	
	while (1) {
		asm volatile ("nop");
	}
	
}

main関数に到達した事を表示した後、前回定義したタイマ割り込み初期化関数を実行します。
そして、CPUコアのIRQ許可設定を有効した後、割り込み発生を待ちます。

ここまでが全ての処理です。



またまた

長くなってしまったので、続きは次回の記事とします。次回で完結予定です。
決して勿体つけているわけではないのですが…。編集が下手なだけですねー。

*1:コプロセッサレジスタの設定により、0xFFFF0000におく事も可能

*2:正確にはもっと必要かもしれませんが…

*3:「外」が多い

*4:それでも発生した事がわかるようにすべきですが…