s3c2440裸机-异常中断

1 异常中断引入#

在arm架构的处理器中,cpu有7中工作模式,2中工作状态。

1.1 CPU模式#

7种Mode: 除了usr/sys,其他5种都是异常模式。我们知道中断属于异常的2中,中断有irq,fiq。
usr sys undefined(und) Supervisor(svc) Abort(abt) irq fiq
用户模式 系统模式 未定义指令异常模 svc管理模式 终止模式(1.指令预取终止(读写某条错误的指令导致终止运行);2.数据访问终止(读写某个非法地址程序终止)) irq中断 快中断

除了usr模式,其他6中为特权模式。 CPU无法从usr模式直接进入特权模式。不能直接进入特权模式,那么怎么进入特权模式呢?

可以通过设置CPSR进入其他模式。

1.2 工作State#

ARM state
Thumb state(几乎用不上)

1.3 ARM寄存器#

(1)通用寄存器:
(2)备份寄存器(banked register):
    CPSR:当前程序状态寄存器(Current Program Status Register) 反映程序处在那种状态
    SPSR:CPSR的备份寄存器 (Saved Program Status Register)  用来保存"被中断前的CPSR"

下图是我们arm状态下的通用寄存器和程序状态寄存器

R13是SP(栈指针)
R14是LR(link register),程序跳转或者发成异常时的返回地址
R15是PC(程序计数器)

假设cpu执行:

1
2
mov R0, R8
mov R0,R8_fiq

在usr/System 模式下访问的R8, 但是在FIQ模式下,访问R8是访问FIQ模式专属的R8寄存器,不是同一个物理上的寄存器。
在5种异常模式中每个模式都有自己专属的R13 R14寄存器,R13用作SP(栈), R14(LR)是用来保存发生异常时的指令地址。

为什么快中断(FIQ)有那么多专属寄存器?
这些寄存器称为备份寄存器,我们先看下中断处理流程

1 保存现场(保存被中断模式的寄存器)---(比如程序正在sys/usr模式下运行,当发生中断时,需要把R0-R14这些寄存器全部保存下来)
2 异常处理(去分辨是哪一个中断源产生了中断,去执行对应的中断服务程序)
3 恢复现场(恢复被中断时保存下来的寄存器R0-R14)

但如果是快中断,那么我就不需要保存系统/用户模式下的R8 ~ R12这几个寄存器,因为在FIQ模式下有自己专属的R8 ~ R12寄存器,省略保存寄存器的时间,加快处理速度,所以它才称得上快中断。

1.3.1 CPSR程序状态寄存器#

在异常中断中PSR寄存器会使用的很频繁,PSR寄存器的格式如下图:

我们再来看看下表,反映的是PSR的 M[4:0]与arm工作模式的关系:

我们可以按照上图的对应关系设置CPSR,让其进入与之对应的模式。

1.3.2 SPSR程序状态备份寄存器#

1.4 异常向量表#

异常向量:不同的异常有不同的入口函数,那么这个异常入口函数的地址就是存放在该异常向量的位置。从该异常向量读取到的数据就是异常入口函数的地址。
异常向量表:就是由异常向量组成的集合。

下图是从uboot源代码中截取的smdk2410 的异常向量表:

1
2
3
4
5
6
7
8
9
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq

异常向量表对应的地址如下图:

2 异常处理流程#

CPU是如何进入到中断模式,执行中断服务程序的?

2.1 中断前#

下图是中断未触发前的程序执行过程:

2.1.1 初始化中断#

1,设置中断源(使中断能够产生,让CPU知道是哪个中断)
2,设置中断控制器(设置中断屏蔽,中断优先级)
3,设置中断总开关CPSR (使能中断)

2.2 中断产生后#

举个栗子:按键按下,产生按键irq。

2.2.1 硬件上的处理流程#

cpu强制跳转到异常向量表上对应的_irq异常向量(0x18)去读取指令(这个是CPU强制执行的,不需要我们去控制)。

具体的进入中断向量和中断返回流程见下图:

1
2
3
4
5
6
7
8
9
进入:
(1) LR_异常=PC + offset(具体多少看下图)
2)SPSR_异常=被中断前的程序模式CPSR
(3) CPSR被设置成对应的异常模式
4)跳转到对应的异常向量去执行
退出(返回):进入和退出就是一个逆过程
1)PC= LR_异常 -offset
2)被中断前的程序模式CPSR = SPSR_异常
3)中断结束,清中断

进入异常和返回异常时pc和lr的关系如下图:

从图中我们发现进入不同异常,offset的值也是有差异的。

2.2.2 软件上的处理流程#

1.当跳转到irq异常向量(0x18)后,发现该处是一条跳转指令“ldr pc, _irq”,
那么会通过ldr绝对跳转指令跳到到真正的中断处理函数_irq去执行。
2.那么在_irq的函数中我们需要按照之前说的**中断处理流程**去执行:
    (1)保存现场
    (2)异常处理(去分辨是哪一个中断源产生了中断,去执行对应的中断服务程序)
    (3)恢复现场

流程图总结下中断产生后的详细处理过程:

3 中断实例#

3.1 und-未定义指令异常#

先来看下当cpu解析到什么样的指令才会触发未定义指令异常呢?

从上面的arm指令格式中可知,只要指令码属于划线的格式,就属于未定义指令异常。

3.1.1 汇编向c函数传参#

我们知道汇编给C语言函数传参是通过r0,r1,…通过堆栈的方式去传递的参数,比如r0=1, r1=2;那么在被调用的c函数中argv0就是r0, argv1就是r1…,那么我们如果通过汇编给C函数传递字符串呢?

声明und_string为一个字符串:

1
2
und_string:
.string "undefined instruction exception"

然后用ldr r1, =und_string,这样r1中就保存了und_string的地址。
这样调用我们的c函数就可以把und_string传入进去。

3.1.2 und异常程序示例#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text
.global _start
_start:
b reset /* vector 0 : reset */
b do_und /* vector 4 : und (看中断向量表)*/
reset:
/*看门狗
时钟
sdram
设置SP
重定位*/
...
bl print1
und_code:
.word 0xdeadc0de; /*定义一条未定义指令*/
/*故意以一个数据的方式引入一条未定义指令,当cpu执行到这里,读取0xdeadc0de指令码的时候,
发现无法识别这条指令,就发生未定义指令异常,就跳转到0x4的中断向量去执行*/

bl print2
...

在未定义指令异常前后加上打印print1, print2,如果出现未定义指令异常后,就会跳到0x4的地方去读取指令,print2也就没法执行

当跳转到0x4的中断向量后,发现此处是一条跳转指令bl do_und, 我们再到未定义指令异常的服务程序do_und中打印出und_string这个字符串的内容。
现在开始写指令异常的服务程序do_und,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
do_und:
/* sp_und未设置, 先设置它 (由于之前一直处于管理模式,现在处在und状态)*/
ldr sp, =0x34000000

/* 保存现场 */
/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr} /*先减后存*/ /* 把栈中的值备份到r0-r12*/

/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string /*保存und_string地址*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /*(ldmia先读后加),把备份的值恢复到栈中,让pc=lr就可以恢复到异常前的指令地址。^会把spsr的值恢复到cpsr里 */

下面来分析一下这个未定义指令异常服务程序:

  1. 进入未定义指令异常服务do_und之前硬件自动完成的事情如下:

    1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
    2. SPSR_und保存有被中断模式的CPSR
    3. CPSR中的M4-M0被设置为11011, 进入到und模式
    4. 跳到0x4的地方执行程序 (bl do_und)
  2. 进入指令异常服务程序do_und后,我们需要保存现场,处理und异常,恢复现场,注意:由于发生了cpu模式切换,如果要用到栈,那么先要设置对应模式的栈。由于栈的地址是向下生长的,这里我就用sdram的末位地址作为栈指针,把sp_und=0x34000000。

  3. 在und异常服务程序中有可能会用到栈, 所以先保存现场,通过stmdb sp!, {r0-r12, lr}语句把栈中的值备份到r0-r12和lr,然后恢复现场的时候通过ldmia sp!, {r0-r12, pc}^,详见上面的注释。

  4. 我们看到保存现场后,我们把cpsr的值放到r0, 把und_string放到r1, 然后用bl printException调用c函数,这样我们的c函数printException就能收到汇编传过来的参数,一个是cpsr模式(r0),一个是und_string汇编传过来的字符串(r1)。我们用C函数实现printException:

1
2
3
4
5
6
7
void printException(unsigned int cpsr, char *str) {
puts("Exception! cpsr = ");
printHex(cpsr);
puts(" ");
puts(str);
puts("\n\r");
}

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
.text
.global _start

_start:
b reset /* vector 0 : reset */

b do_und /* vector 4 : und (看中断向量表)*/

do_und:
/* 执行到这里之前:
* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_und保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为11011, 进入到und模式
* 4. 跳到0x4的地方执行程序 (bl do_und)
*/


/* sp_und未设置, 先设置它 (由于之前一直处于管理模式,现在处在und状态)*/
ldr sp, =0x34000000

/* 保存现场 */
/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr} /*先减后存*/ /* 把栈中的值备份到r0-r12*/

/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string /*保存und_string地址*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /*(ldmia先读后加),把备份的值恢复到栈中,让pc=lr就可以恢复到异常前的指令地址。^会把spsr的值恢复到cpsr里 */

und_string:
.string "undefined instruction exception"

reset:
/* 关闭看门狗 */
/* 时钟 */
/* sdram */
bl copy2sdram
bl clean_bss

bl uart0_init

bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2

//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

测试结果如下:

打印出print1中的字符串‘abc’后,紧接着打印printException函数中的结果,cpsr=0x600000db,那么对应的M[4:0]=11011, 对应下图为und模式。然后从und异常返回,恢复原来的模式继续执行。

3.1.3 示例改进#

3.1.3.1 指令4字节对齐#

我们将上面的代码的und_string字符串修改一下:

1
2
3
4
5
6
7
8
...
und_string:
.string "undef instruction"

reset:
/* 关闭看门狗 */
/* 时钟 */
...

编译烧录再次运行,发现没有任何打印输出,这是为什么呢?我明明只是把und_string字符串改了一下呀。

查看反汇编:

我们发现reset的地址是0x30000032,竟然不是4字节对齐的,我们知道arm指令集是以4字节为基本单位的,那么这里没有对齐,肯定无法解析指令。那么我们手工改进代码如下:

1
2
3
...
und_string:
.string "undef instruction"
.align 4
1
2
3
4
reset:
/* 关闭看门狗 */
/* 时钟 */
...

我们再来看看反汇编,发现reset的地址是30000040,是以4字节对齐的,再次烧录运行,发现能够正常输出print1, 能够进入未定义指令异常。

3.1.3.2 绝对跳转进入异常向量#

如果我们程序非常大,中断向量入口代码的地址可能会大于sram的容量4k,比如do_und和do_swi,那么这个时候就需要用绝对跳转。

1
2
3
4
5
.text
.global _start
_start:
b reset /* vector 0 : reset */
b do_und /* vector 4 : und (看中断向量表)*/

将上面的相对跳转换成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
 .text
.global _start

_start:
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
...
und_addr:
.word do_und
swi_addr:
.word do_swi

这样我们的do_und, do_swi就可放在4k之外的地方, 放到sdram。

3.1.3.3 重定位后跳转sdram上执行#

我们现在不断增加的程序代码量,那么有可能在 ldr pc, =main 这条指令执行之前程序就已经超过4k。那么我们当从nand启动的时候,还没执行到ldr pc, =main这句来,就无法取指令执行了。nor同理超过2M也就无法取指令执行了。 所以我们干脆重定位完代码后就直接跳转到sdram上去执行,代码简要概述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
reset:
/*看门狗
时钟
set SP
sdram_init
重定位*/
ldr pc, =sdram
sdram:
...
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt

我们再来分析下整个程序执行过程:

1.一上电,cpu从0地址执行,执行b reset(进行初始化硬件)
2.重定位程序
3.跳转到sdram去继续执行
4.执行到 deadc0de,发生未定义指令异常
5.跳转到异常向量表的0x4地址去执行
6.跳转到sdram上执行异常处理函数(do_und)
7.异常返回,继续执行

3.2 swi-软中断#

arm有7中工作模式,除了usr模式,其他6种都是特权模式。

我们知道usr模式无法修改CPSR直接进入其他特权模式,但linux应用程序一般运行在usr模式,既然usr模式权限非常低,是无法直接访问硬件寄存器的,那么它是如何访问硬件的呢?

1
linux应用程序是通过系统调用,从而进入内核态,运行驱动程序来访问的硬件,那么系统调用又是如何实现的呢,就是通过软中断swi指令来进入svc模式,进入到svc模式后当然就能访问硬件啦。

所以我们的应用程序在usr模式想访问硬件,必须切换模式:

有以下两种方式:

1
2
1. 发生异常或中断(被动的)
2. swi + 某个值(主动的)

3.2.1 进入软中断swi#

s3c2440 一上电会跳到0地址(reset复位)执行代码,此时CPU处于svc模式,2440异常向量表如下图所示:

为了验证usr模式能够主动的通过swi软中断指令来进入svc模式, 我们先将模式切换到usr模式,那么这个时候就不能访问硬件了,也不能直接修改cpsr直接进入其他模式。

从上图我们设置CPSR让M4-M0处在10000,这样就进入了usr模式。修改start.s如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.global _start

_start:
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
und_addr:
.word do_und
swi_addr:
.word do_swi

reset:
/*
看门狗
时钟
set SP
sdram_init
重定位
bl uart0_init
*/
/*先进入usr模式*/
mrs r0, cpsr /* 读出cpsr 读到r0 */
/*使用bic命令 bitclean 把低4位清零*/
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0 /* 写入cpsr */

/* 设置usr模式下的栈sp_usr */
ldr sp, =0x33f00000
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

那么当执行到swi 0x123,就会触发SWI异常, 进入0x8的向量去执行,调用do_swi,我们参考do_und实现我们的软中断服务程序do_swi。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 /*这里r0, r1只是为了给printException传参*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.global _start
b reset
ldr pc, und_addr
ldr pc, swi_addr
...
und_addr:
.word do_und
swi_addr:
.word do_swi
do_swi:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/

/* sp_svc未设置, 先设置它 */
ldr sp, =0x33e00000

/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string /*这里r0, r1只是为了给printException传参*/
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

.align 4

reset:
/*
看门狗
时钟
set SP
sdram_init
重定位
bl uart0_init
*/
/*先进入usr模式*/
mrs r0, cpsr /* 读出cpsr 读到r0 */
/*使用bic命令 bitclean 把低4位清零*/
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
msr cpsr, r0 /* 写入cpsr */

/* 设置usr模式下的栈sp_usr */
ldr sp, =0x33f00000
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt

do_swi中调用printException,打印出了软中断异常的字符串和CPSR对应的svc模式。

3.2.1.1 打印出swi软中断号#

我们要读出swi 0x123指令,我们知道当执行完swi 0x123指令以后,会发生swi异常,那么lr_svc = PC + offset。从下图看出offset是4:

修改中断服务函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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}

我们要把lr拿出来保存,因为bl printException会破坏lr,那么把lr保存在哪个个寄存器比较好呢?

我们知道当调用bl printException可能会修改某些寄存器,但是又会恢复这些寄存器,那么得知道它会保护哪些些寄存器。
来看下ATPCS规则:

img

在子程序中,使用R4~R11来保存局部变量,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。所以对于 r4 ~ r11在C函数里会保存这几个寄存器,执行完C函数再把它释放掉并且恢复原来的值。我们把lr 保存在r4寄存器里,r4寄存器不会被C语言破坏。

1
2
3
4
5
mov r4, lr
/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException

当执行完swi 0x123指令后,会发生swi异常,swi异常模式里的lr寄存器会保存下一条指令的地址(即’ldr pc, =main’),我们把lr寄存器的地址减去4就是swi 0x123这条指令的地址。

把r4的寄存器赋给r0让后打印我们得写出打印函数:

1
2
3
4
5
6
7
8
9
10
    mov r0, r4

sub r0, r4, #4 //得到swi指令的地址
bl printSWIVal

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

在uart.c添加printSWIVal打印函数:

1
2
3
4
5
void printSWIVal(unsigned int *pSWI) {
puts("SWI val = ");
printHEx(*pSWI & ~0xff000000); //高8位忽略掉
puts("\n\r");
}

3.3 irq-外部中断#

3.3.1 引入外部中断#

我们想实现一个按键点灯程序,我们知道有以下两种方案:

1
2
1.轮询方案:轮询检测按键的电平状态,当检测到被按下后,对应的gpio会拉低,点亮对应的led;(略)
2.中断方案:将按键配置成外部中断源,当有按键按下,触发中断,在中断服务程序(isr)中去完成点灯。

我们用按键作为外部中断源,我们把按键对应的gpio配置成中断引脚,当按键按下,相应的gpio产生了电平跳变,就会触发外部中断。

3.3.2 外部中断示例#

我们想达到按下按键灯亮, 松开按键灯灭这种效果(配成双边沿触发,按下的时候产生下降沿中断,进行点亮,松开产生上升沿中断,进行熄灭)。当然也可做成按一下点亮,再按一下熄灭的效果(设成单边沿触发,每来一次中断,对led电平进行一次取反)。
原理图如下:


从按键的原理图中得知,当按键没有按下时,接上拉电阻,按键为高电平状态。当按键按下时,电位被拉低,按键处于低电平状态。s2-s5分别对应GPF0,GPF2,GPG3,GPG11; D10-D12这3盏led所对应的gpio分别是GPF4,GPF5,GPF6。

那么我们让s2,s3,s4分别控制D10,D11,D12;s5对D10-D12同时控制(按下s5同时点亮3个led)。

3.3.1.1 配置GPIO和中断源#

配置D10-D12的gpio为输出模式,s2-s4的gpio为外部中断模式。

打开芯片手册找到第九章 IO ports,找到对应的gpio控制寄存器,将对应的gpio配置成中断模式。

  1. 配置GPF GPIO为中断引脚:

    同理GPG的寄存器类似。
1
2
3
4
5
GPFCON &= ~((3<<0) | (3<<4));	//先把eint0和eint2这两个引脚清零
GPFCON |= ((2<<0) | (2<<4)); //S2,S3被配置为中断引脚

GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); //S4,S5被配置为中断引脚
  1. 设置中断触发方式:

当电平从高变低时,此时表示按键按下,当电平由低变高,表示松开按键。不妨设置中断方式为双边沿触发,按下按键,触发下降沿中断,中断服务程序就可以去点亮led,反之,松开触发上升沿中断,就可以去熄灭led。


1
2
3
EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
  1. 设置外部中断屏蔽寄存器EINTMASK:

从上图我们知道外部中断0-3是直接连接到中断控制器,而外部中断4-7、外部中断8-23还要经过EINTMASK,那么我们需要配置EINTMASK来打开中断的通道:

1
EINTMASK &= ~((1<<11) | (1<<19));    //打开外部中断通道
  1. 外部中断挂起寄存器EINTPEND:

当一个外部中断(EINT4-EINT23)发生后,那么相应的位会被置1, 所以中断结束后需要清除对应位。这个寄存器可以用来区分外部中断4-23的哪一个中断源。

3.3.3.2 中断控制器设置#

我们先来看下中断控制器的总框图:

1. 首先是SRCPND:用来表示哪个中断源发出了中断请求。

先看下中断源:

从上图我们发现外部中断有24个外部中断,除了外部中断EINT,还有定时器中断,ADC中断,UART中断等…。

我们来认识下SRCPND寄存器:(用来表示哪个(哪些)中断源已产生中断请求,中断结束后要清中断)

从上图中我们发现EINT4-7共用1bit,EINT8-23共用1bit,那么肯定有其他寄存器来区分它们,那就是EINTPEND寄存器(后面5会讲)。

2. 然后到达INTMSK:(中断屏蔽寄存器)

我们需要把INTMSK寄存器配置成非屏蔽状态,默认是中断源时屏蔽的,见下图:

3.INTMOD(中断模式,是fiq还是irq)

4.Priroty:

5.INTPND:
INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位。

中断发生后,SRCPND中会有bit置1,可能好几个(因为同时可能发生几个中断),这些中断会由优先级仲裁器选出一个最紧迫的,然后把INTPND中相应位置1。所以只有INTPND置1,CPU才会处理。

我们知道有可能同时出现多个中断请求,那么INTPND就挑选出当前优先级最高的、正在发生的中断。

当产生irq后,要去分辨是哪个中断源,根据不同的中断源去中断服务程序isr中做不同的事情,那么如何得知当前产生的中断是哪一个外部中断源产生的呢?那么就可以访问这个INTPND寄存器。

可是我们要去手工去解析INTPND里面的位,才能知道是哪个中断源产生了中断请求。那么有没有什么比较快捷的方式自动帮我们解析INTPND呢,直接返回中断号给我们?

当然有啦,有一个INTOFFSET寄存器的值就是代表哪个中断请求产生了,如果INTOFFSET=0表示EINT0产生了中断请求,INTOFFSET=2表示EINT2产生了中断请求。具体见下图:

我们从上图看到ENIT4-7共用一个offset, EINT8-23也共用一个offset,那么要通过访问EINTPEND寄存器来区分它们。

中断控制器设置代码入下:

1
2
3
4
5
6
/* 初始化中断控制器 */
void interrupt_init(void) {
//1是屏蔽我们需要清零,外部中断0 外部中断2 外部中8_23里面还有外部中断11到19
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
//INTMOD默认是irq,可以不设置
}

3.3.3.3 中断总开关#

CPSR有I位,是irq的总开关,我们需要把CPSR寄存器 bit7给清零,这是中断的总开关,如果bit7设置为1,CPU无法响应任何中断。

1
2
3
/* 把bit7这一位清零 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0

3.3.3.4 中断服务程序#

到这里中断前的初始化工作知识点就已经讲完了,当然要提前准备好led初始化工作(就是将led对应的gpio配置成输出模式,这个不讲解)。

那么中断产生后,我们之前讲过,会跳转到0x18异常向量,执行跳转指令ldr pc, =_irq,和之前的swi异常,und异常框架一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.text
.global _start

_start:
b reset /* vector 0 : reset */

ldr pc, und_addr /* vector 4 : und(绝对跳转) */
ldr pc, swi_addr /* vector 8 : swi */
b halt /* vector 0x0c : prefetch aboot */
b halt /* vector 0x10 : data abort */
b halt /* vector 0x14 : reserved */
ldr pc, irq_addr /* vector 0x18 : irq */
b halt /* vector 0x1c : fiq */

und_addr:
.word do_und
swi_addr:
.word do_swi
irq_addr:
.word do_irq

reset:
/* 关闭看门狗 */
/*初始化时钟*/
/*初始化sdram,设置栈*/
/*代码重定位,清bss*/

/* 把bit7这一位清零(打开中断总开关) */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0

ldr pc, =main /* 绝对跳转, 跳到SDRAM */

halt:
b halt

1.我们在start.s中用汇编代码设置cpsr的I位,开启中断开关;

2.在main函数中初始化中断源key_eint_init,初始化中断控制器interrupt_init;

3.然后继续执行main主函数。

4.当中断产生,触发irq异常,进入0x18异常向量,执行do_irq。

do_irq实现如下(和do_und, do_swi类似):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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里 */

handle_irq_c函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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; /* 清中断 : 源头*/
}

/*INTOFFSET中哪一位被设置成1,就表示哪一个 中断源*/
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}

/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);/*清EINT0,EINT2,EINT5*/
INTPND = (1<<bit);
}

3.4 irq-定时器中断#

3.4.1 引入看门狗定时器#

s3c2440共有2种定时器:

1.Watchdog看门狗定时器
2.PWM脉冲可调制定时器

下面详细介绍2种定时器的原理,来了解定时器是如何产生定时器中断的。

3.4.1.1 WatchDog定时器原理#

Watchdog定时器的原理很简单,寄存器很少,框图如下:

  1. 定时器,定时器那肯定是需要用到时钟的,从框图中可以看到Watchdog定时器采用的时钟源是PCLK,从s3c2440时钟体系中也可以体现出来,接的是APB总线。
  2. 然后到达一个8 bit的分频器,可以通过配置WTCON[15:8]来设置分频器的预设值。
  3. 再设置WTCON[4:3]来设置除数因子来进一步分频。
    所以最终的Watchdog定时器的时钟周期t_watchdog = 1/[ PCLK / (Prescaler value + 1) / Division_factor ]
  4. 到达WTCNT:看门狗递减寄存器。WTCNT里的数据就开始在输入时钟频率下递减。WTCNT的值由WTDAT寄存器提供。
  5. WTDAT:WTDAT寄存器用于指定计数器的初始值,也就是它的超时时间,系统上电之后硬件自动的将0x8000的初始值载入到WTCNT里,在发生了第一次超时操作时,WTDAT的值才会载入到WTCNT寄存器

当WTCNT的值减到0时,就会触发看门狗定时器中断,进而产生复位。中断框图中可以看到可以设置WTCON[2]来设置是否产生中断信号,可以设置WTCON[0]来设置是否产生复位信号。

3.4.1.1.1 WTCON寄存器#

3.4.1.1.2 WTCNT、WTDAT寄存器#

3.4.2 WatchDog定时器中断示例#

3.4.2.1 定时器初始化#

在之前的章节中,我们在start.s启动代码中首先做的就是关闭看门狗,把WTCON[5]=0,也就是把Watchdog timer给disable。那么Watchdog Timer就不再工作了,这样做是为了防止在启动代码进行硬件初始化的时候出现超时,发出复位信号又去重启硬件,这样就陷入了不断重启过程中。因为s3c2440芯片默认WTCON[5]是1,也就是Watchdog Timer默认是处于使能状态。

s3c2440时钟体系中配置了PCLK=50M Hz, 那么让WTDAT取默认值0x8000,那么根据公式算出从开机到触发复位重启的时间:

t=WTDAT*( 1/[ PCLK / (Prescaler value + 1) / Division_factor ])

根据WTCON寄存器配置Prescaler value=255,配置Division_factor=128,这样最终定时器分得的频率更低,那么减数器递减的更慢,也就代表从开机到触发复位重启的时间:

T=0x8000 * (1/[50*10^6/(255+1)/128]) = 21474836.48us = 21s

之前的start.s中把看门狗已经关闭了,那么我们在跳转到main函数中调用wtd_timer_init函数实现如下:

1
2
3
4
void wtd_timer_init(void) {
WTCON |= (1<<0) | (1<<5);//使能定时器,开启reset复位
WTCON |= (3<<3) | (255<<8);
}

我们查看测试结果:
果然初始化wtd_timer_init后,过21s后板子重启了,说明我们watchdog定时器功能已经OK了。

现在修改代码如下:

1
2
3
4
5
void wtd_timer_init2(void) {
WTCON |= (1<<0) | (1<<2);//使能定时器,开启watchdog定时器中断
WTCON |= (3<<3) | (255<<8);
WTDAT = 0x4000;
}

我们看到我们现在定时器的初值被修改成了0x4000, 相对于默认值少了一半,那么触发wtd_timer中断的时间应该减半,也就是约等于10s。

3.4.2.2 定时器中断服务程序#

那么需要写一个wtd_timer的中断服务程序,同样需要先在do_irq中去保护现场、调用handle_irq_c、恢复现场。查看INTOFFSET寄存器:

得知:
handle_irq_c代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}
else if(bit == 9)
{
这里还需区分子中断源
}
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

查看芯片手册查找“INT_WDT_AC97”如下图:

从上图可以看到SRCPND和SUBSRCPND的映射关系。
SUBSRCPND寄存器如下图:

我们可以读取SUBSRCPND来区分到底是哪一个子中断源产生了中断,当SUBSRCPND中哪一位被置1,表示对应的中断源发生了中断。

前面做完wtd_timer_init,还要进行中断控制器的初始化,查看INTMSK寄存器如下图:

查看INTSUBMSK寄存器如下图:

在interrupt_init中添加:

1
2
INTMSK &= ~(1<<9);//不屏蔽INT_WDT_AC97
INTSUBMSK &= ~(1<<14);//不屏蔽INT_AC97

修改handle_irq_c:

1
2
3
4
5
6
7
8
9
...
else if (bit == 9)//INTOFFSET==9
{
if (SUBSRCPND & 1<<14)
{
printf("watchdog timer interrupt occured.\n");
}
}
...

3.4.3 PWM脉冲宽度调制定时器#

PWM(Pulse Width Modulation),字面上是脉冲可调制的意思,就是可以调节占空比。

s3c2440有5个定时器,其中定时器0、1、2和3具有脉宽调制(PWM)功能。定时器4是一个无输出引脚的内部定时器。

先认识下s3c2440的pwm timer的框架:

1.时钟源为PCLK
2.pclk经过8 bit的预分频系数(Prescaler),和4 bit的时钟除数因子(clock divider),进行分频
3.经过MUX选择器选择用哪个定时器(5选1)
4.设置TCMPB0和TCNTB0和TCONn寄存器

3.4.1.1 pwm定时器原理#

pwm定时器的逻辑控制单元结构如下:

1
2
3
4
5
1 TCMPBn和TCNTBn寄存器中的值分别加载到TCMPn和TCNTn寄存器
2 每来一个clk(时钟)这个TCNTn减去1
3 当TCNTn == TCMPn时,可以产生中断,pwm输出引脚反转
4 TCNTn继续减1,当TCNTn == 0时,又产生一次中断,pwm引脚再次反转
5 重复1-4过程

设置TCNTBn寄存器来设置加载初值,设置后TCNTn中的值就会按照时钟周期递减。
设置TCMPBn寄存器来设置占空比,从而控制高低电平持续时间的比例。

3.4.3.2 pwm定时器编程实现#

要开始一个PWM定时器功能的步骤如下:(假设使用的是timer0)

3.4.3.2.1 初始化pwm定时器#

定义一个pwm_timer_init()函数。

  1. 设置时钟:

    分别设置定时器0的预分频器值(prescaler)和时钟分频值(clock divider),从而控制TCNT0减数器的频率。

根据公式:

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

PCLK是50M,设置prescaler value=99, divider value=16,所以pwm Timer clk= 50000000/(99+1)/16 = 31250 Hz

1
2
3
TCFG0 = 99; 
TCFG1 &= ~0xf;
TCFG1 |= 3;
  1. 设置初值:
1
2
3
/* 设置比较缓存寄存器TCMPB0和计数缓存寄存器TCNTB0的初始值*/
TCNTB0 = 31250 << 1; /* 2s中断一次 */
TCMPB0 = 31250 >> 1; /* 设置占空比*/
  1. 开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位):

1
TCON |= (1<<1); //开始需要手工更新,这样才能将TCNTB0&TCMPB0同步到TCNT0&TCMP0
  1. 开启定时器0的自动加载:
1
2
TCON &= ~(1<<1); //开启自动加载要先清除手动更新
TCON |= (1<<3);
  1. 启动定时器0(设置TCON的第0位);

1
TCON |= (1<<0);
  1. 初始化中断控制器:


1
2
3
4
5
interrupt_init(){
...
INTMSK &= ~(1<<10); /* enable timer0 int */
...
}

做完这些初始化工作,就可以产生定时器中断了,同样我们需要在handle_irq_c函数中区分中断源:

3.4.3.2.2 pwm定时器中断服务程序#

我们可以通过查看TCNTO0寄存器来查看当前TCNT的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,bit==5还需细分eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND(eint11,2 eint11, eint11) */
}

else if(bit == 9) //INT_WDT_AC97
{
...
}

else if(bit == 10) //timer0
{
printf("timer0 interrupt occured.\n");
print_hex(TCNTO0);
}

/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

3.5 irq的优化改进#

我们对比irq外部中断, irq定时器中断,发现每增加一个中断源,又要去修改中断控制器的初始化interrupt_init()和handle_irq_c(),要在handle_irq_c()中去添加分支去执行不同的中断服务。

那么我们现在不去改变interrupt文件,在timer.c、key_eint.c中去注册自己的中断服务程序即可,这里我们使用函数指针数组,建立一个中断号和中断服务程序的映射关系。这样就可以根据中断号来执行对应的中断服务程序,即在handle_irq_c()中去回调不同类型的中断源注册下来的函数即可。

1
2
3
4
/* 定义函数指针数组 */
#define IRQ_NUM 32
typedef void(*irq_func)(int);
irq_func irq_array[IRQ_NUM];

然后实现一个register_irq(…)如下:

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

handle_irq_c()修改实现如下:

1
2
3
4
5
6
7
8
9
10
11
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;

irq_array[bit](bit); //根据中断号回调不同的中断处理函数

/* 清中断 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}

这样子我们的irq中断就被统一管理了起来,只要在其他各中断模块初始化的时候调用register_irq(…)注册即可。