CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2018/05/18/Linux摄像头驱动3——LCD显示/#more
Linux摄像头驱动学习第三篇,在Tiny4412的LCD上显示摄像头采集图像。
前面的UVC驱动,实现了在Ubuntu主机上显示摄像头采集的图像,但那不是最终目的,最终目的是在嵌入式设备上显示图像。
本篇博客尝试写一个应用程序,实现USB摄像头采集的图像在Tiny4412的LCD上显示。所以本篇算不上驱动开发,更多的是Linux环境下编程。
在开始应用编程之前,需要先准备好驱动,在LCD上显示摄像头图像,至少需要三个驱动:LCD驱动、背光驱动、UVC驱动。
这三个驱动都在前面写过了,只需要加载即可。以前都是使用的insmod xx.ko
进行动态加载驱动,每次开发板上电后,都需要手动/脚本里加载驱动,有点麻烦。
反观内核自带的驱动,使用make menuconfig
进入图形配置界面里,找到对应的驱动,可以设置为Y
(编译到内核)、M
(编译成模块)、N
(不编译)。当设置为Y
后,进入系统后,就自带了该驱动,不再需要手动加载。
前者常用于调试阶段,就算驱动有问题,内核崩溃了,下次内核还能正常启动,修改驱动后重新加载,很方便。
后者常用于发布阶段,加入到内核,就不能再修改了,也就少了一些加载驱动的操作。
本次就仿照内核的方式,使用make menuconfig
将驱动直接加到内核里。
在这之前需要理解三个文件:
driver/
下的每个目录都有,在内核配置时候,提供配置选项;driver/
下的每个目录都有,在编译的时候,判断是否加入内核;因此,以上三个文件,是主要影响内核模块编译的文件,后面只需要修改这三个文件即可。
1.创建驱动目录
在driver/
目录下新建一个hceng_drv
目录作为存放自己驱动源码的目录。
2.创建底层配置文件
在driver/hceng_drv/
下创建Kconfig
和Makefile
,并将驱动源码backlight_drv.c
、lcd_drv.c
、uvc_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
下方的tristate
、depends on
、default
、help
等为config
的属性,用于定义该菜单项的类型、依赖项、默认值、帮助信息等;
编辑Makefile
如下:
obj-$(CONFIG_BACKLIGHT) += backlight_drv.o
obj-$(CONFIG_LCD) += lcd_drv.o
obj-$(CONFIG_UVC) += uvc_drv.o
根据CONFIG_*
是y
、m
还是n
,再决定是否将后面的*.o
编译到内核。
这里的CONFIG_*
就是由最后的.config
决定。
hceng_drv/
上级是driver/
,因此修改driver/
下的Kconfig
和Makefile
。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/
make menuconfig
,在配置界面找到:Device Drivers --->
Hceng add driver --->
将添加的三个驱动勾选上,最后保存、退出,就将更改的内容写到了.config
。
最后重新make
编译,也得到添加自己驱动的内核。
也可以直接修改.config
配置文件,加入:
CONFIG_BACKLIGHT=y
CONFIG_LCD=y
CONFIG_UVC=y
再make
,也一样得到添加自己驱动的内核。
①:首先从摄像头获取数据放入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
管理每个子模块,将每个子模块放入链表,向上提供统一的操作接口,调用对应文件的操作函数。
使用结构体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 %}
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。
首先构建一个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
就包含了摄像头采集的图像数据。
前面的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 %}
这里依旧使用链表来管理,这里有三类转换:MJPEG转RGB、YUV转RGB、RGB转RGB,将它们都放到链表中,通过get_video_convert_format()
传入待转换的格式,从链表中依次查询谁支持该转换,如果支持,就得到p_video_convert
,就可以调用到对应的操作函数。
从这个例子中,稍微能感受到这个框架的优势,添加新格式的话,将变得很容易。
具体的链表操作和前面的差不多。
目前只是需要实现USB摄像头的MJPEG转RGB,所以暂时只对mjpeg2rgb.c
分析。
需要实现video_convert
里构造函数,其中转换的过程是调用的libjpeg
库,其转换流程在LCD驱动_5.测试程序中有详细的分析,这里只对两个不同点就行分析。
libjpeg
库自带的转换错误处理函数在出错时,会退出程序。但在将摄像头图像转换的过程中,某一帧出现问题,可以忽略过去,画面顶多卡顿一下,为了不让程序退出,需要自己定义错误处理函数,并绑定。//参考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 %}
如果源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
指针指向的位置,就是转换后的数据开始位置。
处理部分有两个操作,一个是图像的缩放,一个是将图片放在Framebuffer指定位置。
图像的缩放算法没有去深入研究,这里只简单的学习了下近邻取样插值缩放法
。
巧的是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 %}
使用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 %}
使用结构体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 %}
还是用链表的方式管理图像显示模块,这里的图像显示模块就一个LCD。
除了常规的注册、显示、获取ops的函数,还有选中指定显示模块并初始化select_and_init_disp_dev()
,获取显示设备的参数get_disp_resolution()
,获取显示设备的buf信息get_video_buf_for_disp()
,以及LCD显示flush_pixel_datas_to_dev()
。
lcd.c
里填充disp_operations
结构体的四个操作函数。
device_init()
里通过ioctl()
和mmap()
得到LCD的可变参数和映射地址,保存到disp_operations
结构体里;fb_show_pixel()
用来显示一个像素,根据BPP不同,对传入的颜色进行对应处理,放在基地址后的坐标偏移;fb_clean_screen()
用于全屏显示一种颜色,用于清屏;fb_show_page()
用于显示整屏图像,即将数据复制到显存位置;完成了以上各个模块的函数,现在就在主函数里组织起来。程序框图如下:
{% 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 %}
最后,还需要用Makefile将整个工程组织编译。这里总结一个通用的Makefile模板。
首先总结一些基础知识:
%.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",常用于文件后缀的修改
下面对本程序的Makefile进行分析。在本程序中Makefile分为3类:
1.顶层目录的Makefile
2.顶层目录的Makefile.build
3.各级子目录的Makefile
obj-y
来指定根目录下要编进程序去的文件、子目录外,主要是定义工具链、编译参数、链接参数(即文件中用export
导出的各变量);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
CFLAGS := -Wall -Werror -O2 -g
CFLAGS += -I $(shell pwd)/include
export CFLAGS
LDFLAGS := -lm -ljpeg
export LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := video2lcd
obj-y += main.o
obj-y += video/
obj-y += convert/
obj-y += process/
obj-y += display/
all :
make -C ./ -f $(TOPDIR)/Makefile.build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name “*.o”)
rm -f $(TARGET)
distclean:
rm -f $(shell find -name “.o")
rm -f $(shell find -name ".d”)
rm -f $(TARGET)
{% endcodeblock %}
built-in.o
;PHONY := __build
__build:
obj-y :=
subdir-y :=
include Makefile
__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)------)
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)------)
cur_objs := $(filter-out %/, $(obj-y))
dep_files := ( f o r e a c h f , (foreach f, (foreachf,(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = [email protected]
%.o : %.c
$(CC) ( C F L A G S ) − W p , − M D , (CFLAGS) -Wp,-MD, (CFLAGS)−Wp,−MD,(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
{% endcodeblock %}
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 %}
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.o
与main.o
链接生成新built-in.o
;
⑥最后依赖built-in.o
输出目标文件arm-linux-gnueabihf-gcc -o video2lcd built-in.o -lm -ljpeg
源码:
所有源码见GitHub。
参考文章:
Linux内核C语言中的面向对象
韦东山第三期项目视频_摄像头