[X]关闭

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

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

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

1 简介
本章节与《HDMI显示VDMA2路摄像头采集》章节高度相似,使用两个ov5640摄像头采集数据,经过CEP X3子卡,通过fdma输入转交至ps ddr内。应用程序对摄像头数据处理后,基于framebuffer使用vdma输出至HDMI接口,进而显示出摄像头画面。
2 系统框图
image.jpg
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图
image.jpg
2:计算中断号
image.jpg
放大BD图,找到如下线路,可以看到CAM0的中断接入了In1,CAM1的中断接入了In2,然后两路中断同时被输入了ZYNQ的ip核的PL_PS中断。由此可见CAM0的中断号为PL_PS中断的第二位,CAM1为第三位。打开ug585手册,在P231找到如下表:
image.jpg
第二位的中断号为62,在Linux使用中断号时,实际要减去32,即CAM0的中断号为30。同理CMA1的中断号为33。记住这个中断号,在修改设备树时需要用到。
3:查看寄存器地址
image.jpg
此处可以看到需要用到的寄存器地址,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两个节点注释掉:
image.jpg
image.jpg
2:添加fdma输入节点
1725968064580.jpg
该设备树对应的驱动位于/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/ fdma_function.c。compatible字段为平台驱动的匹配字段,reg为fdma的寄存器地址,interrupts为中断号,mem_addr为fdma的基地址,mem_size为fdma的大小。
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_fdma_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_fdma_hdmi文件夹拷贝到sd卡的/home/uisrc下:
弹出sd卡:
1725968235097.jpg
使用串口登录开发板,用户名uisrc,密码root:
image.jpg
cd并ls查看当前目录下的文件:
image.jpg
输入gcc show_windows.c -o show来编译应用程序:
image.jpg
编译完成后当前目录下名show的可执行文件会被覆盖,这就是编译好的文件。
5 驱动应用分析
5.1 fdma输入驱动
完整代码请在以下路径寻找驱动:/uisrc-lab-xlnx/sources/kernel/drivers/media/msxbo/fdma_function.c
1:平台驱动配置
  1. static struct platform_driver fdma_fun_device_driver = {
  2. .probe = fdma_fun_probe,
  3. .remove = fdma_fun_remove,
  4. .driver = {
  5.   .name = "fdma_func",
  6.   .owner = THIS_MODULE,
  7.   .of_match_table = of_match_ptr(fdma_fun_of_match),
  8. }};
复制代码
2行设置平台驱动初始化函数,3行设置平台驱动卸载函数,7行为设备树的类型匹配函数,驱动中一共两种模式,视频模式为0,adc模式为1。
2:match设备树
  1. int of_fdma_data(struct fdma_fun *pdata, struct platform_device *pdev)
  2. {

  3. struct device_node *np = pdev->dev.of_node;
  4. int ret = 0;

  5. const struct of_device_id *match;

  6. match = of_match_node(fdma_fun_of_match, np);
  7. if (match && match->data)
  8. {
  9.   pdata->type = *(enum fdma_fun_type *)match->data;
  10. }

  11. ret = of_property_read_u32(np, "mem_addr", &pdata->fdma_mem_addr);
  12. if (ret < 0)
  13. {
  14.   dev_err(&pdev->dev, "get mem_addr failed\n");
  15.   return ret;
  16. }

  17. ret = of_property_read_u32(np, "mem_size", &pdata->fdma_mem_size);
  18. if (ret < 0)
  19. {
  20.   dev_err(&pdev->dev, "get mem_size failed\n");
  21.   return ret;
  22. }

  23. ret = of_property_read_u32(np, "num-buf", &pdata->num_buf);
  24. if (ret < 0)
  25. {
  26.   pdata->num_buf = 3;
  27. }

  28. if (pdata->type == FDMA_TYPE_ADC)
  29. {
  30.   pdata->reset_gpio = devm_gpiod_get_optional(&pdev->dev, "reset", GPIOD_OUT_LOW);
  31.   if (IS_ERR(pdata->reset_gpio))
  32.   {
  33.    if (pdata->reset_gpio != NULL)
  34.    {
  35.     printk("[zgq]get reset gpio err\n");
  36.     return PTR_ERR(pdata->reset_gpio);
  37.    }
  38.    else
  39.    {
  40.     printk("[zgq]get reset gpio null\n");
  41.    }
  42.   }

  43.   pdata->start_gpio = devm_gpiod_get_optional(&pdev->dev, "start", GPIOD_OUT_LOW);
  44.   if (IS_ERR(pdata->start_gpio))
  45.   {
  46.    if (pdata->start_gpio != NULL)
  47.    {
  48.     printk("[zgq]get start gpio err\n");
  49.     return PTR_ERR(pdata->start_gpio);
  50.    }
  51.    else
  52.    {
  53.     printk("[zgq]get ctl gpio null\n");
  54.    }
  55.   }
  56. }

  57. pdata->irq = platform_get_irq(pdev, 0);
  58. if (pdata->irq < 0)
  59. {
  60.   printk("[err]unable get irq fdma\n");
  61.   return -1;
  62. }

  63. init_waitqueue_head(&pdata->read_queue);
  64. return 0;
  65. }
复制代码
of_fdma_daa函数用于从设备树读取各项参数。
行15,从设备树读取mem_addr关键词,为fdma的基地址。
行22,从设备树读取mem_size关键词,为fdma的缓存区总大小。
行29,从设备树读取num-buf关键词,为fdma的缓存区个数。
行35~64,从设备树读取start和reset管脚,分别用作fpga数据采集的启停控制和复位控制。
行66,从设备树读取fdma的中断号,用作之后的中断申请。
3:IOCTL控制
  1. static long fdma_func_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
  2. {
  3. int ret = 0;
  4. int __user *user_arg = (int __user *)arg;
  5. int flag;

  6. switch (cmd)
  7. {
  8. case CMD_CLEANBIT:
  9.   if (fdma_data->type == FDMA_TYPE_ADC)
  10.   {
  11.    fdma_data->pkg_done_cnt--;
  12.   }
  13.   else
  14.   {
  15.    if (put_user(fb_switch, user_arg))
  16.     return -EFAULT;
  17.   }
  18.   break;

  19. case CMD_SET_RBUF:
  20.   if (get_user(flag, user_arg))
  21.    return -EFAULT;
  22.   ret = regmap_write(fdma_data->fdma_regmap, FDMA_SET_RBUF, FDMA_BUFSET_MASK | flag);
  23.   if (ret < 0)
  24.   {
  25.    printk("set rbuf err\n");
  26.   }
  27.   break;

  28. case CMD_GET_NUMBUF:
  29.   ret = copy_to_user(user_arg, &fdma_data->num_buf, sizeof(int));
  30.   if (ret)
  31.    printk("copy to user num buf err\n");
  32.   break;

  33. case CMD_SET_START:
  34.   if (get_user(flag, user_arg))
  35.    return -EFAULT;

  36.   if (fdma_data->type == FDMA_TYPE_ADC)
  37.    gpiod_set_value_cansleep(fdma_data->start_gpio, flag);
  38.   break;

  39. default:
  40.   ret = -EFAULT;
  41.   break;
  42. }
  43. return ret;
  44. }
复制代码
IOCTL是一种应用程序直接控制驱动的方式,功能类似于字符设备的read/write函数。
CMD_CLEANBIT:设置/读取缓存区读取完毕标志。
CMD_SET_RBUF:设置读取缓存区个数。(目前弃用)
CMD_GET_NUMBUF:读取各类dma的缓存区个数。
CMD_SET_START:控制fpga采集的启停。
4:中断与poll函数
  1. static unsigned int fdma_func_poll(struct file *file, struct poll_table_struct *wait)
  2. {
  3. int mask = 0;

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

  10. return mask;
  11. }

  12. static irqreturn_t fdma_irq_handler(int irq, void *data)
  13. {
  14. int ret;

  15. // printk("irq fdma\n");
  16. wake_up_interruptible(&fdma_data->read_queue);
  17. fdma_data->irq_reprot = 1;
  18. if (fdma_data->type == FDMA_TYPE_VIDEO)
  19. {
  20.   ret = regmap_read(fdma_data->fdma_regmap, FDMA_SET_WBUF, &fb_switch);
  21.   if (ret < 0)
  22.   {
  23.    printk("irq WBUF err\n");
  24.   }
  25. }
  26. else
  27. {
  28.   fdma_data->pkg_done_cnt++;
  29.   if (fdma_data->pkg_done_cnt > fdma_data->num_buf)
  30.   {
  31.    gpiod_set_value(fdma_data->start_gpio, 0);
  32.    printk("adc done cnt big then 4\n");
  33.   }
  34. }
  35. return IRQ_HANDLED;
  36. }
复制代码
这两个函数是用来控制数据传输的,通过中断读取缓存号,然后通过poll来调整应用端的处理速度。
行20,每当中断发生就唤醒一次poll的等待队列。
行24,读取fdma的当前缓存区号。
行33~37,防止adc读取时越界。
5:应用程序分析
qt程序内集成了驱动调用的代码,位置在工程的mythread.cpp内,以下为qt调用驱动读取adc数据的相关代码。
  1. void GetDataThread::run()
  2. {
  3.     int fd;
  4.     int ret = 0;
  5.     char *cambuf = NULL;
  6.     char *tmp;
  7.     int mem_switch = 0;
  8.     int num_buf;
  9.     int start;
  10.     int fifo_switch = 0;
  11.     struct pollfd poll_fd;
  12.     dis_connect = false;

  13.     fd = open( "/dev/fdma_fun", O_RDWR);
  14.     if (fd < 0){
  15.         printf("can't open file fdma_fun\n");
  16.         return;
  17.     }

  18.     cambuf = (char *) mmap(0, 0x300000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE|MAP_LOCKED , fd, 0);
  19.     ioctl(fd, CMD_GET_NUMBUF, &num_buf);
  20.     printf("num buf = %d\n", num_buf);
  21.     poll_fd.events = POLLIN;
  22.     poll_fd.fd = fd;
  23.     fifo_switch = 0;
  24.     start = 1;
  25.     ioctl(fd, CMD_SET_START, &start);

  26.     while (1){
  27.         ret = poll(&poll_fd, 1, -1);
  28.         if (ret < 0){
  29.             printf("error\n");
  30.             break;
  31.         }
  32.         if (adc_fifo[fifo_switch]->mem_ready == true){
  33.             printf("mem err\n");
  34.             break;
  35.         }

  36.         tmp = cambuf + mem_switch*READ_SIZE;
  37.         memcpy((char *)(adc_fifo[fifo_switch]->data), tmp, READ_SIZE);
  38.         adc_fifo[fifo_switch]->mem_ready = true;
  39.         fifo_switch++;
  40.         fifo_switch = fifo_switch%3;
  41.         ioctl(fd, CMD_CLEANBIT, &mem_switch);
  42.         mem_switch = (mem_switch+1)%num_buf;
  43.     }

  44.     start = 0;
  45.     ioctl(fd, CMD_SET_START, &start);
  46.     mem_switch = 0;
  47.     fifo_switch = 0;
  48.     ::close( fd );
  49.     munmap(cambuf, 0x300000);
  50. }
复制代码
行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:主函数
主函数完成的工作主要是初始化所需资源,然后调用显示函数显示画面。
  1. int main(int argc, char **argv)
  2. {
  3. struct screen_prepare *screen_show = NULL;
  4. int ret;
  5. struct timeval tv;

  6. screen_show = (struct screen_prepare *)malloc(sizeof(struct screen_prepare));
  7. if (!screen_show)
  8. {
  9.   printf("malloc error\n");
  10.   return -1;
  11. }
  12. memset(screen_show, 0, sizeof(struct screen_prepare));

  13. screen_show->num_smwin = 0;

  14. /*初始化dp的drm,配置fb双缓存*/
  15. ret = drm_init(&screen_show->drm);
  16. if (ret < 0)
  17. {
  18.   printf("drm init err\n");
  19.   goto err_m1;
  20. }

  21. win_init(&screen_show->main_w, 0, 0, &screen_show->drm);
  22. screen_show->main_w.p_bpp = 32;

  23. screen_show->drm.front_buf = 0;

  24. gettimeofday(&tv, NULL);
  25. printf("microsecond star:%ld s  %ld us\n", tv.tv_sec, tv.tv_usec);

  26. show_ov5640(screen_show);
  27. drm_commit(&screen_show->drm);
  28. // sleep(30);
  29. clean_show(screen_show);

  30. return 0;
  31. err_m1:
  32. free(screen_show->sm_win);
  33. err_m0:
  34. free(screen_show);
  35. return -1;
  36. }
复制代码
行07~13,为显示空间申请内存,并初始化。
行17~23,初始化drm。
行25,主要用于设置输出画面的大小,输出内容。
行36,drm显示函数,用于绘制画面。
2:drm显示函数
  1. int show_ov5640(struct screen_prepare *prepare)
  2. {
  3. int fd;
  4. int ret = 0;
  5. long int BytesPerLine = 0;
  6. char *mbuf = NULL;
  7. char *tmp_mbuf = NULL;
  8. int fb_switch = 0;
  9. struct pollfd poll_fd[2];
  10. struct timeval tv;
  11. struct timeval tv_end;
  12. struct modeset_buf *drm_mod = &prepare->drm.bufs[prepare->drm.front_buf];
  13. uint32_t height = 720;
  14. uint32_t wideth = 1280;

  15. fd = open("/dev/fdma_fun", O_RDWR);
  16. if (fd < 0)
  17. {
  18.   printf("can't open file fdma_fun\n");
  19.   return (-1);
  20. }

  21. mbuf = (char *)mmap(0, 0x1800000, PROT_READ | PROT_WRITE,
  22.       MAP_SHARED | MAP_POPULATE | MAP_LOCKED, fd, 0);

  23. if (mbuf == -1)
  24. {
  25.   printf("mmap error\n");
  26.   ret = -1;
  27.   goto merr;
  28. }

  29. BytesPerLine = wideth * prepare->main_w.p_bpp / 8;
  30. printf("wideth:%d, height:%d\n", drm_mod->width, drm_mod->height);
  31. printf("pitch:%d, size:%d\n", drm_mod->pitch, drm_mod->size);
  32. printf("BytesPerLine %ld\n", BytesPerLine);

  33. poll_fd[0].events = POLLIN;
  34. poll_fd[0].fd = fd;
  35. poll_fd[1].events = POLLIN;
  36. poll_fd[1].fd = 0;

  37. memset(drm_mod->map, 0, drm_mod->size);

  38. while (1)
  39. {
  40.   ret = poll(poll_fd, 2, -1);
  41.   if (ret < 0)
  42.   {
  43.    printf("error\n");
  44.    break;
  45.   }
  46.   if (poll_fd[1].revents & POLLIN)
  47.    break;

  48.   ioctl(fd, CMD_CLEANBIT, &fb_switch);
  49.   fb_switch = (fb_switch + 2) % 3;
  50.   tmp_mbuf = mbuf + buff_off[fb_switch];
  51.   drm_mod = &prepare->drm.bufs[prepare->drm.front_buf];

  52. #if 0
  53.   for (int j=prepare->height-1; j>=0; j--){
  54.    for (int i = 0; i<prepare->wideth; i++){
  55.     //memcpy(prepare->map+prepare->pitch*j+i*3, mbuf+j*1280*4+i*4+1, 3);
  56.     prepare->map[prepare->pitch*j+i*3 + 0] = mbuf[j*1280*4+i*4 + 2];
  57.     prepare->map[prepare->pitch*j+i*3 + 1] = mbuf[j*1280*4+i*4 + 1];
  58.     prepare->map[prepare->pitch*j+i*3 + 2] = mbuf[j*1280*4+i*4 + 0];
  59.    }
  60.   }
  61. #else
  62.   if (BytesPerLine == drm_mod->pitch)
  63.   {
  64.    memcpy(drm_mod->map, tmp_mbuf, drm_mod->size);
  65.    // neon_memcpy( drm_mod->map, mbuf, drm_mod->size);
  66.   }
  67.   else
  68.   {
  69.    for (int j = height - 1; j >= 0; j--)
  70.    {
  71.     memcpy(drm_mod->map + drm_mod->pitch * j, tmp_mbuf + j * BytesPerLine, BytesPerLine);
  72.    }
  73.   }
  74. #endif
  75.   drm_commit(&prepare->drm);
  76.   prepare->drm.front_buf ^= 1;

  77.   // printf("fb_switch: %d\n", fb_switch);
  78.   // fb_switch = (fb_switch + 1) % 3;
  79. }
  80. all_ret:
  81. munmap(mbuf, 0xBFFFFF);
  82. merr:
  83. close(fd);
  84. // while (1);
  85. return ret;
  86. }
复制代码
行23~24,映射fdma的内存范围,单个fdma帧的大小为0x800000,三帧总大小为0x1800000.
行33~36,展示出画面的各项参数,
行47,poll函数实现阻塞。
行56,ioctl的作用是通知驱动当前帧已被读取。
行57,每次都读取当前帧的前一帧,目的是避免当前帧未完成导致的画面撕裂。
行71~82,内存拷贝将fdma的数据放入drm的内存中。
行84~85,通知drm刷新。
6 方案演示
6.1 硬件连线
1:板卡部分
1725968555935.jpg
6.2 程序测试
1:上电并串口登录
账户:uisrc,密码:root。
image.jpg
此时屏幕已显示开机登录命令行界面。
2:运行程序
输入命令:
cd camx2_fdma_hdmi/                                进入程序文件夹
sudo ./show                                                运行程序,输入密码:root
image.jpg
上图为了方便拍摄拔掉了部分线缆。
可以看到两个摄像头工作正常,目前有红蓝反色问题,可通过修改vivado信号解决。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则