Chapter 05 · ARM 嵌入式 C 开发
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 函数。