3. UART串口通信协议与驱动
UART,说白了就是嵌入式世界里最常用的“聊天工具”。我做了这么多年驱动,可以说90%的调试信息、传感器数据、甚至固件升级,都是通过这根串口线完成的。今天咱们就把UART从协议到驱动,彻底聊透。
3.1 UART帧格式与波特率计算
UART通信其实很简单,就是两根线:TX发、RX收。但为什么能传数据?靠的是双方约定好的“说话节奏”——波特率。
3.1.1 帧格式解剖
一个完整的UART数据帧长这样:
起始位(1bit) + 数据位(5~9bit) + 校验位(0/1bit) + 停止位(1/1.5/2bit)
我习惯用8N1配置:8位数据、无校验、1位停止位。这是最通用的组合,兼容性最好。
| 参数 | 常见配置 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 固定为低电平,标志传输开始 |
| 数据位 | 8 bit | 最常用,兼容ASCII字符 |
| 校验位 | None/Even/Odd | 无校验最省事,校验位用于检错 |
| 停止位 | 1 bit | 高电平,标志传输结束 |
3.1.2 波特率计算
波特率就是每秒传输的比特数。STM32的USART模块通过时钟分频得到目标波特率:
波特率 = fCK / (16 × USARTDIV)
其中fCK是外设时钟,USARTDIV是分频系数
举个例子:fCK=72MHz,想要115200bps:
USARTDIV = 72,000,000 / (16 × 115200) ≈ 39.0625
嗯,这里要注意。USARTDIV的整数部分和小数部分要分别写入寄存器。我见过不少新手直接把39.0625取整,结果波特率偏差太大,通信不稳定。
3.2 中断与DMA接收机制
轮询接收太浪费CPU了。实际项目中,我们几乎只用中断或DMA。
3.2.1 中断接收
中断接收的流程很简单:
- 使能USART接收中断
- 每收到一个字节,硬件自动触发中断
- 在中断服务函数里读取DR寄存器
- 把数据存入缓冲区
代码骨架长这样:
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 把data存入环形缓冲区
ringbuf_write(&uart_rx_buf, data);
}
}
中断接收的好处是实时性好,但缺点也很明显——每个字节都进中断,高频数据时CPU开销大。
3.2.2 DMA接收
DMA接收就优雅多了。硬件自动把数据从USART的DR寄存器搬到内存,完全不用CPU干预。
我一般这样配置:
- 使能USART的DMA接收请求
- 配置DMA为循环模式或正常模式
- 设置传输完成中断(可选)
我曾经在一个4G模块项目里,用DMA+IDLE中断接收数据,CPU占用率从30%降到了2%。效果立竿见影。
3.3 环形缓冲区设计
环形缓冲区,说白了就是一个固定大小的数组,配合读写指针循环使用。这是嵌入式里最经典的数据结构之一。
3.3.1 为什么需要环形缓冲区?
中断接收的数据如果不及时处理,就会被新数据覆盖。环形缓冲区能暂存数据,让主循环慢慢处理。
3.3.2 核心实现
typedef struct {
uint8_t buffer[RINGBUF_SIZE];
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
} ringbuf_t;
// 写数据
int ringbuf_write(ringbuf_t *rb, uint8_t data)
{
uint16_t next = (rb->head + 1) % RINGBUF_SIZE;
if(next == rb->tail) return -1; // 缓冲区满
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}
// 读数据
int ringbuf_read(ringbuf_t *rb, uint8_t *data)
{
if(rb->head == rb->tail) return -1; // 缓冲区空
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RINGBUF_SIZE;
return 0;
}
3.4 实战:串口控制台与AT指令解析
理论说完了,咱们来点实际的。串口控制台和AT指令解析,是嵌入式开发里最常见的两个场景。
3.4.1 串口控制台
串口控制台说白了就是一个命令行界面。用户输入命令,设备执行并返回结果。
我一般这样设计:
- 接收线程(中断/DMA)把数据存入环形缓冲区
- 主循环从缓冲区读取数据,拼成完整的一行
- 遇到换行符(\n)后,解析命令并执行
命令解析可以用简单的字符串比较,也可以用查表法:
typedef struct {
const char *cmd;
void (*handler)(int argc, char **argv);
} cmd_entry_t;
cmd_entry_t cmd_table[] = {
{"help", cmd_help},
{"reset", cmd_reset},
{"version", cmd_version},
{"set", cmd_set},
};
3.4.2 AT指令解析
AT指令是模块通信的标准协议。格式一般是:AT+<命令>=<参数>\r\n
解析流程:
- 接收完整的一行数据(以\r\n结尾)
- 去掉\r\n,提取命令和参数
- 查表执行对应处理函数
- 返回响应(OK/ERROR/数据)
3.5 知识体系总览
下面这张图总结了UART驱动的核心知识结构:
UART驱动看似简单,但要做好其实不容易。协议层要算准波特率,驱动层要选对接收方式,数据层要设计好缓冲区。这三层配合好了,串口通信才能稳定可靠。