UART 串口通信
从帧格式原理到三种收发模式,实现环形缓冲区与 printf 重定向,掌握嵌入式调试最核心的工具。
从帧格式原理到三种收发模式,实现环形缓冲区与 printf 重定向,掌握嵌入式调试最核心的工具。
/** * @brief USART1 初始化 115200-8N1 * 引脚:PA9(TX)=AF7_USART1TX,PA10(RX)=AF7_USART1RX * 挂在 APB2,时钟 84MHz */ UART_HandleTypeDef huart1; void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA9 TX、PA10 RX 复用推挽 */ GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1); } /* ── 轮询发送(阻塞)── */ void UART_SendString(const char *str) { HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 100); } /* ── 轮询接收(阻塞,超时 1 秒)── */ HAL_StatusTypeDef UART_ReceiveByte(uint8_t *byte) { return HAL_UART_Receive(&huart1, byte, 1, 1000); }
/* ─── 环形缓冲区定义 ─────────────────────────────── */ #define RX_BUF_SIZE 256 typedef struct { uint8_t buf[RX_BUF_SIZE]; volatile uint16_t head; /* 写指针(ISR 更新)*/ volatile uint16_t tail; /* 读指针(主循环更新)*/ } RingBuf_t; static RingBuf_t uart_rx_buf = {{0}, 0, 0}; static uint8_t uart_rx_byte; /* HAL 中断接收单字节临时变量 */ static inline uint8_t RingBuf_Empty(RingBuf_t *rb) { return rb->head == rb->tail; } static inline uint8_t RingBuf_Full(RingBuf_t *rb) { return ((rb->head + 1) % RX_BUF_SIZE) == rb->tail; } static inline void RingBuf_Push(RingBuf_t *rb, uint8_t data) { if (!RingBuf_Full(rb)) { rb->buf[rb->head] = data; rb->head = (rb->head + 1) % RX_BUF_SIZE; } /* 满了则丢弃,实际应用可加溢出标志 */ } static inline uint8_t RingBuf_Pop(RingBuf_t *rb, uint8_t *data) { if (RingBuf_Empty(rb)) return 0; *data = rb->buf[rb->tail]; rb->tail = (rb->tail + 1) % RX_BUF_SIZE; return 1; } /* ─── 启动中断接收(单字节模式)─────────────────── */ void UART_StartIT(void) { HAL_UART_Receive_IT(&huart1, &uart_rx_byte, 1); } /* ─── 接收完成回调(每收到1字节触发一次)─────── */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { RingBuf_Push(&uart_rx_buf, uart_rx_byte); HAL_UART_Receive_IT(&huart1, &uart_rx_byte, 1); /* 重新挂起接收 */ } } /* ─── 主循环中处理接收数据 ────────────────────── */ void UART_ProcessRx(void) { uint8_t ch; while (RingBuf_Pop(&uart_rx_buf, &ch)) { /* 处理每个字节,例如回显 */ HAL_UART_Transmit(&huart1, &ch, 1, 10); } }
/* retarget.c — 重写 fputc,将 printf 输出到 UART1 */ #include "stdio.h" #include "stm32f4xx_hal.h" extern UART_HandleTypeDef huart1; /* Keil MDK 重写 fputc */ #ifdef __ARMCC_VERSION int fputc(int ch, FILE *f) { (void)f; HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100); return ch; } #endif /* GCC/Newlib 重写 _write */ #ifdef __GNUC__ int _write(int fd, char *ptr, int len) { (void)fd; HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, 1000); return len; } #endif /* 使用示例 */ /* printf("温度: %.2f°C, 湿度: %.1f%%\r\n", temp, humi); */ /* printf("[%lu] GPIO PA0 = %d\r\n", HAL_GetTick(), state); */
/** * @brief UART1 DMA+IDLE 接收不定长数据(最佳实践) * DMA 持续循环接收,IDLE 中断触发时读取已收字节数 */ #define DMA_RX_BUF_SIZE 128 static uint8_t dma_rx_buf[DMA_RX_BUF_SIZE]; void UART_DMA_IDLE_Init(void) { /* 启动 DMA 循环接收到 dma_rx_buf */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_rx_buf, DMA_RX_BUF_SIZE); __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); /* 禁止半满中断(可选)*/ } /* DMA+IDLE 接收完成回调(STM32 HAL 扩展函数)*/ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { /* Size = 本次实际收到的字节数 */ /* dma_rx_buf[0..Size-1] 是本帧数据 */ Process_Packet(dma_rx_buf, Size); /* 重新挂起 DMA 接收 */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_rx_buf, DMA_RX_BUF_SIZE); __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); } } void Process_Packet(uint8_t *data, uint16_t len) { /* 解析协议,例如 Modbus RTU、AT指令等 */ HAL_UART_Transmit(&huart1, data, len, 100); /* 回显 */ }
STM32 TX → MAX232 T1IN → MAX232 T1OUT → PC RS232 RX
STM32 RX ← MAX232 R1OUT ← MAX232 R1IN ← PC RS232 TX
RS485(半双工):STM32 TX → MAX485 DI;MAX485 RO → STM32 RX;
DE/RE 引脚用 GPIO 控制发送/接收切换方向。发送前拉高 DE,发送完后立即拉低。
乱码:两端波特率不一致,或时钟配置错误(先验证 SystemClock_Config 是否成功)。
只发不收:TX/RX 接线交叉(TX 接对方 RX),或未开启接收中断/DMA。
丢字节:未使用环形缓冲区,ISR 处理过慢,新字节覆盖了旧数据。
printf 无输出:Keil 未关闭 semihosting,或 GCC 未实现 _write 函数。
Modbus RTU 是工业领域最广泛使用的串口通信协议,基于 RS485 物理层,主从架构,每帧以 3.5 个字符时间的静默间隔作为帧边界:
/* 简化版 Modbus RTU CRC-16 计算 */ uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; /* 多项式 0xA001 是 0x8005 的位反转 */ else crc >>= 1; } } return crc; } /* 读保持寄存器请求:FC=0x03 * 主站请求:[从机地址][0x03][起始寄存器高][起始寄存器低][数量高][数量低][CRC低][CRC高] * 从机响应:[从机地址][0x03][字节数][数据0高][数据0低]...[CRC低][CRC高] */ void Modbus_ReadHoldingRegs(uint8_t slave_addr, uint16_t start_reg, uint16_t count) { uint8_t frame[8]; frame[0] = slave_addr; frame[1] = 0x03; /* 功能码:读保持寄存器 */ frame[2] = (start_reg >> 8) & 0xFF; /* 起始地址高字节 */ frame[3] = start_reg & 0xFF; /* 起始地址低字节 */ frame[4] = (count >> 8) & 0xFF; /* 寄存器数量高字节 */ frame[5] = count & 0xFF; /* 寄存器数量低字节 */ uint16_t crc = Modbus_CRC16(frame, 6); frame[6] = crc & 0xFF; /* CRC 低字节在前 */ frame[7] = (crc >> 8) & 0xFF; HAL_UART_Transmit(&huart1, frame, 8, 100); }
STM32 UART 通信三种接收模式:轮询(适合简单场景,会阻塞 CPU)、中断(每字节触发 ISR,适合低速小数据量)、DMA(硬件自动搬运,结合空闲中断实现变长帧接收,最高效)。printf 重定向通过实现 _write 函数配合 huart 句柄完成。RS232 需要 MAX232 电平转换;RS485 半双工需要 GPIO 控制 DE/RE 方向切换。Modbus RTU 是工业串口通信的事实标准,帧格式为:从机地址 + 功能码 + 数据 + CRC16(低字节在前)。DMA 接收变长数据帧推荐使用"DMA 循环接收 + UART 空闲中断"方案。