基于XC7Z100+OV5640(DSP接口)YOLO人脸识别前向推理过程(笔记)

发布时间 2023-06-08 19:02:23作者: 李白的白

PS与PL使用Axi4-lite进行数据交互

  • 内容概述
    • 如何在PS和PL之间使用Axi4-lite接口进行数据交互
    • Axi4-lite是一种简单的总线协议,适用于低吞吐量的数据传输,例如PS发送加速相关的命令给PL
    • 内容分为以下几个部分:
      • PS和PL之间的数据交互方案介绍
      • Axi4-lite接口的使用方法和示例代码
      • Axi4-lite接口的时序分析和理解
      • PS端的配置代码和调试方法

PS和PL之间的数据交互方案介绍

  • PS和PL之间有两种数据交互方式:DMA和Axi4-lite

    • DMA适用于高吞吐量的数据传输,例如视频流或图像处理
    • Axi4-lite适用于低吞吐量的数据传输,例如PS发送加速相关的命令给PL
  • Axi4-lite接口是一种简单的总线协议,只有两种操作:写和读

    • 写操作是指PS向PL写入数据或命令
    • 读操作是指PS从PL读取数据或状态
  • Axi4-lite接口有五个通道:AW、W、B、AR、R

    • AW通道用于传输写地址
    • W通道用于传输写数据
    • B通道用于传输写响应
    • AR通道用于传输读地址
    • R通道用于传输读数据
  • Axi4-lite接口基于握手机制进行数据传输,即只有当value和ready信号同时为高时,才能完成一次有效的数据传输

  • Axi4-lite接口的使用方法和示例代码

    • 在Vivado中创建一个新工程,添加一个Zynq IP和一个Axi4-lite IP
      • Zynq IP用于配置PS端的DDR、GP等资源
      • Axi4-lite IP用于在PL端接收PS端通过Axi4-lite发送过来的数据或命令
    • 使用Vivado自带的工具Create and Package New IP来创建一个Axi4-lite外设,并修改其代码以满足需求
      • 在工具中选择Create AXI4 Peripheral,并输入IP名称、目录等信息
      • 在接口配置中选择Lite类型、Slave模式、32位宽度、4个寄存器数量等参数,并点击Next
      • 在下一步中选择添加IP到当前工程,并点击Finish,完成IP创建过程
      • 在生成的IP代码中,找到顶层模块和子模块,并修改其代码以实现Axi4-lite接口的功能
        • 在顶层模块中添加四个输出端口,分别对应四个寄存器的值,并在模块底部赋值给它们
        • 在子模块中实现Axi4-lite接口的时序逻辑,即根据value和ready信号来判断是否进行数据读写操作,并根据地址来选择对应的寄存器进行操作
    • 在Vivado中将生成的IP添加到block design中,并与Zynq IP连接起来,生成比特流文件并导出硬件信息
    • 在Vitis中创建一个新工程,并编写C语言代码来配置PS端的Axi4-lite接口,并向PL端发送或接收数据
      • 在工程中创建一个main.c文件,并引入xil_io.h头文件,该文件提供了向Axi4-lite接口写入或读取数据的函数xil_out32()和xil_in32()
      • 在代码中定义四个宏,分别对应四个寄存器的地址,这些地址可以在Vivado中查看或在parameter.h文件中找到
      • 在代码中使用xil_out32()函数向四个寄存器写入不同的数据,并使用xil_in32()函数从四个寄存器读取数据,并赋值给一个变量data,以便后续查看或打印
  • Axi4-lite接口的时序分析和理解

    • 在Vivado中添加一个ILA IP,并将其连接到Axi4-lite接口上,以便观察其信号波形并进行调试
    • 在Vitis中启动调试模式,并在main.c文件中设置断点,在每次写入或读取操作后暂停程序运行,并查看ILA上的信号波形变化情况
    • 根据信号波形分析Axi4-lite接口的时序特点,主要关注value和ready信号以及地址和数据信号之间的关系,以下是一些示例:
      • 写地址时序:当AWVALID和AWREADY同时为高时,表示写地址有效,并且AWADDR信号上显示出要写入的地址值;当BVALID和BREADY同时为高时,表示写响应有效,并且BRESP信号上显示出写操作是否成功;
      • 写数据时序:当WVALID和WREADY同时为高时,表示写数据有效,并且WDATA信号上显示出要写入的数据值;当BVALID和BREADY同时为高时,表示写响应有效,并且BRESP信号上显示出写操作是否成功;
      • 读地址时序:当ARVALID和ARREADY同时为高时,表示读地址有效,并且ARADDR信号上显示出要读取的地址值;
      • 读数据时序:当RVALID和RREADY同时为高时,表示读数据有效,并且RDATA信号上显示出读取到的数据值;
  • PS端的配置代码和调试方法

    • PS端的配置代码主要使用了xil_io.h头文件提供的两个函数xil_out32()和xil_in32()来向Axi4-lite接口写入或读取数据,这两个函数需要传入两个参数:地址和数据;
    • 调试方法主要使用了Vitis提供的调试模式,在main.c文件中设置断点,在每次写入或读取操作后暂停程序运行,并查看变量data的值是否与预期相符;

    ZYNQ PL 端中断使用介绍

  • 中断的概念

    • 程序执行过程中遇到急需处理的事件时,暂时终止 CPU 上线行程序的运行,转而执行相应的事件处理程序
    • 处理完成后再返回源程序被中断处或调度其他程序执行的过程
    • 示意图
      • 主程序在 PS 端用 C 语言执行
      • 中断请求来了,主程序在当前位置打一个断点,暂停执行
      • 响应中断请求,进入中断服务程序,执行一段代码进行中断处理
      • 返回到主程序之前的代码位置,继续执行
主程序->中断请求: 中断发生
Note right of 中断请求: 中断源
中断请求->主程序: 中断响应
Note right of 主程序: 暂停执行,保存现场
主程序->中断服务程序: 跳转执行
Note right of 中断服务程序: 处理中断事件
中断服务程序->主程序: 返回继续执行
Note right of 主程序: 恢复现场
  • ZYNQ PL 端中断的使用

    • 在 Euler 的实现方案里,PS 端会给一些命令给 PR 端去完成一些操作
    • 当 PL 端完成后,需要给 PS 一个反馈,可以通过中断操作实现
    • ZYNQ 有专门的文档介绍中断的内容:UG 585
      • 文档介绍了很多关于中断的信息,包括系统层模块图,中断类型和 ID 号等
      • 文档不会完全翻译,只会介绍 PL 端给 PS 端发中断的过程和示例工程
      • 注意 PL 到 PS 的中断可以是上升沿或高电平触发,有两个区间的 ID 号:61-68 和 84-91
  • 示例工程的创建和运行

    • 在 Vivado 中创建 block design ,调用 ZYNQ PS 和 DDR3 配置
    • 在 PL 端添加一个产生中断的模块:jane_intr ,用 VIO 控制计数器,当计数器在 100-300 之间时产生高电平信号作为中断信号
    • 连接中断信号到 PS 端的 IRQ_F2P[15:0] ,选择对应的 ID 号和类型
    • 添加 ILA 模块抓取 VIO 和中断信号,观察波形变化
    • 生成 bitstream 文件,导出硬件信息,创建 Vitis 工程
    • 在 Vitis 中导入中断相关的示例代码:scugic ,查看 main 函数和 device driver handler 函数,了解中断初始化和服务程序的编写方式
    • 运行工程,在 VIO 中控制计数器开始计数,观察 ILA 中的波形和 Vitis 中的打印信息

    Axi4-lite模块完善

  • 如何在 PL 端对 yolo 进行加速实现

  • 对 Axi4 Lite 模块的完善,主要是对寄存器 0 的前 4 个比特位加上自动清零的功能

  • 使用了 Vivado 和 Vitis 工具进行代码编写、编译、调试和运行

  • 给出了相关的代码片段、截图和说明

HR Lite 模块
  • Axi4 Lite 是一种高性能的总线协议,用于 PS 和 PL 之间的数据传输

  • PS 可以通过 Axi4 Lite 向 PL 发送指令,PL 根据指令完成相应的操作

  • Axi4 Lite 模块内部有一个寄存器 0 ,用于存储 PS 发送的指令

  • 寄存器 0 的不同比特位有不同的功能,例如:

    • bit0: start,表示 PS 对 DMA 执行写操作,即 PS 向 PL 传输数据
    • bit1: stop,表示 PS 对 DMA 执行停止操作,即 PS 停止向 PL 传输数据
    • bit2: reset,表示 PS 对 DMA 执行复位操作,即 PS 清空 DMA 的缓冲区
    • bit3: done,表示 PL 对 DMA 执行完成操作,即 PL 完成数据处理并返回给 PS
    比特位 功能 说明
    0 start PS 向 PL 发送数据前打招呼,让 PL 准备接收
    1 stop PS 向 PL 发送数据后结束通信
    2 reset PS 向 PL 发送复位信号
    3 go PS 向 PL 发送启动信号
自动清零功能
  • 自动清零功能是指在 PS 向寄存器 0 的前 4 个比特位写入数据后,PL 端会自动将这些比特位清零
  • 自动清零功能的好处是可以让寄存器 0 的前 4 个比特位呈现出脉冲信号,方便后续的操作
  • 实现自动清零功能的方法是在 HR Lite 模块的代码中,在写入数据后加上一个 else 分支,将寄存器 0 的前 4 个比特位赋值为 0 ,其他位保持不变
  • 示例代码如下:
if (slv_reg_wren) begin // 写入数据
    slv_reg0 <= slv_reg_wdata;
end else begin // 自动清零
    slv_reg0[3:0] <= 4'b0000; // 前 4 位清零
    slv_reg0[31:4] <= slv_reg0[31:4]; // 其他位保持不变
end
编译、调试和运行
  • 使用 Vivado 工具创建 Axi4 Lite 模块,并对代码进行编译和生成 bit 文件
  • 使用 Vitis 工具创建应用程序,并导入 bit 文件和 C 语言文件
  • 使用 Vitis 工具或 ILA 工具对代码进行调试和运行,并观察寄存器 0 的变化情况
  • 验证自动清零功能是否正常工作

main_ctrl模块代码编写

main control 模块的功能
  • 对 Axi4-lite 的计算器信息进行解析

    Axi4-lite 是一种简化的总线协议,用于在 PS 端和 PL 端之间传输数据和命令。计算器信息是 PS 端发送给 PL 端的一组寄存器,包含了加速器需要执行的操作和参数。例如,计算器信息可以指定加速器要进行加法、减法、乘法或除法,以及操作数的值和位宽。main_ctrl模块需要根据 Axi4-lite 的协议规范,从总线上读取计算器信息,并将其存储在内部的信号或寄存器中,以便后续使用。

    • 计算器信息包括:数据类型、卷积类型、卷积核尺寸、输入输出通道数、输入输出特征图尺寸等 (计算器信息包括卷积类型、数据类型、卷积核大小、步长、填充等参数,分别存储在四个寄存器中)
    • 解析的方法是按照表格中给出的比特位分配,将寄存器中的比特提取出来,赋值给相应的信号
  • 根据解析的信息对加速器内部各模块进行协调控制

    加速器内部各模块包括了算术逻辑单元(ALU)、数据存储单元(DSU)、数据传输单元(DTU)等,它们分别负责执行计算、存储数据、传输数据等功能。main_ctrl模块需要根据解析的计算器信息,向各个模块发送相应的控制信号,使它们按照预期的顺序和方式工作。例如,如果计算器信息指定了要进行加法运算,那么 main_ctrl模块需要向 ALU 发送加法指令,并向 DSU 发送读取操作数的地址,并向 DTU 发送写入结果的地址。

    • 加速器内部各模块包括:padding、卷积计算、转置、上采样等
    • 协调控制包括:控制各模块的工作时序、传递相关参数、接收各模块的完成信号等
    • 这个功能在本网页中没有完全实现,而是在后面的课程中逐步完成
  • 向 PS 端产生反馈加速器是否完成该阶段工作的中断请求

    中断请求是一种通知机制,用于告诉 PS 端 PL 端的工作状态。当加速器完成了一个阶段的工作,例如完成了一次计算或一次数据传输,那么 main_ctrl模块需要向 PS 端发送一个中断请求信号,让 PS 端知道 PL 端已经准备好接收下一条命令或处理下一组数据。

    • 中断请求是一个高电平信号,持续200个时钟周期
    • 这个功能是在加速器内部完成一次卷积或其他操作后,向 PS 端发送一个中断信号,通知 PS 端当前工作已经完成
    • 这个功能在本文中通过一个状态机实现,根据不同的操作类型和完成状态,控制中断信号的拉高和拉低
    • 中断请求在加速器完成当前卷积或上采样操作后产生
main control 模块的代码编写
  • 只编写第一个和第三个功能的代码,第二个功能的代码在后续课程中逐步完善

  • 先创建一个 ACU_top 模块作为加速器的顶层模块,然后在其中实例化 main control 模块

  • ACU_top 模块的端口有:

    • 与 DMA 相关的 stream 接口
      • stream 接口是一种 AXI4 流协议,用于传输数据和控制信息
      • stream 接口包括:数据线、有效位、就绪位、最后位等信号
      • stream 接口有两种方向:MM2S(主机到从机)和 S2MM(从机到主机)
    • 与 light 相关的四个寄存器接口
      • 寄存器接口是一种 AXI4 Lite 协议,用于配置和读取寄存器数据
      • 寄存器接口包括:地址线、数据线、读写使能位、响应位等信号
      • 寄存器接口有两种方向:读(从机到主机)和写(主机到从机)
    • 向 PS 端产生中断请求的 task_finish 信号
  • main control 模块的端口有:

    • 四个寄存器接口,用于接收计算器信息
    • 其他与内部模块交互的信号,如 write_start, write_finish, read_start, read_finish, conv_start, conv_finish, transpose_start, transpose_finish, upsample_start, upsample_finish 等(本节视频中暂不编写)
  • main control 模块的内部逻辑有:

    • 对四个寄存器中的计算器信息进行解析,提取出相应的参数并赋值给内部信号
      • 解析方法是按照表格¹中给出的比特分配,将寄存器数据按位切割并赋值给对应的信号
      • 内部信号包括:data_type, conv_type, reset_type, lets_type, control_select, kernel_size, input_channel_num, output_channel_num, input_feature_size, output_feature_size 等
    • 根据内部信号对加速器内部各模块进行协调控制(本节视频中暂不编写)
    • 根据各模块的完成信号产生中断请求信号 task_finish
      • 使用一个状态机来控制 task_finish 的产生和消除
      • 状态机有六个状态:idle, write, read, conv, transpose, upsample, finish
      • idle 状态表示空闲,根据 start 信号跳转到相应的操作状态
      • write/read/conv/transpose/upsample 状态表示正在执行相应的操作,根据 finish 信号跳转到 finish 状态
      • finish 状态表示操作完成,拉高 task_finish 信号,并启动一个计数器,计数到200后跳转回 idle 状态并拉低 task_finish 信号
  • 代码编写的步骤和方法

    • 首先创建一个加速器顶层模块(acu_top),定义它与外部交互的端口,包括 DMA 相关的 stream 接口、四个寄存器、中断信号等
    • 然后创建一个 main control 模块,定义它与加速器顶层模块交互的端口,主要是四个寄存器以及其他需要协调控制的信号
    • 接着在 main control 模块中编写代码,实现对 Axi4-lite 的计算器信息进行解析和向 PS 端产生反馈加速器是否完成该阶段工作的中断请求两个功能
      • 对计算器信息进行解析的方法是按照表格中给出的比特位分配,将寄存器中的比特提取出来,赋值给相应的信号
      • 产生反馈加速器是否完成该阶段工作的中断请求的方法是通过一个状态机实现,根据不同的操作类型和完成状态,控制中断信号的拉高和拉低
    • 最后在后面的课程中逐步完善 main control 模块,实现根据解析的信息对加速器内部各模块进行协调控制的功能

    Stream_rx模块代码编写

  • 功能

    • Stream_rx模块是一个用来接收PS端发送的数据(包括权重、偏置、输入数据、激活查找表等)的模块,需要完成两个功能:
      • 完成对DMA数据的接收功能,并且区分当前接收的是哪一种类型的数据(根据data_type寄存器判断)。
      • 产生write_finish信号,给到main_control模块,表示接收完成。
    • Stream_rx模块需要根据Stream接口的时序关系来进行数据接收,主要涉及到t_valid、t_data、t_last、t_ready等信号。
    • Stream_rx模块需要根据main_control模块发送的light信息来进行数据接收,主要涉及到write_start、data_type等寄存器。
    • Stream_rx模块需要将接收到的数据输出到相应的缓存模块,并且产生相应的value信号(如feature_value、weight_value等),表示数据类型和有效性。
    • Stream_rx模块需要根据t_last和t_valid信号产生write_finish信号,并且只保持一个时钟周期。
  • 代码编写思路

    • 根据stream接口的时序关系,使用state信号和data type信号控制ready信号和value信号
    • 当t_valid和t_last同时为高时,产生一个时钟周期的write_finish高脉冲
    • 根据data type信号,给不同类型的数据产生相应的value信号
  • module stream_rx(
      input clk,
      input rst_n,
      // stream接收端口
      input tvalid,
      input [31:0] tdata,
      input tlast,
      input [3:0] tkeep,
      output tready,
      // data type输出端口
      output [1:0] data_type,
      // write finish输出端口
      output reg write_finish,
      // data输出端口
      output [31:0] data,
      // 不同类型数据的有效标志输出端口
      output reg weight_value,
      output reg bias_value,
      output reg feature_value,
      output reg activation_value
    );
    
    // state信号,用于控制ready信号和value信号
    reg [1:0] state;
    parameter IDLE = 2'b00;
    parameter WRITE = 2'b01;
    parameter FINISH = 2'b10;
    
    // stream data value信号,表示数据有效的标志
    wire stream_data_value;
    
    // 根据state信号和data type信号控制ready信号和value信号
    always @(posedge clk or negedge rst_n) begin
      if (~rst_n) begin
        state <= IDLE;
        tready <= 1'b0;
        weight_value <= 1'b0;
        bias_value <= 1'b0;
        feature_value <= 1'b0;
        activation_value <= 1'b0;
        write_finish <= 1'b0;
      end else begin
        case (state)
          IDLE: begin
            if (tvalid) begin // 如果有数据发送过来
              state <= WRITE; // 进入写状态
              tready <= 1'b1; // 准备好接收数据
            end else begin
              state <= IDLE; // 否则保持空闲状态
              tready <= 1'b0; // 不准备接收数据
            end
          end
          
          WRITE: begin
            if (stream_data_value && tlast) begin // 如果是最后一个有效数据
              state <= FINISH; // 进入完成状态
              write_finish <= 1'b1; // 发送write finish信号
            end else begin
              state <= WRITE; // 否则保持写状态
              write_finish <= 1'b0; // 不发送write finish信号
            end
    
            case (data_type) // 根据data type信号给不同类型的数据产生相应的value信号
              2'b00: feature_value <= stream_data_value; // 如果是feature data类型,产生feature value信号
              2'b01: weight_value <= stream_data_value; // 如果是weight data类型,产生weight value信号
              2'b10: bias_value <= stream_data_value; // 如果是bias data类型,产生bias value信号
              2'b11: activation_value <= stream_data_value; // 如果是activation data类型,产生activation value信号
            endcase
    
          end
          
          FINISH: begin 
            state <= IDLE; // 完成后回到空闲状态 
            write_finish <= 1'b0; // 不发送write finish信号 
          end
          
          default: state <= IDLE; // 默认为空闲状态
          
        endcase 
        
      end 
    end
    
    // 当tvalid和tlast同时为高时,产生一个时钟周期的write finish高脉冲 
    assign stream_data_value = tvalid & tready; 
    
    // 将接收到的数据赋值给data输出端口 
    assign data = tdata; 
    
    endmodule 
    

激活模块代码编写

  • 激活模块的作用

    • 缓存激活函数的查找表数据
    • 将输入数据当成缓存的独立值,输出为激活处理的输出值
  • 激活模块的实现

    • 使用RAM IP来存储查找表数据
      • 数据位宽为8位,无符号整形
      • 数据深度为32,地址位宽为5
      • 写操作时,使用64位数据,一次写入8个激活值
      • 读操作时,使用8位数据,一次读出一个激活值
    • 使用查找表的方式来进行激活处理
      • 使用main lab中的ev数组作为查找表数据
      • 将卷积输出的8个通道数据作为查找表的独立值
      • 输出为对应的激活函数值
  • 激活模块的注意事项

    • 需要根据DMA传输的64位数据来拆分成8个8位数据
    • 需要根据RAM IP的读延时来设置输出有效标志
    • 需要等待卷积模块的输出后再进行初始化

    输入数据缓存模块代码编写

  • 功能:对当前层的输入数据做一个缓存

  • 原因:需要进行填充操作

    因为要进行填充操作,需要在两行数据之间插入0,而DMA发送的数据是连续不断的,没有时间间隙,所以需要先缓存数据,再控制读取时机

    • 填充:在输入数据体边缘处填补特定元素的做法,称为填充
    • 作用:控制经过卷积运算后的输出结果的尺寸大小
    • 常用:使用0元素进行填充,称为0填充
    • 例子:layer 0的输入数据是\(416*416\),经过转接后输出尺寸依然是\(416*416\),因为内部有填充操作,在四周填1行0,变成\(418*418\)
  • 实现:使用FIFO存储器

    • FIFO:First In First Out,先进先出的存储器

    • 优点:没有历史,只需一个读使能信号,可以使用fast water模式

      • 使用FIFO作为存储器,因为FIFO没有历史,可以直接给一个读使能信号就可以读取数据
      • FIFO的模式使用FESTWORD模式,可以保持与输入数据对齐
    • 参数:位宽64比特,深度4096(根据板子资源和输入数据尺寸确定)

      • FIFO的位宽设置为64比特,与输入数据一致,可以同时存储8个通道
      • FIFO的深度可以根据板子上的资源情况自己定,可以分批发送和缓存数据,避免资源浪费或不足
    • 缓存模块的实现就是操作FIFO,根据输入数据的valid信号和读使能信号来控制写入和读出

  • 操作:接收DMA发送的64比特数据(8个通道),写入FIFO,然后根据读使能信号读出8个通道的数据

填充模块设计及代码编写

  • 填充模块的功能

    • 对卷积层的输入数据进行填充操作,即在数据的四周加上一圈0,使得数据尺寸增加
    • 例如,将416416的数据填充为418418的数据
  • 填充模块的设计思路

    • 以第一层卷积层(layer 0)的输入数据为例,图像尺寸为 416 x 416,通道数为 3

    • 在图像四周加上一圈 0,使其变成 418 x 418

    • 在填充之前,先将输入数据缓存到 FIFO中

    • 在填充时,从 FIFO中读取数据,并根据不同的位置类型进行填充

    • FIFO的深度设定为496,所以不能一次性缓存所有的数据,需要分批次进行

    • 例如,416/496约等于9,所以每次缓存9行数据,然后PS端发送给PL端

    • 填充操作根据发送过来的数据的不同位置进行,分为三种情况:

      • 包含第一行数据:在第一行之前加上一行0,在左右两边加上0
      • 包含最后一行数据:在最后一行之后加上一行0,在左右两边加上0
      • 中间位置数据:只在左右两边加上0
    • PS端通过寄存器告诉PL端以下信息:

      • set type:当前层分批发送数据的位置类型,9-10位表示0(包含第一行),1(中间位置),2(包含最后一行)

      • col select:当前层数据的列数量,有6种情况(416,208,104,52,11,13)

      • row:当前发送的这批数据的行数量,0表示1行,63表示64行

        • 如果包含第一行数据,要在第一行之前再加上一行 0,并在左右两边加 0
        • 如果包含中间位置数据,只要在每行的首尾加上 0
        • 如果包含最后一行数据,要在最后一行之后再加上一行 0
        • 不会出现既包含第一行又包含最后一行的情况
        • 根据寄存器中的 set type 来判断位置类型(0 表示包含第一行,1 表示中间位置,2 表示包含最后一行)
        • 根据寄存器中的 col select 和 row 来获取当前缓存的列数和行数
  • 填充模块的代码编写

    • 定义输入输出信号和参数

      • 输入信号包括
        • padding_start:外部信号,表示何时开始执行填充操作(表示开始填充)
        • set_type:寄存器信号,表示当前层的位置类型(0:包含第一行,1:中间位置,2:包含最后一行)
        • col_select:寄存器信号,表示当前层的列数量(0:416列,1:208列,2:104列,3:52列,4:26列)(表示列选择)
        • row:寄存器信号,表示当前发送的批次数据的行数量(0~63)(表示行数)
        • data_in:输入信号,表示从PS端发送过来的原始数据(64位)(表示输入数据)
      • 输出信号包括
        • padding_data:输出信号,表示经过填充后的数据(64位)(表示填充后的数据)
        • padding_data_valid:输出信号,表示填充后的数据是否有效(表示数据有效标志)
        • buffer_rd_en:输出信号,表示FIFO缓存的读使能信号(表示读使能信号)
      • 参数包括 options(表示可选配置)
    • 定义内部变量和逻辑

      • 定义 padding_work(表示填充工作标志),当 padding_start 高时拉高,当填充完成时拉低

        • 当padding_work为高时:
          • col_cnt从0开始计数,每个时钟周期加1,直到col_max(根据col_select转换得到),然后清零并使row_cnt加1
          • row_cnt从0开始计数,直到row_max(根据set_type和feature_row转换得到)
      • 定义 col_cnt:内部计数器信号,表示当前处理到第几列(0~417)和 row_cnt:内部计数器信号,表示当前处理到第几行(0~9)或 (0~10)(表示列计数器和行计数器),用于记录当前填充的位置和数量

      • row_cnt:行计数器信号,范围根据set_type而定:

        • 如果set_type为1(中间位置),则范围与row相同
        • 如果set_type为0或2(包含第一行或最后一行),则范围比row多1
        • 每次padding_work为高且col_cnt等于col_select减1时自增1,在padding_work为低时清零
      • col_max:列最大值信号,根据col_select转换而来:

        • 如果col_select为0,则col_max为418(416+2)
        • 如果col_select为1,则col_max为210(208+2)
        • 如果col_select为2,则col_max为106(104+2)
        • 如果col_select为3,则col_max为54(52+2)
        • 如果col_select为4,则col_max为18(11+7)
        • 如果col_select为5,则col_max为15(13+2)
      • buffer_rden:FIFO读使能信号,根据set_type,row_cnt和col_cnt而定:

        • 如果set_type为0(包含第一行),则在row_cnt大于等于1且col_cnt大于等于1且小于col_max减1时拉高
        • 如果set_type为1(中间位置),则在padding_work为高且col_cnt大于等于0且小于col_max减1时拉高
        • 如果set_type为2(包含最后一行),则在row_cnt小于等于row且col_cnt大于等于1且小于col_max减1时拉高
        • 其他情况下拉低
      • padding_data:输出数据信号,根据buffer_rden而定:

        • 如果buffer_rden为高,则赋值为FIFO读出的数据
        • 如果buffer_rden为低,则赋值为0
      • padding_data_valid:输出数据有效标志信号,直接赋值为padding_work信号

      三行同列模块输出设计及代码编写

  • 如何在 FPGA 中实现三行同列输出模块,即在执行 3x3 卷积计算时,对输入的特征图数据转换为同时输出相邻三行的同列数据。

  • 主要内容可以分为以下几个部分:

三行同列输出模块的功能和思路

  • 三行同列输出模块的功能是在执行 3x3 卷积计算时,对输入的特征图数据转换为同时输出相邻三行的同列数据。
  • 3x3 卷积计算是指对输入的特征图数据,按照相邻的 3x3 的区域,与对应的 3x3 的卷积参数(权重)进行逐元素相乘后再求和,得到输出的特征图数据。
  • 在 FPGA 中实现 3x3 卷积计算的一个大的思路是:
    • 在 FPGA 内部做一个权重缓存,用于存储 3x3 的卷积参数。
    • 在 FPGA 内部做一个图像矩阵构造器,用于将输入的特征图数据转换为 3x3 的图像矩阵。
    • 将权重缓存和图像矩阵构造器的输出进行逐元素相乘后再求和,得到输出的特征图数据。
  • 图像矩阵构造器就是三行同列输出模块要实现的功能,它需要保证输入的特征图数据能够同时出现相邻三行、相邻三列的九个数据。

三行同列输出模块的实现方法

  • 在 FPGA 中实现三行同列输出模块的一个方法是:

    • 三行同列输出模块的功能和原理

      • 在 FPGA 里面实现 3*3 卷积计算的思路是利用权重缓存和输入图像矩阵两个模块,将输入数据和权重参数进行相乘和相加的操作。
      • 输入图像矩阵的作用是构造一个 3*3 的矩阵,让输入数据能同时出现相邻三行、相邻三列的九个数据。
      • 三行同列输出模块的功能就是让输入数据流能够同时输出相邻三行的同一列数据,然后再结合之前缓存的前两列数据,形成一个 3*3 的矩阵。
      • 三行同列输出模块的原理是利用两个 FIFO(先进先出)存储器,将输入数据流分别存入 FIFO A 和 FIFO B,并同时读出 FIFO A 的第一行数据和 FIFO B 的第一行和第二行数据,从而实现同时输出三行同列数据的效果。
    • 使用两个 FIFO(先进先出)存储器,分别命名为 FIFO A 和 FIFO B。

    • 当输入数据流过来第一行数据时,将第一行数据存入 FIFO A。

    • 当输入数据流过来第二行数据时,将第二行数据存入 FIFO A,并将 FIFO A 中存储的第一行数据读出来存入 FIFO B。

    • 当输入数据流过来第三行数据时,将第三行数据存入 FIFO A,并将 FIFO A 中存储的第二行数据读出来存入 FIFO B,并将 FIFO B 中存储的第一行数据读出来。

    • 这样就可以保证同时输出相邻三行的同一列数据,例如:00、10、20;01、11、21;02、12、22等等。

    • 然后再使用一个缓存器,用于缓存前两列数据,并在第三列数据出来时与缓存器中的前两列数据组合成一个 3x3 的图像矩阵。

  • 这种方法有一个缺点,就是 FIFO 的长度是固定的,不能根据不同层次的特征图尺寸进行变化。而在 YOLO 这个项目中,不同层次的特征图尺寸是不一样的,例如:layer 0 里面一行有 416 个数据,layer 2 里面一行有 208 个数据,layer 6 里面一行有 52 个数据。

  • FIFO 存储器的局限性和替代方案

    • FIFO 存储器的长度是固定的,不能根据不同层的输入数据尺寸进行变化。而在 YOLO 这个项目里面,不同层的输入数据一行里面的数据量是不一样的,从 416 到 52 不等。
    • 因此,在 YOLO 这个项目里面,不能直接使用 FIFO 存储器,而要使用一种长度可变的移位寄存器来实现类似 FIFO 的功能。
    • 长度可变的移位寄存器是自定义一个存储器,它可以根据不同层的输入数据长度来选择从哪个寄存器里面直接输出数据。例如,在 layer 0 里面,它会从第 417 个寄存器里面输出数据;在 layer 2 里面,它会从第 209 个寄存器里面输出数据。
    • 长度可变的移位寄存器的原理是利用两个移位寄存器数组(shift reg array),将输入数据流分别存入 shift reg array A 和 shift reg array B,并同时读出 shift reg array A 的第一行数据和 shift reg array B 的第一行和第二行数据,从而实现同时输出三行同列数据的效果。
  • 因此,需要使用另一种方法,就是使用两个长度可变的移位寄存器,分别命名为 Shift Reg A 和 Shift Reg B。

    • 使用 Verilog 的语法,可以定义一个长度可变的移位寄存器,例如:reg [63:0] shift_reg_a [417:0],表示一个深度为 418,位宽为 64 的移位寄存器。
    • 当输入数据流过来第一行数据时,将第一行数据存入 Shift Reg A 的第一个位置(shift_reg_a[0])。
    • 当输入数据流过来第二行数据时,将第二行数据存入 Shift Reg A 的第一个位置(shift_reg_a[0]),并将 Shift Reg A 中存储的第一行数据移位到第二个位置(shift_reg_a[1])。
    • 当输入数据流过来第三行数据时,将第三行数据存入 Shift Reg A 的第一个位置(shift_reg_a[0]),并将 Shift Reg A 中存储的第二行数据移位到第二个位置(shift_reg_a[1]),并将 Shift Reg A 中存储的第一行数据移位到第三个位置(shift_reg_a[2])。
    • 这样就可以保证同时输出相邻三行的同一列数据,例如:shift_reg_a[2][0]、shift_reg_a[1][0]、shift_reg_a[0][0];shift_reg_a[2][1]、shift_reg_a[1][1]、shift_reg_a[0][1];shift_reg_a[2][2]、shift_reg_a[1][2]、shift_reg_a[0][2]等等。
    • 然后再使用一个缓存器,用于缓存前两列数据,并在第三列数据出来时与缓存器中的前两列数据组合成一个 3x3 的图像矩阵。
  • 这种方法的优点是可以根据不同层次的特征图尺寸进行变化,只需要指定不同的移位寄存器的输出位置即可。例如:对于 layer 0,输出位置是 shift_reg_a[417][63:0];对于 layer 2,输出位置是 shift_reg_a[209][63:0];对于 layer 6,输出位置是 shift_reg_a[53][63:0]。

  • 输出有效标志信号的生成

    • 输出有效标志信号(line valid signal)是用来表示相邻三行同列数据是否构造好了。当构造好了时,该信号会拉高,表示可以进行卷积计算了。
    • 输出有效标志信号的生成是利用一个计数器(row cnt),来判断输入数据流是否已经过来了第三行数据。当过来了第三行数据时,该信号就会与输入有效标志信号(data in valid signal)保持对齐,并拉高。
    • 计数器可以直接使用之前填充模块(padding module)里面已经定义好的计数器,以节约资源。

模块功能

  • 在执行 \(3 \times 3\) 卷积计算时
    • 对输入特征图转换为同时输出相邻三行同列九个元素
    • 构造 \(3 \times 3\) 输入图像矩阵
    • \(3 \times 3\) 权重缓存中的参数进行相乘和相加

模块原理

  • 利用两个 FIFO 存储器或两个移位寄存器数组
    • 将输入数据流分别存入 FIFO A 或 shift reg array A
    • 同时读出 FIFO A 或 shift reg array A 的第一行数据
    • 并存入 FIFO B 或 shift reg array B
    • 同时读出 FIFO B 或 shift reg array B 的第一行和第二行数据
    • 从而实现同时输出三行同列数据的效果

模块代码

  • 定义输入输出端口和内部信号
    • 输入端口:data_in(64位)、data_in_valid(1位)、layer_select(3位)
    • 输出端口:line0_data(64位)、line1_data(64位)、line2_data(64位)、line_valid(1位)
    • 内部信号:row_cnt(7位)、shift_reg_array0(64位×418)、shift_reg_array1(64位×418)
  • 定义两个移位寄存器数组
    • 使用 reg [63:0] shift_reg_array0 [417:0] 和 reg [63:0] shift_reg_array1 [417:0] 来定义两个移位寄存器数组,每个数组有 418 个 64 位的寄存器,可以根据不同层的输入数据长度来选择输出数据的位置。
  • 实现数据移位和传输的逻辑
    • 使用 always @(posedge clk) 语句来描述在时钟上升沿时发生的事件,如数据的读写、移位、输出等。
    • 使用 case (layer_select) 语句来根据不同层的选择信号来决定输出数据的位置,如 layer 0 对应 417,layer 2 对应 209 等。
    • 使用 for (i = 0; i <= 417; i = i + 1) 循环语句来实现数据在移位寄存器数组中的移动,如 shift_reg_array0[i+1] <= shift_reg_array0[i] 等。
  • 生成输出有效标志信号
    • 使用 assign line_valid = (row_cnt >= 2) ? data_in_valid : 1'b0; 语句来根据输入有效标志信号和行计数器的值来生成输出有效标志信号,当行计数器大于等于 2 时,表示已经过来了第三行数据,此时输出有效标志信号与输入有效标志信号保持一致,否则输出无效标志信号为低电平。