PS端上板验证Layer0计算结果

发布时间 2023-07-20 23:40:07作者: 李白的白

PS端上板验证Layer0计算结果

目的

- 目的:使用搭建好的上板验证工程,在PS端增加代码,对YOLO网络的第一层(Layer0)的计算结果进行验证
- 前提:SD卡中存储了YOLO网络的参数文件和输入图像数据的bin文件,这些文件需要符合上板验证工程的存储结构要求

- Layer0是一个卷积层,输入是8通道416x416的图像数据(其中5通道为0),输出是16通道208x208的特征图
- 其他层可以参考Layer0的控制方式和代码结构,完成相应的计算和验证
  • 验证YOLO网络的第一层卷积运算是否正确
  • 使用之前搭建好的上板验证工程
  • 在PS端编写代码控制PL端和DMA
  • 读取SD卡中的参数文件和输入数据文件
  • 对比PL端输出结果和Python仿真结果

准备工程

  • 修改Python代码生成适合上板验证的bin文件

    这些bin文件需要符合上板工程的要求,即每个字节对应一个通道的数据,而不是同一个通道内相邻位置像素点的数据

    例如:

    第一个字节存储第一个通道的第一行第一个像素点,

    第二个字节存储第二个通道的第一行第一个像素点,

    依次类推,

    直到第八个字节存储第八个通道的第一行第一个像素点,

    然后第九个字节存储第一个通道的第一行第二个像素点,

    以此类推

    • 图像数据:增加5个通道的全0数据,使得总通道数为8,调整存储顺序,使得每8个字节对应8个通道的同一位置的像素值

    • 权重数据:增加填充数据,使得输入通道数为8的倍数,调整存储顺序,使得每8个字节对应8个通道的同一位置的权重值

      如果输入通道数或权重通道数不是8的整数倍,需要在后面补零,使其成为8的整数倍

    • 偏置数据和激活数据:不需要修改

  • 将生成的bin文件拷贝到SD卡中

    • image_data.bin:存储输入图像数据,由原始的3通道图像数据扩展为8通道,后5个通道全为0
    • l0_b.bin:第一层偏置数据(bias),由原始的16个32位值组成
    • l0_r.bin:第一层激活数据(relu),由原始的256个8位值组成
    • l0_w.bin:第一层权重数据(weight),由原始的3x3x3x16个8位值扩展为3x3x8x16个8位值,后5个通道全为0
  • 修改后的Python代码命名为quantize_print_v2.py,生成的bin文件存放在SD_BIN_V2文件夹中

创建Vitis工程

  • 在Vivado中导出硬件信息文件(.xsa),并将其复制到Vitis文件夹下

  • 在Vitis中创建一个应用工程,选择hello world模板,并导入硬件信息文件(.xsa)

  • 在src文件夹下创建一个main.c文件,用来编写PS端的代码

  • 编写PS端代码,包括以下功能:

    • SD卡读写:使用ff.h库文件,定义SD卡读写函数,将bin文件中的数据读取到DDR3内存中

      按照以下顺序读取bin文件:image_data.bin, l0_b.bin, l0_r.bin, l0_w.bin

    • AXI Lite寄存器写:使用xil_io.h库文件,定义AXI Lite寄存器写函数,向PL端发送命令和控制信号

      按照以下顺序写入寄存器:l0_b, l0_r, l0_w, image_data, conv_start, read_start

    • DMA读写:使用XAxiDma库函数初始化DMA,发送数据给PL端或接收数据从PL端,设置中断回调函数

      先发送BIAS数据,然后发送激活数据,然后发送权重数据,然后发送图像数据。每次发送数据之前,需要先设置DMA的配置信息,包括源地址,目标地址,数据长度等。每次发送数据之后,需要等待PL端完成计算,并通过中断信号通知PS端。然后需要接收PL端计算完成后的结果数据,并存储到DDR3内存中

    • 中断响应:使用XScuGic库函数初始化中断控制器,并注册一个中断处理函数,等待PL端完成运算后触发中断,清除中断标志

      当PL端完成计算后,会向PS端发送一个中断信号,PS端在接收到中断信号后,会调用中断处理函数,在中断处理函数中,可以做一些必要的操作,例如清除中断标志位,打印一些信息等

  • PS端代码流程

    • 初始化SD卡、DMA和中断模块

      先初始化中断控制器和处理器,然后再初始化SD卡和DMA

    • 依次读取SD卡中的image_data.bin、l0_b.bin、l0_r.bin和l0_w.bin文件,并将其存储到DDR3内存中不同的地址

      • image_data.bin:0x10000000
      • l0_b.bin:0x20000000
      • l0_r.bin:0x20000040
      • l0_w.bin:0x20000200
    • 判断文件是否存在,如果不存在,打印错误信息并退出程序

    • 将BIAS数据类型(4)和数据长度(64)转换为16进制格式,使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),向PL端发送BIAS数据类型和数据长度

      先写入数据长度,再写入数据类型,因为数据类型的寄存器地址比数据长度的寄存器地址高4个字节

    • 配置DMA的控制寄存器,设置传输方向为PS到PL,传输长度为64字节,启动位为1,使用Xil_DmaSimpleTransfer函数启动DMA传输,向PL端发送BIAS数据(从DDR3地址0x20000000开始)

    • 使用Xil_DmaIsBusy函数检查DMA是否完成传输,如果完成,继续执行下一步

    • 等待PL端发出中断信号(通过InterruptHandler函数),表示接收完成

    • 将ReLU数据类型(31)和数据长度(256)转换为16进制格式,使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),向PL端发送ReLU数据类型和数据长度

      同样需要先写入数据长度,再写入数据类型

    • 配置DMA的控制寄存器,设置传输方向为PS到PL,传输长度为256字节,启动位为1,使用Xil_DmaSimpleTransfer函数启动DMA传输,向PL端发送ReLU数据(从DDR3地址0x20000040开始)

    • 使用Xil_DmaIsBusy函数检查DMA是否完成传输,如果完成,继续执行下一步

    • 等待PL端发出中断信号(通过InterruptHandler函数),表示接收完成

    • 将权重数据类型(11)和数据长度(1152)转换为16进制格式,使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),向PL端发送权重数据类型和数据长度

      同样需要先写入数据长度,再写入数据类型

    • 配置DMA的控制寄存器,设置传输方向为PS到PL,传输长度为1152字节,启动位为1,使用Xil_DmaSimpleTransfer函数启动DMA传输,向PL端发送权重数据(从DDR3地址0x20000200开始)

    • 使用Xil_DmaIsBusy函数检查DMA是否完成传输,如果完成,继续执行下一步

    • 等待PL端发出中断信号(通过InterruptHandler函数),表示接收完成

    • 循环发送图像数据:

      • 根据当前发送的批次和组数来设置Batch Type和Set Type的值

        需要注意Batch Type和Set Type是两个不同的寄存器地址,并且Batch Type是一个4位的值,而Set Type是一个8位的值

        • Batch Type:表示当前发送的是第几批图像数据(每批9行)
        • Set Type:表示当前发送的是第几组图像数据(每组416个像素点)
        • Batch Type和Set Type都是从0开始计数,并且每次发送后自增1
        • Batch Type和Set Type都是4位二进制数,并且分别占据命令字节的第7~10位 和 第11~14位
      • 将图像数据类型(0)、卷积类型(0)、Batch Type、Set Type、Code Select(0)和数据长度(29952)转换为16进制格式,使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),向PL端发送图像数据类型和数据长度

        需要注意数据长度的寄存器地址比数据类型的寄存器地址高4个字节,并且Batch Type和Set Type的寄存器地址比数据类型的寄存器地址高8个字节

        • 图像数据类型:占据命令字节的第1~4位,表示发送的是图像数据
        • 卷积类型:占据命令字节的第5~6位,表示发送的是普通卷积
        • Code Select:占据命令字节的第15~16位,表示不选择任何特殊功能
        • 数据长度:占据命令字节的第17~32位,表示发送的图像数据的字节数
      • 配置DMA的控制寄存器,设置传输方向为PS到PL,传输长度为29952字节,启动位为1,使用Xil_DmaSimpleTransfer函数启动DMA传输,向PL端发送图像数据(从DDR3地址0x10000000开始)

      • 使用Xil_DmaIsBusy函数检查DMA是否完成传输,如果完成,继续执行下一步

      • 等待PL端发出中断信号(通过InterruptHandler函数),表示接收完成

    • 循环结束后:

      • 根据当前层的卷积类型来设置Code Select的值

        需要注意Code Select是一个4位的值,并且它的寄存器地址比数据类型的寄存器地址高12个字节

        • Code Select:占据命令字节的第15~16位,表示选择特殊功能
        • 如果是普通卷积,Code Select为0
        • 如果是深度可分离卷积,Code Select为1
        • 如果是点积卷积,Code Select为2
      • 将卷积计算命令(494)转换为16进制格式,并使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),并等待PL端完成卷积计算后发出的中断信号。

        需要注意卷积计算命令是一个16位的值,并且它的寄存器地址比数据类型的寄存器地址高16个字节

        • 卷积计算命令:将命令字节的第2位设为1,表示开始卷积计算
      • 等待PL端发出中断信号(通过InterruptHandler函数),表示卷积计算完成

      • 将读取输出数据命令(2)转换为16进制格式,并使用Xil_Out32函数写入AXI Lite寄存器地址(RAG0),向PL端发送读取输出数据命令

        需要注意读取输出数据命令是一个4位的值,并且它的寄存器地址比数据类型的寄存器地址高20个字节

        • 读取输出数据命令:将命令字节的第3位设为1,表示开始读取输出数据
      • 配置DMA的控制寄存器,设置传输方向为PL到PS,传输长度为6656字节,启动位为1,使用Xil_DmaSimpleTransfer函数启动DMA传输,从PL端读取输出数据,并存储到DDR3内存中(从地址0x30000000开始)

        需要注意传输长度是6656字节而不是64字节,并且需要指定一个不同于之前的DDR3内存地址来存储输出数据

      • 使用Xil_DmaIsBusy函数检查DMA是否完成传输,如果完成,继续执行下一步

      • 等待PL端完成输出数据发送后发出的中断信号

    • 重复上述步骤,直到发送完所有的image数据,并接收完所有的输出数据

graph TD A[准备工程] --> B[创建Vitis工程] B --> C[编写PS端代码] C --> D[编译运行] A -->|使用Python代码| E[生成bin文件] E -->|存储结构符合PL端接收方式| F[每字节对应一个通道] C -->|SD卡读写| G[读取bin文件到DDR3] C -->|AXI Lite寄存器写| H[向PL端发送命令] C -->|DMA读写| I[发送或接收数据] C -->|中断响应| J[等待PL端完成计算] D -->|拷贝可执行文件和bin文件到SD卡| K[启动开发板] K -->|观察串口输出和LED灯状态| L[验证PS端代码]

main.c

#include "xparameters.h"	/* SDK generated parameters */
#include "xsdps.h"		/* SD device driver */
#include "xil_printf.h"
#include "ff.h"
#include "xil_cache.h"
#include "xplatform_info.h"
#include "xil_io.h"
#include "xaxidma.h"
#include "xil_types.h"
#include "xil_exception.h"
#include "xil_cache.h"
#include "xscugic.h"


#define INTC_DEVICE_ID		XPAR_SCUGIC_0_DEVICE_ID
#define INTC_DEVICE_INT_ID	XPS_FPGA0_INT_ID


XScuGic InterruptController; 	     /* Instance of the Interrupt Controller */
static XScuGic_Config *GicConfig;    /* The configuration parameters of the
                                       controller */
volatile static int InterruptProcessed = FALSE;
void DeviceDriverHandler(void *CallbackRef);
int Intr_init();
void wait_pl_finish();

static FIL fil;		/* File object */
static FATFS fatfs;

#define	Lite_Reg0			XPAR_AXI4_LITE_V1_0_0_BASEADDR
#define	Lite_Reg1			XPAR_AXI4_LITE_V1_0_0_BASEADDR+0x4
#define	Lite_Reg2			XPAR_AXI4_LITE_V1_0_0_BASEADDR+0x8

static char FileName0[32] = "img_data.bin";
static char FileName1[32] = "l0_b.bin";
static char FileName2[32] = "l0_r.bin";
static char FileName3[32] = "l0_w.bin";

int SD_Init();
int SD_Write(char *FileName, u32 SourceAddress, u32 FileSize);
int SD_Read(char *FileName, u32 DestinationAddress, u32 FileSize);

void DMA_Init();
void DMA_Tx(u32 TxAddr, u32 Length);
void DMA_Rx(u32 RxAddr, u32 Length);

int main()
{
	SD_Init();
	DMA_Init();
	Intr_init();
	// 读取Bin文件至 DDR3内存
	SD_Read(FileName0, 0x1000000, 1384448);
	SD_Read(FileName1, 0x2000000, 64);
	SD_Read(FileName2, 0x2000040, 256);
	SD_Read(FileName3, 0x2000140, 1152);

	Xil_Out32(Lite_Reg2, 0x09004100);
	Xil_Out32(Lite_Reg1, 0x4B5A0000);
	Xil_Out32(Lite_Reg0, 0x21);			// 发送bias数据
	DMA_Tx(0x2000000, 64);
	wait_pl_finish();

	Xil_Out32(Lite_Reg0, 0x31);			// 发送LeakyRelu数据
	DMA_Tx(0x2000040, 256);
	wait_pl_finish();

	Xil_Out32(Lite_Reg0, 0x11);			// 发送Weight数据
	DMA_Tx(0x2000140, 1152);
	wait_pl_finish();

	Xil_Out32(Lite_Reg0, 0x101181);		// 发送Feature数据
	DMA_Tx(0x1000000, 29952);
	wait_pl_finish();
	Xil_Out32(Lite_Reg0, 0x101184);		// 卷积计算
	wait_pl_finish();

	Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
	DMA_Rx(0x3000000, 6656);			// 9行数据+1行填充=10行数据 --->卷积后,8个416的数据量---》经过池化后,变成4*208---> 总共8个通道,即最终数据量4*208*8=6656
	wait_pl_finish();

	// 不包含第一次发送和最后一次发送数据的计算过程,共循环58次
	int tx_addr = 0x1000000;
	int rx_addr = 0x3000000;
	for(int i=0; i<=57; i++) {
		Xil_Out32(Lite_Reg0, 0x101381);			// 发送Feature数据
		tx_addr = tx_addr + 23296;
		DMA_Tx(tx_addr, 29952);
		wait_pl_finish();
		Xil_Out32(Lite_Reg0, 0x101384);			// 卷积计算
		wait_pl_finish();
		if(i%2 == 0) {
			rx_addr = rx_addr + 6656;
			Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
			DMA_Rx(rx_addr, 4992);				//
			wait_pl_finish();
		}
		else {
			rx_addr = rx_addr + 4992;
			Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
			DMA_Rx(rx_addr, 6656);
			wait_pl_finish();
		}
	}

	// Layer0 前8个输出通道的最后一次发送数据
	tx_addr = tx_addr + 23296;
	Xil_Out32(Lite_Reg0, 0x41581);		// 发送Feature数据
	DMA_Tx(tx_addr, 9984);
	wait_pl_finish();
	Xil_Out32(Lite_Reg0, 0x41584);		// 卷积计算
	wait_pl_finish();
	rx_addr = rx_addr + 6656;
	Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
	DMA_Rx(rx_addr, 1664);
	wait_pl_finish();
//////////////////////////////////////////////////////////
	Xil_Out32(Lite_Reg1, 0x4B5A0101);
	Xil_Out32(Lite_Reg0, 0x101181);		// 发送Feature数据
	DMA_Tx(0x1000000, 29952);
	wait_pl_finish();
	Xil_Out32(Lite_Reg0, 0x101184);		// 卷积计算
	wait_pl_finish();

	Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
	DMA_Rx(0x3054800, 6656);			// 9行数据+1行填充=10行数据 --->卷积后,8个416的数据量---》经过池化后,变成4*208---> 总共8个通道,即最终数据量4*208*8=6656
	wait_pl_finish();

	// 不包含第一次发送和最后一次发送数据的计算过程,共循环58次
	tx_addr = 0x1000000;
	rx_addr = 0x3054800;
	for(int i=0; i<=57; i++) {
		Xil_Out32(Lite_Reg0, 0x101381);			// 发送Feature数据
		tx_addr = tx_addr + 23296;
		DMA_Tx(tx_addr, 29952);
		wait_pl_finish();
		Xil_Out32(Lite_Reg0, 0x101384);			// 卷积计算
		wait_pl_finish();
		if(i%2 == 0) {
			rx_addr = rx_addr + 6656;
			Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
			DMA_Rx(rx_addr, 4992);				//
			wait_pl_finish();
		}
		else {
			rx_addr = rx_addr + 4992;
			Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
			DMA_Rx(rx_addr, 6656);
			wait_pl_finish();
		}
	}

	// Layer0 前8个输出通道的最后一次发送数据
	tx_addr = tx_addr + 23296;
	Xil_Out32(Lite_Reg0, 0x41581);		// 发送Feature数据
	DMA_Tx(tx_addr, 9984);
	wait_pl_finish();
	Xil_Out32(Lite_Reg0, 0x41584);		// 卷积计算
	wait_pl_finish();
	rx_addr = rx_addr + 6656;
	Xil_Out32(Lite_Reg0, 0x2);			// 将PL端的数据传至PS端
	DMA_Rx(rx_addr, 1664);
	wait_pl_finish();

	Xil_DCacheFlushRange(0x3054800, 0x54800);

	return 0;

}

int SD_Init()
{
	FRESULT Res;
	/*
	 * To test logical drive 0, Path should be "0:/"
	 * For logical drive 1, Path should be "1:/"
	 */
	TCHAR *Path = "0:/";

	/*
	 * Register volume work area, initialize device
	 */
	Res = f_mount(&fatfs, Path, 0);

	if (Res != FR_OK) {
		return XST_FAILURE;
	}

	/*
	 * Path - Path to logical driver, 0 - FDISK format.
	 * 0 - Cluster size is automatically determined based on Vol size.
	 */
//	Res = f_mkfs(Path, FM_FAT32, 0, work, sizeof work);
//	if (Res != FR_OK) {
//		return XST_FAILURE;
//	}
	return XST_SUCCESS;
}

int SD_Write(char *FileName, u32 SourceAddress, u32 FileSize)
{
	FRESULT Res;
	UINT NumBytesWritten;

	Res = f_open(&fil, FileName, FA_CREATE_ALWAYS | FA_WRITE);
	if (Res) {
		return XST_FAILURE;
	}

	/*
	 * Pointer to beginning of file .
	 */
	Res = f_lseek(&fil, 0);
	if (Res) {
		return XST_FAILURE;
	}

	/*
	 * Write data to file.
	 */
	Res = f_write(&fil, (const void*)SourceAddress, FileSize,
			&NumBytesWritten);
	if (Res) {
		return XST_FAILURE;
	}


	/*
	 * Close file.
	 */
	Res = f_close(&fil);
	if (Res) {
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

int SD_Read(char *FileName, u32 DestinationAddress, u32 FileSize)
{
	FRESULT Res;
	UINT NumBytesRead;

	Res = f_open(&fil, FileName, FA_READ);
	if (Res) {
		return XST_FAILURE;
	}

	/*
	 * Pointer to beginning of file .
	 */
	Res = f_lseek(&fil, 0);
	if (Res) {
		return XST_FAILURE;
	}

	/*
	 * Write data to file.
	 */
	Res = f_read(&fil, (const void*)DestinationAddress, FileSize,
			&NumBytesRead);
	if (Res) {
		return XST_FAILURE;
	}


	/*
	 * Close file.
	 */
	Res = f_close(&fil);
	if (Res) {
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

void DMA_Init()
{
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_TX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_CR_RESET_MASK);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_RX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_CR_RESET_MASK);


	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_TX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_IRQ_ALL_MASK);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_RX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_IRQ_ALL_MASK);

	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_TX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_CR_RUNSTOP_MASK);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_RX_OFFSET+XAXIDMA_CR_OFFSET, XAXIDMA_CR_RUNSTOP_MASK);
}


void DMA_Tx(u32 TxAddr, u32 Length)
{
	Xil_DCacheFlushRange(TxAddr, Length);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_TX_OFFSET+XAXIDMA_SRCADDR_OFFSET, TxAddr);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_TX_OFFSET+XAXIDMA_BUFFLEN_OFFSET, Length);
}

void DMA_Rx(u32 RxAddr, u32 Length)
{
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_RX_OFFSET+XAXIDMA_DESTADDR_OFFSET, RxAddr);
	Xil_Out32(XPAR_AXIDMA_0_BASEADDR+XAXIDMA_RX_OFFSET+XAXIDMA_BUFFLEN_OFFSET, Length);
}

void DeviceDriverHandler(void *CallbackRef)
{
	/*
	 * Indicate the interrupt has been processed using a shared variable
	 */
	InterruptProcessed = TRUE;
}

int Intr_init()
{
	int Status;

	/*
	 * Initialize the interrupt controller driver so that it is ready to
	 * use.
	 */
	GicConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
	if (NULL == GicConfig) {
		return XST_FAILURE;
	}

	Status = XScuGic_CfgInitialize(&InterruptController, GicConfig,
					GicConfig->CpuBaseAddress);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}


	Xil_ExceptionInit();
	/*
	 * Connect the interrupt controller interrupt handler to the hardware
	 * interrupt handling logic in the ARM processor.
	 */
	Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
			(Xil_ExceptionHandler) XScuGic_InterruptHandler,
			&InterruptController);

	/*
	 * Enable interrupts in the ARM
	 */
	Xil_ExceptionEnable();

	/*
	 * Connect a device driver handler that will be called when an
	 * interrupt for the device occurs, the device driver handler performs
	 * the specific interrupt processing for the device
	 */
	Status = XScuGic_Connect(&InterruptController, INTC_DEVICE_INT_ID,
			   (Xil_ExceptionHandler)DeviceDriverHandler,
			   (void *)&InterruptController);

	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}

	XScuGic_SetPriTrigTypeByDistAddr(&InterruptController, INTC_DEVICE_INT_ID, 0x0, 0x3);

	/*
	 * Enable the interrupt for the device and then cause (simulate) an
	 * interrupt so the handlers will be called
	 */
	XScuGic_Enable(&InterruptController, INTC_DEVICE_INT_ID);

	return 0;
}

void wait_pl_finish()
{
	while(InterruptProcessed == FALSE);
	InterruptProcessed = FALSE;
}