UART串口通信驱动:从原理到实战
UART串口,说白了就是嵌入式世界里最常用的"对讲机"。我做了这么多年项目,几乎每个板子都会留一个串口——调试、日志、协议交互,全靠它。今天咱们就把UART从原理到驱动实现,掰开揉碎了讲清楚。
UART协议原理:一帧数据怎么传?
UART全称是Universal Asynchronous Receiver/Transmitter,异步串行通信。什么叫异步?就是没有时钟线,靠双方约定好的波特率来同步。
一帧UART数据长这样:
| 起始位 | 数据位(5~9位) | 校验位(可选) | 停止位(1/1.5/2位) |
|---|---|---|---|
| 1位低电平 | LSB先发 | 奇/偶校验 | 高电平 |
举个例子,发送0x55(二进制01010101),波特率115200。空闲时TX线是高电平。先拉低1位时间(起始位),然后按位发数据:1→0→1→0→1→0→1→0,最后拉高1位时间(停止位)。
关键参数:波特率、数据位、停止位、校验位。收发双方必须完全一致,否则收到的全是乱码。我踩过这个坑——板子默认8N1,上位机配成了7E1,折腾了半小时才发现。
轮询模式:简单但占CPU
轮询模式最直接。CPU不断检查串口状态寄存器,有数据就读,能发送就写。
// 轮询发送一个字节
void UART_SendByte(uint8_t data) {
// 等待发送缓冲区空
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = data;
}
// 轮询接收一个字节
uint8_t UART_RecvByte(void) {
// 等待接收数据就绪
while (!(USART1->SR & USART_SR_RXNE));
return (uint8_t)USART1->DR;
}
这种写法,我刚开始学STM32时天天用。简单、直观、好调试。但问题也很明显——while循环把CPU死死卡住了。你想想看,如果波特率9600,收一个字节就要等1ms左右,CPU啥也干不了。
轮询模式的适用场景:仅用于初始化阶段的硬件自检、低速率调试输出。生产代码里千万别这么写,除非你的MCU只干这一件事。
中断模式:让CPU干正事
中断模式才是正经做法。数据来了,硬件自动触发中断,CPU暂停当前任务去处理,处理完再回来。
配置步骤:
- 使能USART接收中断(RXNEIE)
- 编写中断服务函数
- 在中断里读DR寄存器,清标志位
// 中断服务函数
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR;
// 把数据放入环形缓冲区
RingBuf_Put(&uart_rx_buf, data);
}
}
这里有个关键点——中断服务函数要快进快出。我见过有人直接在中断里做协议解析、写Flash,结果系统响应一塌糊涂。正确的做法是:中断只负责把数据搬到缓冲区,解析工作交给主循环或任务。
我的习惯:每个UART配一个环形缓冲区,大小256字节起步。中断往里写,主循环往外读。这样既不会丢数据,也不会阻塞中断。
DMA传输:解放CPU的终极方案
DMA(Direct Memory Access)说白了就是硬件搬运工。CPU告诉DMA:"把串口收到的1000个字节搬到内存地址0x20001000",然后CPU就可以去干别的了。DMA搬完数据,发个中断通知CPU。
DMA传输的典型配置:
| 参数 | 配置值 | 说明 |
|---|---|---|
| 外设地址 | USART1->DR | 数据源/目标 |
| 内存地址 | 缓冲区首地址 | 数据存放位置 |
| 传输方向 | 外设到内存/内存到外设 | 收发不同通道 |
| 传输模式 | 循环/正常 | 循环模式适合连续接收 |
| 数据宽度 | 8位 | 与UART数据位一致 |
// DMA接收配置示例
void UART_DMA_Init(void) {
// 使能DMA时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 配置DMA通道
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
DMA1_Channel5->CMAR = (uint32_t)uart_dma_buf;
DMA1_Channel5->CNDTR = 256; // 传输256字节
DMA1_Channel5->CCR = DMA_CCR_MINC // 内存地址递增
| DMA_CCR_CIRC // 循环模式
| DMA_CCR_TCIE; // 传输完成中断
DMA1_Channel5->CCR |= DMA_CCR_EN; // 使能DMA
}
DMA传输的好处是零CPU占用。我做过一个项目,4路串口同时以921600波特率收发数据,CPU负载才15%左右。如果用中断模式,CPU直接飙到80%以上。
DMA的坑:DMA传输完成中断和UART空闲中断配合使用,才能准确判断一帧数据结束。我曾经只靠DMA传输完成中断来判帧,结果数据包长度不固定时,经常把两帧数据拼在一起。
串口打印调试工具实现
调试嵌入式系统,printf是最趁手的工具。但STM32的串口没有现成的printf,需要我们自己重定向。
标准做法是重写fputc函数:
// 重定向printf到串口
int fputc(int ch, FILE *f) {
// 等待发送完成
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = (ch & 0xFF);
return ch;
}
// 使用示例
printf("System initialized, clock=%dMHz\r\n", SystemCoreClock / 1000000);
但printf有个问题——它默认是阻塞的。如果串口发送慢,printf会卡住整个系统。我建议在调试阶段用printf,正式发布时换成非阻塞的日志模块。
一个实用的调试工具应该包含:
- 日志分级:ERROR、WARNING、INFO、DEBUG,可以动态开关
- 时间戳:每个日志带上系统运行时间,方便定位问题
- 非阻塞输出:用DMA或中断发送,不影响主逻辑
- 彩色输出:不同级别用不同颜色,一眼看出问题
// 简易日志宏
#define LOG_ERROR(fmt, ...) \
do { \
printf("[ERR][%lu] " fmt "\r\n", GetTick(), ##__VA_ARGS__); \
} while(0)
#define LOG_INFO(fmt, ...) \
do { \
printf("[INF][%lu] " fmt "\r\n", GetTick(), ##__VA_ARGS__); \
} while(0)
我的调试三板斧:1) 串口打印关键变量;2) 用逻辑分析仪抓UART波形,确认时序;3) 在关键函数入口出口打印标记。这三招能解决90%的串口问题。
UART驱动知识体系
下面这张图总结了UART驱动的核心脉络:
从这张图可以看得很清楚:UART驱动从协议出发,衍生出三种实现模式,最终落地到调试工具。每种模式都有它的适用场景,没有银弹。
选型建议:
- 调试阶段、数据量小(<100字节/秒)→ 轮询模式
- 常规通信、数据量中等 → 中断模式 + 环形缓冲区
- 高速通信(>1Mbps)、大数据量 → DMA + 空闲中断
嗯,UART串口驱动这块就讲到这里。记住一句话:串口是嵌入式工程师的"眼睛",把眼睛擦亮了,调试任何问题都事半功倍。