目录
一、概念引入与处理流程
CPU被中断的方式有指令不对、数据访问有问题、Reset信号等这称为异常,还有中断源:按键、定时器、网路数据等称为中断,中断处于一种异常体系,对于异常,首先需要我们保存现场(也就是保存相关的寄存器),然后调用对应的处理函数,对于中断来说首先还需要分辨中断源再处理对应的处理函数,最后恢复现场
总体过程如下,其中标粗的过程是硬件决定的,后面的过程是软件决定的
- 初始化:a.设置中断源,让其可以产生中断;b.设置中断控制器(可屏蔽,可设置优先级);c.设置CPU总开关(使能中断)
- 执行程序
- 产生中断:如按下按键,中断源经过中断控制器,中断控制器向CPU发出请求,CPU处理中断
- CPU每执行完一条指令,都会检查有无异常/中断产生
- 发生有异常/中断产生,开始处理,对于不同的异常,跳去不同的地址执行程序
- 这些地址上,只是一条跳转指令,跳去某个函数,对于这些地址是处于连续的,在硬件上存在异常向量表
- 这些函数,首先会保存现场(各类寄存器),处理异常(中断属于一种异常):分辨中断源,再调用不用的函数,然后恢复现场
对于S3C2440的中断向量表如下
对于u-boot来说,一开始都会指定向量表的跳转指令,其中reset是0地址,undefine_instruction就是地址4,依次增加可以知道irq的地址是24即0x18的地方,当我们发生中断时,CPU跳到这个地址执行该指令,于是CPU就会跳去执行irq的代码:保护现场、首先分辨中断源,调用不同的处理函数、恢复现场
二、CPU模式(Mode)、状态(State)与寄存器
2.1 模式
对于S3C2440来说有7种模式,对于其他ARM有可能还包含其他模式
usr | 用户模式 |
sys | 系统模式,用于运行特权级的操作系统任务 |
Undefined(und) | 未定义指令模式 |
Supervisor(svc) | 管理模式 |
Abort(abt) | 中止模式(1.指令余秋雨中止 2.数据访问中止) |
IRQ(irq) | 中断模式 |
FIQ(fiq) | 快中断模式(快速处理) |
其中除了usr/sys模式外的模式都是异常模式,而异常模式加上sys模式属于特权模式(Privileged Mode),可以编程操作CPSR(当前程序状态寄存器)直接进入其他模式,因此用户模式下即usr模式就不能通过编程操作来进入其他模式,因此在 有操作系统的情况下,应用程序的人处于用户模式,由于不能保证他们代码的稳定性,因此限制了应用程序的权限,防止破坏整个操作系统
对于每种异常之间的差异就是寄存器,灰色三角形代表的是其模式下的专属寄存器,例如MOV R0,R8,在sysy模式下是"MOV R0,R8",在FIQ模式下"MOV R0,R8_fiq",即两个寄存器不是同一个,这些专属的寄存器称为备份寄存器,假需在用户模式下发生异常的时候需要保存R0-R15,对于FIQ异常有自己的R8-R14,那么就可以不保存用户模式下的R8-R14
2.2 状态(State)
对于CPU来说有两种State如下,就比如MOV R0,R1 对于ARM4个字节,对于Thumb2个字节,机器码的字节数不同,对于单片来说ROM寸土寸金用Thumb指令集,对于S3C2440来说有Nand和Nor内存大,用的是ARM指令集,也可以用Thumb指令集
ARM state | 使用ARM指令集,每个指令4个byte |
Thumb state | 使用Thumb指令集,每个指令2个byte |
Thumb 指令可以看做是ARM指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16为的代码密度。Thumb不是一个完整的体系结构,不能指望处理程序只执行Thumb指令而不支持ARM指令集。因此,Thumb 指令只需要支持通用功能,必要时,可借助完善的ARM指令集,例如:所有异常自动进入ARM状态。
在编写Thumb 指令时,先要使用伪指令CODE16声明,而且在ARM指令中要使用BX指令跳转到Thumb指令,以切换处理器状态。编写ARM指令时,可使用伪指令CODE32声明。
2.3 程序状态寄存器
程序状态寄存器, CPSR(当前程序状态寄存器)/SPSR(保存的程序状态寄存器) ,例如"cmp R0,R1"会影响Z位,当R0与R1相等后Z位就会处于1,"beq XXX"指令会使用Z位,如果Z等于1,则跳转,对于SPSR用来保存 "被中断模式的CPSR"
可以读取bit4-bit来知道CPU当前处于哪种模式,也可以修改这些为来处理哪种模式,如果是用户模式就权限来修改,其中bit7显然是IRQ的总开关
进入异常的动作,异常处理流程(硬件)
- LR_异常等于下一跳指令的地址,即被中断的下一条指令的地址,有可能是PC+4或者PC+8,取决于不同的情况
- 然后SPSR_异常等于CPSR
- 修改CPSR的M4~M0进入异常模式
退出异常的处理流程
- PC等于LR_异常减去offset(后面有一张对应表格)
- CPSR等于SPSR_异常
- 清中断(如果是中断的话,对于其他异常就不需要管)
退出异常之后各异常减去的offset值
三、Thumb指令集示例
在gcc选项中加上"-mthumb",这样C文件都以thumb指令集执行,对于汇编文件需要我们自己来指定
all: led.o uart.o init.o main.o start.o
#arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
%.o : %.c
arm-linux-gcc -mthumb -c -o $@ $<
%.o : %.S
arm-linux-gcc -c -o $@ $<
Thumb不重点,例如在跳转到C函数的时候使用Thumb指令集,其中".code 32"表示用的是ARM指令集,由于CPU本身处于arm state,因此设置为".code 16"表示的是Thumb指令集的时候需要先切换到Thumb state模式,怎么切换如下代码所示,将跳转的thumb_func其地址的0位设置为1,CPU就会切换到thumb state模式,对于thumb state模式不能用"ldr pc, =main",需要借助R0来跳转
.text
.global _start
.code 32
_start:
/* 关闭看门狗 */
ldr r0, =0x53000000
ldr r1, =0
str r1, [r0]
/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]
...
/* 怎么从ARM State切换到Thumb State? */
adr r0, thumb_func
add r0, r0, #1 /* bit0=1时, bx就会切换CPU State到thumb state */
bx r0
.code 16
thumb_func:
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr r0, =main /* 绝对跳转, 跳到SDRAM */
mov pc, r0
...
GCC的ARM编译选项可参考:
https://www.cnblogs.com/QuLory/archive/2012/10/23/2735226.html
https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
Getting GCC to compile without inserting call to memcpy :https://stackoverflow.com/questions/6410595/getting-gcc-to-compile-without-inserting-call-to-memcpy
四、und异常模式程序示例
参考异常向量表,可知道发生und会跳转到0x4的地址,故意引入一条未定义的指令(即CPU识别不了的指令),这样就会触发中断,最终会到do_und,此时发生的动作:lr_und保存有被中断模式下一条即将执行的指令的地址,SPSR_und保存有被中断模式的CPSR,CPSR中的M4-M0被设置为11011, 进入到und模式,跳到0x4的地方执行程序,sp_und未设置, 先设置它,在und异常处理函数中有可能会修改r0-r12, 所以先保存,lr(此lr是异常模式自己的lr即lr_und)是异常处理完后的返回地址, 也要保存,"mrs r0, cpsr"将cpsr寄存器的值保存在r0中向c函数传递参数,在汇编中字符串的定义可在网上找到官方的文档:http://web.mit.edu/gnu/doc/html/as_7.html ,里面介绍了两种方式,用.ascii来定义字符串的时候不会自动加上结束符,使用.string的时候会自动加上结束符
当发生异常的时候会打印出cpsr的值和字符串"undefined instruction exception",打印出来的值Bit4 - Bit0就表示的是UND模式
.text
.global _start
_start:
b reset /* vector 0 : reset */
ldr pc, und_addr /* vector 4 : und */
und_addr:
.word do_und
do_und:
/* 执行到这里之前:
* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_und保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为11011, 进入到und模式
* 4. 跳到0x4的地方执行程序
*/
/* sp_und未设置, 先设置它 */
ldr sp, =0x34000000
/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}
/* 保存现场 */
/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string
bl printException
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */
und_string:
.string "undefined instruction exception"
.align 4
...
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
----------------------------------------------------
void printException(unsigned int cpsr, char *str)
{
puts("Exception! cpsr = ");
printHex(cpsr);
puts(" ");
puts(str);
puts("\n\r");
}
五、swi异常模式程序示例
程序执行首先从0地址开始即跳到reset的地方,此时CPU处于Supervisor(svc)管理模式,首先我们需要切换到usr模式,因为swi异常会进入管理模式,对于linux中应用程序的人需要访问硬件,会受限于管理模式,那么APP想访问硬件,必须切换Mode,那怎么切换,就需要发生异常来切换,可以中断、und或者"swi #val"使用软中断来切换,swi是最常用的,在linux中可以用来执行各种系统调用
下列程序可以读出swi指令的值0x123,实现的机制是在svc模式下的lr寄存器会保存PC的值减去offset,因此需要要减去4,参考上面offset的表格,其中"stmdb sp!, {r0-r12, lr}"会保存lr的值,但是调用了"printException"C函数可能会用到lr,有两种解决方式,先执行"printSWIVal"来传参得到swi的值,也可以通过R4-R11来保存lr,在函数中,R4-R11可能被使用,所以在会在入口保存它们,在出口恢复它们,可以参考:ARM常用汇编指令与C程序机制
.text
.global _start
_start:
b reset /* vector 0 : reset */
ldr pc, und_addr /* vector 4 : und */
ldr pc, swi_addr /* vector 8 : swi */
und_addr:
.word do_und
swi_addr:
.word do_swi
...
do_swi:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/
/* sp_svc未设置, 先设置它 */
ldr sp, =0x33e00000
/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}
mov r4, lr
/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException
sub r0, r4, #4
bl printSWIVal
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */
swi_string:
.string "swi exception"
.align 4
...
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0
/* 设置 sp_usr */
ldr sp, =0x33f00000
ldr pc, =sdram
sdram:
bl uart0_init
bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
------------------------------
void printSWIVal(unsigned int *pSWI)
{
puts("SWI val = ");
printHex(*pSWI & ~0xff000000);
puts("\n\r");
}
六、按键中断程序示例
大致过程:
初始化:
- 设置中断源,使其具备发出中断信号
- 其次需要设置2440的中断控制器,使其能够发出中断给CPU
- 设置CPU的CPSR中的I位,总开关使能中断
处理时,需要分辨中断源
处理完清中断
6.1 设置中断源
首先初始化按键引脚为中断引脚,还要设置EXTINT寄存器来设置触发方式
按键中断还要经过EINTMASK才有能力发到中断控制器,其中EINT3-0就不需要
其中GPF1和GPF2为EXTINT寄存器中的EINT0和EINT1,GPG3和GPG11对应着EXTINT寄存器中的EINT11和EINT19,同时设置EINTMASK寄存器使能EINT11和EINT19
void key_eint_init(void)
{
/* 配置GPIO为中断引脚 */
GPFCON &= ~((3<<0) | (3<<4));
GPFCON |= ((2<<0) | (2<<4)); /* S2,S3被配置为中断引脚 */
GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); /* S4,S5被配置为中断引脚 */
/* 设置中断触发方式: 双边沿触发 */
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
}
读取这个寄存器来分辨哪个中断产生了,然后还要清除他,EINT4-23要读这个寄存器才能知道哪个中断产生了,清中断时只需要写这个寄存器的相应位
6.2 设置中断控制器
其次设置2440的中断控制器,对于按键、定时器等是"without sub"类的,因此走的是红框的方向,其中MODE可以设置为FIQ快速中断模式
其中EINT4_7共用一条中断线,EINT8_23共用一条中断线
外部中断不需要经过SUBSRCPND和SUBMASK寄存器,只需要设置SRCPND、MASK、INTPND就可以了
对于SRCPND用来显示哪个中断产生了,执行完之后需要清除SRCPND
对于MASK如果设置为1,即使中断源过来了也不会触发中断
可以读INTPND寄存器得到当前正在处理的中断时哪一个,经过中断优先级的中断,需要清除对应位
次寄存器用来显示INTPND中哪一位被设置为1,因此读该寄存器的值更方便去读INTPND,这个位不需要我们清除,是因为清除了SRCPND和INTPND寄存器之后会自动清楚
6.3 示例
在start.S中使能总开关,同时当发生中断时,会执行handle_irq_c函数
.text
.global _start
_start:
b reset /* vector 0 : reset */
ldr pc, und_addr /* vector 4 : und */
ldr pc, swi_addr /* vector 8 : swi */
b halt /* vector 0x0c : prefetch aboot */
b halt /* vector 0x10 : data abort */
b halt /* vector 0x14 : reserved */
ldr pc, irq_addr /* vector 0x18 : irq */
b halt /* vector 0x1c : fiq */
...
do_irq:
/* 执行到这里之前:
* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_irq保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式
* 4. 跳到0x18的地方执行程序
*/
/* sp_irq未设置, 先设置它 */
ldr sp, =0x33d00000
/* 保存现场 */
/* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr-4是异常处理完后的返回地址, 也要保存 */
sub lr, lr, #4
stmdb sp!, {r0-r12, lr}
/* 处理irq异常 */
bl handle_irq_c
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr_irq的值恢复到cpsr里 */
...
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0
初始化按键, 设为中断源
void key_eint_init(void)
{
/* 配置GPIO为中断引脚 */
GPFCON &= ~((3<<0) | (3<<4));
GPFCON |= ((2<<0) | (2<<4)); /* S2,S3被配置为中断引脚 */
GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); /* S4,S5被配置为中断引脚 */
/* 设置中断触发方式: 双边沿触发 */
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
}
初始化中断控制器,对于其他寄存器在我们发生中断的时候用
void interrupt_init(void)
{
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}
当发生中断时,利用中断控制器的INTOFFSET来分辨中断源,清中断的时候需要从源头开始清除,需要先清除EINTPEND,再清除SRCPND和INTPND
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;
/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
}
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}
用EINTPEND分辨率哪个EINT产生,清除EINTPEND,只需要对其写入相应位
/* 读EINTPEND分辨率哪个EINT产生(eint4~23)
* 清除中断时, 写EINTPEND的相应位
*/
void key_eint_irq(int irq)
{
unsigned int val = EINTPEND;
unsigned int val1 = GPFDAT;
unsigned int val2 = GPGDAT;
if (irq == 0) /* eint0 : s2 控制 D12 */
{
if (val1 & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (irq == 2) /* eint2 : s3 控制 D11 */
{
if (val1 & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}
}
else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
{
if (val & (1<<11)) /* eint11 */
{
if (val2 & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (val2 & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}
EINTPEND = val;
}
6.4 函数指针数组注册中断
实现的机制,在中断源初始化的过程中注册中断服务函数,注册的时候再设置其控制器的INTMSK寄存器来控制中断源发生,这样每使能一个中断源,只需要用register_irq来注册中断号和中断函数
typedef void(*irq_func)(int);
irq_func irq_array[32];
void interrupt_init(void)
{
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}
void key_eint_irq(int irq)
{
...
}
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;
/* 调用对应的处理函数 */
irq_array[bit](bit);
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}
void register_irq(int irq, irq_func fp)
{
irq_array[irq] = fp;
INTMSK &= ~(1<<irq);
}
void key_eint_init(void)
{
/* 配置GPIO为中断引脚 */
GPFCON &= ~((3<<0) | (3<<4));
GPFCON |= ((2<<0) | (2<<4)); /* S2,S3被配置为中断引脚 */
GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); /* S4,S5被配置为中断引脚 */
/* 设置中断触发方式: 双边沿触发 */
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
register_irq(0, key_eint_irq);
register_irq(2, key_eint_irq);
register_irq(5, key_eint_irq);
}