从零开始搞懂 Vivado 2018.3 的串口调试:一个工程师的实战笔记
最近带实习生做 FPGA 项目,又碰到了老朋友——串口没输出。
不是波特率不对,就是引脚接反;有时候明明代码跑通了,终端却只看到一堆乱码……这种问题看似简单,但真卡住的时候,连“Hello World”都出不来,特别折磨人。
于是决定写一篇不讲套话、只说干货的文章,把我在Vivado 2018.3 + AXI UART Lite环境下踩过的坑、总结出的经验,原原本本掏出来。不谈理论堆砌,只讲你真正用得上的东西。
为什么我们还在用 AXI UART Lite?
你可能会问:“现在都有 16550、DMA 支持的高级 UART IP 了,为啥还要学这个‘轻量级’的?”
答案很简单:因为它够小、够稳、够快上手。
尤其是在资源紧张的 Artix-7 或 Zynq-7000 芯片里,你要的可能只是一个能打印日志的通道,而不是跑 Modbus 协议的工业模块。这时候 AXI UART Lite 就成了最佳选择:
- 占用 LUT 不到 100 个;
- 驱动库
xuartlite.h几行代码就能初始化; - Vivado IP Integrator 里拖进来自动连线,省心;
- 搭配 ILA 抓信号,查问题一目了然。
它不是用来传高清数据流的,它是你在系统崩了时,唯一还能“说话”的嘴。
先搞清楚:AXI UART Lite 到底是怎么工作的?
别急着画 Block Design,先弄明白它的本质逻辑。很多人调不通,是因为根本不知道它内部发生了什么。
它只有两个寄存器
对,就这么简单:
| 寄存器 | 功能 |
|---|---|
| RX Register(读) | CPU 从这里读取收到的一个字节 |
| TX Register(写) | CPU 往这里写要发送的一个字节 |
没有 FIFO 深度队列(其实是单字节缓冲),没有中断优先级管理,也没有波特率自适应。所有复杂性都被压到了最低。
工作流程也很直白:
1. 你往 TX 写个'A'→ IP 开始按设定波特率一位位往外发;
2. 外部设备发来'B'→ 接收器采样重组 → 存进 RX 寄存器;
3. CPU 主动去读 RX → 拿到'B'。
全程靠轮询或中断通知,不依赖操作系统。
⚠️ 注意:它只支持标准格式 —— 1 起始位 + 8 数据位 + 1 停止位,无奇偶校验。这也是为什么你用 PuTTY 设置成 7N1 会收不到任何内容。
波特率不准?多半是时钟坑了你!
这是我见过最多人栽跟头的地方。
AXI UART Lite 的波特率完全靠输入时钟分频生成。假设你的板载时钟是 50MHz,想跑 115200bps,那就要分频:
分频系数 = 50_000_000 / (16 × 115200) ≈ 27.13实际取整为 27,真实波特率变成:
实际波特率 = 50_000_000 / (16 × 27) ≈ 115740 bps误差约+0.47%—— 还在容忍范围内(一般要求 < ±3%)。
但如果时钟源本身就不准(比如用了不稳定的 PLL 输出),或者你误用了 100MHz 当作 50MHz 配置,误差直接飙到 10% 以上,通信必挂。
🔧解决办法:
- 在xparameters.h中确认宏定义是否正确:c #define XPAR_AXI_UARTLITE_0_BAUDRATE 115200
- 查看 Vivado 的 Clocking Wizard 是否输出了预期频率;
- 必要时用示波器测tx引脚上的位宽,反推实际波特率。
📌经验法则:只要波特率误差超过 5%,你就该怀疑是不是时钟配错了。
Block Design 怎么搭才不容易翻车?
来,打开 Vivado 2018.3,我们一步步走一遍最稳妥的搭建流程。
第一步:创建处理器系统
无论是 MicroBlaze 还是 Zynq PS,都一样处理。
以 MicroBlaze 为例:
1. 添加 MicroBlaze IP;
2. 自动弹出配置界面,勾选“Debug”选项(后面要用 ILA);
3. 给它配一个 Local Memory(至少 64KB);
4. 时钟选板载 50MHz 输入(别自己瞎改)。
第二步:加 AXI UART Lite
- 搜索 “AXI UART Lite”,拖进去;
- 双击打开配置窗口:
- 修改波特率为你需要的值(如 115200);
- 可选是否启用中断(初学者建议先关掉,用轮询); - 用自动连接功能连到 MicroBlaze 的 AXI_GP 接口;
- Vivado 会在 Address Editor 里自动分配基地址,比如
0x40600000。
✅ 此时你应该能看到:
-axi_uartlite_0_interrupt连到了处理器的中断输入;
- 地址空间已标注,且未冲突。
第三步:引脚约束和生成比特流
别忘了这三件事:
- 写 XDC 文件绑定管脚:
```tcl
set_property PACKAGE_PIN J15 [get_ports uart_rxd] ; # 示例:接开发板 UART_RX
set_property IOSTANDARD LVCMOS33 [get_ports uart_rxd]
set_property PACKAGE_PIN H15 [get_ports uart_txd]
set_property IOSTANDARD LVCMOS33 [get_ports uart_txd]
```
根据你的开发板手册改对应 PIN!
打开 Debug 模式:
在 Block Design 中右键 AXI UART Lite → “Debug” → 勾选tx,rx,baudoutn,dout等关键信号;
Vivado 会自动生成 ILA core 并插入设计。综合 → 实现 → 生成比特流。
如果报 timing failed,先别慌,检查 SDC 是否设置了主时钟约束:
create_clock -period 20.000 -name sys_clk_pin [get_ports sys_clk_p]SDK 软件怎么写才能看到输出?
导出硬件平台.hdf到 SDK 后,新建 Application Project,选 Empty Application。
包含头文件 & 初始化
#include "xparameters.h" #include "xuartlite.h" #include "xil_printf.h" XUartLite Uart; // UART 实例初始化函数一定要判断返回值:
int init_uart() { int status = XUartLite_Initialize(&Uart, XPAR_AXI_UARTLITE_0_DEVICE_ID); if (status != XST_SUCCESS) { return XST_FAILURE; } return XST_SUCCESS; }💡
XPAR_AXI_UARTLITE_0_DEVICE_ID是由 Vivado 自动生成的编号,通常为 0。
发送字符串(别再裸写 SendByte 了!)
虽然XUartLite_SendByte()可以用,但我建议封装一个 send_string:
void send_string(const char *str) { while (*str) { XUartLite_SendByte(XPAR_AXI_UARTLITE_0_BASEADDR, *str++); } }这样你可以轻松输出调试信息:
send_string("System booting...\r\n");关键点:xil_printf 重定向!
很多人为啥看不到输出?因为默认情况下xil_printf不走 UART!
你需要在工程中添加以下文件或手动设置:
👉 打开Standlone Settings→ Stdout 设备选axi_uartlite_0。
或者,在代码开头加上:
extern void outbyte(char c); // SDK 默认会链接这个函数 void outbyte(char c) { if (c == '\n') { XUartLite_SendByte(XPAR_AXI_UARTLITE_0_BASEADDR, '\r'); } XUartLite_SendByte(XPAR_AXI_UARTLITE_0_BASEADDR, c); }这样一来,所有printf、xil_printf都会自动重定向到串口。
串口没反应?五步快速定位法
别一头扎进代码里。按这个顺序排查,90% 的问题都能搞定。
✅ 第一步:物理层检查
- 用万用表测 TX/RX 是否短路?
- USB 转 TTL 模块灯亮吗?供电正常吗?
- 接线有没有交叉?FPGA 的 TX 应接模块的 RX!
我见过太多人把 TX 接 TX,还奇怪为啥没回信……
✅ 第二步:最小系统测试
写个纯硬件循环发送程序:
int main() { init_uart(); while (1) { send_string("OK\r\n"); for(int i=0; i<1000000; i++); // 简单调延时 } }烧进去,开 Tera Term,波特率设成一致(115200,8-N-1),看看有没有“OK”。
没有?继续下一步。
✅ 第三步:ILA 抓波形看真相
回到 Vivado Hardware Manager,加载.bit文件,启动 ILA:
- 触发条件设为
tx下降沿(起始位); - 观察
dout[7:0]是否是你期望发送的字符(比如 ‘O’=0x4F); - 看
baudoutn是否周期稳定(≈8.68μs 对应 115200bps)。
如果dout是对的,但电脑收不到 → 问题是出在外部电路;
如果dout根本没变化 → 软件没成功写寄存器。
✅ 第四步:查地址映射和驱动匹配
打开xparameters.h,确认这几项:
#define XPAR_AXI_UARTLITE_0_BASEADDR 0x40600000 #define XPAR_AXI_UARTLITE_0_HIGHADDR 0x4060FFFF #define XPAR_AXI_UARTLITE_0_BAUDRATE 115200 #define XPAR_AXI_UARTLITE_0_DEVICE_ID 0如果不一致,说明硬件平台没更新,重新导出.hdf。
✅ 第五步:对比官方例程
Xilinx 提供了 AXI UART Lite 的 Example Project:
路径一般在:
<Vivado Install>/data/examples/axi_uartlite/建一个新工程跑官方 Demo,能通 → 说明板子没问题;
不能通 → 检查硬件或换线。
高阶技巧:让调试更高效
技巧 1:加状态灯辅助判断
在 FPGA 上留一个 LED,指示程序是否进入主循环:
assign led = tx; // TX 发数据时灯闪或者在代码里控制 GPIO:
Xil_Out32(GPIO_BASEADDR, 0x01); // 开灯表示启动完成视觉反馈比等串口快多了。
技巧 2:分阶段打日志
不要等到最后才输出一句话。加几个标记:
xil_printf("Step 1: Clock OK\r\n"); xil_printf("Step 2: UART Init Done\r\n"); xil_printf("Step 3: Entering loop\r\n");一旦卡在哪一步,立刻知道问题范围。
技巧 3:接收处理防溢出
AXI UART Lite 的 RX 是单字节缓存,如果你不及时读,下一字节来了就会丢。
所以接收时最好加状态查询:
while ((XUartLite_GetStatusReg(XPAR_AXI_UARTLITE_0_BASEADDR) & 0x01) == 0); char c = XUartLite_RecvByte(XPAR_AXI_UARTLITE_0_BASEADDR);等待数据就绪再读,避免读空。
最后提醒:Vivado 2018.3 已老,但仍可用
我知道很多人还在用 2018.3,尤其是一些学校实验室或老旧项目维护。
但它确实有些局限:
- 不支持 newer devices(如 Kria KV260);
- HLS 和 AI Engine 支持弱;
- 官方不再提供补丁。
📌建议:
- 新项目尽量升级到Vivado 2020.2 或 2023.1;
- 老项目可维持不动,但注意备份工程;
- 如果必须用 2018.3,请统一团队版本,避免.xpr兼容问题。
写在最后
串口看似最基础,却是嵌入式开发中最关键的一环。它不像 PCIe 那样炫酷,也不像 DDR 控制器那样复杂,但它能在你系统崩溃时告诉你:“我还活着。”
掌握 AXI UART Lite 的调试方法,不只是学会一个 IP 的使用,更是建立起一种从硬件到软件、从信号到协议的系统级思维。
下次当你面对一片漆黑的终端窗口时,希望你能冷静下来,按照这几个步骤一步步查下去——
电源 → 时钟 → 引脚 → 地址 → 波特率 → 输出重定向。
总有一个地方,藏着那个让你抓狂的小错误。
如果你也在用 Vivado 做 FPGA 开发,欢迎留言交流你的调试经验。尤其是那些“我以为没问题,结果折腾三天”的坑,咱们一起填平它。