简介:这份资源是Micrium官方发布的uC/OS-III实时操作系统源代码压缩包,面向嵌入式开发工程师、高校学生及RTOS学习者,用于在工业控制、医疗设备、汽车电子等场景中完成系统移植、裁剪与二次开发。包内共28个文件,以21个C源文件和5个头文件为主体,构成内核调度、任务管理、信号量、消息队列、定时器等核心模块,另附1份PDF文档与1份TXT说明,便于查阅API与配置方法,整体约3.03MB,结构紧凑。目前已有845人学习下载,说明其在嵌入式社区中具备一定参考价值。通过研读源码,读者可深入理解抢占式调度、内存堆管理与任务间同步机制,并基于官方示例完成向目标硬件平台的移植与驱动适配,为实际项目开发打下基础。
1. 拿到 ucos III 源代码压缩包之后,先别急着解压
很多做嵌入式的朋友第一次接触 ucos III,是在项目里被 RTOS 逼到墙角的时候——裸机跑不动了,任务调度开始互相踩脚,中断里延时导致丢数据,这时候才想起来找个实时操作系统。而 ucos III 源代码官方下载压缩包,就是大多数人接触这个内核的第一步。它不是一个安装程序,也不是一个 IDE 工程,而是一份完整的、可裁剪的、带移植层的 C 源码集合,里面包含了内核核心、CPU 移植层、板级支持包示例以及一堆文档。你拿到它,意味着你可以在自己的 MCU 上从零构建一个可抢占的实时内核,而不是被某个厂商的 SDK 绑死。适合谁?适合已经能写裸机驱动、理解中断和栈概念、但还没把 RTOS 跑起来的工程师。如果你连启动文件都没改过,这份源码会让你有点难受,但跟着走也能啃下来。
2. 解压后先看目录:ucos III 源码包的骨架与移植层选型
2.1 压缩包里到底有什么
把下载到的压缩包解压到一个没有中文、没有空格的路径下,比如D:\rtos\ucos3_src。你会看到类似这样的顶层结构:
| 目录/文件 | 作用 |
|---|---|
Source/ | 内核核心源码,与 CPU 无关 |
Ports/ | 各架构的移植层,按 CPU 架构分文件夹 |
EvalBoards/ | 官方评估板示例工程,含 BSP |
Doc/ | 内核 API 手册、移植指南、发布说明 |
LICENSE | 授权说明,商用需留意 |
Source/里最关键的几个文件是os_core.c、os_task.c、os_time.c、os_sem.c、os_q.c、os_flag.c、os_mutex.c、os_tmr.c、os_int.c、os_cfg.c、os_prio.c、os_stat.c、os_trace.c、os_var.c、os.h、os_type.h、os_cfg.h。这些文件构成了内核的全部调度、通信、时间管理能力。Ports/下面按架构分,比如ARM-Cortex-M3/、ARM-Cortex-M4/、ARM-Cortex-M7/、RX/、SH/、PowerPC/等。每个移植层里通常有os_cpu.h、os_cpu_a.asm(或.s)、os_cpu_c.c、os_dbg.c。选型时先确认你的 MCU 内核属于哪个架构,再进对应文件夹,不要拿 M3 的移植层往 M4 上硬套,虽然指令集兼容,但 FPU 上下文和中断优先级配置有差异。
2.2 移植层选型:Cortex-M 系列怎么挑
以 Cortex-M 为例,Ports/ARM-Cortex-M3/Generic/IAR/和Ports/ARM-Cortex-M4/Generic/RealView/这种路径很常见。路径里的Generic表示通用移植,不绑定具体芯片;IAR、RealView、GNU表示工具链。你用的编译器决定了选哪个子文件夹。常见做法是:先看你的 IDE 用的是 IAR EWARM、Keil MDK(ARMCC/ARMCLANG)还是 GCC,然后进对应工具链目录,把os_cpu.h、os_cpu_a.asm、os_cpu_c.c三个文件拷到你的工程里。注意,os_cpu_a.asm的汇编语法在不同工具链下完全不同,IAR 用自己的一套伪指令,GNU 用 GAS 语法,不能混用。如果你用的是 Keil MDK 且编译器是 ARMCLANG(AC6),优先找RealView或ARM目录,不要拿GNU目录里的.S文件直接塞进去,否则编译报错能让你怀疑人生。
2.3 内核配置文件os_cfg.h怎么改
os_cfg.h是裁剪内核的开关面板。默认配置通常打开了大部分功能,但你的 MCU 可能 RAM 不够。常见做法是先把不用的功能关掉,比如:
/* os_cfg.h 片段:按需裁剪内核功能 */ #define OS_CFG_TASK_DEL_EN 0u /* 不需要删除任务,省代码 */ #define OS_CFG_TASK_CHANGE_PRIO_EN 0u /* 不需要动态改优先级 */ #define OS_CFG_Q_EN 0u /* 不用消息队列 */ #define OS_CFG_SEM_EN 1u /* 用信号量,保留 */ #define OS_CFG_MUTEX_EN 1u /* 用互斥量,保留 */ #define OS_CFG_TMR_EN 1u /* 用软件定时器,保留 */ #define OS_CFG_STAT_TASK_EN 0u /* 不用统计任务,省 RAM */ #define OS_CFG_TASK_PROFILE_EN 0u /* 不用任务 profiling */逻辑说明:每个OS_CFG_xxx_EN宏控制对应模块的代码是否参与编译。关掉后,os_xxx.c里对应函数会被条件编译剔除,ROM 和 RAM 都会降。参数说明:OS_CFG_TASK_DEL_EN关掉后不能调用OSTaskDel();OS_CFG_STAT_TASK_EN关掉后不能调用OSStatTaskCPUUsageInit(),但如果你不需要看 CPU 占用率,关掉能省一个任务栈和一堆统计变量。注意,OS_CFG_TASK_PROFILE_EN关掉后OSTaskCreate()的p_err参数仍然存在,但任务控制块里的统计字段不更新,调试时看不到栈使用峰值,新手建议先开着,等 RAM 紧张再关。
3. 把 ucos III 跑起来:从 BSP 到第一个任务
3.1 最小工程的文件清单
不要一上来就啃官方 EvalBoards 里的完整示例,那个工程包含 LCD、文件系统、网络协议栈,文件几百个,编译一次几分钟,出了问题根本不知道从哪查。我一般会先搭一个最小工程,只包含以下文件:
os_core.c、os_task.c、os_time.c、os_sem.c、os_mutex.c、os_tmr.c、os_prio.c、os_var.c、os_cfg.cos_cpu.h、os_cpu_a.asm、os_cpu_c.c(从 Ports 对应目录拷)os.h、os_type.h、os_cfg.h- 你自己的
main.c、bsp.c、bsp.h、启动文件、链接脚本
这个清单大概 15 个文件左右,编译一次几秒钟,出问题范围可控。等最小系统跑起来,再逐个加模块。
3.2 时钟节拍与OS_CFG_TICK_RATE_HZ
ucos III 需要一个周期性的时钟节拍中断来驱动时间管理。常见做法是用 Cortex-M 的 SysTick,配置成 1ms 一次。在os_cpu_c.c里通常有一个OS_CPU_SysTickInit()函数,或者你自己在 BSP 里初始化 SysTick。关键参数是os_cfg.h里的:
/* 时钟节拍频率,单位 Hz */ #define OS_CFG_TICK_RATE_HZ 1000u逻辑说明:这个值决定了OSTimeDly()的最小分辨率。设成 1000 表示 1ms 一个 tick,OSTimeDly(1)延时 1ms。设成 100 表示 10ms 一个 tick,OSTimeDly(1)延时 10ms。参数说明:tick 频率越高,调度器被中断的次数越多,CPU 开销越大;tick 频率越低,延时精度越差。常见折中是 100Hz 到 1000Hz。注意,OS_CFG_TICK_RATE_HZ必须和你的 SysTick 重装载值匹配,否则时间会跑偏。比如 1000Hz 对应 1ms 中断,SysTick 重装载值 = 系统时钟频率 / 1000 - 1。
3.3 第一个任务的创建与启动
最小系统的main.c通常长这样:
#include "os.h" #include "bsp.h" /* 任务栈,注意对齐 */ CPU_STK AppTaskStartStk[128]; /* 任务控制块 */ OS_TCB AppTaskStartTCB; /* 任务函数声明 */ static void AppTaskStart(void *p_arg); int main(void) { OS_ERR err; /* 板级初始化:时钟、GPIO、串口 */ BSP_Init(); /* 初始化 ucos III 内核 */ OSInit(&err); if (err != OS_ERR_NONE) { while (1); /* 内核初始化失败,死循环 */ } /* 创建启动任务 */ OSTaskCreate(&AppTaskStartTCB, /* 任务控制块 */ "App Task Start", /* 任务名 */ AppTaskStart, /* 任务函数 */ 0, /* 传给任务的参数 */ 5, /* 优先级,数字越小优先级越高 */ &AppTaskStartStk[0], /* 栈基地址 */ 128 / 10, /* 栈使用警戒线,用掉 10% 时报警 */ 128, /* 栈大小 */ 0, /* 消息队列?不用 */ 0, /* 时间片?不用 */ 0, /* 选项 */ &err); if (err != OS_ERR_NONE) { while (1); /* 任务创建失败 */ } /* 启动多任务调度,永不返回 */ OSStart(&err); while (1); } static void AppTaskStart(void *p_arg) { OS_ERR err; (void)p_arg; /* 初始化 SysTick 作为时钟节拍 */ OS_CPU_SysTickInit(); while (1) { /* 翻转一个 GPIO,用示波器看波形 */ BSP_LedToggle(); OSTimeDly(500, OS_OPT_TIME_DLY, &err); } }逻辑说明:OSInit()初始化内核内部变量和就绪表;OSTaskCreate()创建启动任务,优先级 5;OSStart()启动调度器,从此 CPU 交给 ucos III 管理。启动任务里先初始化 SysTick,然后进入无限循环,每 500 个 tick 翻转一次 LED。参数说明:OSTaskCreate()的第三个参数是任务函数指针,必须是void (*)(void *)类型;第四个参数是传给任务的参数,不用就填 0;第五个参数是优先级,ucos III 里数值越小优先级越高,0 是最高优先级,通常保留给系统任务;第六个参数是栈基地址,注意栈的生长方向,Cortex-M 是满递减栈,栈基地址应该是栈数组的高地址端,但 ucos III 的移植层通常会自动处理,你传&Stk[0]或&Stk[栈大小-1]要看移植层实现,常见做法是传数组首地址,移植层内部做转换。OS_CPU_SysTickInit()不是内核标准 API,是移植层提供的,不同移植层名字可能不同,有的叫OS_CPU_SysTickInit(),有的叫BSP_OS_TickInit(),看os_cpu_c.c里的实现。
3.4 栈大小怎么估
新手最容易翻车的地方就是栈给太小。AppTaskStartStk[128]表示 128 个CPU_STK元素,CPU_STK通常是 32 位,所以是 512 字节。对于只翻转 LED 的任务,512 字节够用。但如果你在任务里调用printf、浮点运算、递归,栈需求会暴涨。常见做法是先用一个较大的栈(比如 1024 字节),跑起来后用OSTaskStkChk()查实际使用峰值,再逐步缩小。注意,OSTaskStkChk()需要OS_CFG_STAT_TASK_EN和OS_CFG_TASK_PROFILE_EN打开,否则返回不了数据。
4. 避坑与排查:ucos III 移植时最容易翻车的五个点
4.1 现象:程序一启动就进 HardFault
原因:最常见的是栈对齐问题。Cortex-M 要求栈指针 8 字节对齐,如果你在OSTaskCreate()里传的栈基地址没有对齐,或者栈数组本身没有__align(8)修饰,内核第一次做上下文切换时就会触发 HardFault。另一个原因是OS_CPU_SysTickInit()在OSStart()之前调用,SysTick 中断在调度器启动前就触发,而中断服务函数里调用了OSIntEnter()但内核还没初始化完。
解决:给栈数组加对齐修饰,比如 IAR 下用#pragma data_alignment=8,GCC 下用__attribute__((aligned(8)))。SysTick 初始化放在启动任务里,不要放在main()里OSStart()之前。如果必须在OSStart()之前初始化,确保 SysTick 中断被禁用,等OSStart()之后再使能。
4.2 现象:任务切换后串口输出乱码或死机
原因:OS_CFG_TICK_RATE_HZ和实际 SysTick 中断频率不匹配,导致OSTimeDly()延时时间错误,任务调度节奏全乱。或者os_cpu_a.asm里的PendSV_Handler和你的工程里已有的PendSV_Handler重名,链接器选了错误的实现。
解决:用示波器测 SysTick 中断间隔,确认是 1ms 还是 10ms。检查工程里所有PendSV_Handler、SysTick_Handler、SVC_Handler的定义,确保只有 ucos III 移植层的一份,其他文件里的同名函数要么删掉,要么改名。常见做法是在启动文件里把这三个中断向量指向 ucos III 的汇编标签,比如PendSV_Handler指向OS_CPU_PendSVHandler。
4.3 现象:OSTaskCreate()返回OS_ERR_TASK_CREATE_ISR
原因:在中断服务函数里调用了OSTaskCreate()。ucos III 不允许在 ISR 里创建任务,因为任务创建涉及内存分配和就绪表操作,可能引起调度器状态不一致。
解决:把任务创建放到启动任务里,或者用OSTaskCreate()的 ISR 版本?没有 ISR 版本。正确做法是在main()里创建启动任务,启动任务里再创建其他任务。如果确实需要在中断里触发任务创建,用信号量或消息队列通知一个任务,让那个任务去创建。
4.4 现象:编译报错undefined symbol OS_CPU_SysTickHandler
原因:os_cpu_c.c里定义了OS_CPU_SysTickHandler(),但你的启动文件里SysTick_Handler没有指向它,或者你根本没把os_cpu_c.c加入工程。
解决:检查工程文件列表,确认os_cpu_c.c和os_cpu_a.asm都在。在启动文件里把SysTick_Handler改成OS_CPU_SysTickHandler,或者在os_cpu_c.c里把函数名改成SysTick_Handler。注意,不同移植层的命名习惯不同,有的用OS_CPU_SysTickHandler,有的直接用SysTick_Handler,看os_cpu_c.c里的实际定义。
4.5 现象:任务跑着跑着就卡死,但中断还能进
原因:栈溢出。某个任务的栈用超了,踩到了相邻任务的控制块或栈,导致就绪表被破坏,调度器找不到下一个任务。或者你在任务里调用了OSTimeDly()但传了 0,OSTimeDly(0)在某些版本里行为未定义,可能让任务永远挂起。
解决:打开OS_CFG_STAT_TASK_EN和OS_CFG_TASK_PROFILE_EN,用OSTaskStkChk()查每个任务的栈使用峰值,把栈大小调到峰值的 1.5 倍以上。OSTimeDly()的参数至少为 1,如果要让出 CPU 但不延时,用OSTimeDly(1)或OSSched()。注意,OSTaskStkChk()本身也有开销,调试完可以关掉。
5. 进阶:用OSTaskStkChk()和OSStatTaskCPUUsageInit()把系统摸透
最小系统跑起来之后,别急着加功能。先花半小时把两个工具用起来:栈检查和 CPU 占用率统计。这两个东西能让你在后续加任务、加通信模块时心里有数,而不是靠玄学调栈大小。
先确保os_cfg.h里打开了统计任务和任务 profiling:
#define OS_CFG_STAT_TASK_EN 1u #define OS_CFG_STAT_TASK_RATE_HZ 10u /* 统计任务每秒运行 10 次 */ #define OS_CFG_TASK_PROFILE_EN 1u #define OS_CFG_TASK_STK_CHK_EN 1u然后在启动任务里初始化统计任务:
static void AppTaskStart(void *p_arg) { OS_ERR err; CPU_STK_SIZE free; CPU_STK_SIZE used; CPU_STK_SIZE usage_pct; (void)p_arg; OS_CPU_SysTickInit(); /* 初始化统计任务,需要传入一个时钟节拍基准 */ OSStatTaskCPUUsageInit(&err); while (1) { /* 检查自己任务的栈使用情况 */ OSTaskStkChk(&AppTaskStartTCB, &free, &used, &err); usage_pct = (used * 100) / (free + used); /* 如果栈使用超过 70%,通过串口报警 */ if (usage_pct > 70) { BSP_Printf("WARN: stack usage %d%%\r\n", usage_pct); } OSTimeDly(1000, OS_OPT_TIME_DLY, &err); } }逻辑说明:OSStatTaskCPUUsageInit()会创建一个统计任务,优先级通常比所有用户任务低,它周期性地计算 CPU 占用率。OSTaskStkChk()检查指定任务控制块对应的栈,返回空闲栈量和已用栈量。参数说明:free和used的单位是CPU_STK元素个数,不是字节。usage_pct是已用百分比。注意,OSStatTaskCPUUsageInit()必须在至少创建了一个用户任务之后调用,否则统计任务没有基准。另外,统计任务本身也占一个任务控制块和栈,RAM 紧张的话调试完可以关掉。
CPU 占用率怎么看?OSStatTaskCPUUsage是一个全局变量,单位是千分之一,比如 125 表示 12.5%。你可以在串口里周期打印这个值。如果 CPU 占用率超过 80%,说明你的任务设计有问题,要么任务太多,要么某个任务里干了太多活,要么 tick 频率太高。常见做法是把耗时操作拆成状态机,每次只干一小块,然后主动让出 CPU。
还有一个技巧:用OSTaskTimeQuantaSet()给同优先级任务设置时间片。ucos III 默认不支持同优先级任务的时间片轮转,但你可以通过OSTaskCreate()的time_quanta参数和OSTaskTimeQuantaSet()来开启。不过大多数嵌入式场景不需要时间片,每个任务优先级不同更简单。
从那以后我每次移植 ucos III,都强制走一遍「最小系统 → 栈检查 → CPU 占用率 → 再加模块」的流程,绝不跳过。希望帮到你。
本文还有配套的精品资源,点击获取