4. UART裸机驱动:从控制器配置到中断收发
UART,说白了就是串口。在Zynq上做裸机开发,串口是你和硬件对话的第一扇门。我记得刚接触Zynq时,第一个外设驱动的就是UART——因为没它,你连printf都看不到,调试全靠猜。
这一节,我们不讲Linux下的ttyPS0怎么配,就讲裸机下怎么让UART老老实实收发数据。你会看到三种模式:轮询、中断、以及我踩过的坑。
4.1 UART控制器硬件结构
Zynq的UART控制器是Cadence IP核,挂在APB总线上。它的寄存器布局很规整,核心就这几个:
| 寄存器 | 偏移 | 作用 |
|---|---|---|
| CR | 0x00 | 控制寄存器(使能、复位、流控) |
| MR | 0x04 | 模式寄存器(数据位、停止位、校验) |
| IER | 0x08 | 中断使能寄存器 |
| ISR | 0x0C | 中断状态寄存器 |
| BRGR | 0x10 | 波特率发生器(分频系数) |
| FIFO | 0x30 | 收发FIFO数据寄存器 |
你可能会问:为什么FIFO地址是0x30而不是0x14?嗯,中间那几个寄存器是留作扩展的,不用管它。
核心要点:UART的收发都通过FIFO操作。读FIFO就是收数据,写FIFO就是发数据。硬件自动处理移位和帧格式。
4.2 寄存器配置:三步走
配置UART其实就三步。我在项目里从来都是按这个顺序来,一次都没出过问题:
- 复位并禁用:写CR寄存器的RST位,然后清掉TXEN/RXEN
- 设置波特率和格式:算分频系数,写BRGR;写MR选8N1
- 使能收发:CR里置位TXEN和RXEN
波特率怎么算?公式很简单:
BRGR = (时钟频率) / (波特率 × 16) - 1
举个例子,Zynq的UART时钟通常是50MHz,要配115200:
BRGR = 50000000 / (115200 × 16) - 1
= 50000000 / 1843200 - 1
≈ 27.13 - 1
= 26
取整后实际波特率是50000000/(27×16) ≈ 115740,误差0.47%,完全OK。
我的习惯:配完波特率后,先发一个0x55(二进制01010101),用示波器看波形。如果每个位宽度一致,说明分频系数算对了。这招帮我省了至少三次查寄存器的时间。
4.3 轮询模式:简单粗暴
轮询模式,说白了就是CPU死等。发数据时等FIFO空,收数据时等FIFO非空。
代码长这样:
// 发送一个字节
void uart_putc(char c) {
// 等TX FIFO空
while (!(readl(UART_BASE + ISR) & (1 << 2)));
// 写FIFO
writel(c, UART_BASE + FIFO);
}
// 接收一个字节
char uart_getc(void) {
// 等RX FIFO有数据
while (!(readl(UART_BASE + ISR) & (1 << 0)));
// 读FIFO
return readl(UART_BASE + FIFO) & 0xFF;
}
你看,就这么几行。但有个问题——如果对方不发数据,程序就卡死在while里了。我在调试一个传感器模块时就遇到过,对方上电慢,结果系统直接死等,连看门狗都来不及喂。
避坑指南:轮询收数据一定要加超时!我曾经在一个工业项目里没加超时,结果对方掉线后整条产线卡死。加个计数器,超时了就返回-1,至少系统还能继续跑。
4.4 中断模式:真正的生产力
轮询模式只适合调试。真正产品里,你得用中断。为什么?因为CPU不能老等着串口,它还有更重要的事要做。
中断模式的配置比轮询多两步:
- 使能UART的中断(IER寄存器)
- 在GIC里注册中断服务函数
中断服务函数(ISR)怎么写?我一般这样:
void uart_isr(void) {
uint32_t status = readl(UART_BASE + ISR);
// 收到数据
if (status & (1 << 0)) {
char c = readl(UART_BASE + FIFO) & 0xFF;
// 把c放进环形缓冲区
ringbuf_push(&rx_buf, c);
}
// 发送完成(如果需要)
if (status & (1 << 2)) {
// 可以发下一包了
}
}
这里有个关键点:ISR里不要做复杂操作,把数据扔进缓冲区就赶紧返回。我见过有人直接在ISR里做协议解析,结果中断嵌套把自己搞死了。
经验之谈:中断里用环形缓冲区是标准做法。缓冲区大小根据波特率算——115200bps下,每字节约87μs,如果主频666MHz,缓冲区256字节够你处理2ms的数据。一般够用。
4.5 收发数据:两种模式的对比
| 对比项 | 轮询模式 | 中断模式 |
|---|---|---|
| CPU占用 | 100%(死等) | 仅收发瞬间 |
| 实时性 | 差(被其他任务阻塞) | 好(硬件触发) |
| 代码量 | ~10行 | ~50行(含缓冲区管理) |
| 适用场景 | 调试、Bootloader | 产品、多任务系统 |
| 风险 | 死锁(无超时) | 中断嵌套、缓冲区溢出 |
你想想看,如果你的系统还要跑网络协议栈、控制电机、处理传感器数据,你敢用轮询吗?反正我不敢。
4.6 实战:把printf挂到UART上
裸机开发没有标准库的printf,但我们可以自己实现一个简易版。原理很简单:把printf的底层输出函数替换成我们的uart_putc。
// 简易printf实现
void my_printf(const char *fmt, ...) {
char buf[256];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// 逐字节通过UART发送
char *p = buf;
while (*p) {
uart_putc(*p++);
}
}
这样你就可以在裸机上用my_printf打印调试信息了。我在做Zynq的DMA驱动时,全靠这个打印寄存器值来定位问题。
小技巧:vsnprintf在ARM编译器里是有的,但如果你用GCC裸机,可能需要自己实现。我一般直接移植Newlib的printf,省事。
4.7 常见问题与排查
做UART驱动这么多年,我总结了几条排查思路:
- 没输出?先查波特率对不对,再用示波器看TX脚有没有波形
- 输出乱码?大概率是波特率或数据格式不匹配,检查BRGR和MR
- 中断不触发?查IER有没有使能,GIC里有没有注册,优先级有没有配对
- 丢数据?缓冲区太小或者ISR处理太慢,加大缓冲区或优化ISR
我记得有一次,客户说串口偶尔丢第一个字节。查了两天,最后发现是FIFO使能后第一个字节写入时硬件还没准备好。解决方案很简单:配置完UART后,先写一个哑字节,然后清掉FIFO。嗯,这种坑,没遇到过的人根本想不到。