轻量级单片机调试串口设计:打造高移植性调试框架

15 阅读6分钟

在单片机开发中,调试串口是定位代码问题、监控运行状态的核心工具。本文将分享一套轻量级调试串口程序的设计思路,重点解决跨平台移植性与调试便利性难题,附完整代码框架与实战经验。


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 通用接口定义

debugUart.h

/* 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 开发板,进行演示。

debugUart 源程序及用例