原创 圆梦小车学习之-------- MicroC/OS在ATMEGA上的移植

2011-8-1 19:47 3887 4 7 分类: MCU/ 嵌入式


https://static.assets-stash.eet-china.com/album/old-resources/2009/5/6/f4cefaee-0744-4134-bbd9-d5a69680b3fe.rar" target="_blank">


一、设计背景


对于一些小型的嵌入式设备而言,往往采用单片机+前后台式系统设计。即前台程序(中断)负责处理外部事件,后台程序是由一死循环+一系列的函数调用构成。通常前台程序可以用于处理比较紧急,如实时性要求较强的事件,而后台程序中的各函数即作为多个任务运行。如图所示:


56a82423-5b94-4631-adc1-4fc0237d6273.jpg


其中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实现机制而提出的。所以,如果没有理解这个系统的实现机制,那么对其中的移植(代码修改),很难理解为什么要这样做。



至于“了解”到这什么程序,这个很难评价!


e29f5c39-1417-4b82-921d-41712157691b.jpg


     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


 


 


 


 

 

文章评论3条评论)

登录后参与讨论

用户403611 2009-5-26 16:53

纠正一个BUG. 上传的OS_CPU_A.S文件中.必须在时钟中断中加上call OSIntEnter否则如果在中断中调用了SSched,会进行任务级切换. 不知道有没有下过原来的那个包?非常抱歉

用户1585709 2009-5-19 11:10

万分感激

用户167165 2009-5-1 21:18

学习
相关推荐阅读
用户403611 2014-01-20 07:28
与TKScope仿真器同行(1) - 看门狗会让你无法调试
  前几日,中矿龙科的李工向我反映了一个有意思的问题: 在使用TKScope仿真器(型号AK100pro)调试STM32时,出现了一个非常奇怪的现像。在Keil环境中的源代码设置了一...
用户403611 2013-02-27 13:41
ARM指令仿真项目经历纪录一
这两天接了个新项目-ARM指令仿真项目,开发时间预期在两个月左右。这次将继承沿续自己以前做Cortex-A8、A9内核仿真项目时的方法,用日志纪录在开发过程中的各种问题解决方案和体会。限于某些原因...
用户403611 2013-02-27 13:39
电子工程师应尝试产品经理的角色
做技术两三年了,发现自己一直陷入到技术细节当中,而从来没有尝试跳出来去从整个产品的角度进行观察。这其中可能是因为需要了解的技术细节太多,没有闲暇去关注技术之外的东西。另一方面也与个人的视野不够开阔...
用户403611 2011-11-13 20:05
EDNChina的博客已经改得面目全面了
 之前有些日子没去ENDChina了。从08年起,断断续续地在这上面写一些技术类的Blog,到现在已经有快4年,虽然文章写的不多,但挺有感情的。   这两天回去看看,访问http://blog...
用户403611 2011-10-14 22:02
TKScope仿真器使用入门视频教程
  相对来说,看视频肯定要比看PDF文档要容易的多吧。部门之前仅在网上发布了TKScope仿真器使用的PDF文档。虽然文档写的很详细,但实际真正愿意去看的不多。前些日子自己录制了TKScope仿真AR...
用户403611 2011-09-18 23:00
尝试建立一个部门内部的知识库站点
前些天有事直接去找了下戚工反映TKScope仿真器方面的几个问题。问题解决之后闲聊了几句,其中就提及了建立一个共享的内部网络站点。当时我听了很兴奋,因为这个想法与我的不谋而合。早在刚进入这个部门不久,...
我要评论
3
4
关闭 站长推荐上一条 /3 下一条