简介:本资源是一套面向嵌入式初学者与进阶开发者的STM32F103单片机实战例程,聚焦UCOSII实时操作系统在HAL库环境下的核心机制实践,涵盖消息队列、信号量集与软件定时器三大关键功能模块,适用于物联网终端、工业控制等需多任务调度的嵌入式项目开发。压缩包共249个文件,主体为121个.h头文件(定义接口与配置)、112个.c源文件(含HAL驱动、UCOSII移植层及应用逻辑),辅以6个汇编启动与CPU相关文件(如os_cpu_a.asm、cpu_a.asm)、4个S文件及构建脚本bat等,整体体积仅1.36MB,结构紧凑、注释详尽,便于快速理解OS内核与外设协同逻辑。已有121人学习下载,代码严格基于KEIL平台开发,适配STM32F103全系列芯片,支持J-Link与ST-Link调试器切换,并在I2C、TIM等常用外设驱动中嵌入硬件连接说明,可直接运行、按需裁剪或迁移至同类项目。
1. 这不是简单的“UCOSII移植”,而是用HAL库在STM32F103上构建可验证的实时通信骨架
你手头有一块STM32F103C8T6最小系统板,刚配好STM32CubeMX生成了基础HAL工程,却卡在“任务间怎么安全传数据”这一步——串口打印乱码、信号量释放后接收任务不唤醒、软件定时器回调里调用OSQPost()失败……这些不是配置错误,而是HAL与UCOSII协同时的时序断层:HAL的阻塞式外设操作(如HAL_UART_Transmit())会阻塞当前任务,而UCOSII的OSTimeDly()又依赖SysTick中断;若SysTick被HAL的HAL_Delay()抢占或重定向,整个调度器就失准。本例程聚焦三个最常出问题的IPC机制:消息队列(解决重复消费与内存碎片)、信号量集(替代单信号量实现多事件等待)、软件定时器(规避硬件定时器资源冲突)。它不追求功能堆砌,而是提供一套可逐行调试、参数可调、失败有回溯路径的最小可行验证集——适合正在从标准外设库转向HAL+RTOS组合开发的嵌入式工程师,尤其当你已遇到OS_ERR_PEND_ISR或OS_ERR_Q_FULL但查不到源头时。
2. 消息队列:从内存分配到指针传递的零拷贝实践
UCOSII的消息队列本质是OS_Q结构体管理的指针数组,其可靠性取决于内存分配方式与指针生命周期管理。HAL库环境下,必须避开malloc()动态分配(栈空间不可控、无内存池保护),改用静态预分配+指针传递。本例程采用OS_QCreate()创建固定长度队列,关键在于队列元素类型定义与发送/接收逻辑的严格匹配。
2.1 静态内存池与队列初始化
// 定义消息结构体(避免结构体对齐导致指针偏移) #pragma pack(1) typedef struct { uint8_t cmd_id; // 命令ID uint16_t data_len; // 数据长度(≤64字节) uint8_t payload[64]; // 实际载荷 } msg_t; #pragma pack() // 静态消息缓冲区(16个消息,每个占用sizeof(msg_t)字节) static msg_t msg_pool[16]; static OS_Q *msg_q; // 消息队列句柄 // 在main()中初始化队列(必须在OSStart()前调用) void msg_queue_init(void) { OS_ERR err; // 创建消息队列:指向msg_pool首地址,长度16,每个元素大小为sizeof(msg_t) msg_q = OSQCreate((CPU_CHAR *)"MsgQ", (OS_MSG_QTY)16, &err); if (err != OS_ERR_NONE) { // 错误处理:LED闪烁或串口报错 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }提示:
#pragma pack(1)强制1字节对齐,防止编译器插入填充字节导致sizeof(msg_t)计算错误。若未加此声明,payload[64]前的data_len可能被对齐到2字节边界,使实际结构体大小变为70字节而非67字节,导致队列内存越界。
2.2 发送端:指针传递与内存所有权移交
// 任务A:采集传感器数据并发送 void TaskA(void *p_arg) { OS_ERR err; msg_t *msg_ptr; while (1) { // 1. 从静态池中获取空闲消息结构体(非阻塞) msg_ptr = &msg_pool[0]; // 简化示例:实际应使用循环索引或空闲链表 // 2. 填充消息内容 msg_ptr->cmd_id = 0x01; msg_ptr->data_len = 4; memcpy(msg_ptr->payload, &sensor_value, 4); // 3. 将指针入队(非拷贝!仅传递地址) OSQPost(msg_q, (void *)msg_ptr, // 传递指针本身 OS_OPT_POST_FIFO, // FIFO模式保证顺序 &err); if (err != OS_ERR_NONE) { // 队列满时处理:丢弃旧消息或触发告警 printf("MsgQ Full! Err: %d\r\n", err); } OSTimeDlyHMSM(0, 0, 0, 100, OS_OPT_TIME_HMSM_STRICT, &err); // 100ms周期 } }注意:
OSQPost()第二个参数是void*,此处直接传&msg_pool[i]地址。接收端取出后必须立即使用,因为发送端下次循环可能覆盖同一内存位置。若需长期持有,应在接收端malloc()新内存并memcpy(),但本例程为避免动态内存,采用“即用即弃”策略。
2.3 接收端:原子性取指针与防重复消费
// 任务B:处理消息 void TaskB(void *p_arg) { OS_ERR err; msg_t *recv_msg; CPU_TS ts; // 时间戳用于调试 while (1) { // 1. 阻塞等待消息(超时500ms) recv_msg = (msg_t*)OSQPend(msg_q, 500, // 超时时间(ms) OS_OPT_PEND_BLOCKING, &ts, &err); if (err == OS_ERR_NONE) { // 2. 原子性处理:读取后立即清空该内存位置(防重复消费) printf("Recv CMD: 0x%02X, Len: %d\r\n", recv_msg->cmd_id, recv_msg->data_len); // 处理payload数据... process_payload(recv_msg->payload, recv_msg->data_len); // 3. 关键步骤:将已处理消息结构体置零(标记为空闲) memset(recv_msg, 0, sizeof(msg_t)); } else if (err == OS_ERR_PEND_ABORT) { printf("Pend aborted!\r\n"); } else { printf("Q Pend timeout or error: %d\r\n", err); } } }核心逻辑:
memset(recv_msg, 0, sizeof(msg_t))是防重复消费的关键。若接收端未清空,发送端下一次循环可能复用同一地址,导致接收端误认为新消息。此做法牺牲部分内存利用率,但换来确定性行为——比依赖OSQFlush()更可控。
3. 信号量集:用OSFlagPost()实现多事件同步与优先级反转规避
UCOSII的信号量集(OS_FLAG_GRP)比单信号量强大得多:它允许一个任务等待多个事件的任意组合(AND/OR),且支持事件清除策略。在STM32F103上,典型场景是“等待串口接收完成且ADC采样结束且外部中断触发”——若用三个独立信号量,需三次OSSemPend(),不仅代码冗长,还可能因等待顺序引发优先级反转。本例程用信号量集统一管理三类事件,并通过OS_OPT_POST_NO_SCHED规避调度开销。
3.1 信号量集初始化与事件位定义
// 定义事件位掩码(每位代表一个事件源) #define EVENT_BIT_UART_RX (1u << 0) // 串口接收完成 #define EVENT_BIT_ADC_DONE (1u << 1) // ADC转换完成 #define EVENT_BIT_EXTI_TRIG (1u << 2) // 外部中断触发 static OS_FLAG_GRP *event_flags; // 信号量集句柄 static OS_ERR flag_err; void flag_group_init(void) { // 创建信号量集:名称"EventFlags",初始值0(所有事件未发生) event_flags = OSFlagCreate((CPU_CHAR *)"EventFlags", (OS_FLAGS)0, &flag_err); if (flag_err != OS_ERR_NONE) { // 初始化失败处理 Error_Handler(); } }注意:
OSFlagCreate()初始值设为0,表示所有事件位默认关闭。若需某事件初始有效(如系统启动时ADC已就绪),可设为EVENT_BIT_ADC_DONE。
3.2 中断服务程序中的事件置位
// 串口接收完成中断(HAL_UART_RxCpltCallback) void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 1. 置位UART_RX事件位(在中断中调用,必须用OS_OPT_POST_NO_SCHED) OSFlagPost(event_flags, EVENT_BIT_UART_RX, OS_OPT_POST_FLAG_SET, // 置1(非翻转) &flag_err); // 2. 重新启动DMA接收(保持持续监听) HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE); } } // ADC转换完成中断(HAL_ADC_ConvCpltCallback) void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { OSFlagPost(event_flags, EVENT_BIT_ADC_DONE, OS_OPT_POST_FLAG_SET, &flag_err); } } // 外部中断(EXTI Line4中断) void EXTI4_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_4) { OSFlagPost(event_flags, EVENT_BIT_EXTI_TRIG, OS_OPT_POST_FLAG_SET, &flag_err); } }关键参数:
OS_OPT_POST_FLAG_SET确保只置1不翻转,避免多次中断重复置位导致状态混乱;OS_OPT_POST_NO_SCHED告诉UCOSII“本次置位不触发任务调度”,因为中断中调用调度器风险极高——由中断退出后自动检查就绪任务。
3.3 任务中等待多事件组合
// 任务C:等待所有三个事件同时发生(AND模式) void TaskC(void *p_arg) { OS_FLAGS events; OS_ERR err; while (1) { // 等待EVENT_BIT_UART_RX && EVENT_BIT_ADC_DONE && EVENT_BIT_EXTI_TRIG events = OSFlagPend(event_flags, (EVENT_BIT_UART_RX | EVENT_BIT_ADC_DONE | EVENT_BIT_EXTI_TRIG), 0, // 无限等待 OS_OPT_PEND_FLAG_SET_ALL | // 必须全部为1才返回 OS_OPT_PEND_BLOCKING, 0, // 无时间戳 &err); if (err == OS_ERR_NONE) { // 所有事件到位,执行联合处理 printf("All events ready! Processing...\r\n"); joint_process(); // 清除已处理事件位(避免下次立即返回) OSFlagPost(event_flags, (EVENT_BIT_UART_RX | EVENT_BIT_ADC_DONE | EVENT_BIT_EXTI_TRIG), OS_OPT_POST_FLAG_CLR, // 清0 &flag_err); } else { printf("Flag wait error: %d\r\n", err); } } }参数详解:
OS_OPT_PEND_FLAG_SET_ALL要求所有指定事件位均为1;OS_OPT_PEND_FLAG_CLR在OSFlagPost()中清除对应位,否则下次OSFlagPend()会立即返回(因位仍为1)。此设计形成“事件-处理-清除”闭环,杜绝虚假唤醒。
4. 软件定时器:用OSTmrCreate()替代HAL_Delay()实现非阻塞延时
HAL库的HAL_Delay()底层依赖HAL_GetTick(),而HAL_GetTick()又基于SysTick中断更新。当UCOSII运行时,SysTick已被OS_CPU_SysTickHandler()接管,若再调用HAL_Delay()会导致SysTick中断嵌套或计数异常。本例程用UCOSII原生软件定时器(OSTMR)实现毫秒级精准延时,且支持单次/周期模式、回调函数、参数传递。
4.1 软件定时器创建与启动
static OS_TMR *led_blink_timer; // LED闪烁定时器句柄 static OS_TMR *data_upload_timer; // 数据上传定时器句柄 void timer_init(void) { OS_ERR err; // 创建LED闪烁定时器:1000ms周期,单次不自动重启 led_blink_timer = OSTmrCreate((CPU_CHAR *)"LED_Tmr", (OS_TICK)1000, // 1000ms OS_OPT_TMR_PERIODIC, // 周期模式 led_blink_callback, // 回调函数 (void *)0, // 传递给回调的参数 &err); if (err != OS_ERR_NONE) { Error_Handler(); } // 创建数据上传定时器:5000ms周期,带参数传递 data_upload_timer = OSTmrCreate((CPU_CHAR *)"Upload_Tmr", (OS_TICK)5000, OS_OPT_TMR_PERIODIC, data_upload_callback, (void *)&upload_config, // 传递配置结构体地址 &err); if (err != OS_ERR_NONE) { Error_Handler(); } // 启动定时器 OSTmrStart(led_blink_timer, &err); OSTmrStart(data_upload_timer, &err); } // LED闪烁回调函数 void led_blink_callback(void *p_tmr, void *p_arg) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } // 数据上传回调函数(接收参数) typedef struct { uint8_t interval_sec; uint16_t retry_count; } upload_cfg_t; upload_cfg_t upload_config = { .interval_sec = 5, .retry_count = 3 }; void data_upload_callback(void *p_tmr, void *p_arg) { upload_cfg_t *cfg = (upload_cfg_t*)p_arg; printf("Upload triggered! Interval: %ds, Retry: %d\r\n", cfg->interval_sec, cfg->retry_count); // 执行上传逻辑... }参数说明:
OS_OPT_TMR_PERIODIC启用周期模式;p_arg可传递任意指针,本例中upload_config结构体地址被安全传入回调,避免全局变量污染。定时器精度取决于OS_CFG_TMR_TASK_RATE_HZ(默认10Hz,即100ms分辨率),若需更高精度(如10ms),需在os_cfg.h中修改此宏并确保SysTick中断频率匹配。
4.2 定时器控制与状态查询
// 动态修改LED定时器周期(例如按键触发加速) void change_led_speed(uint32_t new_period_ms) { OS_ERR err; // 停止当前定时器 OSTmrStop(led_blink_timer, OS_OPT_TMR_STOP_ZERO, 0, &err); // 修改周期 OSTmrSet(led_blink_timer, (OS_TICK)new_period_ms, led_blink_callback, (void*)0, &err); // 重新启动 OSTmrStart(led_blink_timer, &err); } // 查询定时器剩余时间(用于调试) void debug_timer_remaining(void) { OS_ERR err; OS_TICK remaining; remaining = OSTmrRemainGet(led_blink_timer, &err); if (err == OS_ERR_NONE) { printf("LED Timer remaining: %d ticks (%d ms)\r\n", remaining, (int)(remaining * 100)); // 假设10Hz=100ms/tick } }注意:
OSTmrSet()可动态修改周期,但必须先OSTmrStop()。OSTmrRemainGet()返回剩余tick数,结合OS_CFG_TMR_TASK_RATE_HZ可换算为毫秒,是排查定时器未触发问题的首选手段。
5. 调试与排错:从OSView日志到寄存器级故障定位
当消息队列始终为空、信号量集等待永不返回、软件定时器回调缺失时,不能只盯着应用层代码。UCOSII提供OSView(需额外购买)和内置调试接口,而HAL库则暴露__HAL_DBGMCU_FREEZE_TIMx()等寄存器冻结功能。本节给出四类高频故障的可执行诊断路径。
5.1 消息队列空/满的根因分析表
| 现象 | 可能原因 | 验证命令/代码 | 解决方案 |
|---|---|---|---|
OSQPost()返回OS_ERR_Q_FULL | 发送端未等待接收端处理完即重复发送 | printf("Q Size: %d, Used: %d\r\n", msg_q->NbrEntries, msg_q->NbrCurEntries); | 在发送前加OSTimeDly(1)或检查NbrCurEntries |
OSQPend()永不返回 | 接收任务未创建/被挂起/优先级过低 | OS_TaskStat();查看任务状态;OS_TaskQuery()查具体任务 | 检查OSTaskCreate()返回值;确认任务优先级低于其他高优先级任务 |
接收端recv_msg内容乱码 | 指针传递时地址被覆盖或未对齐 | printf("Ptr: 0x%08X, Size: %d\r\n", (uint32_t)recv_msg, sizeof(msg_t)); | 加#pragma pack(1);发送端用memcpy()而非直接赋值 |
5.2 信号量集失效的硬件级检查
若OSFlagPend()卡死,需排除中断未触发:
// 在main()中添加中断使能验证 void interrupt_check(void) { // 1. 检查NVIC寄存器(以USART1为例) printf("USART1 IRQ EN: 0x%08X\r\n", NVIC->ISER[0]); // 查看bit37是否为1 printf("USART1 Pending: 0x%08X\r\n", NVIC->ISPR[0]); // 查看bit37是否置位 // 2. 检查外设中断标志(以EXTI为例) printf("EXTI PR: 0x%08X\r\n", EXTI->PR); // 查看对应bit是否为1 // 3. 检查HAL中断状态(需在中断回调开头加) // __HAL_GPIO_EXTI_GET_FLAG(GPIO_PIN_4); // 返回非零表示中断发生 }操作步骤:将上述代码放入
main()循环,用串口监视输出。若NVIC->ISER[0]中对应位为0,说明HAL_NVIC_EnableIRQ()未执行;若EXTI->PR对应位为0,说明外部中断未物理触发(检查按键电路或信号源)。
5.3 软件定时器不触发的SysTick校验
定时器依赖SysTick,需验证其是否正常工作:
// 在SysTick_Handler中添加计数器 volatile uint32_t systick_count = 0; void SysTick_Handler(void) { HAL_IncTick(); systick_count++; // UCOSII的OS_CPU_SysTickHandler()也在此处调用 OS_CPU_SysTickHandler(); } // 主循环中检查 if (systick_count < 10) { // 1秒内应至少10次(10Hz) printf("SysTick stuck! Count: %d\r\n", systick_count); systick_count = 0; }关键点:
systick_count必须为volatile,否则编译器可能优化掉。若计数停滞,检查HAL_InitTick()是否被重复调用(常见于CubeMX生成代码中HAL_Init()与OSInit()顺序错误)。
5.4 使用HAL库的调试辅助函数快速定位
利用HAL提供的底层函数绕过UCOSII封装:
// 直接读取GPIO状态(验证中断是否真触发) if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) { printf("KEY pressed physically!\r\n"); // 物理按键正常 } else { printf("KEY not pressed!\r\n"); // 检查硬件连接 } // 检查UART接收DMA状态(排除DMA未启动) if (__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TCIF1) != RESET) { printf("DMA Transfer Complete!\r\n"); } else { printf("DMA not complete!\r\n"); }技巧:当UCOSII任务无法响应时,用裸机式
HAL_GPIO_ReadPin()和__HAL_DMA_GET_FLAG()直接验证硬件层状态,可快速区分是RTOS调度问题还是外设驱动问题。此方法比反复检查OSFlagPost()参数更高效。
本文还有配套的精品资源,点击获取