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 | mov R0, R8 |
在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 | .globl _start |
异常向量表对应的地址如下图:
2 异常处理流程#
CPU是如何进入到中断模式,执行中断服务程序的?
2.1 中断前#
下图是中断未触发前的程序执行过程:
2.1.1 初始化中断#
1,设置中断源(使中断能够产生,让CPU知道是哪个中断)
2,设置中断控制器(设置中断屏蔽,中断优先级)
3,设置中断总开关CPSR (使能中断)
2.2 中断产生后#
举个栗子:按键按下,产生按键irq。
2.2.1 硬件上的处理流程#
cpu强制跳转到异常向量表上对应的_irq异常向量(0x18)去读取指令(这个是CPU强制执行的,不需要我们去控制)。
具体的进入中断向量和中断返回流程见下图:
1 | 进入: |
进入异常和返回异常时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 | und_string: |
然后用ldr r1, =und_string
,这样r1中就保存了und_string的地址。
这样调用我们的c函数就可以把und_string传入进去。
3.1.2 und异常程序示例#
1 | .text |
在未定义指令异常前后加上打印print1, print2,如果出现未定义指令异常后,就会跳到0x4的地方去读取指令,print2也就没法执行。
当跳转到0x4的中断向量后,发现此处是一条跳转指令bl do_und
, 我们再到未定义指令异常的服务程序do_und中打印出und_string这个字符串的内容。
现在开始写指令异常的服务程序do_und,实现如下:
1 | do_und: |
下面来分析一下这个未定义指令异常服务程序:
进入未定义指令异常服务do_und之前硬件自动完成的事情如下:
- lr_und保存有被中断模式中的下一条即将执行的指令的地址
- SPSR_und保存有被中断模式的CPSR
- CPSR中的M4-M0被设置为11011, 进入到und模式
- 跳到0x4的地方执行程序 (bl do_und)
进入指令异常服务程序do_und后,我们需要保存现场,处理und异常,恢复现场,注意:由于发生了cpu模式切换,如果要用到栈,那么先要设置对应模式的栈。由于栈的地址是向下生长的,这里我就用sdram的末位地址作为栈指针,把sp_und=0x34000000。
在und异常服务程序中有可能会用到栈, 所以先保存现场,通过
stmdb sp!, {r0-r12, lr}
语句把栈中的值备份到r0-r12和lr,然后恢复现场的时候通过ldmia sp!, {r0-r12, pc}^
,详见上面的注释。我们看到保存现场后,我们把cpsr的值放到r0, 把und_string放到r1, 然后用bl printException调用c函数,这样我们的c函数printException就能收到汇编传过来的参数,一个是cpsr模式(r0),一个是und_string汇编传过来的字符串(r1)。我们用C函数实现printException:
1 | void printException(unsigned int cpsr, char *str) { |
完整的代码如下:
1 | .text |
测试结果如下:
打印出print1中的字符串‘abc’后,紧接着打印printException函数中的结果,cpsr=0x600000db,那么对应的M[4:0]=11011, 对应下图为und模式。然后从und异常返回,恢复原来的模式继续执行。
3.1.3 示例改进#
3.1.3.1 指令4字节对齐#
我们将上面的代码的und_string字符串修改一下:
1 | ... |
编译烧录再次运行,发现没有任何打印输出,这是为什么呢?我明明只是把und_string字符串改了一下呀。
查看反汇编:
我们发现reset的地址是0x30000032,竟然不是4字节对齐的,我们知道arm指令集是以4字节为基本单位的,那么这里没有对齐,肯定无法解析指令。那么我们手工改进代码如下:
1 | ... |
1 | reset: |
我们再来看看反汇编,发现reset的地址是30000040,是以4字节对齐的,再次烧录运行,发现能够正常输出print1, 能够进入未定义指令异常。
3.1.3.2 绝对跳转进入异常向量#
如果我们程序非常大,中断向量入口代码的地址可能会大于sram的容量4k,比如do_und和do_swi,那么这个时候就需要用绝对跳转。
1 | .text |
将上面的相对跳转换成如下代码:
1 | .text |
这样我们的do_und, do_swi就可放在4k之外的地方, 放到sdram。
3.1.3.3 重定位后跳转sdram上执行#
我们现在不断增加的程序代码量,那么有可能在 ldr pc, =main
这条指令执行之前程序就已经超过4k。那么我们当从nand启动的时候,还没执行到ldr pc, =main这句来,就无法取指令执行了。nor同理超过2M也就无法取指令执行了。 所以我们干脆重定位完代码后就直接跳转到sdram上去执行,代码简要概述如下:
1 | ... |
我们再来分析下整个程序执行过程:
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 | 1. 发生异常或中断(被动的) |
3.2.1 进入软中断swi#
s3c2440 一上电会跳到0地址(reset复位)执行代码,此时CPU处于svc模式,2440异常向量表如下图所示:
为了验证usr模式能够主动的通过swi软中断指令来进入svc模式, 我们先将模式切换到usr模式,那么这个时候就不能访问硬件了,也不能直接修改cpsr直接进入其他模式。
从上图我们设置CPSR让M4-M0处在10000,这样就进入了usr模式。修改start.s如下:
1 | .global _start |
那么当执行到swi 0x123,就会触发SWI异常, 进入0x8的向量去执行,调用do_swi,我们参考do_und实现我们的软中断服务程序do_swi。
1 | do_swi: |
完整代码如下:
1 | .global _start |
do_swi中调用printException,打印出了软中断异常的字符串和CPSR对应的svc模式。
3.2.1.1 打印出swi软中断号#
我们要读出swi 0x123指令,我们知道当执行完swi 0x123指令以后,会发生swi异常,那么lr_svc = PC + offset。从下图看出offset是4:
修改中断服务函数如下:
1 | do_swi: |
我们要把lr拿出来保存,因为bl printException会破坏lr,那么把lr保存在哪个个寄存器比较好呢?
我们知道当调用bl printException
可能会修改某些寄存器,但是又会恢复这些寄存器,那么得知道它会保护哪些些寄存器。
来看下ATPCS规则:
在子程序中,使用R4~R11来保存局部变量,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。所以对于 r4 ~ r11在C函数里会保存这几个寄存器,执行完C函数再把它释放掉并且恢复原来的值。我们把lr 保存在r4寄存器里,r4寄存器不会被C语言破坏。
1 | mov r4, lr |
当执行完swi 0x123
指令后,会发生swi异常,swi异常模式里的lr寄存器会保存下一条指令的地址(即’ldr pc, =main’),我们把lr寄存器的地址减去4就是swi 0x123
这条指令的地址。
把r4的寄存器赋给r0让后打印我们得写出打印函数:
1 | mov r0, r4 |
在uart.c添加printSWIVal打印函数:
1 | void printSWIVal(unsigned int *pSWI) { |
3.3 irq-外部中断#
3.3.1 引入外部中断#
我们想实现一个按键点灯程序,我们知道有以下两种方案:
1 | 1.轮询方案:轮询检测按键的电平状态,当检测到被按下后,对应的gpio会拉低,点亮对应的led;(略) |
我们用按键作为外部中断源,我们把按键对应的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配置成中断模式。
- 配置GPF GPIO为中断引脚:
同理GPG的寄存器类似。
1 | GPFCON &= ~((3<<0) | (3<<4)); //先把eint0和eint2这两个引脚清零 |
- 设置中断触发方式:
当电平从高变低时,此时表示按键按下,当电平由低变高,表示松开按键。不妨设置中断方式为双边沿触发,按下按键,触发下降沿中断,中断服务程序就可以去点亮led,反之,松开触发上升沿中断,就可以去熄灭led。
1 | EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */ |
- 设置外部中断屏蔽寄存器EINTMASK:
从上图我们知道外部中断0-3是直接连接到中断控制器,而外部中断4-7、外部中断8-23还要经过EINTMASK,那么我们需要配置EINTMASK来打开中断的通道:
1 | EINTMASK &= ~((1<<11) | (1<<19)); //打开外部中断通道 |
- 外部中断挂起寄存器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 | /* 初始化中断控制器 */ |
3.3.3.3 中断总开关#
CPSR有I位,是irq的总开关,我们需要把CPSR寄存器 bit7给清零,这是中断的总开关,如果bit7设置为1,CPU无法响应任何中断。
1 | /* 把bit7这一位清零 */ |
3.3.3.4 中断服务程序#
到这里中断前的初始化工作知识点就已经讲完了,当然要提前准备好led初始化工作(就是将led对应的gpio配置成输出模式,这个不讲解)。
那么中断产生后,我们之前讲过,会跳转到0x18异常向量,执行跳转指令ldr pc, =_irq
,和之前的swi异常,und异常框架一样。
1 | .text |
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 | do_irq: |
handle_irq_c函数实现如下:
1 | void key_eint_irq(int irq) { |
3.4 irq-定时器中断#
3.4.1 引入看门狗定时器#
s3c2440共有2种定时器:
1.Watchdog看门狗定时器
2.PWM脉冲可调制定时器
下面详细介绍2种定时器的原理,来了解定时器是如何产生定时器中断的。
3.4.1.1 WatchDog定时器原理#
Watchdog定时器的原理很简单,寄存器很少,框图如下:
- 定时器,定时器那肯定是需要用到时钟的,从框图中可以看到Watchdog定时器采用的时钟源是PCLK,从s3c2440时钟体系中也可以体现出来,接的是APB总线。
- 然后到达一个8 bit的分频器,可以通过配置WTCON[15:8]来设置分频器的预设值。
- 再设置WTCON[4:3]来设置除数因子来进一步分频。
所以最终的Watchdog定时器的时钟周期t_watchdog = 1/[ PCLK / (Prescaler value + 1) / Division_factor ]
- 到达WTCNT:看门狗递减寄存器。WTCNT里的数据就开始在输入时钟频率下递减。WTCNT的值由WTDAT寄存器提供。
- 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 | void wtd_timer_init(void) { |
我们查看测试结果:
果然初始化wtd_timer_init后,过21s后板子重启了,说明我们watchdog定时器功能已经OK了。
现在修改代码如下:
1 | void wtd_timer_init2(void) { |
我们看到我们现在定时器的初值被修改成了0x4000, 相对于默认值少了一半,那么触发wtd_timer中断的时间应该减半,也就是约等于10s。
3.4.2.2 定时器中断服务程序#
那么需要写一个wtd_timer的中断服务程序,同样需要先在do_irq中去保护现场、调用handle_irq_c、恢复现场。查看INTOFFSET寄存器:
得知:
handle_irq_c代码修改如下:
1 | void handle_irq_c(void) |
查看芯片手册查找“INT_WDT_AC97”如下图:
从上图可以看到SRCPND和SUBSRCPND的映射关系。
SUBSRCPND寄存器如下图:
我们可以读取SUBSRCPND来区分到底是哪一个子中断源产生了中断,当SUBSRCPND中哪一位被置1,表示对应的中断源发生了中断。
前面做完wtd_timer_init,还要进行中断控制器的初始化,查看INTMSK寄存器如下图:
查看INTSUBMSK寄存器如下图:
在interrupt_init中添加:
1 | INTMSK &= ~(1<<9);//不屏蔽INT_WDT_AC97 |
修改handle_irq_c:
1 | ... |
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 | 1 TCMPBn和TCNTBn寄存器中的值分别加载到TCMPn和TCNTn寄存器 |
设置TCNTBn寄存器来设置加载初值,设置后TCNTn中的值就会按照时钟周期递减。
设置TCMPBn寄存器来设置占空比,从而控制高低电平持续时间的比例。
3.4.3.2 pwm定时器编程实现#
要开始一个PWM定时器功能的步骤如下:(假设使用的是timer0)
3.4.3.2.1 初始化pwm定时器#
定义一个pwm_timer_init()函数。
设置时钟:
分别设置定时器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 | TCFG0 = 99; |
- 设置初值:
1 | /* 设置比较缓存寄存器TCMPB0和计数缓存寄存器TCNTB0的初始值*/ |
开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位):
1 | TCON |= (1<<1); //开始需要手工更新,这样才能将TCNTB0&TCMPB0同步到TCNT0&TCMP0 |
- 开启定时器0的自动加载:
1 | TCON &= ~(1<<1); //开启自动加载要先清除手动更新 |
- 启动定时器0(设置TCON的第0位);
1 | TCON |= (1<<0); |
- 初始化中断控制器:
1 | interrupt_init(){ |
做完这些初始化工作,就可以产生定时器中断了,同样我们需要在handle_irq_c函数中区分中断源:
3.4.3.2.2 pwm定时器中断服务程序#
我们可以通过查看TCNTO0寄存器来查看当前TCNT的值。
1 | void handle_irq_c(void) |
3.5 irq的优化改进#
我们对比irq外部中断, irq定时器中断,发现每增加一个中断源,又要去修改中断控制器的初始化interrupt_init()和handle_irq_c(),要在handle_irq_c()中去添加分支去执行不同的中断服务。
那么我们现在不去改变interrupt文件,在timer.c、key_eint.c中去注册自己的中断服务程序即可,这里我们使用函数指针数组,建立一个中断号和中断服务程序的映射关系。这样就可以根据中断号来执行对应的中断服务程序,即在handle_irq_c()中去回调不同类型的中断源注册下来的函数即可。
1 | /* 定义函数指针数组 */ |
然后实现一个register_irq(…)如下:
1 | void register_irq (int irq, irq_func fp) |
handle_irq_c()修改实现如下:
1 | void handle_irq_c(void) |
这样子我们的irq中断就被统一管理了起来,只要在其他各中断模块初始化的时候调用register_irq(…)注册即可。