3. UART驱动适配:从协议到混合验证
UART这个接口,说老实话,是每个芯片验证工程师都绕不开的坎。它看起来简单,就两根线——TX和RX,但真正要在混合验证环境里把它跑稳,坑还真不少。我个人习惯把UART驱动分成三个层次来理解:协议本身、轮询模式、中断模式。咱们一个一个来拆。
3.1 UART协议回顾——别小看这根串口线
UART全称是Universal Asynchronous Receiver/Transmitter,异步串行通信。什么叫异步?就是没有单独的时钟线,收发双方靠约定的波特率来同步。我遇到过不少新人,上来就问:“为什么UART不需要时钟线?”其实你想想看,双方都按同一个节奏来采样,只要误差在容忍范围内,就能正常通信。
UART一帧数据的典型格式是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 拉低电平,表示数据开始 |
| 数据位 | 5~8 bits | 常用8位,LSB先发 |
| 校验位 | 0/1 bit | 奇校验/偶校验/无校验 |
| 停止位 | 1/1.5/2 bits | 拉高电平,表示帧结束 |
这里有个关键点:波特率误差不能超过±2%。我曾经在一个项目里,因为PLL配置偏差,实际波特率比标称值差了3.2%,结果数据时不时就错一位。查了两天才定位到,从此以后我每次配UART都会用示波器量一下实际波形。
UART通信的核心参数:
- 波特率:9600、115200、921600等
- 数据位:通常8位
- 停止位:1位最常见
- 校验:无校验最常用,奇偶校验用于对可靠性要求高的场景
- 流控:硬件流控(RTS/CTS)或软件流控(XON/XOFF)
3.2 轮询模式驱动实现——简单但低效
轮询模式,说白了就是CPU不停地去查UART的状态寄存器。看数据有没有收到,看发送缓冲区是不是空了。这种方式实现起来最简单,但CPU被占死了,啥也干不了。
下面是一个典型的轮询发送函数:
void uart_polling_send(uint8_t data) {
// 等待发送缓冲区为空
while (!(UART->SR & UART_SR_TXE_MASK));
// 写入数据寄存器
UART->DR = data;
// 等待发送完成(可选)
while (!(UART->SR & UART_SR_TC_MASK));
}
接收也是类似的套路:
uint8_t uart_polling_recv(void) {
// 等待接收数据可用
while (!(UART->SR & UART_SR_RXNE_MASK));
// 读取数据寄存器
return (uint8_t)(UART->DR & 0xFF);
}
嗯,这里要注意:读取UART数据寄存器时,有些芯片会自动清除状态位,有些不会。我吃过这个亏——在某个国产MCU上,读DR之后还得手动清RXNE位,否则下一帧数据永远进不来。
轮询模式的典型问题:
- CPU占用率100%,无法处理其他任务
- 高波特率下容易丢数据——如果CPU被中断打断,轮询来不及读,数据就被覆盖了
- 不适合混合验证环境,因为验证平台需要同时驱动多个外设
3.3 中断模式驱动实现——高效但复杂
中断模式就好多了。数据来了,硬件自动触发中断,CPU在中断服务程序里处理数据。发送也一样,发送缓冲区空了就触发中断,CPU往里面填下一个字节。
中断模式的核心是环形缓冲区。我习惯用两个环形缓冲区,一个收一个发:
#define UART_BUF_SIZE 256
typedef struct {
uint8_t buffer[UART_BUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} ring_buffer_t;
ring_buffer_t uart_rx_buf;
ring_buffer_t uart_tx_buf;
中断服务程序里做的事情很简单:
void UART_IRQHandler(void) {
uint32_t status = UART->SR;
// 接收中断
if (status & UART_SR_RXNE_MASK) {
uint8_t data = (uint8_t)(UART->DR & 0xFF);
// 写入环形缓冲区
uint16_t next = (uart_rx_buf.head + 1) % UART_BUF_SIZE;
if (next != uart_rx_buf.tail) { // 缓冲区未满
uart_rx_buf.buffer[uart_rx_buf.head] = data;
uart_rx_buf.head = next;
}
// 如果满了,数据丢弃——这就是溢出
}
// 发送中断
if (status & UART_SR_TXE_MASK) {
if (uart_tx_buf.tail != uart_tx_buf.head) {
// 还有数据要发
UART->DR = uart_tx_buf.buffer[uart_tx_buf.tail];
uart_tx_buf.tail = (uart_tx_buf.tail + 1) % UART_BUF_SIZE;
} else {
// 没有数据了,关闭发送中断
UART->CR1 &= ~UART_CR1_TXEIE_MASK;
}
}
}
中断模式的关键设计点:
- 环形缓冲区大小要合理——太小容易溢出,太大浪费内存
- 中断优先级要设置好——UART中断通常设中等优先级,别抢了系统时钟中断
- 临界区保护——在中断和主循环同时访问环形缓冲区时,要关中断或使用原子操作
3.4 混合验证中的UART测试点
到了混合验证环境,UART的测试就不能只测功能了。你得考虑它和整个验证平台的交互。我总结了一套测试点清单,每次做UART验证都照着跑一遍:
| 测试类别 | 测试点 | 说明 |
|---|---|---|
| 基本功能 | 单字节收发 | 发一个字节,收一个字节,验证数据正确 |
| 基本功能 | 多字节收发 | 连续发256字节,验证无丢包 |
| 基本功能 | 波特率精度 | 用不同波特率测试,验证±2%容差 |
| 中断处理 | 中断嵌套 | UART中断被更高优先级中断打断,验证数据完整性 |
| 中断处理 | 中断风暴 | 连续快速收发,验证中断不丢失 |
| 缓冲区 | 环形缓冲区溢出 | 故意让接收缓冲区满,验证溢出处理 |
| 缓冲区 | 缓冲区边界 | 测试head和tail在边界处的回绕逻辑 |
| 混合验证 | 与其他外设并发 | UART收发同时,SPI/I2C也在工作 |
| 混合验证 | 时钟域交叉 | UART时钟与系统时钟不同步,验证亚稳态处理 |
| 混合验证 | 电源管理 | 进入低功耗模式后,UART能否唤醒系统 |
这里面我想特别强调一下中断风暴测试。我曾经在一个项目中,UART以921600波特率连续接收数据,中断频率高达每秒115200次。结果CPU有70%的时间都在处理UART中断,主循环几乎跑不动。后来我们加了DMA才解决这个问题。
混合验证中UART测试的常见误区:
- 只测功能不测性能——功能对了不代表在高负载下还能稳定工作
- 忽略中断延迟——验证平台模拟的中断延迟和真实芯片不一样,要加延迟模型
- 不测异常情况——比如线路断开、数据线短路、波特率不匹配
下面这张图展示了UART驱动在混合验证环境中的整体架构和测试覆盖范围:
最后说一句,UART驱动适配在混合验证中其实是个很好的切入点。它既简单到能让新人快速上手,又复杂到能暴露很多系统级问题——中断延迟、缓冲区管理、时钟域同步、电源管理。我个人建议,做混合验证的同学,第一个外设驱动就从UART开始练手。