news 2026/9/12 18:52:51

STM32F103+HAL库集成uC/OS-II实战:消息队列、信号量集与软件定时器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103+HAL库集成uC/OS-II实战:消息队列、信号量集与软件定时器

简介:本资源是一套面向嵌入式初学者与进阶开发者的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_HandlerHardFault_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, ALIGN=2 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库保持一致。若设为其他值(如100),HAL_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; // 粗略转换(1ms=1000us) // 更精确做法:用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,需检查回调函数复杂度或降低节拍频率 }

实测经验:在STM32F103C8T6@72MHz下,空回调平均开销约12μs。若OSTmrCallback()中调用HAL_UART_Transmit()等阻塞函数,将导致后续定时器严重延迟。务必保证回调函数轻量(<50μs),重操作移至消息队列处理。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 18:52:46

AI+电商:AI导购和AIGC商品图如何改变在线购物

一、一个小卖家用AI做商品图的故事摘要&#xff1a;本文通过一个小卖家的真实故事&#xff0c;系统梳理了AI在电商领域的六大应用场景——AI商品图生成、AI导购、个性化推荐、AI客服&#xff0c;以及随之而来的隐忧与未来趋势。AI商品图让商品拍摄成本从数万元降至几十元&#…

作者头像 李华
网站建设 2026/9/12 18:47:50

使用 Python 查询与写入 Loki:HTTP API 客户端实战指南

使用 Python 查询与写入 Loki&#xff1a;HTTP API 客户端实战指南 【免费下载链接】loki Like Prometheus, but for logs. 项目地址: https://gitcode.com/GitHub_Trending/lok/loki Loki 的 HTTP API&#xff08;/loki/api/v1/*&#xff09;是它与外部系统交互的通用接…

作者头像 李华
网站建设 2026/9/12 18:40:48

基于Neo4j的医疗知识图谱问答系统:从实体识别到Cypher查询

简介&#xff1a;基于Python的知识图谱医疗领域问答系统项目&#xff0c;面向Python学习者和知识图谱入门者&#xff0c;提供一套可完整运行的医疗领域问答系统源码与配套数据&#xff0c;适合作为期末大作业或课程设计参考。整个项目针对医疗知识结构化表示与自动问答实现进行…

作者头像 李华
网站建设 2026/9/12 18:40:28

SAP S/4HANA信用管理核心接口视图I_CreditManagementBP详解

干过 SAP Credit Management 项目的朋友都有一种感觉&#xff1a;信用管理这个模块&#xff0c;功能强大&#xff0c;但数据结构的复杂程度也是出了名的。尤其是当你想快速回答一个最简单的业务问题——“这个业务伙伴到底能不能给他放额度、放多少”——的时候&#xff0c;往往…

作者头像 李华
网站建设 2026/9/12 18:38:20

ML-KWS-for-MCU源码评测:Cortex-M语音唤醒的完整AI链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华