近期花了很长时间在libcamera中查找和解决一个bug。下面将这段时间中的工作过程,以及对camera的认识总结如下:
首先是问题的发生,在UM2801中,摄像头的代码已经基本实现,并且相应功能也已经进行了完善。但是,在多次测试使用中发现,摄像头在preview模式下的显示会不经常性的出现屏幕分成两段的情况,上边一段为正常数据的垂直方向压缩图形(压缩为一半),下边一段则为上次开启摄像头时的旧数据的一半。由于情况不明,并且问题是随机发生,难以分析问题的出现到底是在上层还是在驱动层,所以采取从分排除的办法进行分析解决。
查找bug的方法是从整个框架的中间段进行分割查找。所以我首先从libcamera文件夹中的相关代码入手进行分析。 libcamera是上层调用摄像头以及设定摄像头功能模式的接口,是c++的最底端的代码。java层通过JNI接口调用C++层。C++中又分为服务端和客户端两个部分,最后调用到了libcamera中。Libcamera中有Seccamera.cpp, Seccamera.h, SecCameraHWInterface.cpp, SecCameraHWInterface.h四个文件。在SecCameraHWInterface.cpp中有Preview,TakePicture,Record三种模式,开启摄像头默认进入Preview模式,StartPreview函数会在进行相应格式和参数设置后创建一个m_previewThreadFunc()线程,在线程中进行图像预览的处理流程。而takePicture也采用了相似的创建线程处理的模式。而他们的主要功能的函数实现则在Seccamera.cpp文件中。在Seccamera.cpp中可以找到相应的设置格式,参数,以及不同模式的处理过程中的处理函数的实现函数。在这些函数中,他们通过ioctrl的方式控制和调用驱动层来实现相应功能。
通过代码分析,我发现在libcamera中并没有直接处理图像数据,它只是将参数和格式设置通过接口传递到驱动层,并将底层的buffer中的物理地址进行传递到上层的方式进行。具体的数据处理函数则在底层驱动中实现,这样既减少的处理时间,也提高了安全性。为了查看图像数据,我在Seccamera.cpp中通过mmap函数将底层的buffer映射到了上层的m_buffer_c中,然后再将图片数据取出查看。这里的相关代码已经有一部分代码已经有实现的现成代码,所以可以直接使用。
起初,我想通过对TakePicture中的功能进行改写的方式进行捕捉数据,因为这样方便取出单张图片数据。因此,我将TakePicture中的大部分代码注释掉了,然后模仿Preview模式中的方式进行改写。希望在不再重设camera寄存器的情况下,达到取到Preview模式下的图片的效果。但是,由于修改了TakePicture的处理流程中的处理流程,所以不能正确向上层返回,出现死机状况。而如果要正确返回数据需重设数据格式,重新设置摄像头格式后又不能取出需要查看的Preview格式的图片。最后只能抓取TakePicture模式下的JPG和yuyv文件。通过JPG图像和yuyv数据,只能确认在TakePicture模式下的数据为正确数据,不能做出最后判断。
于是,采取从Preview模式下取出数据的方式。通过分析Preview模式的处理流程,发现在这种模式下,它是一个不断进行的线程,在m_previewThreadFunc()线程中不断向上层传递新的数据的物理地址,LCD则才能通过地址调用下层获取新的图像进行显示。所以为了取出单张图片数据,我想通过创建一个全局变量,然后让变量的值满足某种条件来作为抓取数据的条件的方式来完成。但是,编译显示,在libcamera中不允许在其中创建其它的全局变量。所以后来想到了系统time(NULL)函数时间变量,它很好的解决了这个问题。但是,由于要抓取数据,它的数据处理的流程作了相应的改变,在运行中会出现与Preview模式的正常进行产生冲突的问题,抓取数据会出现死机,或者在不暂停数据传输的情况下抓取的数据错乱的情况。为了解决与Preview的正常进行产生冲突的问题。我有分析了TakePicture模式的处理流程,认真分析了预览模式下的实现流程,成功在getPreview函数中找到了安插捕捉图像的代码的地方,正确取出了Preview模式下的图像。由于抓取到的图像是samsung的NV12T模式的数据文件,我写了一个将NV12T图片数据转为RGB32的数据(我的电脑的framebuffer支持格式为RGB32),并将它输出到/dev/fb0中显示的函数。最后,成功查看到了Preview中libcamera的framebuffer原始数据。(归档文件文件夹中nv12t_rgb.c为nv12t转rgb32并输出的函数,yuyv_rgb.c为yuyv422转rgb32并输出的函数)。
通过抓取到图像后,可以确认图像也是在Preview模式下显示的画面一样的分段式的错误图像。因此,可以确认错误的图像最初应该产生在驱动层。因此,转入驱动层代码,开始进行分析。
二.驱动层cameara相关函数分析(fimc与v4l2)
在对驱动层camera相关代码进行阅读分析后。我了解到,在fimc_dev.c函数中注册了三个fimc控制器(fimc0,fimc1,fimc2),并且都注册为V4L2设备子系统(device-subdev)。不同的fimc控制的地方不同,fimc0实现数据从sensor中通过scaler由yuyv422格式转换成nv12t格式并且通过DMA方式传送到fimc0的memory中。而fimc1则从这个memory中取出数据,通过fimc1的scaler将数据转换成rgb32格式,并将它存储到LCD的memory中。fimc0的工作部分在fimc_capture.c中实现,fimc1中的工作部分则在fimc_output.c中实现。他们通过V4L2架构为上层提供接口,与上层连接的接口为统一接口,在v4l2_ioctrl.c中实现,通过不同的type,连接不同的接口和实现函数,如果type为V4L2_BUF_TYPE_VIDEO_CAPTURE,则连接fimc_capture.c的实现函数,如果type为V4L2_BUF_TYPE_VIDEO_OUTPUT,则连接fimc_capture.c的实现函数。然后通过fimc_v4l2.c中的fimc_v4l2_ops结构体,将v4l2的接口函数与fimc的实现函数连接,最后连接fimc_capture.c和fimc_output.c不同的实现函数。
在fimc_capture.c函数中,实现了libcamera的Seccamera.cpp中的调用函数,分配了四个用于存储图像数据的buffer,按照nv12t的格式又将数据分别存储为Y数据区和cbcr数据区。这里提供了PINGPONG存储模式和直接存储模式,而采用的是直接存储模式(上图为PINGPONG模式)。数据的传输采用的是DMA直接内存访问方式,在fimc_streamon_capture.c中首先进行数据源(src)和的格式和大小的设置,数据转换后的格式和大小以及转换方式的设置,然后开启数据接收。
fimc与sensor的连接则通过v4l2系统的subdev_call函数调用接口。在摄像头驱动gt2005.c中v4l2_subdev_ops结构体中连接了最终的实现函数。分为gt2005_video_ops和gt2005_core_ops两个部分。gt2005_core_ops为初始化,重设,控制相关实现。gt2005_video_ops为设置格式,大小等相关的实现。最后通过I2C实现寄存器的配置写入和读出,将设置参数写入摄像头寄存器中。
在分析驱动层代码的大致结构和功能后,我首先在streamon_capture中实现了将preview开启钱buffer中的旧数据擦除的功能。通过phys_to_virt函数将buffer的首地址由物理地址转为虚拟地址,然后擦除了四个buffer大小的数据空间。于是,由于没有旧的数据填充的情况下,在预览模式下可以看到在出现错误图像时的下半部旧数据图像变成黑屏了,这也说明了数据在传进buffer之前已经是一半的错误数据,即数据在进入buffer之前已经是有问题的了。于是我在streamon_capture的功能开启之后的fimc_hwset_enable_capture函数中将fimc的所有的寄存器配置情况通过printk打印出来,一一与datasheet中的配置说明进行比对。但是此次没有发现问题,这是对代码的调用情况还不完全理顺,以及轻易相信数据是streamon_capture的输出信息的缘故。所以,由于打印出来的寄存器配置正确,我将方向转向了buffer中的数据。于是我又在STREAM_PAUSE命令发生后,将buffer[0]的部分数据打印在终端,由于STREAM_PAUSE是在我在上层取出nv12t数据时需要执行调用的命令。所以,这时取出数据与我上层取出的数据达到了同步,我将终端打印出来的16进制的数据与与获取的用vim查看的nv12t的16进制数据进行逐个比对,结果开始由于我将大小搞错,取出的数据不全,比对不完整,判断数据没有发生问题。后来经提醒后改正数据大小,正确结果为数据与上层取出的数据一样,说明错误发生在数据传入buffer之前,这也更加证实了擦除旧数据后显示出半张压缩图像的判断。于是,由于之前认定寄存器配置没有出错,把方向转向了在sensor的配置和时钟的配置。在更换了一个摄像头进行实验后,发现仍然会出现这样的情况,说明不是摄像头的问题,转而指向了时钟出现问题。
在认为可能是时钟的问题后,simon用示波器对时钟进行了测量。通过计算,认为时钟配置正常,没有出现问题,这样便发现自己的方向发生了错误,前面可能产生了误判。
于是我只能往回重新推翻进行查找,在再次肯定上层取出的数据可以认定排除上层出错后,我再次分析了fimc中的代码和框架。并且将所有可能产生错误图像效果的情况的变量值全部输出到终端,进行一一比对。没有发现问题。但是,在对代码的梳理和对框架的理解中,我发现,在preview模式进行时,所有的fimc同时在工作中。并且由于他们是同时注册的v4l2子系统,所以,他们的许多代码都是合并在一起,共同使用的。他们通过不同的hw_ver参数和type进行连接不同的设置,实现各自不同的功能。也就是说,终端打印出来的信息是capture模式和output模式下的两种不同的打印信息的总合。同时发现fimc_regs.c中fimc寄存器配置的相关函数也同时被fimc0和fimc1调用着,分别在实现capture和output的功能。所以,我上次查看的寄存器配置信息有误,打印信息并不一定是capture的配置参数。
于是,我同样用printk函数的方式区分了capture方向的函数调用fimc_hwset_enable_capture函数时的打印信息和output方向的函数调用时打印信息,并取出了capture调用时的fimc0的配置情况进行了比对。此次,我发现,在出现错误图像和正确图像的多次比对中,S3C_CISCCTRL寄存器的信息值正好是两个不同的值,该寄存器的第25为的设置正好相反。datasheet显示,该位配置的功能为隔行读取。就此,确认错误的发生是在该寄存器的配置上发生的问题,发现了问题的发生处。
为了完全确认,我强制关闭了发生错误的配置(隔行读取)功能。经过多次实验,确认问题发生在此处。
三.问题原因
虽然能够确认问题发生的地方。但是,真正触发问题的原因并没有找到。而且,强制关闭驱动层的功能设置不是正确的解决问题的办法。所以,我向上开始查找问题发生的根本原因。在出现错误图片的地方,是由于开启了隔行读取,开启了V4L2_FIELD_INTERLACED功能,而这个功能则是由结构体v4l2_pix_format的field参数控制的,经过对代码的分析查找,这个结构体是由上层传递赋值的。在fimc_capture.c中fimc_s_fmt_vid_capturea函数通过V4L2接口将v4l2_format结构体复制给了v4l2_pix_format结构体,实现的数据传递。
最后转向,c++层libcamera中的Seccamera.cpp中的fimc_v4l2_s_fmt函数。在这个函数中,定义了v4l2_pix_format为pixfmt,但是它结构体中的所有元素,只有field没有初始化,在初始化完其它元素后,将它赋值给v4l2_format结构体,并且通过v4l2传递给了驱动层。由此导致了一个没有初始化的变量传递到了底层。由于是局部变量,在使用它时,它读取了申请的空间的的旧数据作为数据使用,导致了每次读取的数据都不一样。因此,在当读取到的数据等于隔行读取的值时(4),触发了设置隔行读取寄存器位的情况。于是出现了摄像头出现随即图像分段的错误情况。至此,问题根源发现。
问题解决办法:在fimc_v4l2_s_fmt函数中对field进行初始化为: pixfmt.field = V4L2_FIELD_ANY;
经过反复测试证实,问题解决。