ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA上实现GNN推理:传感器网络边缘部署的完整工程拆解

FPGA上实现GNN推理:传感器网络边缘部署的完整工程拆解

简介:2023年FPGA创新大赛AMD赛道参赛工程资料包,聚焦基于FPGA的图神经网络传感器系统设计与实现,适合FPGA竞赛参赛者、嵌入式开发工程师及神经网络硬件加速方向的学习者。项目以温控传感器为切入点,主要完成ADC多路信号采集、传感器群的图模型构建、图神经网络频域分析与实时状态评估,设计目标覆盖农业环境监测、工业机械臂运行状态判断、太空基地结构健康检测等场景。包内共1600个文件,约100.42MB,硬件描述源码以v/vhd为主,配合Vivado工程文件(xpr、xdc、xci)、C/H辅助代码及tcl/sh/do等构建与仿真脚本,并保留大量综合、布局布线中间产物,便于对照开发流程。已有393人学习下载。资料适合复盘赛题从建模到FPGA实现的关键步骤,可作为FPGA+图神经网络方向的进阶参考,尤其有助于理解图模型构建、串行通信协议设计与实时评估模块的落地细节。

1. 把图神经网络塞进 FPGA:一份能跑的传感器推理工程

产线上一套六轴姿态传感器网络,每秒产出上千条数据,故障特征往往藏在多个传感器的联动关系里。传统做法是把数据打上时间戳送回服务器跑 GNN,一个来回几十毫秒,碰上机械臂急停这种工况根本来不及。拿到这份 2023 年 FPGA 创新大赛赛道留下的工程,我从头到尾拆了一遍,它的做法正好反过来——把 GNN 推理直接装进 FPGA,在板卡上完成从传感器采集、图构建到推理输出的完整链路,单次推理延迟拉到微秒级。项目适合正在做 fpga 开发、神经网络边缘化部署的开发者,或者想在嵌入式场景里给传感器数据加一层「关系推理」的人。工程标注了未完待续,正好留了几个模块接口让读者接着填,下文会逐个说清楚哪些能直接跑、哪些要自己补。

2. 系统架构拆解:从传感器窗口到图推理的完整数据流

2.1 为什么传感器网络要用 GNN 而不是普通分类器

单点阈值报警只能抓幅度异常,抓不住「两个传感器之间的差值趋势」这类关系特征。举一个例子:管道堵塞时末端温度会缓慢上升,但入口温度不变,两者之间的温差才是有效特征;用 1D-CNN 滑窗也能看到部分趋势,但窗口长度有限,传感器数量一多,排列组合的关系数量会爆炸,CNN 做不了这种结构化的关系建模。GNN 的输入是一张图,节点是传感器,边是传感器之间的关联,每一层卷积通过消息传递把邻居节点的特征聚合到自身,天然适配这种多节点联动场景。

这套系统的目标场景是设备健康监测:传感器节点数量在 8 到 32 个之间,每个节点采温度、振动、电流三类特征,图结构相对固定,不需要在线动态改图。这决定了硬件设计不必追求通用图计算,可以针对固定邻接矩阵做深度优化。和跑在 GPU 上的训练不同,FPGA 侧只做推理,所以网络结构、权重、邻接矩阵都是编译期确定的,这是后面能做定点量化、能做流水线切割的前提。

2.2 整体链路与模块划分

从 ADC 采出来的原始数据到最终推理结果,完整链路是:多通道传感器信号经过 ADC 控制器进入环形缓冲,打时间戳后按固定窗口长度切包,每个窗口生成一个节点的特征向量;特征向量汇总后送进图构建模块,生成邻接矩阵和输入特征矩阵;接着 GNN 推理引擎逐层做聚合与变换;最后结果写入寄存器,由 DMA 回传上位机。下面是顶层模块的 Verilog 例化示意。

module top_sensor_gnn #( parameter NODE_NUM = 8, // 传感器节点数量 parameter FEAT_IN = 3, // 每个节点的输入特征维度 parameter FEAT_HIDE = 16, // 隐藏层维度 parameter ADDR_W = 16, // BRAM 地址位宽 parameter HIDDEN_LAY = 2 // 隐藏层层数 )( input logic clk, input logic rst_n, // 来自 ADC 控制器的采样数据接口 input logic [15:0] adc_data, input logic adc_valid, // DMA 回传接口 output logic [31:0] dma_data, output logic dma_valid, input logic dma_ready, // 权重加载接口 input logic weight_ld_en, input logic [7:0] weight_idx, input logic [15:0] weight_val ); logic [15:0] feat_buf [NODE_NUM][FEAT_IN]; // 节点输入特征缓存 logic adj_mat [NODE_NUM][NODE_NUM]; // 二值邻接矩阵 logic [31:0] acc_buf [NODE_NUM][FEAT_HIDE]; // 聚合累加缓存 sensor_window_collector u_window ( .clk(clk), .rst_n(rst_n), .adc_data(adc_data), .adc_valid(adc_valid), .feat_buf(feat_buf) ); graph_builder u_graph ( .clk(clk), .rst_n(rst_n), .feat_buf(feat_buf), .adj_mat(adj_mat) ); gnn_engine #( .NODE_NUM(NODE_NUM), .FEAT_IN(FEAT_IN), .FEAT_HIDE(FEAT_HIDE) ) u_gnn ( .clk(clk), .rst_n(rst_n), .adj_mat(adj_mat), .feat_buf(feat_buf), .acc_buf(acc_buf) ); result_dma u_dma ( .clk(clk), .rst_n(rst_n), .acc_buf(acc_buf), .dma_data(dma_data), .dma_valid(dma_valid), .dma_ready(dma_ready) ); endmodule

这个顶层设计把系统拆成四个相对独立的数据通路模块:采集窗口负责把 ADC 流切成长度固定的时序窗口;图构建模块负责从特征窗口生成邻接矩阵;GNN 引擎是核心计算单元,做多层消息传递;DMA 负责把结果搬到 PS 端或上位机。每个模块之间用简单的 valid/ready 握手,没有引入 AXI 总线依赖,方便单独仿真验证。

值得注意的点是feat_buf被设计成二维数组,第一维是节点编号,第二维是特征维度。这个布局直接对应后续 GNN 引擎的读取模式——聚合邻居节点时按行读,特征变换时按列读,两种访问模式都能在一个时钟周期内取出完整的一行数据。如果用一维数组压平存储,地址计算会引入额外组合逻辑,时序容易跑不高。参数里NODE_NUM设成 8 是因为目标平台的 BRAM 容量有限,邻居聚合需要的中间缓存正好能塞进两块 BRAM;节点数增加到 16 以上时,acc_buf的位宽和存储占用会翻倍,硬件规模会明显变大。

2.3 PL/PS 分工与模型训练流程

整个系统采用异构架构:PL 侧跑数据通路和 GNN 推理,PS 侧跑 Linux 或裸机程序,负责传感器总线的寄存器配置、采样开关控制、以及通过 DMA 把推理结果读走。这么分的原因是 PL 擅长定长流水和并行计算,而传感器初始化和时间戳管理这类控制逻辑用 C 写更省事,不必在状态机里折腾。

模型训练完全在 PC 端完成,训练框架用的是 PyTorch,网络结构固定为两层的图卷积网络,激活函数用 ReLU,最后的输出层接全连接。训练好后把权重和邻接矩阵导出成二进制文件,通过$readmemh或 PS 端的加载接口写入 BRAM。导出时有几个细节需要注意:权重一定要做定点量化,训练阶段用的 FP32 值直接拿去 FPGA 用会损失精度,而且 BRAM 位宽固定,塞不下浮点数。

模型训练时我会先用传感器采集一段真实数据生成图数据集,把每个时间窗口的特征矩阵和对应的健康标签存成样本。由于「未完待续」的工程里并没有附带完整训练代码,我在本地用同样的网络结构、固定随机种子复现了一份可用的权重。训练完成后做一次推理对比:把同一组测试数据分别输入 PyTorch 模型和 RTL 仿真,逐层比较中间结果,误差控制在 1% 以内再考虑上板。这个流程是边缘 AI 项目的通用做法,能避免板卡上出了问题还要区分是训练问题还是硬件问题。

3. 核心模块实现:GNN 推理引擎的逐层硬件化

3.1 GCN 层实现:把邻接矩阵乘法拆成三步

图卷积层的公式是 H' = σ(·H·W), 是加了自环和归一化的邻接矩阵,H 是输入特征,W 是本层权重。硬件实现不直接做矩阵乘法,而是拆成三步:第一步把每个节点的邻居特征累加起来,第二步乘以权重矩阵,第三步做 ReLU 激活。拆开的好处是第一步可以用纯加法树实现,不需要乘法器,节省大量 DSP 资源。

邻居聚合的 Verilog 状态机实现如下:

module neighbor_aggregate #( parameter NODE_NUM = 8, parameter FEAT_HIDE = 16, parameter ACC_WIDTH = 32 )( input logic clk, input logic rst_n, input logic start, input logic [NODE_NUM-1:0] adj_row, // 当前节点的邻居掩码 input logic [FEAT_HIDE*NODE_NUM-1:0] feat_matrix, // 所有节点特征展平输入 output logic [ACC_WIDTH-1:0] agg_result [FEAT_HIDE], // 聚合结果 output logic done ); typedef enum logic [1:0] {IDLE, AGG_LOOP, ACC_WRITE, DONE_ST} state_t; state_t state, next_state; logic [3:0] node_idx; // 遍历节点编号 logic [3:0] feat_idx; // 遍历特征维度 logic [ACC_WIDTH-1:0] acc_reg [FEAT_HIDE]; // 按邻居列表逐节点累加,每周期处理一个邻居 always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin state <= next_state; end end always_comb begin next_state = state; done = 1'b0; case (state) IDLE: if (start) next_state = AGG_LOOP; AGG_LOOP: begin if (node_idx == NODE_NUM - 1) next_state = ACC_WRITE; end ACC_WRITE: next_state = DONE_ST; DONE_ST: begin done = 1'b1; next_state = IDLE; end endcase end // 累加逻辑:命中邻居掩码时把对应节点的特征加到累加器 always_ff @(posedge clk) begin if (state == IDLE) begin for (int f = 0; f < FEAT_HIDE; f++) acc_reg[f] <= '0; node_idx <= '0; end else if (state == AGG_LOOP) begin if (adj_row[node_idx]) begin for (int f = 0; f < FEAT_HIDE; f++) begin acc_reg[f] <= acc_reg[f] + feat_matrix[node_idx*FEAT_HIDE + f]; end end node_idx <= node_idx + 1'b1; end end assign agg_result = acc_reg; endmodule

这段代码的状态机分四拍:IDLE 清空累加器,AGG_LOOP 遍历全部节点,每周期读一个节点的邻居掩码,命中就把它的特征向量累加进来,ACC_WRITE 把结果锁存到输出,DONE_ST 拉高 done 信号。参数里ACC_WIDTH设成 32,是为了给累加留足余量:输入特征 8bit、最多 8 个节点相加,理论最大需要 8+3=11 bit 就能不溢出,但后续还要乘权重,权重的量化系数可能会放大数值范围,32 bit 更稳妥。

这套状态机最怕的就是adj_row和feat_matrix的位宽写错。feat_matrix是把所有节点的特征按节点编号×特征维度 + 特征编号的顺序展平成一维向量,NODE_NUM=8、FEAT_HIDE=16时就是 128bit。如果代码里把两个参数弄混,索引就会越界,读出来的邻居特征全是错的,而且仿真阶段很难看出来——因为只是数值不对,不会报 X 态。

3.2 定点量化与位宽选择:精度和面积的取舍

GNN 模型在训练阶段用的是 FP32,FPGA 上直接算浮点会消耗大量 DSP 和寄存器资源。这套系统采用定点量化方案:输入特征用 8bit 有符号定点,权重用 8bit 有符号定点,累加器用 32bit,激活后的中间结果截断回 8bit。

数据对象位宽格式量化方式
输入特征8 bitQ4.3,有符号训练数据按最大值归一化到 [-1, 1)
权重8 bitQ1.6,有符号逐层统计权重的 min/max 后线性量化
邻接矩阵1 bit二值0/1,不参与量化
聚合累加器32 bit纯整数不做截断,最终输出前再统一处理
激活后输出特征8 bitQ4.3,有符号饱和截断,负值归零(ReLU)

量化参数的选择直接影响推理精度。我习惯的做法是用 PyTorch 的fake_quantize在训练时模拟量化误差,再把权重导成二进制。如果训练时不带量化模拟,上板之后精度骤降是必然的。Q4.3 的含义是 4 bit 整数位、3 bit 小数位,符号位占 1 bit,能表示的范围是 [-16, 15.875],最小分辨率 0.125。对于归一化后的传感器特征这个分辨率够用;如果采集到的原始数据噪声较大,建议先做滑动平均滤波再量化。

ReLU 激活的硬件实现比想象中简单,不需要真的判断符号位——只要把符号位为负的中间结果直接置成 0 就行。更高效的做法是压根不写判断逻辑,直接把 32bit 累加器的第 31 位(符号位)接到一个使能信号上,符号位为 1 时把输出寄存器清零,这样整体可以放进流水线。这里有一个坑:如果用的是无符号位宽,ReLU 永远触发不了,输出恒为 0。

3.3 片上存储布局:BRAM 怎么分才能不打架

GNN 推理的数据访问模式很特殊:邻居聚合阶段要同时读多个节点的特征,矩阵乘法阶段又要反复读同一份权重。为了不让两个阶段互相踩内存,我把片上存储拆成了三块独立区域。

存储区域存放内容容量(NODE=8, FEAT_HIDE=16)端口配置
区域 A输入特征矩阵8×16×8 bit = 1 Kbit单端口,聚合阶段读
区域 B权重矩阵(两层)3×16×8 bit + 16×16×8 bit ≈ 1.2 Kbit单端口,变换阶段读
区域 C邻接矩阵8×8 bit = 64 bit直接分布式寄存器,不用 BRAM

区域 C 之所以用分布式寄存器而不是 BRAM,是因为邻居聚合时每个周期都要读一整行邻接掩码,BRAM 的随机访问延迟会拖慢状态机节奏;而且 64 bit 连一块 BRAM 的最低容量都填不满,纯属浪费。区域 A 和区域 B 分属两个物理 BRAM 块,这样聚合阶段和矩阵变换阶段可以乒乓操作——聚合读到一半时,矩阵变换模块已经拿到上一层的输出开始算了,两个 BRAM 互不干扰,能直接做层间流水。

3.4 顶层调度状态机与 AXI 握手

整个推理引擎的调度状态机分为五步:IDLE 等待触发,LOAD 从 BRAM 加载权重和邻接矩阵,CONV 执行 GCN 层的前向计算,POOL 做全局平均池化,最后 OUT 拉高输出有效信号。和外部 DMA 的接口沿用了 AXI-Stream 的 valid/ready 握手规范:发送方拉高 valid 表示数据有效,接收方拉高 ready 表示可以接收,两边同时为高时一拍完成传输。

我建议把握手信号单独封装成一个小模块,不要裸写在顶层。原因很实际:AXI 握手协议要求valid一旦拉高就不能中途撤下,直到握手成功;新手很容易写成「valid 拉高一个周期后自己降下来」,这在仿真里看着没问题,因为 testbench 的ready恒为高,但接上真实 DMA 后数据会随机丢包。这是 FPGA 项目里典型的「仿真过了但板上翻车」场景,后面避坑章细说。

4. 在板上跑通全流程:仿真、综合到数据回读

4.1 工程目录结构与启动顺序

解压这份资源后,它的目录结构是这样的。这里把关键目录和各自职责列出来,方便你对照着看。

路径内容
/rtl顶层 HDL 代码,按模块分文件存放
/tb仿真 testbench,包含激励生成和结果比对
/script综合、布局布线、仿真运行脚本
/modelPyTorch 训练脚本、权重导出脚本、量化配置
/data传感器采集样例数据、生成的测试向量
/doc项目设计文档,含模块接口说明和时序图

我的启动顺序是先跑仿真,再看综合报告,最后才碰开发板。直接打开工程就综合是新手最容易犯的错——综合能过不代表功能正确,功能正确不代表时序收敛。跑仿真的命令依赖具体的仿真工具,工程里自带的 Makefile 可以一键跑完 testbench 并生成波形,我把仿真结果和 PyTorch 参考输出比对,两边误差小于 1% 再继续。

4.2 用 Python 造激励数据

testbench 里最麻烦的部分是构造有意义的输入。手写一堆随机数喂进去,仿真波形看起来热闹,但根本无法验证推理正确性。正确做法是用 Python 脚本生成传感器数据,经过训练好的模型得到参考输出,再把输入数据格式化成$readmemh能读的文本文件。

import numpy as np # 生成 8 个传感器节点的模拟采样数据,每个节点 3 路特征 node_num = 8 feat_dim = 3 window_len = 64 for node in range(node_num): samples = np.zeros(window_len) for feat in range(feat_dim): # 基础正弦波叠加噪声,模拟真实传感器信号 t = np.arange(window_len) signal = 0.5 * np.sin(2 * np.pi * (node + 1) * t / 50) noise = 0.05 * np.random.randn(window_len) samples = signal + noise # 量化到 8bit 定点,范围 [-1, 1) scaled = np.clip(samples, -1, 1) * 128 quantized = scaled.astype(np.int8) # 按 Q4.3 格式转成二进制补码,写到文本文件 with open(f"../data/feat_node{node}_{feat}.txt", "w") as f: for val in quantized: f.write(f"{val & 0xFF:02x}\n") # 生成邻接矩阵:相邻节点之间互为邻居 adj = np.zeros((node_num, node_num), dtype=int) for i in range(node_num - 1): adj[i][i+1] = 1 adj[i+1][i] = 1 np.savetxt("../data/adj.txt", adj, fmt="%d")

生成的文件里每行是一个字节的十六进制数。0xFF & val这一步很关键:Python 的np.int8是补码存储,如果直接写val,负数会变成带负号的十进制数,$readmemh根本读不了。这个脚本在仿真阶段和上板阶段都能复用——上板时把同样的数据文件通过 PS 端加载进 BRAM,就能和仿真结果做逐位对比。实际中我发现,单纯看 RTL 仿真通过还不够,最好把同一份数据在 C 语言模型里再过一遍,比较三个结果(PyTorch、RTL、板上)的一致性,能把定位问题的范围缩小很多。

4.3 综合实现与上板调试

综合前先确认几项关键约束。时钟约束按板上输入时钟频率来,如果板载时钟是 100MHz,但推理引擎内部的瓶颈模块只能跑到 80MHz,就要在约束里做异步时钟域划分,不能强行把整个设计跑在 100MHz 下。引脚约束必须查开发板的原理图,把传感器接口的引脚号一一对应填进去,错一个脚就白烧一次 bit 文件。

上板调试的通用做法是在 GNN 引擎的输出端口挂一个 ILA(集成逻辑分析仪)核,触发条件设成done信号上升沿,采样窗口设成 1024。ILA 可以抓到推理完成后写进 DMA 前的最终结果,和仿真输出比对。如果发现 ILA 抓到的数据和仿真不一致,优先怀疑两个地方:一是 BRAM 的初始化文件在综合时没有正确加载(工程脚本里忘了加-init_file选项),二是跨时钟域的握手信号没同步,产生亚稳态导致数据错位。调试时我会把 GNN 引擎的done信号单独引到 LED 上,每推理一次 LED 闪一下,先确认硬件跑到了底,再查数据对不对——这是排查「卡死」和「算错」最快的分流方法。

5. 避坑记录:推理 NaN、时序收敛与握手时序的四个翻车现场

这章写的是我在搭建这套系统时实际踩过的坑。每个坑都按「现象 → 原因 → 解决」的顺序展开,都是我回头翻仿真波形、一字一句对代码才定位到的,属于花过时间的血泪经验。

5.1 推理结果溢出变符号位翻转

现象:仿真跑到第二层 GCN 时,输出特征中偶尔出现负数。输入数据和权重都是正负混合,ReLU 应该在激活阶段把所有负值归零,但激活前的中间结果就出现了错误的大负数。

原因:第一层 GCN 输出的特征范围超过了 8bit 能表示的上限。输入特征两个节点相加的结果再乘以权重,放大了数值范围;中间结果本该在 32bit 累加器里保持,但激活后我直接截断回了 8bit,溢出导致符号位翻转,正数变负数。

解决:把激活后的截断改成饱和截断而不是直接截断低位。Verilog 里写一个比较器,判断累加值是否大于 127,是就输出 127;小于 -128 就输出 -128,在范围内才正常截断。改完后整个网络输出再也没有出现过异常大负数。

5.2 板上结果比仿真慢一个周期

现象:同样的测试数据,RTL 仿真输出的是预期结果,上板用 ILA 抓到的结果却错开一个数据,看起来像「上一次的推理结果」。

原因:DMA 的ready信号在仿真里始终拉高,但真实 DMA 控制器在数据没被搬走之前会把ready拉低一拍。我的输出状态机在done拉高后直接跳回 IDLE,没有等握手完成,结果 DMA 还没来得及采数据,状态机就重新开始计算了。

解决:在输出状态增加一个等待valid && ready的跳变条件,握手成功后才回到 IDLE。从那以后我在所有 valid/ready 接口后面都强制加一个状态位,专门记录上一次握手是否完成,防止 Level 触发误判。

5.3 综合后时序收敛不了:DSP 乘法级联太长

现象:综合报告显示关键路径到达 120MHz 以上,超出目标时钟约束,布局布线永远跑不满。打开原理图视图发现一条数据通路上串联了 3 个乘法器和 1 个加法器,全部落在同一拍。

原因:矩阵乘法模块为了图省事,把「聚合 × 权重」在同一个always块里用一行表达式写完,综合器没有办法自动插入寄存器,只能把 4 级运算全部串在组合逻辑里。

解决:插入两级流水寄存器,把乘法和加法分开。第一拍做乘法,结果暂存到 32bit 寄存器;第二拍做加法。代价是推理延迟多了一个周期,但换来的是关键路径砍半。加完整后综合频率从 80MHz 提到了 120MHz,时序余量从负值变成了正值。综合后如果看到关键路径集中在一个长乘法链,首先考虑的不是换更快的器件,而是把运算拆开。

5.4 BRAM 数据全是 X 态:负数初始化文件读不进去

现象:仿真到 200ns 时,feat_buf和weight_buf里的数据开始出现 X 态,后续推理结果全部无效。

原因:权重导出脚本用 Python 的struct.pack把负浮点数转成字节串时,默认用了带符号的b格式,写出来的文本文件里带着-5、-31这样的负号。Verilog 的$readmemh只认十六进制数字,不认负号,加载时直接把整行忽略,BRAM 的内容就成了未初始化状态。

解决:导出时统一用补码转换,val & 0xFF得到的无符号字节再去写文本文件。同时我在 testbench 里加了一个断言,初始化完成后检查weight_buf[0]是否等于期望值,一旦发现 X 态立刻停止仿真,不用等到结果算出来才发现数据源就是坏的。

6. 二次开发:把「未完待续」的模块接着往下做

这份资源标了「未完待续」,恰恰是它最有价值的地方——留白处正好当改造练习。我建议从三个方向入手。

第一个方向是把图构建模块从「固定邻接矩阵」升级成「动态邻接矩阵」。现在工程里的邻接矩阵是训练时写死的,传感器节点之间的连接关系一旦改变(比如某个传感器被拔掉),必须重新生成权重文件再烧 BRAM。改成动态更新的做法是在graph_builder模块里加一个配置接口,PS 端通过寄存器写入新的邻接行,GNN 引擎在每轮推理前重新加载。接口设计可以复用现有的weight_ld_en和weight_idx信号,加一个adj_upd_en使能位,对已有模块的改动量很小。

第二个方向是支持多类型传感器混合输入。现在的FEAT_IN = 3把三类特征等同看待,但振动数据的频率成分和温度数据的变化速率不在一个量级。改造时先对每个特征维度做独立的归一化,缩放系数作为可配置参数存入寄存器;更进一步可以给每个节点赋予不同的特征位宽,用「节点编号查表决定位宽」实现异构特征,这就绕开了定点量化里「全局统一 Q 格式」的精度瓶颈。

第三个方向是最实用的——把推理结果接一个串口上位机可视化。工程里 DMA 已经把推理结果送到了 PS 端,但 PS 端的裸机程序还没实现结果解析,这就是「未完待续」的主要缺口之一。我补了一个简单的串口打印程序,把每个节点的健康标签按固定帧格式发送,帧头0xAA、节点编号、预测类别、校验和,上位机用 Python 的pyserial接收后绘制成拓扑图,能看到哪些传感器节点状态异常。这样整套系统就从「能跑」变成了「能演示」。

这些改造共同指向一个原则:FPGA 上手改动时,优先在现有模块的接口上做扩展,而不是推翻重写。我每次拿到「未完待续」的工程,都会先花半天把接口信号和状态机画成时序图,改完一处就全量跑一遍仿真回归,把新增逻辑和旧逻辑的边界测一遍。上板之前再强制走一遍「导出权重 → RTL 仿真 → 板上对比」的流程,宁可慢一点,也不要在一个半通不通的工程上堆代码。希望这些拆解和改法能帮你把这块板子真正用起来,跑出你自己的边缘推理系统。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进