简介这份资源是面向嵌入式专业学生与初学者的UCOS-II课程设计完整工程围绕多任务调度与系统时钟展开可用于课程实验、项目复现与RTOS入门练习。压缩包共63个文件、约283KB以35个h头文件、3个c源文件为核心代码配合9个o目标文件、6个s汇编启动文件及工程配置与烧录相关文件构成可直接参考的移植与编译结构。内容覆盖UCOS-II在目标平台上的安装配置、任务创建与优先级管理、延时与周期性任务的时间管理以及信号量、事件标志组、邮箱和消息队列等同步通信机制并涉及任务状态调试与调度优化思路。已有455人学习适合对照理解抢占式调度与时钟滴答的实现细节为后续嵌入式实时系统开发积累可复用的工程经验。1. 一份能跑通的多任务时钟课设到底长什么样嵌入式课程设计里UCOS-II 多任务系统时钟这个题目年年有人做年年有人卡在同一个地方移植完内核任务建了三个串口能打印但时钟节拍一跑起来就乱——要么任务切换周期对不上要么延时函数精度飘得离谱要么干脆在某个任务里死等整个系统像被按了暂停键。这份课程设计资源解决的正是这个断层它不是一个只讲 API 的文档而是一套把 UCOS-II 内核移植、多任务划分、系统时钟节拍配置、任务间同步串起来的完整工程适合正在做嵌入式课设的学生也适合想补一遍 RTOS 底层机制的在职开发者。下面按「先立住原理、再动手复现、最后排坑」的顺序拆开讲每一步都落到能抄的参数和代码上。2. 内核移植与时钟节拍先把 systick 这口锅烧热UCOS-II 能不能跑起来八成取决于移植层写得对不对而移植层里最容易翻车的就是系统时钟节拍。这一章先把移植的边界讲清楚再落到 systick 的具体配置。2.1 为什么时钟节拍是 UCOS-II 的心跳UCOS-II 是抢占式内核任务调度、延时、超时判断全靠一个周期性的时钟中断来驱动这个中断就是时钟节拍OSTick。每来一次节拍内核做三件事给需要延时的任务减计数、检查是否有任务延时到期、触发一次调度判断。节拍频率由OS_TICKS_PER_SEC决定常见取值 100 到 1000。取 100 意味着每 10ms 一个节拍延时精度就是 10ms 量级取 1000 则是 1ms 精度但中断开销翻十倍。选型上有个血泪经验课设阶段别一上来就追 1ms 精度。节拍太快如果中断服务程序里干了重活比如直接在里面刷串口CPU 会被中断吃掉大半任务反而跑不流畅。我一般建议课设先用 200Hz也就是 5ms 一个节拍既能看出多任务调度的效果又不会把中断负担压得太狠。等系统稳了再按需往上调。节拍的来源通常是 Cortex-M 的 SysTick 定时器它自带 24 位递减计数器配置起来比通用定时器省事。移植时要做的是把 SysTick 的 reload 值算对并在中断里调用OSTimeTick()。2.2 移植层的三个必改文件UCOS-II 官方源码里和处理器相关的部分集中在三个文件os_cpu.h、os_cpu_a.asm或.s、os_cpu_c.c。换一款芯片主要改这三处。os_cpu.h里定义数据类型、开关中断的宏、任务切换的宏。Cortex-M 系列一般用__disable_irq()/__enable_irq()包一层任务切换用PendSV异常触发。关键宏是OS_TASK_SW()它负责在任务级触发一次上下文切换。os_cpu_a.asm里是汇编写的PendSV_Handler和OSStartHighRdy。PendSV 里做的是保存当前任务上下文、恢复最高优先级任务上下文这段汇编是整个移植里最不能错的地方寄存器压栈顺序错一个系统就会在第一次切换时跑飞。os_cpu_c.c里是OSTaskStkInit()负责初始化任务栈。Cortex-M 的任务栈初始化要模拟一次异常压栈的布局把 R0-R3、R12、LR、PC、xPSR 按硬件自动压栈的顺序摆好PC 指向任务函数入口LR 指向一个「任务返回后删除自己」的桩函数。这个布局错了任务第一次被调度就会跳飞。下面这段是 SysTick 初始化和节拍中断的典型写法节拍频率通过宏统一管理/* os_cpu_c.c 片段SysTick 初始化节拍频率由 OS_TICKS_PER_SEC 决定 */ #include os_cpu.h #define CPU_FREQ_HZ 72000000UL /* 芯片主频按实际改 */ #define SYSTICK_RELOAD (CPU_FREQ_HZ / OS_TICKS_PER_SEC - 1) void OS_CPU_SysTickInit(void) { /* 先关掉 SysTick避免配置过程中误触发 */ SysTick-CTRL 0; /* reload 值 主频 / 节拍频率 - 1减一是因为计数器从 reload 递减到 0 才中断 */ SysTick-LOAD SYSTICK_RELOAD; /* 清当前计数值保证第一个节拍周期完整 */ SysTick-VAL 0; /* 时钟源选内核时钟开中断启动计数 */ SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; } /* 节拍中断服务程序必须调用 OSTimeTick */ void SysTick_Handler(void) { OSIntEnter(); /* 通知内核进入中断嵌套计数加一 */ OSTimeTick(); /* 内核节拍处理延时递减、超时判断 */ OSIntExit(); /* 退出中断必要时触发调度 */ }这段代码的逻辑说明SYSTICK_RELOAD用主频除以节拍频率再减一是因为 SysTick 是递减计数器从 reload 值数到 0 才产生一次中断一个完整周期是 reload1 个时钟。CTRL寄存器里CLKSOURCE选内核时钟TICKINT打开中断ENABLE启动。中断服务程序里OSIntEnter()和OSIntExit()成对出现前者记录中断嵌套层数后者在退出时判断是否需要切换任务——这是 UCOS-II 中断处理的标准骨架漏掉任何一个调度都会出问题。参数上要盯住两个CPU_FREQ_HZ必须和实际系统主频一致写错了节拍频率整体偏移OS_TICKS_PER_SEC在os_cfg.h里定义改它等于改整个系统的时间基准所有OSTimeDly()的数值含义都跟着变。2.3 节拍频率与延时精度的换算表课设报告里经常要写「延时精度分析」这张表可以直接用OS_TICKS_PER_SEC单节拍时长OSTimeDly(1) 实际延时适用场景10010ms10ms对精度要求低、中断负载敏感2005ms5ms课设常用平衡精度与开销5002ms2ms需要较细延时中断稍重10001ms1ms高精度场景中断开销最大要注意OSTimeDly(n)的语义是「延时 n 个节拍」不是 n 毫秒。很多人写OSTimeDly(100)以为是 100ms结果节拍是 200Hz 时实际延时 500ms任务节奏全乱。正确做法是用OSTimeDlyHMSM(0, 0, 0, 100)按时分秒毫秒延时内核内部会换算成节拍可读性和正确性都更好。3. 多任务划分与同步三个任务怎么切才不打架移植通了接下来是课设的核心把系统时钟这个功能拆成多个任务让它们各司其职又协同工作。这一章讲任务划分的取舍、优先级怎么定、任务间怎么通信。3.1 任务划分的两种思路与取舍系统时钟这个题目功能上无非是「走时、显示、响应按键调时」。拆任务有两种典型思路。第一种按功能拆一个任务负责计时读 RTC 或软件累加一个任务负责刷新显示一个任务负责扫描按键。这种拆法职责清晰每个任务代码短调试时容易定位问题课设报告也好写。缺点是任务间耦合靠全局变量或消息同步没做好会出现显示读到半更新数据的情况。第二种按数据流拆计时任务算出时间后发消息给显示任务显示任务只管收消息刷新按键任务改时间也是发消息给计时任务。这种拆法耦合低但消息队列一多课设阶段容易把简单问题复杂化。我一般建议课设用第一种为主、关键路径用消息队列兜底计时和显示之间用全局结构体加临界区保护按键到计时用消息队列传递调时命令。这样既不会把工程搞得太重又能体现对任务同步的理解报告里也有东西可写。任务数量上三个任务足够撑起一个课设的评分点Task_Time计时、Task_Display显示、Task_Key按键。优先级安排遵循速率单调原则——周期越短、实时性要求越高的任务优先级越高。按键响应要快给最高计时次之显示最慢给最低。3.2 任务创建与优先级配置的完整代码下面这段是三个任务的创建和主体框架优先级用宏统一管理方便调整/* app_tasks.c三个任务的创建与主体 */ #include includes.h #define TASK_TIME_PRIO 6 /* 计时任务中等优先级 */ #define TASK_DISPLAY_PRIO 8 /* 显示任务最低 */ #define TASK_KEY_PRIO 4 /* 按键任务最高 */ #define TASK_TIME_STK_SIZE 256 #define TASK_DISPLAY_STK_SIZE 256 #define TASK_KEY_STK_SIZE 128 static OS_STK TaskTimeStk[TASK_TIME_STK_SIZE]; static OS_STK TaskDisplayStk[TASK_DISPLAY_STK_SIZE]; static OS_STK TaskKeyStk[TASK_KEY_STK_SIZE]; /* 全局时间结构体计时任务写、显示任务读访问时进临界区 */ typedef struct { uint8_t hour; uint8_t minute; uint8_t second; } sys_time_t; static sys_time_t g_time; static OS_EVENT *g_time_mutex; /* 互斥信号量保护 g_time */ static void Task_Time(void *pdata) { pdata pdata; for (;;) { /* 每 1000ms 走一秒用 HMSM 避免节拍换算错误 */ OSTimeDlyHMSM(0, 0, 1, 0); OSMutexPend(g_time_mutex, 0, err); g_time.second; if (g_time.second 60) { g_time.second 0; g_time.minute; if (g_time.minute 60) { g_time.minute 0; g_time.hour (g_time.hour 1) % 24; } } OSMutexPost(g_time_mutex); } } static void Task_Display(void *pdata) { pdata pdata; for (;;) { OSMutexPend(g_time_mutex, 0, err); /* 拷贝一份再释放锁避免刷屏期间占着锁 */ sys_time_t snapshot g_time; OSMutexPost(g_time_mutex); Display_Refresh(snapshot.hour, snapshot.minute, snapshot.second); OSTimeDlyHMSM(0, 0, 0, 200); /* 200ms 刷一次肉眼够用 */ } } static void Task_Key(void *pdata) { pdata pdata; for (;;) { uint8_t key Key_Scan(); /* 带消抖的按键扫描 */ if (key ! KEY_NONE) { /* 调时命令通过消息队列发给计时任务这里简化直接改 */ OSMutexPend(g_time_mutex, 0, err); if (key KEY_HOUR_UP) g_time.hour (g_time.hour 1) % 24; if (key KEY_MIN_UP) g_time.minute (g_time.minute 1) % 60; OSMutexPost(g_time_mutex); } OSTimeDlyHMSM(0, 0, 0, 20); /* 20ms 扫一次兼顾响应与消抖 */ } } void App_TaskCreate(void) { uint8_t err; g_time_mutex OSMutexCreate(3, err); /* 优先级 3高于所有使用者 */ OSTaskCreate(Task_Key, (void *)0, TaskKeyStk[TASK_KEY_STK_SIZE - 1], TASK_KEY_PRIO); OSTaskCreate(Task_Time, (void *)0, TaskTimeStk[TASK_TIME_STK_SIZE - 1], TASK_TIME_PRIO); OSTaskCreate(Task_Display, (void *)0, TaskDisplayStk[TASK_DISPLAY_STK_SIZE - 1], TASK_DISPLAY_PRIO); }逻辑说明三个任务都是无限循环靠OSTimeDlyHMSM让出 CPU。计时任务每秒走一次显示任务 200ms 刷一次按键任务 20ms 扫一次周期不同但互不阻塞这正是多任务相对裸机前后台架构的价值——裸机里一个delay就把整个系统卡住了。参数说明几个关键点OSTaskCreate的第四个参数是优先级数值越小优先级越高所以按键任务给 4、计时给 6、显示给 8符合速率单调。栈大小按任务复杂度给按键任务逻辑简单给 128 字计时和显示涉及结构体操作给 256 字栈溢出是课设里最常见的跑飞原因宁可给大一点。互斥信号量的优先级给 3高于所有使用者是为了避免优先级反转——如果给低了低优先级任务持锁时被中优先级任务抢占高优先级任务就会干等。3.3 共享数据的临界区保护上面代码里g_time被三个任务访问必须保护。UCOS-II 里保护共享数据有三种手段关中断、调度器上锁、互斥信号量。关中断最简单但影响实时性只适合极短的临界区调度器上锁OSSchedLock()能阻止任务切换但中断照进适合中等长度互斥信号量适合可能引起阻塞的较长访问。课设里我一般这样选单纯读写几个字节的用OSSchedLock()包一下涉及结构体整体拷贝或可能等待的用互斥信号量。上面显示任务里先拷贝快照再释放锁就是避免刷屏这种慢操作占着锁不放这个细节写进报告能加分。提示互斥信号量在 UCOS-II 里会做优先级继承持锁任务被临时提升到等待者中最高优先级这是它比普通信号量更适合保护共享资源的原因。课设答辩被问到「为什么不用二值信号量」这就是标准答案。4. 避坑与排查那些让系统跑飞的细节移植和任务框架都搭好了真正让人熬夜的是各种「看起来对、跑起来错」的问题。这一章按现象、原因、解决三段式列五个课设里高频翻车的点。4.1 现象系统启动后卡在 OSStart 之前不动原因OSStart()之前必须至少创建一个任务且OSStartHighRdy()里的汇编要能正确恢复第一个任务的上下文。常见错误是OSTaskStkInit()里栈布局摆错PC 没指向任务入口或者 xPSR 的 Thumb 位没置 1Cortex-M 下 PC 最低位必须是 1 才进 Thumb 状态。解决单步跟到OSStartHighRdy看压栈内容。重点检查OSTaskStkInit里给 PC 赋的值是不是(OS_STK)task | 1那个或 1 就是置 Thumb 位漏了就跳飞。4.2 现象任务能跑但延时明显不准越跑偏得越多原因OS_TICKS_PER_SEC和实际 SysTick 配置不匹配或者CPU_FREQ_HZ写的是标称主频而实际时钟树配的是另一档。比如芯片跑在 72MHz代码里按 8MHz 算 reload节拍就慢了九倍。解决用示波器或逻辑分析仪量一个 GPIO 翻转在节拍中断里翻转、在主循环里按已知延时翻转对比实测周期和理论值。没有仪器的用串口打时间戳连续打一百次OSTimeGet()看增量是否稳定等于预期节拍数。4.3 现象显示偶尔花屏或数字跳变原因显示任务读g_time时计时任务正好在改它读到了半更新的数据比如秒已经进位但分还没加。这就是典型的共享数据竞争。解决按 3.3 的做法读之前进临界区或拿互斥量拷贝快照后再释放。注意拷贝要用结构体整体赋值别一个字段一个字段读否则临界区再短也可能被切。4.4 现象按键响应迟钝按一下要等好几秒原因某个任务里用了死等式延时比如while(!flag);或者裸机的delay_ms()把 CPU 占死了高优先级按键任务根本得不到调度。UCOS-II 是抢占式但前提是低优先级任务主动让出 CPU。解决把所有阻塞等待换成OSTimeDly或信号量 pend。查代码时搜while和delay凡是循环里没有让出 CPU 的都要改。这也是从裸机思维切到 RTOS 思维最关键的一步。4.5 现象跑一段时间后系统复位或进 HardFault原因任务栈溢出。课设里常见于显示任务里用了大的局部数组比如 sprintf 缓冲区或者中断嵌套深了压栈超限。UCOS-II 有栈检查功能但默认不开。解决在os_cfg.h里打开OS_TASK_STAT_EN和栈检查或者手动在每个任务栈顶填魔数如 0x5A5A跑一段时间后检查魔数被覆盖的深度估算实际栈用量。显示任务的 sprintf 缓冲区改成静态或全局别放栈上。注意栈溢出在课设演示时往往不立刻发作答辩前跑十分钟没事评委面前跑两分钟就复位这种玄学问题十有八九是栈。宁可每个任务多给 64 字栈也别卡着最小值分配。5. 用统计任务验证调度把黑匣子变成可观测数据系统跑起来不等于跑对了。课设答辩最怕被问「你怎么证明任务调度是合理的」光说「能显示时间」说服力不够。UCOS-II 自带统计任务OSStatTask能算出每个任务的 CPU 占用率把这个开起来调度是否均衡一目了然。开启方法是在os_cfg.h里把OS_TASK_STAT_EN置 1然后在OSStart()之前调用OSStatInit()。统计任务会以固定周期采样算出 CPU 使用率存在OSCPUUsage全局变量里。下面这段把统计结果通过串口打出来/* 统计任务开启与 CPU 占用率输出 */ #include includes.h static OS_STK StatStk[128]; void App_StatInit(void) { uint8_t err; /* 统计任务优先级仅高于空闲任务固定为 OS_LOWEST_PRIO - 1 */ OSTaskCreate(OSStatTask, (void *)0, StatStk[127], OS_LOWEST_PRIO - 1); OSStatInit(); /* 必须在多任务启动后、其他任务运行前调用 */ } /* 在显示任务里周期性打印 CPU 占用率 */ void Report_CPU(void) { char buf[32]; sprintf(buf, CPU Usage: %d%%\r\n, OSCPUUsage); UART_SendString(buf); }逻辑说明OSStatInit()内部会先延时两个节拍测出「纯空闲时一秒能跑多少次循环」作为基准之后统计任务每次采样算出非空闲时间占比。OSCPUUsage是 0 到 100 的整数直接读即可。参数上统计任务的栈给 128 字足够优先级必须是OS_LOWEST_PRIO - 1这是内核约定改了统计就不准。实测时有个经验值可以参考三个任务加统计任务正常运行时 CPU 占用率在 5% 到 20% 之间比较健康。如果长期高于 50%说明某个任务里有重活或者死循环如果一直是 0%反而要怀疑统计任务没跑起来或者节拍中断没进。把这个数据写进课设报告比单纯贴代码有说服力得多。再进一步可以用OSTaskStkChk()查每个任务的栈使用情况返回已用和剩余字节数配合前面的魔数法互相印证。我一般会在系统稳定运行五分钟后调一次把结果记下来作为「系统长期运行稳定性」的证据。从那以后我每次做 RTOS 课设都强制在答辩前跑一遍 CPU 占用率和栈检查把这两个数写进报告首页——评委看到可观测的数据追问的欲望会小很多。希望帮到你。本文还有配套的精品资源点击获取