- 零 前言
- 一 ARM的异常与中断
- 1 异常有哪些
- 1.1 七种异常的信息收集表
- 1.2 七种异常的产生
- 1.2.1 复位异常
- 1.2.2 未定义指令异常
- 1.2.3 软中断异常
- 1.2.4 预取异常
- 1.2.5 数据异常
- 1.2.6 外部中断异常
- 1.2.7 快速中断异常
- 2 异常的优先级
- 3 异常与处理器模式
- 4 异常的响应与返回
- 4.1编程时需要知道的:
- 4.1.1 异常发生时
- 4.1.1.1 系统做了什么?
- 4.1.1.2 程序需要做什么?
- 4.1.2 当中断程序执行完之后, 程序需要做什么?
- 4.2 来看详细的
- 4.2.1 arm响应流程
- 4.2.2 关于返回地址
- 二 按键中断程序示例
- 1 查硬件电路
- 2 查寄存器
- 3 程序
本文只供参考按键中断程序示例,arm裸机编程始终不是最终的目标,能够完成简单的外设驱动,能理解裸机编程流程即可,要不求甚解,最后的目标还是系统移植,还是在linux下编程,所以不要对学不会裸机编程过于烦躁。
异常的相关概念和响应,到底哪个放在前面好理解呢?我认为先知道异常的相应原理再去看概念容易被接受,但是不知道有哪些概念,又无法知道响应时到底在干嘛。所以本文还是按先概念再相应原理的顺序来安排,但是本文读者应该是有基础的开发者,因此可以自由翻阅,或者先大致浏览概念,仔细看过异常相应之后再来详细看概念。
概述:当异常发生时,处理器会把pc设置为一个特定的地址,即让程序跳转到那个地址去执行那里的指令;这个地址位于中断向量表中,表中是固定好顺序的几个跳转指令;
存储器映射地址0x0是为向量表保留的,很多处理器的向量表都是从0x0地址开始;在某些操作系统如linux中,向量表地址可以选择到其他地址开始;
arm的异常总共有7种。
| 异常类型 | 英文名 | 处理器模式 | 优先级 | 返回地址 | 返回地址的用途 | 执行地址 |
|---|---|---|---|---|---|---|
| 复位异常 | reset | svc特权模式 | 1 | - | 复位没有lr | 0x0 |
| 未定义指令异常 | undefined interrupt | undefined未定义指令中止模式 | 7 | lr | 指向未定义指令的下一条指令 | 0x04 |
| 软中断异常 | swi | svc特权模式 | 6 | lr | 指向swi指令的下一条指令 | 0x08 |
| 预取异常 | prefetch abort | abort指令预取中止模式 | 5 | lr-4 | 指向导致预取指令异常的那条指令 | 0x0c |
| 数据异常 | data abort | abort数据访问中止模式 | 2 | lr-8 | 指向导致数据中止异常的指令 | 0x10 |
| 外部中断异常 | irq | irq外部中断请求模式 | 4 | lr-4 | 指向发生异常时正在执行的指令 | 0x18 |
| 快速中断异常 | fiq | fiq快速中断请求模式 | 3 | lr-4 | 指向发生异常时正在执行的指令 | 0x1c |
注:0x14地址未使用;
1.2 七种异常的产生 1.2.1 复位异常通常用于系统上电和软硬件导致系统复位两种情况,产生复位异常中断。
复位即从头开始,复位异常中断处理程序进行的是初始化工作。下面列出一些常见的工作:
- 初始化数据栈和寄存器;
- 初始化存储系统,如系统的MMU;
- 初始化关键的I/O设备;
- 使能中断;
- 将处理器切换到合适的模式;
- 初始化C变量,跳转到应用程序执行;
见名知意,就是arm遇到了一条未定义的指令,这种情况不是应该报错吗?为啥还有专门的异常。
因为有协处理的存在,arm中有专门的协处理器指令来操作它,如果程序中写了协处理器指令,但是外部的协处理器未响应,或者根本没有协处理器时,无法执行,则发生未定义指令异常;也就是说,这是一种与硬件相关的异常,而不是一种语法错误,与编译器无关;
我们可以利用它的这种特点,来实现指令集的扩展:
协处理器有很多种,选择当前系统中没有的协处理器,运行其相关的指令,使之可以发生未定义指令异常,在异常处理程序中放上自己想要的代码,可以仿真这种协处理器的功能,也可以干别的事。
如果要仿真,步骤如下:
(1) 将仿真程序入口地址链接到向量表中0x4处。(在start.s中)
(2) 读取该未定义指令的bits[27:24]位,判断其是否是一条协处理器指令,0b1110或者0b110x是协处理器指令,然后根据[11:8]位判断具体是哪一条指令,然后实现其功能;
不仿真的话,就在中断处理程序中写自己的代码;
软中断由swi指令产生,即由程序中的代码产生,与硬件电路无关;软中断进入的是svc模式;
1.2.4 预取异常由系统存储器报告,当处理器去取一条被标记为预取无效的指令时,发生预取异常。
如果系统中不包含MMU,指令预取异常中断处理程序只是简单报告错误并退出,否则引起异常的指令的物理地址被存储到内存中。
数据异常:当存储器访问指令load/store执行,但目标地址不存在或者该地址不允许访问时,由存储器发出数据终止信号。
1.2.6 外部中断异常当处理器的外部中断请求引脚有效,而且CPSR的I位被清0,产生外部中断异常。
1.2.7 快速中断异常当快速中断请求引脚有效且CPSR寄存器的F位被清0,发生快速中断异常。
2 异常的优先级优先级高的优先执行;
3 异常与处理器模式每种异常都会进入一种特定的模式。
用户模式和系统模式不可通过异常进入,只能通过编程来切换。也就是说,在中断处理程序的最后要切换回用户模式;
(1) 用spsr保存cpsr;
(2) 改CPSR寄存器:
- 改工作模式;
- 让处理器模式为:arm;
- 禁止中断;(如果需要)
(3) 保存返回地址到lr;
(4) 设置pc为相应的中断向量;
保存现场到栈区:
用连续地址访问指令ldrm把用到的寄存器和lr一次性保存到栈区我们自己申请的空间内;
(1) 恢复现场–》恢复寄存器的值:用连续地址访问指令strm把栈区我们自己申请的空间内的数据放回到异常之前的寄存器和lr中;
(2) 将spsr的值恢复到cpsr–》恢复工作模式,工作模式不会自动回复,需要我们手动恢复;
(3) 恢复lr到pc—》继续执行之前的代码;那不如直接把栈中的数据返回到pc中,不要中间经过lr;
1.判断处理器状态
上述4.1.1.1中的切换处理器状态,异常发生时,处理器自动切换到arm状态,所以在异常处理函数中要判断,在异常发生前,是arm还是thumb状态,通过spsr的T位可以判断。
一般只在swi处理函数中才需要知道异常发生前处理器的状态,所以在thumb状态下,调用swi软中断异常需要注意:发生异常的指令地址是lr-2而不是lr-4;thumb是16位指令,用LDRH指令来判断中断向量号;
2.向量表
每个异常发生时,程序都会从异常向量表中开始跳转,一般向量表中放的就是跳转指令;
其中FIQ向量地址是0x1c,也就是最后一个向量,那么,FIQ_Handler()可以直接从0x1c开始,省下一条跳转指令;
1.1表中列出了每种异常在返回时需要跳转的地址;
解释:
根据三级流水线,假设有连续的三条指令a; b; c; d; 当处理器将要执行a指令时,pc已经在c指令的地址即pc+8,所以pc-8是a的地址,pc-4是b的地址;
当异常发生时,处理器会把正在执行的指令的下一条指令的地址保存在lr中,也就是pc-4;所有异常都是如此,但是根据不同异常的发生方式,在返回时,还需要对lr上的地址做一点处理;
(1) swi和未定义指令异常,在执行指令时处理异常,返回地址就是lr;
(2) IRQ和FIQ,处理器会把当前指令执行完再处理异常,处理中断时pc已经指到了d指令,所以保存在lr中的pc-4指的是c指令;而我们要返回到b指令,在返回时,需要再补-4才能回到b指令,所以返回地址是lr-4;
(3) 指令预取中止异常,比如b指令是一个无效指令,在预取b指令的时候,b会被标记为预取无效,同时b之前的指令仍然正常运行,当执行到b,pc指d,预取中止异常发生,pc-4也就是c的地址被写入lr,好像跟(1)中一样,但是预取中止异常时程序要返回到b指令那里取读b指令,要返回到b处,对lr补-4;所以返回地址是lr-4;
(4) 数据中止异常,实在访问数据出错时候产生的,也就是在数据访问指令执行结束后才发生异常,而所以返回地址应该为lr-4,但中止异常返回时,需要返回到导致异常的指令,因此再补-4;所以最后返回地址是lr-8;例如执行a访问数据,pc指c,访问出错发生异常是在a执行后,pc指d,而异常返回要求返回到a,所以lr-4指向b,所以要用lr-8返回a;
(5) 复位异常不需要返回地址;
IRQ即中断请求,本节来看一下用于按键的外部中断;
1 查硬件电路 2 查寄存器以下为IO引脚寄存器设置:
引脚中断设置:
如下图,k2接的EINT[9]的id=57,spi port no=25;
下面是中断相关寄存器:
下面是使能id对应位;第二张图中25号对应偏移地址0x104,所以要操作ICDISER1_CPU0;
下面选择分发的cpu接口:
下图可以看出spi_25号应该选择偏移地址0x838;
则为ICDIPTR14_CPU0:
第25号对应8-11位;
设置为0x0,接口0;
下图中使能分发器
使能CPU接口:
设置优先级过滤器:
我们主要看start.s和main.c中的中断配置程序:
@start.s
.text
.global _start
_start: @程序入口
b reset @程序的开始,上电复位,跳转到复位程序;
nop
nop
nop @向量表顺序不能乱,用nop占位;
nop
nop
b irq_handler @向量表中的跳转指令,跳到irq处理程序;
nop
reset:
@告诉cpu0x40008000就是0地址
@那么cpu在找中断向量表时就会找这个位置去找
ldr r0,=0x40008000
mcr p15,0,r0,c12,c0,0 @协处理器指令,读协处理器中的寄存器数据到ARM处理器的r0里面。应该是ARM访问MMU,一般cp15就是MMU。
@此时是svc模式
@切换到irq模式,给sp赋值
msr cpsr,0x52 @0x52是irq模式,arm状态,不禁止中断;
ldr sp,=stack_irq_base @此时sp是sp_irq;
@切换为用户模式,注意不能禁止irq
msr cpsr, #0x50 @0x50是user模式,arm状态,不禁止中断;
ldr sp,=stack_usr_base @此时sp是sp_user;
b main @复位完成后开始执行main();
irq_handler: @irq处理程序;
@irq模式下的栈
@进栈保存现场
@中断产生时,是将pc的值赋给了lr
sub lr,lr,#4
stmfd sp!, {r0-r12,lr}
@做中断要做的事情
bl do_irq
irq_handler_end:
@出栈恢复现场
ldmfd sp!, {r0-r12, pc}^
.data
stack_irq: @为irq申请栈空间;
.space 100 @申请100字节空间;
stack_irq_base: @stack_irq_base即栈顶是100字节空间最大地址;因为arm默认满减栈;
stack_usr: @为usr申请栈空间;
.space 100
stack_usr_base:
.end
#define GPA1CON (*(volatile unsigned int *)0x11400020)
#define ULCON2 (*(volatile unsigned int *)0x13820000)
#define UCON2 (*(volatile unsigned int *)0x13820004)
#define UBRDIV2 (*(volatile unsigned int *)0x13820028)
#define UFRACVAL2 (*(volatile unsigned int *)0x1382002C)
#define UTRSTAT2 (*(volatile unsigned int *)0x13820010)
#define UTXH2 (*(volatile unsigned int *)0x13820020)
#define URXH2 (*(volatile unsigned int *)0x13820024)
#define GPX1CON (*(volatile unsigned int *)0x11000C20)
#define EXT_INT41CON (*(volatile unsigned int *)0x11000E04)
#define EXT_INT41_MASK (*(volatile unsigned int *)0x11000F04)
#define ICDISER1_CPU0 (*(volatile unsigned int *)0x10490104)
#define ICDIPTR14_CPU0 (*(volatile unsigned int *)0x10490838)
#define ICDDCR (*(volatile unsigned int *)0x10490000)
#define ICCICR_CPU0 (*(volatile unsigned int *)0x10480000)
#define ICCPMR_CPU0 (*(volatile unsigned int *)0x10480004)
#define ICCIAR_CPU0 (*(volatile unsigned int *)0x1048000C)
#define EXT_INT41_PEND (*(volatile unsigned int *)0x11000F44)
#define ICDICPR1_CPU0 (*(volatile unsigned int *)0x10490284)
#define ICCEOIR_CPU0 (*(volatile unsigned int *)0x10480010)
void init_uart()
{
//设置GPA1_1这个管脚的功能为UART_TXD
//将4-7位设置为0x2
GPA1CON = GPA1CON & ~(0xf << 4) | (0x2 << 4);
//设置GPA1_0管脚功能为UART_RXD
//将0-3位设置为0x2
GPA1CON = GPA1CON & ~(0xf << 0) | (0x2 << 0);
//设置uart的功能模块
//寄存器名称:ULCON2 UCON2 UBRDIV2 UFRACVAL2
//数据位为8,停止位1位,奇偶校验位无
ULCON2 = 0x03;
//数据发送方式设置为轮询
//UCON2的2-3位设置为0x01
UCON2 = UCON2 & ~(0x3 << 2) | (0x1 << 2);
//波特率为115200
//前提:每个数据位会采样16次,取中间几次,才能确定接收到的是0还是1
//也就是需要有一个节拍来指挥采样,也就是每采样一次需要(1/115200)/16s,其实也就是一个时钟周期
//所以需要的节拍频率(1s会产生多少个时钟周期)是多少呢
//所以:节拍频率 = 115200*16
//经过查芯片手册第7章得知给UART分的工作频率是100MHZ
//UBRDIV2 = 100000000/(115200*16)-1 = 53
//UFRACVAL2 = 0.253*16 = 4
UBRDIV2 = 53;
UFRACVAL2 = 4;
}
void putc(char c)
{
//如果发送缓冲区不为空,就一直循环判断
//UTRSTAT2寄存器的第2位如果为0表示发送缓冲区不为空
while(!(UTRSTAT2 & 0x02));
//否则就往发送缓冲区放数据
UTXH2 = c;
}
char getc()
{
//判断接收缓冲区是否收到了数据
//如果收到就从接收缓冲区获得这个数据
while(!(UTRSTAT2 & 0x01));
char c = (char)URXH2;
return c;
}
void delay()
{
int i;
for(i = 0; i < 1000000; i++);
}
void init_irq_k2()
{
//设置GPX1_1为中断模式
//将4-7位设置为0xF
GPX1CON = GPX1CON & ~(0xf << 4) | (0xf << 4);
//将中断触发方式设置为下降沿触发
//将4-6位设置为0X2
EXT_INT41CON = EXT_INT41CON & ~(0x7 << 4) | (0x2 << 4);
//使能中断
//将1位设置为0
EXT_INT41_MASK &= ~0x02;
//查芯片手册得知
//k2(EINT9)的ID:57 SPI port no:25
//使能相应中断到分发器
//将ICDISER1_CPU0的第25位设置为1,每个寄存器的每一位刚好对应了一个SPI port no
ICDISER1_CPU0 |= 0x1 << 25;
//选择cpu接口(分发)
//ICDIPTR14_CPU0的8-15位设置为00000001,表示选择的是cpu0
ICDIPTR14_CPU0 |= (0x01 << 8);
//使能分发器
ICDDCR = 1;
//接口使能
ICCICR_CPU0 = 1;
//设置优先级,数字越大,优先级越低 设置能够处理所有中断
ICCPMR_CPU0 = 255;
}
void do_irq()
{
//通过ID号判断是什么中断
//如果ID号是57,往串口发送'i'
//ICCIAR_CPU0这个寄存器的低10位为中断ID号
int irqId = ICCIAR_CPU0 & 0x3ff;
switch(irqId)
{
case 57:
putc('i');
//清GPX1_1中断标志
EXT_INT41_PEND |= (0x1 << 1);
//清GIC的中断标志
ICDICPR1_CPU0 = ICDICPR1_CPU0 | (0x1 << 25);
//cpu0级,将处理完成中断ID号写入一个寄存器,表示中断处理完成
ICCEOIR_CPU0 = ICCEOIR_CPU0 & ~(0x3ff) | irqId;
break;
default:
putc('e');
}
}
//现象为串口一直发送a,按下按键发送i;
int main()
{
init_uart();
init_irq_k2();
while(1)
{
putc('a');
delay();
}
return 0;
}



