STC单片机uC/OS-II移植记(3):整理移植源
本帖最后由 杨为民 于 2024-3-1 00:15 编辑一、前言uC/OS-II 实时多任务操作系统是最适合移植到STC的8051(包括32位的8051单片机STC32G/F系列)系列单片机上的RTOS。首先uC/OS-II是一款经典的有广泛生态基础的RTOS,其次是其程序结构是可裁剪和定制的,非常适合在STC这样有众多子型号的单片机上使用。比如从小到家电控制到大到工业控制的应用领域,只需要对uC/OS进行裁剪和设置就可以得到适合的RTOS平台。STC32G/F系列单片机的内核是一款扩展了80251指令集的32位单片机,目前使用Keil的C251编译器。当C251编译器设置为“Xsmall”模式时,程序缺省的数据空间是EDATA空间,对于这个空间变量可以实现一次32位的存取,速度极快,运行RTOS,任务切换的时间可以达微秒量级。STC官方推荐了一款资深专家提供的在STC8系列单片机上运行的uC/OS-II RTOS学习代码(以下简称uCOS学习版),与笔者上文介绍的陈是知先生的移植版相比较,uCOS学习版考虑更周全,也更适合RTOS的入门学习。比如在uCOS学习版中,虽然C51项目的RAM存储设置已经是大模式了,但是移植者仍然在每个函数定义中都加上了“largereentrant”关键字,强制定义这个函数为使用XDATA内存的可重入的函数。比如在uCOS学习版中,除了程序,还给出了“Protues 仿真”文件,在“DOC”的子目录里还给出了原始的官方的“uC/OS-II V2.52”开源代码和说明文件。本系列文章将分步骤介绍把uCOS学习版移植到STC32G/F单片机上的全过程,希望读者从移植的过程中,深入了解宏晶公司的STC8位单片机与STC32位单片机的差别,深入了解Keil公司的C51与C251的差别以及深入了解RTOS任务调度的原理和实现方法。 二、提取移植源程序。uCOS学习版压缩包(可以从STC官网上下载)中包含了很多资料,为了使读者将关注点集中在移植过程上,有必要将无关的参考资料删除,提取出一个“纯”的移植项目。另外原程序使用串口通讯作为多任务的例子,笔者也将会取消掉,改为LED这样直观的显示设备。这么做的原因是多个任务共同使用一个输出设备,存在一系列操作系统理论中碰到的防死锁的方法,解决的具体方法各有不同,因此不适合作为移植单片机RTOS的例子。附件中“01_学习版_uCOSII_STC8”是原始的uCOS学习版程序资料,附件中“02_打狗棒_uCOSII_STC8H”是提取后的移植源程序。(1)移植源与原始的比,主要有以下不同:去掉了供移植外的文件,去掉了与串口相关的程序,将所有程序重新划分了子目录,修改了包含文件路径。 三、整理移植源程序每个程序员都有自己的程序风格,为了保持一致性,需要将uCOS学习版的移植源程序的修改为与笔者前一篇文章一致的风格。附件中“03_整理移植源程序”是整理后的移植源程序。与整理前的比,主要有以下不同:(2)将单片机头文件改为“STC8H.H”,在“OS_CPU_A.A51”文件里直接定义“SP”、“PSW”等8051寄存器,以便将来与80251对接。 (3) 将主函数的定义从“int main(void) {”修改为“void main(void) {”,并且取消了“return OK;”语句。这是因为与DOS程序不同,单片机程序中的主函数是不允许退出的,强行退出会导致程序跑飞。 (4)修改了任务堆栈的大小。在uCOS学习版中堆栈的定义是:OS_STKAppStartStk;OS_STKPowerLedStk;OS_STKUartRcvStk;OS_STKUartSendStk;其中:“TASK_STK_SIZE”的定义为“#define OS_TASK_STAT_STK_SIZE 512”,大小为512字节。但是主程序在任务定义中使用了任务堆栈初始化函数“psp = OSTaskStkInit(task, p_arg, ptos, 0u);”,而在任务堆栈初始化函数中,函数重入指针的设置为: *stk++=(INT16U)(ptos+MAX_STK_SIZE)>> 8; /* ?C_XBP 仿真堆栈指针高8位 */ *stk++=(INT16U)(ptos+MAX_STK_SIZE) &0xFF; /* ?C_XBP 仿真堆栈低8位 */其中使用的堆栈长度为“MAX_STK_SIZE”,其定义为“#define MAX_STK_SIZE1024”,与用户任务堆栈尺度“TASK_STK_SIZE=512”的定义无关,所有任务的堆栈都是1024个字节的。在uCOS学习版中,用户定义的堆栈尺寸小于实际堆栈的定义“MAX_STK_SIZE=1024”,将产生“堆栈覆盖”的严重隐患。只是由于其范例的特殊性,凑巧运行时没有发生问题而已。因此在整理时统一改为“MAX_STK_SIZE=1024”的大小:OS_STKTask_STK_A;OS_STKTask_STK_B;OS_STKTask_STK_C; (5)uCOS学习版建立任务采用了PC版的动态方法,先建立一个用户启动任务“AppStart”,然后在这个任务中再建立其他的用户任务,建立完了就把启动任务自己删除了。使用这种方法的优点是有一个首先执行的任务,然后由这个任务进行用户系统软硬件的初始化和根据需要动态建立和运行任务。如果有DOS的支持,可以在运行中动态地分配和回收RAM内存,这确实是好方法。但是像目前这样的STC8H和STC32G/F单片机,没有DOS的支持,采用裸机编程方法,编译好程序后,一次性将代码烧录到单片机情况,笔者认为还是采用静态建立任务的方法为好。 四、主程序说明整理后的主程序“main.c”分为4个部分,下面分别介绍。(6)实时任务定义:
本范例定义三个任务,任务函数必须声明为可重入的,这样其所有的变量都定义在任务堆栈中,在内存中建立起一个运行框架。
(7)硬件初始化:
由于本范例的实时任务只采用了系统中断进行任务调度,而总中断控制“EA”被用于临界区保护,所以第55行关闭定时器,就禁止了任务调度。这样避免了在下面的RTOS系统的初始化没有完成的时候突然产生中断发生意外。
(8)RTOS系统初始化部分:
第61到地63行定义了三个任务,任务的优先级直接定义为2、3、4,其中优先级2为最高优先级。第66行开始任务调度后,最高优先级任务A开始执行。第76行让定时器开始工作,一旦第81行程序任务A放弃CPU执行权开始休眠,任务调度程序就会切换到任务B继续执行,等到定时器0产生的系统中断任务调度程序判断任务A休眠时间已到,就会让任务A从第82行开始执行下面程序。
(9)实时多任务部分:
虽然任务B和任务C在建立任务后已经处于就绪状态,但是与任务A不同,没有经过任务调度允许就不会自动执行。每个任务被调度执行有三个条件,首先是处于就绪状态,其次在处于就绪状态的多个任务,它的优先级最高,最后是需要任务调度器将任务切换给它。附件一:是本程序运行时的效果:
五、主程序的遗憾通常在主程序中应该明显地看到对整个程序运行的控制。除了系统定时器0中断以外,如果还有其他的中断,比如定时器1,除了要打开它本身的设备中断允许,还要打开中断允许标志“EA”。
在单片机程序设计中,明显关闭和打开中断“EA”,是一个基本的编程要求。但是在主程序中看不到“EA”在什么地方被打开, 也就不知道定时器1是在什么时候开始中断工作的,这是不能不说很大的遗憾。
期待继续,讲透。 牛,也期待后续讲解,学习了!! 期待杨老师的后续STC全力支持 可以用到STC32上吗? 能不能够移植到uC/OS-III 实时多任务操作系统? 这个实时多任务操作系统能够支持,无限量任务。
本帖最后由 熊仔 于 2023-7-30 19:57 编辑
其中使用的堆栈长度为“MAX_STK_SIZE”,其定义为“#define MAX_STK_SIZE1024”,与用户任务堆栈尺度“TASK_STK_SIZE=512”的定义无关,所有任务的堆栈都是1024个字节的。
对这句话表示有疑问。
#define MAX_STK_SIZE 1024 /* Max. stack size */
#define OS_MAX_EVENTS 5 /* Max. number of event control blocks in your application ...*/
/* ... MUST be > 0 */
#define OS_MAX_FLAGS 2 /* Max. number of Event Flag Groups in your application ...*/
/* ... MUST be > 0 */
#define OS_MAX_MEM_PART 5 /* Max. number of memory partitions ... */
/* ... MUST be > 0 */
#define OS_MAX_QS 2 /* Max. number of queue control blocks in your application ...*/
/* ... MUST be > 0 */
#define OS_MAX_TASKS 11 /* Max. number of tasks in your application ... */
/* ... MUST be >= 2 */
#define OS_TICK_STEP_EN 0u /* Disable tick stepping feature for uC/OS-View */
#define TASK_START_PRIO 2 /* App start prio */
#define OS_LOWEST_PRIO 12 /* Defines the lowest priority that can be assigned ... */
/* ... MUST NEVER be higher than 63! */
//#define TASK_STK_SIZE 512 /* Default task stack size */
#define OS_TASK_IDLE_STK_SIZE 512 /* Idle task stack size (# of OS_STK wide entries) */
#define OS_TASK_STAT_EN 0 /* Enable (1) or Disable(0) the statistics task */
#define OS_TASK_STAT_STK_SIZE 512 /* Statistics task stack size (# of OS_STK wide entries) */
#define OS_TASK_PROFILE_EN 0u /*Not include variables in OS_TCB for profiling */
注意#define OS_TASK_IDLE_STK_SIZE 512和#define OS_TASK_STAT_STK_SIZE 512
这两个是512的。所以应该设置都一样。
正确做法应该全部定义512或者256。如果全部定义1024也过大了,51单片机xdata太小了没必要。
#define MAX_STK_SIZE 512/* Max. stack size */
#define TASK_STK_SIZE 512 /* Default task stack size */
其实这个移植的代码,规定任务一样大小,本来就有问题。当然也是最简单的做法。
笔者认为应该可以弄不同的任务堆栈。
由于初始化堆栈函数和新建任务函数没有堆栈大小入口,导致不能传参
OS_STK *OSTaskStkInit (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT16U opt)由于任务新建函数
OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK*ptos,
INT8U prio)通过看代码还发现一个扩展版本任务新建函数,有提供堆栈大小(stk_size)。函数入口参数确实很多,允许指定有关任务的附加信息,该函数用于让uC/OS-II 管理任务的执行。
INT8UOSTaskCreateExt (void (*task)(void *p_arg),
void *p_arg,
OS_STK*ptos,
INT8U prio,
INT16U id,
OS_STK*pbos,
INT32U stk_size,
void *pext,
INT16U opt)方法一:
初始化堆栈函数,有一个形参opt 是没有用到的。我们是否可以借用这个参数做堆栈大小入口呢?
这个就涉及修改原函数的功能。
1,对新建任务函数增加一个入口参数 OSTaskCreate (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT16U stk_size,
INT8U prio)2,初始化堆栈传参
OSTaskStkInit(task, p_arg, ptos, stk_size);3,修改初始化堆栈函数
OS_STK *OSTaskStkInit (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT16U opt) large reentrant
{
OS_STK *stk;
p_arg=p_arg;
opt =opt; /* opt没被用到,保留此语句防止警告产生 */
stk =(OS_STK *)ptos; /* 任务堆栈最低有效地址 */
*stk++=15; /* 任务堆栈长度 */
*stk++=(INT16U)task & 0xFF; /* 任务代码地址低8位 */
*stk++=(INT16U)task >> 8; /* 任务代码地址高8位 */
/* 处理器是按特定的顺序将寄存器存入堆栈的,所以用户在将寄存器存入堆栈的时候也要依照这一顺序 */
*stk++=0x00; /* PSW */
*stk++=0x0A; /* 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,任务切换时根据用户堆栈长度计算得出 */
*stk++=(INT16U)(ptos+opt) >> 8; /* ?C_XBP 仿真堆栈指针高8位 */
*stk++=(INT16U)(ptos+opt) & 0xFF; /* ?C_XBP 仿真堆栈低8位 */
return ((void *)ptos); /* 返回最低地址,这里不用弹出栈顶指针是为了提高计算效率 */
}
方法二:
不改变原函数,新增对仿真堆栈XBP的操作的函数
1,初始化函数 void OSTaskXBPInit(OS_STK *ptos,OS_STK xdata * XBPTask,INT16U Size)
2,对Idle task分配XBP堆栈
3,如果使能OS_TASK_STAT_E=1,对Statistics task 分配XBP堆栈
4,初始化堆栈函数需要改写
熊仔 发表于 2023-7-30 14:53
其中使用的堆栈长度为“MAX_STK_SIZE”,其定义为“#define MAX_STK_SIZE1024”,与用户任务堆栈尺度“TA ...
谢谢讨论,我的回复较长,专门开一楼答复 本帖最后由 杨为民 于 2023-7-30 17:41 编辑
关于uC/OS-II中的等长任务堆栈问题
(1)关于在uC/OS-II中使用“OSTaskCreate”建立任务时的任务堆栈是等长的,我认为是移植者忠实地遵照了uC/OS-II原作者的设计意图,因为uC/OS-II还有一个建立任务的函数“OSTaskCreateExt”是用于建立不等长堆栈的,见下图:
因此为了保持uC/OS-II的整体架构完整,将uC/OS-II移植到8051单片机的移植者没有擅自对“OSTaskStkInit”堆栈初始化函数的功能和接口进行修改。我也是这个观点,因此在将uC/OS-II移植到STC8H和STC32G上的时候,也都没修改“OSTaskStkInit”堆栈初始化函数的功能和接口(见本论坛我的系列帖子)。
(2)uC/OS的作者已经注意到这个在单片机小资源的特色,新的uC/OS-III版本已经解决了这个问题,在uC/OS-III中,新的“OSTaskStkInit”定义如下:
新的“OSTaskCreate”建立任务函数定义如下:
因此在uC/OS-III中已经从根本上支持不等长的任务堆栈了。等到将来把uC/OS-III移植到STC单片机上来,这个问题自然就解决了。(3)当然对原始代码进行修改甚至改变部分架构也是移植的一个部分,也是学习研究RTOS的一种很好的方法。因此支持坛友进行尝试,推出自己的移植版本。
(4)我自己写的所有RTOS,任务堆栈都是可以自定义长度的,比如我在本版块的文章“长缨x51(1)STC单片机软件工具链”(https://www.stcaimcu.com/forum.p ... 2490&extra=page%3D2)中介绍的“泰山x51-RTOS”,任务堆栈就可以是不等长的。只是为了和其他我的uC/OS-II移植文章做对比,文中范例的任务堆栈是等长的。
uCOS-II 在其他芯片是支持不等长任务堆栈的。
只是作者设计这个os的时候没有考虑到8051这么奇葩的data空间。内部ram太少了。而且8051硬件栈是向上增长,仿真栈是向下的。
要移植到8051还是需要下一番功夫。
OSTaskCreateExt这个新建任务函数方式,我也看了,有提供堆栈大小(stk_size)。函数入口参数确实很多,允许指定有关任务的附加信息。
该函数主要用于让uCOS-II 管理任务的执行。比如后期堆栈使用量的统计。
但是这个函数也不支持8051仿真栈的处理。8051使用还是需要等长的。
要想uCOS-II更好的支持8051,必须另想方法,我说的两种方法理论可行。
实际使用中很多任务分配64字节足够。有些重量级的任务需要512或者更多。
当前移植的是等长设计只是最简单的移植做法罢了,是有很大瑕疵的。都用512字节,8k xdata空间,不到16个任务。
支持不等长任务堆栈,这个才是较为完美的系统。
当然uCOS-III已经支持堆栈数量入口。也说明作者已经考虑到之前的有不足的地方,改进了。
uCOS-III已经开源了,协议改成license改为Apache2 。