
简介面向嵌入式开发初学者与进阶工程师的UCOS II任务管理与调度课件共170页聚焦实时操作系统内核机制的落地讲解。内容从任务控制块TCB与就绪、运行、等待、休眠、僵死五种任务状态切入延伸至RMS、时间片轮转等调度算法及TDMA、固定优先级两种调度策略并覆盖中断响应处理以及信号量、互斥锁、条件变量等同步通信机制配合示例代码帮助理解任务间的资源竞争与事件通知。课件按任务管理、调度机制、中断处理与同步通信几个模块顺序展开脉络清晰适合高校课程设计、嵌入式培训讲授与自学复盘也可作为项目中任务划分、优先级设定与实时性优化的参考。整份资料为1个pptx文件压缩包约9.8MB页面编排完整。目前已有153人学习下载。1. uCOS-II 任务管理与调度从三个不同周期的 LED 说起三个 LED 要分别以 100ms、300ms、700ms 闪烁裸机上写三段 while 加空循环也能跑可一旦再叠上串口收包和按键扫描任何一处延时都会把另外两个的节奏带偏。uCOS-II 这类实时操作系统给出的解法是把每个循环体变成独立任务由内核按优先级抢占式调度高优先级任务在事件到来的瞬间就能拿到 CPU。任务管理和调度正是这套机制的两块地基任务管理回答任务是什么、怎么创建、状态怎么流转调度器回答下一个该谁运行、上下文怎么换。把这两块讲透任务不切换、优先级反转、栈溢出这些移植后几乎必踩的坑才有地方对号入座也才能判断手上的项目到底该不该上 RTOS。2. uCOS-II 任务状态机与 OS_TCB 控制块拆解2.1 五种任务状态与转换条件uCOS-II 里一个任务从生到死会经历五种状态理解状态机比背 API 更重要因为绝大多数“任务不跑”的问题都能落回到状态没流转过去。睡眠态是任务代码存在但还没被OSTaskCreate注册OS_TCB 和任务栈都还是一块自由内存就绪态表示任务具备运行条件已经被挂进就绪表等待调度运行态同一时刻只会有一个任务它持有 CPU等待态是任务调用OSTimeDly或OSSemPend后主动让出 CPU中断服务态则发生在 ISR 执行期间中断返回时会触发一次重新调度。当前状态进入方式退出方式睡眠态上电初始 /OSTaskDel之后OSTaskCreate、OSTaskCreateExt就绪态创建完成、延时到期、事件满足被调度器选中或事件被清除运行态OS_Sched选中最高优先级被抢占、主动延时、等待事件等待态OSTimeDly、OSSemPend、OSMboxPend超时到期或事件被投递中断服务态硬件中断触发中断返回重新走一次调度状态之间的转换由内核函数集中控制OSTaskCreate把睡眠态推进就绪态OS_Sched从就绪态挑一个变运行态OSTimeDly把运行态降级成等待态OSTimeTick在每个时钟节拍里把等超时的任务重新拉回就绪态。这几条路径走完之后任务的状态就是可预期的。2.2 OS_TCB 里哪些字段真正参与调度每个任务都有一块OS_TCB可以理解为内核给任务建的档案。字段看着多真正在调度里被频繁读写的其实就几个优先级OSTCBPrio、就绪表用的链表指针OSTCBPrev/OSTCBNext、任务栈顶OSTCBStkPtr、延时计数器OSTCBDly、事件控制块指针OSTCBEventPtr以及挂起与延时标志。其余像任务名、扩展指针属于调试和统计用途不参与选择下一个任务。typedef struct os_tcb { OS_STK *OSTCBStkPtr; /* 任务栈顶指针切换时保存/恢复 SP最关键 */ struct os_tcb *OSTCBNext; /* 就绪表双向链表的后继 */ struct os_tcb *OSTCBPrev; /* 就绪表双向链表的前驱 */ INT16U OSTCBDly; /* 延时剩余节拍0 表示不在等待 */ INT8U OSTCBStat; /* 状态位就绪/延时/挂起/等待事件 */ INT8U OSTCBPrio; /* 优先级0 最高数值越大越低 */ OS_EVENT *OSTCBEventPtr; /* 指向等待的事件控制块 */ INT8U OSTCBX; /* 优先级低 3 位预先算好省查表 */ INT8U OSTCBY; /* 优先级高 3 位预先算好省查表 */ INT8U OSTCBBitX; /* 1 OSTCBX写就绪表用 */ INT8U OSTCBBitY; /* 1 OSTCBY写就绪表用 */ } OS_TCB;OSTCBX/OSTCBY/OSTCBBitX/OSTCBBitY这四个字段是典型的空间换时间创建任务时就把优先级的位位置算好调度和事件投递时直接位移操作不必每次都查OSUnMapTbl。如果你的项目优先级数量很少、RAM 又紧张可以关掉OS_TASK_STAT_EN只保留必须字段但OSTCBStkPtr、OSTCBPrio、OSTCBDly、OSTCBStat一个都不能省。2.3 优先级分配与数量上限uCOS-II 的优先级是 0 到OS_LOWEST_PRIO0 最高数值越大越低。默认配置里OS_LOWEST_PRIO是 63也就是说最多 64 个任务其中优先级OS_LOWEST_PRIO被OS_TaskIdle占用OS_LOWEST_PRIO-1被OS_TaskStat占用开启统计任务时。所以真正能分给用户的一般是 0 到 61。分配顺序上我的习惯是硬实时事件响应放 0~3周期性控制律放 4~10通信解析放 11~20人机界面和日志放 30 以后统计和空闲交给系统。同一时刻每个优先级只能有一个任务OSTaskCreate撞到已占用的优先级会返回OS_ERR_PRIO_EXIST。提示不要为了“看起来清楚”把优先级排得很稀疏0、10、20、30 这样分中间几十个数字全浪费一旦要往中间插入新任务就没位置了。3. OSTaskCreate 创建任务与任务栈的初始化3.1 OSTaskCreate 与 OSTaskCreateExt 的选型uCOS-II 提供两个创建接口。OSTaskCreate只要任务函数、参数指针、栈顶指针和优先级四个参数代码体积小适合大多数中小项目。OSTaskCreateExt多出任务 ID、栈底指针、栈大小、扩展指针和选项五个参数好处是能配合OSTaskStkChk做栈使用统计、能指定OS_TASK_OPT_STK_CHK和OS_TASK_OPT_STK_CLR代价是每个任务多占几十字节 RAM 和一点代码空间。/* 函数原型参数含义逐一对齐 */ INT8U OSTaskCreate(void (*task)(void *pd), /* 任务入口形式上返回 void */ void *p_arg, /* 传给任务的参数可以是结构体指针 */ OS_STK *ptos, /* 栈顶指针注意是顶不是底 */ INT8U prio); /* 优先级必须唯一 */ #define TASK_LED_PRIO 5 #define TASK_LED_STK 128 static OS_STK TaskLedStk[TASK_LED_STK]; void TaskLed(void *p_arg) { (void)p_arg; /* 参数不用就显式丢弃避免编译告警 */ for (;;) { LED_TOGGLE(); OSTimeDly(100); /* 延时 100 个时钟节拍 */ } } void App_TaskCreate(void) { INT8U err OSTaskCreate(TaskLed, (void *)0, TaskLedStk[TASK_LED_STK - 1], /* 传栈顶地址 */ TASK_LED_PRIO); if (err ! OS_ERR_NONE) { /* 创建失败常见原因优先级重复、内存不足、在 ISR 里调用 */ while (1) { } } }这里最容易错的就是第三个参数。uCOS-II 用的是满递减栈栈从高地址向低地址生长所以必须传Stack[SIZE-1]传Stack或Stack[0]会让第一次压栈直接踩到数组外面。传参p_arg本身不做拷贝生命周期必须覆盖任务全程通常就传一个静态结构体地址或者(void *)0。3.2 任务栈深度怎么估算栈深度是新手最容易给少的地方。栈里除了局部变量和函数调用帧还要放下任务上下文在 Cortex-M 上OSTaskStkInit会手工压入 R4~R11、R0~R3、R12、LR、PC、xPSR 共 16 个字也就是 64 字节的固定开销再加上该任务所有函数调用链上的局部变量和中断嵌套时的额外压栈。场景建议栈深度32 位字说明只做 GPIO 翻转、无浮点64 ~ 128常见 LED、按键任务含printf或sprintf256 以上库函数自身嵌套深且常带缓冲含浮点运算256 ~ 512是否开硬件 FPU 差别很大通信协议栈解析256 ~ 384递归下降解析器要再加中断嵌套层数多在基础值上再放大嵌套越深压栈越多给完初值之后不要靠猜用OSTaskStkChk反过来看实际用量。原则是给到实测峰值的 1.5 倍以上留余量因为不同的输入分支会走出不同的最深调用链。3.3 开机流程与多任务启动顺序uCOS-II 要求在OSStart之前至少创建一个任务并且不能在中断里创建。典型顺序是初始化时钟和OSInit创建起始任务最后调OSStart把 CPU 交给就绪表里优先级最高的那个。起始任务一般只负责初始化外设、创建其余任务然后自己删掉或者降到最低优先级去做后台工作。int main(void) { OSInit(); /* 初始化内核就绪表、空闲任务、时钟 */ BSP_Init(); /* 时钟、串口、GPIO 等硬件初始化 */ OSTaskCreate(TaskStart, (void *)0, TaskStartStk[TASK_START_STK - 1], TASK_START_PRIO); /* 只创建起始任务 */ OSStart(); /* 永不返回开始多任务调度 */ return 0; }OSStart会先找到就绪表里的最高优先级然后调用OSStartHighRdy启动第一个任务。如果启动后卡死、第一个任务不执行优先检查三件事OSStartHighRdy里有没有调用OSTaskSwHook、第一个任务的栈顶地址对不对、SysTick有没有使能并正确配置成OS_TICKS_PER_SEC。4. 调度器内核就绪表、OS_Sched 与任务切换4.1 OSRdyGrp 与 OSRdyTbl 的位图结构uCOS-II 的就绪表是一张位图不是链表数组。OSRdyTbl是一个长度为 8 的字节数组把 64 个优先级分成 8 组每组 8 位OSRdyGrp是一个字节它的第 n 位为 1 表示第 n 组里有任务就绪。这样“找最高优先级就绪任务”这个操作被压缩成两次查表加一次拼接执行时间恒定跟就绪任务数量无关。优先级组号 Y高 3 位位号 X低 3 位写入位置000OSRdyTbl[0] 的 bit0505OSRdyTbl[0] 的 bit51214OSRdyTbl[1] 的 bit43543OSRdyTbl[4] 的 bit36377OSRdyTbl[7] 的 bit7置位和清零都用预先算好的OSTCBBitY、OSTCBBitX做位运算这是这套结构里最有意思的地方把除法换成移位和查表。4.2 OS_Sched 的查找过程void OS_Sched(void) { INT8U y; OS_ENTER_CRITICAL(); /* 关中断防止就绪表被改到一半 */ if ((OSIntNesting 0) (OSLockNesting 0)) { y OSUnMapTbl[OSRdyGrp]; /* 最高优先级所在组0..7 */ OSPrioHighRdy (INT8U)((y 3) OSUnMapTbl[OSRdyTbl[y]]); /* 拼出完整优先级 */ if (OSPrioHighRdy ! OSPrioCur) { /* 跟当前任务不同才切 */ OSTCBHighRdy OSTCBPrioTbl[OSPrioHighRdy]; OSCtxSwCtr; /* 统计切换次数调试时好用 */ OS_TASK_SW(); /* 触发上下文切换 */ } } OS_EXIT_CRITICAL(); }OSUnMapTbl是一张 256 字节的常量表输入一个字节输出最低位为 1 的位号。两次查表分别解决了“哪个组有就绪任务”和“组内哪一位有就绪任务”因为优先级数值越小越高所以取最低位正好对应最高优先级。函数开头的两个判断是关键OSIntNesting 0保证不在中断里切换中断里只置一个OSPrioHighRdy标记等OSIntExit再统一处理OSLockNesting 0保证没有在临界区里强行切走否则锁没释放就换了任务会出大问题。4.3 OSCtxSw 与 Cortex-M 上的上下文切换OS_TASK_SW是个宏在 Cortex-M 移植版里通常写成PendSV的触发真正的切换动作发生在OSCtxSw或PendSV_Handler里。切换要干的事情是把当前任务的 CPU 寄存器压进它自己的栈把栈顶指针写回OSTCBCur-OSTCBStkPtr然后从OSTCBHighRdy-OSTCBStkPtr取出新任务的栈顶把寄存器弹回去执行返回。硬件在进异常时自动压 R0~R3、R12、LR、PC、xPSR所以移植层只需要手工保存 R4~R11。; Cortex-M 移植层的任务切换骨架 PendSV_Handler CPSID I ; 进入后关中断 MRS R0, PSP ; 取当前进程栈指针 CBZ R0, PendSV_NoSave ; 首次切换没有有效 PSP跳过保存 STMFD R0!, {R4-R11} ; 手工保存硬件不自动压的寄存器 LDR R1, OSTCBCur LDR R1, [R1] STR R0, [R1] ; 当前 SP 存回自己的 TCB PendSV_NoSave LDR R0, OSTCBPrioHighRdy ; 取即将运行任务的 TCB LDR R1, OSTCBCur LDR R2, [R0] STR R2, [R1] LDR R0, [R2] ; 新任务的栈顶 LDMFD R0!, {R4-R11} ; 恢复手工保存的寄存器 MSR PSP, R0 ; 写回进程栈指针 ORR LR, LR, #0x04 ; 返回后使用 PSP CPSIE I BX LRPendSV被特意设成最低优先级异常这样它不会打断任何还在处理中的中断只会在所有中断都退出后执行。这也是为什么在 ISR 里调OSSemPost不会立刻切换任务中断里只更新就绪表和OSPrioHighRdy中断退出走OSIntExit最后统一由PendSV完成切换。把这条时序理清楚就能解释“在中断里发信号量为什么任务要等一会儿才跑”这个高频疑问。注意OSCtxSwCtr是个很有用的观测点把它的值打印出来就能判断调度到底有没有在发生。系统中枢任务切不切换、一秒切多少次全在这个计数器上。5. 调度踩坑优先级反转与栈溢出排查5.1 优先级反转与互斥量优先级反转是任务管理里最容易被问到的问题低优先级任务拿了共享资源高优先级任务等这个资源中优先级任务又把低优先级任务抢走结果高优先级任务被一个不相干的中优先级任务间接拖住。uCOS-II 的解法是互斥量OSMutexOSMutexPend时若发生反转内核会把持有者的优先级临时抬到等待者的优先级等释放后再恢复。static OS_EVENT *g_share_mutex; void TaskAccessShared(void *p_arg) { INT8U err; (void)p_arg; for (;;) { OSMutexPend(g_share_mutex, 0, err); /* 第二参数 0 表示无限等待 */ if (err OS_ERR_NONE) { Share_Read_Write(); /* 临界区尽量短 */ OSMutexPost(g_share_mutex); } OSTimeDly(10); } }用OSSemCreate当二值信号量也能互斥但它不带优先级继承一旦出现三级任务竞争就会卡住。区分方法很简单只做任务间同步用信号量保护共享资源一律用互斥量。创建互斥量时OSMutexCreate(prio, err)的第一个参数是优先级提升的上限通常传OS_LOWEST_PRIO之外的一个专用值避免提升后撞上已有任务的优先级。5.2 用 OSTaskStkChk 定位栈溢出栈溢出在 uCOS-II 里表现很杂可能是任务跑飞进HardFault也可能只是某个变量莫名被改。最实用的手段是开OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR创建任务然后周期性调OSTaskStkChk拿实际用量。OS_STK_DATA stk_data; INT8U err OSTaskStkChk(TASK_LED_PRIO, stk_data); /* stk_data.OSFree 是剩余空闲栈字节数stk_data.OSUsed 是历史峰值 */ if (err OS_ERR_NONE stk_data.OSFree 32) { LOG_WARN(task %d stack left %d bytes, TASK_LED_PRIO, stk_data.OSFree); }统计的原理是创建时把整块栈清零运行一段时间后从栈底往栈顶扫找第一个不为 0 的位置差值就是峰值用量。所以统计任务必须真的跑过所有分支否则峰值是偏小的。经验值是剩余不低于总栈深的 25%低于这个数就该加栈或者削调用链。5.3 OSTimeDly 的精度边界OSTimeDly(n)延时的实际时长是 n 个时钟节拍误差来自节拍本身的粒度而不是函数实现。OS_TICKS_PER_SEC设成 100 时一个节拍 10ms请求 5ms 只能等到下一个节拍实测会在 10ms 附近跳动。要让延时更准提升节拍频率是最直接的办法但节拍越快OSTimeTick的中断开销越大低优先级任务的可用时间会被挤掉。折中点是 100~1000Hz普通控制用 100Hz 够用需要毫秒级精度的用 1000Hz。时间基准也要注意OSTimeDly依赖OSTimeTick被稳定调用。SysTick 的配置如果被其他库重写节拍就会变慢或变快表现出来是所有延时整体拉长或缩短。排查时调OSTimeGet(err)前后取差跟实际挂钟时间对比一比就能确认节拍频率到底对不对。本文还有配套的精品资源点击获取