嵌入式软件工程师&&太极拳
全部博文(548)
分类: 嵌入式
2012-02-29 17:47:45
概述
Davinci的视频采集接口的驱动涉及到内容包括I2C,AD芯片,V4L2,视频采集等内容。下面主要分成视频采集接口描述,I2C和A/D芯片,V4L2采集驱动以及V4L2应用程序编程。
名词解释:
A-low:
YUV: 在现代彩色电视系统中,通常采用三管彩色摄像机或彩色CCD摄像机进行摄像,然后把摄得的彩色图像信号经分色、分别放大校正后得到RGB,再经过矩阵变换电路得到亮度信号Y和两个色差信号R-Y(即U)、B-Y(即V),最后发送端将亮度和色差三个信号分别进行编码,用同一信道发送出去。这种色彩的表示方法就是所谓的YUV色彩空间表示。
Auto focus:
Auto white balance: ut
Auto exposure:
ITU-R :
标准BT.656并行数据结构:
BT.656并行接口除了传输4:2:2的YCbCr视频数据流外,还有行、列同步所用的控制信号。如图所示,一帧图像数据由一个625行、每行1 728字节的数据块组成。其中,23~311行是偶数场视频数据,336~624行是奇数场视频数据,其余为垂直控制信号。
BT.656每行的数据结构如图所示。
图中,每行数据包含水平控制信号和YCbCr--视频数据信号。视频数据信号排列顺序为Cb-Y-Cr-Y。每行开始的288字节为行控制信号,开始的4字节为EAV信号(有效视频结束),紧接着280个固定填充数据,最后是4字节的SAV信号(有效视频起始)。
SAV和EAV信号有3字节的前导:FF、FF、00;最后1字节XY表示该行位于整个数据帧的位置及如何区分SAV、EAV。XY字节各比特位含义见图。
图中,最高位bit7为固定数据1;F=0表示偶数场,F=1表示奇数场;V=0表示该行为有效视频数据,V=1表示该行没有有效视频数据;H=0表示为SAV信号,H=1表示为EAV信号;P3~P0为保护信号,由F、V、H信号计算生成;P3=V异或H;P2=F异或H;P1=F异或V;P0=F异或V异或H。
CCIR 656:
REC656:
Optical black clamp:
Low-Pass Filter:
Culling:
CFA:
Dark Fram write:
SDTV/LDTV/HDTV:数字电视(Digital TV)包括数字HDTV、数字SDTV和数字LDTV三种。三者区别主要在于图像质量和信道传输所占带宽的不同。从视觉效果来看,数字 HDTV(1000线以上)为高清晰度电视(High Definition Television)的简称,图象质量可达到或接近35mm宽银幕电影的水平;SDTV(500-600线)即标准清晰度电视,主要是对应现有电视的分辨率量级,其图象质量为演播室水平;LDTV(200-300线)即普通清晰度电视,主要是对应现有VCD的分辨率量级。因为电视全数字化是今后的趋势,所以目前提HDTV以及SDTV、LDTV如无特别说明,均指全数字体制。
一 视频采集接口VPFE
Davinci芯片提供一个视频采集接口VPFE主要可以接CMOS/CCD/video decoder等,还有一个视频后端处理接口VPBE主要是接视频输出设备。这里主要讨论视频采集接口VPFE。VPFE接口的结构框图如下图所示:
涉及的模块主要有:
l CCDC控制器
l Preview 预览引擎
l Resizer模块
l H3A模块
l Histogram模块
1. CCDC控制器
CCDC控制器主要从CMOS/CCD中接收原始的视频数据,并且可以支持多种YUV视频格式。
2. preview预览引擎
预览引擎主要是传输从CMOS/CCD中原始的视频数据到YCbCr 422的显示设备或者编码器。通常预览引擎的数据输出到外部的显示/压缩设备如NTSC/PAL模拟编码器或者LCD上。
3. Resize模块
Resizer模块可以对图像进行裁剪和缩放功能。
二 I2C和A/D芯片
Davinci内置了I2C控制器和I2C总线,一般视频前端处理的A/D芯片都是挂载在I2C总线上,通过Davinci的I2C控制器对A/D芯片的I2C从设备进行读写操作。
Davinci的I2C控制器的内部框图如下图所示:
这里I2C只有2根信号线:SCL和SDA。SCL信号线产生clock时钟,SDA数据线通过内部的ICXSR/ICDXR和ICRSR/ICDRR发生和接收数据。
对于I2C在发送和接收数据的时候会产生START位和STOP位。当SCL为高时,SDA由高变低的时候产生START位;当SCL为低时,SDA由低变高的时候产生STOP位。另外I2C支持的数据格式有:7-bit地址模式,10-bit地址模式和Free data格式模式。
I2C外设可以产生下面几种中断事件:
I2C中断 |
发生事件 |
丢失仲裁中断AL |
当I2C仲裁丢失或者非法的START/STOP位发生 |
无应答中断NACK |
当I2C从接收器中没有接收到应答信号 |
寄存器可以访问中断ARDY |
当先前的编程地址,数据和命令已经执行和状态位已经更新了,I2C产生ARDY中断。这个中断让CPU知道I2C寄存器已经可以访问了。 |
接收中断ICRINT,ICRRDY |
当ICRSR寄存器接收的数据已经拷贝到ICDRR寄存器中的时候发送接收中断。可以让CPU来查询ICRRDY位来从ICDRR中读接收数据。 |
发送中断ICXINT和ICXRDY |
可以让CPU来查询ICXRDY位来往ICDXR中发送数据。 |
停止中断SCD |
当STOP位发生 |
AAS中断 |
当I2C发现它的slave地址或者地址全为0。 |
2.1 Davinci的I2C控制器驱动
Davinci的I2C控制器驱动包括I2C总线注册,I2C总线读写操作,I2C总线中断处理几个部分,主要代码集中在/driver/i2c/busses/i2c-davinci.c。
把I2C控制器注册到I2C总线上是把davinci的I2C适配器的数据结构注册到linux 内核的I2C子系统中。I2C适配器的数据结构struct i2c_davinci_adap。
static struct i2c_adapter i2c_davinci_adap = { .owner = THIS_MODULE, .name = "DAVINCI I2C adapter", .id = I2C_ALGO_EXP, .algo = &i2c_davinci_algo, .algo_data = NULL, .client_register = NULL, .client_unregister = NULL, }; |
static int __init i2c_davinci_init(void) { ……………………. init_waitqueue_head(&i2c_davinci_dev.cmd_wait); status = (int)request_region(I2C_BASE, I2C_IOSIZE, MODULE_NAME); i2c_set_adapdata(&i2c_davinci_adap, &i2c_davinci_dev); status = i2c_add_adapter(&i2c_davinci_adap); request_irq(IRQ_I2C, i2c_davinci_isr, 0, "i2c",&i2c_davinci_dev); ………………….. driver_register(&davinci_i2c_driver) platform_device_register(&davinci_i2c_device) return 0; } |
1. 初始化等待队列i2c_davinci_dev.cmd_wait
2. 为I2C寄存器组分配地址空间。这里可能有问题????????
3. 把 davinci的I2C的适配器挂入linux内核的I2C子系统中。
4. 注册I2C中断。
5. 注册linux设备模型。
I2C的读写操作主要的数据结构struct i2c_davinci_alog:
static struct i2c_algorithm i2c_davinci_algo = { .name = "DAVINCI I2C algorithm", .id = I2C_ALGO_EXP, .master_xfer = i2c_davinci_xfer, .smbus_xfer = NULL, .slave_send = NULL, .slave_recv = NULL, .algo_control = NULL, .functionality = i2c_davinci_func, }; |
I2C的主要操作在i2c_davinci_xfer函数中。这个函数主要实现代码片断如下:
static int i2c_davinci_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], int num) { ………………….. if ((ret = i2c_davinci_wait_for_bb(1, adap)) < 0) return ret; ………………….. for (count = 0; count < num; count++) { i2c_davinci_xfer_msg(adap, &msgs[count],(count == (num - 1))); } ………………….. } |
1. 判断I2C是否在忙
2. 通过i2c_davinci_xfer_msg函数来实现。
i2c_davinci_xfer > i2c_davinci_xfer_msg
static int 2c_davinci_xfer_msg(struct i2c_adapter *adap, struct i2c_msg *msg, int stop) { dev->regs->icsar = msg->addr; -----------------------------------1 if (msg->len == 0) { ------------------------------------------------2 dev->buf = &zero_byte; dev->buf_len = 1; } else { dev->buf = msg->buf; dev->buf_len = msg->len; } dev->regs->iccnt = dev->buf_len;
at = dev->regs->icivr; ----------------------------------------------------3
if (msg->flags & I2C_M_RD) -------------------------------------------------------4 dev->regs->icimr |= DAVINCI_I2C_ICIMR_ICRRDY_MASK; else dev->regs->icimr |= DAVINCI_I2C_ICIMR_ICXRDY_MASK;
dev->regs->icmdr = flag; --------------------------------------- 5
wait_event_timeout (dev->cmd_wait, dev->cmd_complete, DAVINCI_I2C_TIMEOUT);---6 ……………..
} |
1. 设置slave地址。
2. 设置数据包的长度。
3. 读ICIVR中断向量寄存器表示清0。
4. 使能接收和发送寄存器。
5. 设置ICMDR寄存器。设置IRS,MST和STT位,具体含义看datasheet。
6. 等待数据读写完成。
当读写的数据完成的时候,会产生中断,进入中断服务例程ISR。
static irqreturn_t 2c_davinci_isr(int this_irq, void *dev_id, struct pt_regs *reg) { while ((stat = dev->regs->icivr) != 0) { ------------------------------------------------1 switch(stat) { case DAVINCI_I2C_ICIVR_INTCODE_RDR: -----------------------------------2 if (dev->buf_len) { *dev->buf++ = dev->regs->icdrr; dev->buf_len--; if (dev->buf_len) { continue; } else { dev->regs->icimr &= ~DAVINCI_I2C_ICIMR_ICRRDY_MASK; } } break;
case DAVINCI_I2C_ICIVR_INTCODE_TDR: -----------------------------------3 if (dev->buf_len) { dev->regs->icdxr = *dev->buf++; dev->buf_len--; if (dev->buf_len) continue; else { dev->regs->icimr &= ~DAVINCI_I2C_ICIMR_ICXRDY_MASK; } } break; case DAVINCI_I2C_ICIVR_INTCODE_RAR: --------------------------------------------4 /*i2c_warn("i2c: RAR detected");*/ dev->regs->icstr |= DAVINCI_I2C_ICSTR_ARDY_MASK; i2c_davinci_complete_cmd(dev); break; } }
} |
1. 当有中断发生时候,icivr中断向量寄存器会告诉我们发生了那个中断。
2. 当接收中断发生的时候,从ICDRR寄存器中取数据,取完后清中断请求。
3. 发送中断处理。
4. 这里的中断说明先前的寄存器读写已经完成。这时候会唤醒等待队列i2c_davinci_dev.cmd_wait.
注意:中断处理程序要在ARDY中断(或者异常中断发生)才会唤醒等待队列。
2.2 ADV7180芯片驱动ADV7180芯片的驱动主要是通过I2C来操作ADV7180本身的寄存器来完成工作的。首先需要把ADV7180做为I2C的从设备注册到I2C总线上。
ADV7180芯片寄存器的读写都是通过I2C来操作的。I2C读写操作如下图所示:
对于写操作,开始位START后,先写入7bit slave地址和一个LSB位,在ACK答应位之后,写入SUB地址和数值,止到STOP位。也就是一个START位后,写入地址和n个数据止到STOP位。
对于读操作,开始位START后,先写入7bit slave地址和一个LSB位,在ACK应答位之后,写入SUB ADDR和等待一个应答位。在开始另个START位之后,才是读出的数据。
static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val) { int err = 0; struct i2c_msg msg[1]; unsigned char data[2]; if (!client->adapter) { err = -ENODEV; } else { msg->addr = client->addr; msg->flags = 0; msg->len = 2; //这里长度是2,即1个START位,发送2个数据,一个是reg,另一个是val。 msg->buf = data; data[0] = reg; data[1] = val; err = i2c_transfer(client->adapter, msg, 1); } return err; } |
static int i2c_read_reg(struct i2c_client *client, u8 reg, u8 * val) { int err = 0; struct i2c_msg msg[1]; unsigned char data[1]; if (!client->adapter) { err = -ENODEV; } else { msg->addr = client->addr; msg->flags = 0; msg->len = 1; msg->buf = data; data[0] = reg; err = i2c_transfer(client->adapter, msg, 1); if (err >= 0) { msg->flags = I2C_M_RD; err = i2c_transfer(client->adapter, msg, 1); if (err >= 0) { *val = data[0]; } } } return err; } |
三 V4L2采集驱动
对于视频采集,为了让上层应用程序能兼容很多不同的采集卡或者摄像头等设备,内核提供了一组同一的标准接口API,即V4L接口。应用程序只需要按照V4L接口标准来编写程序,那么就可以支持所以采用V4L驱动的设备。V4L是一套针对视频采集,音频radio等音视频设备的标准API。
在V4L的基础上,从Linux2.5内核开始发展了V4L2标准。V4L2的可扩展性和灵活性都得到大大的提高,并且能够支持更多的设备。V4L2对VL4进行了彻底的改造,很多关键的API发生了变化,所以V4L2和V4L不兼容。
Davinci的视频采集驱动采用V4L2 API来编写驱动。V4L2 API支持3种采集的方法:read/write, MMAP和USER POINTERS。我们的驱动只支持mmap的方式。
V4L2 API除了传统的字符设备的方法集外,还有很多的操作是特定的ioctl操作来实现的。对于mmap采集来说,比较常用的几个ioctl操作如下:
Ioctl操作 |
用法 |
VIDIOC_QUERYCAP |
查询设备参数 |
VIDIOC_CROPCAR |
设备图像裁剪和缩放的能力 |
VIDIOC_S_CROP |
获取或者设置当前裁剪方框的大小 |
VIDIOC_REQBUFS |
初始化mmap映射的内存 |
VIDIOC_QUERYBUFS |
内存初始化后查询buffer的状态 |
VIDIOC_QBUFS |
把buffer挂入驱动incoming队列 |
VIDIOC_DQBUFS |
从驱动outcoming队列中取buffer |
VIDIOC_STREAMON |
开始capture |
VIDIOC_STREAMOFF |
结束capture |
1. VIDIOC_REQBUFS初始化buffer
这个ioctl操作主要目的是初始化mmap需要用到的buffers。V4L2为了简化驱动编程,抽象了一个buffer的管理buffer的文件/driver/video_buffer.c 文件中。这里会调用到video_buffer.c文件中相关函数来完成buffers的初始化。
l 初始化1个video_queue->stream队列。
l 初始化1个vpfe->dma_queue队列。
l 通过__get_free_pages来配置buffers,这里要分配几个buffer,可以由用户程序来控制。
l 分配struct videobuf_buffer,n个地址空间。
2. Mmap操作
这里mmap操作调用video-buffer.c文件中video_mmap_mapper()函数来实现。对于mmap操作,驱动程序需要为映射的地址范围建立合适的页表,这里主要通过 remap_pfn_range和nopage操作来实现。
3. VIDIOC_QBUF
这个ioctl操作主要实现初始化的时候把空buffers挂入queue->strream队列中和vpfe->dma_queue队列中;当应用程序读了1个buffer数据之后,把这个buffer归还。
4. VIDIOC_STEAMON:
这个ioctl操作主要是开始capture操作。
l 首先调用videobuf_streamon()函数。遍历queue->stream队列,找到组成队列的大结构struct videobuf_buffer。
l 从vpfe->dma_queue队列中取出队列成员vpfe->nextFrm = vpfe->curFrm。
l 设置vpfe->curFrm->state = STATE_ACTIVE。
l 设置AD芯片和配置CCDC控制
l 设置CCDC的输入地址为vpfe->curFrm->boff
l 使能CCDC控制器。
5. VIDIOC_DQBUF:
应用程序通过这个ioctl调用来获取那个buffer数据已经准备好了,从驱动返回v4l2_buffer->index值。
l 从queue->stream队列中取一个video_buffer成员。
l 通过videobuf_waiton函数进行阻塞等待。
l 中断处理之后,把这个video_buffer成员从队列中删除,然后把video_buffer->index返回用户空间。
6. 中断处理
中断处理要注意“场“的概念。因为视频输入源的摄像头有分PROGRESSIVE和INTERLACED之分。PROGRESSIVE的摄像头一帧只有一场(field),而INTERLACED的摄像头一帧有2场,其中field=1即偶场,先执行,然后才是field=0的奇场,当奇场完成是需要唤醒等待队列,然后VIDIOC_DQBUF调用返回参数完成数据的读取。
下面是INTERLACED的摄像头的中断处理过程:
if (fid == 1) { if (!list_empty(&vpfe->dma_queue) && vpfe->curFrm == vpfe->nextFrm) { vpfe->nextFrm = list_entry(vpfe->dma_queue.next, --------------------1 struct videobuf_buffer, queue); list_del(&vpfe->nextFrm->queue); vpfe->nextFrm->state = STATE_ACTIVE; ccdc_setfbaddr( (unsigned long)vpfe->nextFrm->boff); }
if (fid == 0) { -------------------------------------------------2
if (vpfe->curFrm != vpfe->nextFrm) { vpfe->curFrm->state = STATE_DONE; wake_up_interruptible(&vpfe->curFrm->done); vpfe->curFrm = vpfe->nextFrm; } |
1. 一帧数据,一般先是field=1的偶场先完成。在偶场中,取出下一帧buffer,并设置下一帧的ccdc输出地址。
2. 接着是field=0的奇场完成,这时候把当前buffer的状态标记为STATE_DONE,并唤醒等待队列,这时候VIDIOC_DQBUF调用返回v4l2-buffer->index参数,用户程序就可以读取数据了。最后把下一帧设置成当前帧。
四.V4L2应用程序编程
V4L2应用程序编程主要还是需要按照V4L2 API来完成。
1. 设备初始化
if (-1 == ioctl (fd, VIDIOC_QUERYCAP, &cap)) { if (EINVAL == errno) { fprintf (stderr, "%s is no V4L2 device\n", dev_name); exit (EXIT_FAILURE); } else { errno_exit ("VIDIOC_QUERYCAP"); } } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf (stderr, "%s is no video capture device\n", dev_name); exit (EXIT_FAILURE); }
if (!(cap.capabilities & V4L2_CAP_STREAMING)) { fprintf (stderr, "%s does not support streaming i/o\n", dev_name); exit (EXIT_FAILURE); }
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 765 fmt.fmt.pix.height = 576 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (-1 == xioctl (fd, VIDIOC_S_FMT, &fmt)) errno_exit ("VIDIOC_S_FMT"); |
2. buffer请求和mmap系统调用
req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) { if (EINVAL == errno) { fprintf (stderr, "%s does not support " "memory mapping\n", dev_name); exit (EXIT_FAILURE); } else { errno_exit ("VIDIOC_REQBUFS"); } }
if (req.count < 2) { fprintf (stderr, "Insufficient buffer memory on %s\n", dev_name); exit (EXIT_FAILURE); }
buffers = calloc (req.count, sizeof (*buffers));
if (!buffers) { fprintf (stderr, "Out of memory\n"); exit (EXIT_FAILURE); }
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) { struct v4l2_buffer buf;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers;
if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf)) errno_exit ("VIDIOC_QUERYBUF");//buf.m.offset
buffers[n_buffers].length = buf.length; buffers[n_buffers].start = mmap (NULL /* start anywhere */, buf.length, PROT_READ | PROT_WRITE /* required */, MAP_SHARED /* recommended */, fd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffers].start) errno_exit ("mmap"); } |
3. select系统调用来查询buffer是否可读
fd_set fds; struct timeval tv; int r;
FD_ZERO (&fds); FD_SET (fd, &fds);
/* Timeout. */ tv.tv_sec = 2; tv.tv_usec = 0;
r = select (fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r) { if (EINTR == errno) continue;
errno_exit ("select");
|
4. 读数据
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) { exit(); }
assert (buf.index < n_buffers);
process_image (buffers[buf.index].start);
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf)) errno_exit ("VIDIOC_QBUF"); |
注意:这里的VIDIOC_DQBUF调用主要是从驱动中返回v4l2_buffer->index这个参数,有了这个参数就知道那个buffer可以读了。通过process_image读出数据。数据读完之后,需要调用VIDIOC_QBUF来把buffer返回给驱动。