eeSDR PS-PL 串口控制 LED Demo —— 从零到运行完整指南
面向读者:FPGA/Zynq 初学者,要求跟着操作就能跑通。 硬件:eeSDR 开发板 (Xilinx Zynq-7000 XC7Z020CLG400-2) 软件:Vivado 2026.1 + Vitis 2026.1(Linux) 难度:入门级 预计耗时:首次操作约 1~2 小时
目录
- 整体架构:这个 Demo 到底做了什么
- 预备知识:三个你必须先理解的概念
- Phase 1 — Vivado:搭建硬件平台
- 3.1 创建 Vivado 工程
- 3.2 创建 Block Design(搭积木)
- 3.3 添加 Zynq PS IP(ARM 处理器)
- 3.4 配置 Zynq PS — 关键步骤
- 3.5 添加 AXI GPIO IP(PL 端 LED 控制器)
- 3.6 自动连线:Block Automation + Connection Automation
- 3.7 顶层封装:Create HDL Wrapper
- 3.8 引脚约束:XDC 文件
- 3.9 生成比特流:Synthesis → Implementation → Bitstream
- 3.10 导出硬件平台:Export Hardware (XSA)
- Phase 2 — Vitis:编写裸机软件
- 4.1 启动 Vitis
- 4.2 创建 Platform Component(平台)
- 4.3 创建 Application Component(应用)
- 4.4 主程序代码逐段详解
- 4.5 编译与调试
- Phase 3 — 烧录运行
- 附录:常见问题排查
1. 整体架构:这个 Demo 到底做了什么
一句话概括
在 PC 上通过串口终端敲字符,控制开发板上 4 个 FPGA(PL 端)LED 的亮灭。
数据流向
┌──────────┐ USB 线 ┌──────────────────────────────────┐
│ PC 串口 │ ←──────────→ │ eeSDR 开发板 │
│ 终端程序 │ │ │
│(picocom) │ │ ┌─────────────────────────────┐ │
└──────────┘ │ │ FT2232H (USB 转双串口桥) │ │
│ │ ┌──── JTAG(通道A) ────────┐│ │
│ │ └──── UART(通道B) ────────┘│ │
│ └─────────────────────────────┘ │
│ │ │
│ UART (MIO12~13) │
│ ↓ │
│ ┌──────────────────────────────┐│
│ │ PS 端 (ARM Cortex-A9 双核) ││
│ │ • 运行裸机 C 程序 ││
│ │ • 接收 UART 字符 ││
│ │ • 解析命令 ││
│ │ • 通过 AXI4-Lite 总线写寄存器 ││
│ └──────────┬───────────────────┘│
│ │ AXI4-Lite 总线 │
│ ↓ │
│ ┌──────────────────────────────┐│
│ │ PL 端 (FPGA 可编程逻辑) ││
│ │ ┌── AXI SmartConnect ──┐ ││
│ │ └── AXI GPIO IP 核 ────┘ ││
│ │ │ GPIO[3:0] ││
│ │ ↓ ││
│ │ LED0 LED1 LED2 LED3 ││
│ │ (A20) (B19) (B20) (C20) ││
│ └──────────────────────────────┘│
└──────────────────────────────────┘
硬件资源使用
| 资源 | 位置 | 引脚/编号 | 说明 |
|---|---|---|---|
| PL 时钟 | PS 内部 PLL 产生 | FCLK_CLK0 | 由 PS 提供给 PL 的 50MHz AXI 总线时钟 |
| LED0 | PL Bank 35 | A20 | 通过 AXI GPIO bit0 控制 |
| LED1 | PL Bank 35 | B19 | 通过 AXI GPIO bit1 控制 |
| LED2 | PL Bank 35 | B20 | 通过 AXI GPIO bit2 控制 |
| LED3 | PL Bank 35 | C20 | 通过 AXI GPIO bit3 控制 |
| UART1 TX | PS MIO12 | — | 串口发送(ARM → PC) |
| UART1 RX | PS MIO13 | — | 串口接收(PC → ARM) |
| DDR3 | PS 专用引脚 | — | 512MB,程序运行内存 |
2. 预备知识:三个你必须先理解的概念
概念 1:Zynq = PS + PL
Zynq-7000 芯片内部其实是一个二合一:
┌────────────────────────────────────────────┐
│ XC7Z020 (Zynq-7000) │
│ │
│ ┌──────────────────────┐ ┌────────────┐ │
│ │ PS (Processing │ │ PL │ │
│ │ System = 硬核ARM) │ │ (Programmable│
│ │ │ │ Logic │ │
│ │ • 2× Cortex-A9 CPU │ │ = 可编程逻辑)│ │
│ │ • DDR3 控制器 │ │ │ │
│ │ • UART/SPI/I2C/USB │←┼──→ AXI 总线 │ │
│ │ • 中断控制器 GIC │ │ │ │
│ │ • 这些是"硅固化"的 │ │ • 85K 逻辑单元│ │
│ │ 造出来就不能改了 │ │ • 可任意编程 │ │
│ └──────────────────────┘ └────────────┘ │
└────────────────────────────────────────────┘
- PS:相当于一颗独立的 ARM 单片机,不配置 PL 也能跑程序
- PL:相当于一颗独立的 FPGA 芯片,不配置 PS 也能做纯逻辑
- AXI 总线:PS 和 PL 之间的高速公路,让 ARM 能读写 FPGA 里的寄存器
概念 2:AXI4-Lite 总线 = ARM 和 FPGA 之间的"内存映射"桥梁
这是整个 Demo 最核心的原理。
ARM 处理器访问外设的方式是读写某个地址(Memory-Mapped I/O):
- 读
0xE0001000→ 就是读 UART 的接收寄存器 - 写
0x41200000→ 就是写 AXI GPIO 的数据寄存器
那 0x41200000 怎么对应到 PL 端的 LED 引脚呢?
ARM 执行: *(volatile uint32_t *)0x41200000 = 0x05;
│
▼
AXI4-Lite 总线(硬件自动路由)
│
▼
PL 端的 AXI GPIO IP 核
收到写请求,地址匹配 0x41200000
│
▼
把 0x05 写到 GPIO_DATA 寄存器
对应的 bit0=1, bit1=0, bit2=1, bit3=0
│
▼
LED0 亮、LED1 灭、LED2 亮、LED3 灭
💡 关键理解:ARM 写一个地址,PL 端的硬件就自动响应——这就是 AXI 总线做的事情。Vivado 的 Block Design 工具自动帮我们配置好了这个"地址 → 哪个 IP 核"的路由表。
概念 3:Block Design = 用 GUI 画电路图
传统 FPGA 开发是手写 Verilog 代码。但对于 Zynq 这种复杂 SoC,手写 PS ↔ PL 的连接非常繁琐。
Vivado 的 Block Design(IP Integrator)让你像搭积木一样:
- 从 IP 库里拖出需要的模块(Zynq PS、AXI GPIO、AXI Interconnect…)
- 用鼠标连线
- Vivado 自动生成所有底层的 Verilog 代码
3. Phase 1 — Vivado:搭建硬件平台
本阶段目标:在 Vivado 中画好"电路图"(Block Design),综合实现,生成
.bit比特流文件和.xsa硬件描述文件。
3.1 创建 Vivado 工程
操作路径:
启动 Vivado → Create Project → Next
↓
Project name: ps_pl_led_demo
Project location: /home/io/Desktop/LearningFPGA/eeSDR/1_LinkPSPL/
✅ Create project subdirectory
↓ Next
Project Type: RTL Project
✅ Do not specify sources at this time
↓ Next
Default Part: 选 Parts 标签 → 搜索 xc7z020clg400-2 → 选中
↓ Next → Finish
每一步的含义:
| 选择 | 含义 |
|---|---|
| RTL Project | 从 RTL(Register Transfer Level,寄存器传输级)源代码开始设计,而不是从网表或引脚规划开始。这是标准起点 |
| Do not specify sources | 先不添加源文件,后面通过 Block Design 统一管理 |
| xc7z020clg400-2 | 芯片型号:xc7z020(Zynq-7020),clg400(400脚BGA封装),-2(速度等级2) |
3.2 创建 Block Design(搭积木)
操作路径:
Flow Navigator (左侧) → IP Integrator → Create Block Design
↓
Design name: design_1 (保持默认)
↓ OK
📖 Block Design 是什么:一块空白的"画布"。你往上面放 IP 核模块,用线把它们连起来。最终 Vivado 把它自动翻译成 Verilog 代码。
3.3 添加 Zynq PS IP(ARM 处理器)
操作路径:
在 Block Design 画布中:
右键空白处 → Add IP...
搜索: zynq
双击: ZYNQ7 Processing System
📖 这个 IP 代表什么:芯片上 ARM 硬核处理器的"软件模型"。它包含了 Cortex-A9 CPU、DDR3 控制器、UART、SPI、I2C、GPIO 等所有 PS 端外设的配置接口。
3.4 配置 Zynq PS — 关键步骤
双击画布上的 processing_system7_0 模块,打开配置窗口,逐页配置:
Page 1: PS-PL Configuration(PS ↔ PL 通信通道)
PS-PL Configuration → AXI Non Secure Enablement:
✅ M AXI GP0 interface (勾选这个)
其他都不勾选
📖 为什么只勾 M AXI GP0:
- M = Master(主设备),ARM 作为主设备主动发起读写
- GP = General Purpose(通用端口),32 位数据宽度,适合控制低速外设
- 0 = 编号 0(Zynq-7000 有 4 个 GP 主端口)
- 后面 AXI GPIO 会作为从设备挂在 GP0 上
Page 2: Peripheral I/O Pins(外设引脚)
Peripheral I/O Pins → 在 Zynq 封装图上:
点击 UART 1 区域 → 下拉选 UART1
(确认 GPIO MIO 区域显示 GPIO 已使能)
📖 为什么开 UART1:eeSDR 板子上 PS 的 MIO12/13 连到了 FT2232 的通道 B,所以我们能通过 USB 线在 PC 上看到 ARM 的串口输出。
Page 3: DDR Configuration(内存参数)
DDR Configuration → Memory Part:
下拉选: MT41K256M16 RE-125
(这个型号是 eeSDR 板载的 DDR3 芯片)
📖 为什么这一步很重要:DDR3 内存的时序参数(CAS Latency、tRCD、tRP 等)必须和实际芯片匹配,否则 ARM 启动后会因为内存访问错误而崩溃。
Page 4: Clock Configuration(时钟)
Clock Configuration → 确认:
PS 输入时钟: 33.33 MHz (板载 PS 晶振频率)
其他保持默认
配置完成,点击 OK。
3.5 添加 AXI GPIO IP(PL 端 LED 控制器)
操作路径:
在 Block Design 画布中:
右键空白处 → Add IP...
搜索: axi gpio
双击: AXI GPIO
双击 axi_gpio_0 配置:
GPIO Width: 4 (我们要控制 4 个 LED)
GPIO 2: 不勾选(不需要第二组 GPIO)
点击 OK
📖 AXI GPIO IP 内部是什么:它是一个用 Verilog 写好的"AXI 从设备"模块,内部有:
- GPIO_DATA 寄存器(偏移 0x00):写什么值,对应引脚输出什么电平
- GPIO_TRI 寄存器(偏移 0x04):控制每个引脚是输入(1)还是输出(0)
- AXI 接口逻辑:解析 AXI 总线上来的读写请求
当 ARM 执行
*(0x41200000) = 0x05时:
- AXI 总线把这个写请求广播出去
- AXI GPIO 的 AXI 接口检测到地址匹配
- 把数据 0x05 锁存到 GPIO_DATA 寄存器
- 硬件引脚电平立即变化
3.6 自动连线:Block Automation + Connection Automation
这是 Vivado 最智能的部分——帮你自动完成繁琐的连线。
第一步:Block Automation
点击画布上方绿色提示条: "Run Block Automation"
→ 勾选 processing_system7_0
→ OK
Vivado 自动连接:DDR 接口、FCLK_CLK0(输出50MHz给PL)、复位信号。
第二步:Connection Automation
点击画布上方绿色提示条: "Run Connection Automation"
→ 勾选 axi_gpio_0 的所有项
→ OK
Vivado 自动完成:
| Vivado 自动做了什么 | 为什么需要它 |
|---|---|
插入 AXI SmartConnect(AXI 互联矩阵) |
PS 的 AXI 主端口和 GPIO 的 AXI 从端口不能直连,需要通过一个"交换机"来路由 |
插入 Processor System Reset(复位同步器) |
PL 时钟域的复位信号需要和 PS 的复位信号同步 |
| 连接所有 AXI 总线信号(地址、数据、控制) | 几十根线,手连既繁琐又易错 |
自动分配地址:AXI GPIO = 0x41200000 |
Vivado 根据地址空间规划自动分配,保证不冲突 |
此时画布上应该有:
processing_system7_0
│
│ M_AXI_GP0
↓
axi_smc (AXI SmartConnect)
│
│ M00_AXI
↓
axi_gpio_0
│
│ GPIO (4-bit)
↓
gpio_rtl_0_tri_io[3:0] ← 这是引出到顶层的 4 根 GPIO 线
验证设计:
点击工具栏 ✅ Validate Design → OK
3.7 顶层封装:Create HDL Wrapper
操作路径:
Sources 窗口 → 右键 design_1 → Create HDL Wrapper...
→ 选 "Let Vivado manage wrapper and auto-update"
→ OK
📖 为什么需要 Wrapper:
- Block Design(
.bd文件)本质是一个 XML 文件,描述 IP 之间的连接关系- Vivado 的综合器只能处理 Verilog/VHDL 代码
- HDL Wrapper 就是把
.bd翻译成一个顶层 Verilog 模块(design_1_wrapper.v)- 选 “auto-update” 意味着每次修改 Block Design 后 Wrapper 自动更新
3.8 引脚约束:XDC 文件
📖 为什么需要约束文件:到目前为止,我们只说了"有 4 根 GPIO 线叫 gpio_rtl_0_tri_io[3:0]",但没告诉 Vivado 这 4 根线对应芯片的哪 4 个物理引脚。XDC(Xilinx Design Constraints)文件就是 FPGA 的"引脚分配表 + 电气标准说明书"。
操作路径:
Flow Navigator → Add Sources → Add or create constraints → Next
→ + → Create File → 文件名: eeSDR_led, 类型: XDC → OK → Finish
XDC 文件内容(从 eeSDR 原理图获取引脚编号):
# LED[0] = A20 (板载标注 PL_LED3)
set_property PACKAGE_PIN A20 [get_ports {gpio_rtl_0_tri_io[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_rtl_0_tri_io[0]}]
# LED[1] = B19 (板载标注 PL_LED4)
set_property PACKAGE_PIN B19 [get_ports {gpio_rtl_0_tri_io[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_rtl_0_tri_io[1]}]
# LED[2] = B20 (板载标注 PL_LED5)
set_property PACKAGE_PIN B20 [get_ports {gpio_rtl_0_tri_io[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_rtl_0_tri_io[2]}]
# LED[3] = C20 (板载标注 PL_LED6)
set_property PACKAGE_PIN C20 [get_ports {gpio_rtl_0_tri_io[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_rtl_0_tri_io[3]}]
每一行的含义:
| 约束命令 | 含义 |
|---|---|
set_property PACKAGE_PIN A20 |
把信号分配到芯片封装的 A20 引脚(物理位置) |
set_property IOSTANDARD LVCMOS33 |
该引脚使用 3.3V LVCMOS 电平标准(因为 Bank 35 供电是 3.3V) |
[get_ports {gpio_rtl_0_tri_io[0]}] |
指定顶层模块中名为 gpio_rtl_0_tri_io[0] 的端口 |
⚠️ 端口名必须精确匹配:
gpio_rtl_0_tri_io是 AXI GPIO IP 自动生成的端口名。可以在design_1_wrapper.v中确认。如果写错,综合时会报 “port not found”。
3.9 生成比特流:Synthesis → Implementation → Bitstream
操作路径:
Flow Navigator → Program and Debug → Generate Bitstream
→ 弹出 "No implementation results available" → Yes
→ Launch runs → OK
⏱️ 这个过程需要 5~15 分钟。
这三步分别做什么:
Synthesis(综合)
输入: Verilog/VHDL 代码
输出: 门级网表 (Netlist)
比喻: 把 C 代码"编译"成汇编,即把 Verilog 翻译成与或非门电路
┌──────────┐
RTL →│ Synthesis│→ Netlist (网表)
└──────────┘
Implementation(实现)
输入: 门级网表 + 引脚约束
输出: 布局布线结果
比喻: 把逻辑门"摆放"到芯片的物理逻辑块上,并规划好连线路径
┌────────┐ ┌──────────┐
Netlist→│ Mapping│→│Place&Route│→ 物理设计
└────────┘ └──────────┘
Generate Bitstream(生成比特流)
输入: 布局布线结果
输出: .bit 文件(二进制)
比喻: 把物理设计"打包"成 FPGA 能直接读取的二进制配置码
3.10 导出硬件平台:Export Hardware (XSA)
操作路径:
Flow Navigator → Export → Export Hardware
Output: Fixed(固定平台 = 硬件已经确定,不再加新的 PL IP)
Output format: XSA
→ Next → Finish
📖 XSA 文件是什么:一个 zip 包,Vitis 读取它来了解"这个硬件平台长什么样"。里面包含:
文件 作用 design_1_wrapper.bitFPGA 比特流(PL 逻辑) design_1.hwh硬件描述:有哪些 IP、地址映射表、中断号 ps7_init.c/hPS 端初始化代码:DDR3 时序、MIO 配置、时钟树设置 xsa.json平台元数据 导出的 XSA 文件路径一般在工程根目录下:
ps_pl_led_demo/design_1_wrapper.xsa
4. Phase 2 — Vitis:编写裸机软件
本阶段目标:基于 XSA 硬件平台,编写裸机(无操作系统)C 程序,在 ARM 上运行,实现串口命令控制 LED。
4.1 启动 Vitis
操作路径:
Vivado 菜单栏 → Tools → Launch Vitis IDE
→ 工作空间保持默认 → Launch
📖 Vivado vs Vitis 分工:Vivado 管硬件设计(Verilog、综合、布局布线),Vitis 管软件设计(C/C++ 编译、链接、调试、烧录)。两者通过 XSA 文件交接。
4.2 创建 Platform Component(平台)
📖 Platform 是什么:相当于给 Vitis 说"我要在 这个 硬件上跑程序"。Platform 包含 BSP(板级支持包)、驱动库、链接脚本等。
操作路径:
Vitis Explorer → Create Platform Component
↓
Component Name: eeSDR_platform
↓ Next
Hardware Design: 浏览选择 design_1_wrapper.xsa
↓ Next
Software Domain:
Operating System: standalone (裸机:无操作系统)
Processor: ps7_cortexa9_0 (Zynq 的第一个 ARM 核)
Architecture: 32-bit
↓ Next → Finish
4.3 创建 Application Component(应用)
操作路径:
Vitis Explorer → Create Application Component
↓
Component Name: led_control_app
↓ Next
Platform: eeSDR_platform
↓ Next
Domain: standalone on ps7_cortexa9_0
↓ Next
Template: Empty Application
↓ Finish
Vitis 自动生成了工程目录结构:
led_control_app/
├── src/
│ └── main.c ← 我们写代码的文件
├── build/ ← 编译产物(.o, .elf, .bin)
│ └── led_control_app.elf
└── _ide/ ← IDE 内部文件
├── bitstream/ ← 比特流副本
└── psinit/ ← PS 初始化代码
4.4 主程序代码逐段详解
以下是完整的 main.c,我将逐段解释每个部分的作用。
4.4.1 头文件和常量定义
#include "xparameters.h" // Vivado 自动生成的硬件参数宏(基地址、设备ID等)
#include "xgpio.h" // AXI GPIO 驱动 API
#include "xuartps.h" // PS UART 驱动 API
#include "xil_printf.h" // Xilinx 轻量级 printf(比标准 printf 省内存)
/*
* 新版 BSP (2026.1) 不再为每个外设生成 DEVICE_ID 宏。
* 直接使用基地址 (UINTPTR) 来初始化驱动。
*
* 这些地址值是由 Vivado 在 Block Design 中自动分配的:
* 0x41200000 — AXI GPIO 的寄存器基地址(可在 Address Editor 中查看)
* 0xE0001000 — PS UART1 的寄存器基地址(Zynq TRM 固定值)
*/
#define AXI_GPIO_BASEADDR XPAR_XGPIO_0_BASEADDR // = 0x41200000
#define AXI_GPIO_CHANNEL 1 // 使用通道 1
#define AXI_GPIO_LED_MASK 0x0F // 低 4 位 = 4 个 LED
#define UART_BASEADDR XPAR_XUARTPS_0_BASEADDR // = 0xE0001000
#define UART_BAUDRATE 115200 // 波特率
📖
XPAR_XGPIO_0_BASEADDR从哪来:这个宏定义在xparameters.h中,是 Vivado 导出 XSA 时根据 Block Design 的地址分配自动生成的。我们不需要手动写死0x41200000,用宏更安全——如果改动了 Block Design 导致地址变化,重新导出 XSA 后宏会自动更新。
4.4.2 硬件初始化 — PL 端 AXI GPIO
static int init_hardware(void)
{
XGpio_Config *gpio_cfg;
/* ===== Part A: 初始化 PL 端 AXI GPIO ===== */
gpio_cfg = XGpio_LookupConfig(AXI_GPIO_BASEADDR); // ① 根据基地址查找配置
XGpio_CfgInitialize(&led_gpio, gpio_cfg, // ② 初始化驱动实例
gpio_cfg->BaseAddress);
XGpio_SetDataDirection(&led_gpio, AXI_GPIO_CHANNEL, // ③ 设置 GPIO 方向
~AXI_GPIO_LED_MASK);
// ~0x0F = 0xFFFFFFF0 → bit0~3 = 0 → 输出模式
// 每个 bit 对应一个引脚:0=输出, 1=输入
XGpio_DiscreteWrite(&led_gpio, AXI_GPIO_CHANNEL, 0x00); // ④ 初始值:全灭
led_state = 0x00;
逐行解释:
| 步骤 | 代码 | 它做了什么 |
|---|---|---|
| ① | XGpio_LookupConfig(BASEADDR) |
在系统配置表中查找基地址对应的 AXI GPIO 设备,返回其配置结构体(包含寄存器地址范围、中断信息等) |
| ② | XGpio_CfgInitialize(...) |
根据配置初始化 led_gpio 这个驱动实例——把基地址、中断信息等存到结构体里,后续操作都用这个实例 |
| ③ | XGpio_SetDataDirection(..., ~0x0F) |
写 GPIO_TRI 寄存器。~0x0F = 低 4 位为 0 = 低 4 个引脚设置为输出。TRI = Tri-state(三态)控制 |
| ④ | XGpio_DiscreteWrite(..., 0x00) |
写 GPIO_DATA 寄存器 = 0x00,4 个引脚全部输出低电平 |
📖 新版 vs 旧版 API 差异:
- 旧版(Vivado 2018.x 时代):
XGpio_Initialize(&led_gpio, XPAR_XGPIO_0_DEVICE_ID)— 传入设备 ID 编号- 新版(Vivado 2026.1):
XGpio_LookupConfig(BASEADDR) + CfgInitialize()— 传入基地址旧版 API 在 2026.1 中仍然存在但是头文件里不再生成
DEVICE_ID宏,所以无法使用。
4.4.3 硬件初始化 — PS 端 UART
/* ===== Part B: 初始化 PS 端 UART1 ===== */
XUartPs_Config *uart_cfg;
XUartPsFormat uart_fmt;
uart_cfg = XUartPs_LookupConfig(UART_BASEADDR); // ① 查找配置
XUartPs_CfgInitialize(&uart_ps, uart_cfg, // ② 初始化
uart_cfg->BaseAddress);
XUartPs_SetBaudRate(&uart_ps, UART_BAUDRATE); // ③ 波特率 115200
uart_fmt.BaudRate = UART_BAUDRATE; // ④ 数据格式:8N1
uart_fmt.DataBits = XUARTPS_FORMAT_8_BITS; // 8 数据位
uart_fmt.Parity = XUARTPS_FORMAT_NO_PARITY; // 无校验
uart_fmt.StopBits = XUARTPS_FORMAT_1_STOP_BIT; // 1 停止位
XUartPs_SetDataFormat(&uart_ps, &uart_fmt);
XUartPs_SetOperMode(&uart_ps, XUARTPS_OPER_MODE_NORMAL); // ⑤ 正常模式
逐行解释:
| 步骤 | 含义 |
|---|---|
| ① | 搜索基地址 0xE0001000 对应的 UART 设备配置 |
| ② | 初始化 uart_ps 驱动实例 |
| ③ | 设置波特率为 115200 bps。PC 端串口终端也必须设同样的波特率 |
| ④ | 设置"8N1"数据格式(8 位数据、无校验、1 位停止位),这是最常见的串口配置 |
| ⑤ | 设置为正常操作模式(非回环/回声模式) |
4.4.4 主循环:读 UART → 解析命令 → 控制 LED
int main(void)
{
init_hardware();
print_welcome(); // 打印欢迎信息和命令帮助
while (1) {
char cmd = read_char(); // 阻塞等待用户输入一个字符
process_command(cmd); // 解析并执行
}
}
/* 从 UART 读取一个字符(阻塞轮询) */
static char read_char(void)
{
u32 received;
while (XUartPs_Recv(&uart_ps, (u8 *)&received, 1) == 0);
// ↑ 返回 0 表示还没收到数据 → 死循环等待
return (char)(received & 0xFF);
}
📖 为什么用轮询而不是中断:轮询是最简单的实现方式——程序死等,直到有字符到达。缺点是等待期间 CPU 什么也不做,但对于这个 Demo 足够了。下一个进阶练习可以把这里改成中断方式。
/* 翻转单个 LED */
static void toggle_led(u8 led_num)
{
u8 on = !(led_state & (1 << led_num)); // 取反:亮的变灭,灭的变亮
set_led(led_num, on);
}
/* 设置单个 LED(不改变其他 LED 的状态) */
static void set_led(u8 led_num, u8 on)
{
if (on)
led_state |= (1 << led_num); // 置位:led_state bitN = 1
else
led_state &= ~(1 << led_num); // 清零:led_state bitN = 0
XGpio_DiscreteWrite(&led_gpio, AXI_GPIO_CHANNEL, led_state);
// ↑ ↑
// AXI GPIO 写到 GPIO_DATA
// 驱动实例 寄存器的新值
}
📖
XGpio_DiscreteWrite到底做了什么:这个函数内部就一行:Xil_Out32(InstancePtr->BaseAddress + XGPIO_DATA_OFFSET, Mask); // ↑ 0x41200000 ↑ 0x00 // 等价于: *(volatile uint32_t*)0x41200000 = Mask;这行列出了整个 Demo 最关键的动作:ARM 写一个物理地址,PL 端的硬件自动响应。
4.4.5 命令解析
static void process_command(char cmd)
{
switch (cmd) {
case '0': toggle_led(0); break; // 翻转 LED0
case '1': toggle_led(1); break; // 翻转 LED1
case '2': toggle_led(2); break; // 翻转 LED2
case '3': toggle_led(3); break; // 翻转 LED3
case 'a': all_leds_on(); break; // 全部点亮
case 's': all_leds_off(); break; // 全部熄灭
case '?': print_status(); break; // 显示当前状态
}
}
4.5 编译与调试
操作路径:
Vitis Explorer → 右键 led_control_app → Build Component
编译成功后,产物路径:led_control_app/build/led_control_app.elf
5. Phase 3 — 烧录运行
5.1 硬件准备
| 检查项 | 操作 |
|---|---|
| USB 连接 | JTAG+UART Type-C 口 插到 PC |
| 拨码开关 | SW_BOOT 全部向下拨(JTAG 启动模式) |
5.2 验证 USB 识别
在终端执行:
lsusb | grep -i ftdi
# 预期输出: ... ID 0403:6010 Future Technology Devices International, Ltd FT2232C/D/H ...
ls /dev/ttyUSB*
# 预期输出: /dev/ttyUSB1 (通道B = UART)
📖 FT2232H 的双通道:
- 通道 A(/dev/ttyUSB0):MPSSE 模式 → JTAG 调试
- 通道 B(/dev/ttyUSB1):UART 模式 → 串口通信
5.3 从 Vitis 下载运行
Vitis Explorer → 右键 led_control_app → Debug As → Launch Hardware
Vitis 自动执行:
① 通过 JTAG 下载 .bit 比特流到 FPGA(PL 端 AXI GPIO 逻辑就位)
② 通过 JTAG 初始化 PS(DDR3 时序、MIO 配置、时钟树)
③ 通过 JTAG 下载 .elf 到 DDR3 内存
④ 释放 ARM 核复位 → main() 开始执行
5.4 打开串口终端
另开一个终端窗口(不是在 Vitis 里):
picocom -b 115200 /dev/ttyUSB1
退出 picocom:按
Ctrl+A然后Ctrl+X
5.5 预期效果
========================================
eeSDR PS-PL LED Demo
Board: eeSDR (XC7Z020 + AD9361)
PL LEDs: 4 (Bank35, LVCMOS33)
========================================
Commands:
0/1/2/3 - Toggle LED 0/1/2/3
a - All LEDs ON
s - All LEDs OFF
? - Show status
> 0
LED0 -> ON (*)
> 1
LED1 -> ON (*)
> a
All LEDs ON (* * * *)
> s
All LEDs OFF (O O O O)
> ?
LED Status:
LED0 [O] LED1 [O] LED2 [O] LED3 [O]
(* = ON, O = OFF)
6. 附录:常见问题排查
Q1: Validate Design 报错
通常是因为某根必连的线没连。Run Block Automation 和 Run Connection Automation 应该已经连好了大部分线——确保两个 Automation 都执行过。
Q2: 综合 (Synthesis) 报 “port not found”
检查 XDC 文件中的端口名是否和 design_1_wrapper.v 中完全一致。特别注意:
gpio_rtl_0_tri_io[0]不是gpio_rtl_0_tri_io_0- 总线用
[]括起来:{gpio_rtl_0_tri_io[0]}
Q3: 编译报 XPAR_XGPIO_0_DEVICE_ID undeclared
这是 2026.1 版本 BSP 的正常现象——新版不再生成 DEVICE_ID 宏。改用 XPAR_XGPIO_0_BASEADDR + XGpio_LookupConfig() + XGpio_CfgInitialize() 三步。
Q4: 串口终端看不到输出
检查:
- 波特率是否设为 115200
- 打开的是否是
/dev/ttyUSB1(而不是 ttyUSB0) - Zynq PS 配置中 UART1 是否已使能(Peripheral I/O Pins 中确认)
- 程序是否真的在运行(PS LED 是否在闪烁?)
Q5: 串口输出乱码
波特率不匹配。确认程序中的 UART_BAUDRATE 和终端波特率一致。
Q6: LED 不亮
- 确认 XDC 引脚分配正确(A20/B19/B20/C20)
- 确认
XGpio_SetDataDirection已正确设置方向(bit0~3 = 0 = 输出) - 在 Vitis 中加一行
XGpio_DiscreteWrite(&led_gpio, 1, 0x0F)直接测试
Q7: 板子连不上 JTAG
- 确认 SW_BOOT 全部向下拨(JTAG 模式)
- 确认是 JTAG+UART 那个 Type-C 口(不是另一个 USB2.0 口)
lsusb确认能看到 FT2232 设备- 尝试重新插拔 USB 线
附录 B: 全部文件清单
| 文件 | 路径 | 作用 |
|---|---|---|
| Vivado 工程 | ps_pl_led_demo/ps_pl_led_demo.xpr |
Vivado 工程文件 |
| Block Design | ps_pl_led_demo.srcs/.../design_1.bd |
图形化硬件设计 |
| HDL Wrapper | ps_pl_led_demo.gen/.../design_1_wrapper.v |
自动生成的顶层 Verilog |
| XDC 约束 | ps_pl_led_demo.srcs/.../eeSDR_led.xdc |
引脚约束 |
| 比特流 | ps_pl_led_demo.runs/impl_1/design_1_wrapper.bit |
FPGA 配置文件 |
| XSA 平台 | ps_pl_led_demo/design_1_wrapper.xsa |
Vitis 硬件平台描述 |
| 主程序 | vitis_workspace/led_control_app/src/main.c |
ARM 裸机 C 程序 |
| ELF 可执行 | vitis_workspace/led_control_app/build/led_control_app.elf |
ARM 可执行文件 |
附录 C: AXI GPIO 内部寄存器详解
当 ARM 访问地址 0x41200000 时,实际访问的是 AXI GPIO IP 核的内部寄存器:
| 偏移 | 寄存器名 | 读/写 | 功能 |
|---|---|---|---|
0x00 |
GPIO_DATA |
R/W | 数据寄存器。写:设置输出引脚电平。读:读取当前引脚状态 |
0x04 |
GPIO_TRI |
R/W | 三态控制寄存器。bit=0 → 输出模式,bit=1 → 输入模式 |
0x08 |
GPIO2_DATA |
R/W | 通道 2 数据寄存器(我们没用到) |
0x0C |
GPIO2_TRI |
R/W | 通道 2 三态控制(我们没用到) |
0x11C |
GIER |
R/W | 全局中断使能寄存器 |
0x120 |
IP ISR |
R/TOW | 中断状态寄存器 |
0x128 |
IP IER |
R/W | 中断使能寄存器 |
文档版本:v1.0 最后更新:2026-07-22 适用软硬件:eeSDR (XC7Z020) + Vivado 2026.1 + Vitis 2026.1