Petalinux 下使用 UIO 实现 AXI GPIO & AXI Stream FIFO 驱动
瑟如电子TDC的很多用户在standalone环境下使用TDC,但近来也有客户在问,能否在linux下使用。在绝大多数的中低速应用下,一般都推荐用户使用axi stream fifo IP 来获取TDC IP产生的时间戳数据。当然也可以使用AXI DMA,虽然有官方的linux驱动,但是AXI DMA占用面积较大,资源较多,中低速环境下太浪费了。对于Linux驱动,我也才是菜鸟级别,照着示例写过一点字符型驱动。不过一套流程走下来,略显繁琐,而且不便于日后维护。一番比较研究之后,发现使用UIO的框架来实现更为简洁,如果不用中断功能的话,几乎不用在kernel里写程序。因此定下方向:使用UIO实现axi stream fifo 的数据读取。
本文主要的两个参考来源如下:
何晔: 当ZYNQ遇到Linux Userspace I/O
Vacajk: ZYNQ中的UIO驱动和中断程序学习【Xilinx-Petalinux学习】
使用UIO驱动通过AXI GPIO控制PL端的LED。
使用UIO驱动读取AXI Stream FIFO(AXI-Lite)的缓存内容
此外为了便于调试还需要Zynq具有以下端口:Ethernet, SD,
UART。其中,Ethernet用于NFS,便于程序的远程运行与调试。
工程基于黑金的AX7020开发板,FPGA型号设置如下图:
首先添加Zynq PS, 设置DDR、PS/PL MIO,
并使能PL到PS的中断端口。本示例外设的MIO设置如下图:
第二步,添加AXI GPIO IP,用于控制PL侧的LED。GPIO
IP位宽设为8位,低4位连接LED,高4位连接按键。并勾选Enable
Interrupt,添加中断输出端口。本示例只演示控制GPIO低4位的输出,以控制LED。按键输入及中断功能,如有兴趣可自行测试。
第三步,添加AXI Stream FIFO, 配置如下:
因为只用到接收功能,所以没有配置数据发送端口。数据接口格式设置为AXI4-Lite。
第四步,添加自定义IP,产生AXI Stream 数据,输出到AXI Stream
FIFO。在Vivado中点击Tools->Create and package New IP,选择创建AXI4外设:
为自定义IP选取数据端口:AXI Stream
Master。创建完成后,IP中已经包含了示例。该示例的功能为系统初始化后,等待一段时间然后对外发送32字节,也就是8个Word的数据,数据内容为0x01-0x08。可初步满足测试需求。
第五步,连接中断,并自动连接AXI端口。AXI GPIO 以及AXI Stream
FIFO的中断输出通过concate IP 连接到PS的中断接口。
最后,编写XDC文件,将GPIO的端口定义到对应的Led及Key上,并进行综合和实现,最终生成Bitstream。通过File->Export->
Export
Hardware,将硬件定义导出到指定的目录。记住这个目录,之后配置Petalinux的时候要用到。下图是Vivado
工程的Block Diagram。
在这一节中,我们首先基于之前生成的Vivado工程,配置petalinux,使能UIO platform driver, 修改device tree,将AXI GPIO及AXI Stream FIFO的驱动改为generic-uio,最后生成用于从SD启动的镜像文件。
第一步,创建工程以及配置硬件描述文件:
source /opt/petalinux/settings.sh
petalinux-create -t project –-template zynq -n uio_test
cd uio_test
petalinux-config --get-hw-description ../xxxxxxx/
(…/xxxx是之前导出hardware的目录相对路径)
这一步没什么要修改的,保存,退出就可以。
第二步,配置内核:
petalinux-config -c kernel
Device Drivers —>Userspace I/O drivers —>
如上图勾选两个选项。
第三步,编译设备树:
petalinux-build -c device-tree
编译完成后,到petalinux 工程目录下.:
/components/plnx_workspace/device-tree-generation/pl.dtsi中可以看到系统自动识别出的PL侧的设备树信息。可以看到识别出来两个设备:分别为axi_fifo_mm_s_0 以及 axi_gpio_0。
/ {
amba_pl: amba_pl {
\#address-cells = \<1\>;
\#size-cells = \<1\>;
compatible = "simple-bus";
ranges ;
axi_fifo_mm_s_0: axi_fifo_mm_s\@43c00000 {
compatible = "xlnx,axi-fifo-mm-s-4.1";
interrupt-parent = \<\&intc\>;
interrupts = \<0 30 4\>;
reg = \<0x43c00000 0x10000\>;
xlnx,axi-str-rxd-protocol = "XIL_AXI_STREAM_ETH_DATA";
xlnx,axi-str-rxd-tdata-width = \<0x20\>;
xlnx,axi-str-txc-protocol = "XIL_AXI_STREAM_ETH_CTRL";
xlnx,axi-str-txc-tdata-width = \<0x20\>;
xlnx,axi-str-txd-protocol = "XIL_AXI_STREAM_ETH_DATA";
xlnx,axi-str-txd-tdata-width = \<0x20\>;
xlnx,axis-tdest-width = \<0x4\>;
xlnx,axis-tid-width = \<0x4\>;
xlnx,axis-tuser-width = \<0x4\>;
xlnx,data-interface-type = \<0x0\>;
xlnx,has-axis-tdest = \<0x0\>;
xlnx,has-axis-tid = \<0x0\>;
xlnx,has-axis-tkeep = \<0x0\>;
xlnx,has-axis-tstrb = \<0x0\>;
xlnx,has-axis-tuser = \<0x0\>;
xlnx,rx-fifo-depth = \<0x200\>;
xlnx,rx-fifo-pe-threshold = \<0x2\>;
xlnx,rx-fifo-pf-threshold = \<0x1fb\>;
xlnx,s-axi-id-width = \<0x4\>;
xlnx,s-axi4-data-width = \<0x20\>;
xlnx,select-xpm = \<0x0\>;
xlnx,tx-fifo-depth = \<0x200\>;
xlnx,tx-fifo-pe-threshold = \<0x2\>;
xlnx,tx-fifo-pf-threshold = \<0x1fb\>;
xlnx,use-rx-cut-through = \<0x0\>;
xlnx,use-rx-data = \<0x1\>;
xlnx,use-tx-ctrl = \<0x0\>;
xlnx,use-tx-cut-through = \<0x0\>;
xlnx,use-tx-data = \<0x0\>;
};
axi_gpio_0: gpio\@41200000 {
\#gpio-cells = \<2\>;
\#interrupt-cells = \<2\>;
compatible = "xlnx,xps-gpio-1.00.a";
gpio-controller ;
interrupt-controller ;
interrupt-parent = \<\&intc\>;
interrupts = \<0 29 4\>;
reg = \<0x41200000 0x10000\>;
xlnx,all-inputs = \<0x0\>;
xlnx,all-inputs-2 = \<0x0\>;
xlnx,all-outputs = \<0x0\>;
xlnx,all-outputs-2 = \<0x0\>;
xlnx,dout-default = \<0x00000000\>;
xlnx,dout-default-2 = \<0x00000000\>;
xlnx,gpio-width = \<0x8\>;
xlnx,gpio2-width = \<0x20\>;
xlnx,interrupt-present = \<0x1\>;
xlnx,is-dual = \<0x0\>;
xlnx,tri-default = \<0xFFFFFFFF\>;
xlnx,tri-default-2 = \<0xFFFFFFFF\>;
};
};
};
打开文件./project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi,
将pl.dtsi中的内容复制进去,并将两个设备的compatible都改为compatible =
“generic-uio”;并在两个设备后添加:
chosen {
bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio";
stdout-path = "serial0:115200n8";
};
由于本示例没有用到中断,所以设备树里也没有添加中断的描述,如果需要用到,请参考参考资源(1).
第四步就可以编译petalinux工程了。
petalinux-build
编译完成后,生成镜像文件:
petalinux-package --boot --fsbl=./images/linux/zynq_fsbl.elf --fpga --u-boot --force
之后将petalinux工程目录下/images/linux/中的boot.bin和image.ub拷贝到SD卡上,插入AX7020开发板,设置好从SD卡启动的跳线。
为了实现NFS,将开发板接入与上位主机位于同一网段的局域网,启动板子。
来到登录界面后,输入用户名root, 密码root,进入系统。
首先查看UIO设备
ls /dev/uio*:
根据设备树中的顺序,uio0 为axi stream fifo, uio1 为axi gpio。
编写GPIO测试程序
从vacajk的博文中复制过来的gpio-uio-test.c代码:
/*
* This application reads/writes GPIO devices with UIO.
*
*/
#include
#include
#include
#include
#include
#define IN 0
#define OUT 1
#define GPIO_MAP_SIZE 0x10000
#define GPIO_DATA_OFFSET 0x00
#define GPIO_TRI_OFFSET 0x04
#define GPIO2_DATA_OFFSET 0x08
#define GPIO2_TRI_OFFSET 0x0C
#define GIER 0x011C
#define IP_IER 0x0128
#define IP_ISR 0x0120
void usage(void)
{
printf("*argv[0] -d -i|-o \n");
printf(" -d UIO device file. e.g. /dev/uio0");
printf(" -i Input from GPIO\n");
printf(" -o Output to GPIO\n");
return;
}
int main(int argc, char *argv[])
{
int c;
int fd;
int direction=IN;
char *uiod;
int value = 0;
int valued = 0;
int irq_on = 1;
void *ptr;
printf("GPIO UIO test.\n");
while((c = getopt(argc, argv, "d:io:h")) != -1) {
switch(c) {
case 'd':
uiod=optarg;
break;
case 'i':
direction=IN;
break;
case 'o':
direction=OUT;
valued=atoi(optarg);
break;
case 'h':
usage();
return 0;
default:
printf("invalid option: %c\n", (char)c);
usage();
return -1;
}
}
/* Open the UIO device file */
fd = open(uiod, O_RDWR);
if (fd < 1) {
perror(argv[0]);
printf("Invalid UIO device file:%s.\n", uiod);
usage();
return -1;
}
/* mmap the UIO device */
ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* Print Interrupt Registers */
value = *((unsigned *) (ptr + GIER));
printf("%s: GIER: %08x\n",argv[0], value);
value = *((unsigned *) (ptr + IP_IER));
printf("%s: IP_IER: %08x\n",argv[0], value);
value = *((unsigned *) (ptr + IP_ISR));
printf("%s: IP_ISR: %08x\n",argv[0], value);
/* Enable All Interrupts */
printf("%s: Enable All Interrupts in Regs\n", argv[0]);
*((unsigned *)(ptr + GIER)) = 0x80000000;
*((unsigned *)(ptr + IP_IER)) = 0x3;
*((unsigned *)(ptr + IP_ISR)) = 0x3;
/* Enable UIO interrupt */
write(fd, &irq_on, sizeof(irq_on));
if (direction == IN) {
/* Read from GPIO */
*((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 255;
value = *((unsigned *) (ptr + GPIO_DATA_OFFSET));
printf("%s: input: %08x\n",argv[0], value);
} else {
/* Write to GPIO */
*((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 0;
value = valued;
*((unsigned *)(ptr + GPIO_DATA_OFFSET)) = value;
}
/* Print Interrupt Registers */
value = *((unsigned *) (ptr + GIER));
printf("%s: GIER: %08x\n",argv[0], value);
value = *((unsigned *) (ptr + IP_IER));
printf("%s: IP_IER: %08x\n",argv[0], value);
value = *((unsigned *) (ptr + IP_ISR));
printf("%s: IP_ISR: %08x\n",argv[0], value);
munmap(ptr, GPIO_MAP_SIZE);
return 0;
}
在编译之前,我们先简单分析一下源代码:
首先,对输入的参数进行解析:
参数d
定义了UIO设备的文件路径,之后所有对UIO设备的操作,都是以这个文件为接口的。在本示例中,/dev/uio1代表了AXI GPIO, /dev/uio0代表了axi stream fifo。
参数i/o
指定了对GPIO是输入还是输出操作。
之后打开设备文件,通过
ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ\|PROT_WRITE, MAP_SHARED, fd,0);
将设备寄存器地址映射到用户空间的虚拟地址上。对该虚拟地址进行读写操作,就能直接对设备寄存器进行读写操作。
忽略中断的内容直接看这一段:
if (direction == IN) {
/* Read from GPIO */
*((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 255;
value = *((unsigned *) (ptr + GPIO_DATA_OFFSET));
printf("%s: input: %08x\n",argv[0], value);
} else {
/* Write to GPIO */
*((unsigned *)(ptr + GPIO_TRI_OFFSET)) = 0;
value = valued;
*((unsigned *)(ptr + GPIO_DATA_OFFSET)) = value;
}
可见,当对GPIO执行输出操作时,先将对应的三态寄存器写入0,将GPIO设置为输出状态,然后再写入对应的数据寄存器。
接下来在Linux上位机中进行编译:
source /xxxxx/settings.sh
(/xxxx是petalinux所在的绝对路径)
arm-linux-gnueabihf-gcc uio_test.c -o uio_test2
生成了针对目标器件在linux下可执行文件uio_test2
下面要把uio_test2复制到Linux主机下的NFS目录,并在AX7020上挂载目录,并运行程序。
挂载NFS文件夹:
mount -t nfs -o nolock 192.168.1.12:/xxxx/Work_NFS /mnt
(/xxxx/Work_NFS 是用户自己设置的NFS绝对路径。NFS的设置不在本文范围之内,需要注意的是,如果像我一样采用虚拟机运行Linux host的,需要将虚拟机的网卡设为桥接模式。)
将可执行程序uio_test2 复制到/xxxx /Work_NFS。在串口终端中进入/mnt,如果NFS没有问题,可以看到刚刚复制的uio_test2。
接着我们就要运行程序,控制LED。之前提到AXI GPIO的低四位接到了PL侧的LED。而查看AX7020的电路图可以发现,LED对应的pin为低时点亮,高时熄灭。
先熄灭第一个LED,点亮其余;(从左侧第3个LED起为PL LED)
在串口终端中运行如下命令
uio_test2 -d /dev/uio1 -o 1
熄灭第2、3个LED,点亮其余:
uio_test2 -d /dev/uio1 -o 6
有了用UIO 驱动GPIO的经验,写读取AXI Stream FIFO的UIO驱动也就更直观了。在用户空间程序中,我们只要打开axi stream fifo对应的设备文件,映射内存,然后操作对应的寄存器即可。非常有参考价值的两份资料:一是SDK自带的axi stream fifo bare-metal 示例;二是IP的说明文档AXI4-Streaem FIFO IP Product Guide (PG080)。SDK的示例可从bsp的system.mss找到。
点击Import Example:
闲话少说,直接上代码:
#include
#include
#include
#include
#include
#define GPIO_MAP_SIZE 0x10000
#define XLLF_ISR_OFFSET 0x00000000 /**< Interrupt Status */
#define XLLF_IER_OFFSET 0x00000004 /**< Interrupt Enable */
#define XLLF_TDFR_OFFSET 0x00000008 /**< Transmit Reset */
#define XLLF_TDFV_OFFSET 0x0000000c /**< Transmit Vacancy */
#define XLLF_TDFD_OFFSET 0x00000010 /**< Transmit Data */
#define XLLF_AXI4_TDFD_OFFSET 0x00000000 /**< Axi4 Transmit Data */
#define XLLF_TLF_OFFSET 0x00000014 /**< Transmit Length */
#define XLLF_RDFR_OFFSET 0x00000018 /**< Receive Reset */
#define XLLF_RDFO_OFFSET 0x0000001c /**< Receive Occupancy */
#define XLLF_RDFD_OFFSET 0x00000020 /**< Receive Data */
#define XLLF_AXI4_RDFD_OFFSET 0x00001000 /**< Axi4 Receive Data */
#define XLLF_RLF_OFFSET 0x00000024 /**< Receive Length */
#define XLLF_LLR_OFFSET 0x00000028 /**< Local Link Reset */
#define XLLF_TDR_OFFSET 0x0000002C /**< Transmit Destination */
#define XLLF_RDR_OFFSET 0x00000030 /**< Receive Destination */
#define XLLF_INT_RPURE_MASK 0x80000000 /**< Receive under-read */
#define XLLF_INT_RPORE_MASK 0x40000000 /**< Receive over-read */
#define XLLF_INT_RPUE_MASK 0x20000000 /**< Receive underrun (empty) */
#define XLLF_INT_TPOE_MASK 0x10000000 /**< Transmit overrun */
#define XLLF_INT_TC_MASK 0x08000000 /**< Transmit complete */
#define XLLF_INT_RC_MASK 0x04000000 /**< Receive complete */
#define XLLF_INT_TSE_MASK 0x02000000 /**< Transmit length mismatch */
#define XLLF_INT_TRC_MASK 0x01000000 /**< Transmit reset complete */
#define XLLF_INT_RRC_MASK 0x00800000 /**< Receive reset complete */
#define XLLF_INT_TFPF_MASK 0x00400000 /**< Tx FIFO Programmable Full,
* AXI FIFO MM2S Only */
#define XLLF_INT_TFPE_MASK 0x00200000 /**< Tx FIFO Programmable Empty
* AXI FIFO MM2S Only */
#define XLLF_INT_RFPF_MASK 0x00100000 /**< Rx FIFO Programmable Full
* AXI FIFO MM2S Only */
#define XLLF_INT_RFPE_MASK 0x00080000 /**< Rx FIFO Programmable Empty
* AXI FIFO MM2S Only */
#define XLLF_INT_ALL_MASK 0xfff80000 /**< All the ints */
#define XLLF_INT_ERROR_MASK 0xf2000000 /**< Error status ints */
#define XLLF_INT_RXERROR_MASK 0xe0000000 /**< Receive Error status ints */
#define XLLF_INT_TXERROR_MASK 0x12000000 /**< Transmit Error status ints */
/*@}*/
/** @name Reset register values
* These bits are associated with the XLLF_TDFR_OFFSET and XLLF_RDFR_OFFSET
* reset registers.
* @{
*/
#define XLLF_RDFR_RESET_MASK 0x000000a5 /**< receive reset value */
#define XLLF_TDFR_RESET_MASK 0x000000a5 /**< Transmit reset value */
#define XLLF_LLR_RESET_MASK 0x000000a5 /**< Local Link reset value */
/*@}*/
void XLlFifo_IntClear(void* baseaddr, unsigned Mask)
{
*(unsigned*)(baseaddr + XLLF_ISR_OFFSET) = ((Mask) & XLLF_INT_ALL_MASK);
}
int RxReceive (void* baseaddr, unsigned* DestinationAddr)
{
int i;
int Status;
unsigned RxWord;
static unsigned ReceiveLength;
printf(" Receiving data ....\n\r");
/* Read Recieve Length */
int occp = *(unsigned*)(baseaddr + XLLF_RDFO_OFFSET);
printf("occp is %d \n\r", occp);
if( occp)
{
ReceiveLength = (*((unsigned*)(baseaddr + XLLF_RLF_OFFSET)) )/4;
printf("Data Length: is %u \n\r", ReceiveLength);
/* Start Receiving */
for ( i=0; i < ReceiveLength; i++){
RxWord = 0;
RxWord = *(unsigned*)(baseaddr + XLLF_RDFD_OFFSET);
*(DestinationAddr+i) = RxWord;
printf("Data %u is %u \n\r", i, RxWord);
}
}
return 0;
}
void XLlFifo_RxReset(void* baseaddr)
{
*(unsigned*)(baseaddr + XLLF_RDFR_OFFSET) = XLLF_RDFR_RESET_MASK;
}
static unsigned dest_buffer[200];
int main(int argc, char *argv[])
{
int c;
int fd;
char *uiod;
int value = 0;
int valued = 0;
void *ptr;
printf("AXI Stream UIO test.\n");
while((c = getopt(argc, argv, "d:")) != -1) {
switch(c) {
case 'd':
uiod=optarg;
printf("uiod %s \n\r", uiod);
break;
case 'i':
printf("option i \n\r");
break;
default:
printf("invalid option: %c\n", (char)c);
return -1;
}
}
/* Open the UIO device file */
fd = open(uiod, O_RDWR);
if (fd < 1) {
perror(argv[0]);
printf("Invalid UIO device file:%s.\n", uiod);
return -1;
}
/* mmap the UIO device */
ptr = mmap(NULL, GPIO_MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
printf("mmaped address is %x \n\r",ptr);
//XLlFifo_RxReset(ptr);
//*(unsigned*)(ptr + XLLF_LLR_OFFSET) = XLLF_RDFR_RESET_MASK;
//usleep(100);
XLlFifo_IntClear(ptr, 0xffffffff);
printf("xllfifo is intcleared\n\r");
RxReceive(ptr,dest_buffer);
munmap(ptr, GPIO_MAP_SIZE);
}
数据读取逻辑在函数 int RxReceive中实现,实现逻辑如下:
读取RDFO,检查 receive FIFO occupancy,如果非0,继续
读取RLR(RLF),得到receive length in bytes(待接收的字节数,除以4,得到32bit
word的个数)
循环读取RDFD, 每次读取一个32bit word, 直到读完receive length in
bytes/4个word。
编译源文件:
arm-linux-gnueabihf-gcc axisfifo_test.c -o axisfifo_test
复制到NFS文件夹,在串口终端上运行,以下是串口终端的输出结果:
后续更加深入的测试表明,上述代码只能正常读取没有tlast信号的数据,当axis fifo 使能tlast信号后,每一次读取XLLF_RLF_OFFSET寄存器得到的数值表示一个数据包(用tlast信号区分)的字节数,而不是fifo中所有数据的字节长度。因此,需要将int RxReceive (void* baseaddr, unsigned* DestinationAddr)修改一下,见如下代码:
int RxReceive (void* baseaddr, u32* DestinationAddr)
{
int i;
int Status;
unsigned RxWord;
static u32 ReceiveLength;
u32 totalLength = 0;
// printf(" Receiving data ....\n\r");
/* Read Recieve Length */
int occp = *(u32*)(baseaddr + XLLF_RDFO_OFFSET);
while( occp)
{
ReceiveLength = (*((u32*)(baseaddr + XLLF_RLF_OFFSET)) )/4;
/* Start Receiving */
for ( i=0; i < ReceiveLength; i++){
RxWord = 0;
RxWord = *(u32*)(baseaddr + XLLF_RDFD_OFFSET);
*(DestinationAddr+totalLength) = RxWord;
totalLength = totalLength + 1;
}
occp = *(u32*)(baseaddr + XLLF_RDFO_OFFSET);
}
return totalLength;
}
简单的UIO驱动测试就结束了,接下来我会将TDC IP也放到设计中,实现通过UIO控制TDC IP以及时间戳数据的读取,敬请期待!