Zephyr Video驱动之使用说明

Creative Commons
本作品采用知识共享署名

本文简要介绍如何使用Zephyr Video驱动。

本文通过分析Zephyr提供的摄像头示例,说明如何使用Zephyr Video驱动。

介绍

为了方便后文的理解有必要介绍一下Zephyr Video驱动已经支持内容和基本的结构。目前Zephyr的Video驱动支持摄像头和视频软件生成两种设备。摄像头的驱动分成了2部分,一部分是CSI,用于从摄像头读出数据,目前只实现了rt系列的CSI。一部分是sensor,用于控制摄像头,目前只实现了基于I2C的MT9M114。两部分都是以标准的Video驱动模型来实现,但sensor只提供控制功能,没有enqueue和dequeue,CSI部分会包装sensor部分的驱动,然后应用是通过CSI来驱动摄像头的。视频软件生成驱动是通过软件的方法生成指定大小的RGB565彩条,并提供了水平翻转的功能。本文摄像头的测试要使用mimxrt1064_evk开发版,彩条软件任意开发板都可以,包括qemu。
后续的Video驱动系列文章会对驱动实现进行分析以及如何添加新的Video驱动进行说明。

摄像头

配置选项

要使用Video驱动需要先在prj.conf中添加配置,作用如注释

1
2
3
4
5
6
7
8
# 启用Video驱动
CONFIG_VIDEO=y

#启用mt9m114 sensor
CONFIG_VIDEO_MT9M114=y

# 由于mt9m114会依赖I2C,因此需要启用I2C
CONFIG_I2C=y

目前Zephyr只实现了rt的CSI, 当我们配置了CONFIG_VIDEO=y后,构建系统会帮你自动开启CONFIG_VIDEO_MCUX_CSI,选中CSI驱动

设备树

在板子的dts中添加如下内容:

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
&lpi2c1 {
status = "okay";

mt9m114@48 {
compatible = "aptina,mt9m114";
reg = <0x48>;
label = "MT9M114";
status = "okay";

port {
mt9m114_ep_out: endpoint {
remote-endpoint = <&csi_ep_in>;
};
};
};
};

&csi {
status = "okay";
sensor-label = "MT9M114";

port {
csi_ep_in: endpoint {
remote-endpoint = <&mt9m114_ep_out>;
};
};
};

通过设备树说明sensor挂在lpi2c1下,地址为0x48,lable是设备节点名。csi节点下通过sensor-lable说明引用的sensor是MT9M114,一定要和sensor的lable一致。另外通过port将csi和sensor关联起来。

彩条生成

彩条生成是纯软件,不需要修改设备树, 添加下面的配置选项即可

1
2
CONFIG_VIDEO=y
CONFIG_VIDEO_SW_GENERATOR=y

测试代码

测试代码在sample/video/capture
west build -b mimxrt1064_evk samples/video/capture/ 编译出来的镜像将使用摄像头
west build -b qemu_x86 samples/video/capture/ 编译出来的镜像将使用彩条生成
下面通过分析测试代码来说明如何使用Video驱动,一些相关的概念和API说明可以参考Zephyr Video驱动之驱动模型

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
#define VIDEO_DEV_SW "VIDEO_SW_GENERATOR"		//彩条软件生成器的device name

#if defined(CONFIG_VIDEO_MCUX_CSI)
#define VIDEO_DEV DT_LABEL(DT_INST(0, nxp_imx_csi)) //摄像头CSI的device name
#endif

void main(void)
{
struct video_buffer *buffers[2], *vbuf;
struct video_format fmt;
struct video_caps caps;
const struct device *video;
unsigned int frame = 0;
size_t bsize;
int i = 0;

/* 默认使用彩条生成器测试 */
video = device_get_binding(VIDEO_DEV_SW);
if (video == NULL) {
LOG_ERR("Video device %s not found", VIDEO_DEV_SW);
return;
}

/* 如果有实际的video device,就使用实际的video device测试 */
#ifdef VIDEO_DEV
{
const struct device *dev = device_get_binding(VIDEO_DEV);

if (dev == NULL) {
LOG_ERR("Video device %s not found, "
"fallback to software generator.", VIDEO_DEV);
} else {
video = dev;
}
}
#endif

printk("- Device name: %s\n", VIDEO_DEV);

/* 获取video device的能力,因为是摄像头,因此只有一个out ep,所以只获取VIDEO_EP_OUT */
if (video_get_caps(video, VIDEO_EP_OUT, &caps)) {
LOG_ERR("Unable to retrieve video capabilities");
return;
}

printk("- Capabilities:\n");
while (caps.format_caps[i].pixelformat) {
const struct video_format_cap *fcap = &caps.format_caps[i];
/* fourcc to string */
printk(" %c%c%c%c width [%u; %u; %u] height [%u; %u; %u]\n",
(char)fcap->pixelformat,
(char)(fcap->pixelformat >> 8),
(char)(fcap->pixelformat >> 16),
(char)(fcap->pixelformat >> 24),
fcap->width_min, fcap->width_max, fcap->width_step,
fcap->height_min, fcap->height_max, fcap->height_step);
i++;
}

/* 获取Video device默认的支持的格式,可以根据caps中的格式用video_set_format进行修改 */
if (video_get_format(video, VIDEO_EP_OUT, &fmt)) {
LOG_ERR("Unable to retrieve video format");
return;
}

printk("- Default format: %c%c%c%c %ux%u\n", (char)fmt.pixelformat,
(char)(fmt.pixelformat >> 8),
(char)(fmt.pixelformat >> 16),
(char)(fmt.pixelformat >> 24),
fmt.width, fmt.height);

/* 计算一张Video buffer的大小,pitch是一行数据的字节数,乘于高度就是一张Video buffer的大小 */
bsize = fmt.pitch * fmt.height;

/* 分配Video buffer , 这里示例代码是按照2个Video buffer来分配的 */
for (i = 0; i < ARRAY_SIZE(buffers); i++) {
buffers[i] = video_buffer_alloc(bsize);
if (buffers[i] == NULL) {
LOG_ERR("Unable to alloc video buffer");
return;
}
/* 对于out ep来说,要将空的video buffer都加入到out ep buffer队列中 */
video_enqueue(video, VIDEO_EP_OUT, buffers[i]);
}

/* 开始进行Video抓取 */
if (video_stream_start(video)) {
LOG_ERR("Unable to start capture (interface)");
return;
}

printk("Capture started\n");

/* 循环做dequeue和enqueue */
while (1) {
int err;

/* 等待output ep 送出填有数据的Video buffer */
err = video_dequeue(video, VIDEO_EP_OUT, &vbuf, K_FOREVER);
if (err) {
LOG_ERR("Unable to dequeue video buf");
return;
}

/* 实际应用中,可以在这里将vbuf->buffer内的数据拿出来显示或者保存 */
printk("\rGot frame %u! size: %u; timestamp %u ms",
frame++, vbuf->bytesused, vbuf->timestamp);

/* Video buffer内数据被使用后,将Video buffer再次加入到out ep */
err = video_enqueue(video, VIDEO_EP_OUT, vbuf);
if (err) {
LOG_ERR("Unable to requeue video buf");
return;
}
}
}

从上面代码可以看到,一开始就定义了2个Video buffer ‘struct video_buffer *buffers[2]’,但对于实际应用来应该按照caps给出的个数来分配Video buffer, 这里没有出问题是因为刚好driver内部的min_vbuf_count就是2.正常的做法如下:

1
2
3
4
5
6
7
8
9
10
struct video_buffer **buffers;
buffers = malloc(sizeof(struct video_buffer *)*caps->min_vbuf_count);
for (i = 0; i < caps->min_vbuf_count; i++) {
buffers[i] = video_buffer_alloc(bsize);
if (buffers[i] == NULL) {
LOG_ERR("Unable to alloc video buffer");
return;
}
video_enqueue(video, VIDEO_EP_OUT, buffers[i]);
}

上面测试程序运行起来后会看到console输出

Found video device: CSI
width (640,640), height (480,480)
Supported pixelformats (fourcc):
- RGBP
Use default format (640x480)
Capture started
Got frame 743! size: 614400; timestamp 100740 ms

其中Got frame和timestamp会不断变化