以下内容源于韦东山课程的学习与整理,如有侵权请告知删除。
一、中断概念的引入与处理流程
1.1 中断概念的引入
这里有一个很形象的场景比喻:假设一位母亲在大厅里看书,婴儿在房间里睡觉,这位母亲怎样才能知道这个孩子睡醒了?
方式一:过一会就打开一次房门,查看婴儿是否睡醒,然后接着看书。
方式二:一直等到婴儿发出声音以后再过去查看,期间都在读书。
第一种方式叫做轮询方式,优点是简单,缺点是很累。
while(1)
{
(1)read book
(2)open door
if 睡 继续读书
if 醒 照顾小孩
}
第二种方式叫做中断方式,缺点是复杂,优点是轻松。
while(1)
{
read book
}
中断服务子程序
{
照顾小孩
}
1.2 处理流程
对于中断方式,中断服务子程序是怎样被调用的?这是中断体系的核心问题。
1.2.1 母亲的处理流程
仍然以上面例子进行对比说明,母亲的处理流程如下:
1)母亲平时是在看书;
2)房子出现各种声音:远处的猫叫、快递员按响的门铃声、小孩的哭声;
3)母亲的处理过程:
- 在书中放入书签(对应ARM:保存现场);
- 去处理(对于猫叫,忽略;对于门铃声,开门取快递;对于小孩哭声,去照顾小孩)(对应ARM:处理异常);
- 处理完之后继续看书(对应ARM:恢复现场)。
远处的猫叫、快递员按响的门铃声、小孩哭声,这些声音先传入耳朵,耳朵再将信号传给大脑。除了这些可以打断母亲看书,还有其他特殊情况,比如身体不舒服、有只蜘蛛掉下来,对于这些特殊情况无法回避,必须立即处理。如下所示:
1.2.2 ARM系统对中断的处理流程
对于我们的ARM系统,它有CPU和中断控制器。按键、定时器、网络数据等情况都可以发送信号给中断控制器,再由中断控制器发送信号给CPU来表明产生了这些中断(中断属于一种异常)。另外指令不对、数据访问有问题(比如内存访问出错)、reset信号等情况,都会打断CPU的正常运行。
ARM系统对异常的处理过程,具体过程如下:
1)首先进行初始化:设置中断源、设置中断控制器、设置CPU总开关。
- 设置中断源。通过设置中断源,让它可以产生中断,比如某个按键可以产生中断,则需要将它对应的GPIO引脚设置为中断引脚。
- 设置中断控制器。通过设置中断控制器,可以选择屏蔽某个中断(好比母亲可以选择忽略远处的猫叫),可以设置中断的优先级(同时有几个中断发生时先处理哪一个)。
- 设置CPU总开关。CPU里面有一个总开关,可以设置是否使能中断。
2)执行正常的程序。好比母亲在正常地看书。
3)产生中断。比如按下按键,中断信号发给中断控制器,中断控制器又发信号给CPU。
4)CPU每执行完一条指令,都会去检查是否有中断\异常产生。
5)发现有中断\异常产生,开始处理。
对于不同的异常,需要跳到不同的地址去执行该地址上的指令(也就是根据不同的异常,给PC赋予不同的地址值)。这些地址一般是排在一起的,而且每个地址上只存储着一条跳转指令,这条跳转指令表示跳到某处去执行某个函数。
比如复位异常发生时,PC就会被赋值0x00,而0x00地址上存储的指令是“b reset”,那么接下来将跳转到reset函数处开始执行;比如普通中断发生时,PC就会被赋值0x18,而0x18地址上存储的指令是“ldr pc,_irq”,那么接下来将跳转到_irq函数处开始执行。
代码中一般会有一个异常向量表,它是根据ARM的硬件特性编写的一小段代码。比如u-boot的start.S文件中有以下内容:
6)这些函数做什么事情?保存现场、调用中断服务子程序、恢复现场。
母亲的处理过程与ARM系统对异常的处理过程,两者的类比如下表所示:
母亲的处理过程 | ARM系统对异常的处理过程 |
在书中放入书签 | 保存现场 |
去处理各种声音状况 | 调用中断服务子程序 |
处理完后继续看书 | 恢复现场 |
上面的步骤中,步骤3)~5)是由硬件自动完成的,比如普通中断发生时,CPU会自动跳转到0x18这个地址处,不需要人为设置什么;而步骤6)需要由软件来进行设置,比如0x18这个地址上的内容是什么,就需要我们程序员自己编写代码来决定,不过一般都是表示跳转到某个函数(这里叫中断服务子程序)的指令,然后在这个函数中对中断做出相应的操作。
就比如步骤5)中的代码,发生中断时,CPU会自动跳转到0x18这个地址处,那这个地址上的内容是什么呢?这就需要我们程序员自己编写代码来决定,这里我们写的是“ldr pc,_irq”,它表示跳转到_irq这个函数。
二、CPU的工作模式(mode)、状态(state)与寄存器
注意,本节的内容,可以参考杜春蕾《ARM体系结构与编程》。
2.1 CPU的工作模式(mode)
我们仍然以上面母亲的例子进行说明。母亲的模式包括:
- 正常模式:母亲懒洋洋很休闲地看书,无压力,效率不高;
- 兴奋模式:母亲将要考试而紧张看书,有压力,效率很高;
- 异常模式:母亲因为生病而卧床休息。
而对于ARM的CPU,在S3C2440数据手册P72有以下内容,可知CPU的工作模式分为七种模式,其中用户模式(User)可以认为是正常模式,系统模式(System)可以认为是兴奋模式,剩余的五种模式可以认为是异常模式(这只是为了类比,方便理解)。
也可以根据操作权限分成普通模式、特权模式,其中特权模式又细分为六种模式。注意,同一时刻CPU只会处于其中一种模式。
普通模式(Normal)
(1)用户模式(User):非特权模式,大部分任务执行在这种模式。
特权模式(Privilege)
(2)系统模式(System):使用和User模式相同寄存器集的特权模式。
(3)快速中断模式(FIQ):当一个高优先级(fast) 中断产生时将会进入这种模式。
(4)普通中断模式(IRQ):当一个低优先级(normal) 中断产生时将会进入这种模式。
(5)超级用户模式(SVC):当复位或软中断指令执行时将会进入这种模式。
(6)中止模式(Abort):当指令预取、数据访问出现异常时(比如读写某条错误的指令,读写某个地址时出错)将会进入这种模式。
(7)未定义指令模式(Undef):当CPU执行某条未定义指令时会进入这种模式。
诸多工作模式是为了满足操作系统的安全等级需要(在有操作系统的情况下,用户模式是给应用程序使用的,写应用程序的人水平参差不齐,不能保证所写的程序是好是坏,所以得限制一下应用程序的权限,以免破坏操作系统;另外之所以有那么多种异常工作模式,是因为在某种工作模式下更容易处理某种异常),各种工作模式下可以访问的寄存器不同。
用户模式下不可以直接进入其他模式,但是可以通过编程修改CPSR寄存器来切换模式,另外CPU在某些情况下也会自动切换模式。
2.2 CPU的状态(state)
在讲述各种工作模式可访问的寄存器之前,先看一下CPU的两种状态:
(1)ARM state:使用ARM指令集,每个指令4byte。
(2)Thumb state:使用Thumb指令集,每个指令2byte。
比如指令“mov r0,r1”被编译成机器码,如果使用ARM指令集则机器码长度为4字节,如果使用thumb指令集则为2个字节。
ARM指令集与Thumb指令集的区别:
Thumb指令可以看作是ARM指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16位的代码密度,但是它不如ARM指令的效率高。Thumb不是一个完整的体系结构,不能指望一个程序只使用到Thumb指令集,必须借助于ARM指令集。Thumb指令只需要支持通用功能,必要时可以借助于完善的ARM指令集,比如,所有异常自动进入ARM状态。在编写Thumb指令时,先要使用伪指令CODE16进行声明,而且在ARM指令中要使用BX指令跳转到Thumb指令,以切换处理器状态;在编写ARM指令时,则可使用伪指令CODE32声明。
引入thumb指令集的目的,主要为了减少程序体积以节约存储空间,尤其对单片机而言。但是在我们的ARM系统里面,Nand Flash或Nor Flash容量很大,根本没必要去节省这一点空间,所以我们一直使用的都是ARM指令集。
在第三节中将演示使用Thumb指令集进行编译,看生成的bin文件是否会变小很多。
2.3 CPU的寄存器
在S3C2440数据手册P73有以下内容:
从这个图中,我们可以得知以下信息:
1、每种工作模式下,都可以访问r0~r15寄存器。
2、有些寄存器画有阴影三角形,它表示专属于该模式的寄存器;没有画阴影三角形的寄存器,则表示所有模式都可以访问的寄存器。
比如对于指令“mov r0,r8”,在用户模式下和在FIQ模式下,所访问的r0都是同一个r0,但所访问的r8不是同一个r8,因为FIQ模式下所访问的r8是专属于FIQ模式的r8,与用户模式下所访问的r8不是同一个寄存器(尽管名字相同但物理实体不同)。
3、FIQ模式拥有更多的专属寄存器,这是为什么呢?
回顾一下中断处理流程,包括保存现场、调用中断服务子程序、恢复现场。
保存现场,也就是保存(原来模式被中断时)原来模式下的寄存器的值。比如程序正在用户模式下运行,当发生中断时,假设需要把用户模式下的r0~r14这些寄存器的内容全部保存下来,然后去处理异常,最后恢复这些寄存器。
但如果是快中断,则不需要保存用户模式下的r8~r14这几个寄存器,因为在FIQ模式下有自己专属的r8~r14寄存器,这可以节省保存寄存器的时间(FIQ模式说,我有专属的r8~r14寄存器,不必把外部的r8~r14寄存器的内容保存下来,因为我根本不会去修改外部的r8~r14寄存器),从而加快处理速度。
在Linux系统中,并不会使用FIQ模式(什么时候用这种模式?)。
4、CPSR(Current Program Status Register),当前程序状态寄存器。
由图可知,整个CPU只有1个CPSR寄存器。该寄存器用来记录CPU的当前状态,它的位含义如下所示:
(1)CPSR[4:0]:表示当前CPU处于哪一种工作模式。
通过读取CPSR[4:0],我们可以判断CPU处于哪一种工作模式;也可以通过修改CPSR[4:0]来进入某一种工作模式,但是如果当前你处于用户模式,是没有权限修改这些CPSR[4:0]的。
以下是CPSR[4:0]与工作模式的关系图,位于S3C2440数据手册的P78。
(2)CPSR[5]:表示CPU工作于Thumb State还是ARM State,即使用的指令集是什么。
(3)CPSR[6]:当CPSR[6]等于1时,表示禁止FIQ。
(4)CPSR[7]:当CPSR[7]等于1时,表示禁止IRQ。这个位是IRQ的总开关。
(5)CPSR[27:8]:保留位。
(6)CPSR[31:28]:条件标志位。有些指令需要根据条件标志位来判断是否要执行,我们比较关注其中的Z位,即CPSR[30]。
比如以下指令,指令“cmp r0,r1”的比较结果会影响到Z位:如果r0等于r1则Z位等于1,否则为0。然后指令“beq xxx”会判断Z位是否为1,如果为1则跳转到xxx地址处,如果不是1则不会跳转。
cmp r0,r1
beq xxx
5、SPSR(Saved Program Status Register),程序状态保存寄存器。
由图可知,五种异常模式下都有自己专属的SPSR寄存器。
当发生异常CPU进入其他工作模式时,其他工作模式下专属的SPSR寄存器会保存会原来工作模式下的CPSR。比如当前程序运行在用户模式,此时CPSR是某个值,当发生中断时会进入IRQ模式,那IRQ模式下专属的SPSR将保存用户模式下的CPSR的值。
从其他工作模式切换回原来工作模式时,需要将SPSR寄存器的值赋给CPSR寄存器。
6、r15寄存器、r14寄存器、r13寄存器、r12寄存器
这里之所以把r12~r15寄存器放在一起讲解,是因为它们的别名在我们编程时很常见。
(1)r15寄存器
r15寄存器,也叫PC寄存器(口语)、PC指针(口语)、程序计数器(Program Counter的翻译)。
由图可知,整个CPU只有一个PC寄存器,该寄存器用于存放下一条要执行的指令的地址。
比如子程序返回时,需要将LR寄存器中保存的地址赋值给PC寄存器,即“mov pc,lr”(程序跳转时把目标代码的地址放到PC寄存器中)。
(2)r14寄存器
r14寄存器,也叫LR寄存器(口语)、连接接寄存器(Link Register的翻译)。
由图可知,五种异常模式下都有自己专属的r14寄存器,该寄存器有以下两个作用:
作用一:调用子程序时,用来保存子程序的返回地址(从子程序返回后,主程序继续执行的指令的地址称为子程序的返回地址)。当通过bl或blx指令调用子程序时,硬件会自动将子程序返回地址保存在 LR 寄存器中(如果不是通过 bl 或 blx 指令调用子程序,则需要自己写代码将子程序的返回地址存入LR寄存器);在子程序返回时,把LR的值赋值给PC即可实现子程序返回,比如可以使用mov pc,lr完成子程序返回。(主动情形)
作用二:当异常发生时,LR中保存的值等于异常发生时PC的值+4或+8,因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。(被动情形)
(3)r13寄存器
r13寄存器,也叫sp指针(口语)、栈指针(口语),栈指针寄存器(stack pointer register的翻译)。
由图可知,五种异常模式下都有自己专属的r13寄存器。一般情况下,我们通过给sp指针赋值来确定栈的位置。
在r14寄存器中说到,调用子程序时r14会自动保存子程序的返回地址。如果子程序中继续调用其他子程序时,需要先把r14中已保存的值入栈,否则会被新的r14的值覆盖。
//通常子程序这样写,以保证子程序中还可以调用子程序
stmfd sp!, {lr} //表示将lr入栈
....
ldmfd sp!, {pc} //表示将sp的值赋值给pc
以下程序模拟了一个在main中调用func子程序的过程,其中涉及到跳转问题,在跳转到func之后需要对之前的寄存器进行压栈(使用满减栈)保护处理,且程序返回时需要出栈以恢复现场。
main:
mov r1,#1
mov r2,#2
bl func
add r3,r1,r2
b stop
func:
stmfd sp!,{r1,r2}//入栈指令
mov r1,#10
mov r2,#20
add r3,r1,r2
ldmfd sp!,{r1,r2}//出栈指令
mov pc,lr //程序调用返回
stop:
b stop
(4)r12寄存器
r12寄存器,也叫ip寄存器,内部过程调用寄存器(intra-procedure-call scratch register的翻译)。
2.4 发生异常时硬件的处理流程
我们来看看发生异常时CPU是如何协同工作的。在S3C2440数据手册P79有以下内容:
(1)进入异常时的处理流程
- 将原来工作模式的下一条指令的地址保存到异常模式专属的LR寄存器中(这个地址有可能是PC+4也有可能是PC+8,取决于发生的是哪一种异常模式,见上表)。
- 将原来模式下CPSR的值保存到异常模式下专属的SPSR中。
- 通过修改CPSR[4:0]进入异常模式。
- 跳转到异常向量表中该异常对应的地址(该地址上存储着一条跳转指令)。
(2)退出异常时的处理流程
- 让异常模式专属的LR寄存器中的值,减去某个offset的值(offset的值取决于发生的是哪一种异常模式,见上表),然后赋值给PC。
- 把CPSR的值恢复(将异常模式下专属的SPSR的值,赋值给CPSR)。
- 清中断(如果是中断这种异常,则对于其他异常不用设置)。
三、thumb指令集示例
2.2节说到CPU有两种状态,其中ARM State每条指令会占据4字节,Thumb State每条指令占据2b字节。
Thumb指令集并不重要(因为它是为了减少程序体积以节约存储空间,但我们ARM的存储容量很大,没必要节约这么一点空间,因此很少用到Thumb指令集),本节通过修改上一章节“代码重定位”最后一节的代码,演示一下如何使用Thumb指令集来编译某程序。
1、修改Makefile文件
对于.c文件,通过搜索查询(比如关键词“gcc使用 thumb”)得知编译时在arm-linux-gcc命令中加上 -mthumb 选项即可:
all: led.o uart.o init.o main.o start.o
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 $@ $< //这里为什么不加-mthumb选项?因为在汇编代码中,
//需要使用一些伪指令来指示是使用ARM指令集还是THUMP指令集
2、修改start.S文件
对于汇编.S文件,需要修改里面的代码,如下所示:
.text
.global _start
.code 32 //表示后续的指令使用ARM指令集
_start:
//这里篇幅缘故,代码同以前一样,省略
//……
//设置栈
//接下来需要调用sdram_init函数,而该函数是.c文件中的函数,
//由Makefile知道.c文件使用thumb指令集
//上面是使用ARM指令集,怎么从ARM State切换到Thumb State?
//使用bx跳转指令,如果要跳转到的地址其最低位bit0=1,则会切换CPU State到thumb state
adr r0, thumb_func //(2)然后获取这个编号
add r0, r0, #1 /* bit0=1时, bx就会切换CPU State到thumb state */
bx r0 //疑惑:r0已经是r0-1了,还能正确跳转吗?
.code 16 //表明下面都使用thumb指令集
thumb_func: //(1)这里首先要写一个标号
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
(1)重新编译,发现报错如下:
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$ make
arm-linux-gcc -mthumb -c -o led.o led.c
arm-linux-gcc -mthumb -c -o uart.o uart.c
uart.c: In function `printHex':
uart.c:73: warning: comparison is always true due to limited range of data type
arm-linux-gcc -mthumb -c -o init.o init.c
arm-linux-gcc -mthumb -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
start.S: Assembler messages:
start.S:78: Error: lo register required -- `ldr pc,=main'
make: *** [start.o] Error 1
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$
原因是对于thumb指令集,不能像“ldr pc,=mian”这样直接对pc赋值,而是需要像下面这样有一个中介:
ldr r0,=main
mov r0,pc
(2)继续编译,报错如下:
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$ make
arm-linux-gcc -mthumb -c -o init.o init.c
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
init.o(.text+0x6c): In function `sdram_init2':
: undefined reference to `memcpy'
make: *** [all] Error 1
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/1_thumb_014_003$
其实sdram_init2函数是在说明位置有关码时引入的函数,后续没有用到,这里可以注释掉,不过这有点粗暴。
课程里面提到:在sdram_init2里使用到了memecpy,这是编译器搞的鬼,使用memecpy函数把这些值从代码段拷贝到arr局部变量中。好像没有什么方法禁用编译器自作聪明地使用memecpy,但是可以修改这些变量,比如说将其修改为静态变量,那么这些数据就会存放到数据段中,最终重定位时会把数据段的值拷贝到arr所对应的地址里面去,这样就不需要使用memecpy了,如下描述:
重新编译通过,得知.bin文件大小为1.5KB,而没修改之前的.bin文件大小为2KB左右。
xjh@ubuntu:~/iot/embedded_basic/jz2440/1_thumb_014_003$ ll -h sdram.bin
-rwxrwxr-x 1 xjh xjh 1.5K 十月 5 17:19 sdram.bin*
xjh@ubuntu:~/iot/embedded_basic/jz2440/1_thumb_014_003$
我们看一下反汇编文件:
四、未定义指令异常模式程序示例
在本节中,我们写一个程序,故意让它发生未定义指令异常,然后处理这个异常。
4.1 ldr pc,lable vs ldr pc,=lable
作为学习笔记,这里先记录一个困扰我蛮久的问题,即“ ldr pc,lable 与 ldr pc,=lable有何区别 ”,这是因为我对ldr这个(伪)指令的了解不够而产生的问题。我们参考一下裸机集大成者u-boot的代码,/u-boot-1.1.6/cpu/arm920t/start.S文件:
上面代码中,先设置“ldr pc,_irq”,然后设置“_irq: .word irq”。怎么理解呢?
首先,我明白“_irq: .word irq”这条语句的含义,就是在_irq这个地址处(标签即地址)存放irq这个地址,如下图所示。
然后,一开始我以为“ldr pc,_irq”就是把_irq赋给pc,即以为pc=_irq(如下图所示),从而让我迷糊了:_irq这个地址处存放的是irq这个地址,而不是一条指令!你让pc指向它而不指向某条指令,那程序还能继续运行吗?
后面经过思考与验证,才知道“ldr pc,_irq”的含义,其实是“ldr pc,[ _irq ]”。也就是将_irq这个标签地址上的内容(即irq)赋给pc,而不是将_irq这个标签地址赋给pc。如下图所示:
也就是说, “ldr pc,lable”其实等价于“ldr pc,[lable]”,有些类似于“ldr r1,[r2]”这种意思(其实应该就是这种意思,因为第二个操作数之前也是没有“=”的)。那“ldr pc,=lable”又是何意呢?“ldr pc,=lable”其实就是让pc=lable(当不确定一个常数是否可以用“立即数”来表示时,可以使用ldr命令来赋值,此时书写形式是“ldr r1,=常数”。编译时将该常数保存在某个位置里,然后使用内存读取指令把它读出来)。
因此上面的代码其实可以这样写:
.globl _start
_start:
b reset
ldr pc,=undefined_instruction
……
ldr pc,=irq
ldr pc,=fiq
那u-boot为什么不写成上面的形式?这是因为写成上面“ldr pc,=irq”这样的形式时,编译器会将irq这个常数会保存在.bin文件的某个不确定的位置中,后面再使用内存读取指令把它读出来。如果.bin文件太大,那么irq这个常数的保存位置可能就超出了4K的范围,此时如果是Nand Flash启动,将无法取到irq这个常数。如下图所示,现在是到300000c0这个地址处取得数据(这个地址还在4K范围内。如果.bin文件非常大,irq这个常数的保存位置,就可能就超出4K的范围)。
那就应该让irq这个常数保存在.bin文件很靠前的位置,这样就算是Nand Flash启动,也能够读取到irq这个常数。如何让irq这个常数保存在.bin文件很靠前的位置?就是要写成u-boot那种样式!此时反汇编文件内容如下,可见30000008这个地址就很靠前了。
4.2 代码示例
4.2.1 初版代码
S3C2440数据手册P82有以下内容:
根据这个异常向量表,我们的代码框架可以这样写:
.text
.global _start
_start:
b reset
b do_und
do_und:
/*保存现场*/
/*处理und异常*/
/*恢复现场*/
reset:
//原来代码
写成的初版代码如下:
.text
.global _start
_start:
b reset //如果一上电复位,就会跳到0地址开始执行:跳到reset程序
b do_und //如果发生未定义指令异常,就会跳到0x04地址开始执行:跳到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异常---------------
//这里对异常的处理方式,是调用printException函数打印一些信息
mrs r0, cpsr //传参1
ldr r1, =und_string //传参2
bl printException
//----------恢复现场------------------
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */
und_string:
.string "undefined instruction exception"
reset:
//关闭看门狗
//设置 FCLK:HCLK:PCLK = 400m:100m:50m
//设置sp栈
//篇幅缘故,省略这些部分的内容
bl sdram_init//内存初始化
bl copy2sdram//重定位
bl clean_bss//清除bss段
bl uart0_init //原本uart0_init是在main函数中调用的,但是由于调试需要用到而提前
bl print1 //调试作用:puts("abc\n\r");
//这里故意加入一条未定义指令
und_code:
.word 0xdeadc0de//未定义指令
bl print2 //调试作用:puts("123\n\r");
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
下面是对这段代码的一些解释:
(1)代码流程:一上电复位,CPU跳到地址0开始执行,发现0地址上的指令是“b reset”,于是跳转到reset处执行,完成了一系列的初始化,当执行到und_code处的指令时,CPU发现它不能识别指令0xdeadc0de,于是CPU跳转到地址0x4开始执行,由于该地址上的指令是“b do_und”,于是就跳转到do_und处执行(执行do_und之前,硬件自动完成一系列工作,见注释1~4)。
(2)CPU的每种工作模式都有自己专属的栈空间,将数据入栈进行保存前,需要先设置栈指针。
ldr sp, =0x34000000 //内存已经初始化,所以可以使用这个地址作为栈顶
(3)保存现场的入栈操作,使用stmdb指令;恢复现场的出栈操作,使用ldmia指令。db和ia要结合起来使用,也就是你入栈时使用db,我出栈时就应该使用ia。db,d before stm,在stm之前先d,即“先减后存”;ia,i after ldm,在ldm之后再i,即“先读后减”。
//保存现场
stmdb sp!, {r0-r12, lr}
//恢复现场
ldmia sp!, {r0-r12, pc}^
(4)对cpsr寄存器,需要使用mrs、msr指令来操作。可以这样记忆:r表示某个通用寄存器,s表示cpsr寄存器;mrs、msr这两个指令都是从操作数2指向操作数1的(也就是从右到左)。比如对于“r1,cpsr”使用哪个指令?由于从右到左,这里就是从cpsr到r1,r<--s,那么就该使用mrs;对于“cpsr,r1”则是从r1到cpsr,s<--r,那么就该使用msr。
(5)这里对异常的处理方式,是通过调用printException函数打印一些信息。该函数内容如下,可见有两个参数,参数1表示cpsr寄存器的值,参数2表示字符指针(用于指向字符串的首地址)。
void printException(unsigned int cpsr, char *str)
{
puts("Exception! cpsr = ");
printHex(cpsr);
puts(" ");
puts(str);
puts("\n\r");
}
那么参数2应该传入一个字符串的首地址,这里是und_string,如下所示:
mrs r0, cpsr //传参1
ldr r1, =und_string //传参2
und_string:
.string "undefined instruction exception"
(6)添加一条未定义指令,是通过下面这种形式来添加的。
//这里故意加入一条未定义指令
und_code:
//.word 0xff123456
.word 0xdeadc0de//未定义指令
课程里曾经使用过0xff123456,由S3C2440数据手册P86的下图可知,它恰巧是一条SWI指令:
4.2 代码改进
有哪些地方需要改进呢?
1、改进1
原来代码中,“b do_und”使用相对跳转指令b,跳转到do_und函数;而do_und函数中的“bl printException”也使用相对跳转指令bl,跳转到printException函数。如果是Nand Flash启动,那么printException函数有可能在4K之外,此时执行“bl printException”指令则必然出错。因此为了保险,需要跳转到SDRAM中执行SDRAM中的那一份代码。
怎么跳转到SDRAM中呢?将“b do_und”改为“ldr pc,=do_und”即可。这样的话,运行就是SDRAM中的do_und函数,那do_und函数中的“bl printException”,肯定也是跳到SDRAM中的printException函数。
_start:
b reset /* vector 0 : reset */
//b do_und
ldr pc, =do_und
do_und:
//……
但是上面这种写法存在着4.1节最后的问题,因此需要改成下面这样:
_start:
b reset /* vector 0 : reset */
//b do_und
//ldr pc, =do_und
ldr pc,und_addr
und_addr:
.word do_und
do_und:
//……
2、改进2
重定位以及清除bss段之后,程序应该立即跳转到SDRAM中去执行SDRAM中的那一份代码,这是因为接下来的内容有可能在4K之外。
bl clean_bss //清除BSS段
//******添加下面两行内容*******
ldr pc, =sdram /* 绝对跳转, 跳到SDRAM */ //和 ldr pc, =main 含义一致
sdram:
//***************************
bl uart0_init
bl print1
und_code:
.word 0xdeadc0de
bl print2
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
3、改进3
在字符串后面添加“.align 4”,表示4字节对齐,这样才能保证后面的指令(指的是reset下面的指令)是4字节对齐的。之前没有添加“.align 4”,程序也能够正常运行,那是因为恰巧而已。如果字符串是其它长度,就有可能出错的。
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^
und_string:
.string "undefined instruction exception"
.align 4 //添加这一句代码
reset:
/* 关闭看门狗 */
ldr r0, =0x53000000
ldr r1, =0
str r1, [r0]
4、总结
下面来总结一下程序的运行过程:
改进后的代码如下:
_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
reset:
/* 关闭看门狗 */
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]
/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]
/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
* m = MDIV+8 = 92+8=100
* p = PDIV+2 = 1+2 = 3
* s = SDIV = 1
* FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]
/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
* 然后CPU工作于新的频率FCLK
*/
/* 设置内存: sp 栈 */
/* 分辨是nor/nand启动
* 写0到0地址, 再读出来
* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
* 否则就是nor启动
*/
mov r1, #0
ldr r0, [r1] /* 读出原来的值备份 */
str r1, [r1] /* 0->[0] */
ldr r2, [r1] /* r2=[0] */
cmp r1, r2 /* r1==r2? 如果相等表示是NAND启动 */
ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
moveq sp, #4096 /* nand启动 */
streq r0, [r1] /* 恢复原来的值 */
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
ldr pc, =sdram
sdram:
bl uart0_init
bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
五、SWI异常模式程序示例
所谓SWI,即software interrupt,软件中断。简单地理解,就是应用程序中通过使用swi指令来触发中断。
之前提到,当复位或者执行软件中断指令时,将会进入超级用户模式(SVC)。那软件中断异常模式,应该就是SVC模式?
5.1 软件中断的意义
为什么需要软件中断异常模式呢?或者说,软件中断的作用是什么?
我们知道,ARM的CPU有7种工作模式,除了用户模式以外,其他6种都是特权模式,这些特权模式可以直接修改CPSR进入其他模式,而用户模式不能修改CPSR进入其他模式。
Linux应用程序一般运行于用户模式,这是一种受限的模式,比如不可访问硬件。应用程序如果想访问硬件,必须先切换模式。怎么切换模式呢?需要通过异常,比如普通中断IRQ、未定义指令异常Undef、软件中断异常(通过执行“swi xxx”指令来触发SWI异常)。也就是说,软件中断异常是切换CPU工作模式的一致方式。
另外注意一下软件中断和软中断,这两个概念的区别:https://zhuanlan.zhihu.com/p/360683396。
另外有个疑惑,ARM的CPU的工作模式,与Linux内核的用户态、内核态有什么关联?
更多介绍,应该通过搜索引擎来学习,不要局限于课程的知识!
(1)https://zhuanlan.zhihu.com/p/550570075
(2)SWI指令---软件中断实例详解(原创)-CSDN博客
5.2 SWI异常模式程序示例
5.2.1 初版代码
代码如下,可见和未定义指令异常模式的处理流程类似。
.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_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
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}
/* 保存现场 */
/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */
swi_string:
.string "swi exception"
.align 4
reset:
/* 关闭看门狗 */
/* 设置FCLK : HCLK : PCLK = 400m : 100m : 50m */
/* 设置sp栈 */
bl sdram_init
bl copy2sdram
bl clean_bss
/* 复位之后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指令 */
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
下面是对这部份代码的一些解释:
(1)代码流程:复位之后执行reset的内容,CPU处于SVC工作模式,为了体现SWI的特征(从用户模式切换到SWI),需要先把CPU从SVC切换到用户模式。然后执行到“swi 0x123”这条指令时,会触发SWI异常,从而CPU跳转到0x8地址处执行,进而执行do_swi函数。
(2) 同4.2的改进3一样,需要在字符串后面添加“.align 4”,表示接下来的内容要4字节对齐。
(3)用户模式、未定义指令异常模式、SWI异常模式下的栈指针可以随便设置,只要指向未使用的空间即可。这里用户模式的栈指针为0x33f00000,未定义指令异常模式的栈指针是0x34000000,SWI异常模式的栈指针是0x33e00000。
(4)“swi xxx”这个指令中的“xxx”有啥作用呢?课程中说,可以根据应用程序传入的xxx来判断为什么调用swi指令。
5.2.2 读取“swi xxx”中的“xxx”
如何在异常处理函数中,读取“swi xxx”中的“xxx”呢?
修改代码如下:
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 // 见注释(1)
/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException
sub r0, r4, #4 //见注释(1)
bl printSWIVal
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */
swi_string:
.string "swi exception"
.align 4
下面是对这段代码的一些说明:
(1)代码中的“mov r4, lr”,之所以要把lr取出来,是因为一会要用它得到“swi 0x123”这条指令的地址。由下表可知,执行“swi 0x123”时,下一条指令即“ldr pc,=main”的地址为PC+4,而这个地址会保存在lr寄存器中,也就是说lr寄存器中保存的值是PC+4。那为了得到“swi 0x123”这条指令的地址,需要将lr的值减去4。这就是代码中的“sub r0, r4, #4”的含义。
(2)那为什么是赋值给r4呢?赋值给其他寄存器比如r0、r1这样不行吗?原来,赋值给r0~r3时,进入printException函数时,r0~r3有可能会被修改。而r4~r11在进入printException函数时会自动保存下来以备退出时自动恢复。所以可以赋值给r4。这些知识内容在书P56的“ATPCS规则”中。
(3)printSWIVal函数内容如下,它提取“swi xxx”指令的低24bit的内容,即xxx的值。
void printSWIVal(unsigned int *pSWI)
{
puts("SWI val = ");
printHex(*pSWI & ~0xff000000);
puts("\n\r");
}
六、按键中断程序示例-1
中断也是一种异常,那么它的处理流程,应该和前面两种异常(未定义指令异常、SWI异常)的处理流程是类似的;但中断是由硬件产生的,中断源有很多种,因此它的处理流程又与前面两种异常稍微不同。
6.1 中断的处理流程
其实就是1.2.2小节的内容。
(1)首先进行初始化:设置中断源、设置中断控制器、设置CPU总开关。
- 设置中断源。通过设置中断源,让中断源可以产生中断信号。比如让某个按键可以产生中断,则需要将它对应的GPIO引脚设置为中断引脚。
- 设置中断控制器。通过设置中断控制器,可以选择屏蔽某个中断(好比母亲可以选择忽略远处的猫叫)、可以设置中断的优先级(同时有几个中断发生时先处理哪一个)、可以让中断控制器发送中断信号给CPU。
- 设置CPU的中断总开关。通过设置CPSR寄存器的I位,即bit[7],可以设置是否开启中断。
(2)处理时:要分清中断源,对于不同的中断源执行不同的处理函数。
(3)处理完后:相比其他异常,中断这种异常最后还需要清中断。
6.2 代码示例
下面开始写代码。代码流程图如下所示:
6.2.1 start.S文件内容
(1)首先设置中断总开关,让CPSR[7] =0来开启中断,如果CPSR[7] =1则CPU无法响应任何中断。
//省略的内容与第五节的start.s文件一样,这里仅列出修改之处
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
bic r0, r0, #(1<<7) //清除I位, 使能中断
msr cpsr, r0
//省略的内容与第五节的start.s文件一样,这里仅列出修改之处
(2)然后调用按键初始化函数(设置它为中断源)、中断控制器初始化函数。这里先在start.S文件中调用这两个函数,后面改进时会改为在main.c文件中调用这两个函数。
bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
bl interrupt_init //中断控制器初始化函数
bl key_eint_init //按键初始化函数(设置它为中断源)
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
6.2.2 interrupt.c文件内容
主要是实现key_eint_init、interrupt_init函数。
1、初始化按键:key_eint_init函数
/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{
/* 配置GPIO为中断引脚 */
GPFCON &= ~((3<<0) | (3<<4));
GPFCON |= ((2<<0) | (2<<4)); /* S2,S3被配置为中断引脚 */
GPGCON &= ~((3<<6) | (3<<11));
GPGCON |= ((2<<6) | (2<<11)); /* 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));
}
下面是对这函数的一些说明。
(1)将GPIO引脚配置为中断引脚
下面是按键的原理图,我们想达到“按下按键灯亮、松开按键灯灭”这样的效果,需要把S2~S5这4个按键设置为外部中断源。
在S3C2440数据手册上查询“EINT0”得到下面内容,可知S2按键对应着GPF0这个引脚。
或者在原理图中直接搜索“EINT0”,得到下面的内容:
那么我们需要看一下GPFCON寄存器的位含义:
(2)EXTINTn寄存器
该寄存器用了设置中断触发方式。
所谓中断触发方式,也就是什么情况下会触发中断:上升沿?下降沿?低电平时?高电平时?
对于EXTINT0、EXTINT1,我们可以通过EXTINT0寄存器来设置中断触发方式,这里设置为双边沿触发。
对于其他EXTINTn(n≥8),需要设置EXTINT1、EXTINT2寄存器。
(3)EINTMASK寄存器
代码最后涉及EINTMASK寄存器(External Interrupt Mask Register,外部中断屏蔽寄存器)。该寄存器的位含义如下:
由此可知:当该寄存器某位设置为1时,表示禁止对应的外部中断源发信号给中断控制器;当该寄存器某位设置为0时,则表示允许对应的外部中断源发信号给中断控制器;另外EINT0~3是保留的,默认是使能的,可以直接发信号给中断控制器,无需设置。
(4)EINTPEND寄存器
该寄存器的位含义如下:
当发生外部中断时,我们可以通过读取这个寄存器,来分辨是哪个EXINTn产生了(n=4~23)。
另外处理完异常后需要清除中断,往EINTPEND寄存器相应位写“1”即可。
2、初始化中断控制器
/* 初始化中断控制器 */
void interrupt_init(void)
{
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}
在S3C2440数据手册P378有以下内容(可以看作是中断控制器):
图中的MODE是与FIQ、IRQ有关的,这里我们不管FIQ,所以不理会MODE;Priority是与优先级相关的,这里先不管。
由上图可知,不同中断源的处理路线不同,外部中断源走的是标红的那条路线。因此只需要关注INTPND、MASK、SRCPND寄存器。
(1)SRCPND寄存器
该寄存器用来显示产生了哪个中断,如下图所示。
处理完异常最后清除中断时,我们要把对应位设置为1。
由上图可知,SRCPND[5] 对应着 EINT8~23。如果SRCPND[5]=1时,如何判断EINT8~23中的哪一个外部中断源发生了中断(课程中说:这就需要继续看INTPND寄存器了)?
(2)INTPND寄存器
有多个中断源发出中断时,SRCPND寄存器中就会有多个位被设置为1,这多个中断经过priority之后,就只有一个中断通知CPU。
哪一个中断优先级最高呢? 可以读INTPND寄存器。因为该寄存器用来显示当前优先级最高的、正在发生的中断。
处理完异常后清除中断时,需要清除它(写1)。
(3)INTMOD寄存器
内容如下,我们使用默认值即可。
(4)INTMSK寄存器
这个寄存器用来屏蔽中断源(即使你中断源发生了中断,如果我把你屏蔽了,你的中断信号也不会发给CPU), 有点类似于EINTMASK寄存器。不过INTMSK寄存器是属于中断控制器的,而EINTMASK寄存器是I/O端口控制器的。写0表示允许,写1表示禁止。
(5)INTOFFSET寄存器
该寄存器用来显示INTPND寄存器中哪一位被设置为1。比如,如果INTOFFSET=n,那说明INTPND寄存器的bit[n]被设置为1。其实也可以逐位去判断 INTPND 寄存器中哪一位被设置为1。
处理完异常后清除中断时,不需要清除它,因为会被自动清除。
(6)由(1)~(5)可知,我们在初始化中断控制器时,只需要设置INTMSK寄存器即可。其他寄存器在处理中断时才需要设置。
6.2.3 main.c文件内容
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;
int main(void)
{
interrupt_init(); /* 初始化中断控制器 */
key_eint_init(); /* 初始化按键, 设为中断源 */
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
while (1)
{
putchar(g_Char);
g_Char++;
putchar(g_Char3);
g_Char3++;
delay(1000000);
}
return 0;
}
七、按键中断程序-2
7.1 start.S文件内容
在main.c文件中,我们已经进行中断控制器初始化、中断源初始化。假设一按下按键就会产生中断,那CPU跳到哪里执行呢?我们可以根据异常向量表,在start.S文件中添加相应代码。
.text
.global _start
_start:
b reset /* vector 0 : reset */
ldr pc, und_addr /* vector 4 : und */
ldr pc, swi_addr /* vector 8 : swi */
//即使没有发生也要占个坑位才行,其实也可以.word xxx 这样的形式?
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 */
und_addr:
.word do_und
swi_addr:
.word do_swi
irq_addr:
.word do_irq
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
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
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里 */
reset:
/* 关闭看门狗 */
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]
/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]
/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
* m = MDIV+8 = 92+8=100
* p = PDIV+2 = 1+2 = 3
* s = SDIV = 1
* FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]
/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
* 然后CPU工作于新的频率FCLK
*/
/* 设置内存: sp 栈 */
/* 分辨是nor/nand启动
* 写0到0地址, 再读出来
* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
* 否则就是nor启动
*/
mov r1, #0
ldr r0, [r1] /* 读出原来的值备份 */
str r1, [r1] /* 0->[0] */
ldr r2, [r1] /* r2=[0] */
cmp r1, r2 /* r1==r2? 如果相等表示是NAND启动 */
ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
moveq sp, #4096 /* nand启动 */
streq r0, [r1] /* 恢复原来的值 */
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
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
下面是对这一段代码的说明。
(1)由上面的代码可知,其实中断的处理流程,和其他异常模式差不多的。只是异常模式对于异常的处理,是调用打印函数来打印某些字符串,而中断是调用handle_irq_c函数来进行一些操作。
(2)为了构建异常向量表,用了“b halt” 语句,这是必须需要的。
(3)对于栈空间的设置,只要分配不冲突的、没有被使用过的内存即可。
(4)保存现场时,为什么不直接保存lr,而是保存lr-4呢?
sub lr, lr, #4
stmdb sp!, {r0-r12, lr}
这是由硬件结构决定的,如下所示。既然硬件让这么做,遵循就好,没有那么多为什么。
7.2 handle_irq_c函数
接下来我们在interrupt.c中写出handle_irq_c函数。
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);
}
下面是对这段代码的一些说明:
(1)中断处理函数的3个步骤:分辨中断源、调用对应的处理函数、清除中断。
(2)怎么分辨中断源呢?由INTOFFSET寄存器含义可知,它的值表示INTPND寄存器中哪一位被设置为1,也就是指明中断源是哪个。
(3)清除中断,必须从源头开始清,也就是6.2.2第2节的图从左到右。这里是往SRCPND、INTPND寄存器的对应位(即(2)中找到的对应位)写“1”。另外在key_eint_irq函数中也清中断源EINTPEND 。
(4)关于 key_eint_irq 函数的说明。
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;
}
其代码逻辑如下所示,每个if里面的代码其实就是普通的点灯代码而已。
另外注意一下里面的 “if (val & (1<<11)) /* eint11 */”和“else if (val & (1<<19)) /* eint19 */”,它是通过EINTPEND寄存器的对应位是否为1,来判断对应的中断源是否发生了中断的。
这是因为传给key_eint_irq函数的参数是INTOFFSET,这个参数等于5时,表明EXTINT8~23发生了中断,那到底是哪一个发生了中断呢?我们需要读取EINTPND寄存器来判断是哪个产生了中断。
7.3 其他修改
在main.c中添加led的初始化函数。
七、定时器中断程序示例
7.1 定时器内部控制逻辑
参考资料是S3C2440数据手册的第10章“PWM Timer”。
(1)这里面肯定有一个时钟clk,每来一个clk,TCNTn就减去1。
(2)当TCNTn ==TCMPn时,可以产生中断,也可以让对应的PWM引脚反转(比如由高电平转换为低电平)。
(3)TCNTn继续减1,当TCNTn==0时,可以产生中断,PWM引脚再次反转。
(4)TCMPn、TCNTn的初始值来自TCMPBn、TCNTBn。当TCNTn ==0时,可自动加载初始值。
顺便提一下,由于JZ2440没有引出pwm引脚,所以pwm功能无法使用,也就无法做pwm相关实验。所谓pwm,是指可调制脉冲。比如下图,T1高脉冲和T2低脉冲,它的时间T1、T2可调整,可以输出不同频率、不同占控比的波形,这在控制电机时特别有用。
7.2 怎么使用定时器
使用定时器的步骤:1)初始化时钟;2)设置初值;3)加载初始,启动Timer;4)设置为自动加载;5)处理中断。
我们这个程序只做一个实验:当TCNTn这个计数器计数到0时,就产生中断,在这个中断服务程序里我们点灯。
首先我们在main函数中添加timer_init函数,这是时钟初始化函数:
新建一个timer.c文件,在里面实现这个timer_init函数,函数逻辑如下:
在数据手册P313有下面框图(这里我们设置Timer0,所以关注Timer0相关的内容):
由上可知,如果我们想设置Time0,则需要进行以下设置:
1)首先设置8-Bit Prescaler;
2)设置5:1 MUX(选择一个时钟分频);
3)设置TCMPB0、TCNTB0;
4)设置TCONn寄存器。
下面介绍设置过程中使用到的寄存器。
(1)TCFG0寄存器(设置预分频器分频系数)
从上图可知,输送给Timer的时钟频率 = PCLK / {prescaler value+1} / {divider value} 。
- 输送给Timer的时钟频率,是指从“5:1 MUX”出来的时钟频率。
- prescaler value,是指预分频器的分频系数(8bit,所以0~255中的一个整数);
- divider value,是指分频器的分频系数(2、4、8、16分频)。
编程时,我们取值如下:
Timer clk = PCLK / {prescaler value+1} / {divider value}
= 50000000 / (99+1) / 16
= 31250 //从这个值减到0,才过去1秒钟。
所以代码中直接有:TCFG0 = 99; /* Prescaler 0 = 99, 用于timer0,1 */
(2)TCFG1寄存器(选择“5:1 MUX”的输出路线)
多路选择器有5路输入1路输出,我们通过设置TCFG1寄存器来选择其中的一路。
我们要把 [3:0] 设置为 0b0011=3,也就是16分频。
TCFG1 &= ~0xf; //清零
TCFG1 |= 3; /* MUX0 : 1/16 */
//直接写成 TCFG1=3 好像也行吧?
(3)TCNTB0寄存器、TCMPB0寄存器
TCNTB0寄存器保存定时器0的初始计数值,TCMPB0寄存器保存定时器0的比较值。
在启动定时器时,它们的值被传到TCNT0、TCMP0寄存器。
代码中设置如下:由于 Timer_clk 表示1秒31250次,那这里设置为(31250/2),也就是0.5秒。
/* 设置TIMER0的初值 */
TCNTB0 = 15625; /* 0.5s中断一次 */ // 即 Timer_clk/2
由于没有用到 TCMPB0 寄存器,这里没有设置它。
(4)TCNTO0寄存器
通过读取这个寄存器,我们可以得知TCNT0寄存器的实时的值(因为TCNT0寄存器不断减1,其值是变化的)。不需要设置它。
(5)TCON寄存器
代码如下,但TCON &= ~(1<<1); 为何又要清0?好像是硬件规定的。
/* 加载初值 */
TCON |= (1<<1); /* Update from TCNTB0 & TCMPB0 */
/* 设置为自动加载,并启动Time0 */
TCON &= ~(1<<1);//这里为何又要清0?
TCON |= (1<<0) | (1<<3); /* bit0: start, bit3: auto reload */
7.3 设置中断
由下图可知,接下来需要设置中断,显然我们需要提供一个中断处理函数。
我们在Timer章节里没有看到与中断相关的寄存器。所以我们回顾一下中断控制器,看看有没有与定时器相关的中断,但没有看到更加细致的、与设置Timer0有关的寄存器。
我们看一下PWM Timer章节,P314有以下内容:
由图可知,当TCNTn=TCMPn时不会产生中断,只有当TCNTn等于0的时候才可以产生中断。我们之前以为这个定时器可以产生两种中断,因此需要设置某个寄存器来选择这两种中断之一;但现在我们知道只有一种中断,这样的话只需要设置中断控制器即可。
这里设置INTMSK寄存器bit[10]即可(由INTMSK寄存器bit[10]可知),如下所示:
/* 初始化中断控制器 */
void interrupt_init(void)
{
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
INTMSK &= ~(1<<10); /* enable timer0 int */ //新添加这个
}
当定时器减到0的时候就会产生中断,就会进到start.S中执行do_irq函数,在do_irq函数中进入handle_irq_c函数中进行处理,然后在timer.c文件中的timer_irq函数中点灯。
void timer_irq(void)
{
/* 点灯计数 */
static int cnt = 0;
int tmp;
cnt++;
tmp = ~cnt;
tmp &= 7;
GPFDAT &= ~(7<<4);
GPFDAT |= (tmp<<4);
}
我们在Makefile中添加timer.o,进行编译烧写运行,发现灯没有闪烁。
为了调试,我们在main函数中设置打印TCNTO0寄存器的值,看是否有变化。
putchar(g_Char3);
g_Char3++;
delay(1000000);
printHex(TCNTO0);
运行结果:打印TCNTO0寄存器的值全是0。这说明我们的定时器根本就没有启用。
分析原因:在timer.c文件中的 timer_init 函数里,设置为自动加载并启动:需要先清掉手动更新位(之前没有取反所以出错),再或上bit0 与 bit3。
/* 设置为自动加载并启动 */
TCON &= ~(1<<1);//之前没有取反,所以实验失败
TCON |= (1<<0) | (1<<3); /* bit0: start, bit3: auto reload */
再次实验,灯已闪烁。
7.4 改进程序
由于每添加一个中断,都要修改interrupt.c文件中的handle_irq_c函数,这样太麻烦了。能不能不修改interrupt.c文件?
可以的,这里需要用到“函数指针数组”,在interrupt.c文件中定义一个函数指针数组。
typedef void(*irq_func)(int);
irq_func irq_array[32];//这里数组长度之所以是32,是因为SRCPND寄存器也是32bit的
我们想把每一个中断的处理函数,都放到这个函数指针数组中。当发生中断时,我们通过INTOFFSET寄存器得到这个中断号,我们从这个函数指针数组中调用对应的函数即可。
那么我们得提供一个注册函数,把中断号与对应的函数绑定,同时使能中断(这样以后都不需要修改interrupt_init函数了):
void register_irq(int irq, irq_func fp)
{
irq_array[irq] = fp; //把中断号与对应的函数绑定
INTMSK &= ~(1<<irq); //同时使能中断
}
然后怎么用呢?很简单:
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;
#if 0
/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
}
else if (bit == 10)
{
timer_irq();
}
#endif
/* 调用对应的处理函数 */
irq_array[bit](bit);
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}
在key_eint_init函数中注册中断:
/* 初始化按键, 设为中断源 */
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);
}
在timer_init 函数中注册中断:
void timer_init(void)
{
/* 设置TIMER0的时钟 */
/* Timer clk = PCLK / {prescaler value+1} / {divider value}
= 50000000/(99+1)/16
= 31250
*/
TCFG0 = 99; /* Prescaler 0 = 99, 用于timer0,1 */
TCFG1 &= ~0xf;
TCFG1 |= 3; /* MUX0 : 1/16 */
/* 设置TIMER0的初值 */
TCNTB0 = 15625; /* 0.5s中断一次 */
/* 加载初值, 启动timer0 */
TCON |= (1<<1); /* Update from TCNTB0 & TCMPB0 */
/* 设置为自动加载并启动 */
TCON &= ~(1<<1);
TCON |= (1<<0) | (1<<3); /* bit0: start, bit3: auto reload */
/* 设置中断 */
register_irq(10, timer_irq);
}
如此一来,main函数中就可以注释掉interrupt_init函数了(该函数根本就不会用到,因为它的功能已经发散给key_eint_init、timer_init函数了):
int main(void)
{
led_init();
//interrupt_init(); /* 初始化中断控制器 */
key_eint_init(); /* 初始化按键, 设为中断源 */
timer_init();