1、在 51 系列单片机上移植 uCOS-II内容摘要:本文详细系统地介绍了 uC/OS-II 在 51 单片机上的移植、重入实现方法、硬件仿真、固化、人机界面等关键内容。关键词:嵌入式实时多任务操作系统、uC/OS-II、C51引言:随着各种应用电子系统的复杂化和系统实时性需求的提高,并伴随应用软件朝着系统化方向发展的加速,在 16 位/32 位单片机中广泛使用了嵌入式实时操作系统。然而实际使用中却存在着大量 8 位单片机,从经济性考虑,对某些应用场合,在 8 位 MCU 上使用操作系统是可行的。从学习操作系统角度,uC/OS-II for 51 即简单又全面,学习成本低廉,值得推广。结语:C/
2、OS-II 具有免费、简单、可靠性高、实时性好等优点,但也有缺乏便利开发环境等缺点,尤其不像商用嵌入式系统那样得到广泛使用和持续的研究更新。但开放性又使得开发人员可以自行裁减和添加所需的功能,在许多应用领域发挥着独特的作用。当然,是否在单片机系统中嵌入 C/OS-II 应视所开发的项目而定,对于一些简单的、低成本的项目来说,就没必要使用嵌入式操作系统了。 uC/OS-II 原理:uCOSII 包括任务调度、时间管理、内存管理、资源管理(信号量、邮箱、消息队列)四大部分,没有文件系统、网络接口、输入输出界面。它的移植只与 4 个文件相关:汇编文件(OS_CPU_A.ASM)、处理器相关 C 文件
3、(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有 64 个优先级,系统占用 8 个,用户可创建 56 个任务,不支持时间片轮转。它的基本思路就是 “近似地每时每刻总是让优先级最高的就绪任务处于运行状态” 。为了保证这一点,它在调用系统 API 函数、中断结束、定时中断结束时总是执行调度算法。原作者通过事先计算好数据,简化了运算量,通过精心设计就绪表结构,使得延时可预知。任务的切换是通过模拟一次中断实现的。uCOSII 工作核心原理是:近似地让最高优先级的就绪任务处于运行状态。操作系统将在下面情况中进行任务调度:调用 API 函数(用户主动调用),中断(系统占用的时间
4、片中断OsTimeTick(),用户使用的中断)。调度算法书上讲得很清楚,我主要讲一下整体思路。(1)在调用 API 函数时,有可能引起阻塞,如果系统 API 函数察觉到运行条件不满足,需要切换就调用OSSched()调度函数,这个过程是系统自动完成的,用户没有参与。OSSched()判断是否切换,如果需要切换,则此函数调用 OS_TASK_SW()。这个函数模拟一次中断(在 51 里没有软中断,我用子程序调用模拟,效果相同),好象程序被中断打断了,其实是 OS 故意制造的假象,目的是为了任务切换。既然是中断,那么返回地址(即紧邻 OS_TASK_SW()的下一条汇编指令的 PC 地址)就被自
5、动压入堆栈,接着在中断程序里保存 CPU 寄存器(PUSHALL)。堆栈结构不是任意的,而是严格按照 uCOSII 规范处理。OS 每次切换都会保存和恢复全部现场信息(POPALL),然后用 RETI 回到任务断点继续执行。这个断点就是 OSSched()函数里的紧邻 OS_TASK_SW()的下一条汇编指令的 PC 地址。切换的整个过程就是,用户任务程序调用系统 API函数,API 调用 OSSched(),OSSched()调用软中断 OS_TASK_SW()即 OSCtxSw,返回地址(PC 值)压栈,进入 OSCtxSw 中断处理子程序内部。反之,切换程序调用 RETI 返回紧邻 OS
6、_TASK_SW()的下一条汇编指令的PC 地址,进而返回 OSSched()下一句,再返回 API 下一句,即用户程序断点。因此,如果任务从运行到就绪再到运行,它是从调度前的断点处运行。(2)中断会引发条件变化,在退出前必须进行任务调度。uCOSII 要求中断的堆栈结构符合规范,以便正确协调中断退出和任务切换。前面已经说到任务切换实际是模拟一次中断事件,而在真正的中断里省去了模拟(本身就是中断嘛)。只要规定中断堆栈结构和 uCOSII 模拟的堆栈结构一样,就能保证在中断里进行正确的切换。任务切换发生在中断退出前,此时还没有返回中断断点。仔细观察中断程序和切换程序最后两句,它们是一模一样的,P
7、OPALL+RETI。即要么直接从中断程序退出,返回断点;要么先保存现场到TCB,等到恢复现场时再从切换函数返回原来的中断断点(由于中断和切换函数遵循共同的堆栈结构,所以退出操作相同,效果也相同)。用户编写的中断子程序必须按照 uCOSII 规范书写。任务调度发生在中断退出前,是非常及时的,不会等到下一时间片才处理。OSIntCtxSw()函数对堆栈指针做了简单调整,以保证所有挂起任务的栈结构看起来是一样的。(3)在 uCOSII 里,任务必须写成两种形式之一(uCOSII 中文版p99 页)。在有些 RTOS 开发环境里没有要求显式调用 OSTaskDel(),这是因为开发环境自动做了处理,
8、实际原理都是一样的。uCOSII 的开发依赖于编译器,目前没有专用开发环境,所以出现这些不便之处是可以理解的。移植过程:(1)拷贝书后附赠光盘 sourcecode 目录下的内容到 C:YY 下,删除不必要的文件和 EX1L.C,只剩下p187(uCOSII)上列出的文件。(2)改写最简单的 OS_CPU.H数据类型的设定见 C51.PDF 第 176 页。注意 BOOLEAN 要定义成 unsigned char 类型,因为 bit 类型为 C51特有,不能用在结构体里。EA=0 关中断;EA=1 开中断。这样定义即减少了程序行数,又避免了退出临界区后关中断造成的死机。MCS-51 堆栈从下
9、往上增长(1=向下,0=向上),OS_STK_GROWTH 定义为 0#define OS_TASK_SW() OSCtxSw() 因为 MCS-51 没有软中断指令,所以用程序调用代替。两者的堆栈格式相同,RETI 指令复位中断系统,RET 则没有。实践表明,对于 MCS-51,用子程序调用入栈,用中断返回指令 RETI 出栈是没有问题的,反之中断入栈 RET 出栈则不行。总之,对于入栈,子程序调用与中断调用效果是一样的,可以混用。在没有中断发生的情况下复位中断系统也不会影响系统正常运行。详见uC/OS-II第八章 193 页第 12 行(3)改写 OS_CPU_C.C我设计的堆栈结构如下图
10、所示:TCB 结构体中 OSTCBStkPtr 总是指向用户堆栈最低地址,该地址空间内存放用户堆栈长度,其上空间存放系统堆栈映像,即:用户堆栈空间大小=系统堆栈空间大小+1 。SP 总是先加 1 再存数据,因此,SP 初始时指向系统堆栈起始地址(OSStack)减 1 处(OSStkStart) 。很明显系统堆栈存储空间大小=SP-OSStkStart。任务切换时,先保存当前任务堆栈内容。方法是:用 SP-OSStkStart 得出保存字节数,将其写入用户堆栈最低地址内,以用户堆栈最低地址为起址,以 OSStkStart 为系统堆栈起址,由系统栈向用户栈拷贝数据,循环 SP-OSStkStar
11、t 次,每次拷贝前先将各自栈指针增 1。其次,恢复最高优先级任务系统堆栈。方法是:获得最高优先级任务用户堆栈最低地址,从中取出“长度”,以最高优先级任务用户堆栈最低地址为起址,以 OSStkStart 为系统堆栈起址,由用户栈向系统栈拷贝数据,循环“ 长度 ”数值指示的次数,每次拷贝前先将各自栈指针增 1。用户堆栈初始化时从下向上依次保存:用户堆栈长度(15) ,PCL,PCH,PSW,ACC ,B,DPL,DPH,R0 ,R1,R2,R3 ,R4,R5,R6,R7 。不保存 SP,任务切换时根据用户堆栈长度计算得出。OSTaskStkInit 函数总是返回用户栈最低地址。操作系统 tick
12、时钟我使用了 51 单片机的 T0 定时器,它的初始化代码用 C 写在了本文件中。最后还有几点必须注意的事项。本来原则上我们不用修改与处理器无关的代码,但是由于 KEIL 编译器的特殊性,这些代码仍要多处改动。因为 KEIL 缺省情况下编译的代码不可重入,而多任务系统要求并发操作导致重入,所以要在每个 C 函数及其声明后标注 reentrant 关键字。另外, “pdata”、 “data”在 uCOS 中用做一些函数的形参,但它同时又是 KEIL 的关键字,会导致编译错误,我通过把“pdata”改成“ppdata”, “data”改成“ddata”解决了此问题。 OSTCBCur、OSTCB
13、HighRdy、OSRunning、OSPrioCur、OSPrioHighRdy 这几个变量在汇编程序中用到了,为了使用 Ri 访问而不用 DPTR,应该用 KEIL 扩展关键字 IDATA 将它们定义在内部 RAM 中。(4)重写 OS_CPU_A.ASMA51 宏汇编的大致结构如下:NAME 模块名 ;与文件名无关;定义重定位段 必须按照 C51 格式定义,汇编遵守 C51 规范。段名格式为: ?PR?函数名?模块名;声明引用全局变量和外部子程序 注意关键字为“EXTRN”没有E全局变量名直接引用无参数/无寄存器参数函数 FUNC带寄存器参数函数 _FUNC重入函数 _?FUNC;分配堆
14、栈空间只关心大小,堆栈起点由 keil 决定,通过标号可以获得 keil 分配的 SP 起点。切莫自己分配堆栈起点,只要用 DS 通知 KEIL 预留堆栈空间即可。?STACK 段名与 STARTUP.A51 中的段名相同,这意味着 KEIL 在 LINK 时将把两个同名段拼在一起,我预留了 40H 个字节,STARTUP.A51 预留了 1 个字节,LINK 完成后堆栈段总长为 41H。查看 yy.m51 知KEIL 将堆栈起点定在 21H,长度 41H,处于内部 RAM 中。;定义宏宏名 MACRO 实体 ENDM;子程序OSStartHighRdyOSCtxSwOSIntCtxSwOST
15、ickISRSerialISREND ;声明汇编源文件结束一般指针占 3 字节。+0 类型+1 高 8 位数据+2 低 8 位数据 详见 C51.PDF 第 178 页低位地址存高 8 位值,高位地址存低 8 位值。例如 0x1234,基址+0:0x12 基址+1:0x34(5)移植串口驱动程序在此之前我写过基于中断的串口驱动程序,包括打印字节/字/长字/字符串,读串口,初始化串口/缓冲区。把它改成重入函数即可直接使用。系统提供的显示函数是并发的,它不是直接显示到串口,而是先输出到显存,用户不必担心 IO 慢速操作影响程序运行。串口输入也采用了同样的技术,他使得用户在 CPU 忙于处理其他任务
16、时照样可以盲打输入命令。(6)编写测试程序 Demo(YY.C)Demo 程序创建了 3 个任务 A、B、C 优先级分别为 2、3、4,A 每秒显示一次,B 每 3 秒显示一次,C 每6 秒显示一次。从显示结果看,显示 3 个 A 后显示 1 个 B,显示 6 个 A 和 2 个 B 后显示 1 个 C,结果显然正确。显示结果如下:AAAAAA111111 is activeAAAAAA111111 is activeAAAAAA111111 is activeBBBBBB333333 is activeAAAAAA111111 is activeAAAAAA111111 is activeA
17、AAAAA111111 is activeBBBBBB333333 is activeCCCCCC666666 is activeAAAAAA111111 is activeAAAAAA111111 is activeAAAAAA111111 is activeBBBBBB333333 is activeAAAAAA111111 is activeAAAAAA111111 is activeAAAAAA111111 is activeBBBBBB333333 is activeCCCCCC666666 is activeDemo 程序经 Keil701 编译后,代码量为 7-8K,可直接在 K
18、eilC51 上仿真运行。编译时要将 OS_CPU_C.C、UCOS_II.C、OS_CPU_A.ASM、YY.C 加入项目 文件名 : OS_CPU_A.ASM$NOMOD51EA BIT 0A8H.7SP DATA 081HB DATA 0F0HACC DATA 0E0HDPH DATA 083HDPL DATA 082HPSW DATA 0D0HTR0 BIT 088H.4TH0 DATA 08CHTL0 DATA 08AHNAME OS_CPU_A ;模块名;定义重定位段?PR?OSStartHighRdy?OS_CPU_A SEGMENT CODE?PR?OSCtxSw?OS_CPU
19、_A SEGMENT CODE?PR?OSIntCtxSw?OS_CPU_A SEGMENT CODE?PR?OSTickISR?OS_CPU_A SEGMENT CODE?PR?_?serial?OS_CPU_A SEGMENT CODE;声明引用全局变量和外部子程序EXTRN IDATA (OSTCBCur)EXTRN IDATA (OSTCBHighRdy)EXTRN IDATA (OSRunning)EXTRN IDATA (OSPrioCur)EXTRN IDATA (OSPrioHighRdy)EXTRN CODE (_?OSTaskSwHook)EXTRN CODE (_?ser
20、ial)EXTRN CODE (_?OSIntEnter)EXTRN CODE (_?OSIntExit)EXTRN CODE (_?OSTimeTick) ;对外声明 4 个不可重入函数PUBLIC OSStartHighRdyPUBLIC OSCtxSwPUBLIC OSIntCtxSwPUBLIC OSTickISR;PUBLIC SerialISR ;分配堆栈空间。只关心大小,堆栈起点由 keil 决定,通过标号可以获得 keil 分配的 SP 起点。?STACK SEGMENT IDATARSEG ?STACKOSStack:DS 40HOSStkStart IDATA OSStac
21、k-1;定义压栈出栈宏PUSHALL MACROPUSH PSWPUSH ACCPUSH BPUSH DPLPUSH DPHMOV A,R0 ;R0-R7 入栈PUSH ACCMOV A,R1PUSH ACCMOV A,R2PUSH ACCMOV A,R3PUSH ACCMOV A,R4PUSH ACCMOV A,R5PUSH ACCMOV A,R6PUSH ACCMOV A,R7PUSH ACC;PUSH SP ;不必保存 SP,任务切换时由相应程序调整ENDMPOPALL MACRO;POP ACC ;不必保存 SP,任务切换时由相应程序调整POP ACC ;R0-R7 出栈MOV R7,
22、APOP ACCMOV R6,APOP ACCMOV R5,APOP ACCMOV R4,APOP ACCMOV R3,APOP ACCMOV R2,APOP ACCMOV R1,APOP ACCMOV R0,APOP DPHPOP DPLPOP BPOP ACCPOP PSWENDM;子程序;-RSEG ?PR?OSStartHighRdy?OS_CPU_AOSStartHighRdy:USING 0 ;上电后 51 自动关中断,此处不必用 CLR EA 指令,因为到此处还未开中断,本程序退出后,开中断。LCALL _?OSTaskSwHookOSCtxSw_in:;OSTCBCur = D
23、PTR 获得当前 TCB 指针,详见 C51.PDF 第 178 页MOV R0,#LOW (OSTCBCur) ;获得 OSTCBCur 指针低地址,指针占 3 字节。+0 类型+1 高 8 位数据+2 低 8 位数据INC R0MOV DPH,R0 ;全局变量 OSTCBCur 在 IDATA 中INC R0MOV DPL,R0;OSTCBCur-OSTCBStkPtr = DPTR 获得用户堆栈指针INC DPTR ;指针占 3 字节。+0 类型+1 高 8 位数据+2 低 8 位数据MOVX A,DPTR ;.OSTCBStkPtr 是 void 指针MOV R0,AINC DPTRM
24、OVX A,DPTRMOV R1,AMOV DPH,R0MOV DPL,R1;*UserStkPtr = R5 用户堆栈起始地址内容(即用户堆栈长度放在此处) 详见文档说明 指针用法详见C51.PDF 第 178 页 MOVX A,DPTR ;用户堆栈中是 unsigned char 类型数据MOV R5,A ;R5=用户堆栈长度;恢复现场堆栈内容MOV R0,#OSStkStartrestore_stack:INC DPTRINC R0MOVX A,DPTRMOV R0,ADJNZ R5,restore_stack;恢复堆栈指针 SPMOV SP,R0;OSRunning=TRUEMOV R
25、0,#LOW (OSRunning)MOV R0,#01POPALLSETB EA ;开中断RETI;-RSEG ?PR?OSCtxSw?OS_CPU_AOSCtxSw: PUSHALLOSIntCtxSw_in:;获得堆栈长度和起址MOV A,SPCLR CSUBB A,#OSStkStartMOV R5,A ;获得堆栈长度 ;OSTCBCur = DPTR 获得当前 TCB 指针,详见 C51.PDF 第 178 页MOV R0,#LOW (OSTCBCur) ;获得 OSTCBCur 指针低地址,指针占 3 字节。+0 类型+1 高 8 位数据+2 低 8 位数据INC R0MOV DP
26、H,R0 ;全局变量 OSTCBCur 在 IDATA 中INC R0MOV DPL,R0;OSTCBCur-OSTCBStkPtr = DPTR 获得用户堆栈指针INC DPTR ;指针占 3 字节。+0 类型+1 高 8 位数据+2 低 8 位数据MOVX A,DPTR ;.OSTCBStkPtr 是 void 指针MOV R0,AINC DPTRMOVX A,DPTRMOV R1,AMOV DPH,R0MOV DPL,R1;保存堆栈长度MOV A,R5MOVX DPTR,AMOV R0,#OSStkStart ;获得堆栈起址save_stack:INC DPTRINC R0MOV A,R
27、0MOVX DPTR,ADJNZ R5,save_stack;调用用户程序LCALL _?OSTaskSwHook;OSTCBCur = OSTCBHighRdyMOV R0,#OSTCBCurMOV R1,#OSTCBHighRdyMOV A,R1MOV R0,AINC R0INC R1MOV A,R1MOV R0,AINC R0INC R1MOV A,R1MOV R0,A;OSPrioCur = OSPrioHighRdy 使用这两个变量主要目的是为了使指针比较变为字节比较,以便节省时间。MOV R0,#OSPrioCurMOV R1,#OSPrioHighRdyMOV A,R1MOV R
28、0,ALJMP OSCtxSw_in;-RSEG ?PR?OSIntCtxSw?OS_CPU_AOSIntCtxSw:;调整 SP 指针去掉在调用 OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容;SP=SP-4MOV A,SPCLR CSUBB A,#4MOV SP,ALJMP OSIntCtxSw_in;-CSEG AT 000BH ;OSTickISRLJMP OSTickISR ;使用定时器 0RSEG ?PR?OSTickISR?OS_CPU_AOSTickISR: USING 0 PUSHALLCLR TR0MOV TH0,#70H ;定义 Tick=50 次/秒(即 0.02 秒/次)MOV TL0,#00H ;OS_CPU_C.C 和 OS_TICKS_PER_SECSETB TR0LCALL _?OSIntEnterLCALL _?OSTimeTickLCALL _?OSIntExitPOPALL RETI;-CSEG AT 0023H ;串口中断LJMP SerialISR ;工作于系统态,无任务切换。RSEG ?PR?_?serial?OS_CPU_ASerialISR:USING 0 PUSHALLCLR EALCALL _?serial SETB EAPOPALL RETI;-