ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

uC/OS-II在STM32F103上的抢占式调度与移植实践

2026/9/13 15:39:32 拓冰建站 浏览量
uC/OS-II在STM32F103上的抢占式调度与移植实践 简介本资源是一份基于STM32平台的嵌入式实时操作系统实践教学材料面向高校电子/自动化/物联网等专业学生及嵌入式初学者聚焦UCOSII V2.91内核的任务调度机制理解与工程实现。资源完整呈现了三任务协同控制双LEDDS0/DS1的典型调度案例开始任务负责初始化并创建LED0、LED1两个周期性任务分别实现80ms脉冲点亮与300ms亮灭交替直观展示优先级调度与任务挂起/恢复的核心流程。压缩包含89个文件365KB以45个C源码和36个头文件构成主体框架涵盖UCOSII移植层PORT、内核核心CORE、硬件驱动HARDWARE/LED、系统服务SYSTEM及Keil工程配置文件uvprojx/uvoptx结构规范便于逐层剖析移植逻辑与任务管理代码。目前已有926人学习下载配套README与清晰目录划分特别适合课程设计实践、RTOS入门调试及UCOSII二次开发参考。1. UCOSII任务调度不是“多线程模拟”而是抢占式内核在STM32上的硬实时落地很多初学者把UCOSII任务调度理解成“Keil里跑几个while(1)循环”结果一上电DS0和DS1全灭串口没输出J-Link连上却读不到任务状态——这不是代码写错了是根本没理解UCOSII在STM32F10x上运行的物理前提它不依赖编译器调度器也不靠SysTick裸延时而是通过PendSV异常SysTick中断堆栈切换三者协同在Cortex-M3内核级完成任务上下文保存与恢复。本实验用V2.91版本在STM32F103ZET6HD系列上实测三个任务StartTask创建LED0Task和LED1Task后立即调用OSTaskSuspend(OS_PRIO_SELF)挂起自身LED0Task控制PB5DS0每秒亮80ms靠OSTimeDlyHMSM(0,0,0,80)实现精确毫秒级阻塞LED1Task控制PE5DS1执行300ms亮/300ms灭周期使用OSTimeDly(300)配合OSTimeSet(0)重置系统节拍。所有GPIO初始化、SysTick配置、OSInit()和OSStart()调用顺序必须严格遵循《uC/OS-II V2.91移植手册》第4章要求否则OSTaskCreate()返回ERR_TASK_CREATE_FAIL——这不是函数调用失败是内核未就绪导致的优先级队列初始化中断。2. STM32F10x平台下UCOSII V2.91移植关键路径解析2.1 Cortex-M3内核适配层必须重写core_cm3.c中的OS_CPU_SysTickHandlerUCOSII V2.91标准移植包中core_cm3.c默认使用ARM官方CMSIS库的SysTick_Handler但该函数不调用OSIntEnter()和OSIntExit()导致OSTimeDly()阻塞失效。实际工程中必须将startup_stm32f10x_hd.s中原始的SysTick_Handler重定向到UCOSII专用入口// 在stm32f10x_it.c中修改 void SysTick_Handler(void) { OSIntEnter(); // 通知内核进入中断 OSTimeTick(); // UCOSII节拍服务函数 OSIntExit(); // 退出中断并检查任务切换 }提示若未调用OSIntEnter()/OSIntExit()OSTimeDly()会直接返回而不挂起任务LED0Task陷入死循环点亮PB5DS0常亮——这是最常见的“任务不调度”现象本质是内核节拍中断未被识别。2.2 任务栈空间分配必须匹配STM32F10x内存映射特性STM32F103ZET6的SRAM为64KB0x20000000~0x2000FFFF但UCOSII任务栈需连续内存块且不能跨越Bank边界。实验中三个任务栈定义如下见OS_CFG.H#define TASK_START_STK_SIZE 128 // StartTask栈仅执行创建操作128字512字节 #define TASK_LED0_STK_SIZE 256 // LED0Task栈含OSTimeDlyHMSM参数压栈256字1024字节 #define TASK_LED1_STK_SIZE 256 // LED1Task栈同上对应栈数组声明必须显式指定地址段// 在main.c中定义 CPU_STK TaskStartStk[TASK_START_STK_SIZE] __attribute__((section(.ramstack))); CPU_STK TaskLED0Stk[TASK_LED0_STK_SIZE] __attribute__((section(.ramstack))); CPU_STK TaskLED1Stk[TASK_LED1_STK_SIZE] __attribute__((section(.ramstack)));并在链接脚本STM32F10x_FLASH.ld中新增段声明.ramstack (NOLOAD) : { . ALIGN(4); *(.ramstack) . ALIGN(4); } RAM注意若栈未指定.ramstack段Keil默认将其放入.data区而.data初始化依赖SystemInit()后的SRAM使能导致OSTaskCreate()时栈指针指向未初始化RAM任务启动即触发HardFault。实测错误码为SCB-CFSR0x00008200UNALIGNED INVPC根源是PSP寄存器加载了非法栈顶地址。2.3 GPIO初始化必须绕过UCOSII临界区保护缺陷UCOSII V2.91的OS_ENTER_CRITICAL()在Cortex-M3上默认使用__disable_irq()但STM32F10x的GPIO时钟使能RCC-APB2ENR需在临界区外完成。实验中LED初始化代码必须拆分为两阶段// 第一阶段使能时钟不可在临界区内 RCC-APB2ENR | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPEEN; // 第二阶段配置GPIO可在临界区内 OS_ENTER_CRITICAL(); GPIOB-CRH ~(0xF 20); // 清PB5模式位 GPIOB-CRH | (0x2 20); // PB5推挽输出 GPIOE-CRL ~(0xF 20); // 清PE5模式位 GPIOE-CRL | (0x2 20); // PE5推挽输出 OS_EXIT_CRITICAL(); // 第三阶段设置初始电平避免上电闪烁 GPIOB-BSRR GPIO_BSRR_BS5; // PB51DS0灭 GPIOE-BSRR GPIO_BSRR_BS5; // PE51DS1灭表格GPIO引脚与UCOSII任务绑定关系任务名控制引脚硬件连接UCOSII优先级调度方式关键API调用StartTask——10一次性执行OSTaskCreate(), OSTaskSuspend()LED0TaskPB5DS08周期性阻塞OSTimeDlyHMSM(0,0,0,80)LED1TaskPE5DS17周期性阻塞OSTimeDly(300)3. 任务创建与调度验证的四层调试法3.1 编译期检查确认OS_CFG.H与处理器架构匹配UCOSII V2.91需通过OS_CPU.H中的宏定义告知内核目标架构。STM32F10x使用Cortex-M3必须确保// OS_CPU.H中必须有 #define OS_CPU_ARM_CortexM3 1 #define OS_CPU_ARM_THUMB 1 #define OS_CPU_ARM_V7M 1同时OS_CFG.H中关闭不必要功能以节省RAM#define OS_TASK_STAT_EN 0 // 关闭统计任务节省约200字节RAM #define OS_TASK_CREATE_EXT_EN 0 // 关闭扩展创建本实验无需 #define OS_EVENT_NAME_EN 0 // 关闭事件名调试时可开量产必关提示若OS_CPU_ARM_CortexM3未定义OS_CPU_SR_Save()会调用ARM7指令MRS R0, CPSR在Cortex-M3上触发UsageFault——此时Debug窗口显示PC停在0x00000000需检查汇编列表中OS_CPU_SR_Save的指令编码是否为0xE1A00000ARM7而非0xE8BD4000Cortex-M3的POP。3.2 下载后验证通过J-Link RTT Viewer捕获内核状态Keil MDK不支持UCOSII原生任务查看器需借助J-Link RTTReal Time Transfer实时抓取内核日志。在main.c中添加RTT初始化#include SEGGER_RTT.h void RTT_Init(void) { SEGGER_RTT_ConfigUpBuffer(0, RTT_LOG, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); }在StartTask中插入状态打印void StartTask(void *pdata) { INT8U err; // 创建LED任务 err OSTaskCreate(LED0Task, (void *)0, TaskLED0Stk[TASK_LED0_STK_SIZE-1], 8); if(err ! OS_ERR_NONE) { SEGGER_RTT_printf(0, LED0 create fail: %d\r\n, err); } err OSTaskCreate(LED1Task, (void *)0, TaskLED1Stk[TASK_LED1_STK_SIZE-1], 7); if(err ! OS_ERR_NONE) { SEGGER_RTT_printf(0, LED1 create fail: %d\r\n, err); } SEGGER_RTT_printf(0, All tasks created. StartTask suspending...\r\n); OSTaskSuspend(OS_PRIO_SELF); }连接J-Link后运行JLinkRTTClient.exe可实时看到LED0 create fail: 0 LED1 create fail: 0 All tasks created. StartTask suspending...若出现err3OS_ERR_PRIO_EXIST说明优先级8或7已被占用——需检查OS_CFG.H中OS_LOWEST_PRIO是否≥63STM32F10x默认64级优先级。3.3 运行时观测用逻辑分析仪验证任务切换精度LED0Task要求“每秒亮80ms”即亮灭周期为1000ms占空比8%。单纯用OSTimeDlyHMSM(0,0,0,80)无法保证精度因UCOSII节拍率OS_TICKS_PER_SEC默认为100Hz10ms/节拍80ms实际为8个节拍误差±5ms。实测需校准SysTick// 在OS_CPU_C.c中修改SysTick初始化 void OS_CPU_SysTickInit(void) { CPU_INT32U cnts; cnts (CPU_INT32U)(SystemCoreClock / OS_TICKS_PER_SEC); // 72MHz/100 720000 SysTick_Config(cnts); // 启动SysTick后立即清零计数器 SysTick-VAL 0; }用Saleae Logic抓取PB5波形应看到严格周期的80ms高电平脉冲相邻脉冲间隔1000ms。若间隔抖动10ms需检查OS_TICKS_PER_SEC是否被误设为5020ms节拍或2005ms节拍。3.4 故障注入测试强制触发任务切换验证抢占逻辑为验证LED1Task优先级7能否抢占LED0Task优先级8在LED0Task中插入高负载循环void LED0Task(void *pdata) { while(1) { GPIOB-BSRR GPIO_BSRR_BR5; // DS0灭 OSTimeDlyHMSM(0,0,0,80); // 亮80ms // 插入10ms计算负载模拟耗时操作 volatile int i; for(i0; i72000; i); // 72MHz下约10ms GPIOB-BSRR GPIO_BSRR_BS5; // DS0亮 OSTimeDlyHMSM(0,0,0,920); // 灭920ms凑足1s } }此时若LED1Task的300ms亮灭周期不变则证明抢占式调度生效若LED1Task延迟响应则检查OS_LOWEST_PRIO是否设置过小导致优先级反转。4. 基于UCOSII的任务间通信实战用消息队列解耦LED控制逻辑4.1 将LED控制从任务主体剥离为独立驱动模块当前实验中LED0Task直接操作GPIOB违反“任务只负责调度硬件操作由驱动封装”原则。新建led_drv.c实现消息驱动// led_drv.c #include ucos_ii.h #include led_drv.h OS_EVENT *LED_Q; // 消息队列句柄 void LED_DriverInit(void) { LED_Q OSQCreate((void **)LED_QBuf[0], LED_Q_SIZE); } void LED_SetState(INT8U led_id, INT8U state) { void *msg; switch(led_id) { case LED_DS0: msg (void*)(CPU_INT32U)(state ? 0x01 : 0x00); break; case LED_DS1: msg (void*)(CPU_INT32U)(state ? 0x02 : 0x00); break; default: return; } OSQPost(LED_Q, msg); // 发送消息 } // 在LED0Task中改为 void LED0Task(void *pdata) { void *msg; while(1) { msg OSQAccept(LED_Q, err); // 非阻塞接收 if(err OS_NO_ERR (CPU_INT32U)msg 0x01) { GPIOB-BSRR GPIO_BSRR_BS5; // 亮DS0 } OSTimeDlyHMSM(0,0,0,80); } }4.2 使用OSTimeDlyResume()实现外部事件唤醒当需要按键触发LED状态变更时传统轮询会浪费CPU资源。改用信号量OSTimeDlyResume()// 在按键中断服务程序中 void EXTI9_5_IRQHandler(void) { if(EXTI-PR EXTI_PR_PR5) { // PE5按键 OSSemPost(KeySem); // 释放信号量 EXTI-PR EXTI_PR_PR5; } } // 在LED1Task中 void LED1Task(void *pdata) { while(1) { OSSemPend(KeySem, 0, err); // 等待信号量 if(err OS_NO_ERR) { // 执行一键双控逻辑 GPIOE-BSRR GPIO_BSRR_BS5; // 切换DS1状态 } OSTimeDly(300); // 维持原周期 } }注意OSSemPend()必须在任务中调用且KeySem需在StartTask中创建KeySem OSSemCreate(0)。若忘记初始化OSSemPend()会立即返回OS_ERR_SEM_INVALID任务陷入死循环。5. STM32标准库环境下UCOSII任务栈溢出的精准定位技巧5.1 利用STM32F10x固件库的StackWaterMark功能标准库system_stm32f10x.c未提供栈水印检测但可通过修改OS_CPU_C.c中的OS_TaskStkInit()注入标记OS_STK* OS_TaskStkInit(OS_TASK_PTR task, void *pdata, OS_STK *ptos, OS_STK *pbos, INT32U stk_size) { OS_STK *stk; // 初始化栈底为0xA5A5A5A5水印值 for(stk pbos; stk ptos; stk) { *stk 0xA5A5A5A5; } // ... 原有初始化代码 return ptos; }在任务主循环中定期检查void LED0Task(void *pdata) { CPU_STK *sp; while(1) { // 检查栈底水印是否被覆盖 sp (CPU_STK*)__get_PSP(); // 获取当前任务PSP if(*((CPU_STK*)((CPU_INT32U)sp - 4)) ! 0xA5A5A5A5) { SEGGER_RTT_printf(0, LED0 stack overflow at 0x%08X\r\n, (CPU_INT32U)sp - 4); } // ... 任务逻辑 } }5.2 通过Keil μVision的Peripherals→Core Peripherals→SysTick验证节拍稳定性在Debug模式下打开Keil的Peripherals菜单选择SysTick观察VAL寄存器应随倒计时递减至0后自动重载LOAD值CTRL寄存器的COUNTFLAG位在每次中断后应短暂置1若VAL卡在非0值说明OSTimeTick()未被执行——检查SysTick_Handler是否被其他中断向量覆盖实测中常见问题startup_stm32f10x_hd.s中SysTick_Handler标号被Default_Handler覆盖需确认向量表第15项偏移0x3C指向正确地址。用readelf -s检查符号表可验证arm-none-eabi-readelf -s build/UCOSII-1.axf | grep SysTick_Handler # 正确输出应为123: 000082a1 2 FUNC GLOBAL DEFAULT 3 SysTick_Handler若地址为0x00000000说明链接时未解析符号需检查USE_STDPERIPH_DRIVER宏是否定义及stm32f10x_it.c是否加入编译。本文还有配套的精品资源点击获取