--- 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`** ```c #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`** ```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`** ```c #include "stm32f10x.h" #include // 使用 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 库发送接收函数**: ```c // 发送(超时模式) 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 库版)**: ```c // 基于 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帧时间),不是"数据接收完毕"