Zephyr Video驱动之实现分析

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

本文通过分析Zephyr已有的Video驱动,了解Video驱动实现原理。

本文着重分析Video驱动实现的模式,不说明实际硬件如何驱动。

Video buffer管理

内存定义

Zephyr Video驱动之驱动模型一文中已经提到过,Video驱动提供统一的接口实现完成Video buffer的管理。这部分实现的代码在driver/video/video_common.c中
Video_common内管理的buffer有两部分,一部分是struct video_buffer,一部分是video buffer实际装载数据的Buffer。其中vide_buffer是以结构体数组的形式管理,如下:

1
static struct video_buffer video_buf[CONFIG_VIDEO_BUFFER_POOL_NUM_MAX];

CONFIG_VIDEO_BUFFER_POOL_NUM_MAX在driver/video/Kconfig中默认为2, 实际应用中我们要根据Video driver的caps中规定的最小min_vbuf_count来在prj.conf中对VIDEO_BUFFER_POOL_NUM_MAX进行配置

1
2
3
config VIDEO_BUFFER_POOL_NUM_MAX
int "Number of maximum sized buffer in the video pool"
default 2

再来看struct video_buffer的内容,其实际存放数据的地方是buffer

1
2
3
4
5
6
7
struct video_buffer {
void *driver_data;
uint8_t *buffer;
uint32_t size;
uint32_t bytesused;
uint32_t timestamp;
};

因此在分配一个video_buffer的时候需要分配一片连续的内存给buffer指针,在video buffer管理中使用pool来进行分配,因此需要定义buffer用的pool

1
2
3
4
5
K_MEM_POOL_DEFINE(video_buffer_pool,
CONFIG_VIDEO_BUFFER_POOL_ALIGN,
CONFIG_VIDEO_BUFFER_POOL_SZ_MAX,
CONFIG_VIDEO_BUFFER_POOL_NUM_MAX,
CONFIG_VIDEO_BUFFER_POOL_ALIGN);

其中CONFIG_VIDEO_BUFFER_POOL_ALIGN之地的是从pool的对齐大小,默认是64字节。CONFIG_VIDEO_BUFFER_POOL_SZ_MAX指定的是Pool可分配最大的内存块大小,默认是1M,CONFIG_VIDEO_BUFFER_POOL_NUM_MAX指定的是Pool有多少个最大内存块,默认是2.所有的默认定义都在driver/video/Kconfig中。实际使用需要根据video driver的caps在prj.conf中配置,Pool的相关信息可参考Zephyr内存管理之Pool
在实际使用中CONFIG_VIDEO_BUFFER_POOL_SZ_MAX可以配置为最大一张画幅占用内存量,CONFIG_VIDEO_BUFFER_POOL_NUM_MAX前面已经提到了和需要的Video buffer数量一致。CONFIG_VIDEO_BUFFER_POOL_ALIGN是由硬件决定,例如RT的CSI就要求64字节对齐。
由于从pool内分配出来的memory是以struct k_mem_block管理,每个struct video_buffer要配备一个k_mem_block

1
static struct k_mem_block video_block[CONFIG_VIDEO_BUFFER_POOL_NUM_MAX];

代码分析

有了前面的分析

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
struct video_buffer *video_buffer_alloc(size_t size)
{
struct video_buffer *vbuf = NULL;
struct k_mem_block *block;
int i;

/* 在video_buffer数组中查找有效的video buffer */
for (i = 0; i < ARRAY_SIZE(video_buf); i++) {
if (video_buf[i].buffer == NULL) {
vbuf = &video_buf[i];
block = &video_block[i];
break;
}
}

if (vbuf == NULL) {
return NULL;
}

/* 从pool中为video_buffer分配数据存放buffer */
if (k_mem_pool_alloc(&video_buffer_pool, block, size, K_FOREVER)) {
return NULL;
}

/* 为buffer指定数据存放Buffer */
vbuf->buffer = block->data;
vbuf->size = size;
vbuf->bytesused = 0;

return vbuf;
}

void video_buffer_release(struct video_buffer *vbuf)
{
struct k_mem_block *block = NULL;
int i;

/* 查找出要释放video_buffer占用的block */
for (i = 0; i < ARRAY_SIZE(video_buf); i++) {
if (video_block[i].data == vbuf->buffer) {
block = &video_block[i];
break;
}
}

/* 将video buffer中buffer置空,该video buffer又处于可分配状态 */
vbuf->buffer = NULL;
/* 将block释放回pool */
k_mem_pool_free(block);
}

接口说明

视频驱动实现下面接口后注册到Device即可,并不是所有接口都要实现,分为必须实现和可选实现,实际要根据驱动的硬件来决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct video_driver_api {
/* 必须实现 */
video_api_set_format_t set_format;
video_api_get_format_t get_format;
video_api_stream_start_t stream_start;
video_api_stream_stop_t stream_stop;
video_api_get_caps_t get_caps;
/* 可选实现 */
video_api_enqueue_t enqueue;
video_api_dequeue_t dequeue;
video_api_flush_t flush;
video_api_set_ctrl_t set_ctrl;
video_api_set_ctrl_t get_ctrl;
video_api_set_signal_t set_signal;
};

彩条生成器

彩条生成器是Video驱动下一个软件生成器,通过分析这个驱动的实现,可以比较清晰的理解如何实现一个Video驱动。
Zephyr-Video驱动之驱动模型一文中我们已经知道要实现一个Video驱动只需要实现下面一组API即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const struct video_driver_api video_sw_generator_driver_api = {
.set_format = video_sw_generator_set_fmt,
.get_format = video_sw_generator_get_fmt,
.stream_start = video_sw_generator_stream_start,
.stream_stop = video_sw_generator_stream_stop,
.flush = video_sw_generator_flush,
.enqueue = video_sw_generator_enqueue,
.dequeue = video_sw_generator_dequeue,
.get_caps = video_sw_generator_get_caps,
.set_ctrl = video_sw_generator_set_ctrl,
#ifdef CONFIG_POLL
.set_signal = video_sw_generator_set_signal,
#endif
};

这组API实现后被注册到Device,应用层就可以通过设备名”VIDEO_SW_GENERATOR”使用该驱动了

1
2
3
4
DEVICE_AND_API_INIT(video_sw_generator, "VIDEO_SW_GENERATOR",
&video_sw_generator_init, &video_sw_generator_data_0, NULL,
POST_KERNEL, CONFIG_VIDEO_INIT_PRIORITY,
&video_sw_generator_driver_api);

本节内容含有fifo和work的使用,fifo在Zephyr内核对象-数据传递之FIFO-LIFO一文中可以找到详细说明,work本文中会附带简单说明如何工作。

设备初始化

无论哪种Video驱动都有fifo的初始化动作,再根据不同的驱动特性做一些特殊的初始化
设备初始化时初始化video_buffer queue和定期产生数据delay work

1
2
3
4
5
6
7
8
9
10
11
static int video_sw_generator_init(const struct device *dev)
{
struct video_sw_generator_data *data = dev->data;

data->dev = dev;
k_fifo_init(&data->fifo_in); //fifo_in是空闲video buffer的队列
k_fifo_init(&data->fifo_out); //fifo_out是有数据video buffer的队列
k_delayed_work_init(&data->buf_work, __buffer_work); //buf_work是定期产生Video数据的Work

return 0;
}

设置通知信号

驱动是否要实现可选,无论哪种Video驱动基本都是一样的模式
通过video_sw_generator_set_signal将通知信号保存到driver内,当有数据事件发生时通知注册者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int video_sw_generator_set_signal(const struct device *dev,
enum video_endpoint_id ep,
struct k_poll_signal *signal)
{
struct video_sw_generator_data *data = dev->data;

if (data->signal && signal != NULL) {
return -EALREADY;
}

data->signal = signal;

return 0;
}

数据产生

驱动必须实现,数据产生的API在每个Video驱动都存在,但每种驱动都不一样,例如摄像头的数据产生是CMOS感光抓取,然后有硬件提供,而这里的彩条生成器是由软件生成。
在初始化的时候注册了一个delayed work, 当定时到后会调用__buffer_work函数。
video_sw_generator_stream_start会启动这个work而video_sw_generator_stream_stop停止这个work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int video_sw_generator_stream_start(const struct device *dev)
{
struct video_sw_generator_data *data = dev->data;

/* delay work在启动后33ms会执行_buffer_work */
return k_delayed_work_submit(&data->buf_work, K_MSEC(33));
}

static int video_sw_generator_stream_stop(const struct device *dev)
{
struct video_sw_generator_data *data = dev->data;

k_delayed_work_cancel(&data->buf_work);

return 0;
}

在__buffer_work执行过程中会触发下一个33ms执行自己这个work,这样执行一次触发一次,每次间隔33ms,也就是30fps

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
static void __buffer_work(struct k_work *work)
{
struct video_sw_generator_data *data;
struct video_buffer *vbuf;

data = CONTAINER_OF(work, struct video_sw_generator_data, buf_work);
/* 触发33ms后再执行一次buf_work */
k_delayed_work_submit(&data->buf_work,
K_MSEC(1000 / VIDEO_PATTERN_FPS));

/* 取得空闲的Video buffer */
vbuf = k_fifo_get(&data->fifo_in, K_NO_WAIT);
if (vbuf == NULL) {
return;
}

/* 将彩条数据填入video buffer */
switch (data->pattern) {
case VIDEO_PATTERN_COLOR_BAR:
__fill_buffer_colorbar(data, vbuf);
break;
}

/* 将填好数据的video buffer放入fifo out */
k_fifo_put(&data->fifo_out, vbuf);

/* 如果使能了signal机制,则使用注册的信号通知目前有数据了 */
if (IS_ENABLED(CONFIG_POLL) && data->signal) {
k_poll_signal_raise(data->signal, VIDEO_BUF_DONE);
}

k_yield();
}

Video buffer的流转

驱动是否要实现可选,无论哪种Video驱动基本都是一样的模式,根据ep进行处理
使用video_sw_generator_enqueue将空闲的video buffer加入到fifo_in中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int video_sw_generator_enqueue(const struct device *dev,
enum video_endpoint_id ep,
struct video_buffer *vbuf)
{
struct video_sw_generator_data *data = dev->data;

if (ep != VIDEO_EP_OUT) {
return -EINVAL;
}

k_fifo_put(&data->fifo_in, vbuf);

return 0;
}

使用video_sw_generator_dequeue从fifo_out取出带有数据的Video buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int video_sw_generator_dequeue(const struct device *dev,
enum video_endpoint_id ep,
struct video_buffer **vbuf,
k_timeout_t timeout)
{
struct video_sw_generator_data *data = dev->data;

if (ep != VIDEO_EP_OUT) {
return -EINVAL;
}

*vbuf = k_fifo_get(&data->fifo_out, timeout);
if (*vbuf == NULL) {
return -EAGAIN;
}

return 0;
}

使用video_sw_generator_flush清空fifo,将所有传入的空闲buffer都转移到fifo_out中,该动作是要取消视频功能的前奏,让用户可以通过video_sw_generator_dequeue拿到所有的buffer。

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
static int video_sw_generator_flush(const struct device *dev,
enum video_endpoint_id ep,
bool cancel)
{
struct video_sw_generator_data *data = dev->data;
struct video_buffer *vbuf;


if (!cancel) {
/* cancel为0,表示要让所有空闲的Video buffer被使用,没有其它办法只有等待,靠数据来吧fifo_in内的Video bufer占用掉 */
do {
k_sleep(K_MSEC(1));
} while (!k_fifo_is_empty(&data->fifo_in));
} else {
/* cancel为1,将空闲的buffer直接转移到传出的fifo_out*/
while ((vbuf = k_fifo_get(&data->fifo_in, K_NO_WAIT))) {
k_fifo_put(&data->fifo_out, vbuf);
if (IS_ENABLED(CONFIG_POLL) && data->signal) {
// flush完成后,通过信号VIDEO_BUF_ABORTED通知用户数据已经ABORTED
k_poll_signal_raise(data->signal,
VIDEO_BUF_ABORTED);
}
}
}

return 0;
}

格式和能力

驱动必须实现,不同的驱动会有不同的实现方法。
彩条生成器提供video_sw_generator_set_fmt和video_sw_generator_get_fmt,在彩条生成器中set只是简单的保存格式到驱动内,获取就是将保存的格式送出,这里就不列出代码。实际格式生效的地方是在__fill_buffer_colorbar内填充的时候根据格式内容进行填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void __fill_buffer_colorbar(struct video_sw_generator_data *data,
struct video_buffer *vbuf)
{
int bw = data->fmt.width / 8;
int h, w, i = 0;
//根据格式的宽高进行填充
for (h = 0; h < data->fmt.height; h++) {
for (w = 0; w < data->fmt.width; w++) {
int color_idx = data->ctrl_vflip ? 7 - w / bw : w / bw; //根据CID的设置对填充进行翻转
if (data->fmt.pixelformat == VIDEO_PIX_FMT_RGB565) { //检查是否符合格式内色彩空间的要求
uint16_t *pixel = (uint16_t *)&vbuf->buffer[i];
*pixel = rgb565_colorbar_value[color_idx];
i += 2;
}
}
}

vbuf->timestamp = k_uptime_get_32();
vbuf->bytesused = i;
}

彩条生成器能力完全是由软件决定,被预设如下video_sw_generator_get_caps会直接将其送出,具体代码很简单就不再列出了

1
2
3
4
5
6
7
8
9
10
11
12
static const struct video_format_cap fmts[] = {
{
.pixelformat = VIDEO_PIX_FMT_RGB565,
.width_min = 64,
.width_max = 1920,
.height_min = 64,
.height_max = 1080,
.width_step = 1,
.height_step = 1,
},
{ 0 }
};

控制

驱动是否要实现可选,不同的视频驱动有不同的控制内容,需要不同的实现方法
彩条生成器只实现了设置视频翻转,是通用类别中的一种,代码非常简单,就是将释放翻转保存在驱动内,实际在软件填充彩条的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline int video_sw_generator_set_ctrl(const struct device *dev,
unsigned int cid,
void *value)
{
struct video_sw_generator_data *data = dev->data;

switch (cid) {
case VIDEO_CID_VFLIP:
data->ctrl_vflip = (bool)value;
break;
default:
return -ENOTSUP;
}

return 0;
}

摄像头驱动

一文中已经提到过Zephyr目前实现的是RT CSI + MT9M114 Sensor的模式,大体和彩条生成器的情况一样,这里主要分析不同的地方

Sensor

Sensor被抽象为一个只控制不提供数据的Video设备,MT9M114进行如下实现

1
2
3
4
5
6
7
static const struct video_driver_api mt9m114_driver_api = {
.set_format = mt9m114_set_fmt,
.get_format = mt9m114_get_fmt,
.get_caps = mt9m114_get_caps,
.stream_start = mt9m114_stream_start,
.stream_stop = mt9m114_stream_stop,
};

mt9m114_set_fmt是对实际的硬件操作,同I2C将格式配置给mt9m114,而mt9m114_get_fmt是将设置格式送出。mt9m114_stream_start和mt9m114_stream_stop也是写响应的寄存器让mt9m114启动或者停止工作。相关的细节代码不在本文介绍范围内。
mt9m114_get_caps是将mt9m114支持的format提供出去,这取决于mt9m114的硬件特性和驱动的实现程度,目前只提供了下面一种格式

1
2
3
4
5
6
7
8
9
10
11
12
static const struct video_format_cap fmts[] = {
{
.pixelformat = VIDEO_PIX_FMT_RGB565,
.width_min = 640,
.width_max = 640,
.height_min = 480,
.height_max = 480,
.width_step = 0,
.height_step = 0,
},
{ 0 }
};

由于mt9m114本身的数据需要通过CSI来读取,所以Sensor驱动无法直接提供数据,因此没有实现数据流转相关的API,而Sensor也作为CSI视频驱动的插件被使用。
mt9m114硬件本身是支持控制的,但由于驱动实现的程度不足,因此代码中并未去实现set_ctrl/get_ctrl

CSI

CSI是实现了比较完整视频设备驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const struct video_driver_api video_mcux_csi_driver_api = {
.set_format = video_mcux_csi_set_fmt,
.get_format = video_mcux_csi_get_fmt,
.stream_start = video_mcux_csi_stream_start,
.stream_stop = video_mcux_csi_stream_stop,
.flush = video_mcux_csi_flush,
.enqueue = video_mcux_csi_enqueue,
.dequeue = video_mcux_csi_dequeue,
.set_ctrl = video_mcux_csi_set_ctrl,
.get_ctrl = video_mcux_csi_get_ctrl,
.get_caps = video_mcux_csi_get_caps,
#ifdef CONFIG_POLL
.set_signal = video_mcux_csi_set_signal,
#endif
};

CSI主要是完成对数据的搬移处理,因此在视频设备控制上是直接使用Sensor的,我们可以理解为Sensor+CSI才构成一个完整的CSI视频设备
在CSI初始化时,会初始化ep的fifo,同时获取sensor的binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int video_mcux_csi_init(const struct device *dev)
{
const struct video_mcux_csi_config *config = dev->config;
struct video_mcux_csi_data *data = dev->data;
/* 初始化ep fifo */
k_fifo_init(&data->fifo_in);
k_fifo_init(&data->fifo_out);

CSI_GetDefaultConfig(&data->csi_config);

/* 获取sensor的binding */
if (config->sensor_label) {
data->sensor_dev = device_get_binding(config->sensor_label);
if (data->sensor_dev == NULL) {
return -ENODEV;
}
}

return 0;
}

在video_mcux_csi_set_fmt的时候会根据格式对CSI的硬件配置,再通过视频驱动配置sensor

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
static int video_mcux_csi_set_fmt(const struct device *dev,
enum video_endpoint_id ep,
struct video_format *fmt)
{
const struct video_mcux_csi_config *config = dev->config;
struct video_mcux_csi_data *data = dev->data;
unsigned int bpp = video_pix_fmt_bpp(fmt->pixelformat);
status_t ret;

if (!bpp || ep != VIDEO_EP_OUT) {
return -EINVAL;
}

//配置CSI
data->pixelformat = fmt->pixelformat;
data->csi_config.bytesPerPixel = bpp;
data->csi_config.linePitch_Bytes = fmt->pitch;
data->csi_config.polarityFlags = kCSI_HsyncActiveHigh | kCSI_DataLatchOnRisingEdge;
data->csi_config.workMode = kCSI_GatedClockMode; /* use VSYNC, HSYNC, and PIXCLK */
data->csi_config.dataBus = kCSI_DataBus8Bit;
data->csi_config.useExtVsync = true;
data->csi_config.height = fmt->height;
data->csi_config.width = fmt->width;

ret = CSI_Init(config->base, &data->csi_config);
if (ret != kStatus_Success) {
return -EIO;
}

ret = CSI_TransferCreateHandle(config->base, &data->csi_handle,
__frame_done_cb, data);
if (ret != kStatus_Success) {
return -EIO;
}

//配置Sensor,这里最后会呼叫到mt9m114_set_fmt
if (data->sensor_dev && video_set_format(data->sensor_dev, ep, fmt)) {
return -EIO;
}

return 0;
}

video_mcux_csi_get_fmt或以sensor的格式为主重新改写CSI的格式,如果没有sensor的格式才返回CSI的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int video_mcux_csi_get_fmt(const struct device *dev,
enum video_endpoint_id ep,
struct video_format *fmt)
{
struct video_mcux_csi_data *data = dev->data;

if (fmt == NULL || ep != VIDEO_EP_OUT) {
return -EINVAL;
}

//如果Sensor存在,使用Sensor的格式
if (data->sensor_dev && !video_get_format(data->sensor_dev, ep, fmt)) {
/* align CSI with sensor fmt */
return video_mcux_csi_set_fmt(dev, ep, fmt);
}

//Sensor不存在返回CSI的格式
fmt->pixelformat = data->pixelformat;
fmt->height = data->csi_config.height;
fmt->width = data->csi_config.width;
fmt->pitch = data->csi_config.linePitch_Bytes;

return 0;
}

video_mcux_csi_stream_start和video_mcux_csi_stream_stop也是同时要对CSI和sensor进行操作,代码如下

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
static int video_mcux_csi_stream_start(const struct device *dev)
{
const struct video_mcux_csi_config *config = dev->config;
struct video_mcux_csi_data *data = dev->data;
status_t ret;

ret = CSI_TransferStart(config->base, &data->csi_handle);
if (ret != kStatus_Success) {
return -EIO;
}

if (data->sensor_dev && video_stream_start(data->sensor_dev)) {
return -EIO;
}

return 0;
}

static int video_mcux_csi_stream_stop(const struct device *dev)
{
const struct video_mcux_csi_config *config = dev->config;
struct video_mcux_csi_data *data = dev->data;
status_t ret;

if (data->sensor_dev && video_stream_stop(data->sensor_dev)) {
return -EIO;
}

ret = CSI_TransferStop(config->base, &data->csi_handle);
if (ret != kStatus_Success) {
return -EIO;
}

return 0;
}

数据流转上video_mcux_csi_enqueue/video_mcux_csi_dequeue/video_mcux_csi_flush的操作除了和彩条生成器一样的处理fifo外,还会操控CSI硬件这里不再贴出代码,可以直接去代码中看。
video_mcux_csi_set_ctrl/video_mcux_csi_get_ctrl/video_mcux_csi_get_caps 是直接包装对sensor_dev驱动的使用,可以直接去代码中看。
对于CSI的数据处理其它没写出的部分都是和操作CSI硬件,涉及到NXP HAL API的使用以及中断的处理,和驱动实现的模式精密度不高,这里就不做详细说明了,这里附件说明下和数据信号通知相关的内容。
video_mcux_csi_set_signal设置通知信号和彩条生成器的写法一样,在通知的时候有下面几种情况:

  • CSI收完一帧数据在终端中回调__frame_done_cb,该函数会发送VIDEO_BUF_DONE信号通知应用取数据。
  • 在CSI中断发生时回调__frame_done_cb,如果接收到的数据是错误的会发送VIDEO_BUF_ERROR信号通知应用。
  • Flush的时候使用VIDEO_BUF_ABORTED通知数据已经ABORTE,这和彩条生成器一致。