Linux摄像头驱动3——LCD显示

CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2018/05/18/Linux摄像头驱动3——LCD显示/#more

Linux摄像头驱动学习第三篇,在Tiny4412的LCD上显示摄像头采集图像。

前面的UVC驱动,实现了在Ubuntu主机上显示摄像头采集的图像,但那不是最终目的,最终目的是在嵌入式设备上显示图像。
本篇博客尝试写一个应用程序,实现USB摄像头采集的图像在Tiny4412的LCD上显示。所以本篇算不上驱动开发,更多的是Linux环境下编程。

1.将驱动加入内核

在开始应用编程之前,需要先准备好驱动,在LCD上显示摄像头图像,至少需要三个驱动:LCD驱动、背光驱动、UVC驱动。
这三个驱动都在前面写过了,只需要加载即可。以前都是使用的insmod xx.ko进行动态加载驱动,每次开发板上电后,都需要手动/脚本里加载驱动,有点麻烦。
反观内核自带的驱动,使用make menuconfig进入图形配置界面里,找到对应的驱动,可以设置为Y(编译到内核)、M(编译成模块)、N(不编译)。当设置为Y后,进入系统后,就自带了该驱动,不再需要手动加载。

前者常用于调试阶段,就算驱动有问题,内核崩溃了,下次内核还能正常启动,修改驱动后重新加载,很方便。
后者常用于发布阶段,加入到内核,就不能再修改了,也就少了一些加载驱动的操作。

本次就仿照内核的方式,使用make menuconfig将驱动直接加到内核里。
在这之前需要理解三个文件:

  • Kconfigdriver/下的每个目录都有,在内核配置时候,提供配置选项;
  • Makefiledriver/下的每个目录都有,在编译的时候,判断是否加入内核;
  • .config:在源码根目录下,作为最终的内核编译的依据;

因此,以上三个文件,是主要影响内核模块编译的文件,后面只需要修改这三个文件即可。

  • 1.创建驱动目录
    driver/目录下新建一个hceng_drv目录作为存放自己驱动源码的目录。

  • 2.创建底层配置文件
    driver/hceng_drv/下创建KconfigMakefile,并将驱动源码backlight_drv.clcd_drv.cuvc_drv.c,也放在里面。
    编辑Kconfig如下:

#
# Backlight && LCD && UVC device configuration
#

menu "Hceng add driver"
	config BACKLIGHT
	tristate "Backlight support"
	default y  
	help
	  This is backlight driver to tiny4412 from hceng.
	 
	config LCD
	tristate "LCD support"
	depends on BACKLIGHT 
	default y 
	help
	  This is LCD driver to tiny4412 from hceng. 
	  
	config UVC
	tristate "UVC support"
	depends on BACKLIGHT && LCD
	default y 
	help
	  This is UVC driver to tiny4412 from hceng.
	  
endmenu

包含在menu/endmenu中的内容会成为Hceng add driver的子菜单;
每一个子菜单项都是由config来定义的;
congfig下方的tristatedepends ondefaulthelp等为config的属性,用于定义该菜单项的类型、依赖项、默认值、帮助信息等;

编辑Makefile如下:

obj-$(CONFIG_BACKLIGHT)	+= backlight_drv.o
obj-$(CONFIG_LCD)	    += lcd_drv.o
obj-$(CONFIG_UVC)	    += uvc_drv.o

根据CONFIG_*ym还是n,再决定是否将后面的*.o编译到内核。
这里的CONFIG_*就是由最后的.config决定。

  • 3.编辑上级配置文件
    这里我的hceng_drv/上级是driver/,因此修改driver/下的KconfigMakefile
    修改Kconfig,在menu/endmenu之间的任意位置添加:
source "drivers/hceng_drv/Kconfig"

这里的写的位置,会影响在make menuconfig,即这里写得比较靠前,在配置界面也是比较靠前。

修改Makefile,在任意位置添加:

obj-$(CONFIG_BACKLIGHT) 	+= hceng_drv/
obj-$(CONFIG_LCD)       	+= hceng_drv/
obj-$(CONFIG_UVC)       	+= hceng_drv/

  • 4.配置
    执行make menuconfig,在配置界面找到:
Device Drivers  --->
    Hceng add driver  ---> 

将添加的三个驱动勾选上,最后保存、退出,就将更改的内容写到了.config
最后重新make编译,也得到添加自己驱动的内核。

也可以直接修改.config配置文件,加入:

CONFIG_BACKLIGHT=y
CONFIG_LCD=y
CONFIG_UVC=y

make,也一样得到添加自己驱动的内核。

2.软件框架

①:首先从摄像头获取数据放入video_buf,数据的格式(YUV、MJPEG、RGB)和分辨率可能有多种;
②:Tiny4412的LCD仅支持RGB格式,因此需要数据格式转换
③:Tiny4412的LCD分辨率是800X480,因此可能需要大小缩放
④:根据LCD显示流程,必须要将显示数据写入显存(FrameBuffer);
⑤:最后LCD控制器会将显存数据自动搬运到LCD/VGA等显示设备上;

在应用编程中,要习惯面向对象编程(Object Oriented Programming),也就是把对象作为程序的基本单元,一个对象包含了数据操作数据的函数,在C语言中常常用结构体(struct)来实现。

关于Linux内核C语言中的面向对象的实现,可以参考这篇博客,介绍了如何C语言实现面向对象,也通过这个能稍微理解Linux驱动中的操作函数的原理。

这里,模仿内核的编程框架,为每个模块实现一个管理链表,模块对应加入链表,再调用对应的操作函数,框架如下:

这个框架,在这里暂时不能体会到它的优势,以后接触多了,应该就能感受了。

简单说明下这个框架,主要有四个模块:
video用于摄像头数据采集,convert用于格式转换、process用于缩放等操作、dispaly用于显示。
convert为例,有一个manager管理每个子模块,将每个子模块放入链表,向上提供统一的操作接口,调用对应文件的操作函数。

3.编程_获取摄像头数据

使用结构体video_device来表示摄像头设备,包含了设备的文件句柄、像素格式、分辨率、buf信息、操作函数等:
{% codeblock lang:c %}
typedef struct video_device {
int fd; //文件句柄
int pixel_format; //像素格式
int width, height; //分辨率:宽*高

int buf_count;     //buf数量
int buf_maxlen;    //每个buf最大长度
int buf_cur_index; //当前buf索引
unsigned char *video_buf[VIDEO_BUFFER_NUM]; //每个video buf的地址

//操作函数
p_video_operations p_video_fops;

}video_device, *p_video_device;

//摄像头设备的操作函数
typedef struct video_operations {
char *name;

int (*init_device)(char *dev_name, p_video_device p_video_dev);
int (*exit_device)(p_video_device p_video_dev);

int (*get_format)(p_video_device p_video_dev);

int (*get_frame)(p_video_device p_video_dev, p_video_buffer p_video_buf); 
int (*put_frame)(p_video_device p_video_dev, p_video_buffer p_video_buf); 

int (*start_device)(p_video_device p_video_dev);
int (*stop_device)(p_video_device p_video_dev);

struct video_operations *p_next;

}video_operations, *p_video_operations;
{% endcodeblock %}

使用结构体video_buffer来表示摄像头采集的数据,包含每帧数据信息、像素格式等:
{% codeblock lang:c %}
//图片像素的数据
typedef struct pixel_datas {
int width; //宽度: 一行有多少个像素
int height; //高度: 一列有多少个像素
int bpp; //一个像素用多少位来表示
int line_bytes; //一行数据有多少字节
int total_bytes; //所有字节数
unsigned char *pixel_datas_addr; //像素数据存储的地址
}pixel_datas, *p_pixel_datas;

//摄像头的数据
typedef struct video_buffer {
pixel_datas pixel_datas; //图片像素的数据
int pixel_format; //像素的格式
}video_buffer, *p_video_buffer;
{% endcodeblock %}

3.1 video_manager.c

video_manager.c主要功能是操作video_operations构成的链表,涉及的函数有:
{% codeblock lang:c %}
int register_video_ops(p_video_operations p_video_ops);
void show_video_ops(void);
p_video_operations get_video_ops(char *name);
{% endcodeblock %}
此外,通过video_device_init()初始化链表上指定设备节点的设备,通过video_init()注册设备。

对链表的操作比较简单,见GitHub。

3.2 v4l2.c

首先构建一个video_operations结构体,然后注册并具体实现函数的功能。
{% codeblock lang:c %}
//构造一个video_operations结构体
static video_operations v4l2_video_ops =
{
.name = “v4l2”,

.init_device  = v4l2_init_device,
.exit_device  = v4l2_exit_device,

.get_format   = v4l2_get_format,

.get_frame    = v4l2_get_frame_streaming,
.put_frame    = v4l2_put_frame_streaming,

.start_device = v4l2_start_device,
.stop_device  = v4l2_stop_device,

};

/* 注册这个结构体 */
int v4l2_init(void)
{
return register_video_ops(&v4l2_video_ops);
}
{% endcodeblock %}

首先是v4l2_init_device(),它的内容比较多,前面写uvc驱动的时候,对应用层的操作其实都脑补了一遍,包含的步骤如下:

1.VIDIOC_QUERYCAP:获取设备信息(是否为摄像头、名字、版本等)
2.VIDIOC_ENUM_FMT:查询支持哪些种格式
3.VIDIOC_S_FMT:设置设备使用何种格式
4.VIDIOC_REQBUFS:申请buf
5.根据接口类型进行对应操作(streaming接口需要映射)
  streaming接口:
    6.1查询分配的buf(获得每个buf大小、偏移)
    6.2映射buf到用户空间(将用户空间buf和内核空间buf 进行绑定)
    6.3将映射的buf放入驱动的buf队列
  readwrite接口:
    7.1准备read()所需参数

这样一系列操作后,p_video_dev就包含了几乎摄像头设备的所有信息。

  • 对于streaming接口,使用v4l2_get_frame_streaming()v4l2_put_frame_streaming()来获取数据。
    首先poll()查询是否有数据,使用VIDIOC_DQBUF从队列取出数据,最后再VIDIOC_QBUF放入队列。

  • 对于streaming接口,使用v4l2_get_frame_readwrite()来获取数据。

无论是何种方式,最后p_video_buf就包含了摄像头采集的图像数据。

4.编程_格式转换

前面的UVC驱动,通过USB设备描述符知道了摄像头图像数据格式是MJPEG,而LCD只支持RGB格式,且前面LCD驱动,设置的LCD为RGB32格式。因此这里需要把MJPEG转换成RGB32格式。

使用结构体video_convert来表示一种转换,包含名字、判断是否支持转换、转换等:
{% codeblock lang:c %}
typedef struct video_convert
{
char *name;
int (*judge_support)(int pixel_format_in, int pixel_format_out);
int (*convert)(p_video_buffer video_buf_in, p_video_buffer video_buf_out);
int (*convert_exit)(p_video_buffer video_buf_out);
struct video_convert *p_next;
} video_convert, *p_video_convert;
{% endcodeblock %}

4.1 convert_manager.c

这里依旧使用链表来管理,这里有三类转换:MJPEG转RGB、YUV转RGB、RGB转RGB,将它们都放到链表中,通过get_video_convert_format()传入待转换的格式,从链表中依次查询谁支持该转换,如果支持,就得到p_video_convert,就可以调用到对应的操作函数。
从这个例子中,稍微能感受到这个框架的优势,添加新格式的话,将变得很容易。
具体的链表操作和前面的差不多。

4.2 mjpeg2rgb.c

目前只是需要实现USB摄像头的MJPEG转RGB,所以暂时只对mjpeg2rgb.c分析。
需要实现video_convert里构造函数,其中转换的过程是调用的libjpeg库,其转换流程在LCD驱动_5.测试程序中有详细的分析,这里只对两个不同点就行分析。

  • 1.转换错误处理函数
    libjpeg库自带的转换错误处理函数在出错时,会退出程序。但在将摄像头图像转换的过程中,某一帧出现问题,可以忽略过去,画面顶多卡顿一下,为了不让程序退出,需要自己定义错误处理函数,并绑定。
    {% codeblock lang:c %}
    typedef struct my_error_mgr
    {
    struct jpeg_error_mgr pub;
    jmp_buf setjmp_buffer;
    }my_error_mgr, *p_my_error_mgr;

//参考libjpeg里的bmp.c,自定义的libjpeg库出错处理函数:
//默认的错误处理函数是让程序退出,这里不让程序退出
static void my_error_exit(j_common_ptr cinfo)
{
static char err_str[JMSG_LENGTH_MAX];

p_my_error_mgr my_err = (p_my_error_mgr)cinfo->err;

/* Create the message */
(*cinfo->err->format_message) (cinfo, err_str);
printf_debug("%s\n", err_str);

longjmp(my_err->setjmp_buffer, 1);

}

……

cinfo.err = jpeg_std_error(&jerr.pub); //绑定jerr错误结构体至jpeg对象结构体
jerr.pub.error_exit = my_error_exit; //设置为自己定义的出错处理函数

{% endcodeblock %}

  • 2.BPP转换
    前面LCD驱动里,将LCD设置为了RGB32(实际还是RGB24,多出来的没有使用),而摄像头采集的数据格式为RGB24,因此需要RGB24转RGB32。

如果源bpp和目标bpp一致,直接memcpy()复制,长度就是宽的像素个数x每个像素由3*8位构成/8位构成一字节:width\*(8+8+8)/8=width\*3
如果是24BPP转32BPP,需要把源数据变长:

{% codeblock lang:c %}
//把已经从JPG文件取出的一行像素数据,转换为能在显示设备上使用的格式
static int covert_one_line(int width, int scr_bpp, int dst_bpp,
unsigned char *scr_datas, unsigned char *dst_datas)
{
int i;
int pos = 0;
unsigned int red, green, blue, color;
unsigned short *dst_datas_16bpp = (unsigned short *)dst_datas;
unsigned int *dst_datas_32bpp = (unsigned int *)dst_datas;

if (scr_bpp != 24)
	return -1;

if (dst_bpp == 24)
	memcpy(dst_datas, scr_datas, width*3); //len=width*(8+8+8)/8=width*3
else
{
	for (i = 0; i < width; i++)
	{
		red   = scr_datas[pos++];
		green = scr_datas[pos++];
		blue  = scr_datas[pos++];
		if (dst_bpp == 32)
		{
			color = (red << 16) | (green << 8) | blue;
			*dst_datas_32bpp = color;
			dst_datas_32bpp++;
		}
		else if (dst_bpp == 16)
		{
			/* 565 */
			red   = red >> 3;
			green = green >> 2;
			blue  = blue>> 3;
			color = (red << 11) | (green << 5) | (blue);
			*dst_datas_16bpp = color;
			dst_datas_16bpp++;
		}
	}
}
return 0;

}
{% endcodeblock %}
即先定义一个unsigned int *类型的指针dst_datas_32bpp,先提取出r、g、b,再组成新格式,复制给指针指向的变量,由于是unsigned int *类型的指针,指针每增加1,实际移动32位,即刚好指向下一个像素。连续操作后,dst_datas指针指向的位置,就是转换后的数据开始位置。

5.编程_图像处理

处理部分有两个操作,一个是图像的缩放,一个是将图片放在Framebuffer指定位置。

5.1 zoom.c

图像的缩放算法没有去深入研究,这里只简单的学习了下近邻取样插值缩放法
巧的是LCD分辨率是800*480,摄像头采集的图片分辨率是640*480,两者的宽是一样的,实际上并没有用到缩放。
缩放的原理还是比较简单,图片 某个像素的长/宽 与 图片的长/宽 比值是始终不变的,根据这一规则,可以得到坐标的两个关系:

因此,已知缩放后图片中的任意一点(Dx, Dy),可以求得其对应的原图片中的点Sx=Dx*Sw/Dw,Sy=Dy*Sh/Dh,然后直接复制对应原图图像数据到对应的缩放后的图片位置。
此外,为了避免每行重复计算,先将Sx=Dx*Sw/Dw的计算结果保存下来,在每行的处理里直接调用。
{% codeblock lang:c %}
//近邻取样插值方法缩放图片
int pic_zoom(p_pixel_datas origin_pic, p_pixel_datas zoom_pic)
{
unsigned long x, y;
unsigned long scr_y;
unsigned char *scr, *dest;
unsigned long *src_x_table;
unsigned long dst_width = zoom_pic->width;
unsigned long pixel_bytes = origin_pic->bpp / 8;

printf_debug("src:\n");
printf_debug("%d x %d, %d bpp, data: 0x%x\n", origin_pic->width, origin_pic->height,
             origin_pic->bpp, (unsigned int)origin_pic->pixel_datas_addr);

printf_debug("dest:\n");
printf_debug("%d x %d, %d bpp, data: 0x%x\n", zoom_pic->width, zoom_pic->height,
             zoom_pic->bpp, (unsigned int)zoom_pic->pixel_datas_addr);

if (origin_pic->bpp != zoom_pic->bpp)
    return -1;

src_x_table = malloc(sizeof(unsigned long) * dst_width);
if (NULL == src_x_table)
{
    printf_debug("malloc error!\n");
    return -1;
}

for (x = 0; x < dst_width; x++) //生成表 src_x_table
    src_x_table[x] = (x * origin_pic->width / zoom_pic->width);

for (y = 0; y < zoom_pic->height; y++)
{
    scr_y = (y * origin_pic->height / zoom_pic->height);

    dest = zoom_pic->pixel_datas_addr + y * zoom_pic->line_bytes;
    scr  = origin_pic->pixel_datas_addr + scr_y * origin_pic->line_bytes;

    for (x = 0; x < dst_width; x++)
    {
        //原图座标: src_x_table[x],src_y       缩放座标: x, y
        memcpy(dest + x * pixel_bytes, scr + src_x_table[x]*pixel_bytes, pixel_bytes);
    }
}

free(src_x_table);

return 0;

}
{% endcodeblock %}

5.2 merge.c

使用pic_merge()函数来实现将图片放在Framebuffer指定位置。
前面得到了经过缩放(图片的宽和LCD的宽一致)的图片数据,知道了这个数据的地址,理论上直接放到Frambuffer的起始地址即可,这样图片会以LCD左上角为基点显示图片,显示出来效果如下图1,此情况理想的效果应该如图2所示;
如果图片缩放后宽和LCD的宽还不一致,且又把图片数据直接放到Frambuffer的起始地址,则显示效果如图3,此情况理想的效果应该如图4所示;

以图4的极端情况为例,要想图片居中显示,需要(x,y)的坐标,这个简单,用(LCD宽-图片宽)/2得到x,用(LCD高-图片高)/2得到y
还需要将以(0,0)为起点的图片数据,依次复制到以(x,y)为起点,新地址的偏移就是(x,y)前的全部数据。
{% codeblock lang:c %}
//将图片放在Framebuffer指定位置
int pic_merge(int x, int y, p_pixel_datas small_pic, p_pixel_datas big_pic)
{
int i;
unsigned char *scr;
unsigned char *dst;

if ((small_pic->width > big_pic->width)   || 
    (small_pic->height > big_pic->height) ||
	(small_pic->bpp != big_pic->bpp))
	return -1;

scr = small_pic->pixel_datas_addr;
//目标地址的偏移就是指定坐标之前的所有数据:y*每行数据+x的数据
dst = big_pic->pixel_datas_addr + y * big_pic->line_bytes + x * big_pic->bpp / 8;
for (i = 0; i < small_pic->height; i++)
{
	memcpy(dst, scr, small_pic->line_bytes);
	scr += small_pic->line_bytes;
	dst += big_pic->line_bytes;
}
return 0;

}
{% endcodeblock %}

6.编程_图像显示

使用结构体disp_operations来表示显示操作:
{% codeblock lang:c %}
typedef struct disp_operations {
char *name; //显示模块的名字
int x_res; //X分辨率
int y_res; //Y分辨率
int bpp; //一个像素用多少位来表示
int line_width; //一行数据占据多少字节
unsigned char *dis_mem_addr; //显存地址
int (*device_init)(char *name); //设备初始化函数
int (*show_pixel)(int pen_x, int pen_y, unsigned int color); //把指定座标的像素设为某颜色
int (*clean_screen)(unsigned int back_color); //清屏为某颜色
int (*show_page)(p_pixel_datas p_pixel_data); //显示一页,数据源自p_video_mem
struct disp_operations *p_next; //链表
}disp_operations, *p_disp_operations;
{% endcodeblock %}

6.1 disp_manager.c

还是用链表的方式管理图像显示模块,这里的图像显示模块就一个LCD。
除了常规的注册、显示、获取ops的函数,还有选中指定显示模块并初始化select_and_init_disp_dev(),获取显示设备的参数get_disp_resolution(),获取显示设备的buf信息get_video_buf_for_disp(),以及LCD显示flush_pixel_datas_to_dev()

6.2 lcd.c

lcd.c里填充disp_operations结构体的四个操作函数。

  • device_init()里通过ioctl()mmap()得到LCD的可变参数和映射地址,保存到disp_operations结构体里;
  • fb_show_pixel()用来显示一个像素,根据BPP不同,对传入的颜色进行对应处理,放在基地址后的坐标偏移;
  • fb_clean_screen()用于全屏显示一种颜色,用于清屏;
  • fb_show_page()用于显示整屏图像,即将数据复制到显存位置;

7.编程_主函数

完成了以上各个模块的函数,现在就在主函数里组织起来。程序框图如下:

{% codeblock lang:c %}
static void print_help(void)
{
printf(“Usage: video2lcd [options]… [FILE]…\n”);
printf(“The LCD displays the image captured by the camera.\n”);
printf(“Options:\n”);
printf("\t" “-v” “\t\tSelect the camera device, default: /dev/video0\n”);
printf("\t" “-d” “\t\tSelect the lcd display device, default: /dev/fb0\n”);
printf("\t" “-h” “\t\tDisplay this information.\n”);
}

static void stop_app(int signo)
{
printf("\nexit.\n");

_exit(0);

}

int main(int argc, char **argv)
{
int i, ret;
float k;
video_device video_dev;
p_video_convert video_conv;
int pixel_formt_of_video, pixel_formt_of_disp;

int top_left_x, top_left_y;
int lcd_width, lcd_height, lcd_bpp;

p_video_buffer video_buf_cur;
video_buffer video_buf, convert_buf, zoom_buf, frame_buf;

char *get_argv[2] = {};

signal(SIGINT, stop_app);

//0.传入参数判断
for(i = 1; i < argc; i++)
{
    if (!strcmp("-v", argv[i]))
    {
        if(NULL == argv[i + 1])
        {
            print_help();
            return -1;
        }
        else
            get_argv[0] = argv[i + 1];
    }
    else if (!strcmp("-d", argv[i]))
    {
        if(NULL == argv[i + 1])
        {
            print_help();
            return -1;
        }
        else
            get_argv[1] = argv[i + 1];
    }
    else if (!strcmp("-h", argv[i]))
    {
        print_help();
        return 0;
    }
}

//1.初始化显示设备并获取显示设备参数
display_init(); //注册所有显示设备(fb和crt)

if (get_argv[1] == NULL) //选择和初始化指定的显示设备
    select_and_init_disp_dev("lcd", "/dev/fb0"); //default:lcd的/dev/fb0
else
    select_and_init_disp_dev("lcd", get_argv[1]);

get_disp_resolution(&lcd_width, &lcd_height, &lcd_bpp); //获取设备的分辨率和支持的bpp
get_video_buf_for_disp(&frame_buf); //得到显存的各种信息(分辨率、bpp、大小、地址等)

pixel_formt_of_disp = frame_buf.pixel_format;

//2.初始化采集设备
video_init(); //注册所有图像采集设备(v4l2协议)

ret = video_device_init(get_argv[0], &video_dev); //初始化指定的/dev/video0
if (ret)
{
    printf_debug("video_device_init for %s error!\n", get_argv[0]);
    return -1;
}
pixel_formt_of_video = video_dev.p_video_fops->get_format(&video_dev); //获取视频格式

//3.转换初始化
video_convert_init(); //注册所有支持的转换方式(yuv、mjpeg、rgb)
//传入采集设备格式和显示设备支持格式,在链表里依次判断是否支持该格式转换
video_conv = get_video_convert_format(pixel_formt_of_video, pixel_formt_of_disp);
if (NULL == video_conv)
{
    printf_debug("Can not support this format convert\n");
    return -1;
}


//4.启动摄像头设备
ret = video_dev.p_video_fops->start_device(&video_dev);
if (ret)
{
    printf_debug("start_device for %s error!\n", get_argv[0]);
    return -1;
}

memset(&video_buf, 0, sizeof(video_buf));
memset(&convert_buf, 0, sizeof(convert_buf));
convert_buf.pixel_format    = pixel_formt_of_disp;
convert_buf.pixel_datas.bpp = lcd_bpp;


memset(&zoom_buf, 0, sizeof(zoom_buf));

while (1)
{
    //5.读入摄像头数据
    ret = video_dev.p_video_fops->get_frame(&video_dev, &video_buf);
    if (ret)
    {
        printf_debug("get_frame for %s error!\n", get_argv[0]);
        return -1;
    }
    video_buf_cur = &video_buf;

    if (pixel_formt_of_video != pixel_formt_of_disp) //采集的图像格式和显示的图像格式不一致
    {
        //6.格式转换
        ret = video_conv->convert(&video_buf, &convert_buf);
        if (ret)
        {
            printf_debug("convert for %s error!\n", get_argv[0]);
            return -1;
        }
        video_buf_cur = &convert_buf;
    }
    //现在video_buf_cur就是最后的图像数据


    //7.如果图像分辨率大于LCD, 缩放
    if ((video_buf_cur->pixel_datas.width > lcd_width) || (video_buf_cur->pixel_datas.height > lcd_height))
    {
        //确定缩放后的分辨率
        //把图片按比例缩放到video_mem上, 居中显示
        //1. 先算出缩放后的大小
        k = (float)video_buf_cur->pixel_datas.height / video_buf_cur->pixel_datas.width; //长宽比例
        zoom_buf.pixel_datas.width  = lcd_width;
        zoom_buf.pixel_datas.height = lcd_width * k;
        if ( zoom_buf.pixel_datas.height > lcd_height)
        {
            zoom_buf.pixel_datas.width  = lcd_height / k;
            zoom_buf.pixel_datas.height = lcd_height;
        }

        zoom_buf.pixel_datas.bpp         = lcd_bpp;
        zoom_buf.pixel_datas.line_bytes  = zoom_buf.pixel_datas.width * zoom_buf.pixel_datas.bpp / 8;
        zoom_buf.pixel_datas.total_bytes = zoom_buf.pixel_datas.line_bytes * zoom_buf.pixel_datas.height;

        if (!zoom_buf.pixel_datas.pixel_datas_addr)
        {
            zoom_buf.pixel_datas.pixel_datas_addr = malloc(zoom_buf.pixel_datas.total_bytes);
            if (NULL == zoom_buf.pixel_datas.pixel_datas_addr)
                return -1;
        }

        pic_zoom(&video_buf_cur->pixel_datas, &zoom_buf.pixel_datas);
        video_buf_cur = &zoom_buf;
    }

    //合并进framebuffer
    //接着算出居中显示时左上角坐标
    top_left_x = (lcd_width - video_buf_cur->pixel_datas.width) / 2;
    top_left_y = (lcd_height - video_buf_cur->pixel_datas.height) / 2;

    pic_merge(top_left_x, top_left_y, &video_buf_cur->pixel_datas, &frame_buf.pixel_datas);

    flush_pixel_datas_to_dev(&frame_buf.pixel_datas);

    ret = video_dev.p_video_fops->put_frame(&video_dev, &video_buf);
    if (ret)
    {
        printf_debug("put_frame for %s error!\n", get_argv[0]);
        return -1;
    }

    //把framebuffer的数据刷到LCD上, 显示
}

return 0;

}
{% endcodeblock %}

8. Makefile

最后,还需要用Makefile将整个工程组织编译。这里总结一个通用的Makefile模板。

8.1 基础知识

首先总结一些基础知识:

  • 常用通配符
%.o  ——> 表示所有的.o文件
%.c  ——> 表示所有的.c文件
$@   ——> 表示目标
$<   ——> 表示第1个依赖文件
$^   ——> 表示所有依赖文件
  • 常用变量
:=   ——> 即时变量,它的值在定义的时候确定;(可追加内容)
=    ——> 延时变量,只有在使用到的时候才确定,在定义/等于时并没有确定下来;
?=   ——> 延时变量, 如果是第1次定义才起效, 如果在前面该变量已定义则忽略;(不覆盖前面的定义)
+=   ——> 附加, 它是即时变量还是延时变量取决于前面的定义;
  • 常用参数
-Wp,-MD,xx.o.d  ——> 生成依赖xx.o.d
-I /xx          ——> 指定头文件(.h)目录xx
-L /xx          ——> 指定库文件(.so)目录xx
-Wall           ——> 打开gcc的所有警告
-Werror         ——> 将所有的警告当成错误进行处理
-O2             ——> 优化等级
-g              ——> gdb调试
  • 常用函数
$(foreach var,list,text)                ——> 将list里面的每个成员,都作text处理
$(filter pattern...,text)               ——> 在text中取出符合patten格式的值
$(filter-out pattern...,text)           ——> 在text中取出不符合patten格式的值
$(wildcard pattern)                     ——> pattern定义了文件名的格式,wildcard取出其中存在的文件
$(patsubst pattern,replacement,$(var))  ——> 从列表中取出每一个值,如果符合pattern,则替换为replacement

举例:
假设当前路径下有a.c b.c c.c Makefile四个文件,Makefile内容如下:

A = a b c 
B = $(foreach f, $(A), $(f).o)

C = a b c d/
D = $(filter %/, $(C))
E = $(filter-out %/, $(C))

files = $(wildcard *.c)

files2 = a.c b.c c.c d.c e.c  abc
files3 = $(wildcard $(files2))

dep_files = $(patsubst %.c,%.d,$(files2))

all:
	@echo B = $(B)
	@echo D = $(D)
	@echo E = $(E)
	@echo files = $(files)
	@echo files3 = $(files3)
	@echo dep_files = $(dep_files)

执行结果:

B = a.o b.o c.o                     //把A中每个成员加上后缀.o
D = d/                              //取出C中符合搜索条件"/"的成员,常用于取出文件夹
E = a b c                           //取出C中不符合搜索条件"/"的成员,常用于取出非文件夹
files = a.c b.c c.c                 //取出当前路径下的a.c b.c c.c三个文件,常用于得到当前路径的文件
files3 = a.c b.c c.c                //取出当前路径下存在的a.c b.c c.c三个文件,常用于判断文件是否存在
dep_files = a.d b.d c.d d.d e.d abc //替换符合条件".c"的文件为".d",常用于文件后缀的修改

8.2 Makefile分析

下面对本程序的Makefile进行分析。在本程序中Makefile分为3类:

1.顶层目录的Makefile
2.顶层目录的Makefile.build
3.各级子目录的Makefile

  • 1.顶层目录的Makefile
    它除了定义obj-y来指定根目录下要编进程序去的文件、子目录外,主要是定义工具链、编译参数、链接参数(即文件中用export导出的各变量);
    {% codeblock lang:Makefile %}

1.定义编译工具简写并声明(以变其它文件可使用)

CROSS_COMPILE = arm-linux-gnueabihf-
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm

STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

2.定义编译选项并声明(警告信息、优化等级、gdb调试、指定本程序头文件路径)

CFLAGS := -Wall -Werror -O2 -g
CFLAGS += -I $(shell pwd)/include
export CFLAGS

3.定义链接选项并声明(数学库、LibJPEG库)

LDFLAGS := -lm -ljpeg
export LDFLAGS

4.定义顶层目录路径并声明(shell命令实现)

TOPDIR := $(shell pwd)
export TOPDIR

5.程序目标文件

TARGET := video2lcd

6.使用"obj-y"表示各个目标文件,即过程中的所有.o文件(包含当前路径文件和当前路径下的文件夹)

obj-y += main.o
obj-y += video/
obj-y += convert/
obj-y += process/
obj-y += display/

7. 目标all:

7.1在-C指定目录下,执行指定路径下的文件(即在本路径执行Makefile.build)

7.2依赖"built-in.o"生成最终的目标文件

all :
make -C ./ -f $(TOPDIR)/Makefile.build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

8.目标clean:清除所有的.o文件和目标文件

clean:
rm -f $(shell find -name “*.o”)
rm -f $(TARGET)

9.目标distclean:清除所有的.o文件、.d文件(依赖文件)和目标文件

distclean:
rm -f $(shell find -name “.o")
rm -f $(shell find -name "
.d”)
rm -f $(TARGET)
{% endcodeblock %}

  • 2.顶层目录的Makefile.build
    把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o
    {% codeblock lang:Makefile %}

1.定义"PHONY"表示目标(目前包含一个目标:__build)

PHONY := __build

2.定义目标"__build"内容是下面的所有操作

__build:

3.定义"obj-y"表示当前路径的目标文件,定义"subdir-y"表示当前路径下目录的目标文件

obj-y :=
subdir-y :=

4.包含当前路径的Makefile(为了获取"obj-y"的内容)

include Makefile

5. 得到当前路径下各目录名

5.1filter函数从obj-y中筛选出含"/"的内容,即目录

5.2patsubst函数将上述结果中的"/“替换为空,subdir-y即为当前路径的目录名(不含”/")

__subdir-y := ( p a t s u b s t (patsubst %/,%, (patsubst(filter %/, $(obj-y)))
subdir-y += KaTeX parse error: Expected group after '_' at position 2: (_̲_subdir-y) #实测结…(warning ------debug info:subdir-y=$(subdir-y)------)

6.把"obj-y"都加上"/built-in.o"后缀

subdir_objs := ( f o r e a c h f , (foreach f, (foreachf,(subdir-y),KaTeX parse error: Expected 'EOF', got '#' at position 17: …f)/built-in.o) #̲实测结果:第一次为[video…(warning ------debug info:subdir_objs=$(subdir_objs)------)

7.得到"obj-y"中的非文件夹文件(即各个.o文件)

cur_objs := $(filter-out %/, $(obj-y))

8. 得到依赖文件(.d文件)

8.1foreach把前面的*.o文件变为.*.o.d(这是当前目录Makefile提供的数据)

8.2wildcard根据这些.d名字在当前路径查找,得到真正存在的.d文件

dep_files := ( f o r e a c h f , (foreach f, (foreachf,(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))

9.如果"dep_files"不为空,则包含(即包含了.d依赖文件,保证头文件修改后程序会重新编译)

ifneq ($(dep_files),)
include $(dep_files)
endif

10.新增目标(目前包含两个目标:__build和subdir-y的各个成员)

PHONY += $(subdir-y)

11.目标__build依赖于subdir-y各个成员和built-in.o

__build : $(subdir-y) built-in.o

12.对subdir-y的每个成员(即目录),都调用Makefile.build

$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build

13.built-in.o依赖当前路径下的.o和目录下的built-in.o(即将当前路径下的.o链接成built-in.o)

built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^

14.定义dep_file为所有的依赖

dep_file = [email protected]

15.所有的.o依赖于所有的.c,编译过程生成对应.d文件

%.o : %.c
$(CC) ( C F L A G S ) − W p , − M D , (CFLAGS) -Wp,-MD, (CFLAGS)Wp,MD,(dep_file) -c -o $@ $<

16.声明$(PHONY)是个假想目标

.PHONY : $(PHONY)
{% endcodeblock %}

  • 3.各级子目录的Makefile
    指定当前目录下需要编进程序去的文件;
    {% codeblock lang:Makefile %}

1.指定当前目录下需要编进程序去的文件

obj-y += color.o
obj-y += yuv2rgb.o
obj-y += rgb2rgb.o
obj-y += mjpeg2rgb.o
obj-y += jdatasrc-tj.o
obj-y += convert_manager.o
{% endcodeblock %}

  • 4.实际编译过程
    ①执行make,调用顶层Makefile,调用make -C ./ -f /work/drv/code/Makefile.build,执行Makefile.build

Makefile.build里调用make -C $@ -f $(TOPDIR)/Makefile.build对每个目录都执行Makefile.build

③以video目录为例,调用Makefile.build,会执行以下操作:
  - 编译每一个.c:
  arm-linux-gnueabihf-gcc -Wall -Werror -O2 -g -I /work/drv/code/include -Wp,-MD,.v4l2.o.d -c -o v4l2.o v4l2.c
  - 将所有.o链接成built-in.o
  arm-linux-gnueabihf-ld -r -o built-in.o v4l2.o video_manager.o
  
④完成对当前目录的内容编译后,再对当前路径的.c文件编译:
arm-linux-gnueabihf-gcc -Wall -Werror -O2 -g -I /work/drv/code/include -Wp,-MD,.main.o.d -c -o main.o main.c

⑤将各子目录生成的built-in.omain.o链接生成新built-in.o

⑥最后依赖built-in.o输出目标文件arm-linux-gnueabihf-gcc -o video2lcd built-in.o -lm -ljpeg

9. 实测效果及源码

  • 实测效果:
  • 源码:
    所有源码见GitHub。

  • 参考文章:
    Linux内核C语言中的面向对象
    韦东山第三期项目视频_摄像头

你可能感兴趣的:(Linux驱动,嵌入式基础,Linux应用)