Chapter 05 · ARM 嵌入式 C 开发

UART 串口通信

从帧格式原理到三种收发模式,实现环形缓冲区与 printf 重定向,掌握嵌入式调试最核心的工具。

课程进度50%

关键概念

UART vs USART
UART(通用异步收发器)只支持异步通信,无需时钟信号线,TX/RX 两根线即可双向通信。USART 在 UART 基础上增加同步模式(需要时钟线 CK),STM32 上大多数是 USART,通常工作在异步模式。
波特率(Baud Rate)
每秒传输的符号数,异步串口下等于 bps。常见值:9600、115200、921600。波特率越高,传输越快,对线路质量和时钟精度要求越高。STM32 的 BRR 寄存器 = APB 时钟 / 波特率。
串口帧格式
起始位(1 bit 低电平)+ 数据位(通常 8 bit,LSB 先发)+ 可选奇偶校验位 + 停止位(1 或 2 bit 高电平)。空闲时线路保持高电平,起始位下降沿通知接收方数据开始。
RS232 vs RS485
RS232:单端信号,电压 ±3V~±15V,点对点,最远 15 米。RS485:差分信号(A-B),抗干扰强,最远 1200 米,支持总线多节点。STM32 GPIO 是 3.3V TTL,需转换芯片(MAX232/MAX485)才能连接 RS232/RS485。
三种接收方式
轮询:CPU 主动等待,阻塞式,简单但效率低。中断:数据到来触发 ISR,CPU 及时响应,适合低速场景。DMA:硬件自动搬运数据到内存,CPU 零参与,适合高速大量数据传输。
环形缓冲区(Ring Buffer)
固定大小数组模拟的循环 FIFO。写指针(ISR 写入)和读指针(主循环消费)独立移动,写满后回绕到头部。是串口中断接收的标准解耦方案,避免 ISR 与主循环直接耦合。
printf 重定向
C 标准库 printf() 最终调用 fputc() 输出字符。重写 fputc(),将字符通过 UART 发送,即可在 PC 串口助手看到调试输出。Keil 需关闭 semihosting(添加 retarget.c),GCC 需实现 _write()。
IDLE 空闲帧检测
STM32 USART 有 IDLE 中断:总线在接收若干数据后保持空闲(一帧时间无数据)时触发。常与 DMA 结合,用于接收不定长数据包——DMA 持续接收,IDLE 中断判断一帧结束,读取 DMA 已接收的字节数。

串口帧格式图解

UART 帧格式(8N1:8数据位,无奇偶,1停止位) 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止 空闲 ─────┐ ┌───┬───┬───┬───┬───┬───┬───┬───┐ ───── HIGH │ │ │ │ │ │ │ │ │ │ HIGH │ LOW │ LSB MSB │ └──────┘ └───┴───┴───┴───┴───┴───┴───┘ 起始位下降沿 ←── 数据位(低位先发)──────────→ 停止高电平 发送字符 'A'(0x41 = 0100_0001): LOW → 1 → 0 → 0 → 0 → 0 → 0 → 1 → 0 → HIGH 115200 波特率速算: 每 bit = 1/115200 ≈ 8.68 μs 一帧(10 bit) ≈ 86.8 μs 最大吞吐 ≈ 11520 字节/秒 BRR 寄存器: USARTDIV = APBx 时钟 / 波特率 APB2=84MHz, 115200bps → BRR = 84000000/115200 ≈ 729

UART 初始化(HAL)

/**
 * @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);
  }
}

printf 重定向

/* 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); */

DMA + IDLE 接收不定长数据包

/**
 * @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);  /* 回显 */
}
RS232/RS485 电平转换接线

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 函数。