原创 设计自己的嵌入式操作系统内核之五 ----- 中断管理模块机制

2011-8-1 19:40 3671 6 6 分类: MCU/ 嵌入式

1、概述
嵌入式操作系统的一个基本功能就是对硬件资源进行抽像,向应用提供便捷易用的接口。这些硬件资源中,重要的一种是中断机制。中断在嵌入式系统中用于响应内/外部事件,由中断服务子程序(ISR)进行处理。不同的处理器提供了不同类型的中断,包括硬件中断/软中断,及一些异常。

嵌入式CPU一般包含了多类类中断,每类中断对应于一个或多个中断源,即可能有多个中断由同一中断服务子程序进行处理。在flash等待存储区中存在中断向量表,当中断发生时,程序计数器从该表中取指后跳转至相应的服务程序中进行事件的处理。对于应用而言,只关心如何处理外部事件,而并不需要了解处理器是如何处理中断的。

eos的中断管理模块的实现,目标是希望能够简化应用对中断系统的管理,对一些与硬件相关的细节进行抽像,以实现对应用的透明性。至少与ucos的中断管理机制相比,应用起来要简单些。

2、eos中断管理模块设计概要
eos的中断管理模块实现比较简单,只是在原有的硬件中断微量表上,再添加了一级由操作系统维护的向量表,如下图:
? ?点击看大图

一般而言,硬件向量表可位于ROM或RAM内。位于ROM内的向量表是难以修改的,这就意味着,其对应的中断服务子程序一般只能在编译时确定,无法再进行动态的配置,而即使不需要动态配置,实际在编写中断服务子程序时应用必须了解一定的中断相关细节。针对此,eos在原向量表中再添加一级向量表,表项元素为指向实际处理程序的指针。当发生中断时,处理器根据硬件向量表转至上图中统一的中断处理程序irq_handler中,该程序代码完成处理器上下文的保存,执行中断处理子程序,退出中断时清除中断标志,进行中断调度并进行必要的处理器上下文的恢复。其核心就在于irq_handler和位于RAM的二级中断向量表。应用只需要调用中断管理模块提供的接口函数,就可以向中断系统注册、注销中断处理程序,而与中断处理硬件相关的中断响应等待细节用户不需要关心。

3、中断管理模块的具体实现
1)、二级中断向量表结构 ?
? ?
点击看大图

在irq.c中定义了向量表的变量:static irq_tbl_t irq_tbl[ OS_IRQ_NR ]; ?
可以看出,irq_tbl实质为一指针数据,指针指向无参、无返回值的函数。该函数即为实际处理中断对应的外部事件的函数。

另定义了:uint8 irq_cnt; /* 中断嵌套计数 */
用于计数中断发生的嵌套数。

2)、统一的中断处理程序irq_handler()
irq_handler()的任务在于为所有的中断处理提供统一的入口函数。其大致的处理流程为:

1、保存处理器上下文
2、irq_cnt++
3、检测是否irq_cnt == 1
是,没有发生中断嵌套,保存SP至OSTaskCur->stk_top
4、取发生中断的向量号
5、由向量号从二级向量表中取入口函数首址,再调用该函数
6、响应中断,清中断标志
7、如果--irq_cnt == 0,处理最外层中断
a)取最高优先级任务
b)进行中断级任务切换,退出中断处理程序
8、恢复处理器上下文
9、中断返回

实际上,这部分由于与处理器密切相关,仅用C来写是难以完成的。在Atmega32运行的代码上,就用了汇编和C两部分。

中断级任务切换原理,同uc/os,请参考uc/os实现。

三)、中断管理应用接口
提供了若干管理模块的接口函数:
void irq_init( void ); /* 中断系统初始化 */
isr_t irq_attach( uint8 vect, isr_t irq ); /* 安装中断处理函数 */
isr_t irq_deattach( uint8 vect ); /* 销毁中断处理函数 */
void irq_enable_all( void ); /* 开全局中断 */
void irq_disable_all( void ); /* 关全局中断 */
void irq_mask( uint8 vect ); /* 屏蔽指定的中断 */
void irq_unmask( uint8 vect ); /* 开启指定的中断 */

例如对于定时器的中断,任务只需要知道该中断对应的向量后,再定义相应的管理函数,调用irq_attach()进行关联。当中断发生时,该函数自动调用,不需要考虑中断退出时清中断标志、中断级任务调度这些问题了。

以上代码多在irq.c/ irq.h中,另有一部与处理相关的移植性代码(针对Atmega32的),放在arch.c/ arch.h中。

4)、中断管理模块下的系统时钟中断处理
1、在eos内部,维护了一内部时钟,该时钟在硬件定时器的驱动下工作。当定时器每隔约数10ms发生一次中断,该时钟计数器自动增1:
uint16 kernel_ticks; /* 系统tick计数 */
同时有两个接口函数用于设定和获取该计数:
void time_tick_set( uint16 tick ); /* 设置系统tick */
uint16 time_tick_get( void ); /* 获取系统tick */

2、同时,eos还维护了一延时队列:
slist_t OSDelayQ; /* 延时队列 */
该队列为一单级队列,需要延时的任务被放入该队列中,当延时期满时,再从队列中取出。
任务需要延时时,调用task_delay()将自己插入延时队列,再进行任务调度。
当延时未到却需要唤醒时,调用task_delay_wakeup()将自己从延时队列移除。

3、而时钟中断处理程序为time_tick_handler(),该函数遍历OSDelayQ,将任务的延时计数.delay项减一,当其值为0时从延时队列移除,并插入就绪队列。同时,其还实现了简单的时间片调度。其操作流程如下:

遍历OSDelayQ中各项
如果 --(task->delay)==0,即延时期到
如果 任务在等待事件
将任务从等待的事件队列中移除
将任务从延时队列移除并插入就绪队列

如果当前任务时间片用完 ( --OSTaskCur->slice == 0 )
将当前任务从就绪队列头移至就绪队列末

该处理流程的前半部分与uc/os的时钟中断处理类似。后半部分实现了简单的时间片调度。可以看出,时间片调度的实现在此非常简单,仅在任务控制块task_struct结构中维护了一减1计数器,当计数器减至0时,切换至同优先级的下一任务,再重载计数。
在当前的系统中,所有的任务时间片一致,都为 OS_TASK_SLICE 。

当退出time_tick_handler()后,返回至irq_handler(),再进行中断级任务切换。
在系统初始化时,调用了irq_attach( OS_TICK_VECT, time_tick_handler );完成定时器中断处理程序的关联。

5、简单的演示 
提供了一个简单的演示实例app_time_slice.c。

6、总结与思考
eos的中断管理模块对每一类中断只提供一个中断处理子程序,有的内核可配置多个。我想,单个处理程序应该是足够的。所有与中断相关的处理均在该程序中。在有的内核中,将中断处理分为了前半部分、后部部分。前半部分包含的代码量少,执行快速;后半部分则处理一些耗时较长的工作。这里,我没有实现,主要原因为自己还不知道如何设计。

在启用了时间片调度的情况下,如果某个任务跑飞,但是跑飞后不修改系统中的其它变量。则可以看到,除了该任务在跑飞外,其它任务仍正常执行。在proteus仿真时,我曾经看到过这种效果。究其原因,我想主要在于在调用时间片机制后,即使某个任务跑习,但时钟中断发生后,中断服务程序会强制将CPU的执行权转交给其它任务。只要跑飞的任务没有修改某些关键些的数据。这一过程能够顺利进行,进行任务切换。而像ucos那样的系统,一旦任务跑飞,整个系统立即崩溃。

可以考虑为中断处理单独设置堆栈。当进入中断处理时,立即切换至该堆栈,显然能够有效的减少任务的堆栈使用数量。

对于延时队列,有相关的书提到了一种差分时间队列。即在该队列中的任意任务的延时值是在队列中在该任务之前的所有任务延时值之和再加上task_struct结构保存的值。这里为了减少代码量,降低复杂性,没有实现。

中断管理模块中的各函数的实现,强烈依赖于处理器的细节。因为采用了统一的中断处理函数,而在处理函数中要调用相应的中断处理子程序,必须知道发生的中断源。而在Atmega32中,虽然可以通过判定相应中断标志寄存器的标志位,但是不同中断源的标志位常位于不同的寄存器,如果使用查询的方式,显然效率非常低。在当前的系统中,定义了一个变量:extern uint8 vect_num; (arch.c/arch.h)
在发生中断时,首先将中断向量号保存至vect_num中,在irq_handler()中再从中取值,调用相应的处理函数。
对于定时器0的中断处理程序,其汇编部分代码如下:
点击看大图


七、资料与源码
源码irq.c / irq.h / arch.c / arch.h/ core.c / core.h

pdf


文章评论0条评论)

登录后参与讨论
我要评论
0
6
关闭 站长推荐上一条 /3 下一条