Zynq 实战 07|硬件平台设计与验证:IP Integrator 全流程,从搭 Block Design 到导出 .xsa
Zynq 实战 07|硬件平台设计与验证
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 7 篇。 板子:Pynq-Z2(XC7Z020-1CLG400C)。工具链:Vivado / Vitis 2023.2。 上一篇:《Zynq 实战 06|自定义 AXI IP 核开发》
0. 这一篇要解决什么问题
前几篇我们把 AXI 协议、IP 打包流程都走过一遍了。这一篇是硬件设计的汇合点——把所有零件组装成一个完整的 Zynq 系统,生成比特流,导出给 Vitis 开始写软件。
具体我们要搭这样一个系统:
- ZYNQ7 Processing System:PS 核心,使能 M_AXI_GP0 + IRQ_F2P
- AXI GPIO:控制 4 个 LED + 读 2 个按键
- AXI Timer:32-bit 可编程定时器,产生定时中断
- 自定义 PWM IP(来自第 06 篇):输出 PWM 信号,产生中断
- Processor System Reset:统一管理 PL 侧复位
- Concat IP:把多路中断汇聚成一根 IRQ_F2P
本文不会讲:Vitis 软件编写(那是下一篇)、PetaLinux 集成(第 10 篇往后)。
1. 创建工程,进入 IP Integrator
假设你已经创建了 Vivado 工程,目标器件 xc7z020clg400-1(Pynq-Z2)。打开工程后,在 Flow Navigator → IP INTEGRATOR → Create Block Design,弹出对话框填 design_1,点 OK。
Diagram 窗口打开后是空的。在空白区右键选 Add IP(或按快捷键 P),搜索添加第一个 IP:ZYNQ7 Processing System。
拖进来之后你会看到一条绿色提示横幅:“Run Block Automation”。先别急着点,我们先来看看这个操作实际做什么。
Run Block Automation 内部发生了什么
Block Automation 是 Vivado 针对 ZYNQ7 PS IP 的特殊处理,只对 PS 有效(不是所有 IP 都有这个选项)。点击后它会做:
- 读取 Vivado 里配置的 Board Preset(Pynq-Z2 的
.xboard文件) - 自动配置 PS 的 DDR 引脚(PS7_DDR_*)和固定 IO(PS7_FIXED_IO)的约束
- 把 PS 的 DDR 接口和 Fixed IO 在 Diagram 中折叠成两个外部端口(你会看到
DDR和FIXED_IO引脚出现在 PS block 外部)
如果你没有加载 Board Preset(比如用的非官方板子),Block Automation 还是能跑,但它会提示”No preset found”,你需要手动配置 DDR 参数。
点击 Run Block Automation,勾选 Apply Board Preset,确认后 PS block 多出了 DDR 和 FIXED_IO 两个外部接口。
接下来双击 PS block,进入配置界面,做两件事:
① 确认时钟设置(Page: Clock Configuration):
- FCLK_CLK0 保持默认 100 MHz(来自 IOPLL,对应 UG585 第 25 章 Clock Management)
- 如果你需要第二个不同频率的时钟,在这里打开 FCLK_CLK1 并设置目标频率(比如 200 MHz 给高速路径)
② 打开 IRQ_F2P 输入(Page: Interrupts):
- 找到 Fabric Interrupts → PL-PS Interrupt Ports → IRQ_F2P,勾上
[0:0](先只用第 0 号,后面用 Concat 扩展)
2. 添加 PL 端 IP 并 Run Connection Automation
依次添加以下 IP(全部用 P 键搜索):
AXI GPIO× 1(控制 LED 和按键)AXI Timer× 1- 你在第 06 篇打包的
pwm_v1_0(搜索不到就先加入 User Repository:Tools → Settings → IP → Repository) Processor System Reset× 1Concat× 1
添加完这 5 个 IP 后,Diagram 顶部会再次出现绿色提示:“Run Connection Automation”。这次我们来看它做的事更多:
Run Connection Automation 内部发生了什么
Connection Automation 是通用的(对所有 IP 生效),它的逻辑是:
- 扫描 Diagram 里所有未连接的接口引脚
- 对符合特定模式的接口(AXI4-Lite slave ↔ PS GP master;aclk ↔ FCLK;aresetn ↔ 复位)自动完成连线
- 如果有多个 AXI slave,自动插入一个 AXI Interconnect(或 SmartConnect),并分配地址
勾选所有项,点击 OK。Vivado 会:
- 用 AXI Interconnect(4 slave 端口)连接 PS 的
M_AXI_GP0到 GPIO/Timer/PWM 的 AXI slave 接口 - 把每个 IP 的
s_axi_aclk连到FCLK_CLK0(100 MHz) - 把
Processor System Reset的输入时钟也连到FCLK_CLK0 - 把各 IP 的
s_axi_aresetn连到proc_sys_reset_0的peripheral_aresetn[0:0]
完成后整个 Diagram 已经有 80% 的连线了。
🚧 避坑:Connection Automation 不会自动连中断线。中断必须你手动处理——这正是下一节的核心。很多初学者跑完 Automation 就直接 Validate,然后发现 IRQ_F2P 是空的,在 Vitis 里永远等不到中断。
3. 时钟分发:FCLK_CLK0 怎么喂给多个 IP
当前设计里所有 IP 都跑在 FCLK_CLK0(100 MHz)。实际上这已经够了——AXI GPIO 和 AXI Timer 在 100 MHz 都没问题,PWM IP 的 counter 在 100 MHz 跑也能产生足够精度(1 μs 分辨率)。
如果你需要让某个 IP 跑在不同频率,有两种方案:
方案 A:在 PS 里再打开 FCLK_CLK1
进 PS 配置 → Clock Configuration,打开 FCLK_CLK1,设置频率(比如 200 MHz)。然后把需要高频的 IP(比如一个视频处理模块)的 aclk 连到 FCLK_CLK1。
这是最简单的方案。PS 的 IOPLL 可以同时输出最多 4 个独立频率(FCLK_CLK0~3),每个都可以独立设置。精度取决于 IOPLL 的分频系数,不是每个频率都能精确打到——Vivado 会显示你配 200 MHz 实际拿到 199.998 MHz 之类的,正常。
方案 B:接 Clock Wizard IP
从 FCLK_CLK0 出来,进一个 Clocking Wizard,在里面配输出时钟数量和频率。适合需要精确相位关系或非整数分频的场景,比如 LCD PCLK 需要和行场信号严格对齐。
🚧 避坑:MMCM/PLL(Clock Wizard 内部用的)有锁定延迟,上电后要等
locked信号拉高才能撤复位。如果你用了 Clock Wizard,Processor System Reset的dcm_locked输入要连 Clock Wizard 的locked,不然 PL 复位会在时钟还没稳定时就释放,导致初始化出错。
4. Processor System Reset 的作用
Processor System Reset(PG164)这个 IP 几乎每个 Block Design 都需要,但经常被当成”黑盒”随手连上。它做的事:
- 监控多个复位源:PS 发来的
ps7_0/FCLK_RESET0_N(低有效)、来自 MMCM 的dcm_locked信号、外部ext_reset_in - 同步化复位:把异步复位信号同步到本 IP 的工作时钟域,消除亚稳态
- 输出多路复位:
interconnect_aresetn(给 AXI Interconnect 用)和peripheral_aresetn[0:N](给各个外设 IP 用)
复位时序:只有当所有有效的复位源都撤销之后,Processor System Reset 才会释放 peripheral_aresetn。顺序是:PS 复位撤销 → MMCM locked → 等若干个时钟周期 → 才拉高 aresetn。
当前设计里,proc_sys_reset_0 的连线应该是:
slowest_sync_clk→FCLK_CLK0ext_reset_in→ PS 的FCLK_RESET0_N(Connection Automation 已经自动连了)dcm_locked:如果没用 Clock Wizard,接VCC(常量 1,表示”时钟永远锁定”)
5. 中断信号汇聚:Concat IP 是唯一正确方式
这是本篇最重要的细节,也是原始教材里写得最含糊的地方。
ZYNQ7 PS 的 IRQ_F2P 端口接受最多 16 个中断输入([15:0])。但这不意味着你可以直接把 3 个 IP 的中断线各连一根到 IRQ_F2P 的不同位——你在 Vivado 的 Block Design 里做不到这个(Vivado 不允许把一个 bus 口拆开接线)。
正确做法:用 Concat IP 把多路 1-bit 中断合并成一根 N-bit 总线,再接到 IRQ_F2P。
配置 Concat IP:双击 → 设置 Number of Ports = 3(对应 GPIO_interrupt + Timer_interrupt + PWM_interrupt)。
连线:
AXI GPIO ip2intc_irpt ──→ Concat In0
AXI Timer interrupt ──→ Concat In1
pwm_v1_0 irq_out ──→ Concat In2
│
dout[2:0] ──→ ZYNQ7 PS IRQ_F2P[2:0]
为什么不能绕过 Concat 直接连?
IRQ_F2P 中断号分配是按位置固定的:你接到 IRQ_F2P[0] 的 IP,在软件里对应的中断 ID 是 XPS_FPGA0_INT_ID = 61(UG585 Table 7-4);[1] 对应 62,以此类推。如果你因为某种原因(比如手动在 HDL 里拼接)让中断顺序乱了,软件里注册的 ISR 就会被错误的 IP 触发——这种 bug 极难排查。
用 Concat IP 的好处:Vivado 会在生成 .xsa 时,把 Concat 的接线关系写进硬件描述,Vitis/XSCT 可以直接查到 IRQ_F2P 每一位对应的 IP,自动分配中断 ID,无需手算。
6. Address Editor:地址段分配规则
点击 Diagram 顶部的 Address Editor 标签页。你应该看到类似这样的表:
| IP | Interface | Master | Base Address | Range | High Address |
|---|---|---|---|---|---|
| axi_gpio_0 | S_AXI | /ps7_0/M_AXI_GP0 | 0x4120_0000 | 64K | 0x4120_FFFF |
| axi_timer_0 | S_AXI | /ps7_0/M_AXI_GP0 | 0x4280_0000 | 64K | 0x4280_FFFF |
| pwm_v1_0 | S_AXI | /ps7_0/M_AXI_GP0 | 0x43C0_0000 | 64K | 0x43C0_FFFF |
地址分配规则(来自 UG585 第 4.3 节 Address Map):
- Zynq 的 M_AXI_GP0 可访问地址范围:
0x4000_0000~0x7FFF_FFFF(共 1GB) - 每个从机的地址块默认分配 64K,基地址必须 64K 对齐(即低 16 位必须是 0)
- Connection Automation 会自动分配不重叠地址,但如果你手动添加 IP 或改了范围,需要检查有无重叠
如何修改地址:在 Address Editor 里双击 Base Address 直接改数字。改完之后做一次 Validate Design(见下一节),它会检查有无地址重叠。
🚧 避坑:如果你用了 AXI Interconnect 而非 SmartConnect,且超过 16 个从机,地址段会快速占用完 GP0 的 1GB 空间。超过限制时 Validate 会报”address decode capacity exceeded”。解决方案:切换到 SmartConnect(它用 LUTRAM 实现更大的地址解码表),或者使用 HP 端口。
7. Validate Design:读懂红色和橙色警告
按快捷键 F6 或点击菜单 Tools → Validate Design。
如果设计是正确的,会弹出:“Validation successful. There are no errors or critical warnings in this design.” ← 这才是你的目标。
以下是最常见的错误类型和真实含义:
🔴 红色 Error(必须修复)
"IP clock is not connected"→ 某个 IP 的 aclk 引脚是悬空的。检查 Diagram,找到那个缺 clock 连线的 IP,连到 FCLK_CLK0"Interface not connected: /axi_gpio_0/S_AXI"→ IP 的 AXI 接口没接到 Interconnect 上。右键点那个 IP 的 S_AXI 端口,选 Make Connection
🟠 Critical Warning(不阻止生成,但会导致功能错误)
"CRITICAL WARNING: [BD 41-759] ... IRQ_F2P has no connections"→ 你打开了 IRQ_F2P 但没连任何东西。如果不用中断,就在 PS 配置里关掉 IRQ_F2P;如果用,就连好 Concat"CRITICAL WARNING: [BD 41-935] ... Clock domain crossing"→ 你把一个在 Clock A 下的接口连到了 Clock B 下的接口,没有 CDC(Clock Domain Crossing)处理。这不会报 Error,但会导致实际电路出现采样错误,极难调试
🚧 避坑:Vivado 的”Critical Warning”命名非常误导——它不是警告,它代表着你的设计在这个点上很可能功能错误,只是 Vivado 无法 100% 确定而不把它升级为 Error。每一条 Critical Warning 都要仔细读,不要无脑忽略。
8. Generate Output Products → Create HDL Wrapper
Validate 通过后,按照以下顺序操作:
Step 1: Generate Output Products
在 Sources 面板里右键点击 design_1.bd,选 Generate Output Products,会弹出一个设置窗口。关键选项:
Synthesis Options:
- Out of context per IP(默认):每个 IP 单独综合成子 DCP 文件(
.dcp),缓存在ip_user_files/目录。下次 IP 参数没改时复用,大幅加速整体综合。推荐用这个 - Global Synthesis:把所有 IP 和你的顶层逻辑放在一起综合。缺点是每次都要全量综合,慢;好处是跨模块优化更彻底,极少数情况下能拿到更好的时序
实际工程建议:开发阶段用 Out of context,最终流片前可以跑一次 Global 做最终优化。
点 Generate,等待完成(几十秒)。
Step 2: Create HDL Wrapper
右键 design_1.bd → Create HDL Wrapper,弹出对话框选:
✅ Let Vivado manage wrapper and auto-update
这样每次你修改 Block Design 后,顶层 Wrapper 会自动重新生成。选 Copy generated wrapper to allow user edits 的话你自己管,改 BD 后要手动重新 Create 才更新。
生成的顶层文件是 design_1_wrapper.v(默认在 Sources 面板的顶层)。它的结构大概长这样:
// design_1_wrapper.v — 由 Vivado 自动生成,不要手动改
// (只包含了 Block Design 导出的外部端口)
module design_1_wrapper (
// PS 侧的 DDR 和 Fixed IO——这些直接连到芯片封装管脚
inout [14:0] DDR_addr,
inout [2:0] DDR_ba,
inout DDR_cas_n,
inout DDR_ck_n,
inout DDR_ck_p,
inout DDR_cke,
inout DDR_cs_n,
inout [3:0] DDR_dm,
inout [31:0] DDR_dq,
inout [3:0] DDR_dqs_n,
inout [3:0] DDR_dqs_p,
inout DDR_odt,
inout DDR_ras_n,
inout DDR_reset_n,
inout DDR_we_n,
inout FIXED_IO_ddr_vrn,
inout FIXED_IO_ddr_vrp,
inout [53:0] FIXED_IO_mio,
inout FIXED_IO_ps_clk,
inout FIXED_IO_ps_porb,
inout FIXED_IO_ps_srstb,
// PL 侧的自定义 IO
output [3:0] led_tri_o,
input [1:0] btn_tri_i,
output pwm_out
);
design_1 design_1_i (
.DDR_addr (DDR_addr),
// ...所有端口穿透
.led_tri_o (led_tri_o),
.btn_tri_i (btn_tri_i),
.pwm_out (pwm_out)
);
endmodule
注意:DDR 和 FIXED_IO 的端口都是 inout,不需要你在 XDC 里约束引脚位置——这些引脚位置已经固化在 PS7 原语里,由 Board Preset 自动处理。你在 XDC 里只需要处理 PL 侧的外部 IO(LED、按键、PWM 等)。
9. 编写 .xdc 约束文件(Pynq-Z2 真实引脚)
在 Sources 面板右键 → Add Sources → Add or create constraints,新建 pynq_z2.xdc。
以下是 Pynq-Z2 的真实引脚约束(来源:Pynq-Z2 原理图 v1.0,TUL Embedded,主板上丝印位置和 schematic 对应):
# ============================================================
# Pynq-Z2 约束文件 | Vivado 2023.2 | XC7Z020-1CLG400C
# 注意:DDR/FIXED_IO 引脚不需要在这里约束(PS7 原语自动处理)
# ============================================================
# ——— LED(Active High,4 个) ———
set_property PACKAGE_PIN R14 [get_ports {led_tri_o[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led_tri_o[0]}]
set_property PACKAGE_PIN P14 [get_ports {led_tri_o[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led_tri_o[1]}]
set_property PACKAGE_PIN N16 [get_ports {led_tri_o[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led_tri_o[2]}]
set_property PACKAGE_PIN M14 [get_ports {led_tri_o[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led_tri_o[3]}]
# ——— 按键(Active High,2 个)———
# BTN0 = D19(板子左下侧靠近 USB 的按键)
set_property PACKAGE_PIN D19 [get_ports {btn_tri_i[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {btn_tri_i[0]}]
# BTN1 = D20
set_property PACKAGE_PIN D20 [get_ports {btn_tri_i[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {btn_tri_i[1]}]
# ——— 拨码开关(Active High)———
# SW0 = M20
set_property PACKAGE_PIN M20 [get_ports {sw_tri_i[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sw_tri_i[0]}]
# SW1 = M19
set_property PACKAGE_PIN M19 [get_ports {sw_tri_i[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {sw_tri_i[1]}]
# ——— PWM 输出(以 PMOD JA 的 JA1 引脚 Y18 为例)———
set_property PACKAGE_PIN Y18 [get_ports pwm_out]
set_property IOSTANDARD LVCMOS33 [get_ports pwm_out]
# ——— 时钟约束 ———
# FCLK_CLK0 是 PS 内部时钟,不是外部端口,不需要 create_clock
# 如果 PL 有外部直接驱动的时钟端口(非 Block Design 内部 FCLK),才需要:
# set_property PACKAGE_PIN H16 [get_ports sys_clock]
# set_property IOSTANDARD LVCMOS33 [get_ports sys_clock]
# create_clock -period 8.000 -name sys_clk_pin [get_ports sys_clock]
# (H16 是 Pynq-Z2 板上 125 MHz 外部晶振直连 PL 的引脚)
# ——— 误报抑制(可选) ———
# 对于 Zynq 设计,PS7 内部有很多跨时钟路径,Vivado 会产生大量
# false path 警告。如果你的设计纯 FCLK 驱动,可以加:
# set_false_path -from [get_cells -hierarchical -filter {NAME =~ *ps7_0*}]
关于 create_clock 的说明:
很多教程说”Zynq 需要 create_clock -period 10.000”。这对于完全由 Block Design 内部 FCLK 驱动的设计是错的——FCLK_CLK0 是 PS7 原语内部产生的,Vivado 在综合时会自动识别这个时钟,不需要(也不应该)在 XDC 里手动 create_clock。
你需要 create_clock 的情况:在顶层 HDL Wrapper 里(或 Block Design 里)有来自板卡管脚的外部时钟输入端口,比如 sys_clock 直接连到某个 PL 时钟 buffer 的输入。
10. Generate Bitstream → 导出 .xsa 给 Vitis
一切就绪,Flow Navigator 底部点 Generate Bitstream。Vivado 会依次执行:
- Synthesis:把 HDL Wrapper + 所有子模块(包括 BD 生成的网表)转换成门级网表,典型时间 1-5 分钟
- Implementation:布局(Placer)→ 布线(Router)→ 时序优化,典型时间 3-15 分钟
- Bitstream Generation:把布线结果转成 Zynq 配置格式,输出
.bit文件,1-2 分钟
完成后输出文件在:
<project_name>/<project_name>.runs/impl_1/
├── design_1_wrapper.bit ← 比特流文件
└── design_1_wrapper_routed.dcp ← 布线结果(用于增量重编译)
导出硬件平台(.xsa)
菜单 File → Export → Export Hardware:
- Platform type:选
Fixed(固定硬件平台,适合 Vitis Baremetal / FreeRTOS) - Include bitstream:✅ 勾上(这样 Vitis 可以直接下载到板子)
- Output file:选择一个路径,比如
~/Projects/pynq_z2_hw/pynq_z2.xsa
生成的 .xsa 文件是一个 ZIP 档(你可以用 unzip 看它里面有什么):
pynq_z2.xsa
├── design_1_wrapper.bit ← 比特流
├── design_1.bda ← Block Design 记录
├── pynq_z2_bd.tcl ← BD 重建脚本
├── psynth.tcl ← PS 配置
└── xsa.xml ← 硬件元数据(IP 地址映射、中断编号等)
xsa.xml 里包含了所有 IP 的基地址、中断 ID 分配——这是 Vitis 用来生成 BSP(Board Support Package)的关键。所以地址和中断连对了,Vitis 里的驱动才能自动初始化正确。
在 Vitis 里:File → New → Platform Project,选择 Browse for XSA,指向刚才导出的 .xsa,后面的事就是下一篇的内容了。
11. 本篇你应该带走的几个判断
- Run Block Automation(只对 PS)和 Run Connection Automation(对所有 IP)解决的问题不同,不要混用
- FCLK_CLK0 是 PS 内部时钟,不需要
.xdc里的create_clock;外部 PL 时钟端口才需要 -
IRQ_F2P多路中断必须经过ConcatIP 汇聚,否则中断号分配会错乱,软件端 ISR 注册到错误 IP -
Processor System Reset的dcm_locked如果没用 Clock Wizard 则接VCC,不能悬空 - XDC 里 Pynq-Z2 按键 BTN0 = D19,开关 SW0 = M20;DDR/FIXED_IO 引脚不需要手动约束
- Validate Design 的
Critical Warning≠ 普通警告,每条都要看 - Out-of-context 综合比 Global 快,开发期用;需要极限时序时最后跑一次 Global
12. 下一篇预告
下一篇 《Zynq 实战 08|Vitis Baremetal 开发:从 .xsa 到第一个能跑的中断驱动程序》,我们会:
- 在 Vitis 里导入 .xsa,创建 Platform + Application Project
- 用 XIicPs / XGpioPs / XTmrCtr 驱动访问刚才设计的 AXI IP
- 用 Xil_ExceptionRegisterHandler 注册 Concat 接进来的三路中断
- 用 XSCT 命令行下载比特流 + 调试
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 4.3 节 Address Map;第 7 章 Interrupts(IRQ_F2P 中断 ID 表 7-4) |
| UG994 | Vivado Design Suite: Designing IP Subsystems using IP Integrator | Block Automation / Connection Automation 官方手册 |
| PG164 | Processor System Reset Product Guide | PSR IP 复位时序细节 |
| PG144 | AXI GPIO Product Guide | ip2intc_irpt 行为;双通道配置 |
| PG079 | AXI Timer Product Guide | interrupt 信号极性;compare 寄存器 |
| UG912 | Vivado Design Suite Properties Reference Manual | set_property / create_clock 约束语法 |
| UG904 | Vivado Design Suite User Guide: Implementation | out-of-context synthesis 说明;增量实现 |
| Pynq-Z2 Schematic v1.0 | TUL Embedded | 引脚约束来源(BTN0=D19, SW0=M20, LD0=R14 等) |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 7 篇。 如果你在某一步卡住了,最有效的调试方式是:打开 Tcl Console,把 Vivado 报的完整错误信息粘到搜索引擎里——通常 BD 编号(如
[BD 41-759])就能精确定位到对应 Xilinx 论坛帖子。