基于ZCU104的PS和PL数据交互例程(七):驱动编写与测试
基于ZCU104的PS和PL数据交互例程(一):简介
基于ZCU104的PS和PL数据交互例程(二):vivado中封装现有工程成IP
基于ZCU104的PS和PL数据交互例程(三):vivado中创建IP
基于ZCU104的PS和PL数据交互例程(四):vivado中制作Block Design
基于ZCU104的PS和PL数据交互例程(五):Petalinux流程
基于ZCU104的PS和PL数据交互例程(六):上板流程
基于ZCU104的PS和PL数据交互例程(七):驱动编写与测试
更正
注意:做到这一步的时候出现了一些问题,所以修正了之前六篇的内容,目前都已更正
主要包括:
1.在第(四)篇,设计block design的时候把cdma的中断引脚连接,之前忘记连接了,导致linux调用时一直接受不到cdma的中断信号
2.在第(三)篇,controller的代码编写时,忘记给Start_DUT信号赋值,导致上板后,一直启动不了
调整
需要对dut.v做出一个调整
在设计dut.v的时候,读取和写入bram的地址每次要加4,因为数据是32bit,当做成系统的时候,bram的地址会按照字节在linux系统中进行映射,所以我们一次读写一个32bit数据,地址对应要加4。这点跟纯PL开发不一样。另外我们在verilog是地址还是从0开始读写,但是在linux系统中要从bram的首地址对应的物理地址开始读写
原理
通过之前的步骤,我们生成了基于pynq的rootfs+我们自己定制的硬件平台的Linux系统,现在我们上板之后,在linux下如何操作PS和PS交互呢?
核心思想是,我们要找到PS需要操作什么?
回顾下block design中,PL和PS交互的数据类型有两类,信号类和数据类,分别通过AXI-LITE的IP和CDMA完成数据交互。进一步分析发现,
- 对于AXI-LITE的IP,最终是PS是通过操作内部的寄存器(slave0~4)来跟PL交互的
- 对于CDMA,本质上也是PS操作CDMA的配置寄存器来控制CDMA
所以很显然,PS通过操作各个寄存器就可以控制AXI-LITE的IP和CDMA。其实知道这个就可以了,这个不是唯一的方法,但是是有效的方法。
那么PL的计算核心DUT怎么操作呢?其实这个问题,仔细回顾下block design的设计架构,就会发现已经解决了。
下面我们回顾,整体数据流程,跟第(四)篇中一致
- PS端开始任务后,首先通过CDMA把初始数据送到BRAM_INIT(PS读写寄存器完成)
- 初始数据传输完成后,PS端通过AXI4-LITE给控制器发送初始数据加载完成信号(PS写寄存器完成)
- 拿到PS给的初始数据完成信号后,控制器发送Start_DUT信号给DUT (PS不需要操作,PL逻辑控制)
- DUT通过BRAM_INIT_Port读取BRAM_INIT中的数据(这里按照自己DUT的逻辑读取即可 (PS不需要操作,PL逻辑控制)
- DUT读取完数据后,开始计算 (PS不需要操作,PL逻辑控制)
- DUT计算完成后,把结果数据通过BRAM_RES_Port写到BRAM-RES中 (PS不需要操作,PL逻辑控制)
- DUT发送DUT_Finish信号给控制器 (PS不需要操作,PL逻辑控制)
- 控制器通过AXI4-LITE发送PL处理结束信号 (PS读寄存器完成)
- PS端拿到PL处理结束信号后,通过CDMA把结果数据从BRAM_RES中读走 (PS写寄存器完成)
- 完成一次交互逻辑,后续重复操作
其实PS需要做的事情已经完成了,这里注意芯片设计跟软件编程的区别,当你把硬件电路设计好之后,FPGA已经通过加载比特流把逻辑单元变成了你的电路功能,不管你是否调用,电路都在那里。不像软件那边,必须调用才能有效。
驱动编写
linux驱动是挺复杂的事情,这里我们只是针对这个例程进行简单的驱动编写。
linux从系统层次调用寄存器,需要用mmap函数,本质上可以理解成在block design的时候,系统为寄存器分配了地址,就是Address页面里面分配的,这些是物理地址。而通过mmap之后就可以把这些物理地址映射成虚拟地址,然后就可以在linux系统层面,通过指针操作了。映射完之后,从linux系统层面看,跟操作普通的指针没有区别。
这里我们对controller和cdma准备好寄存器映射和相关操作的头文件,后面直接调用就行
controller
#define SLV_REG0_START_SET 0x00000001 // operate OR
#define SLV_REG0_START_RESET 0x00000000 // operate AND
typedef struct axi_lite_reg_t{
volatile uint32_t slv_reg0; //4
volatile uint32_t slv_reg1; //4
volatile uint32_t slv_reg2; //4
volatile uint32_t slv_reg3; //4
}axi_lite_reg_t;
axi_lite_reg_t *axi_lite_reg;
cdma
/** @name Bitmasks of XAXICDMA_CR_OFFSET register
* @{
*/
#define XAXICDMA_CR_RESET_MASK 0x00000004 /**< Reset DMA engine */
#define XAXICDMA_CR_SGMODE_MASK 0x00000008 /**< Scatter gather mode */
/** @name Bitmask for interrupts
* These masks are shared by XAXICDMA_CR_OFFSET register and
* XAXICDMA_SR_OFFSET register
* @{
*/
#define XAXICDMA_XR_IRQ_IOC_MASK 0x00001000 /**< Completion interrupt */
#define XAXICDMA_XR_IRQ_DELAY_MASK 0x00002000 /**< Delay interrupt */
#define XAXICDMA_XR_IRQ_ERROR_MASK 0x00004000 /**< Error interrupt */
#define XAXICDMA_XR_IRQ_ALL_MASK 0x00007000 /**< All interrupts */
#define XAXICDMA_XR_IRQ_SIMPLE_ALL_MASK 0x00005000 /**< All interrupts for simple only mode */
/*@}*/
/** @name Bitmasks of XAXICDMA_SR_OFFSET register
* This register reports status of a DMA channel, including
* idle state, errors, and interrupts
* @{
*/
#define XAXICDMA_SR_IDLE_MASK 0x00000002 /**< DMA channel idle */
#define XAXICDMA_SR_SGINCLD_MASK 0x00000008 /**< Hybrid build */
#define XAXICDMA_SR_ERR_INTERNAL_MASK 0x00000010 /**< Datamover internal err */
#define XAXICDMA_SR_ERR_SLAVE_MASK 0x00000020 /**< Datamover slave err */
#define XAXICDMA_SR_ERR_DECODE_MASK 0x00000040 /**< Datamover decode err */
#define XAXICDMA_SR_ERR_SG_INT_MASK 0x00000100 /**< SG internal err */
#define XAXICDMA_SR_ERR_SG_SLV_MASK 0x00000200 /**< SG slave err */
#define XAXICDMA_SR_ERR_SG_DEC_MASK 0x00000400 /**< SG decode err */
#define XAXICDMA_SR_ERR_ALL_MASK 0x00000770 /**< All errors */
/*@}*/
typedef struct dma_reg_t{
volatile uint32_t CR; //4
volatile uint32_t SR; //4
volatile uint64_t CURD;//8
volatile uint64_t TAIL;//8
volatile uint64_t SA; //8
volatile uint64_t DA; //8
volatile uint32_t LEN; //4
}dma_reg_t;
dma_reg_t *dma_reg;
void CDMA_Transfer(uint32_t* Source,uint32_t* Destination,uint32_t Len)
{
dma_reg->SA = Source;
dma_reg->DA = Destination;
dma_reg->LEN = Len;
printf("[INFO] CDMA0 SA DA LEN : %x %x %d.\r\n",(dma_reg->SA),dma_reg->DA,dma_reg->LEN);
}
void CDMA_Initialize()
{
dma_reg->CR = XAXICDMA_CR_RESET_MASK;
while(dma_reg->CR & XAXICDMA_CR_RESET_MASK);
printf("[INFO] RESET CDMA0 and CR reg : %x.\r\n",dma_reg->CR);
printf("[INFO] CDMA0 SR reg : %x.\r\n",(dma_reg->SR));
//Enable Interrupt for simple
dma_reg->CR = XAXICDMA_XR_IRQ_SIMPLE_ALL_MASK;
printf("[INFO] Enable Interrupt and CR reg : %x.\r\n",dma_reg->CR);
printf("[INFO] CDMA0 SR reg : %x.\r\n",(dma_reg->SR));
}
int CDMA_Finish()
{
uint32_t TimeOut=5;
while(TimeOut>=0)
{
// printf("[INFO] CDMA0 SR reg : %x.\r\n",(dma_reg->SR));
if(dma_reg->SR & XAXICDMA_SR_IDLE_MASK){break;}
else{TimeOut--;}
}
if(TimeOut<0)
printf("[ERROR] CDMA0 SR CURD TimeOut : %x %x %d.\r\n",(dma_reg->SR),(dma_reg->CURD),TimeOut);
//clear interrupt flag
dma_reg->SR = dma_reg->SR & XAXICDMA_SR_IDLE_MASK;
if(TimeOut<0) return -1;
else return 1;
}
上板测试
烧录好镜像到SD卡,zcu104选择SD卡启动模式,上电,通过串口连接电脑
配置网络
简单配置
- 查看网卡,用ifconfig查看,我的网卡是eth0
-
配置ip为192.168.137.149,注意要在Window电脑里面把相连的网口,配置为192.168.137.1,子网掩码是255.255.255.0
sudo ifconfig eth0 192.168.137.149
新建测试工程
文件结构如下
└── pl_bram_test
├── CMakeLists.txt
├── include
│ ├── axi_lite.h
│ └── cdma.h
└── src
└── pl_bram_test.c
pl_bram_test.c
这个是主程序代码
首先声明物理地址,这个跟block design中设计的Address中的地址对应
#define XPAR_AXI_CDMA_0_BASEADDR 0xA0000000
#define CDMA_PSDDR_BASEADDR 0x10000000
#define BRAM_INIT_BASEADDR 0xC0000000
#define BRAM_RES_BASEADDR 0xC2000000
#define PL_USER_BASEADDR 0xA0010000
1.打开dev/mem
/******** Step1: Initialize /dev/mem ********/
printf("[INFO] DUT Test Start! \n");
int memfd = open("/dev/mem", O_RDWR | O_SYNC);
if (memfd < 0) {
printf("[ERROR] Can not open /dev/mem \n");
return (-1);
}
printf("[INFO] /dev/mem is open \n");
CDMA的驱动出现问题,应该是当初block_design的时候没有连接中断线导致的
2.mmap
/******** Step2: Memory map ********/
//CDMA
dma_reg = (dma_reg_t*)mmap(0, DMA_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, memfd, XPAR_AXI_CDMA_0_BASEADDR);
if(dma_reg == (dma_reg_t*)-1) return -2;
printf("[INFO] CDMA0 MAPPED ADDRESS : %x.\r\n",dma_reg);
//PS_DDR need single data so choose uint32_t
volatile uint32_t* psddr_base = (uint32_t*)mmap(NULL, PSDDR_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, memfd, CDMA_PSDDR_BASEADDR);
printf("[INFO] PSDDR MAPPED ADDRESS : %x.\r\n",psddr_base);
//PL_USER
// volatile uint32_t* pl_base = (uint32_t*)mmap(NULL, PL_USER_SIZE, PROT_READ | PROT_WRITE,
// MAP_SHARED, memfd, PL_USER_BASEADDR);
axi_lite_reg = (axi_lite_reg_t*) mmap(NULL, PL_USER_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, memfd, PL_USER_BASEADDR);
将CDMA,PS_DDR和controller的物理地址映射到虚拟地址
3.初始化CDMA
/******** Step3: Initialize CDMA ********/
CDMA_Initialize();
4.启动CMDA,传递给bram_init
/******** Step4: Transfer data to BRAM INIT ********/
//initialize psddr
uint32_t data = 0;
for(int i=0;i<CDMA_TEST_DATA_NUM;i++)
{
*(volatile uint32_t *)(psddr_base + i) = 0x10203040+(i);
}
printf("Data already write to ddr \n");
for(int i = 0;i < CDMA_TEST_DATA_NUM;i++){
data = *(volatile uint32_t* )(psddr_base+i);
//Virtual address
printf("Read addr:%x Read data : %lx \n",(psddr_base+i),data);
}
//transfer PSDDR ---> BRAM
CDMA_Transfer(CDMA_PSDDR_BASEADDR,BRAM_INIT_BASEADDR,CDMA_TEST_DATA_LEN);
CDMA_Finish();
printf("[INFO] CDMA Transfer OVER:[PSDDR] --> [BRAM]\n");
//transfer BRAM ---> PSDDR
for(int i=0;i<CDMA_TEST_DATA_NUM;i++)
{
*(volatile uint32_t *)(psddr_base + i) = 0;
}
printf("Data is already cleaned to ddr \n");
for(int i = 0;i < CDMA_TEST_DATA_NUM;i++){
data = *(volatile uint32_t* )(psddr_base+i);
printf("Read addr:%x Read data : %lx \n",(psddr_base+i),data);
}
CDMA_Transfer(BRAM_INIT_BASEADDR,CDMA_PSDDR_BASEADDR,CDMA_TEST_DATA_LEN);
CDMA_Finish();
printf("[INFO] CDMA Transfer OVER:[BRAM] --> [PSDDR]\n");
for(int i = 0;i < CDMA_TEST_DATA_NUM;i++){
data = *(volatile uint32_t* )(psddr_base+i);
printf("Read addr:%x Read data : %lx \n",(psddr_base+i),data);
}
先初始化psddr,写入测试数据,然后通过cdma将数据从psddr移动到BRAM_INIT中
然后清空psddr,将数据从BRAM_INIT移动到psddr,检查下数据是否真的写入
5.启动controller
/******** Step5: Start controller ********/
//reset finish flag
axi_lite_reg->slv_reg1 = 0;
//pluse slv_reg0
axi_lite_reg->slv_reg0 = 0xFFFFFFFF;
6.等到计算完成
/******** Step6: Wait controller Finish********/
//wait finish flag
for(int w=0;w<=1000;w++)
{
if(axi_lite_reg->slv_reg1 != 0)
break;
if(w==1000){printf("PL error \n");}
}
如果打印PL error,则表示出现问题,PL没有计算完成
7.启动CDMA,从bram_res取回数据
/******** Step7: Transfer data from BRAM Res ********/
//transfer BRAM ---> PSDDR
for(int i=0;i<CDMA_TEST_DATA_NUM;i++)
{
*(volatile uint32_t *)(psddr_base + i) = 0;
}
printf("Data is already cleaned to ddr \n");
for(int i = 0;i < CDMA_TEST_DATA_NUM;i++){
data = *(volatile uint32_t* )(psddr_base+i);
printf("Read addr:%x Read data : %lx \n",(psddr_base+i),data);
}
CDMA_Transfer(BRAM_RES_BASEADDR,CDMA_PSDDR_BASEADDR,CDMA_TEST_DATA_LEN);
CDMA_Finish();
printf("[INFO] CDMA Transfer OVER:[BRAM] --> [PSDDR]\n");
for(int i = 0;i < CDMA_TEST_DATA_NUM;i++){
data = *(volatile uint32_t* )(psddr_base+i);
printf("Read addr:%x Read data : %lx \n",(psddr_base+i),data);
}
用CDMA从BRAM_RES中把数据移动到psddr,查看是否符合逻辑
8.关闭映射
/******** Step: End ********/
close(memfd);
munmap(dma_reg,DMA_SIZE);
munmap(psddr_base,PL_USER_SIZE);
//munmap(pl_base,PL_USER_SIZE);
munmap(axi_lite_reg,PL_USER_SIZE);
return 0;
注意关闭,防止内存泄漏等问题。
测试
如果没有build文件夹,就先创建一个
cd build
cmake ..
make
sudo ./pl_bram_test
注意:一定要用sudo来运行可执行文件,否则不能打开dev/mem
输出结果
可以看到最后从BRAM_RES中读取的结果,正好就是BRAM_INIT数据对应加上dut中的cnt值(0,4,8,12,16,…)
总结
至此我们从最初设计到verilog实现IP,到block design创建整体架构,到petalinux生成镜像,再到linux运行已经成功走完。
最后说一下,后面还会补一个ILA调试,因为下板之后,不方便debug,还是要走下ILA会方便点。
另外就是如果是其他板卡,只需要对应替换IP的支持板卡,petalinux 的BSP包,就行
如果是其他的加速器,想实现这种PS+PL的交互通信,做成演示demo。这个系列博客,可以为大家提供一种参考思路。
希望对大家有帮助,因为这里的很多步骤我当时也是踩了很多坑才走下来的,所以打算把这个流程分享出来,仅能保证按照这个系列的流程是可以走通的,其他的不敢保证。
有什么问题直接评论就行。
另外我想知道做个什么样的工程出来,可以更方便大家使用(就是直接替换自己的加速器,然后按照流程走完就可以上板实现),欢迎大家提意见,如果可行的话(我能搞定的话),我也想做一个开源出来。
本系列的工程代码开源