本帖最后由 LINUX课程 于 2024-9-11 11:16 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 简介 本章节与《HDMI显示VDMA2路摄像头采集》章节高度相似,使用两个ov5640摄像头采集数据,经过CEP X3子卡,通过fdma输入转交至ps ddr内。应用程序对摄像头数据处理后,基于framebuffer使用vdma输出至HDMI接口,进而显示出摄像头画面。 2 系统框图 3 方案介绍 本方案用到了三个驱动,分别是fdma输入驱动用于画面输入,ov5640驱动用于控制摄像头参数,vdmafb驱动负责输出。 实现ov5640驱动,需要完成以下需求: - 实现对于不同i2c总线上设备的初始化
- 通过i2c总线配置摄像头功能
- 由于FEP-GPIO-CEPX3-CARD上摄像头座子方向不同,需要使用驱动调节画面方向
实现fdma输入驱动,需要完成以下需求: - 通过平台驱动于设备树匹配
- 使用字符驱动设备向应用层提供公共接口
- 能通过ioctl读取dma的数据
- 实现poll操作来进行阻塞IO
4 方案搭建 4.1 vivado工程解析 此处不对vivado工程的搭建作详细解释,若想查看vivado搭建请移步FPGA部分课程。接下来将从Linux开发的角度分析vivado内需要注意的几个地方。 1:BD图 2:计算中断号 放大BD图,找到如下线路,可以看到CAM0的中断接入了In1,CAM1的中断接入了In2,然后两路中断同时被输入了ZYNQ的ip核的PL_PS中断。由此可见CAM0的中断号为PL_PS中断的第二位,CAM1为第三位。打开ug585手册,在P231找到如下表: 第二位的中断号为62,在Linux使用中断号时,实际要减去32,即CAM0的中断号为30。同理CMA1的中断号为33。记住这个中断号,在修改设备树时需要用到。 3:查看寄存器地址 此处可以看到需要用到的寄存器地址,vitis导出的设备树会自动生成,所以看一下就行。 4.2 设备树解析 非常不建议初学者自己修改设备树,请使用demo中自带的设备树操作,若能理解设备树的含义与作用可自行修改。 1:vitis生成pl.dtsi vitis一共生成5个节点,分别为CAM0_gpio_sccb、CAM0_uifdma_dbuf_0、CAM1_gpio_sccb、CAM1_uifdma_dbuf_0、gpio_rstn。名称中带uifdma字样的节点为fdma设备树,名称中带sccb的为摄像头i2c总线,gpio_rstn则为摄像头的复位管脚。 首先,将CAM0_uifdma_dbuf_0、CAM1_uifdma_dbuf_0两个节点注释掉: 2:添加fdma输入节点 该设备树对应的驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/ fdma_function.c。compatible字段为平台驱动的匹配字段,reg为fdma的寄存器地址,interrupts为中断号,mem_addr为fdma的基地址,mem_size为fdma的大小。 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_fdma_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_fdma_hdmi文件夹拷贝到sd卡的/home/uisrc下: 弹出sd卡: 使用串口登录开发板,用户名uisrc,密码root: cd并ls查看当前目录下的文件: 输入gcc show_windows.c -o show来编译应用程序: 编译完成后当前目录下名show的可执行文件会被覆盖,这就是编译好的文件。 5 驱动应用分析 5.1 fdma输入驱动 完整代码请在以下路径寻找驱动:/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/fdma_function.c 1:平台驱动配置 - static struct platform_driver fdma_fun_device_driver = {
- .probe = fdma_fun_probe,
- .remove = fdma_fun_remove,
- .driver = {
- .name = "fdma_func",
- .owner = THIS_MODULE,
- .of_match_table = of_match_ptr(fdma_fun_of_match),
- }};
复制代码2行设置平台驱动初始化函数,3行设置平台驱动卸载函数,7行为设备树的类型匹配函数,驱动中一共两种模式,视频模式为0,adc模式为1。 2:match设备树 - int of_fdma_data(struct fdma_fun *pdata, struct platform_device *pdev)
- {
- struct device_node *np = pdev->dev.of_node;
- int ret = 0;
- const struct of_device_id *match;
- match = of_match_node(fdma_fun_of_match, np);
- if (match && match->data)
- {
- pdata->type = *(enum fdma_fun_type *)match->data;
- }
- ret = of_property_read_u32(np, "mem_addr", &pdata->fdma_mem_addr);
- if (ret < 0)
- {
- dev_err(&pdev->dev, "get mem_addr failed\n");
- return ret;
- }
- ret = of_property_read_u32(np, "mem_size", &pdata->fdma_mem_size);
- if (ret < 0)
- {
- dev_err(&pdev->dev, "get mem_size failed\n");
- return ret;
- }
- ret = of_property_read_u32(np, "num-buf", &pdata->num_buf);
- if (ret < 0)
- {
- pdata->num_buf = 3;
- }
- if (pdata->type == FDMA_TYPE_ADC)
- {
- pdata->reset_gpio = devm_gpiod_get_optional(&pdev->dev, "reset", GPIOD_OUT_LOW);
- if (IS_ERR(pdata->reset_gpio))
- {
- if (pdata->reset_gpio != NULL)
- {
- printk("[zgq]get reset gpio err\n");
- return PTR_ERR(pdata->reset_gpio);
- }
- else
- {
- printk("[zgq]get reset gpio null\n");
- }
- }
- pdata->start_gpio = devm_gpiod_get_optional(&pdev->dev, "start", GPIOD_OUT_LOW);
- if (IS_ERR(pdata->start_gpio))
- {
- if (pdata->start_gpio != NULL)
- {
- printk("[zgq]get start gpio err\n");
- return PTR_ERR(pdata->start_gpio);
- }
- else
- {
- printk("[zgq]get ctl gpio null\n");
- }
- }
- }
- pdata->irq = platform_get_irq(pdev, 0);
- if (pdata->irq < 0)
- {
- printk("[err]unable get irq fdma\n");
- return -1;
- }
- init_waitqueue_head(&pdata->read_queue);
- return 0;
- }
复制代码of_fdma_daa函数用于从设备树读取各项参数。 行15,从设备树读取mem_addr关键词,为fdma的基地址。 行22,从设备树读取mem_size关键词,为fdma的缓存区总大小。 行29,从设备树读取num-buf关键词,为fdma的缓存区个数。 行35~64,从设备树读取start和reset管脚,分别用作fpga数据采集的启停控制和复位控制。 行66,从设备树读取fdma的中断号,用作之后的中断申请。 3:IOCTL控制 - static long fdma_func_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
- {
- int ret = 0;
- int __user *user_arg = (int __user *)arg;
- int flag;
- switch (cmd)
- {
- case CMD_CLEANBIT:
- if (fdma_data->type == FDMA_TYPE_ADC)
- {
- fdma_data->pkg_done_cnt--;
- }
- else
- {
- if (put_user(fb_switch, user_arg))
- return -EFAULT;
- }
- break;
- case CMD_SET_RBUF:
- if (get_user(flag, user_arg))
- return -EFAULT;
- ret = regmap_write(fdma_data->fdma_regmap, FDMA_SET_RBUF, FDMA_BUFSET_MASK | flag);
- if (ret < 0)
- {
- printk("set rbuf err\n");
- }
- break;
- case CMD_GET_NUMBUF:
- ret = copy_to_user(user_arg, &fdma_data->num_buf, sizeof(int));
- if (ret)
- printk("copy to user num buf err\n");
- break;
- case CMD_SET_START:
- if (get_user(flag, user_arg))
- return -EFAULT;
- if (fdma_data->type == FDMA_TYPE_ADC)
- gpiod_set_value_cansleep(fdma_data->start_gpio, flag);
- break;
- default:
- ret = -EFAULT;
- break;
- }
- return ret;
- }
复制代码IOCTL是一种应用程序直接控制驱动的方式,功能类似于字符设备的read/write函数。 CMD_CLEANBIT:设置/读取缓存区读取完毕标志。 CMD_SET_RBUF:设置读取缓存区个数。(目前弃用) CMD_GET_NUMBUF:读取各类dma的缓存区个数。 CMD_SET_START:控制fpga采集的启停。 4:中断与poll函数 - static unsigned int fdma_func_poll(struct file *file, struct poll_table_struct *wait)
- {
- int mask = 0;
- poll_wait(file, &(fdma_data->read_queue), wait);
- if (fdma_data->irq_reprot == 1)
- {
- fdma_data->irq_reprot = 0;
- mask |= (POLLIN | POLLRDNORM);
- }
- return mask;
- }
- static irqreturn_t fdma_irq_handler(int irq, void *data)
- {
- int ret;
- // printk("irq fdma\n");
- wake_up_interruptible(&fdma_data->read_queue);
- fdma_data->irq_reprot = 1;
- if (fdma_data->type == FDMA_TYPE_VIDEO)
- {
- ret = regmap_read(fdma_data->fdma_regmap, FDMA_SET_WBUF, &fb_switch);
- if (ret < 0)
- {
- printk("irq WBUF err\n");
- }
- }
- else
- {
- fdma_data->pkg_done_cnt++;
- if (fdma_data->pkg_done_cnt > fdma_data->num_buf)
- {
- gpiod_set_value(fdma_data->start_gpio, 0);
- printk("adc done cnt big then 4\n");
- }
- }
- return IRQ_HANDLED;
- }
复制代码这两个函数是用来控制数据传输的,通过中断读取缓存号,然后通过poll来调整应用端的处理速度。 行20,每当中断发生就唤醒一次poll的等待队列。 行24,读取fdma的当前缓存区号。 行33~37,防止adc读取时越界。 5:应用程序分析 qt程序内集成了驱动调用的代码,位置在工程的mythread.cpp内,以下为qt调用驱动读取adc数据的相关代码。 - void GetDataThread::run()
- {
- int fd;
- int ret = 0;
- char *cambuf = NULL;
- char *tmp;
- int mem_switch = 0;
- int num_buf;
- int start;
- int fifo_switch = 0;
- struct pollfd poll_fd;
- dis_connect = false;
- fd = open( "/dev/fdma_fun", O_RDWR);
- if (fd < 0){
- printf("can't open file fdma_fun\n");
- return;
- }
- cambuf = (char *) mmap(0, 0x300000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE|MAP_LOCKED , fd, 0);
- ioctl(fd, CMD_GET_NUMBUF, &num_buf);
- printf("num buf = %d\n", num_buf);
- poll_fd.events = POLLIN;
- poll_fd.fd = fd;
- fifo_switch = 0;
- start = 1;
- ioctl(fd, CMD_SET_START, &start);
- while (1){
- ret = poll(&poll_fd, 1, -1);
- if (ret < 0){
- printf("error\n");
- break;
- }
- if (adc_fifo[fifo_switch]->mem_ready == true){
- printf("mem err\n");
- break;
- }
- tmp = cambuf + mem_switch*READ_SIZE;
- memcpy((char *)(adc_fifo[fifo_switch]->data), tmp, READ_SIZE);
- adc_fifo[fifo_switch]->mem_ready = true;
- fifo_switch++;
- fifo_switch = fifo_switch%3;
- ioctl(fd, CMD_CLEANBIT, &mem_switch);
- mem_switch = (mem_switch+1)%num_buf;
- }
- start = 0;
- ioctl(fd, CMD_SET_START, &start);
- mem_switch = 0;
- fifo_switch = 0;
- ::close( fd );
- munmap(cambuf, 0x300000);
- }
复制代码行14,使用open函数打开驱动。 行20,通过字符驱动的mmap方法映射内存。 行21,通过ioctl读取fdma的缓存区个数。 行27,通过ioctl控制fpga开始采集。 行30,poll函数对应字符驱动的poll方法 行41,将内核空间的数据拷贝到用户空间,将地址存在adc_fifo[fifo_switch]->data这个指针内。 行46,使用加一取余操作实现0~2循环。 5.2 应用程序 1:主函数 主函数完成的工作主要是初始化所需资源,然后调用显示函数显示画面。 - int main(int argc, char **argv)
- {
- struct screen_prepare *screen_show = NULL;
- int ret;
- struct timeval tv;
- screen_show = (struct screen_prepare *)malloc(sizeof(struct screen_prepare));
- if (!screen_show)
- {
- printf("malloc error\n");
- return -1;
- }
- memset(screen_show, 0, sizeof(struct screen_prepare));
- screen_show->num_smwin = 0;
- /*初始化dp的drm,配置fb双缓存*/
- ret = drm_init(&screen_show->drm);
- if (ret < 0)
- {
- printf("drm init err\n");
- goto err_m1;
- }
- win_init(&screen_show->main_w, 0, 0, &screen_show->drm);
- screen_show->main_w.p_bpp = 32;
- screen_show->drm.front_buf = 0;
- gettimeofday(&tv, NULL);
- printf("microsecond star:%ld s %ld us\n", tv.tv_sec, tv.tv_usec);
- show_ov5640(screen_show);
- drm_commit(&screen_show->drm);
- // sleep(30);
- clean_show(screen_show);
- return 0;
- err_m1:
- free(screen_show->sm_win);
- err_m0:
- free(screen_show);
- return -1;
- }
复制代码行07~13,为显示空间申请内存,并初始化。 行17~23,初始化drm。 行25,主要用于设置输出画面的大小,输出内容。 行36,drm显示函数,用于绘制画面。 2:drm显示函数 - int show_ov5640(struct screen_prepare *prepare)
- {
- int fd;
- int ret = 0;
- long int BytesPerLine = 0;
- char *mbuf = NULL;
- char *tmp_mbuf = NULL;
- int fb_switch = 0;
- struct pollfd poll_fd[2];
- struct timeval tv;
- struct timeval tv_end;
- struct modeset_buf *drm_mod = &prepare->drm.bufs[prepare->drm.front_buf];
- uint32_t height = 720;
- uint32_t wideth = 1280;
- fd = open("/dev/fdma_fun", O_RDWR);
- if (fd < 0)
- {
- printf("can't open file fdma_fun\n");
- return (-1);
- }
- mbuf = (char *)mmap(0, 0x1800000, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_POPULATE | MAP_LOCKED, fd, 0);
- if (mbuf == -1)
- {
- printf("mmap error\n");
- ret = -1;
- goto merr;
- }
- BytesPerLine = wideth * prepare->main_w.p_bpp / 8;
- printf("wideth:%d, height:%d\n", drm_mod->width, drm_mod->height);
- printf("pitch:%d, size:%d\n", drm_mod->pitch, drm_mod->size);
- printf("BytesPerLine %ld\n", BytesPerLine);
- poll_fd[0].events = POLLIN;
- poll_fd[0].fd = fd;
- poll_fd[1].events = POLLIN;
- poll_fd[1].fd = 0;
- memset(drm_mod->map, 0, drm_mod->size);
- while (1)
- {
- ret = poll(poll_fd, 2, -1);
- if (ret < 0)
- {
- printf("error\n");
- break;
- }
- if (poll_fd[1].revents & POLLIN)
- break;
- ioctl(fd, CMD_CLEANBIT, &fb_switch);
- fb_switch = (fb_switch + 2) % 3;
- tmp_mbuf = mbuf + buff_off[fb_switch];
- drm_mod = &prepare->drm.bufs[prepare->drm.front_buf];
- #if 0
- for (int j=prepare->height-1; j>=0; j--){
- for (int i = 0; i<prepare->wideth; i++){
- //memcpy(prepare->map+prepare->pitch*j+i*3, mbuf+j*1280*4+i*4+1, 3);
- prepare->map[prepare->pitch*j+i*3 + 0] = mbuf[j*1280*4+i*4 + 2];
- prepare->map[prepare->pitch*j+i*3 + 1] = mbuf[j*1280*4+i*4 + 1];
- prepare->map[prepare->pitch*j+i*3 + 2] = mbuf[j*1280*4+i*4 + 0];
- }
- }
- #else
- if (BytesPerLine == drm_mod->pitch)
- {
- memcpy(drm_mod->map, tmp_mbuf, drm_mod->size);
- // neon_memcpy( drm_mod->map, mbuf, drm_mod->size);
- }
- else
- {
- for (int j = height - 1; j >= 0; j--)
- {
- memcpy(drm_mod->map + drm_mod->pitch * j, tmp_mbuf + j * BytesPerLine, BytesPerLine);
- }
- }
- #endif
- drm_commit(&prepare->drm);
- prepare->drm.front_buf ^= 1;
- // printf("fb_switch: %d\n", fb_switch);
- // fb_switch = (fb_switch + 1) % 3;
- }
- all_ret:
- munmap(mbuf, 0xBFFFFF);
- merr:
- close(fd);
- // while (1);
- return ret;
- }
复制代码行23~24,映射fdma的内存范围,单个fdma帧的大小为0x800000,三帧总大小为0x1800000. 行33~36,展示出画面的各项参数, 行47,poll函数实现阻塞。 行56,ioctl的作用是通知驱动当前帧已被读取。 行57,每次都读取当前帧的前一帧,目的是避免当前帧未完成导致的画面撕裂。 行71~82,内存拷贝将fdma的数据放入drm的内存中。 行84~85,通知drm刷新。 6 方案演示
6.1 硬件连线 1:板卡部分 6.2 程序测试 1:上电并串口登录 账户:uisrc,密码:root。 此时屏幕已显示开机登录命令行界面。 2:运行程序 输入命令: cd camx2_fdma_hdmi/ 进入程序文件夹 sudo ./show 运行程序,输入密码:root 上图为了方便拍摄拔掉了部分线缆。 可以看到两个摄像头工作正常,目前有红蓝反色问题,可通过修改vivado信号解决。 |