在FPGA上部署轻量BP神经网络-Python与Verilog:信号检测实战
我的学记|刘航宇的博客

在FPGA上部署轻量BP神经网络-Python与Verilog:信号检测实战

刘航宇
昨天发布 /正在检测是否收录...
电子信息类专业“一站式”考研-简历-就业宝藏分享 电子信息类专业“一站式”考研-简历-就业宝藏分享

储备知识:

我们在前文讲了BP神经网络算法和激活函数
BP神经网络算法
https://ee.ac.cn/index.php/archives/612.html
激活函数讲解
https://ee.ac.cn/index.php/archives/617.html

一、背景:为什么要在FPGA上跑神经网络?

在无源物联网(Battery-free IoT)和反向散射通信(Backscatter)场景中,传统的信号检测方法(如能量检测)虽然电路简单,但无法区分同频段的多种信号,且受噪声影响大;而基于PC或GPU的深度学习方案虽然精度高,但功耗动辄数十瓦,显然无法部署在依赖能量采集的微瓦级标签上。

核心矛盾在于:缺乏一种检测精度高、功耗极低(毫瓦级)、且能实时处理的解决方案。

答案是将轻量级的BP神经网络部署到低功耗FPGA上,在边缘端完成信号推理。


二、系统整体架构

整个检测系统的链路如下:

目标信号 → 天线 → 阻抗匹配 → LNA放大 → 包络检波(ENV) → ADC采样(1MSPS, 12bit) → FPGA(BP神经网络推理) → 调制控制信号

在FPGA内部,BP神经网络负责识别当前输入的512点采样数据是否为LoRa信号的前导码(Preamble)。一旦识别成功,输出控制信号,开启后端的Backscatter调制模块。

FPGA选型:Actel IGLOO/e系列 AGLE600V5-FG484。选择它的原因是:

  • Flash架构:上电即运行,无需外部配置芯片,适合无源系统;
  • 超低功耗:静态功耗仅0.046mW,适合能量采集场景;
  • 资源适中:13824个逻辑单元,足以容纳轻量级网络。

三、神经网络模型设计:极致轻量

为了在FPGA上高效部署,网络结构必须足够"轻"。本文设计的网络结构如下:
pasted_1785558315326_lc6zyp.png

层级神经元数说明
输入层512对应ADC采集的一条信号的512个采样点
隐藏层2仅2个神经元,大幅降低计算量
输出层1二分类:是目标信号(1) / 非目标信号(0)

激活函数:隐藏层和输出层均采用 ReLU

$$f(x) = \max(0, x)$$

选择ReLU的原因不仅是训练效果好,更重要的是硬件实现极其简单——只需一个比较器判断正负,无需计算指数或乘法,这对FPGA非常友好。

pasted_1785561816152_bsny25.png

训练环境:TensorFlow/Keras,使用Adam优化器和MSE损失函数。在真实环境数据集上训练后,测试集识别准确率可达 95.76%

pasted_1785561967835_yy5zgm.png


四、FPGA硬件实现详解

FPGA内部的神经网络是一个纯推理引擎。本节按数据流方向,逐个拆解每个模块的设计思路和Verilog实现要点。
pasted_1785562098406_gf64mu.png
整体RTL结构如下:

pasted_1785563631508_f74j4w.png

4.1 串并转换模块(Serial-to-Parallel

pasted_1785563649652_chqrj8.png

ADC(ADS7042)在片选信号CS拉低后,先输出2个前导0,再输出12位有效数据(D11→D0),每个数据位占用一个CLK周期。

设计要点

  • CS和CLK由FPGA内部产生,CS每16个CLK周期拉低一次;
  • 在CS下降沿开始,用计数器对串行数据移位寄存;
  • 凑齐12位有效数据后,输出一组并行数据 adc_data[11:0]
    波形图
    pmhbofe.png

    module serial_p(input data0_in,input cs,input clk,output[12:0] data0_out,input rst);
      reg[11:0] temp;
      reg[12:0] save;
      integer count;
      integer cnt;
    always @(negedge clk  or negedge rst)
              begin 
                    if(!rst) begin//reset
                       temp<=12'b000000000000;
                       count<=-1;
                       save<=12'b111111111111;
                       cnt<=-1;
                       end
                    else if(cs&&count%16==0)
                       begin 
                          save<={1'b0,temp};
                          count<=1;
                          cnt<=cnt+1;
                          if(cnt==512)
                              begin cnt<=1;
                              end
                          end
                    else if(cs) 
                       begin 
                          temp<=12'b0; 
                          count<=1; 
                          end 
                    else if(count<3) 
                          count<=count+1;
                    else    
                       begin 
                          temp<={temp,data0_in}; 
                          count<=count+1;
                          end
              end
    assign data0_out=save;
    endmodule

4.2 数据归一化模块

问题:训练时,输入数据被归一化到 [0, 1.8](对应ADC参考电压1.8V)。如果在FPGA中直接用除法做 Vin = adc_data / 4095 * 1.8,会消耗大量逻辑资源(FPGA做除法器代价极高)。

解决方案在软件端完成归一化,FPGA端只做整数运算

具体做法:

  1. 在Python/Keras训练时,将输入数据 x 归一化为 x_norm = x / 4095 * 1.8
  2. 在提取权值时,将第一层权值 w 除以 1.8 并放大 1024 倍取整;
  3. 这样FPGA输入的原始ADC值 adc_data(范围0~4095)直接与预处理后的权值相乘,等效于完成了归一化+加权。

    # 创建一个自定义的回调对象
    cb = MyCallback()
    # 以只读方式打开训练数据文件
    with open('/content/work/data/data.txt') as traindata:
     # 读取文件内容
     data = traindata.read()
    # 将文件内容转换为numpy数组,并将数据类型转换为float,并将数组形状调整为(47100, 2)
    data = np.array(list(data.split()), dtype=float).reshape((47100, 2))
    # 创建一个最大最小归一化对象
    min_max_scaler = preprocessing.MinMaxScaler()
    # 归一化
    data = data * (1/65535)
    # 以只读方式打开训练标签文件
    with open('/content/work/data/label.txt') as labda:
     # 读取文件内容
     labels = labda.read()
    # 将文件内容转换为numpy数组,并将数据类型转换为float,并将数组形状调整为(47100, 2)
    labels = np.array(list(labels.split()), dtype=float).reshape((47100, 2))
    # 对data和labels进行乱序,保持它们的对应关系
    # 为了保持对应关系,需要先将data和labels沿着第二个维度拼接起来,形成一个(100, 4)的数组
    # 然后对这个数组进行乱序,再将它们沿着第二个维度分开,还原成data和labels
    data_labels = np.concatenate((data, labels), axis=1)
    np.random.shuffle(data_labels)
    data = data_labels[:, :2]
    labels = data_labels[:, 2:]

    FPGA端实现
    实际上,归一化模块在FPGA中被合并到了权值预处理阶段,硬件上不需要额外的归一化电路。但为了模块化清晰,也可以显式设计一个"电压映射"模块,将12bit ADC码值转换为定点数格式:

工程建议:永远不要在FPGA里做除法。所有线性变换(归一化、缩放)都应在软件端合并到权值中,FPGA只做整数乘加。

4.3 隐藏层神经元模块(核心计算单元)

隐藏层的数学表达式:

$$y = \text{ReLU}\left( \sum_{i=1}^{512} x_i \cdot w_i + b \right)$$ , i = 1..512

工程难点

  • 512次乘累加如果在一个时钟周期完成,需要512个乘法器,资源爆炸;
  • 如果纯串行完成,需要512个时钟周期,一条chirp要512×512=262144个周期,太慢。

解决方案:采用 串行MAC流水线 + 双缓冲FIFO 架构。

设计思路:

  1. 权值存储:512个权值和1个偏置在编译时以 reg [31:0] w[0:511] 形式固化在FPGA中;
  2. FIFO缓存:ADC每输出一个采样点,立即写入FIFO。FIFO深度设为16即可(只要保证不溢出),因为读出速度(MAC运算)和写入速度(ADC采样)基本匹配;
  3. 串行MAC:每个时钟周期从FIFO读出一个数据 xi,与对应的 wi 相乘,累加到累加器 acc
  4. 512个周期后:累加器结果加上偏置 b,送入ReLU模块。

pasted_1785563798085_32317i.png

FIFO波形图
pmhqDBt.png
关键设计细节

  • 位宽设计:输入22bit × 权值32bit = 乘法结果54bit,累加512次需要额外9bit,因此累加器至少需要 63bit。如果位宽不够,累加会溢出,导致结果完全错误。
  • 时序约束:乘法器和加法器要满足16MHz时钟的建立保持时间。如果组合逻辑延迟太大,需要插入流水线寄存器(将乘法器和加法器分两级完成)。
  • 双神经元并行:隐藏层有2个神经元,需要例化两个上述模块,各自独立的权值存储和累加器,但共享同一个FIFO输入。

4.4 ReLU激活函数模块

ReLU的硬件实现是整个网络中最简单的部分:
pmhqWcj.png

module relu (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [31:0] x_in,       // MAC+bias结果
    input  wire        x_valid,
    output reg  [31:0] y_out,
    output reg         y_valid
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            y_out  <= 32'd0;
            y_valid <= 1'b0;
        end else begin
            y_valid <= x_valid;
            if ($signed(x_in) >= 0)
                y_out <= x_in;
            else
                y_out <= 32'd0;
        end
    end
endmodule

4.5 输出层神经元模块

输出层只有1个神经元,输入是隐藏层2个神经元的输出:

$$\text{output} = \text{ReLU}\left( h_1 \cdot w'_1 + h_2 \cdot w'_2 + b' \right)$$

这个模块与隐藏层类似,但逻辑资源消耗极小。
pmhq7NT.png

module nerual_out( clk, rst,x1,x2,data_out );
input clk, rst;
input signed[0:49] x1,x2;
output signed[79:0] data_out;

//<statements>
//reg signed[0:28] weight[0:1];
reg signed[0:7] save,save1,save2;
localparam bais=-50'd214979034352654;
//localparam bais = -26'd6406874;
localparam weight0 = 29'd22506522;
localparam weight1 =-29'd341584992;
//integer bais;
integer count;
always @(negedge clk  or negedge rst) 
       begin
              if(!rst) //reset,set values and bias
               begin
//                bais<=-50'd214979034352654;
//                weight[0]<=29'd22506522;
//                weight[1]<=-29'd341584992;
                save<=0;
                save1<=0;
                save2<=0;
                count<=0;
                end 
              else
                begin
                 save1<=x1*weight0;
                 save2<=x2*weight1;
                 count<=count+1;
                if(count>518)
                 save<=save1+save2+bais;//Multiply the input by the weights and add up the results
                end 
              
    end
assign  data_out=save;
endmodule

4.6 阈值判断模块

为什么需要阈值判断?

前面提到,我们在保存权值时将所有参数扩大了 1024 倍(即 $2^{10}$)。这意味着:

  • 隐藏层的输出值也被放大了约1024倍;
  • 输出层的计算结果同样被放大;
  • 原本训练时,输出层ReLU后大于0即判定为目标信号;
  • 现在由于数值被放大,不能简单以0为界,需要设定一个等效的阈值。

阈值推导

  • 原始判定边界:输出 > 0.5(二分类常用阈值);
  • 放大后等效边界:threshold = 0.5 × 1024 × 1024 = 524288
  • 实际上,由于隐藏层和输出层都放大了1024倍,总放大倍数为 $1024^2 = 1048576$,因此:
    threshold = 0.5 × 1048576 = 524288

波形图
pmhqXv9.png

五、关键优化技巧

5.1 归一化的"逆向"处理

再次强调这个最重要的优化:

处理方式FPGA资源消耗推荐度
FPGA内做除法(adc/4095)极高,需除法器IP
FPGA内用移位近似除法中等,精度损失大
权值预处理(本文方法)零额外消耗

具体做法:

  1. 训练时归一化:$V_{\text{in}} = \frac{\text{adc_data}}{4095} \times 1.8$;
  2. 提取权值时:$w' = \frac{w}{1.8} \times 1024$,取整;
  3. FPGA输入:直接使用原始ADC值 $x$(0~4095);
  4. 等效运算。

5.2 权值二值化(BWN)降低功耗

为了进一步压缩模型、降低功耗,本文采用了 Binary-Weight-Networks (BWN) 对权值进行二值化:

  • 权值只取 +1-1
  • 引入缩放因子 α 保证量化后的权值尽可能接近原始浮点权值。

二值化后,乘法运算退化为加减法或符号位判断,FPGA资源占用和动态功耗大幅下降。虽然精度略有损失,但在本场景中仍能保持97%以上的识别率。

二值化乘法器的Verilog实现

// 二值化权值乘法:只需判断符号
wire signed [31:0] product;
assign product = bin_weight ? x_in : -x_in;  // bin_weight=1为正,0为负

相比32bit×32bit的硬件乘法器,这个逻辑几乎不消耗DSP资源。

5.3 亚稳态消除

ADC数据输入和CS信号是异步信号(相对于16MHz系统时钟),必须做双级触发器同步,防止亚稳态传播:

reg adc_dat_d1, adc_dat_d2;
always @(posedge clk) begin
    adc_dat_d1 <= adc_dat;  // 第一级
    adc_dat_d2 <= adc_dat_d1; // 第二级(稳定后使用)
end
wire adc_dat_sync = adc_dat_d2;

六、实验结果:功耗与性能

通过 Actel Libero SOC 对FPGA各模块进行功耗仿真,结果如下:

工作模式静态功耗动态功耗总功耗
空闲态0.046 mW00.046 mW
正常工作0.046 mW2.79 mW2.835 mW
Flash*Freeze模式0.115 mW00.115 mW

2.835 mW 的总功耗,对于低功耗设备来说是完全可以接受的。

各子模块动态功耗分解:

  • 串并转换:0.01 mW
  • 隐藏层神经元(含FIFO):0.083 mW
  • ReLU激活:0.067 mW
  • 输出层神经元:0.067 mW

七、总结与思考

本文介绍了一套完整的从算法到硬件的轻量化神经网络部署方案:

  1. 算法层:针对低功耗约束,设计了512-2-1极简BP网络,避免使用CNN/RNN等重算力模型;
  2. 数据层:在真实环境中采集目标信号,建立含噪样本库,并通过数据增强提升泛化能力;
  3. 硬件层:基于Actel低功耗FPGA,用FIFO流水、整数运算、ReLU硬判决、权值二值化等手段,实现了毫瓦级推理;
  4. 系统层:与射频前端(LNA、包络检波、ADC)紧密配合,形成完整的标签信号检测链路。

可扩展性:该FPGA神经网络框架不仅可用于目标信号检测,只需更换训练数据集和权值参数,即可用于FMCW、RFID等其他射频信号的识别,具备良好的通用性。


python和verilog源程序下载

支付宝支付
价格: 30.00 元
温馨提示:免登录付款后7天内可重复阅读隐藏内容,登录用户付款后可永久阅读隐藏的内容。付费后如无反应,刷新网页即可阅读。 付费可读
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 赞赏
评论 抢沙发
取消