STM32 FreeRTOS自定义UART日志器出现多帧数据合并异常问题求助
我在STM32上基于FreeRTOS做了一个自定义UART日志器,用DMA实现串口传输。现在遇到一个棘手的问题:两个任务每隔500ms分别发送固定格式的日志消息,还有一个任务每隔10ms负责把队列里的消息通过串口发送到PC,但有时候不同任务的消息帧会莫名合并在一起。比如任务1的[ data ]:task1belbel和任务2的[status]:task2blabla,会被混拼成[ data ]:tas[ data ]:ta[status]:task2blabla这种错乱的内容。我已经尝试用互斥锁、消息队列和信号量做同步,但问题依然存在,麻烦大家帮我排查下代码里的问题。
我的代码实现如下:
Logger.c
#include "Logger.h" #include "BSP_Logger.h" #include "cmsis_os.h" #include "string.h" #define PREFIX_SIZE 9 /* size of a message descriptor*/ /* transmission buffer , non circular*/ uint8_t TransmitBuf[40]; extern osMutexId_t LoggerMutexHandle; extern osMessageQueueId_t LoggerQueueHandle; extern osSemaphoreId_t LoggerSemHandle; /* message descriptors */ const char * const logPrefix[]={"[error ]:", "[status]:", "[ info ]:", "[ data ]:", "unknown: "}; const char * const task2comp={"[status]:task2blabla"}; const char * const task1comp={"[ data ]:task1belbel"}; /** @brief: function to place a message in a send queue * @param: str - pointer to a data to place in a queue * @param: len - lenght of data to place in a queue * @param: type - type of descriptor to add to a message * @retval: 0 - data is placed in a queue , 1 - data is not placed */ uint8_t LoggerSend(const char *str, uint8_t len,LogType type) { LogDesctiptor msg; if(str==NULL || len == 0) return 1; // Error: null string or zero length if(type>LOG_DATA) type=LOG_UNKNOWN; msg.type=type; if(osMutexAcquire(LoggerMutexHandle,0)!=osOK) return 1; if(len<=QUEUE_MAX_SIZE) msg.size=len; else msg.size=QUEUE_MAX_SIZE; memcpy(&msg.payload,str,msg.size); if(osMessageQueuePut(LoggerQueueHandle,&msg,0,0)!=osOK) return 1; osMutexRelease(LoggerMutexHandle); return 0; } /** @brief: function to transmit a single message */ void LoggerTransmit(void){ if(osSemaphoreAcquire(LoggerSemHandle,0)==osOK){ if(osMessageQueueGetCount(LoggerQueueHandle)){ if(osMutexAcquire(LoggerMutexHandle,0)==osOK){ LogDesctiptor msg; osMessageQueueGet(LoggerQueueHandle,&msg,NULL,0); memcpy(TransmitBuf,logPrefix[msg.type],PREFIX_SIZE); memcpy((TransmitBuf+PREFIX_SIZE),&msg.payload,msg.size); BSP_LoggerTransmit(TransmitBuf,PREFIX_SIZE+msg.size); osSemaphoreRelease(LoggerSemHandle); osMutexRelease(LoggerMutexHandle); } else osSemaphoreRelease(LoggerSemHandle); } else osSemaphoreRelease(LoggerSemHandle); } }
Logger.h
#ifndef __LOGGER_H #define __LOGGER_H /** @brief: driver for printing debug messages by USART multithread access supported */ #include <stdint.h> #define QUEUE_MAX_SIZE 20 typedef enum __attribute__((packed)){ LOG_ERROR=0, LOG_STATUS=1, LOG_INFO=2, LOG_DATA=3, LOG_UNKNOWN=4 }LogType; typedef struct{ LogType type; uint8_t size; uint8_t payload[QUEUE_MAX_SIZE]; }LogDesctiptor; /** @brief: function copies data to local buffer , transmission is delayed */ uint8_t LoggerSend(const char *str, uint8_t len,LogType type); /** @brief: function that sends data from local buffer */ void LoggerTransmit(void); #endif /* __LOGGER_H */
BSP_Logger.c
#include "cmsis_os.h" #include "BSP_Logger.h" extern UART_HandleTypeDef huart2; extern osSemaphoreId_t LoggerSemHandle; void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){ if(huart==&huart2){ osSemaphoreRelease(LoggerSemHandle); } } void BSP_LoggerTransmit(const uint8_t * buf,uint8_t size){ HAL_UART_Transmit_DMA(&huart2,buf,size); }
问题根源分析
经过排查,核心问题出在信号量的逻辑误用,还有几个细节需要修正:
信号量的使用完全反向
你设计信号量的初衷是同步DMA传输完成,但在LoggerTransmit中,刚启动DMA就立即释放了信号量。这会导致:- DMA是异步传输,启动后函数立即返回,此时数据还在后台发送
- 信号量被提前释放后,下一次定时的
LoggerTransmit会立即获取信号量,启动新的DMA传输 - 上一次的DMA还未完成,全局
TransmitBuf被新消息覆盖,或者HAL库直接中断当前传输,最终导致发送内容错乱、帧合并
TransmitBuf的线程安全问题
全局非环形的TransmitBuf在DMA传输期间绝对不能被修改,但信号量的错误使用导致新消息会覆盖正在传输的数据,直接引发内容混叠。冗余的互斥锁
FreeRTOS消息队列本身是线程安全的,osMessageQueuePut/osMessageQueueGet不需要额外互斥锁保护,你添加的互斥锁属于冗余操作。
修正后的解决方案
1. 修复信号量核心逻辑
信号量要用来保证DMA传输的独占性:只有上一次传输完成,才能启动下一次。正确流程:
- 初始化时信号量设为二值信号量(初始计数1)
LoggerTransmit先获取信号量(等待上一次传输完成)- 启动DMA后不要释放信号量,留到
HAL_UART_TxCpltCallback中完成传输后再释放
修正后的LoggerTransmit:
void LoggerTransmit(void){ // 等待信号量(用永久等待避免消息丢失,可根据需求调整超时) if(osSemaphoreAcquire(LoggerSemHandle, osWaitForever) == osOK){ if(osMessageQueueGetCount(LoggerQueueHandle) > 0){ LogDesctiptor msg; if(osMessageQueueGet(LoggerQueueHandle, &msg, NULL, 0) == osOK){ memcpy(TransmitBuf, logPrefix[msg.type], PREFIX_SIZE); memcpy(TransmitBuf + PREFIX_SIZE, msg.payload, msg.size); BSP_LoggerTransmit(TransmitBuf, PREFIX_SIZE + msg.size); // 此处不释放信号量,等待DMA完成回调释放 } else { // 取队列失败,释放信号量避免死锁 osSemaphoreRelease(LoggerSemHandle); } } else { // 队列无消息,释放信号量 osSemaphoreRelease(LoggerSemHandle); } } }
2. 移除冗余互斥锁
修正后的LoggerSend:
uint8_t LoggerSend(const char *str, uint8_t len, LogType type) { if(str == NULL || len == 0) return 1; if(type > LOG_DATA) type = LOG_UNKNOWN; LogDesctiptor msg; msg.type = type; msg.size = (len <= QUEUE_MAX_SIZE) ? len : QUEUE_MAX_SIZE; memcpy(msg.payload, str, msg.size); // 队列操作线程安全,用永久等待避免消息丢失 if(osMessageQueuePut(LoggerQueueHandle, &msg, 0, osWaitForever) != osOK){ return 1; } return 0; }
3. 额外优化建议
- 把
LoggerTransmit从定时任务改成队列触发:让线程在osMessageQueueGet处等待,有新消息时再唤醒,比定时轮询更高效 - 确保
LoggerSemHandle初始化为二值信号量(计数1)
修正后流程说明
- 初始状态:信号量计数1,允许第一次传输
LoggerTransmit获取信号量,计数变为0,禁止新传输- 启动DMA异步发送,函数返回
- DMA传输完成,进入
HAL_UART_TxCpltCallback释放信号量,计数变回1 - 下一次
LoggerTransmit可以获取信号量,启动新的传输
这样就保证了同一时间只有一次DMA传输在进行,TransmitBuf不会被中途覆盖,彻底解决帧合并问题。
内容来源于stack exchange

