卷积神经网络 FPGA 设计概述
卷积的本质操作
每个输出通道的卷积核是一个大小为 K×K×Cin 的张量,与输入的所有 Cin 个通道做逐通道乘加(Cross-channel sum),每个输出通道由此得出。
示例说明
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3)
- 输入图像是 RGB 彩色图(3 通道);
- 有 64 个卷积核,每个核的大小为 3×3×3;
- 最终输出是 64 个通道(特征图),每个大小为 Hout×Wout。
FPGA 设计意义
- 输入通道数决定了并行输入数据数量;
- 输出通道数决定了需要并行计算多少组卷积;
- 如果使用多个 Processing Element (PE) 实现并行卷积,输出通道数常等于 PE 个数。
Vivado HLS 简介
Xilinx 推出的 Vivado HLS 工具可以直接使用 C、C++ 或 SystemC 对 Xilinx 系列的 FPGA 进行编程。FPGA 设计中从底层向上一共存在着四种抽象层级,依次为:结构性的、RTL、行为性的和高层。
Vivado HLS 的功能简单来说就是把 C、C++ 或 SystemC 的设计转换成 RTL 实现,然后就可以在 Xilinx FPGA 或 Zynq 芯片的可编程逻辑中综合并实现了。
接口信号(Interface → Summary 表格)
红框里的 RTL Ports 是综合后生成的硬件端口(对应 FPGA 的引脚 / 信号),关键信息如下:
| RTL Ports | Dir | Bits | Protocol | 含义解释 |
|---|
ap_clk | in | 1 | ap_ctrl_none | 时钟信号(FPGA 运行需要时钟驱动) |
ap_rst | in | 1 | ap_ctrl_none | 复位信号(用于重置电路状态) |
led | out | 2 | ap_none | 最终控制 LED 的 2 位输出信号 |
- 因为加了
#pragma HLS INTERFACE ap_ctrl_none port=return,工具自动补充了 ap_clk、ap_rst(HLS 默认的控制信号),但因为 ap_ctrl_none,这些信号不会有复杂的握手逻辑,只是基础的时钟、复位。
led 是 2 位输出(对应代码里的 uint2),协议是 ap_none(简单直接的端口,没有总线协议封装)。
接口约束详解
#pragma HLS INTERFACE ap_none port=led
- 指定
led 指针的接口类型为 ap_none。
ap_none 表示这个端口不会被映射到任何标准总线接口,通常用于直接连接到 FPGA 引脚的简单信号。
- 在这里,
led 会被综合成一个直接输出到 LED 引脚的信号。
#pragma HLS INTERFACE ap_ctrl_none port=return
- 指定函数返回的控制接口类型为
ap_ctrl_none。
ap_ctrl_none 表示函数没有控制信号(如 ap_start、ap_done、ap_idle 等)。
- 函数会在硬件复位后立即开始执行,并且不会停止,适合于需要持续运行的应用,如 LED 闪烁控制。
这是 Vivado HLS(High-Level Synthesis,高层次综合)工具的操作界面,用于给代码添加综合约束(Directive),核心是配置函数的接口属性。
-
整体场景
- 工具:Vivado HLS(用于将 C/C++ 代码转换为硬件描述语言,适配 FPGA 开发)。
- 工程:项目代码是控制 LED 闪烁的逻辑。
- 操作:通过 Directive Editor(约束编辑器)配置代码的综合规则,决定代码最终在 FPGA 上的硬件实现方式。
-
关键配置项
- Directive(约束类型):选择
INTERFACE,表示当前配置函数/模块的接口属性(即代码与外部信号、总线的交互方式)。
- Destination(约束存储位置):选
Source File,表示约束会直接嵌入到 C 源码注释中。
- mode (optional):选
ap_ctrl_none,这是 HLS 接口控制模式。ap_ctrl_none 是最简单的模式,没有握手信号,函数一旦启动就一直运行(适合纯组合逻辑、或循环不会退出的'持续运行'场景)。其他模式(如 ap_ctrl_hs)会添加握手信号,支持'启动 - 等待完成 - 重置'流程。
-
操作意图
点击 OK 后,Vivado HLS 会把这些接口约束固化到代码或工程中,后续综合时:函数会被综合成无握手信号、持续运行的硬件模块;指针会被映射为 FPGA 的外部端口(比如直接连到 LED 引脚的信号)。
-
代码与硬件的关联
代码里控制 LED 状态,HLS 结合这些约束,会把指针综合成硬件端口(比如 2 位宽的输出信号),直接驱动 FPGA 上的 LED。而大循环会被综合成硬件延时逻辑(或通过寄存器、计数器实现定时翻转)。
简单说,这一步是告诉 HLS:'我要把函数做成一个持续运行、无复杂握手、直接控制输出的硬件模块',是从'C 代码'到'FPGA 硬件'的关键配置环节。
Verilog HDL 语法与 Vivado 软件
Verilog 的设计多采用自上而下的设计方法(top-down)。即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。这样,可以把一个较大的系统,细化成多个小系统,从时间、工作量上分配给更多的人员去设计,从而提高了设计速度,缩短了开发周期。
Vivado 软件使用流程:新建工程设计→输入分析与综合约束→输入设计实现→生成和下载比特流。
分析与综合 Analysis/ Run Synthesis——约束输入.XDC——设计实现 Run Implementation——功能仿真 tb。
IP 核自动生成的只读的 verilog 例化模板文件。
IP 核使用实验
MMCM/PLL IP 核
MMCM/PLL IP 核介绍
PLL 的英文全称是 Phase Locked Loop,即锁相环,是一种反馈控制电路。PLL 对时钟网络进行系统级的时钟管理和偏移控制,具有时钟倍频、分频、相位偏移和可编程占空比的功能。
Xilinx 7 系列器件中的时钟资源包含了时钟管理单元 CMT,每个 CMT 由一个 MMCM 和一个 PLL 组成。
clk_wiz_0 instance_name (
// Clock out ports
.clk_out_100m(clk_out_100m),
.clk_out_100m_180(clk_out_100m_180),
.clk_out_50m(clk_out_50m),
.clk_out_25m(clk_out_25m),
// Status and control signals
.reset(reset),
.locked(locked),
// Clock in ports
.clk_in1(clk_in1)
);
RAM IP 核
RAM IP 核介绍
RAM 的英文全称是 Random Access Memory,即随机存取存储器,它可以随时把数据写入任一指定地址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度由时钟频率决定。
Xilinx 7 系列器件具有嵌入式存储器结构,嵌入式存储器结构由一列列 BRAM(块 RAM)存储器模块组成,通过对这些 BRAM 存储器模块进行配置,可以实现各种存储器的功能,例如:RAM、移位寄存器、ROM 以及 FIFO 缓冲器。
Vivado 软件自带了 BMG IP 核(Block Memory Generator,块 RAM 生成器),可以配置成 RAM 或者 ROM。
- DINA:RAM 端口 A 写数据信号
- ADDRA:RAM 端口 A 读写地址信号,对于单端口 RAM 来说,读地址和写地址共用同该地址线
- WEA:RAM 端口 A 写使能信号,高电平表示向 RAM 中写入数据,低电平表示从 RAM 中读出数据
- ENA:端口 A 的使能信号,高电平表示使能端口 A,低电平表示端口 A 被禁止,禁止后端口 A 上的读写操作都会变成无效。另外 ENA 信号是可选的,当取消该使能信号后,RAM 会一直处于有效状态
- RSTA:RAM 端口 A 复位信号,可配置成高电平或者低电平复位,该复位信号是一个可选信号
- REGCEA:RAM 端口 A 输出寄存器使能信号,当 REGCEA 为高电平时,DOUTA 保持最后一次输出的数据,REGCEA 同样是一个可选信号
- CLKA:RAM 端口 A 的时钟信号
- DOUTA:RAM 端口 A 读出的数据
FIFO IP 核
FIFO IP 核介绍
FIFO 的英文全称是 First In First Out,即先进先出。与 FPGA 内部的 RAM 和 ROM 的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,缺点就是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。
根据 FIFO 工作的时钟域,可以将 FIFO 分为同步 FIFO 和异步 FIFO:
- 同步 FIFO 是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作
- 异步 FIFO 是指读写时钟不一致,读写时钟是互相独立的
FIFO 常用参数:
- FIFO 的宽度,FIFO 一次读写操作的数据位 N
- FIFO 的深度,FIFO 可以存储多少个宽度为 N 位的数据
- 将空标志(almost_empty),FIFO 即将被读空
- 空标志(empty),FIFO 已空时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出
- 将满标志(almost_full),FIFO 即将被写满
- 满标志(full),FIFO 已满时由 FIFO 的状态电路送出的一个号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出
- 读时钟,读 FIFO 时所遵循的时钟,在每个时钟的上升沿触发
- 写时钟,写 FIFO 时所遵循的时钟,在每个时钟的上升沿触发
使用 Vivado 生成 FIFO IP 核,并实现以下功能:当 FIFO 为空时,向 FIFO 中写入数据,写入的数据量和 FIFO 深度一致,即 FIFO 被写满;然后从 FIFO 中读出数据,直到 FIFO 被读空为止。
在 FPGA 设计中,ILA(Integrated Logic Analyzer,集成逻辑分析仪)是 Xilinx 提供的一种强大的片上调试工具,用于实时捕获和分析 FPGA 内部信号的行为。它相当于传统硬件逻辑分析仪在 FPGA 中的'集成化'实现,无需额外的物理测试点即可监控内部信号。
Interface Type 选项用于选择 FIFO 接口的类型,选择默认的 Native;Fifo Implementation 选项用于选择是同步 FIFO 还是异步 FIFO 以及使用哪种资源实现 FIFO,选择 Independent。
Read Mode 选项用于设置读 FIFO 时的读模式,选择默认的 Standard FIFO。Data Port Parameters 选型用于设置读写端口的数据总线的宽度以及 FIFO 的深度,写宽度 Write Width 设置为 8 位,写深度 Write Depth 设置为 256。Reset Pin 不使用,取消勾选。
Status Flags 窗口,用于设置用户自定义接口或者用于设定专用的输入口,勾选即将写满和即将读空这两个信号。
Data Counts 窗口用于设置 FIFO 内数据计数的输出信号,此信号表示当前在 FIFO 内存在多少个有效数据。为了更加方便地观察读/写过程。
ILA 的核心价值是'无需引出外部引脚即可监控内部信号',从而避免了为调试而额外定义大量输入输出(I/O)引脚的麻烦,同时能直接捕获波形进行分析。
UART 串口实验
UART 串口实验涉及串口通信的基本原理与配置。
IIC 协议驱动模块仿真实验
IIC 协议是一种同步串行通信协议,由飞利浦(NXP)公司开发,主要用于短距离、低速的芯片间通信(如主板上的传感器与 MCU 通信),核心功能和特点如下:
- 总线结构:两根线:SCL(时钟线,主机控制)和 SDA(数据线,双向传输)。支持多主机和多从机,通过从机地址区分设备(通常 7 位或 10 位地址)。
- 通信时序:起始信号(SCL 高电平时,SDA 从高→低跳变)、停止信号(SCL 高电平时,SDA 从低→高跳变)、数据传输(每字节 8 位,高位在前,SCL 高电平时数据有效,低电平时可切换数据)、应答机制(每个字节传输后,接收方需发送 ACK 或 NACK)。
- 读写操作:写操作(主机发送'从机地址 + 写位'→发送内部地址→发送数据→停止信号)、读操作(主机发送'从机地址 + 写位'→发送内部地址→重新发送'从机地址 + 读位'→接收数据→发送 NACK→停止信号)。
- 优势:布线简单(仅需两根线),支持多设备共享总线。速率灵活(标准模式 100kbps,快速模式 400kbps,高速模式 3.4Mbps)。
EEPROM 读写测试实验(IIC 协议读写)
EEPROM 简介
EEPROM (Electrically Erasable Progammable Read Only Memory,E2PROM) 即电可擦除可编程只读存储器,是一种常用的非易失性存储器 (掉电数据不丢失)。ZYNQ 开发板上使用的是 AT24C64,通过 IIC 协议实现读写操作。
AT24C64 的地址格式如图所示:
向 EEPROM(AT24C64) 的存储器地址 0 - 255 分别写入数据 0 - 255,写完之后再读取存储器地址 0 - 255 中的数据,若读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁。
ZYNQ 开发板上 EEPROM 可编程地址 A2、A1、A0 连接到地,故 AT24C64 的器件地址为 1010000。
IIC(Inter - Integrated Circuit)协议通过 SCL 时钟线和 SDA 数据线实现主设备与从设备之间的通信。以下是结合给定代码对 IIC 协议实现过程的介绍:
IIC 总线属于多主多从 (多个主机 (Master),多个从机 (Slave)) 的总线结构,总线上的每个设备都有一个特定的设备地址,以区分同一 I2C 总线上的其他设备,设备连接如图所示:
2、主机发送一个字节 (从机地址 + 数据传送方向)
3、被寻址的从机应答
4、发送器发送一字节数据
5、接收器应答
6、重复步骤 4、5
7、主机发停止信号
起始位:主设备要发起通信时,会将 SDA 线从高电平拉低,此时 SCL 线保持高电平,以此表示通信开始。在给定代码中,当处于 st_sladdr 状态且 cnt 为 7'd1 时,sda_out <= 1'b0,实现了起始位的发送。
发送从设备地址:起始位之后,主设备发送 7 位或 10 位的从设备地址,紧接着是一位读写控制位(0 表示写,1 表示读)。代码中在 st_sladdr 状态下,通过循环依次将从设备地址 SLAVE_ADDR 的每一位从高位到低位通过 SDA 线发送出去,最后根据读写控制信号 i2c_rh_wl 确定发送的是读还是写控制位。
等待从设备应答:主设备发送完从设备地址和读写控制位后,会释放 SDA 线,等待从设备返回确认信号(ACK)。从设备在接收到地址后,会在第 9 个时钟周期将 SDA 线拉低,表示应答。在代码中,当 cnt 为 7'd38 时,会检测 SDA 线的电平,如果 SDA 线为高电平,表示从设备未应答,将 i2c_ack 标志位置 1。
发送字地址:根据 bit_ctrl 信号判断是发送 16 位还是 8 位字地址。如果是 16 位字地址,先在 st_addr16 状态下发送高 8 位,然后在 st_addr8 状态下发送低 8 位;如果是 8 位字地址,则直接在 st_addr8 状态下发送。发送过程中,通过 cnt 计数,依次将字地址的每一位通过 SDA 线发送出去,并在发送完后等待从设备应答。
数据传输方向:写数据:如果是写操作,主设备在发送完字地址并收到从设备应答后,进入 st_data_wr 状态。通过 cnt 计数,将 i2c_data_w 中的数据逐位通过 SDA 线发送出去,每发送一位后,等待从设备应答。读数据:如果是读操作,主设备在发送完字地址并收到从设备应答后,会再次发送从设备地址(读),然后进入 st_data_rd 状态。主设备在该状态下,通过设置 SDA 为输入模式,在每个时钟周期的上升沿从 SDA 线读取数据,存入 data_r 中。读取完 8 位数据后,主设备发送非应答信号(NACK),告知从设备数据已接收完毕。
结束位:数据传输完成后,主设备通过将 SDA 线从低电平拉高,同时 SCL 线保持高电平,表示通信结束。在代码的 st_stop 状态下,当 cnt 为 7'd0 时,sda_dir <= 1'b1 和 sda_out <= 1'b0,先将 SDA 线拉低,然后在 cnt 为 7'd3 时,sda_out <= 1'b1,将 SDA 线拉高,实现了结束位的发送。
SPI 协议驱动模块仿真实验
SPI(Serial Peripheral Interface)是一种同步串行通信协议,用于在微控制器与外围设备之间进行全双工数据传输。它使用 4 根信号线:SCLK (Serial Clock):时钟信号,由主机产生;MOSI (Master Out Slave In):主机发送,从机接收;MISO (Master In Slave Out):从机发送,主机接收;CS (Chip Select):片选信号,低电平有效。
SPI 协议有四种工作模式,由时钟极性 (CPOL) 和时钟相位 (CPHA) 决定:
- CPOL:时钟极性,决定空闲时 SCLK 的电平 (0 = 低电平,1 = 高电平)
- CPHA:时钟相位,决定数据采样的边沿 (0 = 第一个边沿,1 = 第二个边沿)
你提供的代码实现的是 SPI 模式 0,即 CPOL=0,CPHA=0:SCLK 空闲时为低电平,数据在 SCLK 的上升沿采样,下降沿输出。
根据分频计数器生成 SPI 时钟信号,符合模式 0 的特性(空闲低电平,上升沿有效)。
数据发送功能,按照 MSB 优先的顺序将 8 位数据通过 MOSI 线发送出去。
数据接收功能,在 SCLK 的上升沿采样 MISO 线上的数据,同样按照 MSB 优先的顺序接收 8 位数据。
工作流程
- 初始化:系统复位后,所有信号处于默认状态,SPI 时钟和片选信号均为高电平。
- 开始传输:当 spi_start 信号有效时,片选信号 spi_cs 被拉低,开始 SPI 传输。
- 数据传输:发送:在每个 SCLK 的上升沿,将待发送数据的一位放到 MOSI 线上;接收:在每个 SCLK 的上升沿,从 MISO 线上采样一位数据。
- 结束传输:当 spi_end 信号有效且当前字节传输完成后,片选信号 spi_cs 被拉高,结束 SPI 传输。以 CPOL=0,CPHA=0 工作模式为例,通信时序如图所示。
练习:基于 FPGA 实现 CNN
为什么采用半精度浮点数?
通过使用 IEEE-754 半精度浮点数可以提高模型的准确性。
FPGA 实现 CNN 时采用半精度浮点数(FP16)的核心逻辑是:在保证推理精度的前提下,通过减少位宽降低硬件资源占用(存储、计算单元)、提升并行计算能力、加速数据传输,并降低功耗。这一选择完美适配了 FPGA'资源有限但可灵活定制'的特性,同时契合了 CNN 对精度的容错性,最终实现'高效能比'的网络部署。
相比之下,若用整数(如 INT8/INT16)虽能进一步压缩位宽,但需复杂的量化策略(可能损失更多精度);而 FP32 会导致资源浪费和性能下降,因此 FP16 成为 FPGA 实现 CNN 的'最优平衡点'。
一、Processing Element(float16 的'乘法 - 累加')
Processing Element 由 FM(floatMult16),FADD(floatAdd16),result_reg 三个单元组成。
- FM(floatMult16) 单元是执行两个 float16 数据的乘法
- FADD(floatAdd16) 单元是执行两个 float16 数据的加法
- result_reg 寄存器,存放的是新的求和,将电路从组合逻辑转为同步时序电路,保证数据的同步
卷积单元具体实现如图所示,即相乘相加操作。卷积计算具体操作就是点乘,本质就是乘法和加法。图中输入为 float16 类型数据 A 和 B,输出 float16 数据类型的结果。
原理图如图所示,可以看到输入 floatA 和 floatB,以及输出 result 位宽均为 16。
半精度浮点数 FP16(float point 16) 格式理解
首先,一个十进制数可写成一个纯小数乘上 10 的若干次方,类似的,一个二进制数可写成一个纯小数乘上 2 的若干次方。一般地,任一个二进制 N,可表示为 N=f x 2^(e)。f 代表二进制的小数(fraction),e 带表阶数 (exponent)。
首先第一个问题:人们为什么需要 exponent 减 15 或者减 127?因为只有这样,才可以计算出 2 的负次幂,减去的这个数称为''偏置常数'',它等于 2^(n-1)-1,其中 n 为 exponent 的位数,故 2^(5-1)-1=15, 2^(8-1)-1=127。
其次第二个问题:公式里面的 1.fraction 的这个'1'是怎么来的?因为 fraction 最高位的再高一位被称为隐藏位,这个隐藏位就固定是'1'。举个例子更好理解:已知十进制的 0.75 用 FP32 可以表示为 0_01111110_10…0,(省略号表示都是 0),那还有没有其它的表示方法呢?当然有啦!就好比在十进制中 0.75 可以写成 75x10^(-2) 也可以写成 7.5x10^(-1),0.75 用 FP32 还可以表示成 0_10000101_000001100…0,可以看出 0.75 的阶数增加了 7(133-126=7),故尾数右移 7 位,隐藏位就显现出来了,当中尾数多出来的 1 便是隐藏位。所以这个隐藏位 1 在公式中其实是可有可无的。
加法器:1.对阶,将两个小数的 exponent 化为相同的;2.尾数相加;3.化为 FP16 标准;
乘法器:1.阶数相加;2.尾数相乘;3.化为 FP16 标准;
这段代码实现了 IEEE 754 半精度(16 位)浮点数的加法运算,核心是通过指数对齐→尾数加减→结果规格化→组合结果四个步骤完成,同时处理了特殊情况(如零值、相反数等)。
在 IEEE 754 标准中,浮点数的表示采用符号位(Sign)、指数位(Exponent)和尾数位(Mantissa)的组合。对于半精度浮点数(16 位),其格式为:符号位(1 位):[15],0 表示正数,1 表示负数;指数位(5 位):[14:10],存储的是偏移后的指数(实际指数 = 存储值 - 15);尾数位(10 位):[9:0],存储的是小数部分,隐含整数部分 1.(规格化数默认整数部分为 1)。
二进制表示:101.1₂ = 1.011₂ × 2²
符号位:0(正数)
指数位:2 + 15 = 17 → 10001₂
尾数位:011 → 补零至 10 位 → 0110000000
最终二进制:0 10001 0110000000(16 位)
- B = -3.25
二进制表示:-11.01₂ = -1.101₂ × 2¹
符号位:1(负数)
指数位:1 + 15 = 16 → 10000₂
尾数位:101 → 补零至 10 位 → 1010000000
最终二进制:1 10000 1010000000(16 位)
最终结果:sum = {sign, exponent[4:0], mantissa} = 0 01111 0010000000;
转换为十进制:符号位:0(正数)指数位:15 - 15 = 0 → 2⁰尾数位:1.001₂ = 1.125 最终值:1.125 × 2⁰ = 2.25,与 5.5 + (-3.25) = 2.25 一致。
举个例子:将十进制数转换为半精度浮点数格式
- A = 5.5
二、Convolution Unit(单通道、单输出点的一次卷积运算)
卷积单元如图所示,输入为卷积核 filter 和卷积核窗口覆盖的图像 image,计算输出该窗口提取的特征。
原理图如图所示,filter 的位宽为 400,卷积核窗口覆盖的图像 image 的位宽是 400,输出位宽是 16。
- filters 位宽计算:卷积核大小为 5x5,卷积核个数为 1,数据位宽为 float16,所以 5x5x1x16=400
- image 位宽计算:手写数字图像大小为 32x32,卷积核窗口覆盖的图像大小为 5x5,数据位宽为 float16,所,5x5x16=400
- result 位宽计算:输出结果为 float16 数据类型的数,具体计算见 2.4 Processing Element 章节
卷积单元完整的顶层原理图如图所示,对一个卷积核和该卷积核覆盖的图像区域 (可以称为窗口) 进行计算,输出一个计算结果 (float16)。
在深度学习中,'一次卷积运算'通常指一个卷积核在输入图像的某个位置上进行的计算。例如:对于 5×5 的输入图像和 3×3 的卷积核,一次卷积运算需要:将 3×3 的卷积核与输入图像的 3×3 区域逐元素相乘。将所有乘积相加,得到输出特征图的一个像素值。这段代码通过参数化设计(D 和 F),实现了单通道、单输出点的卷积运算。具体来说:
- 输入:image 和 filter 分别存储了待卷积的图像区域(尺寸为 F×F)和卷积核(尺寸同样为 F×F)。
- 计算过程:通过循环逐点读取 image 和 filter 的对应元素(共 D×F×F 个点)。将每对元素送入 processingElement16 进行乘法,并累加到 result 中。
- 输出:result 是最终的卷积结果,对应输出特征图中的一个像素值。
- 完整的卷积层通常需要多个这样的运算:滑动窗口(对输入图像的不同位置重复此运算)、多输出通道(每个输出通道需要一个独立的卷积核,需实例化多个 convUnit)。
三、Single Filter Layer(完整的卷积运算,行优先由高到低存储)
Single Filter Layer 原理图如图所示,由 1 个 RF selector 和 14 个 CU 组成,该部分是计算一个卷积核与一幅图像的卷积,输出卷积提取的完整图像的特征。
RF selector 的作用:将卷积核覆盖的图像区域 (可以称为窗口) 的数据对应传输给 14 个 CU,输入图像尺寸为 32x32x16,卷积核大小为 5x5x16,卷积核滑动步长为 1,此时一幅完整图像将产生 28x28 个窗口数据,每个窗口数据为 5x5x16。因为 14 个 CU 是并行计算的,故 RF selector 输出位宽为 14x5x5x16=5600。
为什么选择使用 14 个 CU,作者给出的解释是:LUT 的数量在单个或多个卷积核模块中呈指数增长,实验对比后,最终决定使用 CU 的数量等于输出特征中单行像素数量的一半。例如,输入图像 32x32,卷积核 5x5,输出特征为 28x28,故 CU 的数量等于 28/2=14。
滑动窗口是卷积操作的核心机制,它通过在输入图像上按固定步长移动窗口来提取局部特征。在这段代码中,主要通过以下参数控制窗口滑动:
- 输入图像尺寸:32×32(参数 H=32, W=32)
- 卷积核尺寸:5×5(参数 F=5)
- 步长:1(隐含参数,由代码逻辑决定)
- 输出特征图尺寸:28×28(计算公式:(H-F+1)×(W-F+1) = (32-5+1)×(32-5+1) = 28×28)
如何得到 28 个 5×5 窗口
代码通过 RFselector 模块实现窗口提取,关键逻辑在于对输入图像地址的计算:
receptiveField[addressFDATA_WIDTH+:FDATA_WIDTH] = image[rowNumberWDATA_WIDTH + cDATA_WIDTH + kHWDATA_WIDTH + iWDATA_WIDTH +:FDATA_WIDTH];
rowNumber:当前处理的输出行(范围 0-27)
c:当前处理的输出列(范围 0-27)
k:通道索引(这里 D=1,只有一个通道)
i:窗口内的行偏移(范围 0-4)
窗口提取过程:对于每个输出位置 (rowNumber, c),提取一个 5×5 的窗口。窗口的左上角位于输入图像的 (rowNumber, c) 位置。通过 iWDATA_WIDTH 实现垂直方向的滑动,通过 c*DATA_WIDTH 实现水平方向的滑动。
28×28 特征图的生成机制
代码采用分块处理策略,将每行 28 个输出分为两组,每组 14 个:
if (column == 0) begin // 处理前 14 个输出位置 (0-13)
for (c = 0; c < (W-F+1)/2; c = c + 1)
begin ... end end
else begin // 处理后 14 个输出位置 (14-27)
for (c = (W-F+1)/2; c < (W-F+1); c = c + 1) begin ... end end
流水线控制:convLayerSingle 模块中的 rowNumber 和 column 变量控制当前处理的行和块。每处理完一个块(14 个输出),切换到下一个块。处理完一行后,切换到下一行。
时序控制:每个 5×5 窗口的卷积需要 D×F×F+2=27 个时钟周期。通过 counter 变量计数时钟周期,完成后更新输出索引。
四、Multi Filter Layer(多层卷积运算)
Multi Filter Layer 原理图如图所示,由 2 个 convLayerSingle 组成,即并行度为 2。上述内容可知 Multi Filter Layer 的输入是图像和 6 个卷积核,因此 6 个卷积核分为 2 个一组,循环 3 次输入到 convLayerSingle,即每次执行 2 个卷积核与图像的卷积。
五、integrationConv(卷积层由多个卷积核组成)
形成多通道特征图,Cout 个卷积核。
激活层设计——以 tanh 为例(泰勒展开)
激活层设计涉及非线性函数的硬件实现,常采用泰勒级数展开法。
池化层设计——自顶而下分析池化层的设计过程
一、AvgUnit(四输入求平均值)
Averaging Unit 如图所示,求输入 4 个数的均值。该单元先求 4 个数 A、B、C、D 的和,再将和乘以 0.25 得到 4 个数的均值。
原理图如图所示,输入为 4 个位宽 16 的数,输出为位宽 16 的均值。
二、AvgPoolSingle(单个通道的平均池化)
该单元是执行单个通道的平均池化操作,由多个 AvgU 组成,如图所示:
原理图如图所示,该单元输入位宽为 12544,输出位宽为 3136。前一层卷积输出特征 H 为 28,W 为 28,Average Pool Single Layer 的深度为 1,因此输入位宽为 28x28x1x16=12544;该项目平均池化的窗口大小为 2x2,故输出位宽为 14x14x1x16=3136。
三、AvgPoolMulti(对多维数据执行 2×2 平均池化)
遍历 6 维输出特征图,对每一维执行单通道 2×2 平均池化操作。
图为该项目的平均池化层,其包含一个 AvgPoolSingle 单元,模块的输入为图像特征矩阵,输出为池化后的特征矩阵。
池化层的原理图如图所示,其中输入位宽为 75264,输出位宽为 18816。池化层位于卷积层和激活层之后,第一次卷积层输出位宽为 75264,因此池化层的输入位宽为 75264。Average Pool Multi Layer 的深度为 6,前卷积层的输出特征 H 和 W 均为 28,故输入位宽为 28x28x6x16=75264;平均池化窗口大小为 2x2,输出特征 H 和 W 变为 14,输出位宽为 14x14x6x16=18816。
SoftMax 层设计
SoftMax 函数的作用是输入归一化,计算各种类的概率,即计算 0-9 数字的概率,SoftMax 层的原理图如图所示,输入和输出均为 32 位宽的 10 个分类,即 32x10=320。
本项目 softmax 实现逻辑为:指数计算 (通过 exponent 实现)、计算指数和 (通过 floatAdd 实现)、求指数和倒数 (通过 floatReciprocal 实现)、计算每个元素的 softmax 值 (通过 floatMult 实现)。