所谓人在江湖漂,哪有不挨刀,就算再牛逼的程序员也会写出有bug的程序。程序bug不可怕,最怕的是引发错误导致系统崩溃,这个在下位机编程是非常常见的,比如内存泄漏导致对不可读写地址进行读写操作导致的Hard
Fault,违反MPU导致的MemManage
Fault,这些错误都会让我们整个系统崩溃。相对而言,上位机程序bug一般就只会让有bug的程序崩溃,很少会让整个系统蓝屏或是opps。这是因为无论是windows或是linux对于用户进程有很好的隔离特性,一旦发现某些进程有危险的倾向,就将其停止或是杀死,来保护整个系统的稳定。虽然比起windows和linux,MQX的任务异常机制要简单多了,但是相比其他的实时性系统,MQX能够做到及时停止有问题的任务,并提供了问题任务详细信息,方便我们的调试和改进。
举一个实际的例子,我有这么一个任务
-
void hello_task
-
(
-
uint_32 initial_data
-
)
-
{
-
int *a;
-
a=(int*)0xffffffff;
-
*a=0;
-
printf("Hello World\r\n");
-
_task_block();
-
}
可以看出这个任务对0xFFFFFFFF的地址进行了写操作,这个操作明显是不允许的。利用这个问题任务我们可以看看MQX的任务异常的处理机制。
单步调试程序,程序很快就进入了_int_kernel_isr。这是因为对不可读写地址的写操作是Hard
Fault错误,在中断向量表(本例中断向量表在rom中),Hard Fault的中断向量号是3,其中断服务程序的入口就是_int_kernel_isr。
-
(vector_entry)__BOOT_STACK_ADDRESS,
-
BOOT_START, /* 0x01 0x00000004 - ivINT_Initial_Program_Counter */
-
DEFAULT_VECTOR, /* 0x02 0x00000008 - ivINT_NMI */
-
DEFAULT_VECTOR, /* 0x03 0x0000000C - ivINT_Hard_Fault */
-
DEFAULT_VECTOR, /* 0x04 0x00000010 - ivINT_Mem_Manage_Fault */
-
DEFAULT_VECTOR, /* 0x05 0x00000014 - ivINT_Bus_Fault */
-
DEFAULT_VECTOR, /* 0x06 0x00000018 - ivINT_Usage_Fault
-
-
#define DEFAULT_VECTOR _int_kernel_isr
其实_int_kernel_isr并不是专门处理异常错误的中断服务程序,在MQX中,除了SVC和PENDSV这两个异常不经过_int_kernel_isr处理外,其他的所有异常和中断都是从_int_kernel_isr这个通用入口来处理的。
-
ASM_LABEL(_int_kernel_isr)
-
cpsid.n i
-
push {lr}
-
-
GET_KERNEL_DATA r3 /* get the kernel data address */
-
-
/* increment in interrupt counter */
-
ldrh r1, [r3, #KD_IN_ISR]
-
add r1, r1, #1
-
strh r1, [r3, #KD_IN_ISR]
-
-
/* create interrupt content */
-
ldr r0, =0 /* error code (set 0) */
-
push {r0} /* store in stack */
-
mrs r2, BASEPRI /* actual priority */
-
mrs r1, IPSR /* exception number */
-
ldr r0, [r3, #KD_INTERRUPT_CONTEXT_PTR] /* previous interrupt content */
-
push {r0-r2} /* store in stack */
-
-
mrs r0, MSP /* get address of interrupt content */
-
str r0, [r3, #KD_INTERRUPT_CONTEXT_PTR] /* store address of actual interrupt content in kernel data */
-
-
mov r0, r1
关于这部分,在之前的博文“中断机制(下)”已经介绍过了,本质是为了记录中断嵌套的相关信息,不明白的同学可以回过去看看。在本例中,我们是从任务进入这里的,所以previous
interrupt
content里是空的,这段代码所作的不过是将本次中断的一些关键变量存在这个中断上下文里,而最关键的是我们知道了本次中断的向量号,这个是从IPSR中读出来的。
-
/* check if isr is in table range */
-
ldr r2, [r3, #KD_LAST_USER_ISR_VECTOR]
-
-
/* cbz r2, _isr_run_default *//* isr not used (cbz not working in CW) */
-
cbnz r2, _isr_skip_run_default1 /* isr not used (this is CW workaround) */
-
b _isr_run_default
-
ASM_LABEL(_isr_skip_run_default1)
-
-
cmp r0, r2
-
bhi _isr_run_default
-
-
ldr r2, [r3, #KD_FIRST_USER_ISR_VECTOR]
-
subs r1, r0, r2 /* r1 = actual exception number in table */
-
blt _isr_run_default
接下来判断我们获取的中断向量号是否是我们之前注册过的MQX要处理的中断。KD_FIRST_USER_ISR_VECTOR和KD_LAST_USER_ISR_VECTOR就是我们之前在通过_psp_int_init(BSP_FIRST_INTERRUPT_VECTOR_USED,
BSP_LAST_INTERRUPT_VECTOR_USED)来注册的中断区间,具体来说是从0~250。我们的Hard Fault自然是满足条件的。
-
ASM_LABEL(_int_kernel_isr_vect_ok)
-
ldr r2, [r3, #KD_INTERRUPT_TABLE_PTR] /* get the interrupt table pointer */
-
lsr r1, r1, #MQX_SPARSE_ISR_SHIFT
-
lsl r1, r1, #2
-
-
ldr r1, [r2, r1] /* get address of first isr in linked list */
-
-
/* cbz r1, _isr_run_default *//* isr not used (cbz not working in CW) */
-
cbnz r1, _isr_skip_run_default2 /* isr not used (this is CW workaround) */
-
b _isr_run_default
-
ASM_LABEL(_isr_skip_run_default2)
-
-
/* r1 - address of first isr in linked list */
-
ASM_LABEL(_isr_search)
-
ldr r2, [r1, #HASH_ISR_NUM] /* get isr num */
-
cbz r2, _isr_search_fail
-
-
cmp r2, r0 /* compare isr number in record with actual isr number */
-
beq _isr_search_suc
-
-
ldr r1, [r1, #HASH_ISR_NEXT] /* next vector in list */
-
tst r1, r1
-
bne _isr_search
接下来就是从KD_INTERRUPT_TABLE_PTR里面找到对应中断的服务函数,这个就和_int_install_isr有关了。要想使用MQX的中断(SVC和PENDSV除外),必须先通过_int_install_isr注册中断服务函数。这么多个注册的中断服务函数构成了一个中断向量表,不同之前我们所说的硬件的中断向量表,这个中断向量表只是MQX构建出调用用户编写的中断服务程序接口,虽然执行效率不能和硬件的中断向量表相比,但可以是随时修改,适用于MQX这样多任务的系统(我们的linux也是这样的)。总而言之,上述的汇编代码就是从这个MQX的中断向量表中找出对应的中断服务程序,一旦该中断没有注册中断服务程序就使用默认的服务程序。
关于这个MQX中断向量表,有两种组织形式。一种是从BSP_FIRST_INTERRUPT_VECTOR_USED到BSP_LAST_INTERRUPT_VECTOR_USED,也就是从0~250,分配一个连续的表,每次调_int_install_isr就往对于的表项里塞东西。这种表的优点是简单,一次分配,后面一直使用,一般来说系统只会使用少量的中断,分配250个表项实在是太浪费了。另一种机制是采用了类似于hash表的组织形式,
每8个中断一个链表头,注册一个中断就往对于链表头里面塞链表。这样做的好处自然是节省空间,不过花费的时间要多点。
由于我们没有注册关于Hard Fault的中断服务程序,于是程序就采用了默认的中断服务程序
-
ASM_LABEL(_isr_run_default)
-
/* r0 - interrupt number */
-
-
ldr r2, [r3, #KD_DEFAULT_ISR]
-
-
b _isr_execute
-
-
ASM_LABEL(_isr_execute)
-
/* r0 = first parameter in C func */
-
/* r2 contain interrupt function address */
-
-
cpsie.n i
-
push {r3}
-
-
blx r2
默认的中断服务程序是KD_DEFAULT_ISR,对应的C代码是
-
_mqx_uint _int_init
-
{
-
....
-
kernel_data->DEFAULT_ISR = _int_default_isr;
-
....
现在进入_int_default_isr
-
void _int_default_isr
-
(
-
pointer vector_number
-
)
-
{ /* Body */
-
KERNEL_DATA_STRUCT_PTR kernel_data;
-
TD_STRUCT_PTR td_ptr;
-
-
_GET_KERNEL_DATA(kernel_data);
-
-
td_ptr = kernel_data->ACTIVE_PTR;
-
_KLOGE5(KLOG_int_default_isr, td_ptr, vector_number,
-
&vector_number, vector_number);
-
-
_int_disable();
-
if (td_ptr->STATE != UNHANDLED_INT_BLOCKED)
-
{
-
td_ptr->STATE = UNHANDLED_INT_BLOCKED;
-
td_ptr->INFO = (_mqx_uint) vector_number;
-
_task_set_error_td_internal(td_ptr, MQX_UNHANDLED_INTERRUPT);
-
_QUEUE_UNLINK(td_ptr);
-
} /* Endif */
-
_int_enable();
-
-
} /* Endbod
可以看出_int_default_isr做了两件关键的事情,第一件事将任务的当前态改为UNHANDLED_INT_BLOCKED,再将这个任务剔除优先级链表,这样这个任务就不能再运行了。第二件事是将相关中断信息存入任务里,由于任务只是停止了,MQX可以从任务的尸体上找到究竟是什么原因导致它的死亡。
_int_default_isr结束后,继续汇编码
-
pop {r0-r2}
-
str r0, [r3, #KD_INTERRUPT_CONTEXT_PTR] /* update pointer to interrupt content */
-
-
pop {r0} /* error code */
-
-
/* decrement interrupt counter */
-
ldrh r1, [r3, #KD_IN_ISR]
-
subs r1, r1, #1
-
strh r1, [r3, #KD_IN_ISR]
-
-
/* check for reschedule */
-
/* check preemtion */
-
ldr r2, [r3, #KD_ACTIVE_PTR] /* TD pointer */
-
ldr r0, [r2, #TD_FLAGS]
-
tst r0, #TASK_PREEMPTION_DISABLED
-
bne _isr_return_end
-
-
cbnz r1, _isr_return_end /* waiting another isr, do not reschedulle */
-
-
/* if a different TD at head of current readyq, then we need to run the scheduler */
-
/* check for reschedule */
-
ldr r1, [r3, #KD_CURRENT_READY_Q]
-
ldr r1, [r1]
-
cmp r1, r2
-
#ifdef __CODEWARRIOR__
-
it ne
-
bl _set_pend_sv
-
#else
-
it ne
-
blne _set_pend_sv
-
#endif
-
-
ASM_LABEL(_isr_return_end)
-
cpsie.n i
-
pop {pc}
到了这里就把之前做的事情逆操作了一便,不过要注意由于我们之前在_int_default_isr把当前任务剔除了优先级链表,导致KD_INTERRUPT_CONTEXT_PTR里的第一个任务不再是KD_ACTIVE_PTR,所以MQX启动了PENDSV来寻找一个优先级最高的任务来调度,这样我们之前的问题任务经过异常之后,就不会再返回了,留下了一具保存完好的尸体,供后人调查死因。
未注册的中断服务程序也可以设为_int_unexpected_isr,但是MQX源码虽然留有相应的接口,但实际没有任何地方调用它。它与_int_default_isr在关于将问题任务踢出运行态的作用上是一样的,不过它侧重将问题任务的出错时的寄存器状态通过printf打印出来(MQX里把这部分注释掉了)。这个和rt
thread挺像的,不过可惜rt thread就一直死循环卡在这里了。
到了这里MQX的任务异常处理就说的差不多了,可以看出MQX具有一定的纠错能力,让通过停止那些有问题的任务来保证系统的稳定运行,这也是MQX不同于其他的实时性嵌入式系统的一个优点。总结全文,聪明的同学发现与其说本文是介绍任务异常,还不如说是介绍了MQX的中断服务处理机制。由于之前“中断机制”那几篇文章没有讨论这块,就以此篇作为之前的补充。
阅读(2318) | 评论(0) | 转发(0) |