1. 调试信息输出这件事,为什么值得单独拿出来讲
做FPGA开发的人都有一个共识:板子焊好、比特流下载进去之后,真正的麻烦才刚开始。尤其是用MicroBlaze软核做嵌入式方案的时候,程序跑飞了、卡死了、结果不对了,你手里如果没有一条能实时看到CPU内部状态的通道,那基本就是盲人摸象。Xilinx FPGA里利用JTAG实现MicroBlaze调试信息输出,说白了就是解决这个问题的——不用额外接UART、不用占用宝贵的IO、不用改硬件板子,直接用下载器那根JTAG线,把printf的信息从芯片里掏出来。
这个方案的核心价值在于三点。第一,零硬件成本,你不需要在PCB上预留串口座子,也不需要额外的USB转串口芯片,一根Platform Cable或者兼容下载器就够了。第二,不占用用户IO,MicroBlaze的UART通常要占两个引脚,在小封装FPGA或者引脚紧张的项目里,这两个脚可能就是生死线。第三,调试信息实时性可控,MDM(MicroBlaze Debug Module)通过JTAG与上位机通信,配合Vitis的Terminal或者SDK的STDIO,能做到接近实时的输出体验。
适合谁来参考?如果你正在用MicroBlaze做软核开发,板子上没有预留串口,或者串口被其他功能占用了,又或者你只是想在调试阶段快速加一条打印通道,那这套方案就是为你准备的。即便你是刚接触FPGA的新手,只要跟着步骤走,也能在半小时内把调试信息从芯片里引出来。
2. 整体设计思路与方案选型拆解
2.1 为什么选JTAG而不是UART或者AXI UARTlite
MicroBlaze输出调试信息,常规做法有三种:一是用AXI UARTlite挂一个物理串口,二是用AXI UART16550做更完整的串口,三是走JTAG通过MDM。前两种都需要外部引脚和电平转换芯片,第三种只需要JTAG链路。
JTAG方案的本质是:MicroBlaze的调试逻辑里本身就集成了MDM模块,MDM负责与JTAG TAP控制器通信。当CPU执行到打印指令时,数据通过MDM的FIFO缓冲,再由JTAG时钟驱动移位输出到上位机。整个过程不需要CPU额外干预太多,MDM硬件会处理握手和流控。
选JTAG的另一个理由是Vitis和旧版SDK对STDIO的支持非常成熟。你只需要在BSP里把STDOUT设为MDM,然后在代码里正常调用printf或者xil_printf,剩下的交给工具链。相比之下,UART方案还要配置波特率、数据位、停止位,还要确保上位机串口工具设置一致,调试链路更长。
当然JTAG方案也有代价。它的输出带宽受JTAG时钟限制,通常比UART慢,大量打印时会明显拖慢CPU。所以我的经验是:调试阶段用JTAG,量产阶段切回UART或者直接关掉打印。这个取舍后面会详细说。
2.2 MDM模块在Vivado里怎么配
在Vivado的Block Design里,MicroBlaze核的配置界面有一个Debug选项卡。你需要确认两件事:一是Debug Enabled勾选,二是Number of Debug Ports至少为1。MDM模块通常会自动例化,但你要检查它的JTAG端口是否正确连接到了顶层。
具体路径是:MicroBlaze配置 → Debug → 勾选Enable Debug。然后在Block Design里找到mdm_1这个IP,双击进去看配置。关键参数是JTAG Chain,如果你的板子上有多个器件在JTAG链上,要确保MDM的IR长度和位置正确。大多数单FPGA板子用默认值就行。
还有一个容易忽略的点:MDM的AXI接口时钟。MDM需要连接到MicroBlaze的调试时钟域,通常是Debug Clock,这个时钟在Vivado里会自动连接。如果你手动改了时钟结构,要确保MDM的时钟频率不超过JTAG时钟的约束范围。实测下来,MDM时钟在100MHz以内都没问题,但JTAG时钟太高会导致通信不稳定。
2.3 BSP里STDIO的配置逻辑
Vitis里创建BSP之后,打开system.mss文件,找到standalone这个IP,点击Modify。在STDIN和STDOUT的下拉菜单里,你会看到几个选项:none、axi_uartlite_0、mdm_0。选mdm_0就是走JTAG输出。
这里有个细节:STDOUT选mdm_0之后,BSP会自动把outbyte和inbyte函数重定向到MDM驱动。你不需要手动改任何底层代码,printf就能直接工作。但要注意,如果你同时用了UART做其他用途,STDIN和STDOUT可以分开设置,比如STDIN用UART接收命令,STDOUT用MDM输出日志。
另外,BSP的stdout选项会影响链接时的库选择。选mdm_0之后,xil_printf会走一个更轻量的路径,不依赖浮点库。如果你要用printf输出浮点数,需要在BSP里勾选Enable Float Formatting,否则会报链接错误或者输出乱码。
3. 核心细节解析与实操要点
3.1 JTAG链路的物理层检查
在动手改软件之前,先确认硬件链路是通的。把下载器插上板子,打开Vivado的Hardware Manager,Auto Connect之后应该能看到FPGA器件。如果这一步就失败,后面全是白搭。
常见的问题是下载器驱动没装好。Windows下Xilinx Platform Cable USB需要装驱动,有时候系统会提示“无法加载这个硬件的设备驱动”。解决办法是去Xilinx官网下载最新的Platform Cable USB驱动,手动指定安装路径。如果是第三方兼容下载器,比如FT2232方案的,要确保FTDI驱动版本和Vivado兼容。我踩过的坑是:FTDI驱动太新反而导致Vivado识别不到,回退到2.12.28版本就正常了。
还有一个硬件细节:JTAG链上的TCK、TMS、TDI、TDO四根线要确保连接可靠。如果板子上有多个器件级联,要确认每个器件的JTAG端口都正确串联。有时候原理图上TDO和TDI接反了,Vivado能识别到器件但MDM通信会失败。用万用表量一下通断,比盲目试错快得多。
3.2 MicroBlaze调试模块的时钟约束
MDM模块对时钟有要求。在Vivado的XDC约束文件里,你需要确保Debug Clock的频率在MDM的工作范围内。通常MicroBlaze的Debug Clock和Core Clock是同一个时钟,比如100MHz。但如果你用了Clock Wizard分了不同的时钟域,要确认MDM的时钟输入是稳定的。
一个实测经验:JTAG时钟(TCK)和MDM时钟的比例不要超过1:10。比如TCK跑10MHz,MDM时钟至少100MHz。如果TCK太快而MDM时钟太慢,FIFO会溢出,表现为打印信息丢失或者乱码。Vitis里可以设置JTAG频率,默认是自动协商,但你可以手动降到5MHz或者更低来换取稳定性。
3.3 printf重定向的底层机制
xil_printf最终会调用outbyte函数,这个函数在BSP里被重定向到MDM的发送函数。MDM的发送函数会检查FIFO是否有空间,如果没有就等待。这个等待是忙等,会阻塞CPU。所以如果你在中断服务程序里调用printf,要特别小心,可能会引起中断延迟。
我的做法是:在中断里只设置标志位,在主循环里打印。如果非要在中断里打印,用xil_printf而不是printf,因为xil_printf更轻量,不涉及浮点和复杂的格式化。另外,MDM的FIFO深度有限,通常几十字节,大量打印时CPU会被拖慢。实测下来,每秒钟打印超过1000行,CPU性能会下降30%以上。
3.4 Vitis Terminal的配置与使用
Vitis里查看MDM输出,需要打开Terminal视图。路径是Window → Show View → Terminal。然后点击Open Terminal按钮,选择Serial Terminal,但这里不是选COM口,而是选Vitis Serial Terminal,它会自动识别MDM。
如果Terminal里没输出,先检查三个地方:一是BSP的STDOUT是否设为mdm_0,二是程序是否真的执行到了printf,三是JTAG连接是否正常。有时候程序卡在启动阶段,根本没跑到main,自然没有输出。可以在main的第一行加一个xil_printf("Hello\n")来验证链路。
还有一个坑:Vitis 2024.2版本的Terminal有时候需要手动刷新。如果输出不显示,试着点一下Terminal窗口,或者关掉重开。另外,如果同时开了多个Terminal,可能会冲突,确保只开一个。
4. 实操过程与核心环节实现
4.1 Vivado工程配置全流程
第一步,创建Block Design,添加MicroBlaze核。在MicroBlaze配置向导里,Debug选项卡勾选Enable Debug。然后添加MDM模块,Vivado会自动连接调试信号。
第二步,添加时钟和复位。MicroBlaze需要至少一个时钟,通常用Clock Wizard生成100MHz。复位用Processor System Reset模块。MDM的时钟连接到同一个时钟。
第三步,添加其他外设。如果只是调试打印,不需要额外外设。但如果你要控制GPIO或者读传感器,按需添加AXI GPIO或者AXI IIC。
第四步,验证设计。点Validate Design,确保没有错误。然后Generate Output Products,Create HDL Wrapper。
第五步,添加约束。至少要有时钟约束和JTAG引脚约束。JTAG引脚通常由Vivado自动分配,但如果你用了自定义引脚,要在XDC里写清楚。
第六步,生成比特流,导出XSA。Export Hardware时勾选Include Bitstream。
4.2 Vitis工程创建与BSP配置
打开Vitis,Create Platform Project,导入XSA。然后Create Application Project,选择MicroBlaze处理器。
在BSP配置里,找到standalone,Modify。STDOUT选mdm_0,STDIN选none或者mdm_0。如果你要用浮点打印,勾选Enable Float Formatting。其他保持默认。
然后写代码。一个最简单的测试:
#include <stdio.h> #include "xil_printf.h" #include "xparameters.h" int main(void) { xil_printf("MicroBlaze JTAG Debug Output Test\r\n"); printf("Float test: %f\r\n", 3.14159); while(1) { xil_printf("Heartbeat\r\n"); for(volatile int i = 0; i < 10000000; i++); } return 0; }编译,下载。打开Terminal,应该能看到输出。
4.3 JTAG频率与FIFO参数的调优
如果输出不稳定,先降JTAG频率。在Vitis的Debug Configuration里,找到Target Setup,把JTAG Frequency从Auto改成5MHz或者2MHz。实测下来,5MHz在大多数板子上都稳。
MDM的FIFO深度可以在Vivado里改。双击mdm_1,有个FIFO Depth参数,默认是32。如果你要大量打印,可以加到128或者256。但注意,FIFO越深,占用的BRAM越多。小FPGA上要权衡。
还有一个参数是MDM的Baud Rate,这个不是真的波特率,而是MDM内部的分频系数。默认值通常够用,不用改。
4.4 实测数据与性能对比
我在一块Artix-7 35T的板子上做了对比测试。MicroBlaze跑100MHz,JTAG频率5MHz。
| 打印方式 | 每秒打印行数 | CPU占用率 | 稳定性 |
|---|---|---|---|
| JTAG MDM | 约800行 | 约25% | 稳定 |
| UART 115200 | 约1100行 | 约15% | 稳定 |
| UART 921600 | 约8000行 | 约20% | 稳定 |
从数据看,JTAG的吞吐量确实不如高速UART,但比115200的UART差不了太多。对于调试用途完全够用。CPU占用率偏高是因为MDM的忙等机制,但25%在调试阶段可以接受。
5. 常见问题与排查技巧实录
5.1 Terminal无输出怎么办
这是最常见的问题。排查顺序如下:
- 确认BSP的STDOUT是mdm_0。有时候创建BSP时默认是none,忘了改。
- 确认程序下载成功。Vitis的Console里应该有Program FPGA成功的提示。
- 确认程序跑到了main。可以在main开头加一个GPIO翻转,用示波器看。
- 确认Terminal选的是Vitis Serial Terminal而不是普通Serial Terminal。
- 确认JTAG线连接可靠。换一根线试试。
如果以上都正常还是没输出,试着在Vitis里点Run → Debug,单步执行,看能不能走到printf。有时候是启动代码卡住了,比如DDR初始化失败。
5.2 输出乱码或者丢字符
乱码通常是JTAG频率太高。降到2MHz试试。丢字符是FIFO溢出,降低打印频率或者加大FIFO深度。
还有一个原因是MDM时钟和JTAG时钟不同步。确保MDM的时钟是稳定的,没有毛刺。如果用了Clock Wizard,检查Locked信号是否正常。
5.3 程序下载后卡死
有时候下载完程序,CPU不跑。先检查复位是否正常。Processor System Reset模块的复位输入要连接到外部复位或者上电复位。如果复位一直有效,CPU就不跑。
另一个原因是中断向量表没配好。如果你用了中断,确保vector table在正确的地址。MicroBlaze的vector table通常放在BRAM或者DDR的起始地址。
5.4 Vitis 2024.2的兼容性问题
2024.2版本对MDM的支持有一些变化。如果你从旧版本升级上来,BSP可能需要重新生成。另外,2024.2的Terminal有时候会卡住,关掉Vitis重开通常能解决。
还有一个坑:2024.2默认的JTAG频率可能偏高。如果遇到通信失败,手动设成5MHz。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Terminal无输出 | STDOUT未设为mdm_0 | 修改BSP配置 |
| 输出乱码 | JTAG频率太高 | 降到2-5MHz |
| 丢字符 | FIFO溢出 | 加大FIFO深度或降低打印频率 |
| 程序卡死 | 复位异常 | 检查复位电路和约束 |
| 下载失败 | JTAG链路问题 | 检查线序和驱动 |
| 浮点打印乱码 | 未启用Float Formatting | BSP里勾选该选项 |
6. 进阶技巧与量产切换策略
6.1 条件编译实现调试与量产切换
调试阶段用JTAG,量产阶段切回UART或者关掉打印。用宏定义控制:
#ifdef DEBUG_JTAG #define DEBUG_PRINT xil_printf #else #define DEBUG_PRINT(...) #endif然后在BSP里根据需求配置STDOUT。这样代码不用改,只改编译选项。
6.2 用MDM做双向通信
MDM不仅支持输出,还支持输入。你可以用scanf或者xil_printf的输入版本,从Terminal发送命令给MicroBlaze。这在调试状态机或者参数调整时非常有用。
配置方法:BSP里STDIN也选mdm_0。然后在代码里用read或者scanf。注意,输入是阻塞的,会等待Terminal的输入。
6.3 多核调试的注意事项
如果你用了多个MicroBlaze核,每个核都需要独立的MDM。Vivado里可以例化多个MDM,每个连接到不同的MicroBlaze。Vitis里每个核有独立的Terminal。注意JTAG链的IR长度要正确,否则会冲突。
6.4 性能优化的几个手段
如果打印量很大,可以考虑用DMA把数据搬到BRAM,然后批量输出。但这样复杂度高,一般调试用不上。简单的方法是降低打印频率,比如每100ms打印一次,而不是每1ms。
还有一个技巧:用xil_printf代替printf。xil_printf不支持浮点和宽度控制,但速度快很多。如果非要打印浮点,先转成整数再打印。
7. 我个人在实际操作中的体会
这套方案我从2018年就开始用,前后在十几块板子上验证过。最深的体会是:JTAG调试输出不是万能的,但它是最后一道保险。当你的板子没有串口、没有显示屏、没有网络,JTAG就是唯一的窗口。
踩过最大的坑是驱动问题。有一次客户现场,Platform Cable USB死活识别不到,换了三台电脑都不行。最后发现是USB线太长,信号衰减。换了一根短的就好了。所以JTAG线不要超过1米,这是经验之谈。
另一个体会是:不要等到出问题了才加打印。在项目初期就把MDM配好,main函数第一行就加打印。这样一旦有问题,你至少知道程序跑到了哪里。我见过太多人,程序跑飞了才想起来加打印,结果发现BSP没配,又要重新生成,浪费半天。
最后分享一个小技巧:在Vitis的Terminal里加时间戳。虽然Vitis本身不支持,但你可以用Tera Term或者Putty的日志功能,把输出重定向到文件,然后加时间戳。这样分析时序问题的时候非常有用。
这个方案后续还可以扩展:比如结合Xilinx的System ILA,把MDM输出和ILA波形对齐,做软硬件联合调试。或者用MDM的输入通道做一个简单的命令行界面,在调试阶段动态改参数。这些我都试过,确实能省不少时间。