简介
本期接上回的工程部署讲一下整个工程的框架逻辑以及相关的模块作用机制,包括mipi抓图像以及后续传递到显示屏的链路逻辑等。
一、核心逻辑
1、如何抓到图像数据并显示
首先能够从摄像头捕获画面并显示是一个相机最起码要能实现的功能。所以先要搞清楚整个链路的大致逻辑。
(1)Linux下的设备驱动系统的控制逻辑
绝大部分情况下,在Linux下对设备的控制,可以用"修改结构体参数,然后提交修改"完成,后面驱动会讲为什么,这里先记住这个原则。举几个例子:
|----------------|--------------------------|-------------------|------------------|--------------------|
| 设备 | V4L2 | Frame buffer | DRM(GPU | ALSA(音频相关 |
| 设备节点 | /dev/video* | /dev/fb* | /dev/dri/card* | /dev/snd/* |
| 主要结构体(太多,不完全列举 | v4l2_format,v4l2_control | fb_var_screeninfo | drm_mode_modeinf | snd_ctl_elem_value |
/* ----------------------------------V4L2 举例-------------------------------*/
struct v4l2_format fmt = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.fmt.pix = { .width = 1920, .height = 1080, .pixelformat = V4L2_PIX_FMT_YUYV },
}; //设置参数
ioctl(fd, VIDIOC_S_FMT, &fmt); // 提交
/*--------------------------------Framebuffer 举例---------------------------*/
struct fb_var_screeninfo var = {
.xres = 1024;
.yres = 768;
.bits_per_pixel = 32;
}; //设置参数
ioctl(fd, FBIOGET_VSCREENINFO, &var);// 提交
/*---------------------------------DRM 举例---------------------------------*/
struct drm_mode_modeinfo mode = {
.hdisplay = 1920,
.vdisplay = 1080,
.clock = 148500,
}; //设置参数
drmModeSetCrtc(fd, crtc_id, buffer_id, 0, 0, &connector_id, 1, &mode); // 提交
可以看到,这些设备都是通过修改相关结构体参数,然后通过ioctl(或类似函数)递交进行相关的修改,从而达到控制设备的效果。
本次工程用的v4l2以及framebuffer都是通过ioctl进行参数提交的,所以再说一下ioctl的函数原型:
int ioctl(int fd, unsigned long request, ... /* void *arg */);
-
- fd,调用open函数打开设备时获取的文件描述符(相当于一个设备的编号
-
- request,控制的指令,一般来说已经进行了相关的宏定义,网上一查就有
-
- arg, 可选参数,一般来说就是上面编辑完以后的结构体的指针
(2)设备参数设置
在了解了上述机制以后,想要控制相应的设备,就该先去找相关的存储属性的结构体并找到递交参数的API函数,这里直接给出:
V4L2:

相当于水源源不断流过来,你有count数量的桶,接满一桶就提走倒进容器里,往复循环。
Framebuffer:

最终设置里就可以写为:
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/types.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include "convertor.h"
#define WIDTH 800
#define HEIGHT 480
#define FMT V4L2_PIX_FMT_YUYV
#define COUNT 4
#define CLAMP(x) ((x) < 0 ? 0 : ((x) > 255 ? 255 : (x)))
int main(int argc, char **argv)
{
unsigned char *buffer_datas[COUNT];
int ret;
int v4l2_fd;
uint8_t rgb_data[800 * 480 * 3];
/*---------------------frame buffer初始化-------------------*/
// open -> 编辑结构体 -> 提交修改
int fb_fd = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo vinfo; //这里仅作演示,就不多调了,默认就可以用,只是不调整格式看起来会怪怪的
ret = ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); //直接提交
//图像存储变量,待会用来装摄像头送来的数据流
size_t fb_size = vinfo.yres_virtual * vinfo.xres_virtual * (vinfo.bits_per_pixel / 8);
uint16_t *fb_mem = (uint16_t *)mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
/*---------------------V4L2初始化-------------------*/
// open -> 编辑结构体 -> 提交修改
v4l2_fd = open("/dev/video0", O_RDWR);
//设置v4l2_froamt 设置结构体 -> 提交
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width = WIDTH;
format.fmt.pix.height = HEIGHT;
format.fmt.pix.pixelformat = FMT;
ret = ioctl(v4l2_fd, VIDIOC_S_FMT, &format); //提交
//设置v4l2_requestbuffers 设置结构体 -> 提交
struct v4l2_requestbuffers reqbuf;
reqbuf.count = COUNT;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf); //提交
//设置v4l2_buffer 设置结构体 -> 提交 ,
//这里因为需要四个buffer来轮流装填,所以用了个for循环,直接执行四次也行,比较憨罢了
struct v4l2_buffer buff;
buff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buff.memory = V4L2_MEMORY_MMAP;
for (int i = 0; i < COUNT; i++)
{
buff.index = i;
ret = ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buff);
ret = ioctl(v4l2_fd, VIDIOC_QBUF, &buff);
//实际上就是图上画的轮流工作的四个buffer
buffer_datas[i] = mmap(NULL, buff.length, PROT_READ, MAP_SHARED, v4l2_fd, buff.m.offset);
}
//启动流
int on = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(v4l2_fd, VIDIOC_STREAMON, &on);
while(1){
memset(&buff, 0, sizeof(buff));
buff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buff.memory = V4L2_MEMORY_MMAP;
ret = ioctl(v4l2_fd, VIDIOC_DQBUF, &buff); //把接满的buffer拿出来
Convert_YUYV_To_RGB565(buffer_datas[buff.index], fb_mem, WIDTH, HEIGHT); //把数据倒出
ret = ioctl(v4l2_fd, VIDIOC_QBUF, &buff); //把空容器放回去
}
for (int i = 0; i < COUNT; i++)
{
buff.index = i;
munmap(buffer_datas, buff.length);
}
close(v4l2_fd);
munmap(fb_mem, fb_size);
close(fb_fd);
return 0;
}
//底下是我做的其他后处理,等一下会讲,看上面就行
void Convert_YUYV_To_RGB888(uint8_t y, uint8_t u, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b){
int C = y;
int D = u - 128;
int E = v - 128;
*r = CLAMP(C + 1.402 * E);
*g = CLAMP(C - 0.344136 * D - 0.714136 * E);
*b = CLAMP(C + 1.772 * D);
}
uint16_t Convert_RGB888_To_RGB565(uint8_t r, uint8_t g, uint8_t b){
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
void Convert_YUYV_To_RGB565(uint8_t *ptr,uint16_t *fb_mem, int width , int height){
uint8_t Y0;
uint8_t U ;
uint8_t Y1;
uint8_t V ;
uint8_t R0, G0, B0, R1, G1, B1;
for(int i=0; i < width * height; i +=2){
Y0 = ptr[0];
U = ptr[1];
Y1 = ptr[2];
V = ptr[3];
Convert_YUYV_To_RGB888(Y0, U, V, &R0, &G0, &B0);
Convert_YUYV_To_RGB888(Y1, U, V, &R1, &G1, &B1);
fb_mem[i] = Convert_RGB888_To_RGB565(R0, G0, B0); //数组名实际上就是指针等价于 &()
fb_mem[i + 1] = Convert_RGB888_To_RGB565(R1, G1, B1);
ptr += 4;
}
}
可以编译运行一下,不出意外的话结果长这样,摄像头的流数据直接流向framebuffer:

2、格式转换
当然了,摄像头直接输出的数据是.raw格式或是.YUYV格式的原生数据,所以还不能直接使用,需要进行一轮格式转换,一般来说这个步骤可以交给编码器完成,这样更快,不过可迁移性就弱了,所以这里使用软转换。
假设使用软转换的话,就需要知道你的源格式长什么样,目标格式长什么样,这些信息都可以网上找到。比如说在这个demo里,我摄像头设置的YUYV422格式,我的屏支持RGB565(部署的时候没说是因为这两种基本都支持,没有修改的必要,假设你需要的是H.264或其他,就需要进行相应的修改了),转换如下:
即:

对应了上述demo的这里,具体函数定义可以看convertor.c

3、嵌入LVGL框架
在了解了如何将摄像头数据传送到屏幕并显示后,接下来的问题就在于如何将二者嵌入到LVGL的框架中,实际上思路也很简单,只要找到LVGL提供的众多API中能够作为其数据载体的即可,并非只有一种方案,这里只是其中一种实现方式。
摄像头数据方面,因为LVGL并不占用系统的摄像头资源,因此直接将上面的demo搬运进lvgl的初始化阶段即可;而framebuffer则被LVGL占用,需要将二者的程序逻辑进行整合,下面简要说明LVGL的刷屏机制:
LVGL中所有的元件都存在一个"占地面积"的属性,静态内容出现buffer的修改则进行刷屏,动态内容不受前者影响独立刷屏,刷新区域仅限该元件脚下的占地。那么将摄像头画面嵌入LVGL显示界面的两种思路即为:不断刷新静态内容或设置一系列buffer串轮询播放
-
- 不断刷新静态内容:不断激活图像组件或类似组件进行刷新,好处在于实时性较强,摄像头画面能够在最短时间刷新出来;但是坏处在于系统负担很重,操作实时性变弱
-
- 设置buffer序列循环播放:使用canvas或者动画组件填充其内存区域进行播放,好处在于独立于操作,实时性较强;坏处在于显示的画面并不等于摄像头当前的画面,动态时存在一定的粘滞感(不跟手)。同时,如果填充和LVGL读取的正好是一个缓冲区,则会出现轻微的画面撕裂(需要引入双buffer进行一次处理)。
-
- 当然,上述两种依赖于LVGL框架内的实现方法,在画面刷新率上都不及上一节demo的实现,追求极致的性能应该使用QT的实现方案,剔除多余的程序逻辑。
上篇程序使用的是第二种方案,因为粘滞感和画面撕裂的解决要比解决系统负载过高来的容易,基本思路是利用LVGL自带的动画框架。和gif图的播放类似,LVGL的动画框架的逻辑就是,使用一系列的buffer串,每个buffer存储一帧画面,从头播到尾,然后再次回到头部,往复循环(实际上序列里只有一帧也行,原地刷新,更快,但是撕裂程度也水涨船高):

详细的说明参见LVGL官方:
https://docs.lvgl.io/master/details/main-modules/animation.html

那么此时,原来demo中摄像头画面数据输入到frame buffer的操作现在就改成了将其放进图中的lv_image_dec_t帧描述结构体中,剩下的就交给LVGL框架处理了。
二、程序框架
了解了相关核心逻辑后,不难得出系统框架为:

1、画面延迟问题
当然,原生的框架下直接用会有之前提到的迟滞感问题,这是因为每一轮循环都会顺序处理所有的动效和逻辑,这就导致了画面送入程序后至少还需要等待一整个循环的时间才能显示到屏幕上。要想解决这个问题,可以使用多线程的方法。
所谓多线程,就是将其从原来的一个大循环拆分成多个小循环,每个小循环执行一小段时间,交替进行,如图:

这就意味着,画面不需要等待完整的循环周期才开始显示,而是拆分成若干个小份一点点显示出来,能够有效解决延迟问题。
2、画面撕裂问题
另外,之前提到我采用的方法的弊端里还有一个画面撕裂的问题,关于这里的处理,再多引一组buffer出来作为中间的缓冲,避免LVGL读取数组的时候数组遭到改动即可。也许是因为树莓派性能本身比较强,这个问题没有造成严重的观感影响,我就没有多此一举了。假设是使用主频不太高的MPU进行实践,也许会出现上述问题,可以按照这个方法解决一下。
总结
到这里,整个程序的框架就讲清楚了,剩下的诸如界面控件之类的就和老版本的LVGL没什么区别,官方的文档里也有很详细的说明,这里就不再赘述(如果需要讲的话也可以留言,没有的话下次讲驱动了)。