导言
USART是嵌入式非常重要的通讯方式,它的功能强大、灵活性高且用途广泛。只停留在HAL库层面上用USART只能算是入门,要加深对USART的理解,必须从寄存器层面入手。接下来,先从最简单的USART串行发送开始。
另外,在接下来的几个章节里,我会逐步地完成一个能在实际产品上使用,可靠的、健壮的串口收发的驱动程序。 比如,为了实现高效地收发程序,肯定要用DMA。然后配合DMA的传输过半中断、传输完成中断来实现高效的串口数据发送与接收。比如,时刻监控USART的健康状态,及时发现通讯异常,解决问题。比如,增加超时通讯机制,进一步提高系统的稳定性等等。
如果你只停留在USART串行发送 + USART接收中断的实践上,那么你真的很有必要再重新梳理一遍怎样完成一个高效且稳定的串口收发程序。废话不多说,开始。
效果如下所示:
每隔1是发送一条字符串"Hello,Wrold.\r\n"。
项目地址:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_ll_library06_usart
一、CubeMX
二、代码(LL库)
2.1、main.c
2.1.1、MX_USART1_UART_Init()

2.1.1.1、USART1_TX(PA9)

复习一下GPIO的寄存器CRL/CRH,如下:
2.1.1.2、USART1_RX(PA10)

复习一下GPIO的寄存器CRL/CRH,如下:
2.1.1.3、为什么USART模式下,PA9与PA10要这样设置GPIO???

如上所示,《STM32F1参考手册》章节8.1.11看到,当GPIO用在USART功能时,TX引脚需要设置推挽复用输出,RX引脚需要设置浮空输入或者带上拉输入。
如上所示,从这张表里看到另外一个很常用的通讯方式CAN总线,TX引脚也是设置推挽复用输出,RX引脚也是设置浮空输入或者带上拉输入。
2.1.1.4、USART1

三、寄存器梳理
3.1、发送器

《STM32F1参考手册》的章节25.3.2-发送器一定要多看几遍,总的来说:
- 将要发送的字节放入USART_DR寄存器之前,一定要等待USART_SR的位TXE = 1。代码如下:
c
void USART1_SendChar_Reg(uint8_t c) {
// 等待TXE置位(发送缓冲区空)
while (!(USART1->SR & (1 << 7)));
// 写入数据到DR
USART1->DR = c;
}
- USART_TXE是发送的"第一级缓冲"状态,表示数据从USART_DR到移位寄存器的转移完成,并不意味着数据已经完全发送出去。如下图所示:
- USART_TC才是代表数据完成发送出去,TC = 1时,表明硬件发送通道完全空闲。比如在低功耗场景中,通过TC = 1的判断,进入低功耗模式。
- 在单字节发送流程里,TXE会先置1,TC后置1。用户写入USART_DR->TXE清零。接着,数据转移到移位寄存器->TXE置1,此时可以写入新数据。最后,移位寄存器逐位发送数据->发送完成,TC置1。
3.2、时钟RCC_APB2ENR
启动外设之前,先启动对应的外设时钟。 如上所示,寄存器RCC_APB2ENR的位14-USART1EN是USART的时钟,置1相当于开启USART时钟。
c
RCC->APB2ENR |= (0x01UL << 14UL); // 使能USART1时钟(位14)
RCC->APB2ENR |= (0x01UL << 2UL); // 使能GPIOA(打开该时钟是因为USART1的TX、RX使用PA9与PA10
3.2、GPIO_CRH

如上所示,PA9是USART1_TX,PA10是USART1_RX。
c
// PA9,复用功能推挽输出模式
GPIOA->CRH &= ~(0xFUL << 4UL); // 清除CNF9与MODE9
GPIOA->CRH |= (0xAUL << 4UL); // 复用功能推挽输出模式(CNF9 = 10,MODE9 = 11),最大速度50MHz
// PA10,浮空输入模式
GPIOA->CRH &= ~(0xFUL << 8UL); // 清除CNF10与MODE10
GPIOA->CRH |= (0x4UL << 8UL); // 浮空输入模式(CNF10 = 01,MODE10 = 00)
3.3、USART1
3.3.1、USART_BRR

摘自《STM32F1参考手册》章节25.3.4,波特率(BBR) = fck / 16 * USARTDIV。
计算过程:
- SystemClock设置72M,APB2没有分频,所以USART1的fck = 72MHz。
- BBR = fck / (16 * BaudRate) = 72000000 / (16 * 115200) = 39.0625。 整数部分39,小数部分0.0625 * 16 = 1。
- 写入USART1->BBR = 0x271(39 << 4 | 1)。 如下所示,39 << 4UL的目的是设置DIV_Mantissa,最后|1是设置DIV_Fraction。
3.3.2、配置数据帧格式 - USART_CR1与USART_CR2
一般情况下UART通讯的数据帧格式是:8位数据 + 1位停止位 + 无奇偶校验。
如上所示,USART_CR1的M(字长)置0与PCE(校验控制使能)置0。
c
USART1->CR1 &= ~(0x01UL << 12UL); // 位M清0,数据一共8位
USART1->CF1 &= ~(0x01UL << 10UL); // 位PCE清0,禁止校验控制

如上所示,USART_CR2的段STOP置0。
c
USART1->CR2 &= ~(0x3UL << 12UL); // 位12、13置0,1个停止位
3.3.3、配置传输方向 - USART_CR1
如上所示,USART_CR1的TE与RE都置1。
c
USART1->CR1 |= (1UL << 3UL); // 使能发送
USART1->CR1 |= (1UL << 2UL); // 使能接收
3.3.4、禁用硬件流控 - USART_CR3

如上所示,USART_CR3的RTSE置0。
c
USART->CR3 &= ~(0x01UL << 8UL); // RTSE = 0,禁用硬件控流
3.3.6、清除无关模式位 - USART_CR2与USART_CR3

如上所示:
- 禁止CK引脚输出时钟。
- 禁止LIN模式。
如上所示: - 不使能红外模式。
- 不选择半双工模式。
- 禁止智能卡模式。
c
// (6) 配置异步模式 (清除无关模式位)
USART1->CR2 &= ~(1UL << 14); // LINEN 位 = 0, 禁用 LIN 模式
USART1->CR2 &= ~(1UL << 11); // CLKEN 位 = 0, 禁用时钟输出
USART1->CR3 &= ~(1UL << 5); // SCEN 位 = 0, 禁用智能卡模式
USART1->CR3 &= ~(1UL << 1); // IREN 位 = 0, 禁用 IrDA 模式
USART1->CR3 &= ~(1UL << 3); // HDSEL 位 = 0, 禁用半双工
3.3.7、启用USART

如上所示,USART_CR1的UE置1,让USART模块使能。
c
USART1->CR1 |= (1UL << 13UL); // USART模块使能
四、代码(寄存器方式)
4.1、main.c

如上所示,函数USART1_Configure(void)
用于初始化外设USART1。
- 错误修复: 第80行代码,改为GPIOA->CRH |= (0x09UL << 4UL)才对,速度10MHz的话,MODE9 = 01。所以应该是1001 = 0x09。

如上所示,函数USART1_SendString(const char \*str)
用于串行发送字符串。
如上所示,在main()的while(1)里每隔1S执行一次函数USART1_SendString()
发送字符串"Hello,World.\r\n"
。
如上所示,代码编译成功。
五、细节补充
5.1、为什么要清除无关模式位?
《3.3.6-清除无关模式位 - USART_CR2与USART_CR3》讲到怎样清除无关模式位,都是将某些位清0。实际上从《STM32F1参考手册》了解到,这些位(比如位14-LINEN)在系统初始化的时候默认就是0的。为什么我的代码还要再一次清0呢?多此一举吗?
首先,ST的LL库代码也会显式清除这些位。MX_USART1_UART_Init()
函数里的LL_USART_ConfigAsyncMode()
,它的内容如下:
既然LL库也会这样做,肯定有原因的。我的理解是:
- 之前的状态可能非复位值:
如果 USART 之前被其他程序或中断配置过(例如在Bootloader
或其他初始化代码中),这些位可能被设置为 1。因此,即使复位值是 0,显式清除可以确保当前状态明确为禁用状态,避免遗留问题。 - 代码可读性和可维护性:
显式清除这些位(即使默认是 0)可以增强代码的可读性,让读者清楚地知道这些功能被故意禁用,而不是依赖复位值。这种做法在嵌入式开发中很常见,属于"防御性编程"。
5.2、为什么外设USART的校验功能一般都是关闭?
- 硬件奇偶校验的局限性
- 只能检测单比特错误:硬件奇偶校验仅能检测数据传输中的单个比特错误,无法检测或纠正多比特错误。对于高可靠性应用(如工业通信、航空航天),这种检测能力不足。
- 性能开销:启用硬件奇偶校验会增加通信帧长度(例如 8 位数据 + 1 位校验 + 1 停止位 = 10 位),从而降低有效数据吞吐量,尤其在高速通信中影响较大。
- 复杂性增加:硬件奇偶校验需要发送端和接收端严格匹配配置(奇/偶、数据长度等),否则会导致错误(PE 标志),增加调试难度。
- 软件校验的灵活性
- 更强大的错误检测:软件可以实现更复杂的校验算法,如 CRC-8、CRC-16、CRC-32,甚至自定义校验和。这些方法能检测多比特错误并提供更高的可靠性。
- 协议层整合:在通信协议(如 Modbus、CAN、I2C)中,通常已经定义了标准校验机制(如 Modbus 的 LRC 或 CRC),硬件奇偶校验可能重复或冲突,不如直接在协议层实现。
- 可扩展性:软件校验可以根据需要动态调整(如改变校验算法或长度),而硬件奇偶校验固定在硬件层面,缺乏灵活性。
- 工程实践的习惯
- 简单性优先:在低成本或简单应用中(如串口调试、传感器通信),开发者往往选择关闭硬件奇偶校验,简化配置(使用 8-N-1 格式:8 位数据、无校验、1 停止位),并通过软件实现必要校验。
- 兼容性:许多设备和工具(如串口调试助手)默认使用无校验配置,启用硬件奇偶校验可能导致不兼容。
5.3、USART1默认是PA9、PA19,怎样复用到PB6、PB7?
如上所示,从寄存器AFIO_MAPR
的bit2-USART1_REMAP可以设置USART1所使用的引脚。代码例子如下: