
1. 项目概述当FPGA遇上硬核处理器在嵌入式系统开发领域一个持续了多年的趋势正在悄然改变设计者的思路软硬协同的边界越来越模糊。过去一个典型的嵌入式系统设计往往意味着在电路板上摆放一颗微控制器MCU和一颗现场可编程门阵列FPGA前者负责运行控制逻辑和上层应用后者则处理高速、并行的硬件任务。这种架构清晰但成本、功耗和板级复杂度都不低。如今随着像赛灵思ZYNQ系列这样集成了ARM硬核处理器系统PS和可编程逻辑PL的异构芯片的普及以及开源RISC-V等MCU IP核的成熟一种更灵活、更高效的设计范式正在成为可能。简单来说你可以用一颗芯片同时干好MCU和FPGA的活儿。这种融合带来的直接好处是显而易见的。一方面对于那些原本就需要MCU进行复杂控制同时又需要FPGA实现特定接口或加速功能的应用ZYNQ这类芯片提供了“一站式”解决方案简化了硬件设计降低了物料成本。另一方面对于纯FPGA设计者而言当遇到一些用Verilog描述起来异常繁琐的状态机或控制逻辑时完全可以考虑在PL部分实例化一个经过验证的、资源占用极小的开源MCU软核来代劳用C语言来编写控制逻辑开发效率和可维护性都能得到极大提升。今天我们就聚焦于第一种应用场景深入探讨如何在一颗ZYNQ芯片上让它的处理器系统PS与可编程逻辑PL协同工作。我们将通过一个最经典也最直观的案例——控制LED灯——来揭示其背后的完整流程。这个案例将分为两个部分第一部分我们将实现一个“裸机”程序即不依赖任何操作系统直接在PS上运行C代码来控制PL中实现的LED寄存器第二部分则会在此基础上移植一个轻量级的嵌入式Linux系统并通过编写内核驱动和用户空间应用程序来完成同样的控制。本文我们先从更基础、更底层的“裸机”模式开始彻底搞懂PS与PL是如何通过芯片内部的高速总线“对话”的。2. 核心思路解析为何选择AXI-Lite总线在ZYNQ的架构中PS双核ARM Cortex-A9和PL可编程逻辑并非孤立存在它们通过芯片内部丰富的高速互连总线紧密耦合。理解并正确选用这些总线是进行软硬件协同设计的第一步。ZYNQ主要提供了三类AXIAdvanced eXtensible Interface总线AXI4、AXI4-Lite和AXI4-Stream。AXI4这是完整功能的AXI协议支持突发传输、缓存、多线程等高级特性数据位宽通常为32位、64位或128位。它适用于PS与PL之间需要高速、大数据量传输的场景例如DMA控制器将PL产生的大量数据直接搬运到PS端DDR内存中。AXI4-Lite这是一个简化版的AXI协议去掉了突发传输等复杂功能每次传输只操作一个数据通常是32位。它接口简单占用逻辑资源少非常适合用于PS对PL中少量寄存器进行配置和状态读取的场景比如控制一个GPIO模块、配置一个UART的波特率等。AXI4-Stream这是一种无地址的、单向流式数据传输协议。数据像水流一样从源端持续流向目的端没有寻址概念。它非常适合视频流、音频流、高速AD/DA数据流等场景。对于我们“控制LED”这个简单任务PS需要做的仅仅是将一个8位的数值写入PL中的一个特定寄存器数据量极小且操作是简单的内存映射I/O。显然杀鸡无需用牛刀AXI4-Lite是最合适的选择。它轻量、高效能够完美匹配“配置寄存器”这一需求并且能最大限度地节省PL侧的逻辑资源。那么PS是如何通过AXI-Lite访问PL的呢其本质是内存映射I/O。在系统设计时我们会为PL中的自定义IP分配一个或多个物理地址。当PS端的ARM处理器执行一条存储指令如STR到这个地址时芯片内部的互连网络会将这次访问路由到对应的AXI-Lite总线上进而传递到我们的IP核中。从软件角度看操作一个外设寄存器就像在内存中写一个变量一样简单。接下来的核心工作就是在PL侧创建一个能够响应AXI-Lite总线事务、并驱动LED引脚的自定义IP核。3. 自定义IP核的创建与封装在Vivado设计套件中创建自定义IP核是一个标准化流程它帮助我们封装硬件逻辑使其能够像使用官方IP如乘法器、时钟管理器一样通过图形化界面Block Design进行拖拽和连接极大地提升了设计效率和复用性。3.1 创建AXI4-Lite外设IP首先打开或创建一个Vivado工程。然后通过菜单栏Tools - Create and Package New IP启动向导。选择创建类型在向导中选择“Create a new AXI4 peripheral”。这是我们创建与PS通信接口的标准方式。定义IP信息接下来需要为IP命名例如led_controller并指定版本号如1.0。同时需要选择一个存放该IP核工程文件的目录。这个目录之后可以被添加到Vivado的IP仓库中方便在其他工程中调用。配置AXI接口这是关键一步。在接口配置页面我们需要接口类型选择“AXI4-Lite”。从/主模式由于是PS来读写我们的寄存器所以我们的IP作为“Slave”从设备。数据位宽设置为32位。这是ARM处理器自然对齐的宽度也是最常用的配置。寄存器数量这里我们先设置为4。这意味着我们的IP内部将有4个32位的寄存器slv_reg0 ~ slv_reg3可供PS访问。虽然我们只需要1个寄存器8位来控制LED但多定义几个可以为未来功能扩展留有余地且资源开销极小。编辑IP完成上述设置后选择“Edit IP”。Vivado会自动生成一个包含完整AXI4-Lite接口框架的IP核工程并在新窗口中打开它。注意Vivado生成的IP核框架包含两个主要模块一个顶层模块如led_controller_v1_0和一个AXI-Lite接口逻辑模块如led_controller_v1_0_S00_AXI。顶层模块主要负责端口定义和对接口模块的例化。而所有与AXI总线协议相关的握手、地址译码、数据读写逻辑都已在S00_AXI模块中由Vivado自动生成并实现。我们的主要工作是在这个框架的基础上添加自己的业务逻辑——将某个寄存器的特定比特位连接到输出端口。3.2 理解并挂接用户逻辑在生成的S00_AXI模块中我们可以找到4个32位的寄存器信号slv_reg0,slv_reg1,slv_reg2,slv_reg3。这些寄存器已经完美地集成到了AXI-Lite的读写时序中。当PS向IP的基地址写入数据时数据会根据地址偏移自动存入对应的slv_reg当PS读取时对应slv_reg的值会被送上数据总线。我们的目标是将slv_reg0的低8位slv_reg0[7:0]输出以驱动8个LED。因此需要在顶层模块中添加输出端口并将它们连接到slv_reg0的相应位上。打开顶层Verilog文件如led_controller_v1_0.v进行如下修改添加输出端口在模块的端口声明中增加output wire [7:0] LED。连接内部信号在模块内部我们需要将LED端口连接到S00_AXI实例中slv_reg0信号的低8位。但是slv_reg0是S00_AXI模块的内部寄存器在顶层不可直接访问。因此标准的做法是在S00_AXI模块中增加一个输出端口将slv_reg0引出。打开led_controller_v1_0_S00_AXI.v文件。在模块端口声明中添加output wire [31:0] slv_reg0_out。在模块内部添加赋值语句assign slv_reg0_out slv_reg0;。传递信号回到顶层模块led_controller_v1_0.v在例化S00_AXI模块时将新增的slv_reg0_out端口连接到一个内部的wire型信号例如wire [31:0] reg0_data;。修改例化语句.slv_reg0_out(reg0_data)。驱动LED端口最后在顶层模块中将LED端口连接到reg0_data的低8位。assign LED reg0_data[7:0];至此一个最简单的自定义IP核就完成了。它的功能是PS通过AXI-Lite总线向该IP的寄存器0写入一个32位数这个数的低8位会实时反映在LED[7:0]这8个输出引脚上。3.3 封装IP并添加到仓库用户逻辑添加完成后需要重新封装IP以便在Block Design中使用。在Vivado的IP打包工程中点击左侧“Package IP”选项卡。检查并确认所有设置特别是“File Groups”、“Customization Parameters”、“Ports and Interfaces”等子项前的状态图标是否为绿色对勾或没有警告感叹号。如果有黄色感叹号通常需要点击“Merge changes from File Groups Wizard”等按钮来同步更新。确认无误后点击“Package IP”按钮。Vivado会完成封装并关闭当前IP工程。将封装好的IP添加到你的IP仓库。在主工程中点击“Settings”在“IP - Repository”设置里添加你刚才存放IP工程的目录路径。添加成功后在IP Catalog窗口中通常可以在“User Repository”下找到你刚创建的led_controllerIP。实操心得在修改IP核源代码后务必回到“Package IP”步骤进行重新封装。仅仅保存源文件是不够的Vivado的IP集成器IPI识别的是封装后的xci或xml描述文件。一个常见的坑是修改代码后直接在主工程中升级IP有时会发现更改并未生效这时就需要检查IP是否已正确重新封装并更新了仓库路径。4. 硬件系统工程搭建与配置有了自定义IP核我们就可以在Vivado中搭建完整的硬件系统了。这个过程主要是在Block Design中通过图形化连线完成的。4.1 创建Block Design与添加ZYNQ处理器在Vivado主界面选择“Create Block Design”。在Diagram空白处右键选择“Add IP”搜索并添加“ZYNQ7 Processing System”。添加后会出现一个ZYNQ处理器的图形块。点击块上方出现的“Run Block Automation”链接。对于ZedBoard这类Vivado内置支持的开发板这个自动化脚本会帮你完成最基础的PS配置包括DDR型号、时钟、固定IO如UART、SD卡等。这能节省大量手动配置时间。4.2 深度配置ZYNQ处理器PS双击ZYNQ处理器块打开重配置窗口。这里面的设置至关重要决定了PS端哪些资源可用。PS-PL Configuration这里配置PS与PL之间的互联接口。我们需要使能至少一个GP Master接口。GPGeneral Purpose接口是基于AXI-Lite的正是我们IP核所需要的。通常启用M_AXI_GP0接口即可。确保其数据位宽为32位。注意HPHigh Performance接口是基于AXI4的用于高速数据传输本例暂不需要。Peripheral I/O Pins这里配置的是PS端直接管理的MIOMultiplexed I/O。对于ZedBoard自动化脚本通常已配置好UART、SD卡、USB等。你需要确认用于调试的UART通常是MIO 14, 15已被启用否则后续无法看到串口打印信息。Clock Configuration这里配置PS产生的时钟。重点关注“PL Fabric Clocks”。我们需要为PL部分提供一个时钟。通常使能FCLK_CLK0并将其频率设置为100MHz或50MHz根据需求。这个时钟将会作为我们自定义IP核以及整个PL部分的驱动时钟。DDR Configuration确认DDR内存型号与开发板一致。ZedBoard通常使用MT41J256M16 RE-125。配置完成后点击“OK”。4.3 添加自定义IP并完成连接添加IP在Diagram中再次“Add IP”这次搜索你刚创建的led_controller并将其添加到设计中。连接时钟与复位将ZYNQ处理器输出的FCLK_CLK0连接到自定义IP的s00_axi_aclkAXI总线时钟和用户逻辑可能需要的主时钟如果IP顶层有时钟输入口。将ZYNQ处理器输出的FCLK_RESET0_N连接到自定义IP的s00_axi_aresetnAXI总线低有效复位。连接AXI总线将ZYNQ处理器的M_AXI_GP0接口展开将其M_AXI_GP0端口连接到自定义IP的S00_AXI端口。由于接口类型匹配AXI-LiteVivado会自动连线。引出LED端口选中自定义IP的LED输出端口右键选择“Make External”。这会在Block Design顶层生成一个对外的端口。地址分配点击Diagram上方的“Address Editor”选项卡。Vivado会自动为led_controller分配一个基地址例如0x43C0_0000。请务必记录下这个地址后续编写软件时需要用它来访问寄存器。你可以在这里修改地址但要注意不要与PS内部或其他外设地址空间冲突。4.4 生成顶层HDL与管脚约束生成输出产品在Sources面板中右键点击你的Block Design如design_1.bd选择“Generate Output Products”。Vivado会为这个图形化设计生成对应的Verilog/VHDL网表文件。创建顶层Wrapper再次右键点击Block Design选择“Create HDL Wrapper”。选择“Let Vivado manage wrapper and auto-update”。这会创建一个顶层的Verilog模块将整个Block Design实例化并引出所有外部端口如LED。添加管脚约束文件XDC根据你的开发板原理图找到8个LED对应的FPGA管脚号。创建一个新的XDC文件或打开已有的板级约束文件添加如下内容以ZedBoard为例管脚号需核实# 定义LED端口和电气标准 set_property PACKAGE_PIN T22 [get_ports {LED[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {LED[0]}] set_property PACKAGE_PIN T21 [get_ports {LED[1]}] set_property IOSTANDARD LVCMOS33 [get_ports {LED[1]}] # ... 类似地添加LED[2]到LED[7]的约束 set_property PACKAGE_PIN U14 [get_ports {LED[7]}] set_property IOSTANDARD LVCMOS33 [get_ports {LED[7]}]综合、实现与生成比特流完成约束后就可以运行“Generate Bitstream”了。这个过程包括综合、布局布线、生成最终的配置文件.bit文件。注意事项在第一次为开发板做硬件设计时务必仔细核对时钟、复位和关键I/O的约束。错误的时钟频率会导致时序不收敛功能异常错误的管脚约束可能会烧毁芯片或无法驱动外设。建议先使用官方提供的板级约束文件作为基础进行修改。5. 裸机Bare-Metal软件程序开发硬件比特流生成后我们需要在PS端的ARM处理器上运行程序来测试它。我们首先使用不依赖操作系统的“裸机”模式这类似于传统的单片机开发。5.1 导出硬件到Vitis SDK现代Vivado版本已用Vitis统一开发平台替代了旧的SDK。流程如下在Vivado中选择File - Export - Export Hardware。在对话框中务必勾选“Include bitstream”。这将生成一个包含硬件平台信息.xsa文件的文件。导出完成后选择Tools - Launch Vitis。Vitis会启动并让你选择一个工作空间Workspace目录。5.2 创建平台与应用程序工程创建硬件平台在Vitis的Explorer视图右键选择“Create Platform Project”。输入项目名然后点击“Next”。在“Hardware Specification”页面选择“从XSA文件创建”并指向刚才Vivado导出的.xsa文件。完成创建后该平台项目定义了你的硬件系统包括处理器类型、内存布局、外设地址等。创建应用工程右键选择“Create Application Project”。第一步选择刚才创建的硬件平台。第二步输入工程名如led_baremetal。第三步选择“Hello World”模板。这是一个最简单的起点包含了必要的板级支持包BSP和基础代码框架。第四步在“Domain”选择页面保持默认设置它关联了硬件平台和操作系统这里选择standalone即裸机。5.3 编写测试程序打开生成的helloworld.c文件将其替换为我们的LED测试代码。核心是使用Xilinx提供的底层库函数Xil_Out32来向特定地址写入数据。#include stdio.h #include platform.h #include xil_io.h // 包含内存映射I/O函数 #include sleep.h // 包含延时函数 // 假设我们在Vivado Address Editor中看到的LED控制器基地址是0x43C00000 #define LED_CTRL_BASE_ADDR 0x43C00000 int main() { int led_pattern 0x01; // 初始模式最低位LED亮 int delay_time 500; // 延时500毫秒 // 初始化平台可选但建议保留 init_platform(); print(Hello from Zynq Bare-Metal LED Test\n\r); while (1) { // 将模式写入LED控制器的寄存器0基地址偏移0 Xil_Out32(LED_CTRL_BASE_ADDR, led_pattern); // 打印当前模式到串口方便调试 xil_printf(Writing pattern: 0x%x to address 0x%x\n\r, led_pattern, LED_CTRL_BASE_ADDR); // 循环左移LED模式产生流水灯效果 if (led_pattern 0x80) { led_pattern 0x01; // 循环到最左边后回到最右边 } else { led_pattern 1; // 左移一位 } // 延时 usleep(delay_time * 1000); // usleep参数是微秒所以乘以1000 } // 清理平台此代码在无限循环中不会执行 cleanup_platform(); return 0; }代码解析Xil_Out32(addr, data)这是Xilinx BSP提供的一个宏/函数用于向物理地址addr写入一个32位的数据data。它本质上生成了一条ARM的存储指令。我们的IP核只使用了32位数据的低8位。因此当我们写入0x01、0x02、0x04...0x80时分别会点亮第0、1、2...7个LED。usleep()函数用于实现毫秒级延时使流水灯效果可见。串口打印信息有助于我们确认程序正在运行并且写入的地址和数据是正确的。5.4 配置运行与调试连接硬件用USB线连接开发板的JTAG/UART接口到电脑。在Vitis中配置好硬件服务器通常自动检测。配置运行目标在应用工程上右键选择“Run As - Run Configurations”。双击“Single Application Debug”创建一个新配置。在“Target Setup”选项卡下确保硬件平台和处理器通常为ps7_cortexa9_0正确。在“STDIO Connection”选项卡连接串口设置正确的波特率如115200这样就能在Vitis的串口终端看到打印信息。最关键的一步在“Bitstream”部分确保勾选了“Download bitstream”。这样在运行程序前Vitis会先将我们生成的.bit文件下载到FPGA中配置PL逻辑。运行点击“Run”。Vitis会依次执行下载比特流到FPGA - 复位系统 - 将编译好的ELF程序加载到DDR内存 - 启动ARM核运行程序。如果一切顺利你将看到开发板上的LED开始以流水灯形式闪烁同时在Vitis的串口终端中看到不断打印的写入地址和数据信息。这标志着你首次成功实现了ZYNQ PS与PL的协同工作6. 常见问题与深度排查指南第一次成功总是令人兴奋但实际开发中遇到问题才是常态。下面记录一些我在此过程中踩过的坑和排查思路。6.1 硬件设计阶段问题问题1比特流生成失败报告“I/O约束错误”或“时钟约束错误”。排查I/O错误仔细检查XDC约束文件。确保get_ports中的端口名与顶层Wrapper文件中的输出端口名完全一致包括大小写和位宽表示例如LED[0]vsled[0]。确认管脚号PACKAGE_PIN和I/O标准IOSTANDARD与开发板原理图匹配。时钟错误检查Block Design中时钟连接。确保ZYNQ的FCLK_CLK0正确连接到了自定义IP的s00_axi_aclk。在ZYNQ配置中确认FCLK_CLK0的频率已设置且合理如100MHz。技巧养成在添加约束后立即运行“Validate Design”和“Synthesize Design”的习惯可以在早期发现语法和基本连接错误。问题2Block Design中AXI接口无法自动连接或连接后出现严重警告。排查接口不匹配最常见的原因是主从设备接口协议不匹配。确保ZYNQ的M_AXI_GP0Master连接到了自定义IP的S00_AXISlave。一个Master可以连接多个Slave但Slave不能直接互连。时钟域不统一AXI总线要求所有信号在同一个时钟域下工作。确保M_AXI_GP0的ACLK和自定义IP的s00_axi_aclk连接的是同一个时钟源即FCLK_CLK0。复位信号同样确保复位信号ARESETN也正确连接且极性一致低有效。6.2 软件程序阶段问题问题3程序运行后LED没有任何反应但串口有打印信息。排查这说明PS端的程序在运行但写入操作可能未成功到达PL。地址错误这是头号嫌疑犯。再次确认LED_CTRL_BASE_ADDR宏定义的值必须与Vivado Address Editor中分配给led_controller的偏移地址一致。注意这个地址是相对于PS视图的地址。一个快速验证的方法是在Xil_Out32前后添加更多的xil_printf打印出地址和写入的值确保无误。比特流未下载或错误检查Vitis运行配置必须勾选“Download bitstream”。尝试先通过Vivado Hardware Manager手动下载一次比特流然后再运行程序看是否有效。硬件逻辑问题用在线逻辑分析仪ILA进行调试。在Vivado中将ILA IP核添加到设计中抓取自定义IP的s00_axi_awaddr,s00_axi_wdata,s00_axi_wvalid,s00_axi_wready等关键信号。重新生成比特流并下载在Vitis中运行程序同时在Vivado中观察ILA波形看是否有正确的写事务发生。这是定位硬件问题最直接的手段。问题4程序运行后LED全亮或呈现乱码而非预期的流水灯。排查数据位对应错误检查自定义IP中assign LED reg0_data[7:0]这行代码。确保是[7:0]而不是[0:7]。同时检查XDC约束中LED[0]到LED[7]是否按顺序对应到了开发板上物理LED的从左到右或从右到左的顺序。有时原理图顺序与编程习惯顺序相反。寄存器复位值检查S00_AXI模块中slv_reg0的复位值。Vivado生成的代码通常复位为0。如果LED是低电平点亮那么复位后LED全灭是正常的。如果你的板子是低电平点亮而写入0x01却导致某个灯熄灭那可能是电平逻辑理解反了。延时问题如果流水灯速度过快看起来就像全亮或乱闪。增加usleep的延时参数比如从500毫秒增加到1秒。问题5串口无任何输出。排查串口配置错误确认Vitis中Run Configuration的串口设置正确的COM端口和波特率ZYNQ裸机通常为115200。ZYNQ MIO配置错误回顾硬件设计中ZYNQ的配置确保用于UART的MIO引脚如ZedBoard是MIO14和MIO15已被启用且功能选择为UART。BSP配置在Vitis中打开应用工程下的platform.spr文件检查板级支持包的设置。确保stdin和stdout已正确关联到对应的UART设备通常是ps7_uart_1。6.3 进阶调试技巧使用ILA集成逻辑分析仪对于复杂的PL逻辑调试ILA是无价之宝。将其添加到设计中连接到你想观察的任何内部网络如总线信号、状态机、计数器等可以像示波器一样实时捕获信号波形直观看到软件读写是否触发、数据是否正确、时序有无问题。在SDK/Vitis中查看内存在调试模式下可以暂停程序使用Memory View工具直接查看0x43C00000地址处的值验证写入操作是否成功改变了内存映射区域的值虽然这个地址对应的是PL的寄存器但在PS的视角里它就是一段内存空间。分阶段验证不要试图一次性完成所有功能。可以先做一个最简单的测试在main函数开头只写一次固定的值如Xil_Out32(0x43C00000, 0x55)看对应的LED0,2,4,6是否点亮。这能排除循环和延时逻辑带来的干扰。从一颗芯片内部总线的协议细节到图形化硬件设计再到底层C语言编程最终让灯光如愿闪烁——这个过程完整地展示了ZYNQ软硬件协同设计的精髓。它打破了传统MCUFPGA的物理界限让硬件灵活性和软件可编程性在硅片层面深度融合。这次裸机实验的成功为我们铺平了道路。在下一部分的探讨中我们将把复杂度提升一个层级为这个系统移植一个嵌入式Linux并编写内核驱动来接管对LED的控制。届时你将看到如何在操作系统的管理下以更规范、更强大的方式与可编程逻辑交互从而解锁网络、文件系统、多进程等更丰富的应用场景。