ZYNQ #4 - Linux环境下使用AXI-DMA读取PL外接ADC
该篇文章是上一篇博客(https://blog.csdn.net/sements/article/details/90230188)的实际应用版本。在上篇中我们只是在PL端搭建了一个简单的AXI-DMA回环数据流进行测试,在该篇博客中,我们将实际的使用AXI-DMA这个IP核,使用xilinx_axidma库对一个挂载在PL端的ADC(AD7606,黑金的拓展子卡称为AN706)使用DMA进行数据传输。
目录
0 - 硬件设计与分析
由于种种原因我虽然只是想要做PS端,生活所迫不得已也得开始进行PL端的学习。但出于验证工程的需求,排除变量,这里我们先选择黑金的样例工程(AN706)来进行本次AXI-DMA读取外部ADC的实验。
我们先来看一下黑金的这个VIVADO样例工程。
这个工程中主要包含了几个部分:1、HDMI输出;2、ad7606 IP核的AXI-Lite配置;3、ad7606 IP核的AXI-HP(High performance)数据传输。下面分类稍微介绍一下,如有纰漏还请各位指点。
0.1 - HDMI输出
这一部分在前面很多的工程里面其实也已经用到了,但是一直没有详细的介绍。
在黑金的HDMI输出设计里,是由PL端模拟完成24位RGB编码输出TMDS差分信号,而没有使用HDMI编码芯片。为了完成视频流从HDMI进行输出,我们有下面几个步骤:
- AXI Video Direct Memory Access 将视频图像数据(这里是绘制在了framebuffer中)利用AXI总线搬运到外部
- AXI Stream to Video Out 将数据量转换成视频输出
- 将RGB数据编码成DVI视频数据输出,这里也是直接当成HDMI视频数据用
- 不同的HDMI分辨率需要不同的时钟,我们还需要一个能产生视频时序和动态时钟的IP
综上,我们总共需要5个IP核。对应着上图浅蓝色选中的IP块(AXI-VDMA没有被选中),我们大致就可以知道这个流程了。视频数据通过AXI总线经过被AXI-VDMA读取后,接入到AXI Stream to Video Out 将数据量转换成视频输出,然后RGB to DVI Encoder将视频数据流转换为TMDS信号,输出到约束的FPGA的PL端引脚;Video Timing Controller 和 Dynamic Clock Generator分别用来产生视频时序和HDMI不同分辨率所需的时序。
然后,我们再来看看当在linux系统下使用这个PL设计需要通过AXI-Lite总线配置哪些IP核才能得到正确的HDMi视频信号输出。我们可以看到,相关HDMI视频输出的IP核中,接入到AXI-Lite总线的IP核只有AXI-VDMA,Dynamic clock generator 以及 Video Timing Controller这三个IP核。所以我们前面博客中为了配置linux使用这个HDMI视频输出线路进行输出时,需要配置迪芝伦Dynamic clock 和 迪芝伦 drm这两个额外的模块了(参考https://blog.csdn.net/sements/article/details/88978302)
0.2 - ad7606 IP
这个IP核没有太多好讲的,因为并不知道其内部的代码,只能作为一个黑盒子看待。从黑金的linux驱动代码可以一看如何配置其的端倪:
这是黑金的adc子卡linux驱动代码中ioctl段的节选。我们可以看到这份代码的最后,在产生 AXI_ADC_DMA_START 控制时,首先使用dma_async_issue_pending开始了DMA传输,然后对总线上的 ad7606 IP核的寄存器基址向后偏移4的位置写入了需要采集的ADC数据个数,然后对 ad7606 IP核的寄存器基址写1,开始数据采集以及8个通道数据输出,这里写入需要采集的ADC数据个数是针对所有通道而言的,比如配置采集1920个点,那么8个通道都会输出1920个数据。
AN706这个子卡使用的是8通道ad7606这块ADC芯片,通过 ad7606_sample_v1.0 这个IP核后,其会输出8个通道的数据,每个通道交替输出16位数据。例如,当配置每个通道输出1920个数据后,其会按照下面格式进行数据输出:
1通道16bit数据 2通道16bit数据 3通道16bit数据 4通道16bit数据 ... 8通道16bit数据 1通道16bit数据.....
大致的介绍就到这里,没有必要去细究了,我们接下来还是着重如何使用xilinx_axidma这个库进行数据传输的实现。
1 - petalinux工程建立
具体的建立过程参考前一个博客(https://blog.csdn.net/sements/article/details/90230188),这里只介绍几个不同点。
还是注意Linux下DMA相关的menuconfig项的开启、CMA大小的设置、uboot从SD卡读取dtb、uboot从SD卡读取dtb的bug去除。。。。
接下来设备树的修改就需要注意,因为这里使用的工程和前一个博客使用的博客不太相同。这篇博客中只使用了一个 AXI-DMA 将PL端的ADC数据传输到PS,以及一个AXI-VDMA将PS的数据传输到外部PL进行HDMI视频输出。这里我们还是先生成一下PL相关的设备树文件(pl.dtsi),用来参考着修改system-user.dtsi。
$ petalinux-config -c device-tree
我们可以从pl.dtsi中看到,ad7606_sample这个ip的挂载总线地址是43c20000,这个是ip核的寄存器地址,我们后面就要用mmap的方式将其映射到内存空间然后对其写入采集数据个数和命令其开始采集。
我们修改好system-user.dtsi如下。这里主要有几点:1、加入了hdmi显示部分的设备树配置(虽然对我们验证程序影响不大);2、加入axidma_chrdev。因为这里id号不冲突,就不需要修改了。但是注意,由于axidma_chrdev是挂载在0地址上的,所以其在dtsi中,要在amba总线下靠前的位置声明,不能放到hdmi部分后面。
/include/ "system-conf.dtsi"
/ {
};
&i2c0 {
clock-frequency = <100000>;
};
&amba_pl {
axidma_chrdev: axidma_chrdev@0 {
compatible = "xlnx,axidma-chrdev";
dmas = <&axi_dma_0 0>;
dma-names = "axidma0_channel";
};
hdmi_encoder_0:hdmi_encoder {
compatible = "digilent,drm-encoder";
digilent,edid-i2c = <&i2c0>;
};
xilinx_drm {
compatible = "xlnx,drm";
xlnx,vtc = <&v_tc_0>;
xlnx,connector-type = "HDMIA";
xlnx,encoder-slave = <&hdmi_encoder_0>;
clocks = <&axi_dynclk_0>;
dglnt,edid-i2c = <&i2c0>;
planes {
xlnx,pixel-format = "rgb888";
plane0 {
dmas = <&axi_vdma_0 0>;
dma-names = "dma";
};
};
};
};
&axi_dynclk_0 {
compatible = "digilent,axi-dynclk";
#clock-cells = <0>;
clocks = <&clkc 15>;
};
&v_tc_0 {
compatible = "xlnx,v-tc-5.01.a";
};
生成 image.ub、system.dtb以及BOOT.BIN,放入SD卡备用。
2 - 应用程序的编写
这里我先贴出我的样例代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include "../libaxidma.h"
#define AN706_SAMPLENUM (1920)
#define AN706_REGBASE (0x43C20000)
//reg operate
int g_iAn706MemFileDev;
void* g_pAn706RegBase;
//axidma operate
axidma_dev_t g_AxiDmaDev;
array_t* g_apRxChannels;
void* g_pAn706DataBuf;
bool g_waitFlag = true;
void callbackAfterRecive(int channelid,void* data)
{
printf("INFO: callback func triggerd,channelid: %d\n",channelid);
for(int i = 0;i < AN706_SAMPLENUM;i++)
{
printf("%x ",*((unsigned short*)(g_pAn706DataBuf)+i));
}
printf("\n");
g_waitFlag = false;
}
int main()
{
//init axidma device
g_AxiDmaDev = axidma_init();
if(g_AxiDmaDev == NULL)
{
printf("Error: can not initialize the AXI DMA device\n");
return 0;
}
//get avaliable rx channels
g_apRxChannels = axidma_get_dma_rx(g_AxiDmaDev);
if(g_apRxChannels->len < 1)
{
printf("Error: no receive channels found\n");
return 0;
}
//print avaliable channels
for(int i = 0; i < g_apRxChannels->len;i++)
{
printf("INFO: receive channel ID: %d\n",g_apRxChannels->data[i]);
}
//mmap the an706 reg base
g_iAn706MemFileDev = open("/dev/mem",O_RDWR | O_SYNC);
if(g_iAn706MemFileDev < 0)
{
printf("ERROR: can not open /dev/mem\n");
return 0;
}
g_pAn706RegBase = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,g_iAn706MemFileDev,AN706_REGBASE);
if(!g_pAn706RegBase)
{
printf("ERROR: unable to mmap adc registers\n");
//close an706 memory file dev
if(g_iAn706MemFileDev)
close(g_iAn706MemFileDev);
return 0;
}
//start DMA recive
//per channel data num = 1920 ,per data 16bit,total has 8 adc channel data
g_pAn706DataBuf = axidma_malloc(g_AxiDmaDev,AN706_SAMPLENUM*2*8);
printf("the init dma recive buf:\n");
for(int i = 0;i < AN706_SAMPLENUM;i++)
{
printf("%x ",*((unsigned short*)(g_pAn706DataBuf + i)));
}
printf("\n");
printf("preper to start adc\n");
axidma_stop_transfer(g_AxiDmaDev,g_apRxChannels->data[0]);
axidma_set_callback(g_AxiDmaDev,g_apRxChannels->data[0],callbackAfterRecive,NULL);
axidma_oneway_transfer(g_AxiDmaDev,g_apRxChannels->data[0],g_pAn706DataBuf,AN706_SAMPLENUM*2*8,false);
*(volatile unsigned long*)(g_pAn706RegBase+4) = AN706_SAMPLENUM;
*(volatile unsigned long*)(g_pAn706RegBase) = 1;
printf("the ADC has start...\n");
while(g_waitFlag);
//close an706 memory file dev
if(g_iAn706MemFileDev)
close(g_iAn706MemFileDev);
//munmap
munmap(g_pAn706RegBase,MAP_SHARED);
axidma_destroy(g_AxiDmaDev);
return 0;
}
以及Makefile
an706_test: main.c
arm-linux-gnueabihf-gcc -L../ -l axidma main.c -o an706_test
程序开始处不难理解,包含了上一目录内的 libaxidma头文件以及相关linux操作头文件。将AN706寄存器总线地址做宏定义(还记得我们前面看到的 0x43c20000 吗?)
然后是相关的一些变量定义,g_iAn706MemFileDev是存放的使用open函数打开 /dev/mem 后的文件id。g_pAn706RegBase是一个指针,指向的是是使用mmap映射ad7606_sample 这个ip的AXI-Lite总线地址到内部内存后的地址。g_AxiDmaDev是xilinx_axidma库里面需要的一个axidma设备对象。
在主函数中,我们首先使用 axidma_init 函数初始化了 axidma设备对象。
使用 axidma_get_dma_rx函数获得了目前设备可用的dma接收通道,这里因为我在dtsi中只声明了连接到ad7606_sample ip的dma通道,并没有声明输出hdmi的vdma用的通道,所以这里只会有一个通道被获取。
使用open 这个linux系统函数打开 /dev/mem,然后使用mmap映射ad7606_sample ip的寄存器配置地址到内存上。
接下来我们准备开始dma传输。首先使用 axidma_malloc函数分配了一块接收数据存储区。
然后对使用的接收通道设置了接收完毕回调函数,回调函数内容如下:
使用 axidma_oneway_transfer函数开启了dma接收传输
然后对ad7606_sample ip的寄存器进行配置,命令其开始采集传输数据,程序进入死循环等待回调函数被唤起。
回调函数中,被唤起后会打印出接收数据存储区中的数据,然后将标志位改变,主程序退出死循环等待,释放并关闭相关变量设备,程序结束。
3 - 编译运行
切换到代码及Makefile目录下,运行make,编译出 a.out 程序
拷贝到开发板的根文件系统下,上电运行
可以看到,我们首先打印出了通过 axidma_malloc申请的缓存数组,然后启动dma,在接收完成回调函数中打印出接收缓存数组,可以看到其已被填充ADC的数据
更多推荐
所有评论(0)