简介本资源是一套面向嵌入式初学者与进阶开发者的STM32F103单片机实战例程聚焦UCOSII实时操作系统核心机制实践涵盖消息队列、信号量集与软件定时器三大关键组件的完整应用方案。适用于物联网终端开发、RTOS课程实验及毕业设计项目帮助读者深入理解任务间通信与同步机制在HAL库环境下的实现逻辑。压缩包共249个文件含121个头文件定义接口与配置、112个C源文件含HAL驱动、UCOSII移植层及应用任务代码、6个汇编启动与CPU相关文件如os_cpu_a.asm、cpu_a.asm以及keil工程配置uvprojx/uvoptx、烧录脚本bat和固件镜像hex整体仅1.36MB轻量易上手。已有121人学习下载所有代码均带中文注释接线定义明确写入源码支持J-Link与ST-Link双调试器适配并提供芯片型号与Flash容量迁移指引可快速适配同系列其他F103型号。1. 这不是“跑个UCOSII demo”那么简单STM32F103上用HAL库搭消息队列信号量集软件定时器的真实工程切口你手头有一块STM32F103C8T6最小系统板刚点亮LED想往“真正嵌入式应用”迈一步——但直接啃《UCOSII中文手册》第7章的OSQPost()和OSSemSet()发现汇编层os_cpu_a.asm报错、HAL_TIM_Base_Start_IT()和UCOSII时基节拍冲突、甚至OSSemPend()卡死在OS_ENTER_CRITICAL()里。这不是环境没配好而是HAL库与UCOSII的耦合点被教科书刻意绕开了。这个例程包的价值恰恰在于它把三处关键缝合点全摊开如何让HAL库的中断服务函数安全接入UCOSII任务调度上下文、怎样用OSQPost()传递传感器原始数据而不丢帧、为什么信号量集OSFlagPost()比单个信号量更适合多事件触发场景、以及软件定时器回调里调用OSQPost()为何必须用OSQPostFromISR()。它面向的是已会用HAL库点灯、串口、I2C正卡在“多任务协同”门口的开发者不是从零讲RTOS概念的入门课。2. HAL库与UCOSII的底层缝合从启动文件到临界区保护的硬核对齐2.1 启动流程重定向为什么os_cpu_a.asm必须替换原版CMSIS启动文件UCOSII要求所有中断入口必须经过其调度器接管而标准HAL库工程默认使用startup_stm32f103xb.s或.asm其NMI_Handler、HardFault_Handler等向量直接跳转到C函数。但UCOSII需要在进入任何中断前先执行OSIntEnter()退出时调用OSIntExit()更新就绪表。本例程中的os_cpu_a.asm正是为此重写; os_cpu_a.asm 片段ARM Thumb模式 IMPORT OSIntEnter IMPORT OSIntExit IMPORT OSTickISR ; UCOSII时基节拍中断处理函数 IMPORT EXTI9_5_IRQHandler ; 示例外部中断9-5组需接入UCOSII框架 AREA |.text|, CODE, READONLY, ALIGN2 THUMB CODE32 ; 替换原启动文件中的EXTI9_5_IRQHandler向量 EXPORT EXTI9_5_IRQHandler EXTI9_5_IRQHandler PUSH {R0-R3,R12,LR} ; 保存寄存器 BL OSIntEnter ; 通知UCOSII进入中断 BL EXTI9_5_IRQHandler_C ; 调用实际C处理函数用户编写 BL OSIntExit ; 通知UCOSII退出中断可能触发任务切换 POP {R0-R3,R12,PC} ; 恢复并返回提示此汇编文件必须放在KEIL工程的Startup组中且禁用原startup_stm32f103xb.s。若未替换OSIntEnter()不会被执行导致OSQPostFromISR()无法唤醒等待消息队列的任务现象是发送端调用成功但接收端永远阻塞。2.2 时基节拍SysTick的双重身份HAL库滴答与UCOSII节拍的共存方案UCOSII依赖精确周期性中断作为任务调度心跳而HAL库也使用SysTick做HAL_Delay()基础。二者冲突是新手最常踩的坑。本例程采用HAL库接管SysTick初始化UCOSII复用其中断服务的策略// main.c 中的SysTick配置关键 void SysTick_Handler(void) { HAL_IncTick(); // HAL库维护滴答计数器 OSIntEnter(); // UCOSII进入中断临界区 OSTimeTick(); // UCOSII更新所有延时任务状态 OSIntExit(); // UCOSII退出中断检查是否需任务切换 } // 在HAL_Init()之后、UCOSII启动前调用 void UCOSII_Init(void) { OSInit(); // 初始化UCOSII内核 // 创建任务... OSStart(); // 启动多任务调度此函数永不返回 }注意HAL_InitTick()不能调用因为HAL_Init()内部已初始化SysTick为1ms周期再调用会导致SysTick重配置失败。UCOSII的OS_TICKS_PER_SEC必须设为1000即1ms节拍与HAL库保持一致。若设为其他值如100HAL_Delay(10)将延迟100ms而非10ms。2.3 临界区保护的双层嵌套HAL_DISABLE_INTERRUPTS()与OS_ENTER_CRITICAL()的协作逻辑HAL库函数如HAL_I2C_Master_Transmit()内部会关闭全局中断__disable_irq()而UCOSII的OS_ENTER_CRITICAL()同样操作。若在UCOSII任务中直接调用HAL库I2C函数可能导致临界区嵌套失效。本例程通过宏定义重定向解决// os_cfg.h 中关键配置 #define OS_CRITICAL_METHOD 3 // 使用CPU_SR_ALLOC() OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL() // 在ucosii_config.h中添加避免HAL与UCOSII临界区冲突 #define HAL_DISABLE_INTERRUPTS() OS_ENTER_CRITICAL() #define HAL_ENABLE_INTERRUPTS() OS_EXIT_CRITICAL() // 用户代码中仍可写 HAL_I2C_Master_Transmit(hi2c1, SLAVE_ADDR, tx_buf, size, HAL_MAX_DELAY); // 实际执行时HAL库的关中断操作即调用OS_ENTER_CRITICAL()与UCOSII完全兼容关键宏原始HAL行为本例程重定向后行为风险规避点HAL_DISABLE_INTERRUPTS()__disable_irq()OS_ENTER_CRITICAL()防止UCOSII内核状态被HAL库意外修改HAL_ENABLE_INTERRUPTS()__enable_irq()OS_EXIT_CRITICAL()确保OS_EXIT_CRITICAL()正确恢复中断优先级掩码HAL_NVIC_SetPriority()直接写NVIC寄存器保留原行为但需确保优先级≤UCOSII最大中断优先级本例设为0x04避免高优先级中断抢占UCOSII内核3. 消息队列与信号量集的实战编码从传感器数据流到多事件协同3.1 消息队列OS_Q解决ADC采样数据跨任务传递的丢帧问题传统裸机做法是用全局数组标志位但多任务下易出现覆盖或读取不完整。本例程用OSQCreate()创建队列专用于传递16位ADC值// 定义消息队列指针 OS_Q *ADC_Q; // 在main()中创建队列容量10每个消息4字节 void AppTaskCreate(void) { ADC_Q OSQCreate(ADC Queue, 10); // 创建10个槽位的消息队列 if (ADC_Q NULL) { // 创建失败处理 } } // ADC中断服务函数HAL库回调 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { uint16_t adc_val HAL_ADC_GetValue(hadc); OS_ERR err; // 从ISR中发送消息必须用OSQPostFromISR OSQPost(ADC_Q, adc_val, // 指向数据的指针非数据本身 sizeof(uint16_t), // 数据大小 OS_OPT_POST_FIFO, // 先进先出 err); if (err ! OS_ERR_NONE) { // 发送失败队列满需丢弃或告警 } } // 接收任务独立任务函数 void ADC_Process_Task(void *p_arg) { OS_ERR err; uint16_t adc_data; void *p_msg; while (1) { p_msg OSQPend(ADC_Q, // 等待消息 0, // 无限等待 OS_OPT_PEND_BLOCKING, adc_data, // 存储接收到的数据 sizeof(uint16_t), // 缓冲区大小 NULL, // 无超时 err); if (err OS_ERR_NONE) { // 处理adc_data滤波、计算、发送到串口等 Process_ADC_Value(adc_data); } } }逻辑说明OSQPostFromISR()是唯一允许在中断服务函数中调用的队列发送API它内部自动调用OSIntExit()检查是否需任务切换。OSQPend()在任务中阻塞等待当队列有数据时立即唤醒避免轮询消耗CPU。参数sizeof(uint16_t)必须与发送端严格一致否则内存越界。3.2 信号量集OS_FLAG_GRP管理按键、串口接收、定时器超时的多事件触发单个信号量只能表示“有/无”而实际项目常需“按键按下 AND 串口收到指令 AND 定时器超时”才执行动作。本例程用信号量集实现// 定义信号量集句柄 OS_FLAG_GRP *EventFlags; // 创建信号量集32位标志每位代表一个事件 void AppTaskCreate(void) { EventFlags OSFlagCreate(Event Flags, 0x00000000, err); } // 按键中断服务GPIO EXTI void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin KEY_PIN) { OSFlagPost(EventFlags, 0x00000001, // 设置bit0按键事件 OS_OPT_POST_FLAG_SET, // 置位操作 err); } } // 串口接收完成回调 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart huart1) { OSFlagPost(EventFlags, 0x00000002, // 设置bit1串口事件 OS_OPT_POST_FLAG_SET, err); } } // 软件定时器回调见4.2节 void Timer_Callback(void *p_arg) { OSFlagPost(EventFlags, 0x00000004, // 设置bit2定时器事件 OS_OPT_POST_FLAG_SET, err); } // 协同任务等待任意组合事件 void Event_Coordinator_Task(void *p_arg) { OS_FLAGS flags; OS_ERR err; while (1) { // 等待按键 OR 串口 OR 定时器任意一个 flags OSFlagPend(EventFlags, 0x00000007, // 监控bit0-bit2 0, // 无限等待 OS_OPT_PEND_FLAG_SET_ANY, // 只要有一个置位即返回 NULL, err); if (err OS_ERR_NONE) { if (flags 0x00000001) Process_Key(); if (flags 0x00000002) Process_UART_Cmd(); if (flags 0x00000004) Process_Timeout(); } } }参数说明OS_OPT_PEND_FLAG_SET_ANY表示“任意一个标志置位即返回”OS_OPT_PEND_FLAG_SET_ALL则要求全部置位。0x00000007是掩码限定只关注低3位避免误响应其他事件。信号量集比多个独立信号量更节省RAM仅需1个结构体且支持原子性组合判断。4. 软件定时器的精准控制从毫秒级延时到周期性任务调度4.1 UCOSII软件定时器机制基于OS_TMR的独立时间管理单元UCOSII的软件定时器不依赖硬件Timer外设而是由时基节拍SysTick驱动的链表管理。每个定时器是一个OS_TMR结构体包含到期时间、周期、回调函数等。本例程创建两个定时器// 定义定时器句柄 OS_TMR *Blink_Tmr; // LED闪烁定时器200ms周期 OS_TMR *Sensor_Tmr; // 传感器采集定时器1s周期 // 创建定时器 void AppTaskCreate(void) { Blink_Tmr OSTmrCreate(Blink Timer, 0, // 初始延迟0ms 200, // 周期200ms OS_TMR_OPT_PERIODIC, // 周期性运行 Blink_Callback, // 回调函数 NULL, // 传给回调的参数 err); Sensor_Tmr OSTmrCreate(Sensor Timer, 1000, // 初始延迟1s 1000, // 周期1s OS_TMR_OPT_PERIODIC, Sensor_Callback, NULL, err); // 启动定时器 OSTmrStart(Blink_Tmr, err); OSTmrStart(Sensor_Tmr, err); } // LED闪烁回调在定时器任务上下文中执行 void Blink_Callback(void *p_tmr, void *p_arg) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } // 传感器采集回调触发ADC转换 void Sensor_Callback(void *p_tmr, void *p_arg) { HAL_ADC_Start_IT(hadc1); // 启动ADC中断转换 }逻辑说明软件定时器回调函数在UCOSII的定时器管理任务OSTmrTask()中执行该任务优先级通常设为OS_PRIO_LOWEST - 1。因此回调中不可调用OSTaskSuspend()等可能引起调度的API但可安全调用OSQPostFromISR()如Sensor_Callback中启动ADC后ADC中断再发消息。OSTmrCreate()的dly参数是首次触发延迟period是后续周期。4.2 软件定时器与HAL库Timer外设的协同避免资源争用虽然软件定时器足够用但某些场景如PWM输出、输入捕获必须用硬件Timer。本例程中stm32f1xx_hal_tim.c被用于驱动LED PWM其HAL_TIM_PWM_Start()与UCOSII无冲突但需注意// 在HAL_TIM_PeriodElapsedCallback()中调用UCOSII API void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim2) { // 假设TIM2用于PWM // 此处不能直接调用OSQPost()必须用FromISR版本 OSQPostFromISR(ADC_Q, pwm_duty, sizeof(uint16_t), OS_OPT_POST_FIFO, err); } }关键区别HAL_TIM_PeriodElapsedCallback()是硬件中断服务函数必须用OSQPostFromISR()而OSTmrCallback()是普通任务函数可用OSQPost()。混淆二者会导致系统崩溃。5. 工程级调试技巧快速定位消息队列阻塞、信号量集超时、定时器不触发5.1 消息队列状态实时监控用OSQQuery()诊断丢帧与积压当发现传感器数据处理变慢先查队列是否积压// 在调试任务中定期打印队列状态 void Debug_Task(void *p_arg) { OS_Q_DATA q_data; OS_ERR err; while (1) { OSQQuery(ADC_Q, q_data, err); // 获取队列当前状态 if (err OS_ERR_NONE) { printf(ADC_Q: Used%d, Free%d, NbrEntries%d\r\n, q_data.NbrUsed, q_data.NbrFree, q_data.NbrEntries); // 若NbrUsed长期为10说明消费者处理太慢或阻塞 if (q_data.NbrUsed 8) { // 触发告警降低采样率或优化处理函数 Adjust_ADC_Sample_Rate(); } } OSTimeDlyHMSM(0, 0, 1, 0, OS_OPT_TIME_HMSM_STRICT, err); // 延迟1秒 } }5.2 信号量集事件溯源用OSFlagQuery()验证事件触发路径若Event_Coordinator_Task()未按预期唤醒检查各事件是否真实发生// 在调试任务中查询信号量集当前值 void Flag_Debug_Task(void *p_arg) { OS_FLAG_GRP_DATA flag_data; OS_ERR err; while (1) { OSFlagQuery(EventFlags, flag_data, err); if (err OS_ERR_NONE) { printf(EventFlags: 0x%08X\r\n, flag_data.Flags); // 手动清除已处理的标志避免重复触发 if (flag_data.Flags 0x00000001) { OSFlagPost(EventFlags, 0x00000001, OS_OPT_POST_FLAG_CLR, err); } } OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_HMSM_STRICT, err); } }5.3 软件定时器精度校准用HAL库微秒级计时验证节拍误差UCOSII节拍受SysTick精度影响实测偏差// 在定时器回调中启动HAL微秒计时 void Precision_Test_Callback(void *p_tmr, void *p_arg) { uint32_t start_us HAL_GetTick() * 1000; // 粗略转换1ms1000us // 更精确做法用DWT_CYCCNT寄存器需使能DWT CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; // 执行耗时操作 for (int i 0; i 1000; i) __NOP(); uint32_t cycles DWT-CYCCNT; uint32_t us_elapsed cycles / (SystemCoreClock / 1000000); // 转换为微秒 printf(Timer callback overhead: %lu us\r\n, us_elapsed); // 若持续50us需检查回调函数复杂度或降低节拍频率 }实测经验在STM32F103C8T672MHz下空回调平均开销约12μs。若OSTmrCallback()中调用HAL_UART_Transmit()等阻塞函数将导致后续定时器严重延迟。务必保证回调函数轻量50μs重操作移至消息队列处理。本文还有配套的精品资源点击获取