3. 搭建最小调试系统:创建Vivado工程、例化RISC-V核、添加ILA探针
好,咱们进入实战环节。这一章,我们要亲手搭建一个最小调试系统。说白了,就是让RISC-V核能在FPGA上跑起来,同时我们还能用ILA盯着它看。
我个人习惯,做任何调试系统之前,先画个框图。脑子里想清楚信号怎么流,再动手。不然代码写一半发现少了个关键信号,那才叫头疼。
3.1 创建Vivado工程——别急着点Next
打开Vivado,选好器件型号。我建议你直接用xc7a35ticsg324-1L或者你板子上对应的型号。创建工程时,记得勾上「RTL Project」,别选「Post-synthesis」——我们后面还要加ILA呢。
创建好工程后,第一件事是添加约束文件。哪怕你还没写任何RTL代码,先把时钟引脚约束写好。我个人习惯用.xdc文件,清晰明了。
# 时钟约束 - 50MHz输入
create_clock -period 20.000 [get_ports clk_i]
# 复位引脚 - 低有效
set_property PACKAGE_PIN R4 [get_ports rstn_i]
set_property IOSTANDARD LVCMOS33 [get_ports rstn_i]
# 串口引脚(后续调试用)
set_property PACKAGE_PIN U12 [get_ports uart_tx_o]
set_property IOSTANDARD LVCMOS33 [get_ports uart_tx_o]
3.2 例化RISC-V核——别被那么多端口吓到
我们用的RISC-V核是VexRiscv,一个用SpinalHDL写的可配置核。你第一次看到它的端口列表可能会有点懵——怎么这么多信号?
其实核心就几类:
- 时钟复位:
clk_i、rstn_i - 指令总线:
iBus_cmd_valid、iBus_cmd_ready、iBus_cmd_payload_pc - 数据总线:
dBus_cmd_valid、dBus_cmd_ready、dBus_cmd_payload_wr - 调试接口:
debug_reset、debug_run、debug_pc
嗯,这里要注意:调试接口不是必须连的。如果你只是想让核跑起来,把debug_reset拉低就行。但我们要加ILA,所以debug_pc这个信号一定要引出来。
// RISC-V 核例化模板
vexriscv_wrapper u_cpu (
.clk_i (sys_clk),
.rstn_i (sys_rstn),
// 指令总线 - 接BRAM
.iBus_cmd_valid (ibus_cmd_valid),
.iBus_cmd_ready (ibus_cmd_ready),
.iBus_cmd_payload_pc(ibus_pc),
.iBus_rsp_valid (ibus_rsp_valid),
.iBus_rsp_payload_inst(ibus_inst),
// 数据总线 - 接BRAM
.dBus_cmd_valid (dbus_cmd_valid),
.dBus_cmd_ready (dbus_cmd_ready),
.dBus_cmd_payload_wr(dbus_wr),
.dBus_cmd_payload_address(dbus_addr),
.dBus_cmd_payload_data(dbus_wdata),
.dBus_rsp_valid (dbus_rsp_valid),
.dBus_rsp_payload_data(dbus_rdata),
// 调试信号 - 引出给ILA
.debug_pc (cpu_pc),
.debug_instruction (cpu_inst),
.debug_stall (cpu_stall)
);
debug_pc和debug_instruction是ILA的核心观测对象。我建议你至少把这俩信号引出来,不然你都不知道CPU跑到哪条指令了。
3.3 添加ILA探针——别贪多,够用就行
ILA(Integrated Logic Analyzer)是Vivado自带的逻辑分析仪。添加ILA有两种方式:
- GUI方式:在Netlist窗口右键,选择「Debug」→「Mark Debug」
- HDL方式:直接在RTL代码里例化ILA IP核
我推荐用第二种方式。为什么?因为可控性更强。GUI方式有时候会莫名其妙地优化掉一些信号,尤其是跨时钟域的信号。
添加ILA IP核的步骤:
- 在IP Catalog里搜索「ILA」
- 配置采样深度:1024够用,别选太大,否则BRAM消耗惊人
- 配置探针数量:我一般选4-8个,每个32位
- 采样时钟:直接用CPU的主时钟
ILA的触发条件设置也很关键。比如你想看CPU执行到地址0x80001000时的行为,可以这样配:
// ILA 触发条件设置(在Vivado Hardware Manager中)
// 触发端口: probe0 (cpu_pc)
// 触发条件: == 0x80001000
// 触发位置: 中间(便于看前后波形)
3.4 连接存储——别忘了程序放哪
RISC-V核需要指令和数据存储器。最简单的做法是用Block RAM。我建议例化一个双端口BRAM,一个端口给指令总线,一个端口给数据总线。
| 存储器 | 地址范围 | 大小 | 说明 |
|---|---|---|---|
| 指令BRAM | 0x80000000 - 0x80003FFF | 16KB | 存放程序代码 |
| 数据BRAM | 0x80004000 - 0x80007FFF | 16KB | 存放堆栈和数据 |
你想想看,如果BRAM初始化内容为空,CPU上电后第一条指令都取不到。所以我们需要用.coe文件或者.mem文件来初始化BRAM。这个我们下一章再细讲。
3.5 综合与实现——第一次跑通
所有代码写完后,点击「Run Synthesis」。Vivado会开始综合。这个过程可能需要几分钟,取决于你的工程大小。
综合完成后,打开综合报告,重点关注:
- 时序报告:看WNS(最差负时序裕量)是不是正数
- 资源使用:LUT、FF、BRAM用了多少
- 警告信息:有没有未连接的端口或者跨时钟域问题
create_clock的周期写错了,导致Vivado以为我们要跑200MHz。
综合通过后,点击「Run Implementation」。布局布线完成后,生成比特流。这一步如果报错,多半是引脚约束冲突或者BRAM不够用。
最后,把比特流下载到FPGA里。打开Vivado Hardware Manager,你应该能看到ILA的窗口了。点击运行,ILA开始采样——如果CPU在跑,你应该能看到cpu_pc在变化。
嗯,到这里,最小调试系统就搭好了。你可能会问:「为什么CPU没跑起来?」别急,下一章我们就要解决这个问题——程序加载和启动流程。