USART 
Author:余生
通信协议是规定了不同设备或系统之间如何进行数据交换的规则集。在 STM32 的应用场景中,通信协议可以分为物理层、数据链路层、网络层、传输层和应用层等几个层次,但通常我们更关注的是具体的通信接口标准和技术,如 UART、SPI、I2C 等。下面我们将学习几种常见的串行通信协议及其在 STM32 中的应用。
我们来从最底层的物理信号开始,深入剖析:一个高电平、一个低电平,是如何一步步演化成完整的通信协议(以 USART 为例)的? 并进一步讲解:底层寄存器操作是如何实现的?库函数又是如何封装这些寄存器操作的?
这是一场从 “电子” 到 “协议” 再到 “代码” 的完整旅程,涵盖物理层、数据链路层、硬件外设、寄存器编程、库函数封装、中断机制等,力求深入、系统、透彻。
一:从 “0” 和 “1” 说起 —— 物理层的电平信号 
1.1 什么是高电平和低电平? 
在数字电路中,电压被抽象为两种状态:
- 低电平(Low):通常接近 0V,代表逻辑 “0”。
- 高电平(High):通常接近电源电压(如 3.3V 或 5V),代表逻辑 “1”。
注意:实际电压值有容差范围。例如在 3.3V 系统中,0~0.8V 为低电平,2.0~3.3V 为高电平。
这些电平通过导线在两个设备之间传输。但单个电平本身没有意义,它只是一个状态。要传输信息,必须赋予它时间维度和结构化规则。
二:从电平到比特流 —— 串行通信的诞生 
2.1 为什么要串行通信? 
并行通信:用多根线同时传输多个比特(如 8 根线传 1 字节),速度快,但线多、成本高、抗干扰差。
串行通信:用一根线逐位传输比特,线少、成本低、适合长距离,但速度慢。
USART 是典型的异步串行通信,即发送方和接收方没有共享的时钟线,靠 “约定” 来同步。
2.2 如何让接收方知道 “什么时候开始接收”? 
这是异步通信的核心难题:没有时钟线,如何同步?
解决方案:加入起始位和停止位,形成 “帧结构”。
帧格式(Frame Format) 
一个典型的 USART 数据帧如下:
[ 起始位 ] [ 数据位(8位) ] [ 奇偶校验位(可选) ] [ 停止位 ]
     1          8                  0 或 1               1 或 2
   低电平     LSB → MSB             奇/偶校验            高电平2
3
- 空闲状态:线路保持高电平(逻辑 1)。
- 起始位:发送方拉低电平,持续 1 个比特时间(T_bit)。接收方检测到从高到低的跳变,就知道 “数据要来了”。
- 数据位:随后发送 8 个比特(LSB 在前),每个持续 T_bit。
- 奇偶校验位:可选,用于简单错误检测。
- 停止位:发送方拉高电平,持续 1 或 2 个 T_bit,恢复空闲状态。
举例:发送字符 'A'(ASCII 码 0x41 = 0b01000001)
二进制:
1 0 0 0 0 0 1 0(注意 LSB 在前,所以是 1,0,0,0,0,0,1,0)完整帧(8N1:8 数据位,无校验,1 停止位):
txt[低] [1][0][0][0][0][0][1][0] [高] 起 数据位(8位) 停止1
2
2.3 时间同步:波特率(Baud Rate) 
接收方如何知道每个比特持续多长时间?
答案:双方事先约定 “波特率”(Baud Rate),即每秒传输的比特数。
- 波特率 9600:每秒 9600 比特 → 每个比特持续时间 T_bit = 1 / 9600 ≈ 104.17 μs。
- 接收方内部有一个时钟,以 16 倍波特率的频率采样(即每 T_bit 采样 16 次),用于精确定位起始位跳变和后续比特中心。
为什么是 16 倍?这是为了抗干扰和时钟漂移。通过多次采样判断电平,提高可靠性。
三:硬件实现 —— STM32 的 USART 外设 
STM32 内部有一个专用的硬件模块:USART 外设,它由多个寄存器控制,自动处理电平到数据的转换。
3.1 USART 主要寄存器(以 STM32F103 为例) 
| 寄存器 | 作用 | 
|---|---|
| USART_SR | 状态寄存器(Status Register) | 
| USART_DR | 数据寄存器(Data Register) | 
| USART_BRR | 波特率寄存器(Baud Rate Register) | 
| USART_CR1/CR2/CR3 | 控制寄存器(Control Register) | 
3.2 发送过程(硬件自动完成) 
- CPU 写数据:将要发送的字节写入 USART_DR。
- 硬件编码:USART 自动添加起始位、停止位,形成串行比特流。
- 移位输出:通过 TX 引脚,按波特率逐位输出(从 LSB 开始)。
- 状态更新:发送完成后, USART_SR中的TXE(Transmit Data Register Empty)标志置 1。
3.3 接收过程(硬件自动完成) 
- 检测起始位:USART 持续监测 RX 引脚。当检测到从高到低的跳变,并持续约 0.5 T_bit,确认起始位。
- 同步采样:在起始位中心后,每隔 T_bit 采样一次数据位(通常在第 8 次采样点,即 16 倍频的中间)。
- 组装数据:将 8 个采样位组装成一个字节。
- 写入 DR:将接收到的字节放入 USART_DR。
- 状态更新: USART_SR中的RXNE(Read Data Register Not Empty)标志置 1。
四:从寄存器到 C 代码 —— 底层库函数的实现 
我们以 STM32 标准外设库(StdPeriph Lib)或 HAL 库的风格,手写底层函数。
4.1 寄存器映射(C 语言视角) 
STM32 的寄存器被映射到内存地址。C 语言通过结构体访问:
// 定义 USART1 寄存器结构
typedef struct {
    volatile uint32_t SR;  // Status Register
    volatile uint32_t DR;  // Data Register
    volatile uint32_t BRR; // Baud Rate Register
    volatile uint32_t CR1; // Control Register 1
    volatile uint32_t CR2; // Control Register 2
    volatile uint32_t CR3; // Control Register 3
    volatile uint32_t GTPR; // Guard Time and Prescaler Register
} USART_TypeDef;
// 指向 USART1 基地址
#define USART1 ((USART_TypeDef*)0x40013800)2
3
4
5
6
7
8
9
10
11
12
13
4.2 波特率计算与设置 
波特率由 BRR 寄存器决定:
// 计算 BRR 值
// BRR = f_PCLK / (16 * baudrate)
// 如果有小数部分,需拆分到 DIV_Mantissa 和 DIV_Fraction
void USART_SetBaudRate(USART_TypeDef* USARTx, uint32_t baudrate) {
    uint32_t pclk = 72000000; // 假设 APB2 时钟为 72MHz
    uint32_t divisor = (pclk + 8 * baudrate) / (16 * baudrate); // 四舍五入
    USARTx->BRR = divisor;
}2
3
4
5
6
7
8
4.3 发送一个字节(轮询方式) 
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) {
    // 等待发送数据寄存器空(TXE=1)
    while (!(USARTx->SR & (1 << 7))); // TXE 位在 SR 的 bit7
    
    // 将数据写入 DR,硬件自动开始发送
    USARTx->DR = data;
}2
3
4
5
6
7
关键点:CPU 写
DR后,硬件接管,自动串行化并输出。
4.4 接收一个字节(轮询方式) 
uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) {
    // 等待接收数据寄存器非空(RXNE=1)
    while (!(USARTx->SR & (1 << 5))); // RXNE 位在 SR 的 bit5
    
    // 从 DR 读取数据
    return (uint8_t)(USARTx->DR);
}2
3
4
5
6
7
关键点:读
DR会自动清除RXNE标志。
4.5 使能 USART 
void USART_Enable(USART_TypeDef* USARTx) {
    USARTx->CR1 |= (1 << 13); // UE: USART Enable
}2
3
4.6 使能发送 / 接收 
void USART_EnableTxRx(USART_TypeDef* USARTx) {
    USARTx->CR1 |= (1 << 3) | (1 << 2); // TE: Transmit Enable, RE: Receive Enable
}2
3
五:中断机制 —— 让通信更高效 
轮询方式浪费 CPU。更高效的方式是使用中断。
5.1 配置接收中断 
void USART_EnableRxInterrupt(USART_TypeDef* USARTx) {
    USARTx->CR1 |= (1 << 5); // RXNEIE: RX Interrupt Enable
}2
3
5.2 编写中断服务程序(ISR) 
// 假设是 USART1 的中断
void USART1_IRQHandler(void) {
    if (USART1->SR & (1 << 5)) { // RXNE 标志
        uint8_t received_data = (uint8_t)(USART1->DR); // 读 DR 清除标志
        
        // 处理接收到的数据
        // 例如:存入缓冲区、解析协议等
        process_received_byte(received_data);
    }
    
    if (USART1->SR & (1 << 7)) { // TXE 标志
        // 发送缓冲区还有数据?继续发送
        if (tx_buffer_has_data()) {
            USART1->DR = get_next_tx_byte();
        } else {
            // 发送完成,可以关闭 TXE 中断
            USART1->CR1 &= ~(1 << 7);
        }
    }
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
优势:CPU 不再等待,可以执行其他任务,事件驱动。
六:从比特流到协议 —— 封装应用层 
USART 只负责传输原始字节流。要实现有意义的通信,需要上层协议。
6.1 简单帧协议设计 
例如,定义一个简单协议:
[ 帧头 0xAA ] [ 命令 1字节 ] [ 数据长度 1字节 ] [ 数据 N 字节 ] [ 校验和 1字节 ]6.2 协议解析(状态机) 
typedef enum {
    WAIT_HEADER,
    WAIT_CMD,
    WAIT_LEN,
    WAIT_DATA,
    WAIT_CHECKSUM
} ParseState;
ParseState state = WAIT_HEADER;
uint8_t cmd, len, data[255], checksum;
int data_index = 0;
void process_received_byte(uint8_t byte) {
    switch (state) {
        case WAIT_HEADER:
            if (byte == 0xAA) state = WAIT_CMD;
            break;
        case WAIT_CMD:
            cmd = byte;
            state = WAIT_LEN;
            break;
        case WAIT_LEN:
            len = byte;
            data_index = 0;
            if (len == 0) {
                state = WAIT_CHECKSUM;
            } else {
                state = WAIT_DATA;
            }
            break;
        case WAIT_DATA:
            data[data_index++] = byte;
            if (data_index >= len) {
                state = WAIT_CHECKSUM;
            }
            break;
        case WAIT_CHECKSUM:
            checksum = byte;
            // 验证校验和
            if (verify_checksum(cmd, len, data, checksum)) {
                handle_command(cmd, len, data);
            }
            state = WAIT_HEADER; // 重置
            break;
    }
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
第七章:高级话题 
7.1 DMA(直接内存访问) 
对于高速、大数据量传输,使用 DMA 可以让数据直接在内存和 USART 之间搬运,无需 CPU 干预。
7.2 硬件流控(RTS/CTS) 
当接收方处理不过来时,可通过 CTS 信号通知发送方暂停发送,避免数据丢失。
7.3 多处理器通信 
USART 支持地址位检测,可用于多设备总线通信。
7.4 错误处理 
USART 可检测:
- 帧错误(Framing Error):停止位不是高电平
- 噪声错误(Noise Error):采样时检测到噪声
- 溢出错误(Overrun Error):新数据到来时 DR 未读
这些错误在 SR 寄存器中有对应标志位。
总结:从电平到协议的完整链条 
| 层级 | 内容 | 
|---|---|
| 物理层 | 高 / 低电平在导线上传输 | 
| 数据链路层 | 起始位、停止位、波特率、奇偶校验 → 构成比特流 | 
| 硬件层 | USART 外设自动完成串并转换、采样、同步 | 
| 寄存器层 | 通过读写 SR、DR、BRR、CRx 等寄存器控制硬件 | 
| 驱动层 | C 函数封装寄存器操作(如 USART_SendByte) | 
| 中断层 | 使用中断实现事件驱动通信 | 
| 协议层 | 定义帧结构、命令、校验,实现有意义的数据交换 | 
结语 
一个高电平和一个低电平,看似简单,但在时间维度和结构化规则的加持下,演变为可靠的串行通信。STM32 的 USART 外设是这一过程的 “自动化引擎”,而寄存器编程则是我们与硬件对话的 “母语”。理解从电平到协议的每一层,是掌握嵌入式通信的基石。
这不仅是技术细节的堆砌,更是工程智慧的体现:用简单的规则,构建复杂而可靠的信息桥梁。