eeSDR PS-PL 串口控制 LED Demo —— 从零到运行完整指南

面向读者:FPGA/Zynq 初学者,要求跟着操作就能跑通。 硬件:eeSDR 开发板 (Xilinx Zynq-7000 XC7Z020CLG400-2) 软件:Vivado 2026.1 + Vitis 2026.1(Linux) 难度:入门级 预计耗时:首次操作约 1~2 小时


目录

  1. 整体架构:这个 Demo 到底做了什么
  2. 预备知识:三个你必须先理解的概念
  3. Phase 1 — Vivado:搭建硬件平台
  4. Phase 2 — Vitis:编写裸机软件
  5. Phase 3 — 烧录运行
  6. 附录:常见问题排查

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)让你像搭积木一样:

  1. 从 IP 库里拖出需要的模块(Zynq PS、AXI GPIO、AXI Interconnect…)
  2. 用鼠标连线
  3. 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 时:

  1. AXI 总线把这个写请求广播出去
  2. AXI GPIO 的 AXI 接口检测到地址匹配
  3. 把数据 0x05 锁存到 GPIO_DATA 寄存器
  4. 硬件引脚电平立即变化

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.bit FPGA 比特流(PL 逻辑)
design_1.hwh 硬件描述:有哪些 IP、地址映射表、中断号
ps7_init.c/h PS 端初始化代码: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 AutomationRun 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: 串口终端看不到输出

检查:

  1. 波特率是否设为 115200
  2. 打开的是否是 /dev/ttyUSB1(而不是 ttyUSB0)
  3. Zynq PS 配置中 UART1 是否已使能(Peripheral I/O Pins 中确认)
  4. 程序是否真的在运行(PS LED 是否在闪烁?)

Q5: 串口输出乱码

波特率不匹配。确认程序中的 UART_BAUDRATE 和终端波特率一致。

Q6: LED 不亮

  1. 确认 XDC 引脚分配正确(A20/B19/B20/C20)
  2. 确认 XGpio_SetDataDirection 已正确设置方向(bit0~3 = 0 = 输出)
  3. 在 Vitis 中加一行 XGpio_DiscreteWrite(&led_gpio, 1, 0x0F) 直接测试

Q7: 板子连不上 JTAG

  1. 确认 SW_BOOT 全部向下拨(JTAG 模式)
  2. 确认是 JTAG+UART 那个 Type-C 口(不是另一个 USB2.0 口)
  3. lsusb 确认能看到 FT2232 设备
  4. 尝试重新插拔 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