https://static.assets-stash.eet-china.com/album/old-resources/2009/5/6/f4cefaee-0744-4134-bbd9-d5a69680b3fe.rar" target="_blank">
一、设计背景
对于一些小型的嵌入式设备而言,往往采用单片机+前后台式系统设计。即前台程序(中断)负责处理外部事件,后台程序是由一死循环+一系列的函数调用构成。通常前台程序可以用于处理比较紧急,如实时性要求较强的事件,而后台程序中的各函数即作为多个任务运行。如图所示:
其中ISR2具有较高优先级,能中断ISR1,用于响应更紧急的外部事件。superLoop即为一死循环,在无中断时检查各任务是否可运行,进行任务调度。通常情况下是按FIFO的方式调度,也有采用状态机的。
采用这种方式控制,我认为有如下的缺点:
1、后台各任务实时性很差,执行的时间不确定;由于任务必须在其前面的任务运行完后才可运行,因为当事件发生到该任务响应(即任务级响应时间)不确定;
2、系统软件的编制复杂,特别是对于较大的系统而言。对于一些较小的任务,比如LCD 的显示,LCD显示比较慢,执行时间较长,后面的任务会因此而严重受到影响.
在圆梦小车的设计中,当然可以采用采用这种方式让小车跑起来。但如果要让小车实现比较复杂的功能,比较可靠的无线通信+智能避障+自动规则;显然这种软件结构有些力不从心,而且编程会相当复杂。特别是在一些需要保证一定的实时性场合。
这里采用的 MicroC/OS,多任务的操作系统.很多人都是比较熟悉的。在反复读过几次源码后,我觉得其实现还是比较容易理解的。虽然代码量少,但非常小巧、灵活。包含了基本的任务管理、任务间的同步与通信机制和简单的存储管理。直至在看过ucos源码后,我才对操作系统的原理有所了解。
有关ucos的具体情况,请见<< 嵌入式实时操作系统 uC/OS-II>>。对于如何在单片机上应用ucos构件常用的构件可参考《嵌入式系统构件》。
采用多任务操作系统的好处是能将系统划人为的划分为多个相互协作的任务,为软件实现提供了统一的平台,代码实现容易。一个复杂的功能能够通过分解由其它多个任务协作完成,在实时性上有所提高,且操作系统提供的一些机制能使得任务高效协作。整体上提高了系统的效率。当要使小车更加“智能化”时,采用操作系统平台能降低实现的复杂度。
二、设计目标
目标:将uc/os移植到ATMEGA单片机上。
说明:
1、ATMEGA系列包括了M8, M16, M32, ...., M128。其体系统结构上都是一样的。可以认为在将uc/os移植代码编写好后,就可以为其中任何一种编译后即可运行在相应单片机上,不再需要再做其它的修改。这里之所以用ATMEGA而不是ARM主要是觉得ATMEGA较简单,比较适合我现在的水平,而且目前我只有适合小车的ATMEGA的板子。
另外,Atmega上片内资源跑操作系统还是有点勉强。但这也无妨,这样倒是可以促使自己在写代码时综合考虑,以实现资源使用的最优化。
2、软件:UCOS Verison 2.52
AVR Studio 4.13, AVR-GCC 3.4.6,用于代码移植、编译、调试。
Proteus 7.12, VMLAB 用于软件仿真运行测试.
3、硬件: M16的系统板,实际只需要最小系统+若干LED即可。
三、设计概要
实际有关UC/OS的移植的指导,有<< 嵌入式实时操作系统 uC/OS-II>>有详细的说明,这里仅针对具体的单片机进行说明.
在移植之前,我想引用<ARM嵌入式系统基础教程>中的内容:
要移植一个操作系统到一个特定的CPU体系统结构上并不是一件容易的事情,它对移植者有以下的要求:
1、对目标体系结构要有很深入的了解;
2、对OS原理要有较深入的了解;
3、对所使用的编译器有较深入的了解;
4、对需要移植的操作系统要有相当的了解;
5、对具体使用的芯片也要有一定的了解;
我的理解是:第4点特别重要。曾经有网友问我ucos移植的问题,这些问题实际上都是因为没有完整的理解ucos实现机制而提出的。所以,如果没有理解这个系统的实现机制,那么对其中的移植(代码修改),很难理解为什么要这样做。
至于“了解”到这什么程序,这个很难评价!
Ucos体系结构图
移植的步骤如下:
1、创建 工程文件, 添加ucos源码文件;
2、修改具体的移植代码( OS_CPU.H, OS_CPU_A.S, OS_CPU_C.C)
3、编写应用代码;
4、进行多任务运行的仿真,包含软件、硬件;
四、具体实现
以下是在<< 嵌入式实时操作系统 uC/OS-II>>的指导下完成的移植过程:
说明:这里的ucos源码是从<< 嵌入式实时操作系统 uC/OS-II>>所附的光盘中直接拷贝,修改于为X86的移植源码.
1、创建工程文件:
在安装好AVR Studio 4.13, AVR-GCC 3.4.6后,打开AVR Studio创建GCC的工程文件,目标芯片选ATMEGA16; 添加ucos 的源码os_cpu.c, os_cpu_a.s, ucos_ii.c, 创建main.c文件;如下图所示。
2、修改移植代码:
Ucos的作者在设计时已经考虑到了可移植性,因而在移植时要改写的代码量极少。主要需要修改的文件有 前面ucos体系统结构图中所示的os_cpu.c, os_cpu_a.s, os_cpu.h.
a)、修改os_cpu.h文件。
Os_cpu.h主要包含了与处理器、编译器相关的常量、宏、类型定义:
1)、基本数据类型
UCOS中基本数据类型的别名声明。这里因为是用GCC进行编译,实际的类型说明是与GCC相关,具体细节请参考GCC手册。
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U; /* Unsigned 8 bit quantity */
typedef signed char INT8S; /* Signed 8 bit quantity */
typedef unsigned int INT16U; /* Unsigned 16 bit quantity */
typedef signed int INT16S; /* Signed 16 bit quantity */
typedef unsigned long INT32U; /* Unsigned 32 bit quantity */
typedef signed long INT32S; /* Signed 32 bit quantity */
typedef float FP32; /* floating point( not used ) */
typedef double FP64; /* Double precision floating point */
2)、堆栈与OS_CPU_SR
Atmega的堆栈操作单位以字节为单位,所以OS_STK为unsigned char 别名.
OS_CPU_SR类型用于定义保存SREG内容的变量。SREG为8位,因此OS_CPU_SR也位8位相关类型.
typedef unsigned char OS_STK; /* Each stack entry is 8-bit wide */
typedef unsigned char OS_CPU_SR; /* (SREG = 8 bits) */
3)、临界区进出宏定义:
本次移植只使用了第1、3种方式,第2种未用。实际使用时,只使用第3种。有关OS_CPU_SR_SAVE(), OS_CPU_SR_RESTORE()的实现见下文.
第3种方式,指通过ATMEGA上进行保存SREG, 开/关中断实现临界区保护。这种方式也是推荐的方式。
#define OS_CRITICAL_METHOD 1
#if OS_CRITICAL_METHOD == 1
#define OS_ENTER_CRITICAL() asm volatile("cli"); /* Disable interrupts */
#define OS_EXIT_CRITICAL() asm volatile("sei"); /* Enable interrupts*/
#endif
#if OS_CRITICAL_METHOD == 2
#define OS_ENTER_CRITICAL() /* Disable interrupts */
#define OS_EXIT_CRITICAL() /* Enable interrupts */
#endif
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() (cpu_sr = OS_CPU_SR_SAVE() ) /* Disable interrupts */
#define OS_EXIT_CRITICAL() ( OS_CPU_SR_RESOTRE( cpu_sr) ) /* Enable interrupts */
#endif
4)、堆栈与任务级切换宏
ATMEGA堆栈由高地址往低地址增长。OS_STK_GROWTH定义为1
任务级的切换是通过直接调用OSCtxSw()实现,因为没有相关的软中断机制。
#define OS_STK_GROWTH 1 /* Stack grows from HIGH to LOW */
#define OS_TASK_SW() OSCtxSw()
B)、OS_CPU_C.C文件.都是一些C函数,最主要是的OSTaskStkInit()函数:
OSTaskInit()用于创建任务时初始化任务堆栈。这里任务的堆栈设计为:
保存任务时,依次将PC,R0-R31, SREG入栈,恢复任务时,逐个恢复SREG, R31-R0.,再执行RET指令即可返回.因而在任务创建时,堆栈初始化为依次压入PC值,R0-R31,地址指针往低地址移动。
OS_STK *OSTaskStkInit (void (*task)(void *pdata ), void *pdata, OS_STK *ptos, INT16U opt)
{
opt = opt;
*ptos-- = (INT16U)task & 0xff; /* PCL */
*ptos-- = ( (INT16U)task >> 8 ) & 0xff; /* PCH */
*ptos-- = 0; /* Register from r0 to r31 */
*ptos-- = 0; /* 注意,一般R1为0 */
*ptos-- = 2;
*ptos-- = 3;
*ptos-- = 4;
*ptos-- = 5;
*ptos-- = 6;
*ptos-- = 7;
*ptos-- = 8;
*ptos-- = 9;
*ptos-- = 10;
*ptos-- = 11;
*ptos-- = 12;
*ptos-- = 13;
*ptos-- = 14;
*ptos-- = 15;
*ptos-- = 16;
*ptos-- = 17;
*ptos-- = 18;
*ptos-- = 19;
*ptos-- = 20;
*ptos-- = 21;
*ptos-- = 22;
*ptos-- = 23;
*ptos-- = (INT16U)pdata & 0xff; /* Save arguemnt in r25:r24 */
*ptos-- = ( (INT16U)pdata>>8) & 0xff;
*ptos-- = 26;
*ptos-- = 27;
*ptos-- = 28;
*ptos-- = 29;
*ptos-- = 30;
*ptos-- = 31;
*ptos-- = 0x80; /* Save SREG, interrpt enabled */
return ptos;}
其中在设置任务的初始SREG值时,为0x80,默认为开中断。这样当切换至该任务时,任务不会将中断关掉。
R25:R24在最初始切换至该任务时保存了 传递给任务的参数值。这与GCC参数传递的机制相关.
OS_CPU_C.C中还有其它一些HOOK函数,函数体清空即可.
C)、OS_CPU_A.ASM。这个文件主要实现几个宏定义和与任务切换函数。
A)、OS_CPU_SR_SAVE()进入临界区。实现返回SREG值并关中断.
;进入临界区.
OS_CPU_SR_SAVE:
IN R24, _SFR_IO_ADDR( SREG ) ;保存SREG,并关中断
CLI
RET
B)、OS_CPU_SR_RESTORE( cpu_sr )退出临界区.恢复SREG并开 中断
以上的实现比较简单,
;退出临界区
OS_CPU_SR_RESTORE:
OUT _SFR_IO_ADDR( SREG ),R24 ;恢复SREG并开 中断
RET
;要特别注意入栈和出栈的顺序
;任务上下文的保护
.macro PUSHA
PUSH R0
PUSH R1
PUSH R2
PUSH R3
PUSH R4
PUSH R5
PUSH R6
PUSH R7
PUSH R8
PUSH R9
PUSH R10
PUSH R11
PUSH R12
PUSH R13
PUSH R14
PUSH R15
PUSH R16
PUSH R17
PUSH R18
PUSH R19
PUSH R20
PUSH R21
PUSH R22
PUSH R23
PUSH R24
PUSH R25
PUSH R26
PUSH R27
PUSH R28
PUSH R29
PUSH R30
PUSH R31
.endm
;任务上下文的恢复
.macro POPA
POP R0
OUT _SFR_IO_ADDR( SREG ), R0
POP R31
POP R30
POP R29
POP R28
POP R27
POP R26
POP R25
POP R24
POP R23
POP R22
POP R21
POP R20
POP R19
POP R18
POP R17
POP R16
POP R15
POP R14
POP R13
POP R12
POP R11
POP R10
POP R9
POP R8
POP R7
POP R6
POP R5
POP R4
POP R3
POP R2
POP R1
POP R0
.endm
C)、OSStartHighRdy()。这个函数在OSStart()被调用。以实现多任务启动时切换至第一个任务。这里仅给出实现代码说明:
OSStartHighRdy:
; 调用OSTaskSwHook()
; OSRunning <= TRUE
; 取最高优先级任务堆栈地址并恢复至SP
; 恢复任务上下文,开始运行多任务
; RET。将PC指向任务的起始地址.
OSCtxSw:
;保存当前任务的上下文,保存SP至TCB
; 调用OS_CPU_HOOKS_EN()
; OSPrioCur <= OSTCBHighRdy
; OSTCBCur <= OSTCBHighRdy
; OSTCBCur <= OSTCBHighRdy
;恢复新任务的SP至SP
;恢复新任务的上下文,实现任务切换
; RET
OSTickISR是操作系统时钟中断的处理程序。主要调用OSTimeTick()刷新任务的延时时间,将延时期到的任务置于就绪态,并进行调度。OSTickISR实际为定时器2的时钟中断处理程序
OSTickISR:
SIG_OUTPUT_COMPARE2:
OSTickISR:
PUSHA
IN R16, _SFR_IO_ADDR( SREG )
ORI R16,0x80 ; 必须保证任务存储的SREG 的位I为1
CALL OSIntEnter
PUSH R16
LDS R28, OSTCBCur ; 是,保存SP至当前任务TCB
LDS R29, OSTCBCur + 1
IN R0, _SFR_IO_ADDR( SPL )
ST Y+, R0
IN R0, _SFR_IO_ADDR( SPH )
ST Y+, R0
CALL OSTimeTick
CALL OSIntExit
POPA
RETI
这里,我并不打算支持中断的嵌套,因为M16里的内存资源有限,中断嵌套意味着需要为任务配置更多的堆栈空间.所以在中断ISR中中断被屏蔽。在这种情况下,对于其它中断,也并不配置其为允许中断嵌套。
需要特别注意黑体部分的代码.在AVR进入中断ISR前,硬件自动将SREG的I位清零,在执行RETI时会置1.如果任务直接用PUSHA将SREG保存,那么存储的SREG值的最高位(即位I)为0,当任务恢复SREG时,SREG的最高为为0,这时所有的中断都被屏蔽!如果在保存R0-R31后,保存SREG时,先读到R16,再置位R16中的最高位,再压栈。当恢复SREG时,SREG的最高位为1,这时全局中断是打开的。不会产生任何影响。另一种做法是ISR的第一条指令为SEI,这时任务存储的SREG为正确值,但在压栈的过程中,会发生中断嵌套的可能,而这是所不期望的。显然采用前一种方式可以将避免在ISR中发生中断嵌套。
参见avr-libc-user-manual-1.44.pdf中的说明 ( P121 )
Nested interrupts The AVR hardware clears the global interrupt flag in SREG before entering an interrupt vector. Thus, normally interrupts will remain disabled inside the handler until the handler exits, where the RETI instruction (that is emitted by the compiler as part of the normal function epilogue for an interrupt handler) will eventually re-enable further interrupts. For that reason, interrupt handlers normally do not nest. For most interrupt handlers, this is the desired behaviour, for some it is even required in order to prevent infinitely recursive interrupts (like UART interrupts, or level-triggered external interrupts). In rare circumstances though it might be desired to re-enable the global interrupt flag as early as possible in the interrupt handler, in order to not defer any other interrupt more than absolutely needed. This could be done using an sei() instruction right at the beginning of the interrupt handler, but this still leaves few instructions inside the compiler-generated function prologue to run with global interrupts disabled. The compiler can be instructed to insert an SEI instruction right at the beginning of an interrupt handler by declaring the handler the following way........
; OSIntCtxSw()
; 中断级的任务切换
OSIntCtxSw:
;OS_CPU_HOOKS_EN ()
; OSPrioCur <= OSTCBHighRdy
; OSTCBCur <= OSTCBHighRdy
; 恢复新任务的上下文,实现任务切换
RETI
如果对比UCOS书中的移植指导,按指导进行代码修改,会觉得还是比较容易实现的。
五、运行测试
在移植好代码后,必须进行测试,已验证多任务是否能正确运行。
具体的测试步骤如下:.
在main.c中的main()中创建task0, task1.
int main()
{
OSInit();
sem = OSSemCreate( 1 );
OSTaskCreate( task0, (void *)0, &stk[0][99], 2 ); // 创建task0
OSTaskCreate( task1, (void *)0, &stk[1][99], 3 ); // 创建task0
OSStart();
return 0;
}
void task0( void * pdata )
{
INT8U error;
TimerInit(); /* 初始化时钟节拍,必需 */
while(1)
{
OSSemPend( sem, 0, &error );
LED_ON(); /* 点亮LED */
OSTimeDlyHMSM(0,0,0,LED_DELAY_MS );
OSSemPost( sem );
}
}
void task1( void * pdata )
{
INT8U error;
while(1)
{
OSSemPend( sem, 0, &error );
LED_OFF(); /* 熄灭LED */
OSTimeDlyHMSM(0,0,0,LED_DELAY_MS );
OSSemPost( sem );
}
}
上代码中,Task0点亮LED,延时LED_DELAY_MS后发信号给Task1, 再次请求信号量;Task1获取运行权后熄灭LED,延时LED_DELAY_MS后发信号给Task0,再次请求信号量。
这些代码,实际可以完整的测试移植的代码。
首先main()中调用OSStart(),OSStart()会调用OSStartHighRdy()切换至Task0, 如果切换成功,则说明OSTaskStkInit(), OSStartHighRdy()正确。当Task0调用OSSemPend()时会调用OSCtxSw(),一旦切换至Task1,则说明该函数移植正确。如调用OSTimeDlyHMSM(),在定时器产生相应次数的时钟中断后,Task0能恢复执行,则出为OSTickISR, OSIntExit移植正确。可以单步调试进行检查.
运行效果,见附件.
六、设计总结
1、在分析ucos源码后,觉得其实现机理是比较简单、易于理解的。如ucos这样的嵌入式操作系统,在设计时已经考虑到了在不同处理器上的移植。同时又由于其应用场合的特殊性,这些操作系统没有包含复杂的功能,使用灵活。以上面的测试程序为例,虽然在前后台系统中可以通过进行定时中断或软件延时进行定时的开关LED,但在多任务操作系统上,因为有了操作系统平台的支持,任务代码的编写很简单,易于修改。
2、操作系统是对底层硬件作了一种抽像。多个任务并发的运行在自己的“虚拟机”上。
但是由于并发的引入,使得不再像前后台系统那样单个任务运行时独占系统所有的资源。这样任务间必须处理临界区问题,需要操作系统提供比较复杂的机制。一些新问题如“死锁”因此而产生。但总的来说,因为操作系统能对任务进行高效的调度,系统的总体效率在很大程序上得到提调。
3、在移植过程中容易忽视的点是必须注意所采用工具的版本。较新GCC版本在参数传递的处理上和以前的不太相同。这些不同在移植代码时必须被考虑进行,当转换至新的版本编译时,可能发生系统“跑飞”的情况。
李述铜 于 四川大学电气信息学院
2009-4-30
七、参考资料
1、《嵌入式实时操作系统uc/OS ii 》 Jean J.Labrosse
2、《嵌入式系统构件 》 Jean J.Labrosse
3、<<ARM嵌入式系统基础教程>> 周立功等编
4、Atmega16 DataSheet Atmel corporation
5、Avr-libc-user-manual-1.44.pdf
用户403611 2009-5-26 16:53
用户1585709 2009-5-19 11:10
用户167165 2009-5-1 21:18