6.3 软硬件协同设计


6.3 软硬件协同设计

本节摘要:软硬件协同设计是 SoC FPGA 开发的"分蛋糕"艺术——决定哪块功能跑软件、哪块跑硬件、两边怎么通信。本节给出功能划分的判据与常见通信模式(寄存器、共享内存、DMA),对比轮询与中断的代价,并用一个"软件控制 + 硬件加速"的最小系统案例走完从划分到联调的完整流程。

先回答:这块该谁干

SoC 设计的第一个问题永远不是"怎么写代码",而是"这块功能放哪边"。划分错了,后面全是返工。四条判据可以帮你快速决策:

  1. 灵活性与生态:要跑协议栈、文件系统、网络?→ 软件。这些逻辑复杂、迭代快,且软件生态现成。
  2. 性能与吞吐:要处理视频流、做卷积、跑 FFT?→ 硬件。数据通路并行化是 FPGA 的看家本领。
  3. 实时性与确定性:要求延迟确定、不受调度抖动影响(伺服控制、协议时序)?→ 硬件。
  4. 开发与验证成本:功能变化频率 vs 硬件迭代成本。变化快的放软件(改一行重编译),稳定核心放硬件(一次实现长期受益)。

划分的终极判据:把"变化的部分"和"稳定的部分"分开。变化快的上软件,稳定且性能敏感的进硬件,两边接口固定后,软件迭代不影响硬件,硬件升级不牵动软件——这才是协同设计的红利。

三种通信模式

划分完,两边怎么"对话"?有梯度的三档选择:

模式 原理 适用 代价
寄存器读写 软件读写 PL 里的控制寄存器 控制命令、状态查询 数据量小,仅适合低频
共享内存 软件与硬件共访 DDR 的同一块区域 中等数据量 需处理缓存一致性
DMA 传输 硬件直接搬大块数据,搬完通知 大批量数据流 配置稍复杂

寄存器最简单:软件往寄存器写命令、读状态,硬件轮询或靠中断。适合"下指令、看结果"。

共享内存是吞吐主力:软件把数据写进 DDR 某块区域,硬件直接读;硬件算完写回,软件再读结果。省去了逐笔搬运,但引出一个经典问题——缓存一致性:CPU 有缓存,它可能认为内存里的数据还是旧的(缓存没刷新),而硬件已经改了。处理方式:软件操作共享区前做缓存刷新/失效操作,或用设备专用内存映射。这是 Linux 下写 FPGA 驱动的必修课。

DMA 是"甩手掌柜":配置好源地址、目的地址、长度,硬件自己搬,搬完发中断。视频、网络这类持续大流量几乎都靠 DMA。

轮询还是中断

软件怎么知道硬件"干完了"?两条路:

  • 轮询:软件循环读状态寄存器,等它翻转。简单、延迟低(微秒级响应),但烧 CPU——忙等期间核被占满,高并发场景不可接受。
  • 中断:硬件做完拉中断,软件被唤醒来处理。省 CPU,但有切换开销(中断上下文、调度延迟,几微秒到几十微秒)。

选型原则:低频操作、延迟敏感、能接受占用 → 轮询;高频数据流、CPU 还要干别的 → 中断。大型系统里两者混用很常见:状态查询轮询,数据就绪中断。

最小系统实战:软件控制、硬件加速

把上述全部串起来,走一个最小系统:PS 通过寄存器命令触发 PL 里的累加器,加速一列数据求和,结果通过中断回报。这是一个麻雀虽小五脏俱全的骨架。

// 软件侧:驱动 PL 加速器的核心流程(概念代码,示意) #include <stdint.h> #define ACC_CTRL 0x40000000 // 控制寄存器基址(示意) #define ACC_STATUS 0x40000004 #define ACC_DATA 0x40001000 // 数据区地址(共享内存) #define ACC_GO 0x1 #define ACC_DONE 0x2 void accel_start(uint32_t *data, int n) { // 1. 把数据写进共享内存(此处为演示,实际数据早已备好) // 2. 写控制寄存器:长度 + 启动 *(volatile uint32_t*)(ACC_CTRL) = (uint32_t)n | ACC_GO; } // 中断服务函数(示意):硬件完成后被唤醒 void accel_irq_handler(void) { if (*(volatile uint32_t*)(ACC_STATUS) & ACC_DONE) { // 数据已就绪,从共享区读取结果 uint32_t sum = *(volatile uint32_t*)(ACC_STATUS + 0x8); // 通知上层业务处理... } }
// PL 侧:累加器从设备的核心逻辑(概念代码,示意) module accel_slave ( input clk, input s_axi_awvalid, s_axi_awready, // AXI 写地址握手 // ...其余 AXI 信号略 output reg irq ); reg [31:0] ctrl_reg; // 控制寄存器:bit0 启动 reg [31:0] sum_reg; reg [15:0] count; // 收到启动命令后,逐个读取共享区数据累加(示意简化) always @(posedge clk) begin if (start_pulse) begin count <= data_len; sum_reg <= 32'd0; end else if (count != 0) begin sum_reg <= sum_reg + mem_data[count-1]; // 读 DDR 共享区 count <= count - 1; end else if (done_pulse) begin irq <= 1'b1; // 完成后拉中断通知软件 end end endmodule

这个例子里,软件负责编排(准备数据、下发命令、响应中断),硬件负责重活(流水线式累加),通信走寄存器 + 共享内存,完成通知走中断——协同设计的四个要素(划分、通信、同步、通知)全部齐了。真实系统只是把这个骨架放大:加速器换成卷积、FFT、加密引擎,共享区换成分页缓冲,中断换成多级。

联调时的建议顺序:先寄存器路径通(软件能读写 PL 寄存器)→ 再共享内存通(数据能进出)→ 最后中断通(事件能通知)。每一步都独立验证,别一上来就端到端——协同系统端到端调试,错误来源太多,分层排错才可控。

💡 关键直觉:协同设计的本质是"各取所长":软件擅长分支与生态,硬件擅长流水与并行。接口(寄存器/内存/DMA/中断)是双方的合同,把合同定稳定,两边就能独立演进。

本节要点回顾

  • 划分四判据:灵活生态走软、吞吐性能走硬、确定实时走硬、变化快的走软。
  • 通信三模式:寄存器管控制、共享内存管中等数据、DMA 管大流量。
  • 缓存一致性是坑:共享内存的读写要处理缓存刷新,Linux 驱动必修课。
  • 轮询 vs 中断:低频延迟敏感用轮询,高频省 CPU 用中断,可混用。
  • 骨架四要素:划分、通信、同步、通知,一个协同系统就这四件事。
  • 联调分层:寄存器→内存→中断逐步打通,别端到端一把梭。

下一步进入第 7 章:系统搭完,怎么证明它可靠?验证、调试与可靠性的质量防线。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U