4. HLS 接口综合:ap_ctrl_hs、ap_memory、axis 接口

好,咱们今天聊点实在的。你写好了卷积加速器的 HLS 代码,仿真也过了,综合也过了。然后呢?怎么把它放到 Zynq 的 PS 里跑起来?

说白了,就是你的 IP 核得跟 ARM 核“对上话”。这个对话的桥梁,就是 HLS 的接口综合。我刚开始接触这块时,觉得不就是几个 pragma 嘛,随便写写就行。结果呢?PS 端一调用,要么数据传不过去,要么时序乱掉,折腾了好几天。

嗯,这里要注意。接口选错了,后面全白干。今天我就把 ap_ctrl_hs、ap_memory 和 axis 这三个最常用的接口给你讲透。

4.1 接口综合的本质:PS 怎么“看见”你的 IP

你想想看,PS 是 ARM 核,跑的是软件。你的 IP 是硬件逻辑,跑在 PL 里。它们之间怎么通信?

其实就三件事:

  • 控制:PS 告诉 IP “开始干活”,IP 告诉 PS “干完了”
  • 数据搬运:PS 把输入数据塞给 IP,IP 把结果吐回来
  • 状态查询:PS 时不时看看 IP 忙不忙、有没有出错

HLS 的接口综合,就是把你的 C 函数参数和函数行为,翻译成 PS 能读写的硬件寄存器。我习惯把每个接口想象成一个“信箱”,PS 往里面丢信,IP 从里面取信。

核心原则:接口类型决定了数据通路和控制逻辑的实现方式。选错了,性能可能差 10 倍。

4.2 ap_ctrl_hs:让 PS 能“按开关”

ap_ctrl_hs 是 HLS 默认生成的接口。它负责函数级别的控制。说白了,就是让 PS 能启动你的 IP,并且知道它什么时候结束。

我举个例子。你写了一个卷积函数:

void conv2d(
    float in[256],
    float out[128],
    float weights[64]
);

HLS 综合后,会自动生成几个控制寄存器:

寄存器偏移 名称 功能
0x00 AP_CTRL 启动(bit0)、完成(bit1)、空闲(bit2)
0x04 AP_START 写1启动,写0忽略
0x08 AP_DONE 只读,为1表示完成
0x10+ 参数寄存器 存放函数参数的地址或值

PS 端的调用流程是这样的:

  1. 把输入数据的地址写到参数寄存器
  2. 把输出缓冲区的地址写到参数寄存器
  3. 往 AP_CTRL 的 bit0 写 1,启动 IP
  4. 轮询 AP_DONE,直到为 1
  5. 从输出缓冲区读结果

我的经验:ap_ctrl_hs 适合“粗粒度”控制。如果你的卷积核很大,一次处理很多数据,用这个接口很稳。但如果你要频繁启动 IP(比如每来一个像素就启动一次),那轮询的开销会让你崩溃。

4.3 ap_memory:最直接的“共享邮箱”

ap_memory 接口,说白了就是把你的数组参数映射到一块共享内存上。PS 和 IP 都能读写这块内存。

我在项目中遇到过一个问题:用 ap_memory 接口时,PS 写数据到内存,IP 读数据。看起来很简单对吧?但坑在于——缓存一致性。

Zynq 的 PS 有 L1/L2 cache,PS 写数据时可能先写到 cache 里,还没刷回 DDR。IP 直接从 DDR 读,读到的全是旧数据。

怎么解决?

  • PS 端用 Xil_DCacheFlushRange() 刷 cache
  • 或者用 Xil_DCacheInvalidateRange() 让 cache 失效
  • 再或者,直接用 AXI_HP 端口 + AXI_ACP 端口,走一致性通路

避坑指南:我曾经在 ap_memory 接口上栽过跟头。PS 写数据后忘了刷 cache,IP 读到的全是 0。查了两天才发现是 cache 的问题。从那以后,我养成了“写后必刷,读前必失效”的习惯。

ap_memory 的优点是简单,适合大数据块传输。缺点是延迟高,因为每次访问都要走 AXI 总线到 DDR。

4.4 axis 接口:流式传输的“高速公路”

axis 接口,全称 AXI4-Stream。它跟 ap_memory 完全不同。ap_memory 是“共享邮箱”,axis 是“传送带”。

数据在 axis 接口上是一个一个“拍”(beat)地流过去。没有地址,没有握手?不,有握手。TVALID 和 TREADY 信号就是握手信号。

为什么卷积加速器喜欢用 axis?

  • 数据是流式的,不需要等全部数据到位再处理
  • 可以做到“乒乓操作”,输入和输出同时进行
  • 延迟低,适合实时处理

在 HLS 里声明 axis 接口很简单:

void conv2d_axis(
    hls::stream<ap_axis<32,1,1,1> >& in,
    hls::stream<ap_axis<32,1,1,1> >& out
);

这里 ap_axis<32,1,1,1> 表示:

  • 数据宽度 32 位
  • 有 TUSER 信号(1 bit)
  • 有 TLAST 信号(1 bit)
  • 有 TID 信号(1 bit)

PS 端怎么用 axis 接口?通常需要配合 DMA。PS 把数据写到 DMA,DMA 再通过 axis 流给 IP。IP 处理完,再通过 axis 流回 DMA,DMA 再通知 PS 取数据。

性能对比:同样是传输 1024 个 32 位数据,ap_memory 需要 1024 次 AXI 读 + 1024 次 AXI 写,每次都有地址开销。axis 只需要 1024 个 beat,没有地址开销,带宽利用率高得多。

4.5 三种接口的选型指南

我整理了一个表格,方便你对照选择:

场景 推荐接口 原因
一次性处理大块数据(如整张图片) ap_memory 简单,PS 直接读写内存
流式处理(如视频逐行扫描) axis 低延迟,高吞吐,适合流水线
需要频繁启动/停止 IP ap_ctrl_hs + axis 控制灵活,数据流式
数据量小且随机访问 ap_memory 地址随机访问方便
高性能卷积加速 axis + 多个通道 可以做到每个时钟周期出一个结果

4.6 实战:让 PS 调用你的卷积 IP

好,理论说完了。咱们看看实际代码怎么写。

假设你有一个卷积 IP,用 axis 接口:

// HLS 代码
void conv2d_top(
    hls::stream<ap_axis<32,1,1,1> >& input_stream,
    hls::stream<ap_axis<32,1,1,1> >& output_stream
) {
    #pragma HLS INTERFACE axis port=input_stream
    #pragma HLS INTERFACE axis port=output_stream
    #pragma HLS INTERFACE s_axilite port=return bundle=CTRL_BUS
    
    // ... 卷积实现 ...
}

PS 端的调用代码(C 语言):

#include "xconv2d_top_hw.h"
#include "xconv2d_top.h"

XConv2d_top ip;
XConv2d_top_Config *cfg;

// 初始化
cfg = XConv2d_top_LookupConfig(XPAR_XCONV2D_TOP_0_DEVICE_ID);
XConv2d_top_CfgInitialize(&ip, cfg);

// 启动 DMA 传输
// 假设你已经配置好了 AXI DMA
XDma dma;
// ... DMA 初始化代码 ...

// 把输入数据从 DDR 搬到 IP
XDma_StartTransfer(&dma, input_buffer, DMA_TO_DEVICE, size);

// 启动 IP
XConv2d_top_Start(&ip);

// 等待 IP 完成
while(!XConv2d_top_IsDone(&ip));

// 把结果从 IP 搬回 DDR
XDma_StartTransfer(&dma, output_buffer, DMA_FROM_DEVICE, size);

我的习惯:我会在 PS 端用中断代替轮询。IP 完成时触发中断,PS 在中断服务函数里处理结果。这样 CPU 不用空转,可以做其他事情。

4.7 接口综合的常见陷阱

最后,分享几个我踩过的坑:

  • ap_memory 的地址对齐:PS 传地址时,必须 4 字节对齐。不对齐的话,IP 读数据会出错。
  • axis 的 TLAST 信号:别忘了在最后一个数据拍上置 TLAST。否则 DMA 不知道数据结束了,会一直等。
  • ap_ctrl_hs 的启动脉冲:启动信号需要至少一个时钟周期的宽度。PS 写寄存器时,最好写两次(先写 1,再写 0),确保 IP 能捕捉到。
  • 接口带宽匹配:PS 的 AXI 总线频率可能跟 IP 的时钟频率不同。跨时钟域处理要做好,否则数据会丢。

嗯,接口综合这块内容不少,但核心就一句话:选对接口,事半功倍。ap_ctrl_hs 管控制,ap_memory 管数据,axis 管流式。三者配合好了,你的卷积加速器就能在 Zynq 上跑得飞起。

HLS 接口综合:PS 与 PL 的通信架构 Zynq PS (ARM) 软件控制 数据搬运 状态查询 中断处理 Zynq PL (FPGA) 卷积加速器 IP 流水线处理 并行计算 数据流处理 HLS 接口 ap_ctrl_hs ap_memory axis AXI-Lite / AXI-Full 三种接口的核心区别 🔹 ap_ctrl_hs:控制接口,负责启动/停止/状态查询 🔹 ap_memory:共享内存接口,PS 和 IP 直接读写 DDR 🔹 axis:流式接口,数据像流水一样连续传输 选型建议: • 大数据块 + 简单控制 → ap_memory • 流式处理 + 高性能 → axis • 频繁启动 + 灵活控制 → ap_ctrl_hs + axis 组合 • 小数据 + 随机访问 → ap_memory

可可精品资料库