Vitis工程的搭建
接上文,在准备好硬件.xsa文件后,打开Vitis,设置好嵌入式工程的存放路径。
单击 Create Platform Project创建基于逻辑硬件平台的工程。在后续select hardware platform时添加准备好的.xsa文件。
添加完成后可以在嵌入式工程页面看到对应的硬件平台。
在硬件平台下可以看到对应工程下AXI总线上的各个逻辑模块的地址信息,基地址和范围信息来自于Vivado设计时的Address Editor地址配置。
在这里同样可以看到硬件工程下的全部IP模块信息,XIlinx官方IP可以查询对应的寄存器偏移地址。
右键硬件平台可以进行Build Project。
该操作是利用 Vivado 导出的.xsa硬件描述文件(Xilinx Support Archive)来定义软件的运行环境,随后生成对应的平台描述文件.xpfm并配置底层的编译工具。
整个流程可以概括为:导入硬件描述 -> 生成平台抽象层 -> 配置底层编译环境,具体步骤如下:
加载 .xsa 文件:Vitis 会读取由 Vivado 生成的
.xsa文件,其中包含了 FPGA 比特流、处理器 IP 以及自定义 IP 的寄存器地址映射。生成 .xpfm 平台文件:构建过程中,Vitis 会根据
.xsa自动生成.xpfm格式的平台文件,其中定义了该硬件平台可用的外设、时钟和内存布局,供后续的应用工程引用。配置编译工具链与 BSP:Vitis 会针对平台中指定的处理器(如 MicroBlaze 或 ARM Cortex-A)调用底层的 GCC 编译器,并生成对应的板级支持包(BSP),如库文件头文件 (
xparameters.h) 以及必要的初始化文件(如 FSBL)。
在Build Project完成后,可以查看BSP文件。
BSP文件的设置
可以看到在BSP界面,Vitis提供了官方模块的一些嵌入式例程供开发参考。
在Modify BSP Setting下,可以选择不同的参考库文件。以下图为例,在没有勾选lwip依赖库的情况下,是无法进行PS侧的网络通信开发的。
切换到standalone子页,这里的stdin和stdout为比较关键的配置项,选择正确的、与物理连接相符的串口才能看到正常打印。下图中因为逻辑工程中只有一个usrt模块,所以不存在选择问题。如果是ZYNQ架构的开发板,同时板卡引出了PS-Uart及PL-Uart两组资源,那么在这里选则了PS的串口,外部接线就需要接到PS Uart的物理接口,反之同理。
在修改了BSP相关设置后Vitis会rebuild工程。
至此外部的依赖环境配置完成,可以正式开始嵌入式开发了。
Uart串口打印
新建hello world工程
通过菜单栏新建嵌入式工程。
以起步来说,最合适的就是Vitis提供的hello world例程。该例程在嵌入式代码层面非常简单,更注重的是打通了嵌入式代码在PS侧运行并调用PL侧逻辑资源,实现串口输出的流程。
工程的添加导入按照Vitis指引流程即可,选择hello world例程。导入完成后可以看到添加了相应的功能.c代码及依赖库文件。
串口线连接
在串口线已连接,板卡上电后,打开上位机的设备管理器,可以查看对应的串口号信息。如果插拔串口线,设备管理器的端口子栏下没有看到变化,那么建议查看一下是否有标感叹号的无法识别的设备,检查并安装相应的设备驱动。
Vitis界面,切换至Debug模式,打开串口Terminal,添加串口时选择设备管理器中看到的串口号。对应的波特率、位宽、校验码对照逻辑IP的配置参数进行设置。
成功连接后会有打印信息提示,下图是因为Vitis已经连上了对应串口,所以提示Already connected。这里需要注意,提示串口已连接并不能说明选择的串口就是正确的,后续如果看不到打印信息建议复查以下串口号是否正确。
程序运行
Debug Configuration下,选择需要运行的debug工程。
切换到第二页Target Setup,因为嵌入式程序运行时依赖逻辑bit流的,在这里可以选择本次调试需要烧录的比特流。
Reset entire system建议勾上,尤其是在单一硬件平台上存在多个可执行嵌入式工程时,如果不进行系统复位,在未作地址隔离的情况下,不同嵌入式工程运行时会访问相同存储空间的地址,导致报错。
Program FPGA也建议勾上,这样可以确保在每次调试时底层逻辑处于初始、可控状态。
串口输出打印Hello World。
Generate Linker Script*
右键嵌入式工程可以查看工程的内存地址配置
因为在工程中使能了I-Cache与D-Cache并挂载了外部DDR存储设备,这里可以看到两块独立的存储空间。当前嵌入式工程的指令及数据均缓存载Bram区域,Bram资源十分有限,当嵌入式工程上了体量之后就会空间不足。因此以hello world工程为例,将嵌入式代码的缓存空间迁移至DDR中。
首先可以提供Vivado Hardware Manager查看一下MIG的运行状态,这是Xilinx自带的对外部存储的一个自测工具,显示CAL PASS即状态正常。
将linker script下的存储地址均改为DDR后,重新生成Linker Script即可。
可以看到现在嵌入式工程的相关文件全部缓存在DDR空间下了。
这一过程对嵌入式工程的功能并无影响,只是演示一下开放了I-Cache及D-Cache后Vitis下对外部缓存空间的使用。
GPIO功能调试
在做了基础的串口打印信息之后,可以进一步了解一下PL与PS侧的交互。这里提供一个验证思路:外部硬件的按钮提供开关输入信号,输入信号(数字0-1电平)给到逻辑PL侧,通过AXI总线供嵌入式PS侧读取访问;而后嵌入式PS侧又可以通过AXI总线写逻辑侧PL侧的寄存器,再将逻辑侧的电平输出变化给到LED。这一整套流程即包含了PL与PS的交互过程。
PL侧工程准备
首先需要准备好可供PS调用的PL侧逻辑资源,在现有的SoC工程上添加GPIO IP。
在做设计时,对于功能明确的GPIO,可以在配置时就定义好输入输出,这样可以简化嵌入式部分的工作,当前为了尽量全面的展示嵌入式部分,先不定义接口数据方向。
GPIO Width根据开发板的资源可以动态调整,例如只有1组按钮-LED,那就对应Width配置为1。
Default Value定义逻辑初始化之后的默认值,配置为0则对应的引脚在load bit之后输出0电压,1则是满幅Bank电压;
Enable Interrupt 使能中断信号,便于在嵌入式工程演示中断。
配置完成后引出到外部引脚,中断信号挂到MicroBlaze,记得在Address Editor下给GPIO模块分配地址。
添加约束完成后,生成bit流,导出到.xsa文件。
PS侧工程准备
代码同步上传了绑定资源
| 文件 | 职责 |
|---|---|
axi_gpio_intr.h/.c | GPIO 中断服务的核心:初始化 GPIO、注册中断处理函数、中断服务例程(ISR) |
sys_xintr.h/.c | 中断控制器(INTC)的初始化、启动和异常注册封装 |
axi_gpio_test.c | 主程序入口,协调初始化并运行主循环,响应中断标志更新输出 |
1. 系统初始化流程
main() └── init_intr_sys() ├── XInit_Intr_System() // 初始化中断控制器(INTC) ├── Gpiopl_init() // 初始化 GPIO: │ ├── 方向设置:通道1为输入(按键),通道2为输出(LED) │ └── 输出初值 0 ├── Gpiopl_Setup_Intr_System() // 配置中断: │ ├── XIntc_Connect() // 连接 ISR 到中断 ID │ ├── XIntc_Enable() // 使能 INTC 中断源 │ ├── XGpio_InterruptEnable() // 使能 GPIO 通道1中断 │ └── XGpio_InterruptGlobalEnable() // 使能 GPIO 全局中断 └── XSetup_Intr_Exception() // 启动 INTC、注册异常入口、开启全局中断
关键点:
中断 ID
GPIO_INTR_ID定义为 1(对应硬件连接的中断号)GPIO 通道1 为 2-bit 输入(按键),通道2为 2-bit 输出(LED/控制信号)
中断触发方式由逻辑工程中AXI Interrupt Controller配置决定
2. 中断服务程序(ISR)逻辑
void GpioplIntrHandler(void *Callback) { XGpio *GpioPtr = (XGpio *)Callback; u32 IrqStatus; u32 raw_val; gpio_isr_count++;//中断计数自增(调试用) IrqStatus = XGpio_InterruptGetStatus(GpioPtr);//读取 GPIO 中断状态寄存器 if (IrqStatus & 0x01)//若通道1中断触发(Bit0=1) { raw_val = XGpio_DiscreteRead(GpioPtr, 1) & 0x03;//读取通道1原始输入值(2-bit) gpio_val = (~raw_val);//取反后存入 gpio_val(用于输出) gpio_intr_flag = 1;//设置 gpio_intr_flag = 1(通知主循环) } /* * 只清除 Channel 1 */ XGpio_InterruptClear(GpioPtr, 0x01); }ISR 只做最小必要工作(读状态、存数据、置标志),不执行耗时操作(如打印)
通过全局标志
gpio_intr_flag与主循环同步,确保数据一致性
3. 主循环业务逻辑
while(1) { if (gpio_intr_flag) { gpio_intr_flag = 0; XGpio_DiscreteWrite(&Gpio, 2, gpio_val); // 输出反相后的值 xil_printf(...); // 打印调试信息 } }响应模式:中断触发 → 置标志 → 主循环检测标志 → 执行输出和打印 → 清标志
输出值取反,实现按键按下(输入低电平)→ 输出高电平(LED亮)的典型映射(这里的具体亮灭关系取决于开发板硬件)
4. 关键全局变量说明
| 变量 | 作用 |
|---|---|
gpio_intr_flag | 通知主循环有新中断数据 |
gpio_val | 待输出的 2-bit 值(按键反相) |
gpio_isr_count | ISR 调用次数(调试用) |
程序运行
参考hello world部分,烧写bit流并运行嵌入式程序。按钮控制LED灯亮灭,并在串口侧可以看到打印信息。
关于printf与xil_printf*
在 Vitis + MicroBlaze 裸机开发中,printf()和xil_printf()看起来都是串口打印,但底层机制和资源开销差别很大。
| 项目 | printf() | xil_printf() |
|---|---|---|
| 来源 | C 标准库 / newlib | Xilinx/AMD BSP |
| 代码体积 | 较大 | 小 |
| RAM/栈开销 | 较大 | 较小 |
| 格式化能力 | 完整 | 精简 |
%d | ✅ | ✅ |
%u | ✅ | ✅ |
%x | ✅ | ✅ |
%f | 通常支持 | ❌/不建议 |
%s | ✅ | ✅ |
| 浮点格式化 | ✅ | 通常不支持 |
| 多线程/锁 | 可能涉及 | 简单 |
| 执行时间 | 长 | 短 |
| ISR 中使用 | 不推荐 | 也不推荐,但风险相对小 |
| MicroBlaze 裸机调试 | 不太推荐大量使用 | 更推荐 |
为什么printf()特别容易影响 MicroBlaze?printf("******");看起来只有一句代码,但 CPU 实际做的事情远不止一条 UART 写操作。
从printf() ── 参数解析 ── format string解析 ── %x处理 ── 整数转换 ── buffer处理 ── stdout处理 ├── _write() ── UART驱动 ── 字符一个一个发送;需要执行大量代码。
更关键的是:printf()可能需要大量栈空间,尤其是当linker script下:
.text → DDR .data → DDR .bss → DDR heap → DDR stack → DDR
于是printf() 下大量的 stack 操作就会导致频繁的DDR访问,这对于时序敏感的中断行为无疑是致命的。
总之就是debug的时候适量使用xil_printf(),至于printf()那就尽量不要用了。
在进行GPIO按钮的功能测试时,如果在gpio的入口信号添加ila进行debug,可以发现有时在按下一次按钮后,对应的GPIO输入电平会多次变化。这便引出了数字-模拟直接的转换概念,下一篇准备通过按钮的消抖,来进一步展开FPGA设计中时序的概念。
Fin