在
51系列单片机上移植
uCOS-II
内容摘要:本文详细系统地介绍了
uC/OS-II在
51单片机上的移植、重入实现方法、硬件仿真、固化、人机界面等关键内容。
关键词:嵌入式实时多任务操作系统、
uC/OS-II、
C51
引 言:随着各种应用电子系统的复杂化和系统实时性需求的提高,并伴随应用软件朝着系统化方向发展的加速,在
16位
/32位单片机中广泛使用了嵌入式实时操作 系统。然而实际使用中却存在着大量
8位单片机,从经济性考虑,对某些应用场合,在
8位
MCU上使用操作系统是可行的。从学习操作系统角度,
uC/OS- II for 51即简单又全面,学习成本低廉,值得推广。
结语:
μC/OS-II具有免费、简单、可靠性高、实时性好等优点,但也有缺乏便利开 发环境等缺点,尤其不像商用嵌入式系统那样得到广泛使用和持续的研究更新。但开放性又使得开发人员可以自行裁减和添加所需的功能,在许多应用领域发挥着独 特的作用。当然,是否在单片机系统中嵌入
μC/OS-II应视所开发的项目而定,对于一些简单的、低成本的项目来说,就没必要使用嵌入式操作系统了。
uC/OS-II原理:
uCOSII 包括任务调度、时间管理、内存管理、资源管理(信号量、邮箱、消息队列)四大部分,没有文件系统、网络接口、输入输出界面。它的移植只与
4个文件相关:汇 编文件(
OS_CPU_A.ASM)、处理器相关
C文件(
OS_CPU.H、
OS_CPU_C.C)和配置文件(
OS_CFG.H)。有
64个优先级,系 统占用
8个,用户可创建
56个任务,不支持时间片轮转。它的基本思路就是
“近似地每时每刻总是让优先级最高的就绪任务处于运行状态
”
。为了保证这一点,它在调用系统
API函数、中断结束、定时中断结束时总是执行调度算法。原作者通过事先计算好数据,简化了运算量,通过精心设计就绪表结 构,使得延时可预知。任务的切换是通过模拟一次中断实现的。
uCOSII工作核心原理是:近似地让最高优先级的就绪任务处于运行状态。
操作系统将在下面情况中进行任务调度:调用
API函数
(用户主动调用
),中断
(系统占用的时间片中断
OsTimeTick(),用户使用的中断
)。
调度算法书上讲得很清楚,我主要讲一下整体思路。
(1) 在调用
API函数时,有可能引起阻塞
,如果系统
API函数察觉到运行条件不满足,需要切换就调用
OSSched()调度函数,这个过程是系统自动完成的, 用户没有参与。
OSSched()判断是否切换,如果需要切换,则此函数调用
OS_TASK_SW()。这个函数模拟一次中断
(在
51里没有软中断,我用 子程序调用模拟,效果相同
),好象程序被中断打断了,其实是
OS故意制造的假象,目的是为了任务切换。既然是中断,那么返回地址
(即紧邻
OS_TASK_SW()的下一条汇编指令的
PC地址
)就被自动压入堆栈,接着在中断程序里保存
CPU寄存器
(PUSHALL)……。堆栈结构不是任意 的,而是严格按照
uCOSII规范处理。
OS每次切换都会保存和恢复全部现场信息
(POPALL),然后用
RETI回到任务断点继续执行。这个断点就是
OSSched()函数里的紧邻
OS_TASK_SW()的下一条汇编指令的
PC地址。切换的整个过程就是,用户任务程序调用系统
API函数,
API调用
OSSched(),
OSSched()调用软中断
OS_TASK_SW()即
OSCtxSw,返回地址
(PC值
)压栈,进入
OSCtxSw中断处理子程 序内部。反之,切换程序调用
RETI返回紧邻
OS_TASK_SW()的下一条汇编指令的
PC地址,进而返回
OSSched()下一句,再返回
API下一 句,即用户程序断点。因此,如果任务从运行到就绪再到运行,它是从调度前的断点处运行。
(2)中断会引发条件变化,在退出前必须进行任务调度。
uCOSII要求中断的堆栈结构符合规范,以便正确协调中断退出和任务切换。前面已经说到任务切换实际是模拟一次中断事件,而在真正的中断里省去了模拟
(本身就是中断嘛
)。只要规定中断堆栈结构和
uCOSII模拟的堆栈结构一样,就能保证在中断里进行正确的切换。任务切换发生在中断退出前,此时还没有返 回中断断点。仔细观察中断程序和切换程序最后两句,它们是一模一样的,
POPALL+RETI。即要么直接从中断程序退出,返回断点;要么先保存现场到
TCB,等到恢复现场时再从切换函数返回原来的中断断点
(由于中断和切换函数遵循共同的堆栈结构,所以退出操作相同,效果也相同
)。用户编写的中断子程序 必须按照
uCOSII规范书写。任务调度发生在中断退出前,是非常及时的,不会等到下一时间片才处理。
OSIntCtxSw()函数对堆栈指针做了简单调 整,以保证所有挂起任务的栈结构看起来是一样的。
(3)在
uCOSII里,任务必须写成两种形式之一
(《
uCOSII中文版》
p99页
)。在有些
RTOS开发环境里没有要求显式调用
OSTaskDel(),这是因为开发环境自动做了处理,实际原理都是一样的。
uCOSII的开发依赖于编译器,目前 没有专用开发环境,所以出现这些不便之处是可以理解的。
移植过程:
(1)拷贝书后附赠光盘
sourcecode目录下的内容到
C:\YY下,删除不必要的文件和
EX 1L .C,只剩下
p187(《
uCOSII》
)上列出的文件。
(2)改写最简单的
OS_CPU.H
数据类型的设定见
C51.PDF第
176页。注意
BOOLEAN要定义成
unsigned char 类型,因为
bit类型为
C51特有,不能用在结构体里。
EA=0关中断;
EA=1开中断。这样定义即减少了程序行数,又避免了退出临界区后关中断造成的死机。
MCS-51堆栈从下往上增长
(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
我设计的堆栈结构如下图所示:
TCB结构体中
OSTCBStkPtr总是指向用户堆栈最低地址,该地址空间内存放用户堆栈长度,其上空间存放系统堆栈映像,即:用户堆栈空间大小
=系统堆栈空间大小
+1。
SP总是先加
1再存数据,因此,
SP初始时指向系统堆栈起始地址
(OSStack)减
1处
(OSStkStart)。很明显系统堆栈存储空间大小
=SP-OSStkStart。
任 务切换时,先保存当前任务堆栈内容。方法是:用
SP-OSStkStart得出保存字节数,将其写入用户堆栈最低地址内,以用户堆栈最低地址为起址,以
OSStkStart为系统堆栈起址,由系统栈向用户栈拷贝数据,循环
SP-OSStkStart次,每次拷贝前先将各自栈指针增
1。
其次,恢复 最高优先级任务系统堆栈。方法是:获得最高优先级任务用户堆栈最低地址,从中取出
“长度
”,以最高优先级任务用户堆栈最低地址为起址,以
OSStkStart为系统堆栈起址,由用户栈向系统栈拷贝数据,循环
“长度
”数值指示的次数,每次拷贝前先将各自栈指针增
1。
用户堆栈初始化时从下向上依次保存:用户堆栈长度(
15),
PCL,
PCH,
PSW,
ACC,
B,
DPL,
DPH,
R0,
R1,
R2,
R3,
R4,
R5,
R6,
R7。不保存
SP,任务切换时根据用户堆栈长度计算得出。
OSTaskStkInit函数总是返回用户栈最低地址。
操作系统
tick时钟我使用了
51单片机的
T0定时器,它的初始化代码用
C写在了本文件中。
最 后还有几点必须注意的事项。本来原则上我们不用修改与处理器无关的代码,但是由于
KEIL编译器的特殊性,这些代码仍要多处改动。因为
KEIL缺省情况下 编译的代码不可重入,而多任务系统要求并发操作导致重入,所以要在每个
C函数及其声明后标注
reentrant关键字。另外,
“pdata”、
“data”在
uCOS中用做一些函数的形参,但它同时又是
KEIL的关键字,会导致编译错误,我通过把
“pdata”改成
“ppdata”,
“data”改成
“ddata”解决了此问题。
OSTCBCur、
OSTCBHighRdy、
OSRunning、
OSPrioCur、
OSPrioHighRdy这几个变量在汇编程序中用到了,为了使用
Ri访问而不用
DPTR,应该用
KEIL扩展关键字
IDATA将 它们定义在内部
RAM中。
(4)重写
OS_CPU_A.ASM
A51宏汇编的大致结构如下:
NAME 模块名
;与文件名无关
;定义重定位段 必须按照
C51格式定义,汇编遵守
C51规范。段名格式为:
?PR?函数名
?模块名
;声明引用全局变量和外部子程序 注意关键字为
“EXTRN”没有
'E’
全局变量名直接引用
无参数
/无寄存器参数函数
FUNC
带寄存器参数函数
_FUNC
重入函数
_?FUNC
;分配堆栈空间
只关心大小,堆栈起点由
keil决定,通过标号可以获得
keil分配的
SP起点。切莫自己分配堆栈起点,只要用
DS通知
KEIL预留堆栈空间即可。
?STACK 段名与
STARTUP.A51中的段名相同,这意味着
KEIL在
LINK时将把两个同名段拼在一起,我预留了
40H个字节,
STARTUP.A51预留了
1个字节,
LINK完成后堆栈段总长为
41H。查看
yy.m51知
KEIL将堆栈起点定在
21H,长度
41H,处于内部
RAM中。
;定义宏
宏名
MACRO 实体
ENDM
;子程序
OSStartHighRdy
OSCtxSw
OSIntCtxSw
OSTickISR
SerialISR
END ;声明汇编源文件结束
一般指针占
3字节。
+0类型
+1高
8位数据
+2低
8位数据 详见
C51.PDF第
178页
低位地址存高
8位值,高位地址存低
8位值。例如
0x1234,基址
+0:0x12 基址
+1:0x34
(5)移植串口驱动程序
在此之前我写过基于中断的串口驱动程序,包括打印字节
/字
/长字
/字符串,读串口,初始化串口
/缓冲区。把它改成重入函数即可直接使用。
系统提供的显示函数是并发的,它不是直接显示到串口,而是先输出到显存,用户不必担心
IO慢速操作影响程序运行。串口输入也采用了同样的技术,他使得用户在
CPU忙于处理其他任务时照样可以盲打输入命令。
(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 active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
Demo程序经
Keil701编译后,代码量为
7-8K,可直接在
KeilC51上仿真运行。
编译时要将
OS_CPU_C.C、
UCOS_II.C、
OS_CPU_A.ASM、
YY.C加入项目
文件名
: OS_CPU_A.ASM
$NOMOD51
EA BIT 0A 8H.7
SP DATA 081H
B DATA 0F 0H
ACC DATA 0E0H
DPH DATA 083H
DPL DATA 082H
PSW DATA 0D0H
TR0 BIT 088H.4
TH0 DATA 08CH
TL0 DATA 08AH
NAME OS_CPU_A ;模块名
;定义重定位段
?PR?OSStartHighRdy?OS_CPU_A SEGMENT CODE
?PR?OSCtxSw?OS_CPU_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 (_?serial)
EXTRN CODE (_?OSIntEnter)
EXTRN CODE (_?OSIntExit)
EXTRN CODE (_?OSTimeTick)
;对外声明
4个不可重入函数
PUBLIC OSStartHighRdy
PUBLIC OSCtxSw
PUBLIC OSIntCtxSw
PUBLIC OSTickISR
;PUBLIC SerialISR
;分配堆栈空间。只关心大小,堆栈起点由
keil决定,通过标号可以获得
keil分配的
SP起点。
?STACK SEGMENT IDATA
RSEG ?STACK
OSStack:
DS 40H
OSStkStart IDATA OSStack-1
;定义压栈出栈宏
PUSHALL MACRO
PUSH PSW
PUSH ACC
PUSH B
PUSH DPL
PUSH DPH
MOV A,R0 ;R0-R7入栈
PUSH ACC
MOV A,R1
PUSH ACC
MOV A,R2
PUSH ACC
MOV A,R3
PUSH ACC
MOV A,R4
PUSH ACC
MOV A,R5
PUSH ACC
MOV A,R6
PUSH ACC
MOV A,R7
PUSH ACC
;PUSH SP ;不必保存
SP,任务切换时由相应程序调整
ENDM
POPALL MACRO
;POP ACC ;不必保存
SP,任务切换时由相应程序调整
POP ACC ;R0-R7出栈
MOV R7,A
POP ACC
MOV R6,A
POP ACC
MOV R5,A
POP ACC
MOV R4,A
POP ACC
MOV R3,A
POP ACC
MOV R2,A
POP ACC
MOV R1,A
POP ACC
MOV R0,A
POP DPH
POP DPL
POP B
POP ACC
POP PSW
ENDM
;子程序
;-------------------------------------------------------------------------
RSEG ?PR?OSStartHighRdy?OS_CPU_A
OSStartHighRdy:
USING 0 ;上电后
51自动关中断,此处不必用
CLR EA指令,因为到此处还未开中断,本程序退出后,开中断。
LCALL _?OSTaskSwHook
OSCtxSw_in:
;OSTCBCur ===> DPTR 获得当前
TCB指针,详见
C51.PDF第
178页
MOV R0,#LOW (OSTCBCur) ;获得
OSTCBCur指针低地址,指针占
3字节。
+0类型
+1高
8位数据
+2低
8位数据
INC R0
MOV DPH,@R0 ;全局变量
OSTCBCur在
IDATA中
INC R0
MOV DPL,@R0
;OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;指针占
3字节。
+0类型
+1高
8位数据
+2低
8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是
void指针
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1
;*UserStkPtr ===> R5 用户堆栈起始地址内容
(即用户堆栈长度放在此处
) 详见文档说明 指针用法详见
C51.PDF第
178页
MOVX A,@DPTR ;用户堆栈中是
unsigned char类型数据
MOV R5,A ;R5=用户堆栈长度
;恢复现场堆栈内容
MOV R0,#OSStkStart
restore_stack:
INC DPTR
INC R0
MOVX A,@DPTR
MOV @R0,A
DJNZ R5,restore_stack
;恢复堆栈指针
SP
MOV SP,R0
;OSRunning=TRUE
MOV R0,#LOW (OSRunning)
MOV @R0,#01
POPALL
SETB EA ;开中断
RETI
;-------------------------------------------------------------------------
RSEG ?PR?OSCtxSw?OS_CPU_A
OSCtxSw:
PUSHALL
OSIntCtxSw_in:
;获得堆栈长度和起址
MOV A,SP
CLR C
SUBB A,#OSStkStart
MOV R5,A ;获得堆栈长度
;OSTCBCur ===> DPTR 获得当前
TCB指针,详见
C51.PDF第
178页
MOV R0,#LOW (OSTCBCur) ;获得
OSTCBCur指针低地址,指针占
3字节。
+0类型
+1高
8位数据
+2低
8位数据
INC R0
MOV DPH,@R0 ;全局变量
OSTCBCur在
IDATA中
INC R0
MOV DPL,@R0
;OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;指针占
3字节。
+0类型
+1高
8位数据
+2低
8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是
void指针
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1
;保存堆栈长度
MOV A,R5
MOVX @DPTR,A
MOV R0,#OSStkStart ;获得堆栈起址
save_stack:
INC DPTR
INC R0
MOV A,@R0
MOVX @DPTR,A
DJNZ R5,save_stack
;调用用户程序
LCALL _?OSTaskSwHook
;OSTCBCur = OSTCBHighRdy
MOV R0,#OSTCBCur
MOV R1,#OSTCBHighRdy
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A
;OSPrioCur = OSPrioHighRdy 使用这两个变量主要目的是为了使指针比较变为字节比较,以便节省时间。
MOV R0,#OSPrioCur
MOV R1,#OSPrioHighRdy
MOV A,@R1
MOV @R0,A
LJMP OSCtxSw_in
;-------------------------------------------------------------------------
RSEG ?PR?OSIntCtxSw?OS_CPU_A
OSIntCtxSw:
;调整
SP指针去掉在调用
OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容
;SP=SP-4
MOV A,SP
CLR C
SUBB A,#4
MOV SP,A
LJMP OSIntCtxSw_in
;-------------------------------------------------------------------------
CSEG AT 000BH ;OSTickISR
LJMP OSTickISR ;使用定时器
0
RSEG ?PR?OSTickISR?OS_CPU_A
OSTickISR:
USING 0
PUSHALL
CLR TR0
MOV TH0,#70H ;定义
Tick=50次
/秒
(即
0.02秒
/次
)
MOV TL0,#00H ;OS_CPU_C.C 和
OS_TICKS_PER_SEC
SETB TR0
LCALL _?OSIntEnter
LCALL _?OSTimeTick
LCALL _?OSIntExit
POPALL
RETI
;-------------------------------------------------------------------------
CSEG AT 0023H ;串口中断
LJMP SerialISR ;工作于系统态,无任务切换。
RSEG ?PR?_?serial?OS_CPU_A
SerialISR:
USING 0
PUSHALL
CLR EA
LCALL _?serial
SETB EA
POPALL
RETI
;-------------------------------------------------------------------------
END
;-------------------------------------------------------------------------
文件名
: OS_CPU_C.C
void *OSTaskStkInit (void (*task)(void *pd), void *ppdata, void *ptos, INT16U opt) reentrant
{
OS_STK *stk;
ppdata = ppdata;
opt = opt; //opt没被用到,保留此语句防止告警产生
stk = (OS_STK *)ptos; //用户堆栈最低有效地址
*stk++ = 15; //用户堆栈长度
*stk++ = (INT16U)task & 0xFF; //任务地址低
8位
*stk++ = (INT16U)task >> 8; //任务地址高
8位
*stk++ = 0x00; //PSW
*stk++ = 0x 0A ; //ACC
*stk++ = 0x0B; //B
*stk++ = 0x00; //DPL
*stk++ = 0x00; //DPH
*stk++ = 0x00; //R0
*stk++ = 0x01; //R1
*stk++ = 0x02; //R2
*stk++ = 0x03; //R3
*stk++ = 0x04; //R4
*stk++ = 0x05; //R5
*stk++ = 0x06; //R6
*stk++ = 0x07; //R7
//不用保存
SP,任务切换时根据用户堆栈长度计算得出。
return ((void *)ptos);
}
#if OS_CPU_HOOKS_EN
void OSTaskCreateHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}
void OSTaskDelHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}
void OSTimeTickHook (void) reentrant
{
}
#endif
//初始化定时器
0
void InitTimer0(void) reentrant
{
TMOD=TMOD&0xF0;
TMOD=TMOD|0x01; //模式
1(16位定时器
),仅受
TR0控制
TH0=0x70; //定义
Tick=50次
/秒
(即
0.02秒
/次
)
TL0=0x00; //OS_CPU_A.ASM 和
OS_TICKS_PER_SEC
ET0=1; //允许
T0中断
TR0=1;
}
文件名
: YY.C
#include
#define MAX_STK_SIZE 64
void TaskStartyya(void *yydata) reentrant;
void TaskStartyyb(void *yydata) reentrant;
void TaskStartyyc(void *yydata) reentrant;
OS_STK TaskStartStkyya[MAX_STK_SIZE+1];//注意:我在
ASM文件中设置
?STACK空间为
40H即
64,不要超出范围。
OS_STK TaskStartStkyyb[MAX_STK_SIZE+1];//用户栈多一个字节存长度
OS_STK TaskStartStkyyc[MAX_STK_SIZE+1];
void main(void)
{
OSInit();
InitTimer0();
InitSerial();
InitSerialBuffer();
OSTaskCreate(TaskStartyya, (void *)0, &TaskStartStkyya[0],2);
OSTaskCreate(TaskStartyyb, (void *)0, &TaskStartStkyyb[0],3);
OSTaskCreate(TaskStartyyc, (void *)0, &TaskStartStkyyc[0],4);
OSStart();
}
void TaskStartyya(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\n\t\t*******************************\n");
PrintStr("\t\t* Hello! The world. *\n");
PrintStr("\t\t*******************************\n\n\n");
for(;;){
PrintStr("\tAAAAAA111111 is active.\n");
OSTimeDly(OS_TICKS_PER_SEC);
}
}
void TaskStartyyb(void *yydata) reentrant
{
yydata=yydata;
for(;;){
PrintStr("\tBBBBBB333333 is active.\n");
OSTimeDly(3*OS_TICKS_PER_SEC);
}
}
void TaskStartyyc(void *yydata) reentrant
{
yydata=yydata;
for(;;){
PrintStr("\tCCCCCC666666 is active.\n");
OSTimeDly(6*OS_TICKS_PER_SEC);
}
}
重入问题的解决:
任 务函数中带有形参和局部变量时若使用
reentrant关键字会引起重入,从
C51.PDF 129-131页的内容知:为了函数重入,形参和局部变量必须保存在堆栈里,由于
51硬件堆栈太小,
KEIL将根据内存模式在相应内存空间仿真堆栈
(生长 方向由上向下,与硬件栈相反
)。对于大模式编译,函数返回地址保存在硬件堆栈里,形参和局部变量放在仿真堆栈中,栈指针 为
?C_XBP,
XBPSTACK=1时,起始值在
startup.a51中初始化为
FFFFH+1。仿真堆栈效率低下,
KEIL建议尽量不用,但为了重 入操作必须使用。
KEIL可以混合使用
3种仿真堆栈
(大、中、小模式
),为了提高效率,针对
51推荐统一使用大模式编译。
为了支持重入,重新设计 了堆栈结构
(如下图
)。增加了保存仿真堆栈指针
?C_XBP和堆栈内容的数据结构。相应改变的文件有:
OS_CPU_A.ASM、
OS_CPU_C.C、
OS_CPU.H、
YY.C。由图可知,用户栈中保存的仿真栈与硬件栈相向生长,中间为空闲间隔,显然
uCOSII的堆栈检测函数失效。硬件栈的保存恢复 详见上节,仿真堆栈的保存与
8086移植中的一样,
OS只提供堆栈空间和只操作堆栈指针,不进行内存拷贝,效率相对很高。
建议使用统一的固定大小的堆栈空间,尽管
uCOSII原作者把不同任务使用不同空间看成是优点,但为了在
51上有效实现任务重入,针对
51笔者还是坚持不使用这个优点。
用 户堆栈空间的大小是可以精确计算出来的。用户堆栈空间
=硬件堆栈空间
+仿真堆栈空间。硬件栈占用内部
RAM,内部
RAM执行效率高,如果堆栈空间过大,会 影响
KEIL编译的程序性能。如果堆栈空间小,在中断嵌套和程序调用时会造成系统崩溃。综合考虑,我把硬件堆栈空间大小定成了
64字节,用户根据实际情况 可以自行设定。仿真堆栈大小取决于形参和局部变量的类型及数量,可以精确算出。因为所有用户栈使用相同空间大小,所以取占用空间最大的任务函数的空间大小 为仿真堆栈空间大小。这样用户堆栈空间大小就唯一确定了。我将用户堆栈空间大小用宏定义在
OS_CFG.H文件中,宏名为
MaxStkSize。
51 的
SP只有
8位,无法在
64K空间中自由移动,只好采用拷贝全部硬件堆栈内容的笨办法。
51 本来就弱,这么一来缺点更明显了。其实,引入
OS必然要付出代价,一般
OS要占用
CPU10%-20%的负荷能力,请权衡利弊决定。切换频率决定了
CPU 的耗费,频率越高耗费越大,大到一定程度就该换更强的
CPU了。我选了
50Hz的切换频率,不高也不低,用户可以根据需要自行定夺。在耗费无法避免的情况 下,我采取了几个措施来提高效率:
1。
ret和
reti混用减少代码;
2。
IE、
SP不入出栈,通过另外方式解决;
3。用
IDATA关键字声明在汇编中用 到的全局变量,变
DPTR操作为
Ri操作;
4。设计堆栈结构,简化算法;
5。让串口输入输出工作在系统态,不占用任务
TCB和优先级,增加弹性缓冲区,减 少等待。
在
51单片机上硬件仿真
uCOS51的说明:
zyware网友
2002/11/22 来信询问
uCOS51在单片机上的硬件仿真问题,具体情况是
“在
51上用
uCOS51核,以及一些构件,
keilc上仿真通过,用
wave接硬件仿真程序乱飞,
wave仿真以前的程序没有问题,不知是何缘故
”。
由 于我的
OS程序已经在
KEIL软件仿真和硬件上实际测试过,所以不可能是程序错。可能的原因只能是硬件仿真软件设置问题。本人用的是
Medwin软件,在
Insight上调试,使用
uCOS51编译测试程序一样跑飞。即使添加修改后的
startup.a51(详见《在
51单片机上固化
uCOS51的说 明》
)也不正常。我发现
Medwin似乎没有编译
startup.a51,因为它把该文件加在了
other Files目录下而不是
source Files目录,于是我猜测只有放在
source Files目录下的文件才被编译。由观察知,以
.c和
.asm做后缀的文件均被放在此目录下且被编译。于是我立即将
startup.a51改成
startup.asm并加入项目编译,结果测试正常。不必担心
startup改名造成冲突,
KEIL在链接目标文件时会自动处理重名段,本目录的文件优 先级高
(我是这么理解的,具体原理不清楚,这只是根据实践得到的结论,希望了解此处理过程的朋友能告之,不胜感激。
)。
具体做法如下:
1。按《在
51单片机上固化
uCOS51的说明》一文修改
startup.a51,并将其更名为
startup.asm。
2。将
startup.asm、
yy1.c、
os_cpu_c.c、
ucos_ii.c、
os_cpu_a.asm五个文件加入项目编译。
3。运行
在
51单片机上固化
uCOS51的说明:
近 来,收到多位网友来信询问
uCOS51在
51单片机上的固化问题,归纳其焦点就是:为什么
OS在
KeilC51上模拟可以正常运行,但把它烧录在
CPU上 却不能工作?理论上,程序在软件仿真通过测试后,将其烧录在硬件上,硬件调试应该一次成功。许多网友也有这个经验,可为什么在调试
uCOS51时失效了 呢?难道操作系统调试很特殊吗?
其实问题出在重入函数的引入。原来
KEILC51软件仿真在不修改
startup.a51文件的情况下,缺剩使用
64K外部
RAM,它把
0000H-FFFFH全部仿真为可读写的
RAM,而用户的硬件系统可能没有用到那么大的
RAM空间,比如只用了
8K/16K /32K等,或者用户把一些地址空间映射给了别的设备,比如
8019AS等。在没有调用
OSTaskCreate前,定义为
reentrant的函数将用
FFE0H做仿真堆栈栈顶指针,而此处在用户的系统里不是
RAM,造成程序跑飞。比如在我的用户板上,将
FE00H-FFFFH空间的一部分分配给
8019AS使用,如果把
demo程序编译后直接烧到
51上,将不能运行。解决办法是根据系统
RAM配置,修改
startup.a51文件,并将其加入项 目编译,如下所示:
XBPSTACK EQU 1 ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 07FFFH+1; set top of stack to highest location+1.
按 此修改后,在有
32K外部
RAM的系统上可以正常运行。用户可根据自己
XRAM的实际配置情况修改
startup.a51相关参数,并将其添加到项目里编 译。不必理会
KEIL/C51/LIB目录下的同名文件,此处的
startup.a51优先级高,
KEIL将按此处该文件的配置编译项目。
这也解释了有些网友问到的,
“为什么加入
reentrant关键字,在软件仿真时正确,烧在芯片上就死机,去掉
reentrant后两者都正常
”的问题。由于大多数人很少使用重入函数,往往不了解这个细节,特此提请大家注意。
关 于
uCOS51不能正常工作的原因还可能是因为串口波特率和
OS_TICKS_PER_SEC及
TH0、
TL0设置不正确引起的。
demo程序默认使用
22.1184MHz晶体,
19200波特率,切换频率为
50Hz。为此,
1。在
SERIAL.C中设置
“TL1=0xFD;TH1=0xFD;”使波特 率为
19200;
2。在
OS_CPU_C.C和
OS_CPU_A.ASM中设置
“TH0=0x70;TL0=0x00;”使时钟节拍
tick=50次
/ 秒;
3。在
OS_CFG.H中设置
OS_TICKS_PER_SEC为
50Hz。用户应根据实际情况,相应地修改这些参数,否则运行不正确。
定时器初值设置:
定时器
0用于时钟节拍发生器
//*****************************************************************************
//初值计算公式:
// (2^16-x)*F=Fosc/12
// 其中:
F=时钟节拍频率
tick;
Fosc=晶体或晶振频率;
x=初值;
// 本例中,
F=50;
Fosc=21.1184MHz;所以
x=0x7000。
//*****************************************************************************
定时器
1用于波特率发生器
//*****************************************************************************
//初值计算公式:
// TH1=256-(2^SMOD/32*Fosc/12*1/Bound)
// 其中:
SMOD=0,
1;
Fosc=晶体或晶振频率;
Bound=波特率
// 本例中,
SMOD=0;
Fosc=21.1184MHz;
Bound=19200,所以
TH1=0xFD。
//*****************************************************************************
demo程序项目中增加按如上方法改写的
startup.a51后,在我的用户板硬件上运行正确。
为
uCOS51增加
Shell界面:
uCOSII 只提供了操作系统内核,用户要自己添加文件处理、人机界面、网络接口等重要部分。其中
Shell(人机界面
)提供了人与机器交互的界面,是机器服务于人的 体现,是系统必不可少的重要组成部分。现代的很多
OS如
UNIX、
DOS、
VxWorks都提供了友好的命令行界面。
Windows更是提供了
GUI。大 部分人认识
OS都是从这里开始的。
uCOS51同样拥有
Shell,它是我从以前写的前后台程序中移植过来的。
命令行
Shell的工作原理比较简单,主要思路就是单片机接收用户键盘输入的字符存入命令缓冲区,并回显到屏幕,当用户按下回车键,触发软件状态机状态变迁,从输入态转移到命令解释态,然后根据用户命令调用相关子程序执行相应操作,执行完毕后重新回到输入态。
我感觉原理很好掌握,程序也不长,但是细节部分要反复调试多次才能稳定工作。比如:命令行左右边界的保护、退格键的处理、词表的设计等等。
Shell程序由词表、取词子程序、状态机框架程序
(输入回显和命令解释执行
)、命令相关子程序组成
(详见源程序清单
)。
词表结构如程序清单所示,由词数目,左括号数,右括号数,每个词的具体信息(长度,字符串)构成。左右括号数用于括号匹配检查;词数目用于程序循环;词的具体信息作为解释
/执行程序的输入参数。
取 词子程序从命令行语句中提取单词并存入词表同时进行匹配检查和词法分析。默认字符为:
0-9、
a-z、
A-Z、
'.';定界符为:空格、逗号,左
/右括 号。建议用户补充默认字符集
(? / \ -)以便实现更灵活的语法。注意:现在版本的
Shell只检查左右括号数量的匹配,无优先级和语法含义。
输 入回显程序循环检查用户键盘输入。如果输入回车,程序状态转入解释执行态;如果输入退格
(8)则回显退格、空格、退格,模拟删除字符,同时输入缓冲区清除 相应字节,清除前先检查左边界是否越界。如越界则鸣响报警且不执行清除操作;其他字符输入直接存入输入缓冲区并回显,此前检查右边界是否溢出,如果溢出则 鸣响报警且抛弃刚输入的字符。
命令解释程序调用取词子程序分析用户命令行输入,根据词表第一个单词在散转表中的位置调用相应执行子程序处理命令,如果散转表中无此单词,则打印
“Bad command!
”。取词子程序返回错误指示时也打印此句。
命令解释程序向相应的命令相关子程序传入词表指针,具体执行由用户自行决定。由命令相关子程序返回后重新回到命令输入态,完成一次输入执行全过程。此过程周而复始地循环执行。
Shell界面的命令按功能分为以下几组:
1。操作系统相关命令:
查看就绪任务
lt / 中止任务
kill / 恢复任务执行
call / CPU利用率
usage / 版本查询
ver / 查某个任务信息
(TCB、堆栈内容
)lt
查看切换次数和时间
lts
2。网络相关命令:
显示配置
MAC地址
macadr / 显示配置主机
IP地址
host / 显示配置子网掩码
mask / 显示配置缺省网关
gateway
显示网络配置总情况
lc / 连通测试命令
ping / 用户数据报发送命令
udp / telnet命令
tel / 相关应用命令
**
显示
ARP高速缓冲区地址对
ls / 显示发送缓冲区信息
lti
3。屏幕显示相关命令:
清屏
clr / 帮助
help / 功能键
F3、
F7处理
/ 组合键
Ctrl+C、
Ctrl+B处理
4。外设
(闪盘
X5045和
I/O口
)相关命令:
读闪盘
rdx / 读
I/O口
rdp / 写闪盘
wdx
5。安全相关命令:
身份认证密码权限
usr、
pass
6。应用相关命令:
用户自行定义
用户命令大小写不敏感,程序将命令字符串统一成小写形式。程序中各种参数
(如:最大词长度、词数量
……)定义成宏放在一个头文件中,随时可修改配置,很方便。
Shell作为一个任务工作于内核之外,占用一个任务号。
源程序:
词表
typedef struct{
int Num;
int LeftCurveNum,RightCurveNum;
struct{
int Length;
unsigned char Str[MaxLenWord+1]; /*for '\0'*/
} wt[MaxLenWordTable];
} WORDTABLE;
取词
bit GetWord(unsigned char *ComBuf,WORDTABLE *WordTable)
{
int i=0; /*ComBuf String pointer*/
int j=0; /*Length of Word */
int k=-1; /*The number of WordTable*/
int StrFlag=0; /*There is "0-9/a-z/A-Z" before " ,()"*/
int SentenceEndFlag=0; /*Sentence end*/
char ch;
WordTable->Num=0;
WordTable->LeftCurveNum=0;
WordTable->RightCurveNum=0;
ch=ComBuf[0];
while(!SentenceEndFlag&&i<MAXLENCOMBUF){
if((ch>='0'&&ch<='9')||(ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z')||(ch=='.')){
if(StrFlag==0){
StrFlag=1;k=k+1;j=0;
if(k>=MaxLenWordTable) return 0;
WordTable->wt[k].Str[j]=ch;
WordTable->Num=k+1;
}
else{
j=j+1;
if(j>=MaxLenWord) return 0;
WordTable->wt[k].Str[j]=ch;
}
}
else if(ch==' '||ch==','||ch=='('||ch==')'||ch=='\0'){
if(ch=='(') WordTable->LeftCurveNum++;
if(ch==')') WordTable->RightCurveNum++;
if(StrFlag==1){
StrFlag=0;j=j+1;
WordTable->wt[k].Str[j]='\0';
WordTable->wt[k].Length=j;
}
if(ch=='\0') SentenceEndFlag=1;
}
else{
return 0;
}
i=i+1;
ch=ComBuf[i];
}
if(i
if(WordTable->LeftCurveNum==WordTable->RightCurveNum) return 1;
else return 0;
}
else{
return 0;
}
}
输入回显和命令解释执行
void yyshell(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\t\t***********************************************\n");
PrintStr("\t\t* Welcom to use this program *\n");
PrintStr("\t\t* Author:YangYi 20020715 *\n");
PrintStr("\t\t***********************************************\n\n\n");
/*Login & Password*/
PrintStr("% ");
while(!ShellEnd){
switch(State){
case StatInputCom:{
if(yygetch(&ch)){
if(ch==13) /*Enter return key*/
{
PrintStr("\n");
ComBuf[i+1]='\0';
if(i+1==0) PrintStr("% ");
else
State=StatExeCom;
}
else{
i=i+1;
if((i>=MaxLenComBuf)&&(ch!=8)){
PrintChar(7);
i=MaxLenComBuf-1;
}
else{
if(ch==8){
i=i-2;
if(i<-1) {i=-1;PrintChar(7);}
else{
PrintChar(8);
PrintChar(' ');
PrintChar(8);
}
}
else{
PrintChar(ch);
ComBuf[i]=ch;
}
}
}
break;
}
else{
//OSTimeDly(10);
break;
}
}
case StatExeCom:{
if(GetWord(ComBuf,&WordTable)==1&&WordTable.Num!=0){
yystrlwr(WordTable.wt[0].Str);
for(tem=0;tem<MAXCOMNUM&&!COMMATCHFLAG;TEM++)
if(yystrcmp(WordTable.wt[0].Str,ComTable[tem])==0) ComMatchFlag=1;
if(ComMatchFlag){
tem--;
switch(tem){
case 0:{DisplayTask(&WordTable);break;}
case 1:{Kill(&WordTable);break;}
case 2:{PingCommand(&WordTable);break;}
case 3:{UDPCommand(&WordTable);break;}
case 4:{CfgHost(&WordTable);break;}
case 5:{CfgMask(&WordTable);break;}
case 6:{CfgGateway(&WordTable);break;}
case 7:{
//ShellEnd=1;
PrintStr("\n\tThis Command is limited!\n\n");
break;
}
case 8:{PrintConfig(&WordTable);break;}
case 9:{clrscr();break;}
case 10:{DisplayHelpMenu(&WordTable);break;}
}
}
else
PrintStr(" Bad command!\n\n");
}
else{
if(WordTable.Num) PrintStr(" Bad command!\n\n");
}
ComMatchFlag=0;
State=StatInputCom;
if(ShellEnd) {PrintStr("\n\n");}
else PrintStr("% ");
i=-1;
break;
}
default:{
//ShellEnd=1;
PrintStr("System fatal error!\n");
PrintChar(7);PrintChar(7);PrintChar(7);
}
}
}
}
以 上是我这次移植
uCOS51的一些心得,写出来只是让准备在
51上运行操作系统的同行们少走弯路并增强使用信心。我强烈推荐大家在自己的
51系统中使用
uCOS这个简单实用的自己的操作系统。它的大小应该不是问题,性能上的提高却是显著的。但愿此文能对朋友们有所帮助,错误在所难免,希望读者指正。