06-异常与中断

声明:

本文是本人在韦东山笔记的基础上加了一些注释,方便理解。原文地址:http://wiki.100ask.org/%E7%AC%AC014%E8%AF%BE_%E5%BC%82%E5%B8%B8%E4%B8%8E%E4%B8%AD%E6%96%AD

请来访的读者注意:如果你没读过本章笔记,请你忽略这一黑块里的总结。这一黑块是我多次阅读本笔记的总结。你可
以直接从本章“第01节 概念引入与处理流程”开始读。每读一遍往往有得:
1、处理und、swi异常无非就是那几个步骤,两者相同
2、处理中断,中断是一种特殊的异常。在本笔记示例中,und、swi异常是你故意加一句错误语句让程序错误。
而中断不然,中断的处理函数比较复杂,你得分辨中断源,然后处理中段,还得清中断。
并且你得对中断控制器(也即mask寄存器)和中断源(比如按键配置成中断)进行初始化。
3、本章重要内容,我想两点吧,第一是异常的机制,即第4节(第5节其实和第4节一样);第二点是中断的机制,标准
的中断编程比直接看本章的最后代码即可,即把中断处理函数用一个注册函数指针数组保存起来,以后用的时候很方便。
对这种中断注册编程进行总结:
1、程序从start.S的
_start:b reset          /* vector 0 : reset */
然后执行
reset:
然后
ldr pc, =main  /* 绝对跳转, 跳到SDRAM */
2、然后看主函数:
int main(void)
{key_eint_init();   /* ①初始化按键,②初始化相应的中断控制器相应的EINTMASK去使能相应的位,③把key_eint_init()注册到一个函数指针数组里*/timer_init(); /* ①初始化时钟,②初始化相应的中断控制器相应的EINTMASK去使能相应的位,③把timer_init()注册到一个函数指针数组里*/
/*注册函数:
void register_irq(int irq, irq_func fp)
{irq_array[irq] = fp;INTMSK &= ~(1<
/*比如:
register_irq(0, key_eint_irq);
就表示,一旦外部来0中断,那么就会跳到key_eint_irq()函数*/
3、一旦发生中断信号,那么汇编start.S文件中就会跳到中断处理函数,handle_irq_c()
void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;/* 调用对应的处理函数执行 */irq_array[bit](bit);/*要知道,你前面注册过了,来了哪个中断信号,就会处理相应的函数(比如对于本章的按键中断示例来说,那就是key_eint_irq())*//* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}4、我在练习中断编程的时候遇到的坑:
我的友善之臂的四个按键为:
k1 - GPG0 - EINT8;  k2 - GPG3 - EINT11;  k3 - GPG5 - EINT13;  k4 - GPG6 - EINT14;
而在void key_eint_irq(int irq)函数中,
/*先把这个值 读 出来,清的时候我再把他写进去。达到了清EINTPEND的目的*/unsigned int val1 = EINTPEND;if (irq == 5) //注意:EINT8、EINT11、EINT13、EINT14都包含在INTOFFSET的bit5,即这四者的irp共为5!!!{然后使用EINTPEND区分是四者中的哪一个。区分方法为:if(val1 & (1<<8)){//注意:不是if(val1 == 8)这种语法!!!!}if(val1 & (1<<11)){}if(val1 & (1<<13)){}if(val1 & (1<<14)){}}然后:EINTPEND = val1;//即清中断(恢复原本的EINTPEND值)
4、我在练习中断编程的时候遇到的坑二:
第四点中的

对下图解释:
屏蔽字就是屏蔽寄存器中的内容,屏蔽寄存器中的内容就是把各个屏蔽触发器组合到一起,而每个中断请求触发器都有一个屏蔽触发器。总而言之,你需要知道,每个中断源都可以通过设置MASK的值而被屏蔽,注意,屏蔽的是中断源信号。
而开、关中断,是指的,CPU是否去响应中断源的信号,是从CPU的角度出发的。
在这里插入图片描述

第01节 概念引入与处理流程

先来取个场景解释中断概念。
假设有个大房间里面有小房间,婴儿正在睡觉,他的妈妈在外面看书。问:这个母亲怎么才能知道这个小孩醒?
第一种方法:过一会打开一次房门,看婴儿是否睡醒,让后接着看书
第二种方法:一直等到婴儿发出声音以后再过去查看,期间都在读书

第一种 叫做查询方式:
优点:简单
缺点: 累
写程序表示这种方式:

while(1)
{1 read book(读书)2 open door(开门)if()return(read book)else照顾小孩}

第二种叫中断方式:
优点:不累
缺点:复杂
写程序:

while(1)
{read book中断服务程序()//如何被调用?{处理照顾小孩}
}

我们看看母亲被小孩哭声打断如何照顾小孩?

母亲的处理过程:
1 平时看书
2 发生了各种声音,如何处理这些声音
①有远处的猫叫(听而不闻,忽略)
②门铃声有快递(开门收快递)
③小孩哭声(打开房门,照顾小孩)
3 母亲的处理

故母亲只会处理门铃声和小孩哭声处理过程如下:
a 现在书中放入书签,合上书(保存现场)
b 去处理 (调用对应的中断服务程序)
c 继续看书(恢复现场)

不同情况,不同处理:
a 对于门铃:开门取快件
b 对于哭声:照顾小孩

我们将母亲的处理过程抽象化——母亲的头脑相当于CPU

耳朵听到声音会发送信号给脑袋,声音来源有很多种,有远处的猫叫,门铃声,小孩哭声。这些声音传入耳朵,再由耳朵传给大脑,除了这些可以中断母亲的看书,还有其他情况,比如身体不舒服,有只蜘蛛掉下来,对于特殊情况无法回避,必须立即处理。对比我们的arm系统:
a 有CPU,有中断控制器。
b 中断控制器可以发信号给CPU告诉它发生了那些紧急情况
c 中断源有按键、定时器、有其它的(比如网络数据)
这些信号都可以发送信号给中断控制器,再由中断控制器发送信号给CPU表明有这些中断产生了,这些成为中断(属于一种异常)。
异常就像是有一只蜘蛛掉下来,属于特殊情况无法回避,需要中断CPU,必须处理。比如指令不对,数据访问有问题。又reset信号,这些都可以中断CPU 这些成为异常中断。
母亲处理对比我们的arm系统如下图:
在这里插入图片描述
异常中断的重点在于保存现场以及恢复现场

异常中断处理过程
a 保存现场(各种寄存器)
b 处理异常(中断属于一种异常)
c 恢复现场

arm对异常(中断)处理过程
1 初始化:
a 设置中断源,让它可以产生中断
b 设置中断控制器(可以屏蔽某个中断,优先级)
c 设置CPU总开关,(使能中断)
2 执行其他程序:正常程序
3 产生中断:按下按键—>中断控制器—>CPU
4 cpu每执行完一条指令都会检查有无中断/异常产生
5 发现有中断/异常产生,开始处理。对于不同的异常,跳去不同的地址执行程序。这地址上,只是一条跳转指令,跳去执行某个函数(地址)(对于下面的程序,发生中断,就跳去0x18地址去执行某个函数(地址)_irq),这个就是异常向量。如下就是异常向量表,对于不同的异常都有一条跳转指令。
(注:3-5步都是硬件强制自行做的)

.globl _start
_start: b	resetldr	pc, _undefined_instructionldr	pc, _software_interruptldr	pc, _prefetch_abortldr	pc, _data_abortldr	pc, _not_usedldr	pc, _irq //发生中断时,CPU跳到这个地址(假设地址为0x18),执行该指令 ldr	pc, _fiq
//我们先在0x18这里放 ldr pc ,__irq,于是cpu最终会跳去执行__irq代码
//保护现场,调用处理函数,恢复现场

6 这些函数(地址)(如_irq)做什么事情?
软件做的:
a 保存现场(各种寄存器)
b 处理异常(中断):
b.a 分辨中断源
b.b 再调用不同的处理函数
c 恢复现场
(对比母亲的处理过程来比较arm中断的处理过程。)

中断处理程序怎么被调用?
CPU—>0x18 --跳转到其他函数->做保护现场->调用函数(分辨中断源、调用对应函数)->恢复现场
cpu到0x18是由硬件决定的,跳去执行更加复杂函数(由软件决定)。

第02节 CPU模式(Mode)、状态(State)和寄存器

这节课我们来讲CPU的工作模式(Mode) 、状态(State)和寄存器。可以参考书籍 《ARM体系结构与编程》作者:杜春雷

7种Mode:

usr/sys
undefined(und)
Supervisor(svc)
Abort(abt)
IRQ(irq)
FIQ(fiq)

2种State:

ARM state
Thumb state

寄存器:

通用寄存器
备份寄存器(banked register)
当前程序状态寄存器(Current Program Status Register);CPSR
CPSR的备份寄存器:SPSR(Save Program Status Register)

我们仍然以这个母亲为例讲解这个CPU模式:
这个母亲无压力看书 -->(正常模式)
要考试,看书—>(兴奋模式)
生病---->(异常模式)

对于ARM CPU有7种模式:
1、 usr :用户模式,类比 正常模式
2、 sys :系统模式,类比兴奋模式
3、 五种异常模式:(2440用户手册72页)
① und :未定义模式(CPU执行时遇到某条指令不认识时为此异常)
② svc :管理模式
③ abt :终止模式
a 指令预取终止(读写某条错误的指令导致终止运行,即CPU执行某条命令时,已在解析下一条指令,预取下下一条指令,而这个预取可能会出错,导致“指令预取终止”)。
b 数据访问终止 (读写某个地址,这个过程出错)。
这两者都会进入终止模式。
④ IRQ: 中断模式
⑤ FIQ: 快中断模式(快速处理中断)

我们可以称以下6种为特权模式(privileged mode)

.6种特权模式
und未定义模式
svc管理模式
abt终止模式
IRQ中断模式
FIQ快中断模式
sys系统模式

usr用户模式(不可直接进入其他模式) 。(防止写应用程序的人意外破坏整个系统)。
而上面的6种特权模式可以随便切换,可以编程去操作CPSR寄存器直接进入其他模式。这些异常模式是为了更好的处理相应的异常,每一种异常模式的差别在于CPU寄存器的差别,查看2440手册:
在这里插入图片描述
这个图是有关各个模式下所能够访问的寄存器,再讲这个图之前我们先引入 2种state。

CPU有两种state:
1 ARM state:使用ARM指令集,每个指令4byte
2 Thumb state:使用的是Thumb指令集,每个指令2byte

比如同样是:
mov R0, R1 编译后
对于ARM指令集,上面的这条汇编指令编译成的机器码要占据4个字节;
对于Thumb指令集,上面的这条汇编指令编译成的机器码要占据2个字节。
因此,引入Thumb可以减少存储空间。本章第3节会演示使用Thumb指令集编译,看是否生成的bin文件会变小很多。

ARM指令集与Thumb指令集的区别:
Thumb 指令可以看作是 ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有 16 位的代码密度,但是它不如ARM指令的效率高 。
Thumb 不是一个完整的体系结构,不能指望处理执行Thumb 指令而不支持 ARM 指令集。
因此,Thumb 指令只需要支持通用功能,必要时可以借助于完善的 ARM 指令集,比如,所有异常自动进入 ARM 状态。在编写 Thumb 指令时,先要使用伪指令 CODE16 声明,而且在 ARM 指令中要使用 BX指令跳转到 Thumb 指令,以切换处理器状态,编写 ARM 指令时,则可使用伪指令 CODE32声明。

好,理解了CPU的state之后我们来继续看这个图:ARM State General Registers and Program Counter即ARM状态通用寄存器和程序计数器。
在这里插入图片描述
上图,CPU在每种模式下都有R0 ~ R15。
1、在这张图注意到有些寄存器画有灰色的三角形,表示访问该模式下访问的专属寄存器,比如FIQ模式的r8_fiq寄存器。FIQ模式的r8_fiq寄存器和 System 模式下的r8是两个在物理性质上不同的寄存器。
2、不带灰色的三角形的寄存器是各种模式下的通用寄存器,比如r0,是这7种模式共用的寄存器,是同一个物理上的寄存器。

比如:
mov R0, R8
在System 模式下访问的是R0 ~ R8,在所有模式下访问R0都是同一个寄存器。
mov R0,R8_fiq
但是在FIQ模式下,访问R8是访问的FIQ模式专属的R8寄存器,不是同一个物理上的寄存器。

在这五种异常模式中每个模式都有自己专属的R13 R14寄存器,R13用作SP(栈), R14用作LR(返回地址) ,LR是用来保存发生异常时的指令地址。

为什么快中断(FIQ)有那么多专属寄存器(这些寄存器称为备份寄存器)。
解答这个问题,先回顾一下中断的处理过程
1 保存现场(保存被中断模式的寄存器)。
就比如说我们的程序正在系统模式/用户模式下运行,当你发生中断时,需要把R0 ~ R14这些寄存器全部保存下来,让后处理异常,最后恢复这些寄存器。
但如果是快中断,那么我就不需要保存 系统/用户模式下的R8 ~ R12这几个寄存器,在FIQ模式下有自己专属的R8 ~ R12寄存器,省略保存寄存器的时间,加快处理速度。但是注意,在Linux中并不会使用FIQ模式。
2 处理。
3 恢复现场。

上面这个图片还说到了CPSR、SPSR寄存器,CPSR是当前程序状态寄存器,这是一个特别重要的寄存器。SPSR保存的程序状态寄存器,他们二者的格式如下:在这里插入图片描述
上图中的解释:
1、首先 M4 ~ M0 表示当前CPU处于哪一种模式(Mode);我们可以读取这5位来判断CPU处于哪一种模式,也可以修改这一种模式位,让其修改这种模式;
2、假如你当前处于用户模式下,是没有权限修改这些位的;
3、M4 ~ M0对应什么值,在2440手册中有说明:
4、要知道,CPU只有一个CPSR寄存器,所以从一个模式到另一个模式时,要对CPSR寄存器进行设置。
在这里插入图片描述
查看上上一副图中的其他位(即除了M4~M0位):
1、Bit5 State bits表示CPU工作用Thumb State还是ARM State指令集。
2、Bit6 FIQ disable当bit6等于1时,FIQ是不工作的。
3、Bit7 IRQ disable当bit5等于1时,禁止所有的IRQ中断,这个位是IRQ的总开关。
4、Bit8 ~ Bit27是保留位。
5、Bite28 ~ Bit31是状态位, 什么是状态位呢,比如说执行一条指令:

cmp R0, R1

如果R0 等于 R1 那么zero位等于1,这条指令影响 Z 位,即如果R0 == R1,则Z = 1。
beq跳转到xxx这条指令会判断Bit30(即Z)是否为1,是1的话则跳转,不是1的话则不会跳转 使用 Z 位,如果 Z 位等于1 则跳转,这些指令是借助状态位实现的。

SPSR保存的程序状态寄存器: 表示发生异常时这个寄存器会用来保存被中断的模式下他的CPSR。
就比如我的程序在system系统模式下运行的 CPSR寄存器是某个值,当发生中断时会进入irq模式,这个SPSR_irq就保存系统模式下的CPSR的值。

现在,我们来看看发生异常时CPU是如何协同工作的:
进入异常的处理流程(硬件部分实现的),查看2440手册,如下图:
在这里插入图片描述
我们来翻译一下上面图片中的文字(硬件部分实现的):
发生异常时,我们的CPU会做什么事情:
1、把下一条指令的地址保存在LR寄存器里(某种异常模式的LR等于被中断模式的下一条指令的地址)
它有可能是PC + 4有可能是PC + 8,到底是那种取决于不同的情况。
2、 把CPSR(当前程序状态寄存器)保存在SPSR里面(某一种异常模式下SPSR里面的值等于CPSR)
3、 修改CPSR的模式为进入异常模式(即修改CPSR的M4 ~ M0进入异常模式)。
4 、跳到向量表。

退出异常怎么做?查看2440手册,如下图:
在这里插入图片描述
对上面图片文字进行翻译:
1、 让LR减去某个值,让后赋值给PC(即PC = 某个异常LR寄存器减去 offset)。
减去什么值呢(offset是什么呢)?也就是说,我们怎么返回去继续执行原来的程序(被中断模式),根据下面这个表来取值。
在这里插入图片描述
图表含义,比如:
①、如果发生的是SWI可以把 R14_svc复制给PC;
②、如果发生的是IRQ可以把R14_irq的值减去4赋值给PC。
2、 把CPSR的值恢复(CPSR 值等于 某一个异常模式下的SPSR)。
3、 清中断(如果是中断的话,对于其他异常不用设置)。

第03节 Thumb指令集程序示例

:本节并不重要,本节只是演示一下thumb指令集,在后续的学习中是用不到的。对于这一节,你不需要理解透彻,记住步骤即可。甚至不用会,以后用到的时候再来看就可以。
因为ARM的flash空间比较大,使用thumb节省内存的意义不大。一般flash比较小的单片机会比较重视这个)。

在上节视频里说ARMCPU有两种状态:
1、ARM State 每条指令会占据4byte;
2、Thumb State 每条指令占据2byte。

我们说过Thumb指令集并不重要,本节演示把一个程序使用Thumb指令集来编译它。
(本节代码见013_thumb_014_003文件夹)
使用上一章节的重定位代码,打开Makefile和Start.S。
以前的Makefile文件如下:

all:arm-linux-gcc -c -o led.o led.carm-linux-gcc -c -o uart.o uart.carm-linux-gcc -c -o init.o init.carm-linux-gcc -c -o main.o main.carm-linux-gcc -c -o start.o start.S#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-objcopy -O binary -S sdram.elf sdram.binarm-linux-objdump -D sdram.elf > sdram.dis
clean:rm *.bin *.o *.elf *.dis

若使用Thumb指令集,对其进行更改:

all:arm-linux-gcc -mthumb -c -o led.o led.c//只需要在arm-linux-gcc加上 mthumb命令即可arm-linux-gcc -c -o uart.o uart.carm-linux-gcc -c -o init.o init.carm-linux-gcc -c -o main.o main.carm-linux-gcc -c -o start.o start.S#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-objcopy -O binary -S sdram.elf sdram.binarm-linux-objdump -D sdram.elf > sdram.dis
clean:rm *.bin *.o *.elf *.dis

但是上面这种仅对led.c进行了使用thumb指令集,若全部使用还得一个一个改,麻烦。故进行如下改进:

all: led.o uart.o init.o main.o start.o //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.elfarm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-objcopy -O binary -S sdram.elf sdram.binarm-linux-objdump -D sdram.elf > sdram.dis
clean:rm *.bin *.o *.elf *.dis%.o : %.carm-linux-gcc -mthumb -c -o $@ $< //对于所有的.c文件使用规则就可以使用thumb指令集编译 $@表示目标 $<表示第一个依赖%.o : %.Sarm-linux-gcc -c -o $@ $<  //.S文件不在这里改,而是在代码中改。

对start.S需要修改代码
原重定位章节Start.S文件:

.text
.global _start_start:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

对其更改,下面是使用thumb指令集的Start.S文件

.text
.global _start
/*更改处一------------------------------*/
.code 32 //表示后续的指令使用ARM指令集
_start:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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]   /* 恢复原来的值 *//*更改处二------------------------------*//* 怎么从ARM State切换到Thumb State? */adr r0, thumb_func //定义此标号的地址add r0, r0, #1  /* bit0=1时, bx就会切换CPU State到thumb state */bx r0/*更改处三------------------------------*/
.code 16 //下面都使用thumb指令集	
thumb_func:	//需要得到这个标号的地址/*下面就是使用thumb指令来执行程序*/bl sdram_init//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 *//* 重定位text, rodata, data段整个程序 */bl copy2sdram/* 清除BSS段 */bl clean_bss//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr r0, =main  /* 绝对跳转, 跳到SDRAM ,先把main的地址赋值给R0 */mov pc, r0  /*让后再移动到PC*/halt:b halt

上传代码编译测试,出现错误,如下:

init.o(.text+0x6c):In function 'sdram_init2';
undefined reference to 'memcpy'

发现是init,o里sdram_init2使用的了memcpy函数的问题。查看init.c:

#include "s3c2440_soc.h"void sdram_init(void)
{BWSCON = 0x22000000;BANKCON6 = 0x18001;BANKCON7 = 0x18001;REFRESH  = 0x8404f5;BANKSIZE = 0xb1;MRSRB6   = 0x20;MRSRB7   = 0x20;
}#if 0/**************************************************************************   
* 设置控制SDRAM的13个寄存器
* 使用位置无关代码
**************************************************************************/   
void memsetup(void)
{unsigned long *p = (unsigned long *)MEM_CTL_BASE;	p[0] = 0x22111110;		//BWSCONp[1] = 0x00000700;		//BANKCON0p[2] = 0x00000700;		//BANKCON1p[3] = 0x00000700;		//BANKCON2p[4] = 0x00000700;		//BANKCON3	p[5] = 0x00000700;		//BANKCON4p[6] = 0x00000700;		//BANKCON5p[7] = 0x00018005;		//BANKCON6p[8] = 0x00018005;		//BANKCON7p[9] = 0x008e07a3;		//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4p[10] = 0x000000b2;		//BANKSIZEp[11] = 0x00000030;		//MRSRB6p[12] = 0x00000030;		//MRSRB7
}
#endif
/*下面函数使用了memcpy函数,显然是编译器的操作,使用了memcpy把数组里的值从代码段拷贝到了arr局部变量里
是否可以禁用掉memcpy*/
void sdram_init2(void)
{unsigned int arr[] = {0x22000000, 	//BWSCON0x00000700, 	//BANKCON00x00000700, 	//BANKCON10x00000700, 	//BANKCON20x00000700, 	//BANKCON3	0x00000700, 	//BANKCON40x00000700, 	//BANKCON50x18001, 	//BANKCON60x18001, 	//BANKCON70x8404f5, 	//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f40xb1,	//BANKSIZE0x20,	//MRSRB60x20,	//MRSRB7};volatile unsigned int * p = (volatile unsigned int *)0x48000000;int i;for (i = 0; i < 13; i++){*p = arr[i];p++;}}

查阅资料,资料文章中说没有什么方法禁用memecpy,但是可以修改这些变量。比如说将其修改为静态变量,这些数据就会放在数据段中,最终重定位时会把数据类拷贝到对应的arr地址里面去。即在unsigned int arr[]前加上const 和static。如下:

void sdram_init2(void)
{const static unsigned int arr[] = {  //加上const 和static0x22000000, 	//BWSCON0x00000700, 	//BANKCON00x00000700, 	//BANKCON10x00000700, 	//BANKCON20x00000700, 	//BANKCON3	0x00000700, 	//BANKCON40x00000700, 	//BANKCON50x18001, 	//BANKCON60x18001, 	//BANKCON70x8404f5, 	//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f40xb1,	//BANKSIZE0x20,	//MRSRB60x20,	//MRSRB7};volatile unsigned int * p = (volatile unsigned int *)0x48000000;int i;for (i = 0; i < 13; i++){*p = arr[i];p++;}
}

编译代码,得到的bin文件有1.4k左右。查看之前的文件使用ARM指令集是2K左右。从这里你可以看出thumb指令集的作用。我们再接着查看使用thumb指令集得到的反汇编代码,看看每条指令是否仅占用2个字节。看出地址30000074以后的指令都是占2个字节。

sdram.elf:     file format elf32-littlearmDisassembly of section .text:/*前面这些ARM指令还是占用4个字节*/
30000000 <_start>:
30000000:	e3a00453 	mov	r0, #1392508928	; 0x53000000
30000004:	e3a01000 	mov	r1, #0	; 0x0
30000008:	e5801000 	str	r1, [r0]
3000000c:	e3a00313 	mov	r0, #1275068416	; 0x4c000000
30000010:	e3e01000 	mvn	r1, #0	; 0x0
30000014:	e5801000 	str	r1, [r0]
30000018:	e59f005c 	ldr	r0, [pc, #92]	; 3000007c <.text+0x7c>
3000001c:	e3a01005 	mov	r1, #5	; 0x5
30000020:	e5801000 	str	r1, [r0]
30000024:	ee110f10 	mrc	15, 0, r0, cr1, cr0, {0}
30000028:	e3800103 	orr	r0, r0, #-1073741824	; 0xc0000000
3000002c:	ee010f10 	mcr	15, 0, r0, cr1, cr0, {0}
30000030:	e59f0048 	ldr	r0, [pc, #72]	; 30000080 <.text+0x80>
30000034:	e59f1048 	ldr	r1, [pc, #72]	; 30000084 <.text+0x84>
30000038:	e5801000 	str	r1, [r0]
3000003c:	e3a01000 	mov	r1, #0	; 0x0
30000040:	e5910000 	ldr	r0, [r1]
30000044:	e5811000 	str	r1, [r1]
30000048:	e5912000 	ldr	r2, [r1]
3000004c:	e1510002 	cmp	r1, r2
30000050:	e59fd030 	ldr	sp, [pc, #48]	; 30000088 <.text+0x88>
30000054:	03a0da01 	moveq	sp, #4096	; 0x1000
30000058:	05810000 	streq	r0, [r1]
3000005c:	e28f0004 	add	r0, pc, #4	; 0x4
30000060:	e2800001 	add	r0, r0, #1	; 0x1
30000064:	e12fff10 	bx	r030000068 <thumb_func>:
30000068:	f94ef000 	bl	30000308 <sdram_init>
3000006c:	f9fef000 	bl	3000046c <copy2sdram>
30000070:	fa24f000 	bl	300004bc <clean_bss>
/**下面的thumb指令占据2个字节**/
30000074:	4805      	ldr	r0, [pc, #20]	(3000008c <.text+0x8c>)
30000076:	4687      	mov	pc, r030000078 <halt>:
30000078:	e7fe      	b	30000078 <halt>
3000007a:	0000      	lsl	r0, r0, #0
3000007c:	0014      	lsl	r4, r2, #0
3000007e:	4c00      	ldr	r4, [pc, #0]	(30000080 <.text+0x80>)
30000080:	0004      	lsl	r4, r0, #0
30000082:	4c00      	ldr	r4, [pc, #0]	(30000084 <.text+0x84>)
30000084:	c011      	stmia	r0!,{r0, r4}
30000086:	0005      	lsl	r5, r0, #0
30000088:	1000      	asr	r0, r0, #0
3000008a:	4000      	and	r0, r0
3000008c:	04fd      	lsl	r5, r7, #19
3000008e:	3000      	add	r0, #0

综上所述,如果你的flash很小的话可以考虑使用Thumb指令集。Thumb指令集在后面的学习中没有任何作用,我们只是简单作为介绍。

第04节 und异常模式的程序示例

这节课我们写一个程序故意让其发生未定义异常,然后处理这个异常。

1、首先查看uboot中源码uboot\u-boot-1.1.6\cpu\arm920t,打开里面的start.S,看里面的下面这些就是异常向量表:

/*code: 28 -- 72*/#include #include /**************************************************************************** Jump vector table as in table 3.1 in [1]***************************************************************************/#define GSTATUS2   (0x560000B4)#define GSTATUS3   (0x560000B8)#define GSTATUS4   (0x560000BC)#define REFRESH(0x48000024)#define MISCCR (0x56000080)#define LOCKTIME	0x4C000000	/* R/W, PLL lock time count register */#define MPLLCON		0x4C000004	/* R/W, MPLL configuration register */#define UPLLCON		0x4C000008	/* R/W, UPLL configuration register */#define CLKCON		0x4C00000C	/* R/W, Clock generator control reg. */#define CLKSLOW		0x4C000010	/* R/W, Slow clock control register */#define CLKDIVN		0x4C000014	/* R/W, Clock divider control *//******下面这些就是异常向量表*****/.globl _start_start:	b   resetldr	pc, _undefined_instructionldr	pc, _software_interruptldr	pc, _prefetch_abortldr	pc, _data_abortldr	pc, _not_usedldr	pc, _irqldr	pc, _fiq_undefined_instruction:	.word undefined_instruction_software_interrupt:	.word software_interrupt_prefetch_abort:	.word prefetch_abort_data_abort:		.word data_abort_not_used:		.word not_used_irq:			.word irq_fiq:			.word fiq.balignl 16,0xdeadbeef

2、查看2440手册异常向量表定义:
在这里插入图片描述
3、接下来我们写程序:
(源码在014_und_exception_014_004)
打开我们以前的start.S文件,在这个基础上做修改:

.text.global _start_start:b reset  /* vector 0 : reset (vector是向量的意思,即上面的异常响亮表中的Addre)*/  /*一上电复位,是从0x00地址开始执行,跳到reset处*/b do_und /* vector 4 : und */ /*如果发生了 未定义指令异常 ,就会跳到0x04地址的 未定义指令异常处,执行do_und程序*//*假设一上电从0地址开始执行,reset,做一系列初始化之后*故意加入一条未定义指令und_code:.word 0xdeadc0de  /* 未定义指令 */当CPU发现无法执行此条指令时,就会发生未定义指令异常,就会执行do_undbl print2,*/do_und:/* 执行到这里之前:* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_und保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为11011, 进入到und模式* 4. 跳到0x4的地方执行程序 *///需要重新设置sp栈即r13,指向某一块没有使用的地址    /* sp_und未设置, 先设置它 */ldr sp, =0x34000000/*sp指向64M的SDRAM的最高地址*//* --------------保存现场 -------------*//*即把r0-r12,lr保存在栈中*//* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 *//* 发生异常时,被中断的地址会保存在lr寄存器中 先减后存即stmdb*//* lr是异常处理完后的返回被中断函数的返回地址, 也要保存 */stmdb sp!, {r0-r12, lr}/*------------ 处理und异常:*新建一个 exception.c文件,写用于打印异常信息的printException()函数,在这里调用---------*/mrs r0, cpsr//把cpsr的值读入r0ldr r1, =und_string//把下面的字符串地址赋值给r1,字符串地址在下面,里面有字符串。bl printException/*--- ------------恢复现场 -----------------------*/	/* r0-r12,lr这些寄存器保存在栈中,把他读取出来就可以了*//* 先读后加即ldmia*//* 把r0 ~ r12的值从栈中都取出来,并且把原来保存的lr值,赋值到pc中去*/ldmia sp!, {r0-r12, pc}^  /* ^ 会把spsr的值恢复到cpsr里 *//*注意,前面进入异常模式时,把cpsr的值保存到spsr里 是硬件完成的*//*如何定义字符串,可以百度搜索 arm-linux-gcc 汇编 定义字符串,*有一篇官方的说明文档:http://web.mit.edu/gnu/doc/html/as_7.html*方法为   .string "str"*下面两行是字符串实现的程序:*/ 	und_string:.string "undefined instruction exception"reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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_bssbl uart0_initbl print1/*---------------- 故意加入一条未定义指令--------------------- */und_code:.word 0xff123456  /* 未定义指令。CPU读入这条指令以后,执行不了这个未定义指令,*故pc跳到b do_und,从而跳到do_und 函数*/bl print2//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

上面的start.S文件中的printException()函数在一个新建exception.c文件中,用于打印异常信息:

   #include "uart.h"void printException(unsigned int cpsr, char *str) //cpsr打印相应的寄存器,str打印一个字符串{puts("Exception! cpsr = ");\\打印cpsrprintHex(cpsr);//输出cpsr的值puts(" ");//输出空格puts(str);//输出str值puts("\n\r");//回车,换行}

修改makefile添加文件,即加上exception.c文件,并且不再使用thumb指令集了,如下:
其中:
(1)第三行的 中 的 表 示 所 有 的 依 赖 , 即 依 赖 第 一 行 的 s t a r t . o l e d . o u a r t . o i n i t . o m a i n . o e x c e p t i o n . o 。 ( 2 ) 注 意 第 一 行 的 s t a r t . o l e d . o u a r t . o i n i t . o m a i n . o e x c e p t i o n . o 的 顺 序 。 ( 3 ) 第 十 行 的 ^中的 ^表示所有的依赖,即依赖第一行的start.o led.o uart.o init.o main.o exception.o。 (2)注意第一行的start.o led.o uart.o init.o main.o exception.o的顺序。 (3)第十行的 start.oled.ouart.oinit.omain.oexception.o2start.oled.ouart.oinit.omain.oexception.o3<中的 ^表示左面的第一个依赖,即依赖.c文件。

   all: start.o led.o uart.o init.o main.o exception.o#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elfarm-linux-ld -T sdram.lds $^ -o sdram.elf #用$ ^来包含所有的依赖arm-linux-objcopy -O binary -S sdram.elf sdram.binarm-linux-objdump -D sdram.elf > sdram.disclean:rm *.bin *.o *.elf *.dis%.o : %.carm-linux-gcc -c -o $@ $<%.o : %.Sarm-linux-gcc -c -o $@ $<*.dis

编译成功烧写。运行,结果发现没有输出我们想要的字符串。现在来调试。
很多同学想学会如何调试程序,这里我们演示:
第一步:
在上面的start.S的如下部分加入print1()和print2()两个函数。

    	bl print1 //添加print1/* 故意加入一条未定义指令 */und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2 //添加print2,实现这两个函数,来打印//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

第二步:在uart.c这个文件里写print1 print2这两个打印函数

  void print1(void){puts("abc\n\r");}void print2(void){puts("123\n\r");}

上传代码烧写,发现print1、print2并未执行成功。这是怎么回事。我们发现:在start.S中,在printf2()函数的后面才是main()函数,而uart0_init()在main()函数中。也就是说在执行print1函数、执行未定义指令、print函数时,并未初始化 uart0_init()。因此我们要删除main.c中的uart0_init()初始化函数,并把uart0_init()加到print1函数的前面问题就解决了。

	bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xff123456  /* 未定义指令 */bl print2//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

再次编译烧写,程序正常运行,print1 print2全部打印。但仍没有打印,未定义异常信息,这表明未定义指令并未运行,难道这个地址是一个已经定义的地址而不会触发异常?

为验证这个猜想,我们打开2440芯片手册,找到ARM指令集如下图:
(注:下图有一点错误,0-31应该是从右往左的而不是从左往右的,手册写错了)
(注:看从左往右数的5-8位数字,不符合这上面的就不是ARM指令集,我们随便挑个0011,在程序计算器里计算即把25、24位置1,得0x03000000,因此我们把

 und_code:.word 0xdeadc0de

中的0xdeadc0de改为0x03000000,来试试。
在这里插入图片描述
编译烧写执行,成功。打印了print1、未定义指令异常CPSR地址、打印了字符串、打印了print2、最后执行main函数。串口显示如下:

abc
Exception!  cpsr = 0x6000000DB  undefined  instruction exception
123g_A = 0x0000000
AaBbCcDdEeFfGg

其实下面这个也是一条未定义指令 ,只要指令地址对不上上表就是未定义指令。

.word 0xdeadc0de  

我们来查看一下cpsr是否处于未定义模式。根据打印结果:cpsr = 0x6000000DB,把0x6000000DB转为2进制发现bit[4:0]为 11011。从手册的图标可以知道,bit[4:0]为 11011表示CPU是处于und模式的。

下面对程序进行改进。
先看一下源程序,也就是上面的start.S文件。
有几个问题:
(1)程序中的b do_und即跳转到do_und函数,如果是nand启动,仍会跳转到sram里。
再看下面的一条程序bl printException,这里又使用bl指令跳转,如果是nand启动,而这个函数如果又在4k之外呢(有这种可能吧),这个函数必定出错。
所以为了保险,我们想让b do_und换成ldr pc, =do_und ,跳转到sdram中执行do_und。
(2)还有一个问题,你在(1)中这样改:ldr pc, =do_und,有一个问题:我们先按(1)方案进行编译,我们查看其反汇编码发现,ldr 从0x00000c0这个地方取值,0x00000c0存放的是0x30000008,0x30000008就是SDRAM上的do_und函数地址。也就是说do_und函数地址是存放在0x00000c0这个地方的。
普及一下知识:编译器通常把函数do_und的地址存放在汇编文件的最后。
问题来了:如果这个汇编文件大于4k的话,函数do_und的地址将无处存放。
怎么办?
我们可以把函数do_und的地址保存在汇编文件的前面的地址啊。方案是将ldr pc, =do_und改为ldr pc, und_addr,在代码ldr pc, und_addr下面加上如下代码:

und_addr:.word do_und

(3)改进处三:在start.S文件中的重定位代码之后,我们应该立刻跳到SDRAM中。因为重定位代码之后也是有可能在4K之外的把,所以时候跳转是很可取的。因此加入代码ldr pc, =sdram
(4)改进处四:.string "undefined instruction exception"中的字符串长度是不确定的,在该代码后面加上 .align 4,才能保证 后面的 程序以4字节对齐,保证程序运行。

    	ldr pc, =sdramsdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2

改进后代码如下

    .text.global _start_start:b reset  /* vector 0 : reset *//*----------------改进处一、二---------------------*/ldr pc, und_addr /* vector 4 : und *//*跳转到sdram执行这个函数,那么这个函数一定在sdram中。*我们需要指定让他去前面这块内存去读这个值,担心如果这个文件很大,超过4K,nand就没法去读这个文件*//*增加如下 查看反汇编,在08的地址读让后跳到3c*/    und_addr:.word do_unddo_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, cpsrldr r1, =und_stringbl printException/*前面的命令ldr pc, und_addr已经跳转到SDRAM中了,所以这里跳转到的*printException函数也是SDRAM中的函数*//* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 *//*----------------改进处四---------------------*/und_string:.string "undefined instruction exception"/**如果你的字符串长度稍有变化,就不能保证运行。故加上 .align 4才能保证  后面的  程序以4字节对齐,保证程序运行。**/.align 4reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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/*----------------改进处三---------------------*//*把链接地址赋值给pc 直接就跳转到sdram中*/   ldr pc, =sdramsdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

对于改进后的程序,我们来看一下整个程序的执行过程:
(图中有一处错误:nand flash前4k是复制到sram中运行的,故左面的内存应该是Nor flash/SRAM才对)。
需要知道,第4步发生异常以后pc回到的是Nor/SRAM,执行ldr pc,und_addr,而后跳转到SDRAM执行do_und。
在这里插入图片描述

第05节 swi异常模式的程序示例

这节我们再来演示swi的处理流程,源码在015_swi_exception_014_005文件夹。
swi为软件中断的意思:software interrupt。

在前面的视频中我们讲过ARMCPU有7中模式,除了用户模式以外,其他6种都是特权模式,这些特权模式可以直接修改CPSR进入其他模式。而usr用户模式不能修改CPSR进入其他模式。
我们知道,Linux应用程序一般运行于用户模式,即APP运行于usermode,(受限模式,不可访问硬件)。
但是,APP若想访问硬件,必须切换模式,怎么切换?
我们知道,发生异常有3种模式:
①、中断是一种异常,,但是中断可遇不可求。
②、und,但是也是可遇不可求。
③、swi + 某个值可以发生异常,让APP访问硬件(即使用软中断切换模式)

现在开始写程序。
首先,把在start.S中要做的事情列出来:
1、复位之后, cpu处于svc模式;
切换到usr模式;
设置栈;
跳转执行。
2、 故意引入一条swi指令。
3、 需在_start这里放一条swi指令。

查看异常向量表swi异常的向量地址是0x8,如下图:
在这里插入图片描述
我们先切换到usr模式下,下面两张图为资料:

/**5 先进入usr模式*/
mrs r0, cpsr      /* 读出cpsr 读到r0 */
/使用bic命令 bitclean 把低4位清零/
bic r0, r0, #0xf  /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0/*6 设置栈*/
/* 设置 sp_usr */
ldr sp, =0x33f00000

在这里插入图片描述在这里插入图片描述
下面是start.S文件的代码:

.text
.global _start_start:b reset          /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und *//*--------------根据swi_addr,跳转到SDRAM的do_swi------------*/ldr pc, swi_addr /* vector 8 : swi */und_addr:.word do_und
/*--------------存储do_swi函数地址于 汇编代码中靠前位置 的swi_addr------------*/
swi_addr:.word do_swido_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, cpsrldr r1, =und_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */und_string:.string "undefined instruction exception".align 4/*--------------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}  /* 处理swi异常 */mrs r0, cpsrldr r1, =swi_stringbl printException/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 *//*--------------swi_string函数,注:要加上.align 4,让下面的代码(reset)的地址4字节对齐。
*避免字符串长度造成的影响。
*我不是说执行完swi_string函数以后执行reset,而是说reset的地址在swi_string之后。看反汇编--------*/	
swi_string:.string "swi exception"
.align 4reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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模式 */msr cpsr, r0/* 设置 sp_usr */ldr sp, =0x33f00000ldr pc, =sdram
sdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 *///bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

下载烧写,程序执行完全没有问题。

swi可以根据应用程序传入的val来判断为什么调用swi指令,我们的异常处理函数能不能把这个val值(即上面程序中swi 0x123中的0x123)读出来。根据上面的代码,再写一个代码(改进)
start.S文件中改进的代码:

do_swi:
/*---------... ...省略一部分代码,这里不写,上面的start.S中有---------------*/
/*--------------我们知道: lr是异常处理完后的返回地址, 也要保存------- */stmdb sp!, {r0-r12, lr}  /* ----------------第二---------------
现在来获得lr中的地址。
我们要把lr拿出来保存,因为bl printException会破坏lr,即:mov rX, lr。
我把lr保存在那个寄存器?
答:这个函数 bl printException 可能会修改某些寄存器,但是又会恢复这些寄存器,我得知道他会保护哪些寄存器。我们之前讲过ATPCS规则:对于 r4 ~ r11在C函数里他都会保存这几个寄存器,如果用到的话就把他保存起来,执行完C函数再把它释放掉。
因此我们可以把lr 保存在r4寄存器里,因为r4寄存器不会被C语言破坏。
*/mov r4, lr/* 处理swi异常 */mrs r0, cpsrldr r1, =swi_stringbl printException/*-------------第一-----------------
我们在.文件中写printSWIVal函数,用于打印val即0x123。
如何才能知道swi的值呢?
我们得读出swi 0x123指令,这条指令保存在内存中,所以我们得找到他的内存地址。
我们知道,执行完0x123指令以后,会发生一次异常,那个异常模式里的lr寄存器会保存下一条指令(被中断模式)的地址。
综上,我们把lr寄存器的地址减去4就是swi 0x123这条指令的地址。
*//*-------------第三-------------
我再把 (r4寄存器-4) 赋给r0,然后r0(内容是swi 0x123指令的地址)传参给printSWIVal,然后打印。
之所以减4,是因为r4是 ldr pc, =main地址即被中断函数的地址。而r4-4是swi 0x123指令的地址。
*/sub r0, r4, #4bl printSWIVal/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */swi_string:
.string "swi exception"

在uart.c添加printSWIVal打印函数

/*把swi 0x123指令的地址传递过来,对地址取值
swi 0x123的反汇编:3000011c:	ef000123 	swi	0x00000123
把ef000123的前八位置零,并取后面的值即为0x123
*/
void printSWIVal(unsigned int *pSWI)
{puts("SWI val = ");printHEx(*pSWI & ~0xff000000); //高8位忽略掉  puts("\n\r");
}

这节视频我们讲解的是swi的处理流程。

第06节 按键中断的程序示例_概述与初始

注:本节源码在016_eint_014_006文件夹。

6.1 概述

在前面的视频里我们举了一个例子,母亲看书被声音打断,远处的声音来源有多种多样,声音传入耳朵,再由耳朵传入大脑,整个过程涉及声音来源耳朵大脑,为了确保这个母亲看书的过程能够被声音打断,我们必须保证声音来源可以发出声音,耳朵没有聋,脑袋没有傻。
类比嵌入式系统我们可以设置中断源,让他发出中断信号,还需要设置中断控制器,让他把这些信号发送给CPU,还需要设置CPU让他能够处理中断。

中断的处理流程:

中断初始化:
① 我们需要设置中断源,让它能够发出中断信号
② 设置中断控制器,让它能发出中断给CPU
③ 设置CPU,CPSR有I位,是总开关
经过上面三点的初始化设置,中断源才能有给CPU发送中断信号的能力。
处理时,要分辨中断源,对于不同的中断源要执行不同的处理函数。
处理完要清中断。

下面写程序:打开start.S 先做初始化工作,先做第

③“ 设置CPU,CPSR有I位,是总开关”。
我们需要把CPSR寄存器 bit7给清零,这是中断的总开关,若bit7设置为1,CPU则无法响应任何中断。
在这里插入图片描述
我们写代码,注:这个start.S文件是在上一节swi异常程序示例中的start.S的基础上修改的。
(1)在start.S文件中加入设置CPU的CPSR的I位的总开关。
(2)加入代码bl interrupt_init 初始化中断控制器;加入bl eint_init 初始化按键,设为中断源。

/*---------这里的代码仅是为说明CPU CPSP I位的设置,故很多代码省略。--------------*/reset:
/*---------------..........程序省略------------------*/
/* 清除BSS段 */bl clean_bss/* 复位之后, cpu处于svc模式* 现在, 切换到usr模式*/mrs r0, cpsr         /* 读出cpsr */bic r0, r0, #0xf     /* 修改M4-M0为0b10000, 进入usr模式 */
/* 1-----------把bit7这一位清零-------------------*/bic r0, r0, #(1<<7)  /* 清零I位, 使能中断 */msr cpsr, r0/* 设置 sp_usr */ldr sp, =0x33f00000ldr pc, =sdram
sdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 *//* 2---------------- 调用两个中断----------------- */bl interrupt_init /*初始化中断控制器*/bl eint_init /*初始化按键,设为中断源*//*--------需要初始化上面这两个函数-----------*/ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

添加一个 interrupt.c文件,用于写interrupt_init()、eint_init()函数。
这个程序( interrupt.c)稍微有些复杂,我们先来分析再来编写代码。
首先我们画个流程图:
注:main里一直在循环地打印字符串,按下按键时为产生中断,点灯。松开按键又陷入main()的循环打印字符串。
在这里插入图片描述

6.2 eint_init函数分析与编写

我们先来分析eint_init()(按键)。
我们想达到的效果是:按下按键灯亮松开按键灯灭。可以把下面四个按键全部配置为外部中断按键。
看原理图。在2440手册上分别搜索EINT0、EINT2、EINT11、EINT19,可以确定:
配置EINT0即配置GPF0、配置EINT2即配置GPF2;配置EINT11即配置GPG3、配置EINT19即配置GPG11。
在这里插入图片描述
打开芯片手册找到第九章 IO ports,找配置寄存器 GPFCON。
在这里插入图片描述
配置GPFCON:

 /* 初始化按键, 设为中断源 */void key_eint_init(void){/*1 配置GPIO为中断引脚 *///先把eint0和eint2这两个引脚清零GPFCON &= ~((3<<0) | (3<<4));//清零GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 *//*所谓中断引脚就是说,按下按键或者松开按键时有触发中断信号。具体是松开按下*下面有解释*/

通过电路图得知 S4 S5按键为EINT11号中断引脚和EINT19号中断引脚:
在这里插入图片描述
配置GPGCON:

GPGCON &= ~((3<<6) | (3<<11));
GPGCON |= ((2<<6) | (2<<11));   /* S4,S5被配置为中断引脚 */

现在来设置四个按键的中断触发方式: (按下松开,从低电源变为高电源,或者从高电源变为低电源),双边沿触发。
设置EINT0、EINT2、EINT11、EINT19为双边沿触发。代码如下。2440手册参考资料是下面的几张图片。

 EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */EXTINT1 |= (7<<12);             /* S4 */EXTINT2 |= (7<<12);             /* S5 */

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

沿着上面继续往下翻2440手册,我们看到了外部中断屏蔽寄存器EINTMASK。其设置为1的话就禁止向外部发出中断信号,只有EINTMASK相应的位设置为0外部中断才能给中断控制器发信号。因此我们需要设置这个寄存器。2440手册资料如下图。
在这里插入图片描述
EINTMASK这个寄存器里只有eint11,19,只对这两个设置即可。对于EINT0 和EINT2显示为保留,默认时使能的,可以直接发送给中断控制器,无需设置。原因如下图:编程如下:

/* 设置EINTMASK使能eint11,19 */EINTMASK &= ~((1<<11) | (1<<19));

在这里插入图片描述
到这里,eint_init()写完了。
但是需要注意:还有一个寄存器EINTPEND,它的作用是读EINTPEND,以分辨是哪个外部中断(EINT)产生,外部中断是eint4~23。并且要清除它,清除中断时, 写EINTPEND的相应位。资料如下:

在这里插入图片描述
到这里,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));
}/* 读EINTPEND分辨率哪个EINT产生(eint4~23)* 清除中断时, 写EINTPEND的相应位*/

6.3 interrupt_init分析与编写

我们接下来需要阅读2440手册的第14章 Interrupt Controller章节,设置中断控制器,我们只需要按照下面这张流程图设置就可以了:
在这里插入图片描述
看上面的图,我们需要设置:
①、MASK 屏蔽寄存器;
②、INTPND 等待处理,我们可以读这个寄存器,确定是哪个中断产生了;
③、SRCPND不同的中断类型不可以直接到达这里执行。即with sub -register和without sub -register两种。我们来看一下外部中断属于哪一种。
打开芯片手册,从上往下读
在这里插入图片描述
由上图可得 EINT4_7 、EINT8_23合用一条中断线ARB1。也就是说可以直接到达SRCPND不需要设置SUBSRCPND和SUBMASK这两个寄存器。
因此我们使用的外部中断源只需要设置SRCPND、 MASK 、INTPND这三个就可以。
在这里插入图片描述
上图中的SRCPND 寄存器作用:
(1)用来显示哪个中断产生了。(SRCPND寄存器是32位的,哪个中断源产生,SRCPND就会有相应的位 去置1,从而该中断信号经过该寄存器就会产生中断请求,这个请求不受INTMASK屏蔽和INTPND优先级的影响)。
(2)并且注意:执行完毕后需要清除SRCPND 对应位。
(3)看2440手册385页:
①SRCPND 寄存器的bit0对应eint0;
②SRCPND 寄存器 的bit2对应eint2;
③SRCPND 寄存器的bit5对应eint8_23;(表明bit5等于1的时候, eint8到23中的某一个已经产生。那么,到底是8到23中的哪一个呢?答:我们需要 EINTPEND寄存器分辨是 eint8到23中的哪一个EINT产生)。
综上:
(1)SRCPND自动完成中断信号的请求(这是硬件操作)。我们需要做的是,在使用完之后对SRCPND相应的位进行清位。
(2)怎么知道到底是8到23中的哪个中断信号EINT产生呢?我们读EINTPEND寄存器即可。

接着往下看手册,INTMOD寄存器 默认值为IRQ模式即可,不需要设置。
在这里插入图片描述
再接着往下看手册,INTMASK寄存器,需要设置为0。执行完毕后不不不需要清除INTMASK对应位。
INTMSK 用来屏蔽中断, 1对应masked屏蔽中断,我们需要设置相应位设置为0。

  • bit0-eint0
  • bit2-eint2
  • bit5-eint8_23
    在这里插入图片描述
    往下接着看手册。我们知道, 同时可能有多个中断产生。这么多个中断经过优先级以后,只会有一个通知CPU,是哪一个中断优先级最高,可以读INTPND就能知道当前处理的唯 一 一个中断是那一个。
    1 表示这个中断已经产生,需要配置相应的位。
    INTPND 用来显示当前优先级最高的、正在发生的中断,。并且需要清除对应位。
    bit0-eint0
    bit2-eint2
    bit5-eint8_23
    在这里插入图片描述
    在这里插入图片描述
    再往下看手册。INTOFFSET是用来显示INTPND寄存器中哪一位正在等待处理。
    INTPAD中bit0等于1的话,则INTOFFSET就等于0 ;
    INTPAD中bit1等于1的话,则INTOFFSET值就等于1;
    INTOFFSET作用 : 用来显示INTPND中哪一位被设置为1。
    在这里插入图片描述
    再往下看,SUBSRCPND寄存器我们用不到:
    简单说一下这个寄存器:当SRCPND某一位等于1时,比如INT_UART0为1时,它的中断来源可能有多个。这是串口0的中断,串口0的中断产生时有可能是接收到了数据(INT_RXD0),也有可能是发送了数据(INT_TXD0),还有可能是产生了错误,那么到底是哪一个呢?这就需要去读取SUBSRCPND下一级的源寄存器。这就是SUBSRCPND的作用。
    在这里插入图片描述
    综上所述,interrupt_init()我们只需要设置INTMSK这个寄存器即可。而SRCPND和INTPND只有发生中断时才需要设置。interrupt_init()的完整代码如下:
/* 初始化中断控制器 */
void interrupt_init(void)
{//1是屏蔽我们需要清零,外部中断0 外部中断2 外部中8_23里面还有外部中断11到19INTMSK &= ~((1<<0) | (1<<2) | (1<<5));}

interrupt_init ()、key_eint_init ()是在start.S中跳转的。我们直接在main()中调用这两个函数多好。因此我们修改start.S,删除里面的:

bl interrupt_init  /* 初始化中断控制器 */
bl key_eint_init  /* 初始化按键, 设为中断源 */

能使用c语言就使用C语言,在main.c文件中添加调用C函数:

int main(void)
{interrupt_init();  /* 初始化中断控制器 */key_eint_init();   /* 初始化按键, 设为中断源 */puts("\n\rg_A = ");printHex(g_A);puts("\n\r");

这节课的代码还没有写完,是一个半成品。下节课我们继续写按键中断的代码。

第07节 按键中断的程序示例_接着06节的程序继续写

假设一按按键就会产生中断,CPU就会跳到start.S的 执行。

_start:b reset          /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und */ldr pc, swi_addr /* vector 8 : swi */

具体跳到哪里执行,我们需要看看中断向量表,看看跳到哪里:
可以看出,我们跳到0x00000018 IRQ模式。
在这里插入图片描述
写start.S文件中的代码:

/*-----------------这里的start.S省略了很多代码,只是把与按键中断相关的代码写出来了。
*详细的代码源码文件夹的start.S------------------------*/
_start:b reset          /* vector 0 : reset */ldr pc, und_addr /* vector 4 : und */ldr pc, swi_addr /* vector 8 : swi *//*------------------------------1--------------------*/b halt			 /* vector 0x0c : prefetch abort *//*加上b halt是因为加入发生了*prefetch abort中断就让程序卡死*/b halt			 /* vector 0x10 : data abort */b halt			 /* vector 0x14 : reserved */ldr pc, irq_addr /* vector 0x18 : irq *//*一按按键就会产生中断,CPU就这里执行这句话。*/b halt			 /* vector 0x1c : fiq *//*----------------2----------------------*/
irq_addr:.word do_irq
/*--------------------3---------------------*/
do_irq:/* 执行到这里之前:* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址* 2. SPSR_irq保存有被中断模式的CPSR* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式* 4. 跳到0x18的地方执行程序 *//*------------------- 4 保存现场---------- *//* sp_irq未设置, 先设置它 */
/* --------- 分配不冲突的没有使用的内存就可以了-------------*/ldr sp, =0x33d00000
/*-----------发生中断时irq返回值是R14 -4 为什么要减去4,硬件结构让你怎么做就怎么做----*//* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 *//* lr-4是异常处理完后的返回地址, 也要保存 */sub lr, lr, #4stmdb sp!, {r0-r12, lr}  /* ------------------ 5 处理irq异常--------------- */
/*---------------在这C函数里分辨中断源,处理中断------------*/bl handle_irq_c/*---------------------6------------------*/	/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */

接下来我们在interrupt.c中写出 handle_irq_c() 处理函数, 这个是处理中断 的C函数。handle_irq_c()需要包括1、 分辨中断源;2、 调用对应的处理函数;3 、清中断 : 从源头开始清 这三步。
代码如下:

void handle_irq_c(void)
{/* -------------------1 分辨中断源---------------------------- *//*读INTOFFSET,在芯片手册里找到这个寄存器,它里面的值表示:INTPND寄存器中的哪一位被设置成1。*设置成1的那一位对应相应的外部中断。*/int bit = INTOFFSET;/*这里的bit即是被设置成的1的那一位,是位的序号,如EINT0、EINT3、EINT8_23等*//*--------------------2 调用对应的处理函数----------------- */if (bit == 0 || bit == 2 || bit == 5)  /* 对应eint0,2,eint8_23 *//*在这个程序里,bit只有*等于0、2、5三者中的一种*/{/*我们会调用一个按键处理函数*/key_eint_irq(bit); /* 处理中断, 并 清中断源EINTPEND */}/*-------------------------3 清中断 : 从源头开始清---------------*首先清 除掉中断源里面的某些寄存器(注:EINTPEND在上面的key_eint_irq()函数中清除了  )*其次清 SRCPND*最后清 INTPND*/SRCPND = (1<<bit);/*置1清位*/ /*清SRCPND 寄存器。记住,这个寄存器清的时候是置1是清除。你记住就行了。并且芯片手册上也有解释。*//*以我的理解就是,对SRCPND 读取的时候相应位的1表示中断请求。而清寄存器的时候1表示清,0表示不清。这两个功能不关联。一个是对寄存器读,一个是对寄存器写*/INTPND = (1<<bit);	
}

在inierrupt.c文件中写上一个程序中的key_eint_irq()函数,如下:

void key_eint_irq(int irq)
{
/*先把这个值 读 出来,清的时候我再把他写进去。达到了清EINTPEND的目的*/unsigned int val = EINTPEND;unsigned int val1 = GPFDAT;unsigned int val2 = GPGDAT;if (irq == 0) /* eint0 : s2 控制 D12(LED号) */{/****为了知道是按下还是松开,我们需要读值,写if else*/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;
}  

下面是上面程序的几个资料和补充说明:
(1)
在这里插入图片描述
(2)到底是发生哪一种中断,我们需要读取 EINTPND来判断是那个中断产生。
如果bit19等于1的话表明外部中断EINT19产生了,如果bit11等于1表用外部中断11产生,这里我们需要判断。

		if (val & (1<<11)) /* 表明外部中断eint11产生 */{	}else if (val & (1<<19)) /* 表用外部中断eint19 */{}

2440手册 EINTPND:
在这里插入图片描述
上传代码测试,编译通过。成功!!!

(3)清中断从源头开始清 ,这完全是按照中断流程操作的:
在这里插入图片描述

第08节 定时器中断程序示例

注:程序是在上一节程序的基础上改编的。源码在017_timer_014_008文件夹。

8.1 001文件夹程序

这节课我们来写一个定时器的中断服务程序 ,使用定时器来实现点灯计数。
查考资料是第2440的10章PWM TIMER, 可以参考书籍《嵌入式Linux应用程序开发完全手册》第10章。
我们先把这个结构图展示出来:
在这里插入图片描述
这个图的结构很好。
这里面肯定有一个clk(时钟),
1 、每来一个clk(时钟)这个TCNTn减去1 ;
2、 当TCNTn = TCMPn时,可以产生中断,也可以让对应的PWM引脚反转,(比如说原来是高电平,发生之后电平转换成低电平)
3、 TCNTn继续减1,当TCNTn = 0时,可以产生中断,pwm引脚再次反转 。
4、TCMPn 和 TCNTn的初始值来自 TCMPBn,TCNTBn。
5、TCNTn = 0时,可自动加载初始。

怎么使用定时器?步骤如下:
1 设置时钟
2 设置初值
3 加载初始,启动Timer
4 设置为自动加载
5 中断相关
注:由于2440没有引出pwm引脚,所以pwm功能无法使用,也就无法做pwm相关实验,所谓pwm是指可调制脉冲。PWM波如下:T1高脉冲和T2低脉冲,它的时间T1, T2可调整,可以输出不同频率不同占控比的波形,在控制电机时特别有用。
在这里插入图片描述我们这个程序只做一个实验,当TCNTn这个计数器到0的时候,就产生中断,在这个中断服务程序里我们点灯

写代码 打开我们的main函数。
我们需要实现定时器初始化函数:

int main(void)
{led_init();interrupt_init();  /* 初始化中断控制器 */
//我们初始化了中断源,同样的,我们初始化timerkey_eint_init();   /* 初始化按键, 设为中断源 */
//初始化定时器
/*---------------加入一个timer_init()函数-------------------*/timer_init();

新建一个 timer.c,我们肯定需要操作一堆寄存器, 在timer.c里添加头文件 #include “s3c2440_soc.h”。

 #include "s3c2440_soc.h"void timer_init(void){
设置TIMER0的时钟
设置TIMER0的初值
加载初值, 启动timer0
设置为自动加载并启动(值到0以后会自动加载)
设置中断,显然我们需要提供一个中断处理函数void timer_irq(void)在这里面我们需要点灯
}
void timer_irq(void){/*点灯计数*/
}

打开芯片手册,看下图:我们想设置timer0的话
首先设置8-Bit Prescaler
然后设置5.1 MUX(选择一个时钟分频)
再设置TCMPB0和TCNTB0(初值)
最后设置TCONn寄存器。
在这里插入图片描述
接着往下翻手册,看手册上写如何初始化timer:
在这里插入图片描述
大概意思是:
1、把初始值写到TCNTBn 和TCMPBn寄存器
2、设置手动更新位
3、设置启动位

接着往下看到时钟配置寄存器:
在这里插入图片描述
上图中有个计算公式:

Timer clk = PCLK / {(预分频数)prescaler value+1} / {divider value(5.1MUX值)}

我们知道我们的PCLK是50M。预分频数设置为99。divider value(即5.1MUX值亦即clock diver为1/16)设置为16。
Timer clk = 50000000 / (99+1) / 16
= 31250
也就是说我们设置TCON是31250,从31250一直减到0。

设置Prescaler0。 Prescaler0等于99,因此我们写入代码:

TCFG0 = 99;  /* Prescaler 0 = 99, 用于timer0,1 */

下面设置divider value。
TCFG1寄存器 :MUX多路复用器的意思,他有多路输入,我们可以通过MUX选择其中一路作为输出
在这里插入图片描述
根据上面mux的值,我们要把MUX0 设置成0011 只需要设置这4位即可,先清零 再或上 0011 就是3:因此我们写入代码:

TCFG1 &= ~0xf;
TCFG1 |= 3;  /* MUX0 : 1/16 */

现在我们再来看看初始值控制寄存器TCNTB0:
一秒钟点灯太慢了 ,就让0.5秒

TCNTB0 = 15625;  /* 0.5s中断一次 */

在这里插入图片描述
TCNTO0寄存器是用来观察里面的计数值的,不需要设置,不用管。

现在可以设置TCON寄存器了:
在这里插入图片描述
现在需要设置Timer0:
在这里插入图片描述
开始需要我们自己手工进行更新,代码如下:

TCON |= (1<<1);   /* Update from TCNTB0 & TCMPB0 */

把这两个值放到TCMTB0 和 TCMPB0中:
在这里插入图片描述
注意:这一位必须清楚才能写下一位

设置为自动加载并启动,我们需要先清掉手动更新位,再或上bit0 bit3

TCON &= (1<<1);
TCON |= (1<<0) | (1<<3);  /* bit0: start, bit3: auto reload */

现在我们来进行设置中断,显然我们需要提供一个中断处理函数void timer_irq(void) ,在Timer里没有看到中断相关的控制器,我们需要回到中断章节去看看中断控制器,看看有没有定时器相关的中断, 我们没有看到更加细致的Timer0寄存器。
在这里插入图片描述
从上面这幅2440中的图我们发现,当TCNTn=TCMPn时,他不会产生中断,只有当TCNTn等于0的时候才可以产生中断。我们之前以为这个定时器可以产生两种中断,那么肯定有寄存器中断或者禁止两种寄存器其中之一,那现在发现只有一种中断的话,就相对简单些 ,设置中断的话,我们只需要设置中断控制器。
设置interrupu.c中断控制器

/*初始化中断控制器 */
void interrupt_init(void){INTMSK &= ~((1<<0) | (1<<2) | (1<<5));INTMSK &= ~(1<<10);  /* enable timer0 int *//*bit 10的原因见下面的2440资料图片*/
}

把定时器相应的位清零就可以了,哪一位呢?
INTPND的哪一位? INT_TIMER0第10位即可:
在这里插入图片描述
到这里我们先来总结一下上面做的工作:
在main()函数里初始化中断void timer_init(void),当定时器减到0的时候就会产生中断,产生中断信号,就会进到start.s这里一路执行do_irq。

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, #4stmdb sp!, {r0-r12, lr}  /* 处理irq异常 */bl handle_irq_c/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */然后进入irq处理函数中处理,处理这个irq
void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;/* 调用对应的处理函数 */if(bit ==0 || bit == 2 || bit == 5)/*eint0,2,rint8_23*/
{key_eint_irq(bit);/*处理中断,清中断源EINTPEND*/
}
/*------------------------------1------------*/
else if(bit == 10)//如果等于10的话说明发生的是定时器中断,这时候就调用我们得timer_irq
{timer_irq();
}/* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}回到timer.c文件中,在这个定时器处理函数中我们需要点灯
void timer_irq(void)
{/* 点灯计数 循环点灯*/static int cnt = 0;int tmp;cnt++;tmp = ~cnt;tmp &= 7;GPFDAT &= ~(7<<4);GPFDAT |= (tmp<<4);
}

代码写完了。我们在Makefile中添加timer.o。我们来实验一下,上传代码,进行编译 ,编译后进行烧写 ,现象:灯没有闪烁。
我们来解决:
来调试看看问题出在哪:他不是有一个观察寄存器TCNYO0么?我们前面也说了。我们不断的打印这个值,看是否有变化。我们在main函数中不断打印这个值。
在这里插入图片描述

main(){
....省略代码
int main(void)
{
....省略代码while (1){
....省略代码
/*-------------------加入这句话--------------------------*/printHex(TCNTO0);}return 0;
}

编译实验
打印的值全都是0,也就是说我们的定时器根本就没有启用。
去发现错误:即在timer.c文件void timer_init(void)函数里TCON &= (1<<1);写错了,应该写为TCON &= ~(1<<1);

TCON &= ~(1<<1);//我们没有设置取反
TCON |= (1<<0) | (1<<3);  /* bit0: start, bit3: auto reload */

再次实验,成功!!!至此,程序写完。把程序保存在001文件夹。

8.2 002文件夹(程序改进)

对程序进行改进
我们的程序首先进入main函数中执行 timer_init();然后还需要修改interrupt.c 初始化函数 void interrupt_init(void) ;还需要调用中断处理函数 void handle_irq_c(void)
如果每次添加一个中断我都需要修改handle_irq这个函数,这样太麻烦,我能不能保证这个interrupt文件不变,只需要在timer.c中引用即可,这里我们使用指针数组:

首先,在interrupt.c中定义函数指针数组:
(函数指针数组就是,有一个数组,里面存的是函数的地址)
然后定义一个数组,我们来卡看下这里有多少项,一共32位,我们想把每一个中断的处理函数都放在这个数组里面来,当发生中断时,我们可以得到这个中断号,然后我从数组里面调用对应的中断号就可以了。
在interrupt.c中编程如下:

typedef void(*irq_func)(int);
irq_func irq_array[32];

那么我们得在interrupt.c中提供一个注册函数:

void register_irq (int irq, irq_func fp)
{irq_array[irq] = fp;INTMASK &= ~(1 << irq)
}

以后我就可以直接调用对应的处理函数

void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;/* 调用对应的处理函数 */
irq_array[bit](bit);/* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}//按键中断初始化函数需要注册/* 初始化按键, 设为中断源 */
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));/*-----------注册---------------key_eint_init()初始化时,对bit0,2,5进行注册,也就是说,*把key_eint_irq函数地址存放在数组irq_array[0]、 irq_array[2]、irq_array[5]中---------*/register_irq(0, key_eint_irq);register_irq(2, key_eint_irq);register_irq(5, key_eint_irq);}

在timer.c中也需要设置中断

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 *//* ----------设置中断 *//*-----------注册---------------key_eint_init()初始化时,对bit10进行注册,也就是说,*把key_eint_irq函数地址存放在数组irq_array[10]中---------------*/register_irq(10, timer_irq);}

。详细代码看文件夹中的源码。

下面我们来看看我们做了什么事情,总结一下。

我们定义了一个指针数组

typedef void(*irq_func)(int);

这个指针数组里面放有各个指针的处理函数

irq_func irq_array[32];

当我们去初始化按键中断时,我们给这按键注册中断函数

register_irq(0, key_eint_irq);
register_irq(2, key_eint_irq);
register_irq(5, key_eint_irq);

这个注册函数会做什么事情,他会把这个数组放在注册函数里面,同时使能中断

void register_irq(int irq, irq_func fp)
{irq_array[irq] = fp;INTMSK &= ~(1<<irq);
}//我们的timer.c中
timer_init();
//也会注册这个函数/* 设置中断 *//*-----------注册---------------key_eint_init()初始化时,对bit10进行注册,也就是说,*把key_eint_irq函数地址存放在数组irq_array[10]中---------------*/register_irq(10, timer_irq);

把这个中断irq放在第10项里同时使能中断,以后我们只需要添加中断号,和处理函数即可,再也不需要修改函数 、烧写执行这么麻烦了。

一上电程序运行过程

我们从start.s开始看, 一上电从 b reset运行做一列初始化

.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 */reset:/* 关闭看门狗 */ldr r0, =0x53000000ldr r1, =0str r1, [r0]/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m *//* LOCKTIME(0x4C000000) = 0xFFFFFFFF */ldr r0, =0x4C000000ldr r1, =0xFFFFFFFFstr r1, [r0]/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */ldr r0, =0x4C000014ldr r1, =0x5str r1, [r0]/* 设置CPU工作于异步模式 */mrc p15,0,r0,c1,c0,0orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iAmcr 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, =0x4C000004ldr 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, #0ldr 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, =0x33f00000ldr pc, =sdram
sdram:bl uart0_initbl print1/* 故意加入一条未定义指令 */
und_code:.word 0xdeadc0de  /* 未定义指令 */bl print2swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 *//***最后执行main函数***///bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */ldr pc, =main  /* 绝对跳转, 跳到SDRAM */halt:b halt

进入main.c做一系列初始化

int main(void)
{led_init();//interrupt_init();  /* 初始化中断控制器 */key_eint_init();   /* 初始化按键, 设为中断源 */timer_init();puts("\n\rg_A = ");printHex(g_A);puts("\n\r");

进入按键初始化程序 interrupt.c 初始化按键, 设为中断源

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.c函数一直循环执行 输出串口信息

 while (1){putchar(g_Char);g_Char++;putchar(g_Char3);g_Char3++;delay(1000000);//printHex(TCNTO0);}

定时器减到0的时候就会产生中断,start.S 跳到 0x18的地方执行

	ldr pc, irq_addr /* vector 0x18 : irq */b halt			 /* vector 0x1c : fiq */
.align 4do_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, #4stmdb sp!, {r0-r12, lr}  /* 处理irq异常 */bl handle_irq_c/* 恢复现场 */ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */

看看怎么处理irq

void handle_irq_c(void)
{/* 分辨中断源 */int bit = INTOFFSET;/* 调用对应的处理函数执行 */irq_array[bit](bit);/* 清中断 : 从源头开始清 */SRCPND = (1<<bit);INTPND = (1<<bit);	
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部