06-串口USART通信.md 12 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第8章:USART通信 + 配套代码07~12" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

串口USART通信

用生活理解:USART 串口通信就像两个人之间拉了一根电话线——一次只能一个人说(半双工),或者可以两个人同时说(全双工)。双方约定好说话的速度(波特率)、一句话的格式(数据位+校验+停止位),才能正常沟通。printf 重定向就是给这个电话线配了一个自动翻译机——把 printf 的内容自动通过串口发出去。


USART概述

通信分类

分类 定义 示例
单工 数据只在一个方向传输 广播
半双工 双方可以交替发送,但不能同时 对讲机
全双工 双方可同时发送 电话
同步 有时钟线同步 SPI、I2C
异步 无时钟线,约定波特率 USART

USART = Universal Synchronous/Asynchronous Receiver/Transmitter 通用同步/异步收发器。STM32F103 的 USART1 支持同步模式,USART2/3 仅异步。

异步串行协议帧格式

    空闲      起始位     数据位(8/9位)     校验位(可选)  停止位(1/2位)
    ──────┐  ┌──┐──┐──┐──┐──┐──┐──┐──┐──┌──┐────┐────────
           │  │  │  │  │  │  │  │  │  │  │  │    │
    ──────┘──┘  └──┘──┘──┘──┘──┘──┘──┘──┘──┘────┘────────
           ↑  LSB(先发)                    MSB(后发)
位数 说明
起始位 1 拉低电平,表示开始发送
数据位 8或9 先发 LSB(最低有效位)
校验位 0或1 奇校验/偶校验/无校验
停止位 1/1.5/2 拉高电平,表示传输结束

校验位计算

  • 无校验(No Parity):不发送校验位
  • 奇校验(Odd):数据位中"1"的个数为奇数时,校验位=0;为偶数时,校验位=1
  • 偶校验(Even):数据位中"1"的个数为偶数时,校验位=0;为奇数时,校验位=1

波特率

波特率 = 每秒传输的位数(bps)。

常用值:9600、19200、38400、115200、921600

STM32 USART 波特率计算公式

Tx/Rx 波特率 = fCK / (16 × USARTDIV)

其中: fCK = USART 时钟频率
      USARTDIV = BRR 寄存器中写入的分频值(含小数部分)

示例:fCK=72MHz(USART1 挂在 APB2),目标 115200bps USARTDIV = 72,000,000 / (16 × 115200) = 39.0625 BRR 寄存器写入值 = 0x271

参考:参考手册 §18(USART 寄存器描述)

各 USART 模块的频率差异

USART 总线 时钟频率
USART1 APB2 72MHz
USART2 APB1 36MHz
USART3 APB1 36MHz

注意:USART2/3 只有 APB1 时钟的一半,相同 BRR 值下波特率也减半。


USART功能框图

关键组成部分:

  1. 数据寄存器(DR):发送/接收数据的缓冲区
  2. 发送移位寄存器:将 DR 中并行数据串行化发送
  3. 接收移位寄存器:将 RXD 串行数据转为并行存入 DR
  4. 波特率发生器:基于 BRR 产生收发时钟
  5. 控制逻辑:CR1/CR2/CR3 寄存器控制各种模式

参考:参考手册 §18 中的 USART 功能框图

关键寄存器

寄存器 地址偏移 功能
SR 0x00 状态寄存器(TXE、RXNE、IDLE、TC 等标志位)
DR 0x04 数据寄存器(发送/接收共用)
BRR 0x08 波特率寄存器(USARTDIV 值)
CR1 0x0C 控制寄存器1(UE、TE、RE、M、PCE、PS、TXEIE、RXNEIE)
CR2 0x10 控制寄存器2(STOP 位配置、LINEN 等)
CR3 0x14 控制寄存器3(CTSE、RTSE、DMAT、DMAR)

SR 状态寄存器的重要位

名称 说明 用途
5 RXNE 接收数据寄存器非空 有数据收到时=1,读DR后自动清0
6 TC 发送完成 发送完成后=1
7 TXE 发送数据寄存器空 可写入数据时=1,写DR后自动清0
4 IDLE 空闲线路检测 收到空闲帧(高电平超过1帧)时=1

硬件电路设计

USART1 使用 PA9(TX)和 PA10(RX):

  • PA9 (USART1_TX):复用推挽输出(CNF=10, MODE=11)
  • PA10 (USART1_RX):浮空输入(CNF=01, MODE=00)

通常还需要连接 CH340G(USB转串口芯片)与电脑通信。

参考:开发板原理图串口部分、CH340G 数据手册


实验:串口收/发通信

需求描述

通过串口发送字符/字符串,接收并回显收到的数据。波特率 115200,8N1(8 数据位、无校验、1 停止位)。

软件设计(寄存器版 — 轮询方式)

项目路径stm32/07_usart_register

文件:stm32/07_usart_register/Hardware/USART/usart.h

#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"

void USART_Init(void);
void USART_SendChar(uint8_t ch);
uint8_t USART_ReceiveChar(void);
void USART_SendString(uint8_t *str, uint8_t size);
void USART_ReceiveString(uint8_t buffer[], uint8_t *size);

#endif

文件:stm32/07_usart_register/Hardware/USART/usart.c

#include "usart.h"

// USART1 初始化(PA9-TX, PA10-RX, 115200-8N1)
void USART_Init(void)
{
    /* ===== 1. 开启时钟 ===== */
    // USART1 挂载在 APB2 总线上
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;   // |=: USART1时钟使能
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;      // |=: GPIOA时钟使能

    /* ===== 2. 配置GPIO模式 ===== */
    // PA9 (TX): 复用推挽输出 CNF=10, MODE=11(50MHz)
    // CRH控制Pin8~Pin15, PA9的位域在第4~7位
    GPIOA->CRH |= GPIO_CRH_MODE9;             // MODE9[1:0]=11
    GPIOA->CRH |= GPIO_CRH_CNF9_1;             // CNF9[1]=1
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;            // CNF9[0]=0

    // PA10 (RX): 浮空输入 CNF=01, MODE=00
    GPIOA->CRH &= ~GPIO_CRH_MODE10;            // MODE10=00(输入)
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;           // CNF10[1]=0
    GPIOA->CRH |= GPIO_CRH_CNF10_0;            // CNF10[0]=1

    /* ===== 3. 配置USART ===== */
    // 3.1 波特率 115200 (72MHz / (16 * 115200) = 39.0625 = 0x271)
    USART1->BRR = 0x271;

    // 3.2 使能USART1、发送、接收
    USART1->CR1 |= USART_CR1_UE;   // UE=1: USART使能
    USART1->CR1 |= (USART_CR1_TE | USART_CR1_RE);  // TE=1: 发送使能, RE=1: 接收使能

    // 3.3 配置帧格式: 8位数据位, 无校验, 1位停止位
    USART1->CR1 &= ~USART_CR1_M;    // M=0: 8位数据位
    USART1->CR1 &= ~USART_CR1_PCE;  // PCE=0: 无校验
    USART1->CR2 &= ~USART_CR2_STOP; // STOP=00: 1位停止位
}

// 发送一个字符(轮询方式)
void USART_SendChar(uint8_t ch)
{
    // 等待 TXE (发送数据寄存器空) = 1
    // TXE=1 表示 DR 中数据已转移到移位寄存器,可以写入新数据
    while ((USART1->SR & USART_SR_TXE) == 0)  // &测试: TXE位是否为0
    {
    }

    // 写入数据到 DR,硬件自动移位发送
    USART1->DR = ch;
}

// 接收一个字符(轮询方式)
uint8_t USART_ReceiveChar(void)
{
    // 等待 RXNE (接收数据寄存器非空) = 1
    // RXNE=1 表示 DR 中已收到新数据
    while ((USART1->SR & USART_SR_RXNE) == 0)
    {
    }

    return USART1->DR;
}

// 发送字符串
void USART_SendString(uint8_t *str, uint8_t size)
{
    for (uint8_t i = 0; i < size; i++)
    {
        USART_SendChar(str[i]);
    }
}

// 接收字符串(检测空闲帧 IDLE 判断结束)
void USART_ReceiveString(uint8_t buffer[], uint8_t *size)
{
    uint8_t i = 0;

    // 逐字符接收直到检测到空闲帧(IDLE=1)
    while ((USART1->SR & USART_SR_IDLE) == 0)  // IDLE=0 时说明还在接收数据
    {
        buffer[i] = USART_ReceiveChar();
        i++;
    }

    // 读取 DR 清除 IDLE 标志(先读SR再读DR)
    USART1->DR;

    *size = --i;  // 减去IDLE引起的多余计数
}

软件设计(寄存器版 — printf重定向)

项目路径stm32/11_usart_printf_register

通过重写 fputc 函数,将 printf 输出重定向到 USART1。

文件:stm32/11_usart_printf_register/User/main.c

#include "stm32f10x.h"
#include <stdio.h>      // 使用 printf 需要

// 重写 fputc —— printf 底层调用此函数输出每个字符
// 参数: ch = 要输出的字符, f = 文件流指针(未使用)
// 返回: 输出成功的字符
int fputc(int ch, FILE *f)
{
    // USART1 发送一个字符
    while ((USART1->SR & USART_SR_TXE) == 0)  // 等待 TX 缓冲区空
    {
    }
    USART1->DR = ch;  // 写入字符到 DR

    return ch;
}

int main(void)
{
    USART_Init();  // USART初始化(与轮询版相同)

    printf("尚硅谷 STM32 串口 printf 实验\r\n");   // \r\n 是回车换行
    printf("当前系统时钟: 72MHz\r\n");
    printf("波特率: 115200, 数据格式: 8N1\r\n");

    while (1)
    {
    }
}

Keil 配置:要使用 printf,必须在 Keil 中勾选 "Use MicroLIB"(微库): Options for Target → Target → Code Generation → Use MicroLIB 微库提供了精简版的 printf 实现。


软件设计(HAL库版)

HAL 库封装了 USART 的寄存器操作,使用结构体 UART_HandleTypeDef 管理串口。

HAL 库串口配置流程

1. CubeMX 选择 USART1 异步模式
2. 配置波特率、数据位、校验位、停止位
3. 生成代码
4. HAL_UART_Transmit() 发送 / HAL_UART_Receive() 接收

HAL 库发送接收函数

// 发送(超时模式)
HAL_UART_Transmit(&huart1, (uint8_t *)"Hello", 5, 1000);
// 参数: huart句柄, 数据指针, 数据长度, 超时ms

// 接收(超时模式)
HAL_UART_Receive(&huart1, buffer, 10, 1000);

// 中断方式接收(推荐)
HAL_UART_Receive_IT(&huart1, buffer, 10);
// 接收10个字节后自动调用 HAL_UART_RxCpltCallback()

printf 重定向(HAL 库版)

// 基于 HAL_UART_Transmit 的重写
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
    return ch;
}

核心函数速查表

寄存器版

函数 说明 实现要点
USART_Init() 时钟+GPIO+波特率+帧格式 CR1/BRR/CR2 配置
USART_SendChar(ch) 发送1字节 轮询 TXE 标志
USART_ReceiveChar() 接收1字节 轮询 RXNE 标志
USART_SendString(str, len) 发送字符串 循环调用 SendChar
USART_ReceiveString(buf, &len) 接收字符串 使用 IDLE 判断结束

HAL库版

HAL函数 说明
HAL_UART_Transmit(huart, data, size, timeout) 轮询发送
HAL_UART_Receive(huart, data, size, timeout) 轮询接收
HAL_UART_Receive_IT(huart, data, size) 中断接收
HAL_UART_Transmit_IT(huart, data, size) 中断发送
HAL_UART_RxCpltCallback(huart) 接收完成回调

常见问题与避坑

  1. 串口无输出 → 检查波特率是否匹配、GPIO 模式(TX=复用推挽)、USART 时钟是否开启
  2. printf 不输出 → Keil 中是否勾选 Use MicroLIB、fputc 是否重写、TXE 轮询是否正确
  3. 接收数据乱码 → 波特率误差超过 3%(检查 BRR 计算)、数据位/校验位/停止位不匹配
  4. HAL_UART_Receive 阻塞 → 设置了超时但未收到足够数据;改用 _IT 中断方式
  5. USART1 和 USART2/3 波特率不同 → 它们时钟源不同(APB2=72MHz, APB1=36MHz),BRR 值需分别计算
  6. IDLE 中断逻辑不清 → IDLE 是"空闲帧"检测(连串高电平超过1帧时间),不是"数据接收完毕"