25、FPGA与ARM协同设计:Zynq架构简介、AXI总线通信、Linux驱动开发、FPGA加速示例

说实话,FPGA和ARM协同设计这块,是我这些年做项目时感触最深的部分。以前我们做嵌入式系统,要么纯ARM跑软件,要么纯FPGA做逻辑,各有各的局限。直到Zynq这种异构架构出现,才真正把两者的优势捏在了一起。

我个人习惯把这种协同设计叫做「软硬兼施」——ARM负责控制、调度、网络协议栈这些串行任务,FPGA负责图像处理、信号滤波、加密解密这些并行计算。你想想看,这不就是取长补短吗?

25.1 Zynq架构简介

Zynq不是简单的「FPGA里塞了个ARM」,它是一颗真正的异构SoC。内部包含两大部分:PS(Processing System) 和 PL(Programmable Logic)。

组成部分 内容 我的理解
PS(处理系统) 双核Cortex-A9、DDR控制器、外设接口 相当于一颗完整的ARM芯片
PL(可编程逻辑) FPGA逻辑单元、DSP切片、BRAM 就是咱们熟悉的FPGA部分
互联总线 AXI总线、EMIO、中断控制器 PS和PL之间的高速公路

我在项目中遇到过一个问题:有人把Zynq当成普通FPGA用,只用了PL部分,PS完全闲置。这其实很浪费。Zynq的精髓就在于PS和PL协同工作,ARM跑Linux系统,FPGA做硬件加速,两边通过AXI总线高速通信。

核心要点:Zynq的启动流程是先启动PS,PS初始化DDR和时钟后,再配置PL。所以PL的bit文件可以存在SD卡或QSPI Flash里,由ARM加载。

25.2 AXI总线通信

AXI总线是PS和PL之间的桥梁。说白了,就是ARM和FPGA互相读写数据的通道。Zynq里主要用到三种AXI接口:

  • AXI_HP(高性能端口):直接连到DDR控制器,带宽最高,适合大数据量传输。我一般用它做视频数据搬运。
  • AXI_GP(通用端口):带宽较低,适合配置寄存器、控制状态。比如ARM给FPGA发个启动信号。
  • AXI_ACP(加速一致性端口):可以保持ARM和FPGA的缓存一致性,这个比较高级,我用的不多。

嗯,这里要注意:AXI总线的时序协议比较复杂。如果你自己写AXI Slave逻辑,建议先用Vivado的IP Integrator搭个简单的测试,再用ILA抓波形看看握手信号对不对。

我的小技巧:刚开始学AXI通信时,别急着写复杂逻辑。先做一个简单的「ARM写数据到FPGA寄存器,FPGA读回来再返回」的loopback测试。这个通了,后面就好办了。

25.3 Linux驱动开发

ARM上跑Linux,FPGA作为外设,那就得写驱动。Zynq的Linux驱动开发,我总结下来就三种方式:

  1. UIO(Userspace I/O):最简单,把FPGA的寄存器映射到用户空间,应用程序直接读写。适合控制类场景。
  2. 字符设备驱动:标准Linux驱动模型,支持open/read/write/ioctl。适合数据流传输。
  3. DMA驱动:配合AXI DMA IP核,实现高速数据搬运。这个稍微复杂,但性能最好。

我曾经踩过一个坑:用UIO方式映射了FPGA的AXI_GP端口,结果发现读写速度只有几MB/s。后来查资料才知道,UIO没有缓存一致性处理,每次读写都要flush cache。换成DMA方式后,速度直接飙到几百MB/s。

避坑指南:写驱动时,一定要确认FPGA的地址映射是否正确。我见过有人把0x43C00000写成了0x43C00001,结果读写全乱套。用devmem命令先测一下物理地址,再写驱动。

25.4 FPGA加速示例

光说不练假把式。我拿一个实际项目举例:图像边缘检测的硬件加速。

纯ARM做Sobel边缘检测,处理一帧640x480的图像大概要30ms。用FPGA做流水线加速后,只需要2ms。为什么这么快?因为FPGA可以同时处理多个像素,而且没有CPU的指令取指、分支预测这些开销。

具体实现思路是这样的:

  • ARM通过AXI_HP把图像数据写入DDR
  • FPGA的Sobel加速IP通过AXI_HP读取DDR中的图像
  • FPGA内部做3x3卷积、梯度计算、阈值判断
  • 结果写回DDR的另一块区域
  • ARM读取结果,显示或保存
// 简单的Sobel加速IP接口定义(Verilog)
module sobel_accelerator (
    input  wire        clk,
    input  wire        rst_n,
    // AXI Slave接口(ARM配置寄存器)
    input  wire [31:0] s_axi_awaddr,
    input  wire        s_axi_awvalid,
    // AXI Master接口(读写DDR)
    output wire [31:0] m_axi_araddr,
    input  wire [31:0] m_axi_rdata,
    // 控制寄存器
    input  wire        start,
    output reg         done
);
    // 内部流水线处理...
endmodule

你看,ARM端只需要调用一个ioctl命令,告诉FPGA「开始处理」,然后等待中断信号。FPGA自己就把活干完了。这就是协同设计的精髓——ARM做调度,FPGA做苦力。

性能对比:同样的Sobel算法,ARM纯软件30ms,FPGA硬件加速2ms,加速比15倍。而且FPGA功耗只增加了不到0.5W。这就是为什么现在很多边缘计算设备都用Zynq的原因。

最后说一句:FPGA与ARM协同设计,不是简单的「把代码分成两份」。你得从系统层面考虑,哪些任务适合ARM跑,哪些适合FPGA跑。我个人的经验是:控制流、协议栈、文件系统给ARM;数据流、矩阵运算、信号处理给FPGA。这样分工,才能发挥Zynq的真正实力。