Zephyr Video驱动之实现分析

本文通过分析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是以结构体数组的形式管理,如下:

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进行配置

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

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

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

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

static struct k_mem_block video_block[CONFIG_VIDEO_BUFFER_POOL_NUM_MAX];

代码分析 链接到标题

有了前面的分析

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即可,并不是所有接口都要实现,分为必须实现和可选实现,实际要根据驱动的硬件来决定

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即可

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"使用该驱动了

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

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内,当有数据事件发生时通知注册者

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

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

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中

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

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。

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内填充的时候根据格式内容进行填充:

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会直接将其送出,具体代码很简单就不再列出了

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 }
};

控制 链接到标题

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

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进行如下实现

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的硬件特性和驱动的实现程度,目前只提供了下面一种格式

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是实现了比较完整视频设备驱动:

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

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

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的格式

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进行操作,代码如下

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,这和彩条生成器一致。