请选择 进入手机版 | 继续访问电脑版
[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX应用篇连载-04 HDMI显示VDMA2路摄像头采集

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

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

1 简介
本章节使用两个ov5640摄像头采集数据,经过CEP X3子卡,通过vdma输入转交至ps ddr内。应用程序对摄像头数据处理后,基于framebuffer使用vdma输出至HDMI接口,进而显示出摄像头画面。
2 系统框图
image.jpg
3 方案介绍
本方案用到了两个驱动,分别是vdma输入驱动用于画面输入,ov5640驱动用于控制摄像头参数。请注意本章节的vdma输入驱动有别于vdmafb驱动,本章节的vdma用于采集摄像头输入,而vdmafb驱动则是负责输出,在阅读源码时需要明白两者的区别。
实现ov5640驱动,需要完成以下需求:
  • 实现对于不同i2c总线上设备的初始化
  • 通过i2c总线配置摄像头功能
  • 由于FEP-GPIO-CEPX3-CARD上摄像头座子方向不同,需要使用驱动调节画面方向
实现vdma输入驱动,需要完成以下需求:
  • 通过平台驱动于设备树匹配
  • 使用字符驱动设备向应用层提供公共接口
  • 能通过ioctl分别读取到两个dma的数据
  • 实现poll操作来进行阻塞IO
4 方案搭建
4.1 vivado工程解析
此处不对vivado工程的搭建作详细解释,若想查看vivado搭建请移步FPGA部分课程。接下来将从Linux开发的角度分析vivado内需要注意的几个地方。
1:BD图
image.jpg
2:计算中断号
image.jpg
放大BD图,找到如下线路,可以看到CAM0的中断接入了In0,CAM1的中断接入了In1,然后两路中断同时被输入了ZYNQ的ip核的PL_PS中断。由此可见CAM0的中断号为PL_PS中断的第一位,CAM1为第二位。打开ug585手册,在P231找到如下表:
image.jpg
第一位的中断号为61,在Linux使用中断号时,实际要减去32,即CAM0的中断号为29。同理CMA1的中断号为30。之后通过vitis生成的设备树也能验证我们的计算。
image.jpg
3:查看寄存器地址
此处可以看到需要用到的寄存器地址,vitis导出的设备树会自动生成,所以看一下就行。
4.2 设备树解析
非常不建议初学者自己修改设备树,请使用demo中自带的设备树操作,若能理解设备树的含义与作用可自行修改。
1:vitis生成pl.dtsi
vitis一共生成5个节点,分别为CAM0_axi_vdma_1、CAM0_gpio_sccb、CAM1_axi_vdma_1、CAM1_gpio_sccb、gpio_rstn。名称中带vdma字样的节点为vdma设备树,名称中带sccb的为摄像头i2c总线,gpio_rstn则为摄像头的复位管脚。
2:添加vdma输入节点
1725966850030.jpg
该设备树对应的驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/vdma_function.c。compatible字段为平台驱动的匹配字段,dmas字段将生成的vdma设备树打包起来。xres用于设置摄像头的输出画面宽度,yres用于设置摄像头的输出画面高度,num-frm规定了输入vdma 的缓冲区数量。
3:添加摄像头节点
本章节使用了两路摄像头,分别挂载在两路不同的i2c总线上,所以摄像头也要分开写。
image.jpg
分别添加两路i2c总线,名称为i2c2与i2c3。sda-gpios与scl-gpios分别为i2c的sda与scl线,之前在分析pl.dtsi的时候就已经分析到sccb为i2c总线,所以在行124~125、142~143分别将两路i2c接入。
此外还添加有ov5640_0、ov5640_1两个摄像头,分别在其对应的i2c总线框架内。reg为摄像头的id,出厂便已经设定好,无法更改。reset-gpio为摄像头的复位管脚,控制摄像头的复位与启动功能。xszize和ysize顾名思义就是水平像素和垂直像素,flip控制画面的水平翻转,mirror控制画面的垂直翻转,用来将方向不同的画面转到同一个方向。
4.3 构建系统
1:从vivado到vitis生成所需文件
若不清楚如何搭建,请复习Linux基础篇内第四章的内容,此处不再赘述。若想快速验证,在本课对应的demo中有工程已编译好可直接使用。
image.jpg
camx2_vdma_hdmi:应用工程,用于显示画面。
boot:启动文件,使用boot文件替换boot分区,即可搭配任意文件系统运行程序。
soc_dts、soc_hw、soc_prj、soc_sdk:移植系统所需工程,分别为设备树、硬件描述文件、vivado工程和vitis工程。若还不熟悉这些文件,请先学习Linux基础篇内第四章的内容。
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的sd卡系统启动,参考第一章第5节。
4:本地编译应用程序
将demo中的camx2_vdma_hdmi文件夹拷贝到sd卡的/home/uisrc下:
弹出sd卡:
1725967105039.jpg
使用串口登录开发板,用户名uisrc,密码root:
image.jpg
cd至demo的路径:
image.jpg
使用gcc show_windows.c -o show编译:
image.jpg
编译完成后当前目录下名show的可执行文件会被覆盖,这就是编译好的文件。
5 驱动应用分析
vdma_function.c驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/vdma_function.c,该程序主要用于驱动vdma输入。
5.1 vdma输入驱动
1:平台驱动
  1. static struct platform_driver vdma_fun_device_driver = {
  2. .probe = vdma_fun_probe,
  3. .remove = vdma_fun_remove,
  4. .driver = {
  5.   .name = "vdma_demo",
  6.   .owner = THIS_MODULE,
  7.   .of_match_table = of_match_ptr(vdma_fun_of_match),
  8. }};
复制代码
行2,驱动注册函数。
行3,驱动注销函数。
行7,驱动匹配函数。
2:设备树匹配
  1. static struct of_device_id vdma_fun_of_match[] = {
  2. {
  3.   .compatible = "vdma_demo",
  4. },
  5. {},
  6. };
复制代码
行3,compatible字段与4.2.2中设备树相匹配。
3:驱动注册函数
  1. static int vdma_fun_probe(struct platform_device *pdev)
  2. {
  3. struct device *dev = &pdev->dev;
  4. struct vdma_fun *pdata = dev_get_platdata(dev);
  5. int ret = 0;

  6. if (!pdata)
  7. {
  8.   pdata = devm_kzalloc(dev, sizeof(struct vdma_fun), GFP_KERNEL);
  9.   if (!pdata)
  10.    return -ENOMEM;

  11.   platform_set_drvdata(pdev, pdata);
  12. }

  13. ret = of_vdma_data(pdata, pdev);
  14. if (ret < 0)
  15.   goto out;

  16. ret = vdma_cdev_init(pdata);
  17. if (ret < 0)
  18.   goto out;
  19. vdma_data = pdata;
  20. out:
  21. return ret;
  22. }
复制代码
行7~14,为dev设备申请内存。
行16,获取设备树信息。
行20,字符设备初始化。
4:读取设备树
  1. int of_vdma_data(struct vdma_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->dmad[0] = devm_kmalloc(&pdev->dev, sizeof(struct vdma_para), GFP_KERNEL);
  8. if (pdata->dmad[0] == NULL)
  9. {
  10.   printk("kmalloc err\n");
  11.   return -1;
  12. }
  13. memset(pdata->dmad[0], 0, sizeof(struct vdma_para));
  14. pdata->dmad[0]->dma = dma_request_chan(&pdev->dev, "cam0");
  15. if (IS_ERR_OR_NULL(pdata->dmad[0]->dma))
  16. {
  17.   printk("get dma0 err\n");
  18.   return PTR_ERR(pdata->dmad[1]->dma);
  19. }

  20. ret = of_property_read_u32(pdev->dev.of_node,
  21.           "xres0", &pdata->dmad[0]->xres);
  22. if (ret < 0)
  23. {
  24.   pr_err("vdmatest: missing xres property\n");
  25.   return ret;
  26. }

  27. ret = of_property_read_u32(pdev->dev.of_node,
  28.           "yres0", &pdata->dmad[0]->yres);
  29. if (ret < 0)
  30. {
  31.   pr_err("vdmatest: missing yres property\n");
  32.   return ret;
  33. }

  34. pdata->dmad[1] = devm_kmalloc(&pdev->dev, sizeof(struct vdma_para), GFP_KERNEL);
  35. if (pdata->dmad[1] == NULL)
  36. {
  37.   printk("kmalloc err\n");
  38.   return -1;
  39. }
  40. memset(pdata->dmad[1], 0, sizeof(struct vdma_para));
  41. pdata->dmad[1]->dma = dma_request_chan(&pdev->dev, "cam1");
  42. if (IS_ERR(pdata->dmad[1]->dma))
  43. {
  44.   printk("get dma1 err\n");
  45.   return PTR_ERR(pdata->dmad[1]->dma);
  46. }

  47. ret = of_property_read_u32(pdev->dev.of_node,
  48.           "xres1", &pdata->dmad[1]->xres);
  49. if (ret < 0)
  50. {
  51.   pr_err("vdmatest: missing xres property\n");
  52.   return ret;
  53. }

  54. ret = of_property_read_u32(pdev->dev.of_node,
  55.           "yres1", &pdata->dmad[1]->yres);
  56. if (ret < 0)
  57. {
  58.   pr_err("vdmatest: missing yres property\n");
  59.   return ret;
  60. }

  61. ret = of_property_read_u32(pdev->dev.of_node,
  62.           "num-frm", &cnt);
  63. if (ret < 0)
  64. {
  65.   pr_err("vdmatest: missing num-frm property\n");
  66.   return ret;
  67. }
  68. cnt = cnt > 32 ? 32 : cnt;
  69. pdata->dmad[0]->frm_cnt = cnt;
  70. pdata->dmad[1]->frm_cnt = cnt;

  71. hsize = pdata->dmad[0]->xres * pdata->dmad[0]->yres * 4;
  72. printk("zgq x=%d, y=%d,hsize=%d\n", pdata->dmad[1]->xres, pdata->dmad[1]->yres, hsize);
  73. for (i = 0; i < cnt; i++)
  74. {
  75.   pdata->dmad[0]->fb_virt[i] = dma_alloc_coherent(&pdev->dev, PAGE_ALIGN(hsize),
  76.               &pdata->dmad[0]->fb_phys[i], GFP_KERNEL);
  77.   if (pdata->dmad[0]->fb_virt[i] == NULL)
  78.   {
  79.    printk("can't alloc mem\n");
  80.   }
  81.   pdata->dmad[1]->fb_virt[i] = dma_alloc_coherent(&pdev->dev, PAGE_ALIGN(hsize),
  82.               &pdata->dmad[1]->fb_phys[i], GFP_KERNEL);
  83.   if (pdata->dmad[1]->fb_virt[i] == NULL)
  84.   {
  85.    printk("can't alloc mem\n");
  86.   }
  87. }

  88. init_waitqueue_head(&pdata->read_queue);
  89. return 0;
  90. }
复制代码
由于经常讲这类操作,所以此处简写。
行15~36,获取摄像头0的各项参数。
行38~66,获取摄像头1的各项参数。
行68~77,获取vdma帧数量。
5:字符设备注册
  1. int vdma_cdev_init(struct vdma_fun *pdata)
  2. {
  3. int rc;
  4. struct cdev *c_dev;

  5. rc = alloc_chrdev_region(&pdata->t_dev, 0, 1, "vdma_fun");

  6. if (rc)
  7.   goto out_err;

  8. c_dev = cdev_alloc();
  9. if (!c_dev)
  10.   goto out_err;

  11. cdev_init(c_dev, &vdma_fops);
  12. rc = cdev_add(c_dev, pdata->t_dev, 1);
  13. if (rc)
  14.   goto out_unreg;

  15. pdata->vdma_class = class_create(THIS_MODULE, "vdma");
  16. if (IS_ERR(pdata->vdma_class))
  17. {
  18.   printk("[err]class_create error\n");
  19.   rc = -1;
  20.   goto out_devdel;
  21. }

  22. pdata->vdma_dev = device_create(pdata->vdma_class, NULL, pdata->t_dev, NULL, "vdma_fun");
  23. if (!pdata->vdma_dev)
  24. {
  25.   rc = -1;
  26.   goto class_err;
  27. }

  28. return 0;

  29. class_err:
  30. class_destroy(pdata->vdma_class);

  31. out_devdel:
  32. cdev_del(c_dev);

  33. out_unreg:
  34. unregister_chrdev_region(pdata->t_dev, 1);

  35. out_err:

  36. return rc;
  37. }
复制代码
行6,注册字符设备。
行11,为字符设备申请内存空间。
行15,字符设备初始化。
行20,创建一个类,用于创建设备。
行28,创建设备,将字符设备与类绑定。
6:poll函数
  1. 1.static unsigned int vdma_func_poll(struct file *file, struct poll_table_struct *wait)
  2. 2.{
  3. 3. int mask = 0;
  4. 4.
  5. 5. poll_wait(file, &(vdma_data->read_queue), wait);
  6. 6. if (vdma_data->irq_reprot == 1)
  7. 7. {
  8. 8.  vdma_data->irq_reprot = 0;
  9. 9.  mask |= (POLLIN | POLLRDNORM);
  10. 10. }
  11. 11.
  12. 12. return mask;
  13. 13.}
复制代码
poll函数用于将获取到的画面放入等待队列中,这样应用程序不被阻塞后即可从队列中获取数据。
行5,将数据放入等待队列中。
行6~10,设置数据已存取的标志。
7:ioctl函数
  1. static long vdma_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. switch (cmd)
  6. {
  7. case CMD_GETWIN:
  8.   ret = copy_to_user(user_arg, &vdma_data->chanl, sizeof(int)) ? -EFAULT : 0;
  9.   break;

  10. case CMD_READ_DMA0:
  11.   vdma_data->chanl &= ~(1 << 0);
  12.   ret = data_to_user(vdma_data->dmad[0], user_arg, 0);
  13.   break;

  14. case CMD_READ_DMA1:
  15.   vdma_data->chanl &= ~(1 << 1);
  16.   ret = data_to_user(vdma_data->dmad[1], user_arg, 1);
  17.   break;

  18. default:
  19.   ret = -EFAULT;
  20.   break;
  21. }
  22. return ret;
  23. }
复制代码
行8,CMD_GETWIN用于获取当前有数据的摄像头编号。
行12,CMD_READ_DMA0获取摄像头0的画面数据。
行17,CMD_READ_DMA1获取摄像头1的画面数据。
8:中断函数
  1. static void vdma_irq_handler(void *data)
  2. {
  3. int num = (int)data;

  4. //printk("%d\n", num);
  5. vdma_data->irq_reprot = 1;
  6. vdma_data->chanl |= (1 << num);

  7. wake_up_interruptible(&vdma_data->read_queue);
  8. if (vdma_data->trans_en)
  9.   irq_change_mem(vdma_data->dmad, num);
  10. }
复制代码
中断函数用来从等待队列中获取画面的数据。
行6~7,设置标志。
行9,唤醒队列,从里面取出一个数据。
6 方案演示
6.1 硬件连线
1:板卡部分
1725967467373.jpg
6.2 程序测试
1:上电并串口登录
账户:uisrc,密码:root。
image.jpg
此时屏幕已显示开机登录命令行界面。
2:运行程序
输入命令:
cd camx2_vdma_hdmi/                                进入程序文件夹
sudo ./show                                                运行程序,输入密码:root
image.jpg
上图为了方便拍摄拔掉了部分线缆。
可以看到两个摄像头工作正常,目前有红蓝反色问题,可通过修改vivado信号解决。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则