在单片机开发中,调试串口是定位代码问题、监控运行状态的核心工具。本文将分享一套轻量级调试串口程序的设计思路,重点解决跨平台移植性与调试便利性难题,附完整代码框架与实战经验。
1. 调试串口的核心价值与现状痛点
为什么需要专用调试串口?
- 问题定位:实时输出变量状态、程序流信息 ;
- 无侵入监控:不打断程序执行流程(相比断点调试);
- 生产级诊断:现场设备运行日志记录 ;
常见调试方案对比
调试方式 | 优点 | 缺点 |
---|---|---|
硬件调试器 | 全功能调试 | 需要专用接口/成本高 |
printf串口输出 | 简单易用 | 代码膨胀/缺乏统一管理 |
自定义协议 | 灵活扩展 | 开发维护成本高 |
2. 框架设计目标与核心思路
2.1 设计目标
- 跨平台兼容:支持STM32/ESP32/CH32等主流MCU ;
- 代码极简:debugUart.c 和 debugUart.h 2个文件 ;
- 智能分级:无需复杂配置,适合快速验证 ;
- 可靠接收:中断驱动接收不丢数据 ;
2.2 适用场景
- 资源受限的MCU开发 ;
- 快速原型验证阶段 ;
- 需要可靠数据接收的简单通信 ;
2.3 架构设计
graph LR
A[应用层] --> B[调试输出(阻塞发送)]
C[中断接收] --> D[环形缓冲区]
D --> E[数据处理线程]
3. 核心代码实现(STM32示例)
3.1 通用接口定义
/* DEBUG 启用配置:
0 - 不启用;
1 - 启用;
*/
#ifndef DEBUG_ENABLE
#define DEBUG_ENABLE 1
#endif
// 毫秒延时函数
extern void TskOsDelayMs(unsigned int ms);
#define DEBUG_DLY_MS(ms) TskOsDelayMs(ms)
#if(1 == DEBUG_ENABLE)
// 调试信息输出打印接口
/* ' ## '的意思是,如果可变参数被忽略或为空,将使预处理器( preprocessor )
去除掉它前面的那个逗号 */
#define DEBUG_PRINTF(format,...) printf(format, ##__VA_ARGS__)
// 使用重构的 printf
extern void DebugMyPrintf(const char* format, ...);
//#define DEBUG_PRINTF(format,...) DebugMyPrintf(format, ##__VA_ARGS__)
// 16进制数据输出打印接口
extern void DebugUartPrintfHex(char* data, unsigned int dataLen);
#define DEBUG_PRINTF_HEX(hexData, len) DebugUartPrintfHex(hexData, len)
#else
#define DEBUG_PRINTF(format,...)
#define DEBUG_PRINTF_HEX(hexData, len)
#endif
void DebugUartInit(void); // 打印串口初始化
void DebugUartDeInit(void); // 打印串口反初始化
void DebugUartLoop(void); // 串口处理函数,处理接收数据
unsigned char DebugUartWrtie(char* data, unsigned int dataLen); // 原始数据发送接口
3.2 阻塞式发送函数
unsigned char DebugUartWrtie(char* data, unsigned int dataLen)
{
#if(1 == DEBUG_ENABLE)
if((!data) || (dataLen <= 0)) return 1;
if(0 == s_UsartDebugInit) return 2;
HAL_UART_Transmit(&DEBUG_HUART, (uint8_t*)data, dataLen, 0xFF);
#endif
return 0;
}
// 重定义fputc函数
int fputc(int ch, FILE *f)
{
#if(1 == DEBUG_ENABLE)
if(1 == s_UsartDebugInit) {
HAL_UART_Transmit(&DEBUG_HUART, (uint8_t)ch, 1, 4);
}
#endif
return ch;
}
3.3 接收中断环形缓冲处理
static void uartIrqCallback(UART_HandleTypeDef* uartHandle)
{
unsigned int flagTmp = (uartHandle->Instance)->SR;
if(!uartHandle) return ;
if(UART_FLAG_RXNE == (flagTmp & UART_FLAG_RXNE)) {
if(0 == s_debugRcv.posR ){
if(s_debugRcv.posW < DEBUG_USART_BUF_SIZE){
s_debugRcv.buf[s_debugRcv.posW++] = (uartHandle->Instance)->DR; // 读取数据后,会自动清除标志
}
}
else if(s_debugRcv.posR != (s_debugRcv.posW + 1)) {
s_debugRcv.posW = s_debugRcv.posW % DEBUG_USART_BUF_SIZE;
s_debugRcv.buf[s_debugRcv.posW++] = (uartHandle->Instance)->DR; // 读取数据后,会自动清除标志
}
}
__HAL_UART_CLEAR_OREFLAG(uartHandle);
HAL_UART_IRQHandler(uartHandle);
}
4. 使用操作步骤
4.1 调试打印使能配置
在 debugUart.h 文件,设置 DEBUG_ENABLE 是否启用调试打印。调试时开启,稳定发行版程序可禁用调试打印。
/* DEBUG 启用配置:
0 - 不启用;
1 - 启用;
*/
#ifndef DEBUG_ENABLE
#define DEBUG_ENABLE 1
#endif
#if(1 == DEBUG_ENABLE)
// 调试信息输出打印接口
/* ' ## '的意思是,如果可变参数被忽略或为空,将使预处理器( preprocessor )
去除掉它前面的那个逗号 */
#define DEBUG_PRINTF(format,...) printf(format, ##__VA_ARGS__)
// 使用重构的 printf
extern void DebugMyPrintf(const char* format, ...);
//#define DEBUG_PRINTF(format,...) DebugMyPrintf(format, ##__VA_ARGS__)
// 16进制数据输出打印接口
extern void DebugUartPrintfHex(char* data, unsigned int dataLen);
#define DEBUG_PRINTF_HEX(hexData, len) DebugUartPrintfHex(hexData, len)
#else
#define DEBUG_PRINTF(format,...)
#define DEBUG_PRINTF_HEX(hexData, len)
#endif
4.2 定义毫秒延时处理函数
在 debugUart.h 文件, 进行定义更改
// 毫秒延时函数
extern void TskOsDelayMs(unsigned int ms); // 可以是阻塞延时,也可以是任务延时; 用于数据接收完成处理
#define DEBUG_DLY_MS(ms) TskOsDelayMs(ms)
4.3 串口硬件配置
使用 STM32CubeMX 生成基础代码,包括打印串口的配置. 在 debugUart.c 文件配置硬件的使用:
// RX 端口
#define DEBUG_UART_RX_PORT GPIOA
#define DEBUG_UART_RX_PIN GPIO_PIN_10
#define DEBUG_UART_RX_PORT_CLK __HAL_RCC_GPIOA_CLK_ENABLE
#define DEBUG_UART_RX_AF GPIO_AF7_USART1 // 引脚复用
// TX 端口
#define DEBUG_UART_TX_PORT GPIOA
#define DEBUG_UART_TX_PIN GPIO_PIN_9
#define DEBUG_UART_TX_PORT_CLK __HAL_RCC_GPIOA_CLK_ENABLE
#define DEBUG_UART_TX_AF GPIO_AF7_USART1 // 引脚复用
// 使用串口
#define DEBUG_UART USART1
#define DEBUG_UART_CLK_EN __HAL_RCC_USART1_CLK_ENABLE // 串口时钟使能
#define DEBUG_UART_CLK_DISABLE __HAL_RCC_USART1_CLK_DISABLE // 串口时钟禁用
#define DEBUG_UART_IRQ USART1_IRQn // 中断类型
#define DEBUG_UART_IRQHandler USART1_IRQHandler // 串口的中断函数
#define DEBUG_UART_IT UART_IT_RXNE // 使用串口中断
// 要清除的中断标志位
#define DEBUG_CLEAR_FLAG (UART_FLAG_CTS | UART_FLAG_RXNE | UART_FLAG_TC)
// 串口通信配置
#define DEBUG_UART_BAUD 115200 // 波特率 115200
#define DEBUG_UART_DATA_BIT UART_WORDLENGTH_8B // 数据位
#define DEBUG_UART_STOP_BIT UART_STOPBITS_1 // 停止位
#define DEBUG_UART_PARITY UART_PARITY_NONE // 校验位
// 根据使用串口 更改此处定义
extern UART_HandleTypeDef huart1;
#define DEBUG_HUART huart1
#define DEBUG_USART_BUF_SIZE 1024 // 缓冲区大小
STM32CubeMX 生成基础代码中,对 usart.c 文件进行更改 :
UART_HandleTypeDef huart1;
extern void Debug_USART1_MspInit(UART_HandleTypeDef* uartHandle);
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance == USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE BEGIN USART1_MspInit 1 */
Debug_USART1_MspInit(uartHandle); // 自定义函数实现硬件的自主可控
/* USER CODE END USART1_MspInit 1 */
}
}
// 串口初始化的时候,不使用函数 MX_USART1_UART_Init() ,而是 DebugUartInit()
4.4 串口初始化
在 main 函数里,初始化串口
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
LedInit();
//MX_USART1_UART_Init();
DebugUartInit(); // 调试串口初始化
/* USER CODE BEGIN 2 */
DEBUG_PRINTF(" system start \r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
DebugUartLoop(); // 调试串口数据接收处理
HAL_Delay(500);
Led0Toggle();
Led1Toggle();
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
void DebugUartLoop(void)
{
char* bufTmp = NULL;
unsigned int len = 1024;
len = DebugUartRead(&bufTmp, len);
if(len == 0) {
return ;
}
// 打印接收到的数据
DEBUG_PRINTF_HEX(bufTmp, len);
// 在此处,添加协议解析函数进行协议解析处理
{
}
DebugUartStartReceive(); // 重新开始接收
}
若芯片平台采用的是标准库或寄存器操作,只需对 debugUart.c 硬件初始化进行更改即可。
5. 完整代码地址
本示例是使用正点原子 STM32F407 开发板,进行演示。