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 外设是这一过程的 “自动化引擎”,而寄存器编程则是我们与硬件对话的 “母语”。理解从电平到协议的每一层,是掌握嵌入式通信的基石。
这不仅是技术细节的堆砌,更是工程智慧的体现:用简单的规则,构建复杂而可靠的信息桥梁。