从 HLS 到 Verilog 的转变解析1:以 AXI 接口为例
从 HLS 到 Verilog 的核心技术解析1:以 AXI 接口为例
这篇文档将作为你的学习指南,帮助你彻底理解 HLS 是如何将一个简单的 C++ 函数 add
转换为一个带有标准 AXI 接口的、可被处理器控制的硬件 IP 核。
第一步:理解“为什么”——为何不是简单的 input
/output
?
你可能会想,c=a+b
最直观的 Verilog 不应该是这样吗?
module add (input [31:0] a,input [31:0] b,output [31:0] c
);assign c = a + b;
endmodule
HLS 当然可以生成这样的代码(如果使用 ap_fifo
等接口类型)。但你选择的 #pragma HLS INTERFACE s_axilite
是在告诉 HLS 一个至关重要的信息:
“我设计的这个硬件模块,不是一个独立的芯片,而是一个需要被嵌入式处理器(如 ARM 核)控制的硬件加速器(IP Core)。”
在现代片上系统(SoC)中,处理器是主(Master),硬件加速器是从(Slave)。它们之间需要一套标准的“沟通语言”和“交通规则”,而 AXI (Advanced eXtensible Interface) 就是业界最通用的规则之一。s_axilite
是 AXI 的轻量级版本,用于控制和状态寄存器的读写。
核心思想:HLS 生成 AXI 接口,是为了让你的 C++ 算法无缝地接入到一个以处理器为核心的系统中。处理器可以像读写内存一样,通过总线来设置输入参数 a
和 b
,并读取结果 c
。
第二步:HLS 的“分而治之”策略——数据路径 vs 控制接口
现在看你的 Verilog 文件,HLS 做了一个非常漂亮的设计分离:
- 数据路径 (Data Path) - 核心算法的硬件化
- 位置:
add.v
模块中的assign c = (b + a);
- 作用: 这是对你 C++ 算法
c = a + b;
最直接、最纯粹的硬件翻译。它是一个简单的组合逻辑加法器。HLS 分析出这个运算不需要时钟周期(零延迟),所以就给出了最高效的实现。这是计算的核心。
- 位置:
- 控制/接口逻辑 (Control/Interface Logic) - 与外部世界沟通的桥梁
- 位置:
add_control_s_axi.v
模块。 - 作用: 这个模块不执行任何加法运算。它的唯一职责是处理复杂的 AXI4-Lite 总线协议。它像一个“翻译官”或“秘书”,把处理器通过 AXI 总线发来的“命令”(读/写请求)翻译成对内部寄存器的简单操作。这是控制的核心。
- 位置:
add.v
作为顶层模块,将这两部分粘合在一起。它例化了 add_control_s_axi
,并把加法器电路和这个“翻译官”连接起来。
第三步:深入 AXI 接口的核心——add_control_s_axi.v
精解
这是我们学习的重点。我们来解剖这个“翻译官”是如何工作的。
核心1:地址映射 - 从软件变量到硬件寄存器
处理器如何区分 a
、b
和 c
?答案是地址。HLS 已经为你做好了地址映射:
// 0x10 : Data signal of a
// 0x18 : Data signal of b
// 0x20 : Data signal of c
这意味着:
- 当处理器向
0x10
地址写入数据时,它意图设置a
的值。 - 当处理器向
0x18
地址写入数据时,它意图设置b
的值。 - 当处理器从
0x20
地址读取数据时,它想要获取c
的值。
这是软件思维到硬件思维转变的第一座桥梁:软件中的变量,变成了硬件中具有唯一地址的寄存器。
核心2:写操作流程 - 处理器如何“设置”输入
当处理器想要写入 a=5
时,AXI 总线上会发生一连串被称为“握手”的事件。我们来看 add_control_s_axi.v
中的写状态机(Write FSM):
-
等待地址 (WRIDLE 状态):
- 处理器将地址
0x10
放到地址总线AWADDR
上,并拉高AWVALID
(表示地址有效)。 - 我们的 IP 核在
WRIDLE
状态下,看到AWVALID
后,拉高AWREADY
作为回应(表示地址已接收)。 - 握手成功! IP 核在内部锁存住地址
0x10
(waddr <= AWADDR
),然后状态机进入WRDATA
。
- 处理器将地址
-
等待数据 (WRDATA 状态):
-
处理器将数据
5
放到数据总线WDATA
上,并拉高WVALID
(表示数据有效)。 -
我们的 IP 核在
WRDATA
状态下,看到WVALID
后,拉高WREADY
作为回应。 -
握手成功! 此时,关键的寄存器更新逻辑被触发:
if (w_hs && waddr == ADDR_A_DATA_0) // w_hs是WVALID和WREADY同时为高int_a[31:0] <= WDATA[31:0];
因为之前锁存的地址是
0x10
(ADDR_A_DATA_0
),所以总线上的数据5
被存入了内部寄存器int_a
。 -
状态机进入
WRRESP
。
-
-
发送响应 (WRRESP 状态):
- IP 核拉高
BVALID
,告诉处理器:“你的写操作我已经完成了。” - 处理器看到
BVALID
后,会拉高BREADY
来确认收到响应。 - 握手成功! 状态机返回
WRIDLE
,一次完整的写操作结束。
- IP 核拉高
写入 b
的流程完全一样,只是地址换成了 0x18
。
核心3:读操作流程 - 处理器如何“获取”结果
-
等待地址 (RDIDLE 状态):
-
处理器将地址
0x20
放到ARADDR
上,并拉高ARVALID
。 -
我们的 IP 在
RDIDLE
下看到后,拉高ARREADY
。 -
握手成功! IP 核在内部锁存读地址
0x20
,并进入RDDATA
状态。同时,根据锁存的地址,准备要发送的数据:case (raddr)ADDR_C_DATA_0: beginrdata <= int_c[31:0]; // 准备发送int_c的值end endcase
-
-
发送数据 (RDDATA 状态):
- IP 核将准备好的数据
rdata
(现在是int_c
的值)放到RDATA
总线上,并拉高RVALID
,告诉处理器:“你要的数据准备好了!” - 处理器看到
RVALID
后,从RDATA
总线上取走数据,并拉高RREADY
作为回应。 - 握手成功! 状态机返回
RDIDLE
,一次完整的读操作结束。
- IP 核将准备好的数据
第四步:连接一切 - 两个模块如何协同工作
现在,我们把两部分串联起来,看看 5+10
的完整数据流:
-
输入阶段:
处理器
->AXI总线
->control_s_axi_U
经过写操作,将5
存入int_a
,将10
存入int_b
。
-
计算阶段(在
add.v
中发生):-
control_s_axi_U
模块的int_a
和int_b
通过输出端口a
和b
连接到add.v
顶层的wire a
和wire b
。// 在 add.v 中 wire [31:0] a; wire [31:0] b; // ... control_s_axi_U(.a(a), // control_s_axi_U的输出a连接到顶层的wire a.b(b) // control_s_axi_U的输出b连接到顶层的wire b );
-
wire a
和wire b
作为输入,送入加法器电路:// 在 add.v 中 assign c = (b + a); // 计算器立即得出结果 15
-
-
结果锁存阶段(数据从
add.v
回到add_control_s_axi.v
):-
计算结果
15
在wire c
上。这个wire c
被连接到control_s_axi_U
的输入端口c
。 -
在
add_control_s_axi.v
内部,每个时钟周期都会执行:always @(posedge ACLK) beginif (c_ap_vld) // c_ap_vld 在顶层被硬编码为1,所以总为真int_c <= c; // 将计算结果15锁存到内部寄存器int_c中 end
-
-
输出阶段:
处理器
->AXI总线
->control_s_axi_U
经过读操作,从int_c
中读取到结果15
并返回。
至此,一次完整的“软件设置 -> 硬件计算 -> 软件读取”流程就完成了。
总结与下一步
- HLS 的本质: HLS 是一个高级的代码生成器。它为你封装了标准化的、经过验证的接口逻辑(如 AXI FSM),让你能专注于核心算法的 C/C++ 实现。
- 解耦:
add.v
和add_control_s_axi.v
的分离是典型的硬件设计思想——将数据处理与控制逻辑解耦,使得两部分都可以独立修改和验证。 - 你的机会: 现在你完全理解了这个流程。如果你发现 HLS 生成的 AXI 逻辑在某些方面(如延迟、吞吐率)不满足你的精细化需求,你现在拥有了足够的基础知识去:
- 修改
add_control_s_axi.v
: 例如,改变一些状态转换条件,或者添加流水线寄存器来优化时序。 - 完全手写你自己的 AXI 模块: 当你成为专家后,你可以完全抛开 HLS 的接口生成,手写一个为你的特定应用量身定制的、最高效的 AXI 接口,然后与 HLS 生成的数据路径(或者你手写的数据路径)进行集成。
- 修改
建议的下一步:
尝试一个带循环的 HLS 项目,比如向量加法。你会发现 HLS 会在 AXI 地址映射中增加一些控制位,比如 ap_start (地址 0x00) 和 ap_done (地址 0x00 的某个状态位)。去分析 HLS 是如何用这些控制位来启动、监控你的硬件的,这将是你精通 HLS 到 Verilog 设计的下一个关键里程碑。