4、UART串口驱动:从协议到中断与DMA
UART,说白了就是串口。这东西在嵌入式开发里太常见了,几乎每个板子都会引出几个串口。我刚开始做RISC-V开发时,第一个调通的外设就是UART——毕竟调试信息全靠它打印出来。
这一章我们就把UART串口驱动从头到尾捋一遍。从协议基础,到波特率怎么配,再到中断和DMA两种收发模式。嗯,内容不少,但都很实用。
4.1 UART协议基础
UART是异步串行通信,没有时钟线。收发双方靠约定的波特率来同步。一帧数据通常包含:起始位(1位低电平)、数据位(5~8位)、可选的校验位、停止位(1~2位高电平)。
我见过不少新手在调试时,两边波特率明明配的一样,但数据就是乱码。后来发现是停止位位数或者数据位长度没对齐。所以,通信双方的所有帧格式参数必须完全一致。
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200, 921600 | 每秒传输的符号数 |
| 数据位 | 8 | 最常用,兼容ASCII |
| 校验位 | None, Even, Odd | 无校验最常用 |
| 停止位 | 1, 2 | 1位停止位最常用 |
4.2 波特率配置
波特率配置是UART驱动的第一道坎。RISC-V芯片里的UART外设,通常有一个波特率发生器,核心是一个分频计数器。
计算公式很简单:
波特率 = 时钟频率 / (16 × 分频值)
反过来:
分频值 = 时钟频率 / (16 × 目标波特率)
举个例子。假设系统时钟是50MHz,想要115200波特率:
分频值 = 50000000 / (16 × 115200) ≈ 27.13
取整后是27。实际波特率 = 50000000 / (16 × 27) ≈ 115740。误差只有0.47%,完全在可接受范围内(通常要求误差 < 2%)。
在RISC-V平台上,配置代码大致是这样:
// 假设UART基地址为 0x10000000
// 寄存器偏移:DLL(0x00), DLH(0x04), LCR(0x0C)
void uart_set_baudrate(uint32_t base, uint32_t clk_hz, uint32_t baud) {
uint32_t divisor = clk_hz / (16 * baud);
// 设置DLAB位( Divisor Latch Access Bit)
uint8_t lcr = read8(base + 0x0C);
write8(base + 0x0C, lcr | 0x80);
// 写分频值
write8(base + 0x00, divisor & 0xFF); // DLL
write8(base + 0x04, (divisor >> 8) & 0xFF); // DLH
// 清除DLAB位
write8(base + 0x0C, lcr & ~0x80);
}
4.3 收发实现——轮询模式
轮询模式最简单。发送时不断检查发送保持寄存器是否为空,接收时不断检查接收数据是否就绪。
void uart_send_byte(uint32_t base, uint8_t data) {
// 等待发送缓冲区空
while (!(read8(base + 0x14) & 0x20));
// 写数据
write8(base + 0x00, data);
}
uint8_t uart_recv_byte(uint32_t base) {
// 等待接收数据就绪
while (!(read8(base + 0x14) & 0x01));
// 读数据
return read8(base + 0x00);
}
轮询模式在裸机或RTOS的初始化阶段够用。但一旦进入正常业务逻辑,CPU一直被占着等数据,效率太低了。这时候就该中断上场了。
4.4 中断驱动模式
中断驱动的思路是:收到数据或发送完成时,硬件自动触发中断,CPU在中断服务程序里处理。
配置步骤:
- 使能UART的接收/发送中断
- 设置中断控制器(PLIC或CLINT)使能对应中断源
- 编写中断服务程序
- 在中断服务程序里读取数据或填充发送数据
RISC-V的中断处理与ARM不太一样。它没有中断向量表自动跳转,需要软件在异常入口处读取mcause或scause来判断中断源。不过,UART外设本身的中断寄存器设计是类似的。
// 中断服务程序(简化版)
void uart_isr(uint32_t base) {
uint8_t iir = read8(base + 0x08); // 中断标识寄存器
if (iir & 0x04) { // 接收中断
uint8_t data = read8(base + 0x00);
// 把数据放入环形缓冲区
ringbuf_put(&rx_ring, data);
}
if (iir & 0x02) { // 发送中断
// 从发送缓冲区取数据发送
if (ringbuf_get(&tx_ring, &data)) {
write8(base + 0x00, data);
} else {
// 没有数据了,关发送中断
uart_disable_tx_interrupt(base);
}
}
}
4.5 DMA模式
中断模式虽然比轮询好,但每个字节都触发一次中断,高频通信时CPU开销还是很大。DMA模式可以彻底解放CPU——数据搬运由DMA控制器完成,CPU只在传输完成时收到一次中断。
UART+DMA的典型工作流程:
- 发送DMA:CPU配置好源地址(内存缓冲区)、目的地址(UART数据寄存器)、长度,启动DMA。DMA自动把数据逐个写入UART发送寄存器。
- 接收DMA:DMA自动从UART接收寄存器读取数据,写入内存缓冲区。当接收完指定字节数或发生超时时,触发中断通知CPU。
配置DMA时要注意几个细节:
- UART的DMA请求信号要正确连接到DMA控制器
- DMA传输宽度要与UART数据寄存器宽度匹配(通常是8位)
- 接收DMA建议配合超时机制,避免收不到完整帧时一直等待
// DMA发送配置示例(伪代码)
void uart_dma_send(uint32_t uart_base, uint8_t *buf, uint32_t len) {
// 1. 配置DMA源地址 = buf
dma_set_source( DMA_CH0, (uint32_t)buf );
// 2. 配置DMA目的地址 = UART数据寄存器
dma_set_dest( DMA_CH0, uart_base + 0x00 );
// 3. 配置传输长度
dma_set_count( DMA_CH0, len );
// 4. 配置传输宽度为字节
dma_set_width( DMA_CH0, DMA_WIDTH_8BIT );
// 5. 使能UART的DMA发送请求
uart_enable_dma_tx(uart_base);
// 6. 启动DMA
dma_start( DMA_CH0 );
// 7. 等待DMA完成中断(或轮询)
}
4.6 三种模式对比
| 模式 | CPU占用 | 实时性 | 适用场景 |
|---|---|---|---|
| 轮询 | 高(一直占用) | 中 | 初始化、低速率、简单调试 |
| 中断 | 中(每字节一次) | 高 | 中低速通信、交互式应用 |
| DMA | 低(整批一次) | 高 | 高速通信、大数据量、低功耗 |
我个人建议:项目初期先用中断模式,稳定后再考虑是否升级到DMA。别一上来就上DMA,调试起来麻烦得多。
嗯,UART串口驱动的内容就这些。从协议到配置,从轮询到中断再到DMA,每一步都有它的道理。你在实际项目中,可以根据通信速率和CPU负载灵活选择。记住一点:驱动没有银弹,适合场景的才是最好的。