[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX应用篇连载-02 AD信号的以太网传输

文档创建者:LINUX课程
浏览次数:385
最后更新:2024-09-10
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 2_LINUX应用开发
本帖最后由 LINUX课程 于 2024-9-11 11:14 编辑

软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
登录“米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!

1 简介
本章节使用Linux实现了基于socket的远程数据显示。数据从信号发生器输出后,进入FEP-DAQ7606子卡传输至fpga,fpga通过dma将数据保存至ps ddr,Linux下通过dma驱动实现数据mmap、处理中断和缓存切换控制。板卡上程序收集到数据后使用socket发送到pc端,最后PC通过qt展示出波形。
2 系统框图
image.jpg
3 方案介绍
本方案通过7606子卡采集数据,经fpga通过DMA传输至PS端ddr内。PS端负责读取处理数据后,使用socket通信发送数据到PC端,PC端的qt负责接收与展示。
DMA驱动的需求应有如下:
  • 设置IOCTL,用于应用程序的数据读取。
  • 通过实现poll函数实现阻塞IO。
  • 实现一个字符设备以便在打开驱动时,控制fpga启停采集的信号。
  • 使用平台驱动来match设备树。
应用程序的需求有:
  • 使用poll实现阻塞IO。
  • 使用驱动提供的IOCTL控制。
  • 提供memcpy拷贝内存。
  • 使用socket发送数据。
Qt程序的需求有:
  • 接收socket发送来的数据。
  • 使用波形展示。
4 方案搭建
4.1 vivado工程解析
此处不对vivado工程的搭建作详细解释,若想查看vivado搭建请移步FPGA部分课程。接下来将从Linux开发的角度分析vivado内需要注意的几个地方。
1:BD图
image.jpg
bd图上半部分为HDMI输出,本章未使用,不作解释。
看到右侧部分的管脚:
user_start:数据采集开始
user_rstn:数据采集复位
2:查看寄存器地址
image.jpg
此处可以看到需要用到的寄存器地址,三个寄存器分别是DMA以及两个上面提到的控制FPGA采集启停的信号,有这三样东西即可从FPGA读取数据。其中vdma的寄存器本章节用不到,不必控制。
vitis导出的设备树会自动生成这些节点,通常位于pl.dtsi中。
4.2 设备树解析
非常不建议初学者自己修改设备树,请使用demo中自带的设备树操作,若能理解设备树的含义与作用可自行修改。以下内容仅为参考,自行修改会导致工程无法正常运行。
vitis生成设备树内的pl.dtsi内已经生成了上述我们所需的三个节点axi_dma_0、gpio_user_rstn、gpio_user_start,所以无须修改。但是为了能让我们的驱动匹配到dma,我们需要增加一个节点。
  1. adcdma_fun_0: adcdma_fun0@0 {
  2. compatible = "adcdma_demo";
  3. dmas = <&axi_dma_0 0>;
  4. dma-names = "adc";
  5. num-buf = <3>;
  6. reset-gpios = <&gpio_user_rstn 0 0 0>;
  7. enable-gpios = <&gpio_user_start 0 0 0>;
  8. };
复制代码
image.jpg
简单讲解一下设备树内重要字段的含义。
compatible字段用于匹配驱动。
dmas设置了dma的地址。
num-buf设置了dma的帧个数。
reset-gpios为控制FPGA端复位的信号。
enable-gpios为控制FPGA端启停的信号。
4.3 构建系统
1:从vivado到vitis生成所需文件
若不清楚如何搭建,请复习Linux基础第四章的内容,此处不再赘述。若想快速验证,在本课对应的demo中有工程已编译好可直接使用。
image.jpg
tcp_adc_dma:板卡Linux内的读取驱动并socket发送程序。
boot:启动文件,使用boot文件替换boot分区,即可搭配任意文件系统运行程序。
QT:pc端的qt接收与展示波形工程。
soc_dts、soc_hw、soc_prj、soc_sdk:移植系统所需工程,分别为设备树、硬件描述文件、vivado工程和vitis工程。若还不熟悉这些文件,请先学习《3-4-1Linux基础入门》内第四课的内容。
2:拷贝文件
本方案使用的开发包版本为uisrc-lab-xlnx20220601.tar.gz,请确保正在使用的版本一致。若不清楚或不一致,请前往https://www.uisrc.com/t-3268.html下载。
以往的开发包,需要手动设置好5个文件,分别是fsbl.elf、system_wrapper.bit和kernel、uboot的设备树(设备树在demo中已经提供)。在新版本的开发包中可以无须手动改名,放置到指定路径即可。若不清楚本段的内容表述,请先重复Linux基础第四章的内容,熟悉基础步骤。
image.jpg
将四个文件放置到/uisrc-lab-xlnx/boards/mz7x/ubuntu/output/files指定位置。其中fsbl.elf、system_wrapper.bit两个文件直接放在目录下,而kernel和uboot的设备树要放在对应的文件夹内。注意kernel-dts文件夹内的设备树支持include,所以可以放入多个文件,但是uboot-dts内设备树不支持include,只能支持名为zynq-mz7x.dts的设备树。
3:构建所需系统
首先source环境变量,先前往以下路径/uisrc-lab-xlnx/scripts:
image.jpg
运行命令:
source mz7xcfg.sh                        用来设置环境变量
move_files.sh                                将刚才我们拷贝的文件重命名并拷贝到对应的位置
make_uboot.sh                        编译uboot
make_kernel.sh                        编译kernel
create_image.sh                        生成启动文件
紧接着插入sd卡,并连接到虚拟机内,继续输入命令:
make_parted.sh                        给sd卡分区,先输入sdb,再输入y
deploy_image.sh                        烧录系统
7100FC 的开机部分参考第一章的第5节,从qspi启动sd卡。
4:本地编译应用程序
将demo中的01_tcp_adc_dma文件夹内文件全部拷贝到sd卡的/home/uisrc下:
image.jpg
弹出sd卡:
1725964246759.jpg
使用串口登录开发板,用户名uisrc,密码root:
image.jpg
ls查看当前目录下的文件:
image.jpg
输入gcc send_app.c来编译应用程序:
image.jpg
编译完成后当前目录下会多出一个名为a.out的可执行文件,这就是编译好的文件。
5:编译QT程序
由于本步骤比较繁琐,没有修改QT需求的可以直接使用demo中编译好的qt程序(/QT/adc_mlk_boxed.exe)。若有修改qt程序需求的可以参考以下内容。
前往https://www.uisrc.com/t-3268.html下载uisrc_windows10.zip虚拟机系统,这是一个装有qt环境的win10虚拟机系统。下载完成后解压到合适的位置,然后打开并运行虚拟机:
image.jpg
将demo的“QT/QT工程”路径下的文件夹放到合适的位置:
image.jpg
通过桌面图标启动Qt Creator:
image.jpg
打开刚才放在虚拟机中的工程文件,并选择.pro后缀的文件打开:
image.jpg
打开会提示没有配置,直接确认即可:
image.jpg
点击配置工程:
image.jpg
左下角默认为debug模式,切换选择到release:
image.jpg
点击运行按钮
image.jpg
Qt程序运行成功:
image.jpg
回到工程目录,边上多了一个文件夹,进入这个文件夹的“release”路径内,找到编译成功的exe文件:
image.jpg
将exe文件拷贝出来,在合适的地方新建一个文件夹放进去:
image.jpg
开始菜单内找到qt命令行,并打开:
1725964402889.jpg
输入windeployqt+刚才exe的路径:
image.jpg
再回到刚才存放exe的文件夹,多了很多库文件:
image.jpg
这时候的exe就可以运行了。
5 驱动应用分析
由于驱动代码比较多,本部分只分析重要的代码部分,若想查看完整代码请在以下路径寻找驱动:/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/dma_adc.c
5.1 平台驱动配置
  1. static struct platform_driver adcdma_fun_device_driver = {
  2. .probe = adcdma_fun_probe,
  3. .remove = adcdma_fun_remove,
  4. .driver = {
  5.   .name = "adcdma_demo",
  6.   .owner = THIS_MODULE,
  7.   .of_match_table = of_match_ptr(adcdma_fun_of_match),
  8. }};
复制代码
行2,驱动初始化函数。
行3,驱动注销函数。
行7,驱动匹配。
5.2 match设备树
  1. int of_adcdma_data(struct adcdma_fun *pdata, struct platform_device *pdev)
  2. {
  3. int cnt = 0;
  4. int ret = 0;
  5. int i = 0;
  6. int hsize = 0;

  7. pdata->reset_gpio = devm_gpiod_get_optional(&pdev->dev, "reset",
  8.             GPIOD_OUT_LOW);
  9. if (IS_ERR(pdata->reset_gpio))
  10. {

  11.   printk("[zgq]get reset gpio err\n");
  12.   return PTR_ERR(pdata->reset_gpio);
  13. }

  14. pdata->enable_gpio = devm_gpiod_get_optional(&pdev->dev, "enable",
  15.              GPIOD_OUT_LOW);
  16. if (IS_ERR(pdata->enable_gpio))
  17. {
  18.   printk("[zgq]get enable gpio err\n");
  19.   return PTR_ERR(pdata->enable_gpio);
  20. }
  21. adcdma_reset();

  22. pdata->dmad = devm_kmalloc(&pdev->dev, sizeof(struct adcdma_para), GFP_KERNEL);
  23. if (pdata->dmad == NULL)
  24. {
  25.   printk("kmalloc err\n");
  26.   return -1;
  27. }
  28. memset(pdata->dmad, 0, sizeof(struct adcdma_para));
  29. pdata->dmad->dma = dma_request_chan(&pdev->dev, "adc");
  30. if (IS_ERR_OR_NULL(pdata->dmad->dma))
  31. {
  32.   printk("get dma0 err\n");
  33.   return PTR_ERR(pdata->dmad->dma);
  34. }

  35. ret = of_property_read_u32(pdev->dev.of_node,
  36.           "num-buf", &cnt);
  37. if (ret < 0)
  38. {
  39.   pr_err("adcdmatest: missing num-frm property\n");
  40.   return ret;
  41. }
  42. cnt = cnt > 32 ? 32 : cnt;
  43. pdata->dmad->buf_con = cnt;

  44. hsize = ADCDATA_SIZE;

  45. #if ADC_DMAINIT_S
  46. for (i = 0; i < cnt; i++)
  47. {
  48.   pdata->dmad->adc_virt[i] = dma_alloc_coherent(&pdev->dev, PAGE_ALIGN(hsize),
  49.                &pdata->dmad->adc_phys[i], GFP_KERNEL);
  50.   if (pdata->dmad->adc_virt[i] == NULL)
  51.   {
  52.    printk("can't alloc mem\n");
  53.   }
  54.   memset(pdata->dmad->adc_virt[i], 0, hsize);
  55. }
  56. #endif
  57. init_waitqueue_head(&pdata->read_queue);
  58. return 0;
  59. }
复制代码
行08~15,获取FPGA端复位管脚。
行17~23,获取FPGA端启停管脚。
行26~38,获取DMA信息。
行40~48,获取dma的帧数量。
行52~63,为dma的每个帧都申请好内存。
5.3 IOCTL控制
  1. static long adcdma_func_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
  2. {
  3. int ret = 0;
  4. void __user *user_arg = (void __user *)arg;
  5. int flag;
  6. int n;

  7. switch (cmd)
  8. {
  9. case CMD_GET_DMADATA:
  10.   ret = data_to_user(adcdma_data->dmad, user_arg);
  11.   break;

  12. case CMD_SET_START:
  13.   copy_from_user(&flag, user_arg, sizeof(flag));
  14.   printk("flag = %d\n", flag);

  15.   adcdma_data->dmad->mem_switch = 0;
  16.   adcdma_data->dmad->mem_flag = 0;
  17.   adcdma_data->dmad->read_mem = 0;

  18.   if (flag == 0)
  19.   {
  20.    adcdma_data->trans_en = 0;
  21.    dmaengine_terminate_all(adcdma_data->dmad->dma);
  22.   }
  23.   else
  24.   {
  25.    adcdma_data->trans_en = 1;

  26.    ret = adcdma_setup(adcdma_data->dmad, 0);
  27.    dma_async_issue_pending(adcdma_data->dmad->dma);
  28.   }
  29.   gpiod_set_value_cansleep(adcdma_data->enable_gpio, flag);

  30.   break;

  31. default:
  32.   ret = -EFAULT;
  33.   break;
  34. }
  35. return ret;
  36. }
复制代码
可以看到一共有两个IOCTL函数,一个叫CMD_GET_DMADATA,另一个叫CMD_SET_START。
行10~12,CMD_GET_DMADATA用于向应用端提供数据。
行14~34,CMD_SET_START用来设置采集的开始与结束,0为停止,1为开始。
5.4 中断回调函数
  1. static void adcdma_irq_handler(void *data)
  2. {
  3. int num = (int)data;

  4. // printk("irq %d\n", num);
  5. adcdma_data->irq_reprot = 1;

  6. if (adcdma_data->trans_en)
  7. {
  8.   wake_up_interruptible(&adcdma_data->read_queue);
  9.   irq_change_mem(adcdma_data->dmad, num);
  10. }
  11. }
复制代码
这里的中断回调比较简单,只需要唤醒等待队列和切换帧即可。
5.5 poll函数
  1. static unsigned int adcdma_func_poll(struct file *file, struct poll_table_struct *wait)
  2. {
  3. int mask = 0;

  4. poll_wait(file, &(adcdma_data->read_queue), wait);
  5. if (adcdma_data->irq_reprot == 1)
  6. {
  7.   adcdma_data->irq_reprot = 0;
  8.   mask |= (POLLIN | POLLRDNORM);
  9. }

  10. return mask;
  11. }
复制代码
poll函数用于等待中断产生,中断产生后将标志再置0。
5.6 应用程序
  1. ret = poll(poll_fd, 2, -1);
  2. if (ret < 0)
  3. {
  4. printf("error\n");
  5. break;
  6. }
  7. if (poll_fd[0].revents & POLLIN)
  8. {
  9. printf("input\n");
  10. break;
  11. }
  12. ret = ioctl(fd, CMD_GET_DMADATA,
  13. if (ret < 0)
  14. {
  15. perror("get data err\n");
  16. continue;
  17. }
  18. ret = send_data(data, ADC_SIZE);
  19. if (ret < 0)
  20. {
  21. printf("send err\n");
  22. break;
  23. }
复制代码
应用程序仅分析while中内容。
行1,阻塞。
行07~11,监测键盘输入,若有输入则停止。
行12,使用ioctl读取数据。
行18,通过socket发送数据。
6 方案演示
6.1 硬件连线
1:板卡部分
1725964707080.jpg
2:信号发生器部分
image.jpg
6.1 程序测试
1:上电并串口登录
账户:uisrc,密码:root。
image.jpg
此时屏幕已显示开机登录命令行界面。
检查板卡和电脑IP是否均在192.168.137.XXX网段,确保使用ping命令可以互相连通。不连通请先查看01《Linux开机测试》内的内容。此外确保pc的防火墙为全部关闭状态。
2:运行qt
双击打开pc端的应用程序,勾选通道并按start:
3:串口运行
使用命令运行程序,sudo ./a.out 192.168.137.1,此处pc的ip为192.168.137.1,密码为root:
image.jpg
4:观察qt程序
image.jpg
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则