本帖最后由 LINUX课程 于 2024-9-11 11:17 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 简介 本章节使用两个ov5640摄像头采集数据,经过CEP X3子卡,通过vdma输入转交至ps ddr内。应用程序对摄像头数据处理后,基于framebuffer使用vdma输出至HDMI接口,进而显示出摄像头画面。 2 系统框图 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图 2:计算中断号 放大BD图,找到如下线路,可以看到CAM0的中断接入了In0,CAM1的中断接入了In1,然后两路中断同时被输入了ZYNQ的ip核的PL_PS中断。由此可见CAM0的中断号为PL_PS中断的第一位,CAM1为第二位。打开ug585手册,在P231找到如下表: 第一位的中断号为61,在Linux使用中断号时,实际要减去32,即CAM0的中断号为29。同理CMA1的中断号为30。之后通过vitis生成的设备树也能验证我们的计算。 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输入节点 该设备树对应的驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/vdma_function.c。compatible字段为平台驱动的匹配字段,dmas字段将生成的vdma设备树打包起来。xres用于设置摄像头的输出画面宽度,yres用于设置摄像头的输出画面高度,num-frm规定了输入vdma 的缓冲区数量。 3:添加摄像头节点 本章节使用了两路摄像头,分别挂载在两路不同的i2c总线上,所以摄像头也要分开写。 分别添加两路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中有工程已编译好可直接使用。 camx2_vdma_hdmi:应用工程,用于显示画面。 boot:启动文件,使用boot文件替换boot分区,即可搭配任意文件系统运行程序。 soc_dts、soc_hw、soc_prj、soc_sdk:移植系统所需工程,分别为设备树、硬件描述文件、vivado工程和vitis工程。若还不熟悉这些文件,请先学习Linux基础篇内第四章的内容。 2:拷贝文件 以往的开发包,需要手动设置好5个文件,分别是fsbl.elf、system_wrapper.bit和kernel、uboot的设备树(设备树在demo中已经提供)。在新版本的开发包中可以无须手动改名,放置到指定路径即可。若不清楚本段的内容表述,请先重复Linux基础篇内第四章的内容,熟悉基础步骤。 将四个文件放置到/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: 运行命令: 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卡: 使用串口登录开发板,用户名uisrc,密码root: cd至demo的路径: 使用gcc show_windows.c -o show编译: 编译完成后当前目录下名show的可执行文件会被覆盖,这就是编译好的文件。 5 驱动应用分析 vdma_function.c驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/vdma_function.c,该程序主要用于驱动vdma输入。 5.1 vdma输入驱动 1:平台驱动 - static struct platform_driver vdma_fun_device_driver = {
- .probe = vdma_fun_probe,
- .remove = vdma_fun_remove,
- .driver = {
- .name = "vdma_demo",
- .owner = THIS_MODULE,
- .of_match_table = of_match_ptr(vdma_fun_of_match),
- }};
复制代码行2,驱动注册函数。 行3,驱动注销函数。 行7,驱动匹配函数。 2:设备树匹配 - static struct of_device_id vdma_fun_of_match[] = {
- {
- .compatible = "vdma_demo",
- },
- {},
- };
复制代码行3,compatible字段与4.2.2中设备树相匹配。 3:驱动注册函数 - static int vdma_fun_probe(struct platform_device *pdev)
- {
- struct device *dev = &pdev->dev;
- struct vdma_fun *pdata = dev_get_platdata(dev);
- int ret = 0;
- if (!pdata)
- {
- pdata = devm_kzalloc(dev, sizeof(struct vdma_fun), GFP_KERNEL);
- if (!pdata)
- return -ENOMEM;
- platform_set_drvdata(pdev, pdata);
- }
- ret = of_vdma_data(pdata, pdev);
- if (ret < 0)
- goto out;
- ret = vdma_cdev_init(pdata);
- if (ret < 0)
- goto out;
- vdma_data = pdata;
- out:
- return ret;
- }
复制代码行7~14,为dev设备申请内存。 行16,获取设备树信息。 行20,字符设备初始化。 4:读取设备树 - int of_vdma_data(struct vdma_fun *pdata, struct platform_device *pdev)
- {
- int cnt = 0;
- int ret = 0;
- int i = 0;
- int hsize = 0;
- pdata->dmad[0] = devm_kmalloc(&pdev->dev, sizeof(struct vdma_para), GFP_KERNEL);
- if (pdata->dmad[0] == NULL)
- {
- printk("kmalloc err\n");
- return -1;
- }
- memset(pdata->dmad[0], 0, sizeof(struct vdma_para));
- pdata->dmad[0]->dma = dma_request_chan(&pdev->dev, "cam0");
- if (IS_ERR_OR_NULL(pdata->dmad[0]->dma))
- {
- printk("get dma0 err\n");
- return PTR_ERR(pdata->dmad[1]->dma);
- }
- ret = of_property_read_u32(pdev->dev.of_node,
- "xres0", &pdata->dmad[0]->xres);
- if (ret < 0)
- {
- pr_err("vdmatest: missing xres property\n");
- return ret;
- }
- ret = of_property_read_u32(pdev->dev.of_node,
- "yres0", &pdata->dmad[0]->yres);
- if (ret < 0)
- {
- pr_err("vdmatest: missing yres property\n");
- return ret;
- }
- pdata->dmad[1] = devm_kmalloc(&pdev->dev, sizeof(struct vdma_para), GFP_KERNEL);
- if (pdata->dmad[1] == NULL)
- {
- printk("kmalloc err\n");
- return -1;
- }
- memset(pdata->dmad[1], 0, sizeof(struct vdma_para));
- pdata->dmad[1]->dma = dma_request_chan(&pdev->dev, "cam1");
- if (IS_ERR(pdata->dmad[1]->dma))
- {
- printk("get dma1 err\n");
- return PTR_ERR(pdata->dmad[1]->dma);
- }
- ret = of_property_read_u32(pdev->dev.of_node,
- "xres1", &pdata->dmad[1]->xres);
- if (ret < 0)
- {
- pr_err("vdmatest: missing xres property\n");
- return ret;
- }
- ret = of_property_read_u32(pdev->dev.of_node,
- "yres1", &pdata->dmad[1]->yres);
- if (ret < 0)
- {
- pr_err("vdmatest: missing yres property\n");
- return ret;
- }
- ret = of_property_read_u32(pdev->dev.of_node,
- "num-frm", &cnt);
- if (ret < 0)
- {
- pr_err("vdmatest: missing num-frm property\n");
- return ret;
- }
- cnt = cnt > 32 ? 32 : cnt;
- pdata->dmad[0]->frm_cnt = cnt;
- pdata->dmad[1]->frm_cnt = cnt;
- hsize = pdata->dmad[0]->xres * pdata->dmad[0]->yres * 4;
- printk("zgq x=%d, y=%d,hsize=%d\n", pdata->dmad[1]->xres, pdata->dmad[1]->yres, hsize);
- for (i = 0; i < cnt; i++)
- {
- pdata->dmad[0]->fb_virt[i] = dma_alloc_coherent(&pdev->dev, PAGE_ALIGN(hsize),
- &pdata->dmad[0]->fb_phys[i], GFP_KERNEL);
- if (pdata->dmad[0]->fb_virt[i] == NULL)
- {
- printk("can't alloc mem\n");
- }
- pdata->dmad[1]->fb_virt[i] = dma_alloc_coherent(&pdev->dev, PAGE_ALIGN(hsize),
- &pdata->dmad[1]->fb_phys[i], GFP_KERNEL);
- if (pdata->dmad[1]->fb_virt[i] == NULL)
- {
- printk("can't alloc mem\n");
- }
- }
- init_waitqueue_head(&pdata->read_queue);
- return 0;
- }
复制代码由于经常讲这类操作,所以此处简写。 行15~36,获取摄像头0的各项参数。 行38~66,获取摄像头1的各项参数。 行68~77,获取vdma帧数量。 5:字符设备注册 - int vdma_cdev_init(struct vdma_fun *pdata)
- {
- int rc;
- struct cdev *c_dev;
- rc = alloc_chrdev_region(&pdata->t_dev, 0, 1, "vdma_fun");
- if (rc)
- goto out_err;
- c_dev = cdev_alloc();
- if (!c_dev)
- goto out_err;
- cdev_init(c_dev, &vdma_fops);
- rc = cdev_add(c_dev, pdata->t_dev, 1);
- if (rc)
- goto out_unreg;
- pdata->vdma_class = class_create(THIS_MODULE, "vdma");
- if (IS_ERR(pdata->vdma_class))
- {
- printk("[err]class_create error\n");
- rc = -1;
- goto out_devdel;
- }
- pdata->vdma_dev = device_create(pdata->vdma_class, NULL, pdata->t_dev, NULL, "vdma_fun");
- if (!pdata->vdma_dev)
- {
- rc = -1;
- goto class_err;
- }
- return 0;
- class_err:
- class_destroy(pdata->vdma_class);
- out_devdel:
- cdev_del(c_dev);
- out_unreg:
- unregister_chrdev_region(pdata->t_dev, 1);
- out_err:
- return rc;
- }
复制代码行6,注册字符设备。 行11,为字符设备申请内存空间。 行15,字符设备初始化。 行20,创建一个类,用于创建设备。 行28,创建设备,将字符设备与类绑定。 6:poll函数 - 1.static unsigned int vdma_func_poll(struct file *file, struct poll_table_struct *wait)
- 2.{
- 3. int mask = 0;
- 4.
- 5. poll_wait(file, &(vdma_data->read_queue), wait);
- 6. if (vdma_data->irq_reprot == 1)
- 7. {
- 8. vdma_data->irq_reprot = 0;
- 9. mask |= (POLLIN | POLLRDNORM);
- 10. }
- 11.
- 12. return mask;
- 13.}
复制代码poll函数用于将获取到的画面放入等待队列中,这样应用程序不被阻塞后即可从队列中获取数据。 行5,将数据放入等待队列中。 行6~10,设置数据已存取的标志。 7:ioctl函数 - static long vdma_func_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
- {
- int ret = 0;
- void __user *user_arg = (void __user *)arg;
- switch (cmd)
- {
- case CMD_GETWIN:
- ret = copy_to_user(user_arg, &vdma_data->chanl, sizeof(int)) ? -EFAULT : 0;
- break;
- case CMD_READ_DMA0:
- vdma_data->chanl &= ~(1 << 0);
- ret = data_to_user(vdma_data->dmad[0], user_arg, 0);
- break;
- case CMD_READ_DMA1:
- vdma_data->chanl &= ~(1 << 1);
- ret = data_to_user(vdma_data->dmad[1], user_arg, 1);
- break;
- default:
- ret = -EFAULT;
- break;
- }
- return ret;
- }
复制代码行8,CMD_GETWIN用于获取当前有数据的摄像头编号。 行12,CMD_READ_DMA0获取摄像头0的画面数据。 行17,CMD_READ_DMA1获取摄像头1的画面数据。 8:中断函数 - static void vdma_irq_handler(void *data)
- {
- int num = (int)data;
- //printk("%d\n", num);
- vdma_data->irq_reprot = 1;
- vdma_data->chanl |= (1 << num);
- wake_up_interruptible(&vdma_data->read_queue);
- if (vdma_data->trans_en)
- irq_change_mem(vdma_data->dmad, num);
- }
复制代码中断函数用来从等待队列中获取画面的数据。 行6~7,设置标志。 行9,唤醒队列,从里面取出一个数据。 6 方案演示 6.1 硬件连线 1:板卡部分 6.2 程序测试 1:上电并串口登录 账户:uisrc,密码:root。 此时屏幕已显示开机登录命令行界面。 2:运行程序 输入命令: cd camx2_vdma_hdmi/ 进入程序文件夹 sudo ./show 运行程序,输入密码:root 上图为了方便拍摄拔掉了部分线缆。 可以看到两个摄像头工作正常,目前有红蓝反色问题,可通过修改vivado信号解决。 |