4. UART串口通信驱动开发:从原理到实战
UART串口,做嵌入式的人几乎天天打交道。它简单、可靠,是调试的“最后一根稻草”。我个人习惯,在新板子上的第一个驱动,永远是UART——点亮LED只能证明板子活着,UART能告诉我它怎么活的。
这一章,我们就把CMS32的UART模块彻底讲透。从寄存器配置到轮询模式,再到中断驱动,最后把printf重定向到串口——这样你就能用熟悉的printf来调试了。
4.1 UART模块原理:它到底怎么工作的?
UART全称是通用异步收发器。注意“异步”两个字——收发双方没有独立的时钟线,靠约定的波特率来同步。
数据格式很简单:
- 起始位:1位,低电平,告诉接收方“我要开始发了”
- 数据位:5~9位,常用8位
- 校验位:可选,奇校验或偶校验
- 停止位:1位或2位,高电平
我在项目中遇到过一个问题:两个设备波特率都配成了115200,但就是收不到数据。查了半天,发现一个配了8数据位1停止位,另一个配了7数据位2停止位。嗯,格式不匹配,神仙也救不了。
核心要点:UART通信双方必须完全一致——波特率、数据位、校验位、停止位,缺一不可。
4.2 波特率配置:别小看这个除法器
CMS32的UART波特率由系统时钟经过分频得到。说白了,就是用一个计数器对时钟计数,计到特定值就翻转一次信号。
计算公式:
波特率 = 系统时钟频率 / (16 × (UBRR + 1))
其中UBRR是波特率寄存器值。反过来:
UBRR = (系统时钟频率 / (16 × 目标波特率)) - 1
举个例子,系统时钟48MHz,目标波特率115200:
UBRR = (48000000 / (16 × 115200)) - 1
= (48000000 / 1843200) - 1
= 26.04 - 1
= 25.04
≈ 25
取整后实际波特率:
实际波特率 = 48000000 / (16 × (25 + 1)) = 115384
误差只有0.16%,完全可用。
我曾经踩过的坑:系统时钟源切换后忘记更新UBRR值。调试时串口突然乱码,查了半小时才发现是时钟树变了。所以,每次修改时钟配置后,务必重新计算并设置波特率。
CMS32的波特率配置代码:
void UART_Init(uint32_t baudrate)
{
uint32_t ubrr;
uint32_t sysclk = GetSystemClock(); // 获取系统时钟频率
// 计算UBRR值
ubrr = (sysclk / (16 * baudrate)) - 1;
// 设置波特率寄存器
UART->UBRRH = (uint8_t)(ubrr >> 8);
UART->UBRRL = (uint8_t)(ubrr & 0xFF);
// 配置数据格式:8数据位,无校验,1停止位
UART->UCSRC = (1 << UCSZ1) | (1 << UCSZ0); // 8位数据
// 使能发送和接收
UART->UCSRB = (1 << TXEN) | (1 << RXEN);
}
4.3 发送与接收驱动:轮询模式
轮询模式最简单——CPU不断检查状态寄存器,直到可以发送或收到数据。说白了就是“死等”。
发送一个字节:
void UART_SendByte(uint8_t data)
{
// 等待发送缓冲区空
while (!(UART->UCSRA & (1 << UDRE)));
// 写入数据寄存器
UART->UDR = data;
}
接收一个字节:
uint8_t UART_ReceiveByte(void)
{
// 等待接收完成
while (!(UART->UCSRA & (1 << RXC)));
// 读取数据
return UART->UDR;
}
发送字符串:
void UART_SendString(const char *str)
{
while (*str)
{
UART_SendByte(*str++);
}
}
个人建议:轮询模式适合调试阶段或低速率通信。如果主循环里有大量计算任务,轮询会阻塞CPU,这时候就该上中断了。
4.4 发送与接收驱动:中断模式
中断模式的核心思想:CPU不主动等待,而是由硬件在事件发生时通知CPU。
发送中断:当发送缓冲区空时触发,CPU在中断里填充下一个字节。
接收中断:当接收到一个字节时触发,CPU在中断里读取数据。
初始化代码:
void UART_InitInterrupt(uint32_t baudrate)
{
// 先初始化UART(波特率、格式等)
UART_Init(baudrate);
// 使能接收完成中断
UART->UCSRB |= (1 << RXCIE);
// 使能全局中断
sei();
}
接收中断服务函数:
ISR(UART_RX_vect)
{
uint8_t data = UART->UDR;
// 将数据放入环形缓冲区
if (!RingBuf_IsFull(&rxBuf))
{
RingBuf_Put(&rxBuf, data);
}
else
{
// 缓冲区满,丢弃数据
// 可以设置溢出标志
uart_overflow = 1;
}
}
为什么用环形缓冲区?因为中断服务函数要快进快出,不能在里面做复杂处理。把数据暂存到缓冲区,主循环再慢慢处理。
避坑指南:我曾经在中断里直接调用printf,结果程序跑飞了。原因很简单——printf本身可能依赖UART,递归调用导致栈溢出。记住:中断里只做最必要的事,复杂逻辑交给主循环。
4.5 printf重定向:让调试更优雅
printf是C标准库函数,默认输出到stdout。在嵌入式系统里,我们需要把它重定向到UART。
CMS32基于GCC工具链,重定向方法如下:
int _write(int file, char *ptr, int len)
{
for (int i = 0; i < len; i++)
{
UART_SendByte((uint8_t)*ptr++);
}
return len;
}
如果是IAR或Keil,方法略有不同:
// IAR环境
int putchar(int ch)
{
UART_SendByte((uint8_t)ch);
return ch;
}
// Keil环境
int fputc(int ch, FILE *f)
{
UART_SendByte((uint8_t)ch);
return ch;
}
重定向之后,你就可以这样用了:
printf("系统初始化完成,当前频率:%d Hz\n", GetSystemClock());
printf("传感器读数:温度=%.2f°C,湿度=%d%%\n", temp, humidity);
我的习惯:调试阶段用printf输出所有关键信息。但发布固件前,我会用宏把printf关掉——毕竟串口输出会占用CPU时间,影响实时性。
#ifdef DEBUG_ENABLE
#define DEBUG_PRINT(...) printf(__VA_ARGS__)
#else
#define DEBUG_PRINT(...)
#endif
4.6 本章知识体系
下面这张图总结了UART驱动开发的完整脉络:
4.7 常见问题与排查方法
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无输出 | 波特率不匹配、TX引脚未配置、UART未使能 | 用示波器看TX引脚是否有波形 |
| 输出乱码 | 波特率误差过大、数据格式不一致 | 计算实际波特率,核对双方配置 |
| 丢数据 | 中断响应不及时、缓冲区太小 | 增大环形缓冲区,检查中断优先级 |
| printf不工作 | 重定向函数未实现、链接库不对 | 确认_write/fputc/putchar已实现 |
最后提醒一句:UART驱动看似简单,但它是整个系统的“眼睛”和“嘴巴”。我见过太多项目因为串口驱动写得马虎,导致调试效率极低。花半小时把驱动写扎实,后面能省下几十个小时的调试时间。