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数据,并接收完所有的输出数据
-
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;
}