前言
上一章我们掌握了DMA直接存储器访问的核心原理与全场景实战,实现了零CPU占用的高速数据传输,而串口USART正是DMA最常用的联动外设,也是嵌入式设备与上位机、外设之间最通用、最基础的通信方式。对应51单片机的UART串口,其仅能通过定时器1实现固定波特率的基础收发,无硬件FIFO、无DMA支持,高速收发时极易丢包,且配置繁琐、容错率低;而STM32内置多路USART外设,支持同步/异步双模式、硬件流控、可编程波特率,可与DMA无缝联动,完美适配工业场景下的调试日志、指令交互、传感器数据上传等全场景需求。新手入门串口普遍面临三大痛点:数据乱码、收发丢包、中断卡死、DMA模式无法正常工作,且不清楚三种收发模式的选型逻辑。本章将严格遵循「先寄存器原理拆解,再HAL库封装逻辑」的顺序,联动51单片机对应知识点,从底层到实操全面吃透USART串口通信,完成工业级全场景收发实战。
本章目录
- 一、本章学习目标
- 二、核心知识点
- 2.1 串口通信基础与51单片机UART核心对比
- 2.2 USART底层工作原理与帧结构、波特率计算
- 2.3 三种收发模式核心原理与工业场景选型
- 2.4 USART核心寄存器与C语言位操作联动
- 2.5 HAL库USART封装逻辑与核心API深度解析
- 三、STM32CubeMX+Keil5保姆式实操:串口全场景收发实战
- 3.1 工程创建与基础配置
- 3.2 USART与DMA图形化配置
- 3.3 工程代码生成与三种模式业务代码编写
- 3.4 编译、烧录与效果验证
- 四、保姆式排错指南
- 五、我的踩坑记录
- 六、课后小练习(附完整标准答案)
- 6.1 基础巩固练习
- 6.2 进阶实战练习
- 七、核心知识点速记
- 八、本章小结
一、本章学习目标
- 掌握USART异步串口通信的底层工作原理,对比51单片机UART的核心差异,理解串口通信的帧结构、波特率计算规则,从根源解决数据乱码问题
- 吃透轮询、中断、DMA三种收发模式的底层工作流程、优缺点与工业场景选型逻辑,能根据业务需求选择最优收发方案
- 掌握USART寄存器级配置方法,理解HAL库USART的底层封装逻辑,能独立实现寄存器级的串口收发功能,实现51到STM32的无缝衔接
- 熟练使用HAL库完成轮询回显、中断指令控制、DMA+空闲中断不定长收发三大实战场景,代码符合工业级开发规范,可直接用于项目开发
- 能独立排查串口乱码、收发丢包、中断卡死、DMA不工作等高频问题,掌握串口调试的核心方法,为后续总线通信、物联网模块开发打下坚实基础
二、核心知识点
2.1 串口通信基础与51单片机UART核心对比
术语通俗解释:USART全称通用同步/异步收发器,UART为通用异步收发器,二者核心差异是USART支持同步通信(带时钟同步信号),UART仅支持异步通信,日常开发中99%的场景使用异步UART模式,俗称串口通信。异步串口通信的核心是两根线(TX发送、RX接收),无需同步时钟,收发双方通过约定好的波特率、帧结构实现全双工数据传输,类比两个人打电话,双方必须使用相同的语速、语言规则才能正常沟通。
我们从已掌握的51单片机UART出发,无缝衔接理解STM32 USART的核心优势:
| 核心特性 | 51单片机UART | STM32F103 USART | 对开发的核心影响 |
|---|---|---|---|
| 外设数量 | 仅1路UART,无法满足多外设通信需求 | 最多5路USART+2路UART,可同时对接上位机、蓝牙、GPS、4G等多个外设 | 无需软件模拟串口,硬件原生支持多设备通信,开发难度大幅降低 |
| 波特率配置 | 依赖定时器1溢出率实现,仅能实现有限波特率,误差大,115200波特率误差高达6.99% | 原生16位波特率寄存器,支持整数+小数分频,72MHz主频下115200波特率误差仅0.15%,支持300bps~4.5Mbps宽范围波特率 | 彻底解决波特率误差导致的乱码问题,支持高速率、高精度的串口通信 |
| 收发机制 | 无硬件FIFO,仅1字节数据缓存,必须在1字节接收时间内读取数据,否则会被覆盖,高速收发极易丢包 | 内置1字节硬件发送/接收缓存,配合DMA可实现无限深度的软件FIFO,支持硬件流控,高速收发零丢包 | 可实现高速大数据量的稳定传输,无需CPU频繁干预,避免丢包风险 |
| 收发模式 | 仅支持轮询、基础接收中断,无DMA支持,高速收发时CPU占用率100% | 支持轮询、中断、DMA三种收发模式,DMA模式下收发全程零CPU占用 | 三种模式可灵活适配不同场景,系统性能提升数十倍 |
| 帧结构支持 | 仅支持固定8位数据位、1位停止位,无硬件校验位,适配性差 | 支持可编程数据位(8/9位)、停止位(0.5/1/1.5/2位)、奇偶校验位,支持硬件流控CTS/RTS | 可适配工业标准Modbus-RTU等各类串口协议,无需软件模拟时序 |
2.2 USART底层工作原理与帧结构、波特率计算
1. 异步串口帧结构
异步串口通信无同步时钟,收发双方通过帧结构的起始位、停止位实现数据同步,一帧完整的数据结构如下(工业最常用配置):
空闲位 | 起始位(1位,低电平) | 数据位(8位,LSB低位在前) | 校验位(0/1位,可选) | 停止位(1位,高电平) | 空闲位
核心规则:收发双方的波特率、数据位、停止位、校验位必须完全一致,否则会出现数据乱码、无法识别的问题,这是串口通信的第一准则。
2. 波特率核心计算
波特率是指每秒传输的二进制位数,单位bps,工业常用波特率为9600、115200bps,其中115200是调试、数据传输的首选。
STM32 USART的波特率计算公式如下,联动C语言数值计算知识点:
波特率 = 外设时钟频率 / (16 × USARTDIV)
USARTDIV = 外设时钟频率 / (16 × 目标波特率)
- 外设时钟规则:USART1挂载在APB2总线上,72MHz主频下时钟频率为72MHz;USART2/3/4/5挂载在APB1总线上,时钟频率为36MHz。
- USARTDIV :是一个12位整数+4位小数的数值,存储在波特率寄存器
BRR中,高12位为整数部分,低4位为小数部分,实现精准的小数分频,彻底解决51单片机的波特率误差问题。
常用场景计算示例(USART1,72MHz时钟,115200bps):
USARTDIV = 72000000 / (16 × 115200) = 72000000 / 1843200 ≈ 39.0625
整数部分:39 → 十六进制0x27
小数部分:0.0625 × 16 = 1 → 十六进制0x1
BRR寄存器值 = 0x271
计算得到的波特率误差仅0.15%,远低于2%的最大允许误差,完全不会出现乱码问题。
2.3 三种收发模式核心原理与工业场景选型
STM32 USART支持三种收发模式,分别适配不同的业务场景,核心差异在于CPU的参与度与实时性,我们逐一拆解:
| 收发模式 | 底层工作原理 | 核心优势 | 核心劣势 | 工业场景选型 |
|---|---|---|---|---|
| 轮询模式 | CPU主动循环查询串口状态寄存器的发送/接收标志位,标志位置1后,手动读写数据寄存器完成收发 | 逻辑简单、无需中断、无回调函数,代码量少 | CPU全程阻塞,收发期间无法执行其他任务,高速收发易丢包,CPU占用率100% | 简单低速场景、一次性数据发送、调试日志打印、上电初始化指令发送 |
| 中断模式 | 串口收到数据/发送完成后,自动触发中断,CPU暂停当前任务,在中断回调函数中完成数据读写,完成后返回主程序 | 非阻塞,仅收发完成时触发中断,CPU占用率低,实时性好 | 高频收发时频繁触发中断,中断开销大,大数据量收发易丢包 | 中等速率、小数据量的指令交互、上位机指令控制、短数据包收发 |
| DMA模式 | 串口收到数据/发送数据时,自动触发DMA传输,数据直接从串口寄存器搬运到内存/从内存搬运到串口,全程无需CPU干预,传输完成后触发中断通知CPU | 全程零CPU占用,支持高速大数据量收发,零丢包,可实现循环缓存接收 | 配置相对复杂,需配合DMA控制器,需处理缓存溢出问题 | 高速大数据量传输、传感器数据连续上传、Modbus协议通信、物联网模块数据交互,是工业开发的首选方案 |
核心联动知识点:DMA模式完全复用了上一章学习的DMA传输原理,串口收发本质是外设与内存之间的数据传输,完美适配DMA的传输场景,实现零CPU占用的高速收发。
2.4 USART核心寄存器与C语言位操作联动
USART的所有功能均通过32位寄存器配置,我们以USART1为例,拆解核心寄存器的功能与C语言位操作实现,联动51单片机对应寄存器,实现无缝衔接。
| 寄存器名称 | 结构体成员 | 读写属性 | 核心功能与位操作详解 | 51单片机对应寄存器 |
|---|---|---|---|---|
| 状态寄存器 | USART1->SR |
只读 | 串口状态标志位,核心位: - 位5(RXNE):接收数据寄存器非空,收到1字节数据后硬件置1,读取DR后自动清零 - 位6(TC):发送完成标志位,一帧数据发送完成后硬件置1 - 位7(TXE):发送数据寄存器空,可写入下一个发送数据 | SCON寄存器的RI、TI标志位 |
| 数据寄存器 | USART1->DR |
读写 | 低8位有效,存储发送/接收的数据,写入数据启动发送,读取数据获取收到的字节 | SBUF寄存器 |
| 波特率寄存器 | USART1->BRR |
读写 | 存储USARTDIV值,高12位为整数部分,低4位为小数部分,决定串口波特率 | 无对应寄存器,51通过定时器1配置波特率 |
| 控制寄存器1 | USART1->CR1 |
读写 | 核心控制寄存器,核心位: - 位13(UE):USART使能位,写1开启串口 - 位3(TE):发送使能位,写1开启发送功能 - 位2(RE):接收使能位,写1开启接收功能 - 位5(RXNEIE):接收中断使能位,写1开启RXNE中断 - 位6(TCIE):发送完成中断使能位 | SCON寄存器的REN、TI、RI控制位 |
C语言寄存器操作完整示例:配置USART1为115200bps、8位数据、1位停止位、无校验,实现轮询收发回显功能,完全对应51单片机的串口逻辑
c
#include "stm32f1xx.h"
// USART1初始化函数
void USART1_Init(void)
{
// 1. 开启GPIOA和USART1时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
// 2. 配置PA9(TX)为复用推挽输出,50MHz
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
GPIOA->CRH |= GPIO_CRH_MODE9_1 | GPIO_CRH_CNF9_1;
// 3. 配置PA10(RX)为浮空输入
GPIOA->CRH &= ~(GPIO_CRH_MODE10 | GPIO_CRH_CNF10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;
// 4. 配置波特率115200,BRR=0x271
USART1->BRR = 0x271;
// 5. 开启USART、发送、接收功能
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
// 发送1个字节
void USART1_Send_Byte(uint8_t data)
{
// 等待发送数据寄存器空
while((USART1->SR & USART_SR_TXE) == 0);
// 写入数据到DR寄存器,启动发送
USART1->DR = data;
}
// 接收1个字节,阻塞式
uint8_t USART1_Receive_Byte(void)
{
// 等待接收数据寄存器非空
while((USART1->SR & USART_SR_RXNE) == 0);
// 读取DR寄存器,返回收到的数据
return (uint8_t)USART1->DR;
}
// 主函数:串口回显
int main(void)
{
USART1_Init();
uint8_t recv_data;
while(1)
{
// 收到数据后原路返回
recv_data = USART1_Receive_Byte();
USART1_Send_Byte(recv_data);
}
}
2.5 HAL库USART封装逻辑与核心API深度解析
HAL库将USART的底层寄存器操作封装为标准化的结构体与API函数,无需手动配置寄存器,同时与中断、DMA深度联动,大幅提升开发效率,保证代码的可移植性。
1. USART核心配置结构体
HAL库用UART_HandleTypeDef结构体封装串口的所有配置参数,与寄存器一一对应,联动C语言结构体知识点:
c
typedef struct {
USART_TypeDef *Instance; // 串口外设基地址,USART1/USART2等
UART_InitTypeDef Init; // 串口核心初始化参数
uint8_t *pTxBuffPtr; // 发送缓存指针
uint16_t TxXferSize; // 发送数据长度
__IO uint16_t TxXferCount; // 剩余发送长度
uint8_t *pRxBuffPtr; // 接收缓存指针
uint16_t RxXferSize; // 接收数据长度
__IO uint16_t RxXferCount; // 剩余接收长度
DMA_HandleTypeDef *hdmatx; // 发送DMA句柄
DMA_HandleTypeDef *hdmarx; // 接收DMA句柄
HAL_LockTypeDef Lock; // 锁保护
__IO HAL_UART_StateTypeDef State; // 串口运行状态
__IO uint32_t ErrorCode; // 错误代码
} UART_HandleTypeDef;
// 串口核心初始化参数结构体
typedef struct {
uint32_t BaudRate; // 波特率,如115200
uint32_t WordLength; // 数据位,UART_WORDLENGTH_8B
uint32_t StopBits; // 停止位,UART_STOPBITS_1
uint32_t Parity; // 校验位,UART_PARITY_NONE
uint32_t Mode; // 收发模式,UART_MODE_TX_RX
uint32_t HwFlowCtl; // 硬件流控,UART_HWCONTROL_NONE
uint32_t OverSampling; // 过采样,16倍过采样
} UART_InitTypeDef;
2. HAL库USART核心API与底层对应关系
| HAL库API函数 | 核心功能 | 底层寄存器操作 |
|---|---|---|
HAL_UART_Init(UART_HandleTypeDef *huart) |
串口基础初始化,配置波特率、帧结构、GPIO复用 | 开启串口与GPIO时钟,配置CR1、BRR寄存器,配置GPIO为复用模式 |
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) |
轮询模式发送数据,超时退出 | 循环检测TXE标志位,逐字节写入DR寄存器,超时返回错误 |
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) |
轮询模式接收数据,超时退出 | 循环检测RXNE标志位,逐字节读取DR寄存器,超时返回错误 |
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) |
中断模式发送数据,非阻塞 | 使能发送完成中断,写入首字节数据,剩余数据在中断中发送 |
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) |
中断模式接收数据,非阻塞 | 使能接收中断,收到数据后自动存入缓存,达到指定长度触发回调 |
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) |
DMA模式发送数据,全程零CPU占用 | 配置DMA传输参数,启动DMA发送,串口自动触发DMA传输 |
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) |
DMA模式接收数据,全程零CPU占用 | 配置DMA循环接收,串口收到数据自动存入缓存,无需CPU干预 |
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) |
接收完成回调函数,__weak弱函数,用户重写实现业务逻辑 |
中断/DMA模式下,接收完成后自动调用,无需修改中断服务函数 |
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) |
发送完成回调函数,接收完成后自动调用 | 中断/DMA模式下,发送完成后自动调用 |
3. HAL库串口中断接收核心流程
- 调用
HAL_UART_Receive_IT()开启接收中断,设置接收缓存与长度; - 串口收到1字节数据,RXNE标志位置1,触发USART全局中断;
- 中断服务函数调用
HAL_UART_IRQHandler()公用处理函数; - 处理函数读取DR寄存器,将数据存入接收缓存,剩余长度减1;
- 剩余长度为0时,关闭接收中断,调用
HAL_UART_RxCpltCallback()回调函数; - 用户在回调函数中处理接收数据,如需继续接收,需再次调用
HAL_UART_Receive_IT()重新开启接收中断。
三、STM32CubeMX+Keil5保姆式实操:串口全场景收发实战
本次实操适配STM32F103C8T6核心板,实现三大工业级核心场景:① 轮询模式串口回显;② 中断模式上位机指令控制LED;③ DMA+空闲中断不定长数据收发,全程无跳步,零基础可零报错跟随完成。
硬件说明
- 串口USART1:PA9(TX)、PA10(RX),波特率115200,8位数据、1位停止位、无校验
- 板载LED:PC13引脚,推挽输出,低电平点亮,User Label:LED
- 硬件接线:USB-TTL模块的RX接PA9,TX接PA10,GND与核心板GND共地,严禁接反TX/RX
3.1 工程创建与基础配置
- 打开STM32CubeMX,点击
ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。 - 调试接口配置:点击左侧
System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。 - 时钟配置:点击
RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,APB2时钟72MHz,APB1时钟36MHz,无红色错误提示。
3.2 USART与DMA图形化配置
- GPIO引脚配置:PC13引脚选择
GPIO_Output,配置为推挽输出、默认高电平、无上下拉、低速,User Label设为LED。 - USART1基础配置:
- 点击左侧
Connectivity -> USART1,Mode选择Asynchronous(异步模式); - 配置参数:Baud Rate=115200,Word Length=8 Bits,Parity=None,Stop Bits=1,Direction=Receive and Transmit;
- 高级设置保持默认,16倍过采样,无硬件流控。
- 点击左侧
- DMA配置:
- 点击USART1配置界面的
DMA Settings,点击Add添加2个DMA通道:- 发送DMA:DMA Request=USART1_TX,Channel=DMA1 Channel 4,Direction=Memory To Peripheral,Priority=Medium,Mode=Normal,Data Width=Byte,Memory地址自增,Peripheral地址不增;
- 接收DMA:DMA Request=USART1_RX,Channel=DMA1 Channel 5,Direction=Peripheral To Memory,Priority=High,Mode=Circular,Data Width=Byte,Memory地址自增,Peripheral地址不增。
- 点击USART1配置界面的
- NVIC中断配置:
- 点击左侧
System Core -> NVIC,优先级分组选择Priority Group 2; - 勾选
USART1 global interrupt,抢占优先级设为1; - 勾选
DMA1 channel4 global interrupt、DMA1 channel5 global interrupt,抢占优先级均设为1。
- 点击左侧
3.3 工程代码生成与业务代码编写
- 工程生成配置:进入
Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheral和Keep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。 - 业务代码编写:所有代码必须写在
/* USER CODE BEGIN */和/* USER CODE END */之间,避免重新生成代码时被覆盖。
步骤1:全局变量定义(main.c文件)
c
/* USER CODE BEGIN PV */
// 轮询模式收发缓存
uint8_t poll_tx_buf[] = "STM32 USART Poll Mode Test\r\n";
uint8_t poll_rx_data;
// 中断模式接收缓存与指令
#define RX_BUF_SIZE 1
uint8_t it_rx_buf[RX_BUF_SIZE];
uint8_t led_ctrl_cmd[] = "led on";
uint8_t led_off_cmd[] = "led off";
// DMA+空闲中断接收配置
#define DMA_RX_BUF_SIZE 128
uint8_t dma_rx_buf[DMA_RX_BUF_SIZE] = {0}; // DMA循环接收缓存
uint8_t uart_rx_buf[DMA_RX_BUF_SIZE] = {0}; // 数据处理缓存
uint16_t rx_len = 0; // 接收数据长度
uint8_t rx_done = 0; // 接收完成标志
/* USER CODE END PV */
步骤2:重写回调函数与空闲中断处理(main.c文件)
c
/* USER CODE BEGIN 0 */
// 重定向printf到USART1,用于串口打印
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
// 中断模式接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
// 指令判断:收到"led on"点亮LED,"led off"熄灭LED
static uint8_t cmd_buf[32] = {0};
static uint8_t cmd_idx = 0;
// 存入指令缓存
cmd_buf[cmd_idx++] = it_rx_buf[0];
// 收到换行符,判断指令
if(it_rx_buf[0] == '\n' || cmd_idx >= 31)
{
if(strstr((char *)cmd_buf, "led on") != NULL)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
printf("LED ON\r\n");
}
else if(strstr((char *)cmd_buf, "led off") != NULL)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
printf("LED OFF\r\n");
}
// 清空指令缓存
memset(cmd_buf, 0, 32);
cmd_idx = 0;
}
// 重新开启接收中断,等待下一个字节
HAL_UART_Receive_IT(&huart1, it_rx_buf, RX_BUF_SIZE);
}
}
// 重写USART1中断服务函数,添加空闲中断处理
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
// 检测串口空闲中断:一帧数据接收完成,总线空闲
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志
// 停止DMA接收,计算接收长度
HAL_UART_DMAStop(&huart1);
rx_len = DMA_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 拷贝数据到处理缓存
memcpy(uart_rx_buf, dma_rx_buf, rx_len);
rx_done = 1; // 标记接收完成
// 重新启动DMA循环接收
memset(dma_rx_buf, 0, DMA_RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_RX_BUF_SIZE);
}
}
/* USER CODE END 0 */
步骤3:主函数初始化与主循环业务逻辑(main.c文件)
c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// ===================== 1. 轮询模式发送测试数据 =====================
HAL_UART_Transmit(&huart1, poll_tx_buf, sizeof(poll_tx_buf), 100);
// ===================== 2. 开启中断模式接收 =====================
HAL_UART_Receive_IT(&huart1, it_rx_buf, RX_BUF_SIZE);
// ===================== 3. 开启DMA+空闲中断循环接收 =====================
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_RX_BUF_SIZE);
printf("STM32 USART All Mode Test Start\r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// ===================== 1. 轮询模式回显 =====================
// 轮询接收1个字节,超时10ms
if(HAL_UART_Receive(&huart1, &poll_rx_data, 1, 10) == HAL_OK)
{
// 原路回显收到的字节
HAL_UART_Transmit(&huart1, &poll_rx_data, 1, 10);
}
// ===================== 2. DMA不定长数据处理 =====================
if(rx_done == 1)
{
// 原路返回收到的完整数据
printf("DMA Received: %s\r\n", uart_rx_buf);
// 清空缓存与标志
memset(uart_rx_buf, 0, DMA_RX_BUF_SIZE);
rx_len = 0;
rx_done = 0;
}
HAL_Delay(10);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
3.4 编译、烧录与效果验证
- 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示
0 Error(s), 0 Warning(s),说明编译成功。 - 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择
ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。 - 硬件接线核心注意事项:USB-TTL模块的TX接核心板PA10(RX),RX接PA9(TX),必须将USB-TTL的GND与核心板GND连接,否则会出现乱码。
- 效果验证:
- 打开串口助手,配置115200波特率、8位数据、1位停止位、无校验,打开串口,核心板上电后会收到测试数据;
- 串口助手发送任意字符,核心板会原路回显,验证轮询模式正常;
- 发送
led on加换行,板载LED点亮,串口返回LED ON;发送led off加换行,LED熄灭,验证中断模式指令控制正常; - 发送任意长度的字符串,串口会返回
DMA Received: 发送的内容,验证DMA+空闲中断不定长收发正常。
四、保姆式排错指南
| 异常现象/报错信息 | 核心根因 | 一步到位解决方法 |
|---|---|---|
| 串口收发无任何反应,完全收不到数据 | 1. TX与RX接反,USB-TTL的TX接了核心板TX,RX接了RX;2. 串口收发功能未使能,GPIO模式配置错误;3. 未开启串口时钟或GPIO时钟;4. USB-TTL与核心板未共地 | 1. 交叉接线:USB-TTL TX→核心板RX,USB-TTL RX→核心板TX;2. 检查CubeMX配置,确认USART模式为收发模式,GPIO为复用推挽/浮空输入;3. 确认串口与GPIO时钟已开启;4. 必须将USB-TTL的GND与核心板GND可靠连接,共地是串口通信的前提 |
| 串口收到的数据全是乱码,无法识别 | 1. 收发双方波特率、数据位、停止位、校验位不一致;2. 系统时钟配置错误,导致串口波特率计算错误;3. 未共地,地电平不一致导致采样错误;4. 波特率误差过大,超过2%的允许范围;5. USB-TTL模块损坏或驱动异常 | 1. 严格核对串口助手与STM32的串口四要素,必须完全一致;2. 检查系统时钟配置,确认72MHz主频,USART1时钟为72MHz;3. 确保USB-TTL与核心板共地;4. 72MHz主频下推荐使用115200、9600等标准波特率,避免非标准波特率;5. 更换USB-TTL模块,重新安装驱动 |
| 中断模式接收只能进一次,之后再也无法触发接收中断 | 1. 接收完成回调函数中未重新调用HAL_UART_Receive_IT()开启下一次接收;2. 回调函数中处理时间过长,导致新数据被覆盖,溢出错误;3. 接收中断标志位未自动清零,中断一直触发 |
1. 回调函数处理完成后,必须再次调用HAL_UART_Receive_IT()重新开启接收中断;2. 回调函数内仅做数据存储,业务逻辑放到主循环执行,避免回调函数耗时过长;3. 确保中断服务函数中调用了HAL_UART_IRQHandler(),自动清零标志位 |
| DMA模式接收无数据,回调函数不触发 | 1. DMA模式配置错误,外设与内存方向搞反;2. 未开启内存地址增量模式,所有数据都写入同一个地址;3. 未使能串口DMA请求,或DMA通道与串口不匹配;4. 数据宽度配置错误,未使用Byte字节宽度;5. 循环模式下未开启空闲中断,无法触发接收完成回调 | 1. 接收DMA方向必须是Peripheral To Memory,发送为Memory To Peripheral;2. 必须开启内存地址增量模式,外设地址关闭增量;3. 确认DMA通道与串口映射匹配,USART1_RX对应DMA1通道5,TX对应通道4;4. 串口收发数据宽度必须为Byte字节;5. 循环DMA接收必须配合空闲中断,实现不定长数据接收 |
| 高速收发时频繁丢包,数据不完整 | 1. 使用轮询模式收发,CPU阻塞导致数据被覆盖;2. 中断模式高频收发时,频繁中断导致CPU无法及时处理,数据溢出;3. 接收缓存过小,数据未及时处理被覆盖;4. 未开启硬件流控,发送速率超过接收处理能力 | 1. 高速收发必须使用DMA模式,零CPU占用,避免丢包;2. 增大接收缓存,使用循环缓存+空闲中断,及时处理数据;3. 降低发送速率,或开启CTS/RTS硬件流控;4. 避免在中断/回调函数中执行耗时操作,及时处理接收数据 |
| 串口发送正常,但完全收不到数据 | 1. 接收功能未使能,CR1寄存器的RE位未置1;2. RX引脚配置错误,未设置为浮空输入/上拉输入;3. 接收中断/DMA未开启;4. 串口引脚被其他功能复用,冲突导致无法接收 | 1. 检查CubeMX配置,确认USART模式为TX_RX,收发双向使能;2. 确认RX引脚配置为浮空输入模式,无上下拉;3. 确认已开启接收中断或DMA接收;4. 检查引脚复用情况,确保无其他外设使用RX引脚 |
五、我的踩坑记录
-
踩坑现象 :第一次调试串口,接线、配置都检查了,结果完全收不到数据,发送也没反应,折腾了一下午没找到问题。
底层原因 :我把TX和RX接反了,想当然地认为TX接TX、RX接RX,完全忽略了串口通信是交叉接线,发送端要接接收端,接收端接发送端。51单片机开发时用的是集成的串口下载模块,不用自己接线,第一次手动接USB-TTL就踩了这个低级坑。
最终解决方案:将USB-TTL的TX接核心板的PA10(RX),USB-TTL的RX接核心板的PA9(TX),重新接线后,串口收发立即正常。 -
踩坑现象 :串口能收发,但收到的数据全是乱码,换了好几个波特率都没用,偶尔能识别几个字符,大部分都是乱码。
底层原因 :我在CubeMX里配置了外部8MHz晶振,但时钟配置里还是用的内部HSI时钟,系统时钟实际是64MHz,不是72MHz,导致波特率计算错误,实际波特率和预期不符,误差超过10%,必然出现乱码。同时USB-TTL和核心板没有共地,地电平有压差,采样错误加剧了乱码。
最终解决方案:重新配置系统时钟,选择HSE作为时钟源,配置PLL倍频为9,系统时钟72MHz,同时将USB-TTL的GND与核心板GND可靠连接,重新烧录后乱码问题彻底解决,数据收发完全正常。 -
踩坑现象 :中断模式接收,第一次能正常触发,之后再也进不了接收中断,只能复位重启。
底层原因 :我在接收完成回调函数里只处理了数据,没有重新调用HAL_UART_Receive_IT()开启下一次接收。HAL库的中断接收是定长的,达到指定长度后会自动关闭接收中断,必须手动重新开启才能继续接收。我以为开启一次就能一直接收,完全忽略了HAL库的这个机制,导致中断只进一次。
最终解决方案 :在接收完成回调函数的最后,添加HAL_UART_Receive_IT(&huart1, it_rx_buf, RX_BUF_SIZE),重新开启接收中断,修改后中断能正常循环触发,连续接收完全正常。 -
踩坑现象 :DMA循环接收数据,只能收到前几个字节,后面的数据全丢了,缓存里只有开头的几个字符。
底层原因 :我把DMA接收模式配置成了Normal正常模式,不是Circular循环模式,传输完一次指定长度后,DMA就自动停止了,后面的数据不会再传输。同时我没有开启空闲中断,无法判断一帧数据是否接收完成,只能收到固定长度的数据,不定长数据就会丢包。
最终解决方案:将DMA接收模式改为Circular循环模式,同时开启串口空闲中断,在空闲中断里计算接收长度、拷贝数据,重新启动DMA接收,修改后不定长数据收发完全正常,零丢包,连续传输稳定。
六、课后小练习(附完整标准答案)
6.1 基础巩固练习
练习1:基于轮询模式,实现串口调试日志打印功能,封装printf函数,实现不同等级的日志输出。
标准答案:
c
/* USER CODE BEGIN 0 */
// 重定向printf到USART1
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
// 日志等级宏定义
#define LOG_INFO(fmt, ...) printf("[INFO] " fmt "\r\n", ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) printf("[WARN] " fmt "\r\n", ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) printf("[ERROR] " fmt "\r\n", ##__VA_ARGS__)
/* USER CODE END 0 */
/* USER CODE BEGIN WHILE */
while (1)
{
LOG_INFO("System Running, Time: %dms", HAL_GetTick());
LOG_WARN("This is a warning test");
LOG_ERROR("This is a error test");
HAL_Delay(1000);
}
/* USER CODE END WHILE */
练习2:基于中断模式,实现上位机指令控制,发送"on"点亮LED,发送"off"熄灭LED,发送"blink"实现LED闪烁。
标准答案:
c
/* USER CODE BEGIN PV */
#define RX_BUF_SIZE 1
uint8_t it_rx_buf[RX_BUF_SIZE];
uint8_t cmd_buf[32] = {0};
uint8_t cmd_idx = 0;
uint8_t blink_flag = 0;
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(it_rx_buf[0] == '\n' || cmd_idx >= 31)
{
// 去除换行符
if(cmd_buf[cmd_idx-1] == '\r') cmd_buf[cmd_idx-1] = 0;
// 指令判断
if(strcmp((char *)cmd_buf, "on") == 0)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
blink_flag = 0;
printf("LED ON\r\n");
}
else if(strcmp((char *)cmd_buf, "off") == 0)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
blink_flag = 0;
printf("LED OFF\r\n");
}
else if(strcmp((char *)cmd_buf, "blink") == 0)
{
blink_flag = 1;
printf("LED BLINK\r\n");
}
// 清空缓存
memset(cmd_buf, 0, 32);
cmd_idx = 0;
}
else
{
cmd_buf[cmd_idx++] = it_rx_buf[0];
}
// 重新开启接收中断
HAL_UART_Receive_IT(&huart1, it_rx_buf, RX_BUF_SIZE);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, it_rx_buf, RX_BUF_SIZE);
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
if(blink_flag == 1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
}
HAL_Delay(10);
}
/* USER CODE END WHILE */
练习3:基于DMA模式,实现定长数据收发,上位机发送16字节数据,核心板收到后原路返回。
标准答案:
c
/* USER CODE BEGIN PV */
#define DMA_BUF_SIZE 16
uint8_t dma_rx_buf[DMA_BUF_SIZE] = {0};
uint8_t rx_done = 0;
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
rx_done = 1;
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
if(rx_done == 1)
{
// 原路返回收到的16字节数据
HAL_UART_Transmit_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
// 等待发送完成
while(HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
// 重新开启DMA接收
memset(dma_rx_buf, 0, DMA_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
rx_done = 0;
}
HAL_Delay(10);
}
/* USER CODE END WHILE */
6.2 进阶实战练习
练习1:实现DMA+空闲中断的循环缓存接收,解决大数据量收发的缓存溢出问题,实现无缝连续接收。
标准答案:
c
/* USER CODE BEGIN PV */
#define RING_BUF_SIZE 256
uint8_t ring_buf[RING_BUF_SIZE] = {0};
volatile uint16_t write_idx = 0; // 写入索引
volatile uint16_t read_idx = 0; // 读取索引
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
// 计算缓存中可读取的数据长度
uint16_t ring_buf_available(void)
{
return (write_idx - read_idx + RING_BUF_SIZE) % RING_BUF_SIZE;
}
// 从循环缓存中读取数据
uint16_t ring_buf_read(uint8_t *buf, uint16_t len)
{
uint16_t available = ring_buf_available();
if(len > available) len = available;
for(uint16_t i=0; i<len; i++)
{
buf[i] = ring_buf[read_idx++];
read_idx %= RING_BUF_SIZE;
}
return len;
}
// 串口空闲中断处理
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 停止DMA,计算本次接收长度
HAL_UART_DMAStop(&huart1);
uint16_t rx_len = RING_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 更新写入索引
write_idx = (write_idx + rx_len) % RING_BUF_SIZE;
// 重新启动DMA接收,写入地址为当前写入索引
HAL_UART_Receive_DMA(&huart1, &ring_buf[write_idx], RING_BUF_SIZE - write_idx);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, ring_buf, RING_BUF_SIZE);
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
uint8_t read_buf[128] = {0};
uint16_t len = ring_buf_read(read_buf, 128);
if(len > 0)
{
// 处理收到的数据,原路返回
HAL_UART_Transmit(&huart1, read_buf, len, 100);
}
HAL_Delay(10);
}
/* USER CODE END WHILE */
练习2:基于串口实现Modbus-RTU协议的基础从机功能,支持03功能码读取保持寄存器。
标准答案:
c
/* USER CODE BEGIN PV */
#define MODBUS_SLAVE_ADDR 0x01
#define MODBUS_BUF_SIZE 256
uint8_t modbus_rx_buf[MODBUS_BUF_SIZE] = {0};
uint16_t modbus_rx_len = 0;
uint8_t modbus_rx_done = 0;
// 保持寄存器,4个16位寄存器
uint16_t hold_reg[4] = {0x1234, 0x5678, 0xABCD, 0xEF01};
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
// Modbus CRC16校验函数
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;
else crc >>= 1;
}
}
return crc;
}
// 空闲中断接收Modbus数据
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
modbus_rx_len = MODBUS_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
modbus_rx_done = 1;
}
}
// Modbus数据处理函数
void modbus_process(void)
{
// 校验从机地址
if(modbus_rx_buf[0] != MODBUS_SLAVE_ADDR) return;
// 校验CRC
uint16_t crc_recv = (modbus_rx_buf[modbus_rx_len-1] << 8) | modbus_rx_buf[modbus_rx_len-2];
uint16_t crc_calc = modbus_crc16(modbus_rx_buf, modbus_rx_len-2);
if(crc_recv != crc_calc) return;
// 03功能码:读取保持寄存器
if(modbus_rx_buf[1] == 0x03)
{
uint16_t reg_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t reg_num = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
// 校验寄存器地址与数量
if(reg_addr >= 4 || reg_num > 4 || (reg_addr + reg_num) > 4) return;
// 构建响应帧
uint8_t tx_buf[256] = {0};
uint8_t tx_idx = 0;
tx_buf[tx_idx++] = MODBUS_SLAVE_ADDR;
tx_buf[tx_idx++] = 0x03;
tx_buf[tx_idx++] = reg_num * 2;
// 填充寄存器数据,高字节在前
for(uint16_t i=0; i<reg_num; i++)
{
tx_buf[tx_idx++] = hold_reg[reg_addr + i] >> 8;
tx_buf[tx_idx++] = hold_reg[reg_addr + i] & 0xFF;
}
// 添加CRC校验
uint16_t crc = modbus_crc16(tx_buf, tx_idx);
tx_buf[tx_idx++] = crc & 0xFF;
tx_buf[tx_idx++] = crc >> 8;
// 发送响应
HAL_UART_Transmit_DMA(&huart1, tx_buf, tx_idx);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, modbus_rx_buf, MODBUS_BUF_SIZE);
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
if(modbus_rx_done == 1)
{
modbus_process();
// 重新启动DMA接收
memset(modbus_rx_buf, 0, MODBUS_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, modbus_rx_buf, MODBUS_BUF_SIZE);
modbus_rx_done = 0;
}
HAL_Delay(10);
}
/* USER CODE END WHILE */
七、核心知识点速记
- 串口通信第一准则:收发双方的波特率、数据位、停止位、校验位必须完全一致,否则必然出现乱码。
- 串口接线必须交叉:USB-TTL TX→MCU RX,USB-TTL RX→MCU TX,必须共地,共地是串口通信的前提。
- STM32 USART1挂载在APB2总线,72MHz主频下时钟72MHz;USART2/3挂载在APB1,时钟36MHz,波特率计算必须使用对应外设时钟。
- 三种收发模式选型:简单低速用轮询,中等指令交互用中断,高速大数据量用DMA+空闲中断,工业开发首选DMA模式。
- HAL库中断接收是定长的,接收完成后必须重新调用
HAL_UART_Receive_IT()开启下一次接收,否则中断只会触发一次。 - 不定长数据接收必须使用DMA循环模式+串口空闲中断,空闲中断在一帧数据接收完成、总线空闲时触发,完美适配不定长数据收发。
- 串口乱码的核心原因:波特率不匹配、系统时钟配置错误、未共地、帧结构不一致,按此顺序排查可解决99%的乱码问题。
- 高速收发必须使用DMA模式,全程零CPU占用,可实现零丢包的稳定传输,避免轮询/中断模式的CPU高占用与丢包风险。
- 串口接收缓存必须定义为全局变量,禁止使用局部栈空间数组,避免DMA传输时栈空间被释放,导致内存越界与HardFault。
八、本章小结
本章我们深入拆解了USART串口通信的底层工作原理,对比51单片机UART的核心差异,掌握了帧结构、波特率计算规则,吃透了轮询、中断、DMA三种收发模式的底层逻辑与工业场景选型,完成了寄存器级配置与HAL库全场景实战,实现了串口回显、指令控制、DMA不定长收发三大核心功能,解决了乱码、丢包、中断卡死等高频问题。串口是外设通信的基础,下一章我们将学习I2C、SPI总线协议,完成OLED、LCD显示外设的驱动开发,实现数据的可视化输出。