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

转自:https://hceng.cn/2018/05/18/Linux%E6%91%84%E5%83%8F%E5%A4%B4%E9%A9%B1%E5%8A%A83%E2%80%94%E2%80%94LCD%E6%98%BE%E7%A4%BA/

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如下:

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

     

    #

    # 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如下:

 

1

2

3

 

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之间的任意位置添加:
     

    1

     

    source "drivers/hceng_drv/Kconfig"

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

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

 

1

2

3

 

obj-$(CONFIG_BACKLIGHT) += hceng_drv/

obj-$(CONFIG_LCD) += hceng_drv/

obj-$(CONFIG_UVC) += hceng_drv/

 

  • 4.配置
    执行make menuconfig,在配置界面找到:
     

    1

    2

     

    Device Drivers --->

    Hceng add driver --->

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

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

 

1

2

3

 

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信息、操作函数等:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

 

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;

 

使用结构体video_buffer来表示摄像头采集的数据,包含每帧数据信息、像素格式等:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

 

//图片像素的数据

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;

 

3.1 video_manager.c

video_manager.c主要功能是操作video_operations构成的链表,涉及的函数有:

 

1

2

3

 

int register_video_ops(p_video_operations p_video_ops);

void show_video_ops(void);

p_video_operations get_video_ops(char *name);


此外,通过video_device_init()初始化链表上指定设备节点的设备,通过video_init()注册设备。

 

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

3.2 v4l2.c

首先构建一个video_operations结构体,然后注册并具体实现函数的功能。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

 

//构造一个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);

}

 

首先是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来表示一种转换,包含名字、判断是否支持转换、转换等:

 

1

2

3

4

5

6

7

8

 

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;

 

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库自带的转换错误处理函数在出错时,会退出程序。但在将摄像头图像转换的过程中,某一帧出现问题,可以忽略过去,画面顶多卡顿一下,为了不让程序退出,需要自己定义错误处理函数,并绑定。

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

     

    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; //设置为自己定义的出错处理函数

  • 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,需要把源数据变长:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

 

//把已经从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;

}

即先定义一个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的计算结果保存下来,在每行的处理里直接调用。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

 

//近邻取样插值方法缩放图片

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;

}

 

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)前的全部数据。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

 

//将图片放在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;

}

 

6.编程_图像显示

使用结构体disp_operations来表示显示操作:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

 

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;

 

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.编程_主函数

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

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

 

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;

}

8. Makefile

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

8.1 基础知识

首先总结一些基础知识

  • 常用通配符

     

    1

    2

    3

    4

    5

     

    %.o ——> 表示所有的.o文件

    %.c ——> 表示所有的.c文件

    $@ ——> 表示目标

    $< ——> 表示第1个依赖文件

    $^ ——> 表示所有依赖文件

  • 常用变量

     

    1

    2

    3

    4

     

    := ——> 即时变量,它的值在定义的时候确定;(可追加内容)

    = ——> 延时变量,只有在使用到的时候才确定,在定义/等于时并没有确定下来;

    ?= ——> 延时变量, 如果是第1次定义才起效, 如果在前面该变量已定义则忽略;(不覆盖前面的定义)

    += ——> 附加, 它是即时变量还是延时变量取决于前面的定义;

  • 常用参数

     

    1

    2

    3

    4

    5

    6

    7

     

    -Wp,-MD,xx.o.d ——> 生成依赖xx.o.d

    -I /xx ——> 指定头文件(.h)目录xx

    -L /xx ——> 指定库文件(.so)目录xx

    -Wall ——> 打开gcc的所有警告

    -Werror ——> 将所有的警告当成错误进行处理

    -O2 ——> 优化等级

    -g ——> gdb调试

  • 常用函数

     

    1

    2

    3

    4

    5

     

    $(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内容如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

 

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)

 

执行结果:

 

1

2

3

4

5

6

 

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导出的各变量);

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

     

    # 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)

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

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    60

    61

    62

    63

     

    # 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 := $(patsubst %/,%,$(filter %/, $(obj-y)))

    subdir-y += $(__subdir-y)

    #实测结果:第一次为[video convert process display],后面每次都为空

    #$(warning ------debug info:subdir-y=$(subdir-y)------)

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

    subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

    #实测结果:第一次为[video/built-in.o convert/built-in.o ……],后面每次都为空

    #$(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 := $(foreach f,$(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 = .$@.d

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

    %.o : %.c

    $(CC) $(CFLAGS) -Wp,-MD,$(dep_file) -c -o $@ $<

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

    .PHONY : $(PHONY)

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

     

    1

    2

    3

    4

    5

    6

    7

     

    # 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

  • 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. 实测效果及源码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值