对已有YOLO加速模块进行Layer0仿真(Layer0仿真平台搭建)

发布时间 2023-06-20 20:41:34作者: 李白的白

Layer0仿真平台搭建

代码概述

image

  • 代码功能

    • 实现一个卷积神经网络的第一层(layer0)的计算
    • 将偏置、激活函数、权重和特征数据从内存中读取出来,并通过AXI总线接口发送给卷积计算模块
  • 包括一个顶层模块layer0_sim和四个子模块layer0_bias_txlayer0_leakyrelu_txlayer0_weight_txlayer0_feature_tx

  • 顶层模块layer0_sim的主要功能是根据不同的状态,将子模块的输出数据通过AXI4-Stream接口发送给下一级模块,并将一些寄存器的值输出给Lite-Reg接口。

    graph TD A[S_IDLE] -->|task_finish == 1| B[S_BIAS_TX] B -->|task_finish == 1| C[S_LEAKYRELU_TX] C -->|task_finish == 1| D[S_WEIGHT_TX] D -->|task_finish == 1| E[S_FEATURE_TX] E -->|task_finish == 1| F[S_CONV_CAL] F -->|task_finish == 1| G[S_FINISH]
    - 复位状态:state = S_IDLE, 所有寄存器清零(空闲状态,等待开始信号)
    - s_rst_n = 1: state = S_BIAS_TX, 偏置数据传输状态,将偏置数据发送到m_axis_mm2s接口
    - task_finish = 1: state = S_LEAKYRELU_TX, 激活函数数据传输状态,将激活函数参数发送到m_axis_mm2s接口
    - task_finish = 1: state = S_WEIGHT_TX, 权重数据传输状态,将权重数据发送到m_axis_mm2s接口
    - task_finish = 1: state = S_FEATURE_TX, 特征图数据传输状态,将特征图数据发送到m_axis_mm2s接口
    - task_finish = 1: state = S_CONV_CAL, 卷积计算状态,调用conv_mult_dsp模块进行卷积核与特征图的乘加运算
    - task_finish = 1: state = S_FINISH, 第一层结束(完成状态,等待复位信号)
    

image

  • 代码分为两个部分:tb_top模块和layer0_sim模块。tb_top模块是测试平台,用于产生时钟信号sclk和复位信号s_rst_n,并实例化layer0_sim模块。layer0_sim模块是仿真平台,用于将偏置、激活函数、权重和特征数据从内存中读取出来,并通过AXI总线接口m_axis_mm2s发送给卷积计算模块。

  • layer0_sim: 仿真平台,用于测试卷积神经网络的第一层的计算。

    • 定义了七个状态,分别对应空闲、偏置值传输、激活函数传输、权重传输、特征图传输、卷积计算和完成。
    • 根据task_finish信号判断是否完成当前状态的任务,并转移到下一个状态。
    • 根据当前状态选择不同的子模块输出作为m_axis_mm2s信号,并将其发送给下一级模块。
    • 输出Lite-Reg信号作为调试信息。
    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • s_rst_n: 系统复位信号,低电平有效
      • m_axis_mm2s_tdata: 64位数据输出信号,用于传输偏置、激活函数、权重和特征数据
      • m_axis_mm2s_tkeep: 8位数据保持输出信号,用于指示有效的字节
      • m_axis_mm2s_tvalid: 数据有效输出信号,高电平表示有有效数据
      • m_axis_mm2s_tready: 数据就绪输入信号,高电平表示可以接收数据
      • m_axis_mm2s_tlast: 数据结束输出信号,高电平表示最后一个数据
      • slave_lite_reg0~3: 32位寄存器输出信号,用于存储一些状态或配置信息
      • task_finish: 任务完成输入信号,高电平表示当前阶段的计算完成
  • layer0_bias_tx: 偏置传输模块,用于从内部数组中读取偏置数据,并通过m_axis_mm2s_tdata等信号输出。

    • 定义了一个数组存储偏置值数据,并使用索引来访问数组元素。
    • 每个时钟周期输出两个偏置值数据,并将其拼接成64位的信号。
    • 当索引达到最大值时,输出last信号,并复位valid信号。

    从一个固定的数组中读取8个偏置值,每次输出两个偏置值,共4个时钟周期

    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • s_rst_n: 系统复位信号,低电平有效
      • bias_data: 64位偏置数据输出信号,每次输出两个8位的偏置值
      • bias_valid: 偏置数据有效输出信号,高电平表示有有效数据
      • bias_last: 偏置数据结束输出信号,高电平表示最后一个数据
      • ready: 数据就绪输入信号,高电平表示可以接收数据

image

  • layer0_leakyrelu_tx: 激活函数传输模块,用于从文件中读取激活函数参数,并通过m_axis_mm2s_tdata等信号输出。

    • 使用$readmemh函数从文本文件中读取激活函数数据,并存储在一个数组中。
    • 每个时钟周期输出一个激活函数数据,并将其扩展成64位的信号。
    • 当索引达到最大值时,输出last信号,并复位valid信号。

    从一个文本文件中读取128个激活函数值,每次输出一个激活函数值,共128个时钟周期

    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • s_rst_n: 系统复位信号,低电平有效
      • leakyrelu_data: 64位激活函数参数输出信号,每次输出八个8位的参数值
      • leakyrelu_valid: 激活函数参数有效输出信号,高电平表示有有效数据
      • leakyrelu_last: 激活函数参数结束输出信号,高电平表示最后一个数据
      • ready: 数据就绪输入信号,高电平表示可以接收数据
  • layer0_weight_tx: 权重传输模块,用于从文件中读取权重数据,并通过m_axis_mm2s_tdata等信号输出。

    • 使用$readmemh函数从文本文件中读取权重数据,并存储在八个数组中,分别对应八个通道。
    • 每个时钟周期输出八个权重数据,并将其拼接成64位的信号。
    • 当索引达到最大值时,输出last信号,并复位valid信号。

    从8个文本文件中读取144个权重值,每次输出8个权重值,共18个时钟周期

    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • s_rst_n: 系统复位信号,低电平有效
      • weight_data: 64位权重数据输出信号,每次输出八个8位的权重值
      • weight_valid: 权重数据有效输出信号,高电平表示有有效数据
      • weight_last: 权重数据结束输出信号,高电平表示最后一个数据
      • ready: 数据就绪输入信号,高电平表示可以接收数据
  • layer0_feature_tx: 特征传输模块,用于从文件中读取特征数据,并通过m_axis_mm2s_tdata等信号输出。

    • 使用$readmemh函数从文本文件中读取特征图数据,并存储在三个数组中,分别对应三个通道(RGB)。
    • 每个时钟周期输出三个特征图数据,并将其拼接成64位的信号,高位补零。
    • 当索引达到最大值时,输出last信号,并复位valid信号。

    从3个文本文件中读取37632个特征值,每次输出3个特征值,共12544个时钟周期

    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • s_rst_n: 系统复位信号,低电平有效
      • feature_data: 64位特征数据输出信号,每次输出三个8位的特征值,分别对应RGB三个通道
      • feature_valid: 特征数据有效输出信号,高电平表示有有效数据
      • feature_last: 特征数据结束输出信号,高电平表示最后一个数据
      • ready: 数据就绪输入信号,高电平表示可以接收数据
  • conv_mult_dsp: 卷积乘法模块,用于实现卷积运算的核心部分。

    • 使用DSP48E1原语实现乘法器功能。
    • 输入三个8位有符号数(data_a, data_b, data_d),并将其扩展成18位有符号数。
    • 输出两个16位有符号数(data_ab, data_db),分别为data_a和data_b的乘积,以及data_d和data_b的乘积。
    • 使用AxB和AxC两个乘法器管脚,并配置相应的参数。
    • 使用CE管脚作为时钟使能信号,使得乘法器只在时钟上升沿有效。
    • 它有以下输入和输出信号:
      • sclk: 系统时钟信号
      • data_a: 8位输入数据A
      • data_b: 8位输入数据B
      • data_d: 8位输入数据D
      • data_ab: 16位输出数据AB,表示A和B的乘积
      • data_db: 16位输出数据DB,表示D和B的乘积
代码时序
  • 使用了一个时钟信号sclk和一个复位信号s_rst_n来控制各个模块的时序。

  • 当复位信号为低电平时,所有的寄存器和状态机都被初始化为零或空闲状态。

  • 当复位信号为高电平时,顶层模块的状态机根据任务完成信号task_finish来切换不同的状态,分别为:

    • S_IDLE: 空闲状态,等待复位结束后进入下一个状态。
    • S_BIAS_TX: 偏置值传输状态,调用子模块layer0_bias_tx来发送偏置值数据流,当发送完成后进入下一个状态。
    • S_LEAKYRELU_TX: 激活函数参数传输状态,调用子模块layer0_leakyrelu_tx来发送激活函数参数数据流,当发送完成后进入下一个状态。
    • S_WEIGHT_TX: 权重值传输状态,调用子模块layer0_weight_tx来发送权重值数据流,当发送完成后进入下一个状态。
    • S_FEATURE_TX: 特征图数据传输状态,调用子模块layer0_feature_tx来发送特征图数据流,当发送完成后进入下一个状态。
    • S_CONV_CAL: 卷积计算状态,等待下一级模块完成卷积计算,并将结果返回给Lite-Reg接口,当计算完成后进入下一个状态。
    • S_FINISH: 完成状态,保持在此状态直到复位信号为低电平。
  • 子模块的时序主要由两个信号控制:有效信号(例如bias_valid)和就绪信号(例如ready)。有效信号表示当前输出数据是否有效,就绪信号表示下一级模块是否准备好接收数据。当有效信号和就绪信号都为高电平时,表示数据传输成功,并且索引值加一。当索引值达到最大值时,表示数据传输结束,并且最后信号(例如bias_last)为高电平。

代码设计思路
  • - 分为两个部分:tb_top模块和layer0_sim模块
    - tb_top模块是测试平台,用于产生时钟信号sclk和复位信号s_rst_n,并实例化layer0_sim模块
    - layer0_sim模块是仿真平台,用于实现FSM和子模块的逻辑
    - 子模块分别对应偏置、激活函数、权重和特征数据的读取和发送
    - 使用$readmemh函数来从文本文件中读取数据并存储到内存中
    - 使用{}符号来将多个8位的数据连接起来,并按照高位在前低位在后的顺序排列
    - 使用localparam来定义不同数据的长度,并根据index和数据长度的关系来生成m_axis_mm2s_tvalid和m_axis_mm2s_tlast信号
    
  • 子模块来实现

    • 第一步是输出偏置数据(layer0_bias_tx),该数据是一个固定的数组,存储在寄存器中。该模块根据索引值(index)从数组中读取两个偏置值,并拼接成64位的数据输出到AXI总线。同时,该模块还输出有效信号(bias_valid)和最后一个数据信号(bias_last)来控制AXI总线的传输。

    • 第二步是输出激活函数数据(layer0_leakyrelu_tx),该数据是从文件中读取的(layer0_leakyrelu.txt)。该模块根据索引值(index)从文件中读取八个激活函数值,并拼接成64位的数据输出到AXI总线。同时,该模块还输出有效信号(leakyrelu_valid)和最后一个数据信号(leakyrelu_last)来控制AXI总线的传输。

    • 第三步是输出权重数据(layer0_weight_tx),该数据是从文件中读取的(layer0_ch0.txt, layer0_ch1.txt, ..., layer0_ch7.txt)。该模块根据索引值(index)从文件中读取八个权重值,并拼接成64位的数据输出到AXI总线。同时,该模块还输出有效信号(weight_valid)和最后一个数据信号(weight_last)来控制AXI总线的传输。

    • 第四步是输出特征数据(layer0_feature_tx),该数据是从文件中读取的(r_data.txt, g_data.txt, b_data.txt)。该模块根据索引值(index)从文件中读取三个特征值,并拼接成64位的数据输出到AXI总线。同时,该模块还输出有效信号(feature_valid)和最后一个数据信号(feature_last)来控制AXI总线的传输。

    • 第五步是进行乘法运算(conv_mult_dsp),该模块使用DSP48E1原语来实现两个8位有符号数的乘法,并输出16位有符号数的结果。该模块使用data_a, data_b, data_d作为输入,使用data_ab, data_db作为输出。

名词解释

  • AXI4-Stream协议是一种用于传输数据流的标准接口,它只需要四个信号:数据信号(例如m_axis_mm2s_tdata),有效信号(例如m_axis_mm2s_tvalid),就绪信号(例如m_axis_mm2s_tready)和最后信号(例如m_axis_mm2s_tlast)。这种接口可以简化数据传输的控制逻辑,提高数据传输的效率和灵活性。
  • 有限状态机(FSM)是一种用于描述系统行为的模型,它由一组状态(例如S_IDLE),一组输入(例如task_finish),一组输出(例如m_axis_mm2s_tdata)和一组状态转移规则(例如if(task_finish == 1'b1) state <= S_LEAKYRELU_TX;)组成。这种模型可以清晰地表示系统的不同工作状态和状态之间的转换条件,便于系统的设计和调试。
代码遇到问题解决方法
  • 问题一:如何从文本文件中读取数据并存储到内存中?
  • 解决方法:使用$readmemh函数来从十六进制格式的文本文件中读取数据,并将其赋值给相应的寄存器数组。例如:
initial $readmemh("./layer0_txt/r_data.txt", ch0_data_arr);
  • 问题二:如何将8位的数据拼接成64位的数据?
  • 解决方法:使用{}符号来将多个8位的数据连接起来,并按照高位在前低位在后的顺序排列。例如:
assign  weight_data  =       {ch7_data_arr[index],
                              ch6_data_arr[index],
                              ch5_data_arr[index], 
                              ch4_data_arr[index],
                              ch3_data_arr[index],
                              ch2_data_arr[index],
                              ch1_data_arr[index],
                              ch0_data_arr[index]};
  • 问题三:如何根据不同的数据长度来生成m_axis_mm2s_tvalid和m_axis_mm2s_tlast信号?
  • 解决方法:使用localparam来定义不同数据的长度,并根据index和数据长度的关系来生成m_axis_mm2s_tvalid和m_axis_mm2s_tlast信号。例如:
localparam      INDEX_END       =       'd32;

assign  leakyrelu_last  =       (index == (INDEX_END-1)) ? 1'b1 : 1'b0;

always  @(posedge sclk or negedge s_rst_n) begin
        if(s_rst_n == 1'b0)
                leakyrelu_valid      <=      1'b0;
        else if(index < (INDEX_END-1))
                leakyrelu_valid      <=      1'b1;
        else
                leakyrelu_valid      <=      1'b0;
end

其他问题

  • 数据溢出:由于数据的位宽和符号位可能不一致,导致数据在运算或传输过程中发生溢出,造成数据失真或错误。解决方法是在数据定义和赋值时,注意保持数据的位宽和符号位一致,或者使用适当的截断或扩展操作来避免溢出。
  • 数据对齐:由于数据的位宽和顺序可能不匹配,导致数据在传输或接收过程中发生错位,造成数据失真或错误。解决方法是在数据定义和赋值时,注意保持数据的位宽和顺序匹配,或者使用适当的拼接或分割操作来实现对齐。
  • 数据同步:由于数据的时钟域和速率可能不一致,导致数据在传输或接收过程中发生延迟或丢失,造成数据失真或错误。解决方法是在数据定义和赋值时,注意保持数据的时钟域和速率一致,或者使用适当的同步或握手机制来实现同步。