
简介本资源是一套面向嵌入式初学者与进阶开发者的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机制消息队列解决重复消费与内存碎片、信号量集替代单信号量实现多事件等待、软件定时器规避硬件定时器资源冲突。它不追求功能堆砌而是提供一套可逐行调试、参数可调、失败有回溯路径的最小可行验证集——适合正在从标准外设库转向HALRTOS组合开发的嵌入式工程师尤其当你已遇到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要求所有指定事件位均为1OS_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)); // 假设10Hz100ms/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)或检查NbrCurEntriesOSQPend()永不返回接收任务未创建/被挂起/优先级过低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()参数更高效。本文还有配套的精品资源点击获取