1. DW_apb_uart 到底是个什么东西
第一次接触 DW_apb_uart 是在一个 SoC 验证项目里,当时需要在 FPGA 原型上把一颗外设 IP 跑通,翻手册翻到 UART 章节,看到满屏的寄存器偏移和位域定义,说实话头是有点大的。后来做得多了才发现,这东西虽然寄存器不少,但真正需要天天打交道的就那么几个,把初始化的逻辑理顺之后,剩下的就是按部就班地配置和调试。
DW_apb_uart 是 Synopsys DesignWare 系列里的一个 APB 总线挂载的 UART IP 核。它的定位很明确:给 SoC 设计者提供一个可综合、可配置的串口控制器,支持标准的异步串行通信。和你在 STM32 上用的 USART 外设不同,DW_apb_uart 是一个独立的 IP 核,需要集成到 SoC 的地址空间中,通过 APB 总线访问它的寄存器。这意味着它的初始化流程和裸机 MCU 上的串口配置有本质区别——你不是在操作一个固定的外设基地址,而是在操作一段映射到总线上的寄存器空间。
这个 IP 核的典型应用场景包括:SoC 芯片的调试串口、嵌入式系统的控制台输出、工业设备之间的低速数据通信、以及 FPGA 原型验证中的日志输出通道。基本上只要你的芯片需要一个"能打印信息"的串口,DW_apb_uart 就是一个常见的选择。
它的核心特性包括:
- 可配置的 FIFO 深度(通常 16 到 256 字节)
- 支持 5 到 8 位数据位、1 到 2 位停止位
- 支持奇偶校验和无校验模式
- 可编程的波特率分频器
- 支持 DMA 握手信号
- 支持中断和轮询两种工作模式
- 可选的 IrDA 和 RS-485 模式
理解这些特性很重要,因为初始化本质上就是把这些可配置项按照你的实际需求填到对应的寄存器里。配置错了,串口要么不出数据,要么出乱码,要么根本进不了中断。
2. 初始化流程的整体设计思路
2.1 为什么初始化顺序不能随便调
很多人拿到 IP 手册之后,看到寄存器列表就开始逐个写,觉得反正都是配置寄存器,先写哪个后写哪个应该差不多。这个想法在 DW_apb_uart 上是会踩坑的。
DW_apb_uart 的寄存器分为几个功能区:全局控制区、波特率配置区、FIFO 控制区、中断控制区、状态查询区。这些区域之间有依赖关系。比如波特率分频寄存器(通常叫 DLL 和 DLH,即 Divisor Latch Low/High)和 FIFO 控制寄存器共享同一个地址偏移,需要通过线路控制寄存器(LCR)里的 DLAB 位来切换访问。如果你不先设置 DLAB,直接去写分频值,那写进去的实际上是 FIFO 控制寄存器的内容,结果就是波特率完全不对,FIFO 行为也乱了。
所以初始化的核心思路是:先建立访问上下文,再配置通信参数,最后使能中断和 FIFO。这个顺序不是随便定的,而是由硬件设计决定的。
2.2 初始化的四个阶段
我把 DW_apb_uart 的初始化分成四个阶段,每个阶段有明确的目标:
阶段一:软复位与状态确认。通过全局控制寄存器触发 FIFO 复位,确认 IP 核处于已知状态。这一步经常被忽略,但如果 IP 核之前被使用过(比如 bootloader 阶段已经初始化过一次),不复位就可能导致 FIFO 里有残留数据,影响后续通信。
阶段二:通信参数配置。设置波特率分频值、数据位宽度、停止位数量、校验方式。这些参数决定了串口的物理层行为,必须和通信对端完全匹配。
阶段三:FIFO 与流控配置。设置 FIFO 触发阈值、使能或禁用 FIFO、配置硬件流控(RTS/CTS)。这一步决定了数据缓冲的行为和吞吐效率。
阶段四:中断与使能。配置中断使能寄存器,设置中断触发条件,最后使能 UART。这一步做完之后,串口就可以正常收发数据了。
注意:有些 DW_apb_uart 的配置版本在复位后需要等待几个时钟周期才能访问寄存器,具体等待时间看手册里的复位释放时序要求。我遇到过因为没等够时间导致第一次写寄存器丢失的情况,排查了很久才发现是时序问题。
2.3 轮询还是中断:怎么选
在实际项目中,DW_apb_uart 的使用方式主要分两种:轮询模式和中断模式。
轮询模式适合调试阶段和低数据量场景。你不断地读状态寄存器(USR),检查 RX FIFO 是否有数据、TX FIFO 是否为空,然后手动搬运数据。优点是逻辑简单,不需要配置中断控制器,缺点是浪费 CPU 时间,高波特率下可能丢数据。
中断模式适合正式产品和高速通信场景。你配置好中断使能寄存器(IER),当 RX FIFO 达到触发阈值或者发送保持寄存器为空时,硬件产生中断,CPU 在中断服务程序里处理数据。优点是效率高,缺点是需要正确配置中断控制器,调试起来稍微复杂一些。
我的建议是:调试阶段先用轮询把基本通信跑通,确认波特率和数据格式没问题之后,再切换到中断模式。这样可以把问题隔离,避免同时排查通信参数和中断配置两个变量。
3. 核心寄存器详解与配置要点
3.1 全局控制寄存器(GCR)
GCR 是 DW_apb_uart 里最容易被忽视但很重要的寄存器。它的主要作用是控制 FIFO 的复位和使能。
典型的 GCR 位域定义如下:
| 位域 | 名称 | 说明 |
|---|---|---|
| bit[0] | UART_EN | UART 全局使能 |
| bit[1] | RX_FIFO_RST | RX FIFO 复位(写1复位,自动清除) |
| bit[2] | TX_FIFO_RST | TX FIFO 复位(写1复位,自动清除) |
| bit[3] | DMA_MODE | DMA 模式使能 |
初始化的第一步通常是写 GCR,把 RX 和 TX FIFO 都复位一次。复位的目的是清空之前可能残留的数据,确保 FIFO 指针回到初始位置。
/* 假设 UART 基地址为 UART0_BASE */ #define UART0_BASE 0x10000000 #define UART_GCR (UART0_BASE + 0x00) /* 复位 RX 和 TX FIFO */ writel(0x06, UART_GCR); /* 等待复位完成,通常几个时钟周期即可 */ udelay(1); /* 使能 UART,清除复位位 */ writel(0x01, UART_GCR);这段代码里有一个细节:复位位是自清除的,写完 1 之后硬件会自动清零,但你需要等待至少一个时钟周期才能确认复位完成。在 FPGA 原型上,由于时钟频率可能比较低,等待时间要相应放长。
3.2 波特率分频寄存器(DLL/DLH)
波特率的计算是 DW_apb_uart 初始化里最容易出错的地方。公式本身不复杂:
波特率 = 输入时钟频率 / (16 × 分频值) 分频值 = 输入时钟频率 / (16 × 目标波特率)但问题在于,分频值必须是整数,而实际计算出来的结果往往不是整数。这时候就需要取整,然后反算实际波特率,确认误差在可接受范围内(通常要求误差小于 2%)。
举个例子:假设 UART 的输入时钟是 50MHz,目标波特率是 115200。
分频值 = 50000000 / (16 × 115200) = 50000000 / 1843200 ≈ 27.13取整后分频值为 27,反算实际波特率:
实际波特率 = 50000000 / (16 × 27) = 50000000 / 432 = 115740.7 误差 = (115740.7 - 115200) / 115200 ≈ 0.47%误差小于 2%,可以接受。
但如果输入时钟是 48MHz,目标波特率是 115200:
分频值 = 48000000 / (16 × 115200) = 48000000 / 1843200 ≈ 26.04取整后分频值为 26,反算:
实际波特率 = 48000000 / (16 × 26) = 48000000 / 416 = 115384.6 误差 = (115384.6 - 115200) / 115200 ≈ 0.16%这个误差更小,所以 48MHz 时钟比 50MHz 更适合 115200 波特率。
实操心得:在选 UART 输入时钟的时候,尽量选那些能被常见波特率整除的频率。比如 14.7456MHz、18.432MHz、36.864MHz、48MHz 这些,都是 UART 时钟的好选择。如果时钟频率选得不好,高波特率下的误差会累积,导致通信不稳定。
写 DLL 和 DLH 之前,必须先设置 LCR 的 DLAB 位为 1:
#define UART_LCR (UART0_BASE + 0x0C) #define UART_DLL (UART0_BASE + 0x00) #define UART_DLH (UART0_BASE + 0x04) /* 设置 DLAB=1,切换到分频寄存器访问模式 */ writel(0x80, UART_LCR); /* 写分频值低字节 */ writel(27 & 0xFF, UART_DLL); /* 写分频值高字节 */ writel((27 >> 8) & 0xFF, UART_DLH); /* 清除 DLAB,回到正常寄存器访问模式 */ writel(0x00, UART_LCR);3.3 线路控制寄存器(LCR)
LCR 决定了串口的数据格式,包括数据位、停止位和校验方式。它的位域定义如下:
| 位域 | 名称 | 说明 |
|---|---|---|
| bit[1:0] | DLS | 数据位长度:00=5位,01=6位,10=7位,11=8位 |
| bit[2] | STOP | 停止位:0=1位,1=2位(5位数据时1.5位) |
| bit[3] | PEN | 校验使能:0=无校验,1=使能校验 |
| bit[4] | EPS | 校验类型:0=奇校验,1=偶校验 |
| bit[5] | STICK | 粘滞校验位 |
| bit[6] | BREAK | 发送 break 信号 |
| bit[7] | DLAB | 分频寄存器访问使能 |
最常用的配置是 8 位数据、1 位停止位、无校验,对应的 LCR 值为 0x03。如果需要偶校验,就是 0x1B(DLS=11, PEN=1, EPS=1)。
/* 8位数据,1位停止位,无校验 */ writel(0x03, UART_LCR);3.4 FIFO 控制寄存器(FCR)
FCR 控制 FIFO 的使能和触发阈值。典型位域:
| 位域 | 名称 | 说明 |
|---|---|---|
| bit[0] | FIFO_EN | FIFO 使能 |
| bit[1] | RX_FIFO_RST | RX FIFO 复位 |
| bit[2] | TX_FIFO_RST | TX FIFO 复位 |
| bit[7:6] | RX_TRIG | RX FIFO 触发阈值:00=1字节,01=1/4满,10=1/2满,11=2字节少 |
RX 触发阈值的选择直接影响中断频率和响应延迟。阈值设得太低,中断太频繁,CPU 负载高;阈值设得太高,数据在 FIFO 里积压,实时性差。一般调试场景下设为 1 字节触发,正式产品里根据数据量和 CPU 处理能力来调整。
/* 使能 FIFO,RX 触发阈值设为 1/4 满,同时复位两个 FIFO */ writel(0x07, UART_FCR);3.5 中断使能寄存器(IER)
IER 控制哪些事件可以产生中断:
| 位域 | 名称 | 说明 |
|---|---|---|
| bit[0] | ERBFI | RX 数据可用中断使能 |
| bit[1] | ETBEI | TX 保持寄存器空中断使能 |
| bit[2] | ELSI | RX 线路状态中断使能 |
| bit[3] | EDSSI | 调制解调器状态中断使能 |
调试阶段通常只使能 RX 数据可用中断(ERBFI=1),发送用轮询方式。正式产品里可能还需要使能 TX 空中断来实现高效的发送流程。
/* 使能 RX 数据可用中断 */ writel(0x01, UART_IER);4. 完整初始化代码与实操过程
4.1 初始化函数实现
把上面的寄存器配置串起来,一个完整的初始化函数大概长这样:
#define UART0_BASE 0x10000000 #define UART_RBR (UART0_BASE + 0x00) #define UART_THR (UART0_BASE + 0x00) #define UART_DLL (UART0_BASE + 0x00) #define UART_DLH (UART0_BASE + 0x04) #define UART_IER (UART0_BASE + 0x04) #define UART_IIR (UART0_BASE + 0x08) #define UART_FCR (UART0_BASE + 0x08) #define UART_LCR (UART0_BASE + 0x0C) #define UART_MCR (UART0_BASE + 0x10) #define UART_LSR (UART0_BASE + 0x14) #define UART_USR (UART0_BASE + 0x7C) #define UART_GCR (UART0_BASE + 0x00) void dw_uart_init(uint32_t uart_base, uint32_t clk_freq, uint32_t baudrate) { uint32_t divisor; uint32_t lcr_val; /* 阶段一:软复位 */ writel(0x06, uart_base + 0x00); /* 复位 RX/TX FIFO */ udelay(10); writel(0x01, uart_base + 0x00); /* 使能 UART */ /* 阶段二:通信参数配置 */ divisor = clk_freq / (16 * baudrate); lcr_val = 0x80; /* DLAB=1 */ writel(lcr_val, uart_base + 0x0C); writel(divisor & 0xFF, uart_base + 0x00); /* DLL */ writel((divisor >> 8) & 0xFF, uart_base + 0x04); /* DLH */ lcr_val = 0x03; /* 8N1, DLAB=0 */ writel(lcr_val, uart_base + 0x0C); /* 阶段三:FIFO 配置 */ writel(0x07, uart_base + 0x08); /* 使能 FIFO,复位,1/4 触发 */ /* 阶段四:中断配置 */ writel(0x00, uart_base + 0x04); /* 先关闭所有中断 */ writel(0x01, uart_base + 0x04); /* 使能 RX 数据可用中断 */ }4.2 发送与接收的基本操作
初始化完成之后,发送一个字节的流程是:等待 TX FIFO 有空闲空间,然后写 THR 寄存器。
void dw_uart_putc(uint32_t uart_base, char c) { /* 等待 TX FIFO 不满 */ while (!(readl(uart_base + 0x7C) & 0x02)) ; writel(c, uart_base + 0x00); } void dw_uart_puts(uint32_t uart_base, const char *str) { while (*str) { if (*str == '\n') dw_uart_putc(uart_base, '\r'); dw_uart_putc(uart_base, *str++); } }接收一个字节的轮询方式:
int dw_uart_getc(uint32_t uart_base) { /* 检查 RX FIFO 是否有数据 */ if (readl(uart_base + 0x7C) & 0x01) return readl(uart_base + 0x00) & 0xFF; return -1; }4.3 中断服务程序框架
如果使用中断模式,中断服务程序需要先读 IIR 寄存器判断中断类型,然后分别处理:
void dw_uart_isr(void) { uint32_t iir = readl(UART0_BASE + 0x08); uint32_t int_id = iir & 0x0F; switch (int_id) { case 0x04: /* RX 数据可用 */ while (readl(UART0_BASE + 0x7C) & 0x01) { char c = readl(UART0_BASE + 0x00) & 0xFF; /* 处理接收到的字符 */ rx_buffer[rx_head++] = c; rx_head %= RX_BUF_SIZE; } break; case 0x02: /* TX 保持寄存器空 */ /* 发送下一个字节 */ break; case 0x06: /* 线路状态错误 */ readl(UART0_BASE + 0x14); /* 读 LSR 清除错误 */ break; default: break; } }注意:IIR 寄存器的 bit[0] 为 0 表示有中断待处理,为 1 表示无中断。在中断服务程序里,通常先读 IIR 判断类型,处理完对应的数据后,中断会自动清除。但如果 RX FIFO 里还有数据没读完,中断会再次触发,所以 RX 处理要用 while 循环把 FIFO 读空。
5. 调试过程中常见的坑与排查方法
5.1 串口无输出
这是最常见的问题,可能的原因有很多。我的排查顺序是这样的:
第一步:确认时钟。用示波器或者逻辑分析仪测 UART 的输入时钟是否正常。如果时钟都没有,那后面都不用看了。在 FPGA 原型上,经常出现时钟约束没写对导致时钟不翻转的情况。
第二步:确认寄存器写入是否生效。通过调试器读回寄存器值,确认写入的值和读回的值一致。如果读回的值不对,可能是 APB 总线地址映射有问题,或者 IP 核没有被正确使能。
第三步:确认波特率。用示波器测 TX 引脚上的波形,看一个位的宽度是否和波特率匹配。比如 115200 波特率下,一个位的宽度大约是 8.68 微秒。如果测出来差很多,那就是分频值算错了。
第四步:确认引脚复用。有些 SoC 的 UART 引脚和 GPIO 是复用的,需要先配置引脚复用寄存器,把引脚功能切换到 UART 模式。这个在 SoC 集成阶段经常被遗漏。
5.2 输出乱码
乱码通常意味着波特率不匹配或者数据格式不一致。排查方法:
- 确认两边的波特率是否一致。如果一边是 115200,另一边是 9600,那肯定是乱码。
- 确认数据位、停止位、校验方式是否一致。一边是 8N1,另一边是 8E1,也会乱码。
- 检查分频值计算是否有误。特别是当输入时钟频率不是常见值时,容易算错。
- 检查时钟精度。如果 UART 时钟源是 RC 振荡器,精度可能不够,高波特率下会累积误差导致乱码。
5.3 中断不触发
中断不触发的问题排查起来比较绕,因为涉及 UART IP、中断控制器、CPU 三个环节。
先确认 UART 的 IER 寄存器是否正确使能了对应的中断位。然后读 IIR 寄存器,看是否有中断 pending。如果 IIR 显示有中断但 CPU 没响应,那就是中断控制器的问题,需要检查中断控制器的使能寄存器和优先级配置。如果 IIR 显示没有中断,那就是 UART 的接收条件没满足,检查 RX FIFO 触发阈值是否设得太高。
还有一个容易忽略的点:有些 DW_apb_uart 配置版本的中断是电平触发还是边沿触发,需要和中断控制器的配置匹配。如果 UART 输出的是电平中断,但中断控制器配置成了边沿触发,那就可能丢失中断。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无输出 | 时钟未使能 | 示波器测时钟引脚 |
| 完全无输出 | 引脚复用未配置 | 检查 GPIO 复用寄存器 |
| 完全无输出 | UART 未使能 | 读 GCR 确认 UART_EN 位 |
| 输出乱码 | 波特率不匹配 | 示波器测位宽,反算波特率 |
| 输出乱码 | 数据格式不一致 | 确认两边 LCR 配置 |
| 输出乱码 | 时钟精度不足 | 更换时钟源或降低波特率 |
| 中断不触发 | IER 未使能 | 读 IER 确认中断位 |
| 中断不触发 | 中断控制器未配置 | 检查中断控制器使能寄存器 |
| 中断不触发 | 触发阈值太高 | 降低 RX FIFO 触发阈值 |
| 数据丢失 | FIFO 溢出 | 提高中断优先级或降低波特率 |
| 数据丢失 | 轮询间隔太长 | 改用中断模式或 DMA |
5.5 几个实用的调试技巧
用回环模式验证 IP 核。很多 DW_apb_uart 支持内部回环(Loopback)模式,通过 MCR 寄存器的 bit[4] 使能。在回环模式下,TX 的数据会直接回到 RX,不需要外部连线。这个模式非常适合验证 IP 核的基本功能是否正常。如果回环模式下能正常收发,但外部通信不行,那问题就在引脚或者外部电路上。
用已知良好的串口工具做对比。如果你怀疑是 DW_apb_uart 的配置问题,可以先用一个 USB 转串口模块(比如 CP2102 或者 FT232)和 PC 上的串口调试助手通信,确认 PC 端和线缆没问题。然后再把 DW_apb_uart 接上去,这样可以排除外部因素。
在初始化完成后打印寄存器快照。把 GCR、LCR、DLL、DLH、FCR、IER 这些关键寄存器的值都读出来打印到日志里。这样出问题的时候可以对照手册逐个检查,比盲目猜测高效得多。
注意 FIFO 的复位时序。有些版本的 DW_apb_uart 要求 FIFO 复位后等待至少两个时钟周期才能进行下一次寄存器访问。如果复位后立刻写其他寄存器,可能导致配置丢失。我在一个项目里就遇到过这个问题,后来在复位后加了 udelay(10) 就稳定了。
6. 从调试到产品化的几个关键考量
6.1 波特率自适应
在产品化阶段,固定波特率往往不够用。有些场景需要 UART 自动检测对端的波特率,这就是波特率自适应。DW_apb_uart 本身不直接支持自适应,但可以通过软件实现:先用一个已知的波特率发送一个特定字符(比如 'U'),然后测量 RX 引脚上收到这个字符的位宽,反算出对端的波特率,再重新配置自己的分频值。
这个功能的难点在于测量精度。如果时钟频率不够高,测量出来的位宽误差会比较大。一般建议 UART 时钟至少是波特率的 16 倍以上,测量精度才能满足要求。
6.2 DMA 配合
在高吞吐场景下,用中断搬运数据仍然会消耗大量 CPU 时间。DW_apb_uart 支持 DMA 握手信号,可以和 DMA 控制器配合,实现数据的自动搬运。
配置 DMA 模式需要注意几点:首先要在 GCR 里使能 DMA 模式,然后配置 DMA 控制器的源地址和目的地址,最后使能 UART 的 DMA 请求。DMA 传输完成之后,需要检查 UART 的状态寄存器确认数据是否全部发送完毕。
6.3 低功耗设计
如果 SoC 有低功耗需求,UART 也需要支持休眠唤醒。DW_apb_uart 通常支持在 RX 引脚上检测到起始位时产生唤醒中断。配置方法是使能 IER 里的调制解调器状态中断,然后在系统进入低功耗模式之前确保 UART 时钟仍然在运行(或者配置为在唤醒事件时自动恢复时钟)。
这个功能的调试比较麻烦,因为涉及时钟域切换和电源域管理。建议先在 FPGA 上验证功能逻辑,再在真实芯片上调试低功耗行为。
6.4 多串口管理
很多 SoC 里不止一个 DW_apb_uart,可能有 2 个、4 个甚至更多。多串口管理的关键是抽象出一个统一的接口层,把基地址、中断号、时钟频率这些差异封装起来,上层应用不需要关心具体用的是哪个 UART。
typedef struct { uint32_t base_addr; uint32_t clk_freq; uint32_t baudrate; uint32_t irq_num; void (*isr)(void); } uart_dev_t; uart_dev_t uart_devs[] = { {0x10000000, 48000000, 115200, 10, uart0_isr}, {0x10001000, 48000000, 115200, 11, uart1_isr}, {0x10002000, 48000000, 9600, 12, uart2_isr}, };这种结构在项目初期可能显得有点过度设计,但当串口数量增加到 3 个以上时,统一接口带来的维护便利性就体现出来了。
6.5 日志系统的集成
DW_apb_uart 最常见的用途之一就是作为系统日志输出通道。把 UART 驱动和日志系统集成的时候,要注意几个问题:
- 日志输出不能阻塞太久。如果 UART 波特率低,一条长日志可能要几十毫秒才能发完,这期间 CPU 什么都做不了。解决方案是用环形缓冲区加中断发送,日志写入缓冲区后立即返回,中断服务程序负责慢慢发送。
- 日志级别要可控。调试阶段可能需要输出大量详细信息,产品阶段只需要输出错误和警告。通过编译宏或者运行时变量控制日志级别,避免产品阶段输出太多无用信息。
- 注意中断上下文中的日志安全。在中断服务程序里调用日志输出函数时,要确保不会因为等待 TX FIFO 空间而死循环。可以用非阻塞的方式,FIFO 满了就丢弃或者缓存到环形缓冲区。
7. 几个真实项目中的经验教训
7.1 一次因为 DLAB 导致的诡异问题
在一个项目里,我按照手册配置好了 UART,波特率设的是 115200,但实际测出来是 57600 左右。检查了分频值计算,没问题;检查了时钟频率,也没问题。后来用调试器读寄存器,发现 DLL 和 DLH 的值和我写进去的不一样。仔细一看,原来是在写 DLL/DLH 之后,我写 LCR 清除 DLAB 的时候,不小心把 LCR 的值写成了 0x00,但之前配置的 8N1 也被清掉了,变成了 5 位数据、1 位停止位。更关键的是,DLAB 清除的时机和分频值的锁存有关系,导致分频值没有正确加载。
后来改成先写 LCR 设置 DLAB,再写 DLL/DLH,最后写 LCR 设置数据格式并清除 DLAB,问题就解决了。这个教训是:LCR 的写入顺序和值都要仔细确认,不能想当然。
7.2 FIFO 阈值设错导致的数据丢失
另一个项目里,UART 在低波特率下工作正常,但波特率提高到 921600 之后就开始丢数据。排查发现是 RX FIFO 触发阈值设成了 1/2 满,而 FIFO 深度是 16 字节,也就是说要收到 8 个字节才触发中断。在 921600 波特率下,8 个字节的传输时间只有不到 90 微秒,如果中断响应不够快,FIFO 就溢出了。
把触发阈值改成 1/4 满(4 字节触发)之后,问题解决。但这也带来了新的问题:中断频率变高了,CPU 负载增加。最终的方案是改用 DMA 模式,让 DMA 控制器自动搬运数据,CPU 只在 DMA 传输完成中断里处理一批数据。
7.3 时钟频率选型的重要性
有一个项目选了一个 25MHz 的时钟给 UART,目标波特率是 115200。计算分频值:
分频值 = 25000000 / (16 × 115200) = 25000000 / 1843200 ≈ 13.56取整为 14,实际波特率:
实际波特率 = 25000000 / (16 × 14) = 25000000 / 224 = 111607.1 误差 = (111607.1 - 115200) / 115200 ≈ 3.1%误差超过了 2% 的容限,通信不稳定。后来把时钟改成 24MHz:
分频值 = 24000000 / (16 × 115200) = 24000000 / 1843200 ≈ 13.02取整为 13,实际波特率:
实际波特率 = 24000000 / (16 × 13) = 24000000 / 208 = 115384.6 误差 = (115384.6 - 115200) / 115200 ≈ 0.16%误差降到了 0.16%,通信稳定。这个经历让我在项目初期选时钟的时候,一定会先算一遍常见波特率的分频误差,确认在可接受范围内才定下来。
7.4 中断优先级配置不当导致的系统卡死
在一个实时性要求比较高的项目里,UART 中断优先级设得比较低,结果在高负载情况下,UART 中断被其他高优先级中断长时间阻塞,导致 RX FIFO 溢出。更严重的是,UART 中断服务程序里有一个等待 TX FIFO 空间的循环,如果 TX FIFO 一直不满(因为对端设备没准备好接收),这个循环就会一直执行,把整个系统卡死。
解决方案有两个:一是提高 UART 中断优先级,确保它能及时响应;二是把中断服务程序里的阻塞等待改成非阻塞方式,TX FIFO 满了就先把数据放到软件缓冲区,等 TX 空中断触发时再发送。这两个措施一起上,问题彻底解决。
7.5 调试工具的选择
调试 DW_apb_uart 的时候,工具的选择很重要。我最常用的组合是:
- 逻辑分析仪:抓 TX/RX 引脚波形,测量位宽,确认波特率。Saleae 或者类似的逻辑分析仪都可以,采样率至少要 10 倍于波特率。
- 串口调试助手:PC 端接收数据,确认数据内容是否正确。注意要选支持高波特率的工具,有些老工具最高只支持 115200。
- 调试器:读寄存器值,确认配置是否生效。在 FPGA 原型上,通常用 JTAG 调试器;在真实芯片上,可能用 SWD 或者 JTAG。
- 示波器:看信号质量,确认有没有过冲、振铃、毛刺等问题。特别是在高速波特率下,信号完整性问题会导致误码。
这些工具配合使用,基本上可以覆盖 DW_apb_uart 调试的所有场景。关键是要知道什么现象用什么工具去查,不要盲目地换工具试。
8. 写在最后的一些个人体会
DW_apb_uart 这个 IP 核,说复杂也复杂,寄存器有十几个,位域定义一大堆;说简单也简单,核心就是配置波特率、数据格式、FIFO 和中断这四件事。把初始化流程理顺之后,剩下的就是根据具体场景调整参数和排查问题。
我在多个项目里用过不同版本的 DW_apb_uart,发现不同配置版本之间有一些细微差异。比如有些版本的 GCR 寄存器偏移是 0x00,有些是 0x04;有些版本的 FIFO 深度是 16 字节,有些是 64 字节。所以每次拿到一个新的配置,第一件事就是仔细看手册里的寄存器映射表,确认偏移地址和位域定义,不要凭经验想当然。
调试的时候,最有效的方法还是"分而治之":先用回环模式确认 IP 核本身没问题,再用已知良好的串口工具确认外部链路没问题,最后把两边接起来调通信参数。这样可以把问题范围逐步缩小,避免同时排查多个变量。
还有一个建议是:在初始化代码里加足够的日志输出。把关键寄存器的配置值打印出来,把波特率计算过程打印出来,把实际测到的波特率误差打印出来。这些信息在出问题的时候非常有用,比事后用调试器一个个读寄存器要高效得多。
最后,如果你在调试过程中遇到了奇怪的现象,先不要怀疑 IP 核有 bug,大概率是配置或者时序的问题。DW_apb_uart 是一个经过大量项目验证的成熟 IP,它的行为在手册里都有明确定义。仔细读手册,对照寄存器定义逐位检查,问题总能找到。