首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

数字摄像头接口DCMI

DCMI是一种并行的同步数据接口用于接收8位到14位的CMOS摄像头模块发出的数据。本文将详细介绍DCMI的结构和使用方法, 并参考正点原子的教程和示例驱动OV2640摄像头。

1. DCMI结构

下图1(a)描述了DCMI涉及到的信号。这些信号可以分为两类:

图1(a) DCMI信号图 图1(b) DCMI结构框图

如图1(b)所示,DCMI大体上由同步器(Synchronizer)、数据提取器(Data extraction)、队列和数据格式化器(FIFO/Data formater)、 控制和状态寄存器(Control/Status register)四个部分构成,此外还有DMA和AHB总线接口用于系统控制。

同步器和数据提取器用于根据外部同步时钟信号获取数据。由于DCMI支持8位、10位、12位、14位不同位宽的数据信号, 所以需要一个缓存和数据格式化工具,进行综合处理,每一个32位数据块就产生一个DMA中断请求。而控制和状态寄存器则控制了DCMI模块的工作方式, 具体描述了数据的位宽、接收数据格式等信息。

AHB总线接口很好理解,DCMI是挂在AHB总线上的设备,我们访问其中的控制寄存器,DMA操作都需要通过AHB总线。

与SPI等其它同步通信接口不同的是,在DCMI接口中同步信号不是由MCU发出的,而是由外设提供的,所以DCMI模块只能被动的接收数据。 由于摄像头的数据量大,而且还有一定的速率要求才能获得流畅的视频效果。所以我们几乎不能以字节的形式一个一个的接收捕获的数据, 通常都是通过DMA的形式把接收到的图像或者视频数据放到指定的缓冲区中。这个缓冲区由DMA控制器管理与DCMI无关。

2. 数据接收时序

在DCMI中数据和同步时钟信号都是由摄像头模块提供的,在PIXCLK的上升沿或者下降沿采集数据, 时钟沿极性可以通过修改控制寄存器CR.PCKPOL位指定。而VSYNC称为帧同步信号,它指示了一帧数据的开始和结束; HSYNC称为行同步信号,指示了一帧图像中每一行数据的开始和结束,在JPEG模式下则用于指示可用数据。

图2 DCMI时序图

上图2是一个接收数据时序的示意图,图中在PIXCLK的下降沿获取数据,HSYNC和VSYNC的有效状态为1。 (按:看图中的状态应该是在HSYNC和VSYNC为低电平的时候有效,这里却说1是有效状态。查看DCMI的控制寄存器CR的位定义, 文档中对于这两个信号的极性做了如下图的描述。所以,个人猜测HSYNC和VSYNC指示的是无效的数据状态。)

在DCMI中支持8位、10位、12位、14位的数据总线,我们可以通过控制寄存器CR的EDM位具体声明。 这些数据线总是以最低位(LSB)的形式接入DCMI的,比如说8位的线宽, 接入DCMI的数据线是DCMI_DR[0:7],10位则是DCMI_DR[0:9]。在DCMI中总是将接收到的数据拼接为一个32位的数据后,触发DMA请求。

对于8位的数据线宽,捕获一个32位数据需要4个像素时钟周期(PIXCLK),第一个字节放在LSB的位置上,第四个字节放在MSB上。 对于10位及以上的数据线宽,只需要2个像素时钟周期就可以产生一个32位数据,第一个周期采集的数据放在低16位的LSB上, 低16位的高位部分用0补齐;第二个周期则放在高16位的LSB上,高位用0补齐。

DCMI模块除了支持HSYNC和VSYNC这样的硬件时钟同步的方式外,还支持内嵌码的形式同步数据。 所谓的内嵌码是指在一帧图像中使用特定数字标着着一帧图像或者一行数据的开始和结束。这些特定的数字则不能够作为图像的数据内容使用, 通常是0xFF和0x00。这种内嵌码的工作方式支持8位线宽的摄像头,在DCMI中采用的0xFF0000XY的形式定义同步码的, XY的数据内容可以通过寄存器ESCR指定。详细参见官方手册。

3. DCMI的特殊功能

DCMI支持快照和连续采集两种工作模式。快照模式就是拍照一次只采集一张图片,接收到完整的一帧图像后会自动禁用DCMI; 连续采集则采集完一张图片后,会紧接着开始采集下一张图片,需要软件手动的清除控制寄存器的CAPTURE位停止采集。

此外DCMI还可以对采集的图像进行裁剪,只抓取我们感兴趣的一部分图像。它通过DCMI_CWSTRT和DCMI_CWSIZE两个寄存器实现, 它们分别描述了裁剪窗口的起始坐标和大小。需要注意的是描述窗口大小的时候应当保证是4的整数倍,这样才能通过DMA正确传输数据。 如果裁剪的区域超出了原始图像的范围,则使用原来范围。

DCMI支持8/10/12/14位的单色或者raw bayer的逐行视频格式,也支持YCbCr422和RGB565逐行视频格式。 对于这些逐行视频的格式,支持最大图像是2048×2048的。并且是小端对齐的形式。关于图像格式暂不做介绍,以后会有专门的主题详解。

一些摄像头模块可以直接输出JPEG压缩格式的图像,DCMI模块同样兼容这一功能。 我们通过控制寄存器的JPEG位可以告知DCMI接收的图像是JPEG格式的,此时HSYNC不再标志着图像的一行数据的开始和结束, 而是有效数据内容的开始和结束。对于JPEG图像数据,我们是不能够使用内嵌码的因为JPEG图像数据中0xFF是大量使用的特殊数字。 我们也不能对其进行裁剪,因为JPEG是压缩后的图像,单纯的指示裁剪窗口是没有意义的。

为了方便管理数据,DCMI提供了一个4-word的简单FIFO。这个FIFO没有溢出保护的功能,如果同步信号出错, 或者FIFO发生溢出,DCMI将复位FIFO同时等待新的数据帧开始。

4. DCMI的初始化

通过上面的描述,我们知道对于DCMI的配置无外乎以下几点内容:

因此,我们可以编写初始化程序如下:
        DCMI->CR.bits.CM = 1;                   // 快照模式
        DCMI->CR.bits.CROP = 0;                 // 捕获完整图像
        DCMI->CR.bits.ESS = 0;                  // 硬件同步HSYNC,VSYNC
        DCMI->CR.bits.PCKPOL = 1;               // PCLK 上升沿有效
        DCMI->CR.bits.HSPOL = 0;                // HSYNC 低电平有效
        DCMI->CR.bits.VSPOL = 0;                // VSYNC 低电平有效
        DCMI->CR.bits.FCRC = DCMI_CR_FCRC_All;  // 捕获所有的帧
        DCMI->CR.bits.EDM = DCMI_CR_EDM_8bit;   // 8位数据格式  
        DCMI->IER.bits.FRAME = 1;               // 开启帧中断 
        DCMI->CR.bits.ENABLE = 1;               // DCMI使能
可以通过CR.CAPTURE开启DCMI,捕获数据。但是在开始捕获数据之前,我们需要先完成DCMI和DMA的所有配置。
        DCMI->CR.bits.CAPTURE = 1;      //DCMI捕获使能  

参考DMA的教程,我们知道进行DMA操作时, 需要提供至少DMA请求信号、数据源地址、数据目标地址三个要素。 通过查找参考手册, 我们可以看到DMA2的Stream1可以用于DCMI的DMA请求,所以需要配置其选择连通DCMI的通道1。 数据的DMA方向是从外设到内存中,数据源是DCMI的DR寄存器,目标地址则需要我们提供。由此我们定义DMA配置函数如下:

        void dcmi_init_dma(uint32 bufaddr, uint16 buflen) { 
            RCC->AHB1ENR.bits.dma2 = 1;
            dma_reset_stream(DMA2_Stream1);
            
            DMA2_Stream1->CR.bits.CHSEL = 1;                // 通道选择
            DMA2_Stream1->CR.bits.DIR = DMA_DIR_P2M;        // 传输方向    
            DMA2_Stream1->CR.bits.CIRC = 1;                 // 循环模式
            DMA2_Stream1->CR.bits.PL = DMA_Priority_High;   // 高优先级
            DMA2_Stream1->CR.bits.PINC = 0;                 // 外设地址不增长
            DMA2_Stream1->CR.bits.PSIZE = DMA_PSIZE_32Bits; // 外设数据宽度
            DMA2_Stream1->CR.bits.MINC = 1;                 // 内存增长
            DMA2_Stream1->CR.bits.MSIZE = DMA_PSIZE_32Bits; // 内存数据宽度
            DMA2_Stream1->CR.bits.MBURST = DMA_Burst_0;     // Single Transfer
            DMA2_Stream1->CR.bits.PBURST = DMA_Burst_0;     // Single Transfer
            DMA2_Stream1->FCR.bits.DMDIS = 0;               // 保持Direct Mode
            DMA2_Stream1->FCR.bits.FTH = DMA_FIFO_4;
            
            DMA2_Stream1->PAR = (uint32)&DCMI->DR;          // 外设地址
            DMA2_Stream1->M0AR = bufaddr;                   // 缓存地址
            DMA2_Stream1->NDTR.all = buflen;                // 缓存大小
        }
正常情况下在完成了DCMI和DMA的配置之后,我们就可以开始接收来自摄像头的数据了,接收到的图像将保存到我们指定的缓存中。 而摄像头的具体使用,根据不同的厂家还略有不同。下面我们参照正点原子的摄像头例程,完成本文的例子,驱动一个OV2640的摄像头。

5. 摄像头模块OV2640

OV2640是OmniVision公司推出的一款低电压CMOS UXGA(1632×1232)图像传感器。 它在5725μm×6285μm的封装上提供了单片的UXGA摄像头和图像处理的所有功能。支持8位和10位的并行图像接口, 支持RGB565/555、YUV422/420、YCbCr422、JPEG、Raw RGB等多种数据格式。数据通过并行接口和同步时钟与STM32的DCMI通信, 对于OV2640的各种配置则需要通过SCCB总线实现。关于DCMI接口,在本文中我们已经做了比较详细的介绍。 下面简单介绍一下两线形式的SCCB总线。

SCCB总线, 全称串行摄像头控制总线(Serial Camera Control Bus),是OmniVision提出的一种摄像头传感器控制协议。 它有两线模式和三线模式,其中两线模式基本上就是I2C总线协议,由数据和时钟两根信号线构成, 我们可以很容易通过GPIO模拟一个时序出来。下图是两线形式的SCCB框图:

图中主设备(Master Device)就是我们的开发板或者说是STM32F407,从设备(Slave Device)则是OV2640图像传感器。 主设备与从设备之间通过时钟(SIO_C)和数据(SIO_D)实现通信。时钟是一个单向的信号,由主设备控制,用于同步数据的发送和接收。 数据信号则是一个双向的通道,由数据的发送方控制。在时钟信号为低电平时,数据发送方切换数据线的电平, 接收方在时钟信号的高电平时段采集数据线上的电平逻辑。数据总是先发送高位再发送低位。 总线空闲的时候,由主设备保证时钟和数据信号都是高电平。 其通信的电平逻辑和I2C通信基本一致, 本文例程中参考正点原子的SCCB实现, 根据个人习惯做了简单的修改。

本文例程中,我们将要实现快照的功能,在开发板上通过串口接收到一个字母'A'后,拍摄一张照片,并通过串口上传到上位机, 照片采用JPEG的格式。下图是探索者开发板上OV2640模块的驱动原理图。它涉及到以下四类信号:

据此,我们对OV2640的驱动引脚以及相关时钟做如下的初始化配置。下面是对控制引脚和SCCB相关引脚的配置, 由于买到的OV2640模块不需要DCMI_XCLK提供外部时钟,所以这里不再对其进行配置。
        void ov2640_init_gpio(void) {
            // 控制引脚:PG9 -> DCMI_PWDN, PG15 -> DCMI_RESET
            RCC->AHB1ENR.bits.gpiog = 1;
            struct gpio_pin_conf pincof;
            pincof.mode = GPIO_Mode_Out;
            pincof.otype = GPIO_OType_PP;    
            pincof.pull = GPIO_Pull_Up;
            pincof.speed = GPIO_OSpeed_Very_High;
            gpio_init(GPIOG, GPIO_Pin_9 | GPIO_Pin_15, &pincof);
            // SCCB:PD6 -> DCMI_SCL, PD7 -> DCMI_SDA
            RCC->AHB1ENR.bits.gpiod = 1;
            pincof.mode = GPIO_Mode_Out;
            pincof.otype = GPIO_OType_PP;    
            pincof.pull = GPIO_Pull_Up;
            pincof.speed = GPIO_OSpeed_Very_High;
            gpio_init(GPIOD, GPIO_Pin_6 | GPIO_Pin_7, &pincof);
函数ov2640_init_gpio()剩下的部分则是对DCMI接口的初始化工作,由于涉及到的引脚比较多,所以代码显得略长了一些。 总体上分为三块,首先使能相关的时钟,然后配置各个引脚工作在DCMI模式下,最后通过gpio_init()完成对引脚的具体配置。
            // DCMI,相关时钟
            RCC->AHB1ENR.bits.gpioa = 1;
            RCC->AHB1ENR.bits.gpiob = 1;
            RCC->AHB1ENR.bits.gpioc = 1;
            RCC->AHB1ENR.bits.gpioe = 1;
            RCC->AHB2ENR.bits.dcmi = 1;
            // DCMI引脚功能分配
            GPIOA->AFR.bits.pin4 = 0x0D;    // PA4 -> DCMI_HREF
            GPIOA->AFR.bits.pin6 = 0x0D;    // PA6 -> DCMI_PCLK
            GPIOB->AFR.bits.pin6 = 0x0D;    // PB6 -> DCMI_D5
            GPIOB->AFR.bits.pin7 = 0x0D;    // PB7 -> DCMI_VSYNC
            GPIOC->AFR.bits.pin6 = 0x0D;    // PC6 -> DCMI_D0
            GPIOC->AFR.bits.pin7 = 0x0D;    // PC7 -> DCMI_D1
            GPIOC->AFR.bits.pin8 = 0x0D;    // PC8 -> DCMI_D2
            GPIOC->AFR.bits.pin9 = 0x0D;    // PC9 -> DCMI_D3
            GPIOC->AFR.bits.pin11 = 0x0D;   // PC11 -> DCMI_D4
            GPIOE->AFR.bits.pin5 = 0x0D;    // PE5 -> DCMI_D6
            GPIOE->AFR.bits.pin6 = 0x0D;    // PE6 -> DCMI_D7
            // DCMI引脚具体配置
            pincof.mode = GPIO_Mode_Af;
            pincof.otype = GPIO_OType_PP;    
            pincof.pull = GPIO_Pull_Up;
            pincof.speed = GPIO_OSpeed_Very_High;
            gpio_init(GPIOA, GPIO_Pin_4 | GPIO_Pin_6, &pincof);
            gpio_init(GPIOB, GPIO_Pin_6 | GPIO_Pin_7, &pincof);
            gpio_init(GPIOC, GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_11, &pincof);
            gpio_init(GPIOE, GPIO_Pin_5 | GPIO_Pin_6, &pincof);
        }
然后,我们对正点原子的OV2640_Init()函数做简单的调整,完成对摄像头模块的初始化操作。
        uint8 OV2640_Init(void) {
            ov2640_init_gpio();
            sccb_init();
            dcmi_init();
接着复位OV2640,如下面左边代码所示;并读取MID和PID判定摄像头模块是否已经正常连接,如右面代码所示。
            OV2640_PWDN=0;  //POWER ON
            delay(168000);
            OV2640_RST=0;   //复位OV2640
            delay(168000);
            OV2640_RST=1;   //结束复位
            
            sccb_wite_reg(OV2640_DSP_RA_DLMT, 0x01);
            sccb_wite_reg(OV2640_SENSOR_COM7, 0x80);
            delay(1680000); 
        // 读取厂家MID
        uint16 reg = sccb_read_reg(OV2640_SENSOR_MIDH) << 8;
        reg |= sccb_read_reg(OV2640_SENSOR_MIDL);
        if(reg != OV2640_MID)
            return 1;
        // 读取厂家PID
        reg = sccb_read_reg(OV2640_SENSOR_PIDH) << 8;
        reg |= sccb_read_reg(OV2640_SENSOR_PIDL);
        if(reg != OV2640_PID)
            return 2;
若OV2640已经正常连接,我们就可以配置其具体的工作模式。 这里用了一个二维ov2640_uxga_init_reg_tbl的数组记录了对OV2640各个寄存器的配置。 这个二维数组就是一个<寄存器-值>的列表,其中第0列表示将要写的寄存器,第1列为对应的值。 我并没有深究各个寄存器都是做什么用的。 OV2640的数据手册中有简单的描述, 但是很多寄存器都是保留的, 在软件手册有说明如何配置OV2640, 但会见到各种保留寄存器,也没给出详细的说明。据正点原子的说法,他们也是根据Linux驱动移植的。所以这里我也就不再仔细研究了, 直接拷贝过来。
           //初始化 OV2640,采用SXGA分辨率(1600*1200)  
            for(uint16 i = 0; i < sizeof(ov2640_uxga_init_reg_tbl)/2; i++)
            {
                sccb_wite_reg(ov2640_uxga_init_reg_tbl[i][0],ov2640_uxga_init_reg_tbl[i][1]);
            } 
            return 0x00;
        }
在main()函数中,我们依次完成OV2640的初始化工作,配置DCMI接口的DMA操作,设定OV2640工作在JPEG模式下, 并设定采集320×240的图像。需要说明的是,在初始化OV2640的时候,我们在一个while循环中进行的。 若函数OV2640_Init()没有正常运行,也就是没有读到正确的MID和PID,它就会返回一个非零的值。 我们在while的条件语句中对返回值进行判定,一直到OV2640正常初始化为止。 jpeg_buf是一个缓存,用于存放DCMI接口接收到的数据,也就是我们的图像。
        while (OV2640_Init());
        dcmi_init_dma((uint32)jpeg_buf, jpeg_buf_size);
        OV2640_JPEG_Mode();
        OV2640_OutSize_Set(320,240);
在main函数的超级循环中,我们不断的检查串口是否接收到了数据。如果接收到数据就触发DCMI开始一次捕捉。 在DCMI的中断服务函数中,我们标记接收完毕一帧图像,并计算接收到数据的长度。 在超级循环中,通过一个while语句检查接收到一帧图像后,控制DCMI停止接收数据。通过检查数据是以0xFFD8开始, 并以0xFFD9结束判定接收到一帧JPEG格式的数据,最后通过串口的DMA形式发送出去。
    DCMI_Start();
    while (1 != jpeg_data_ok);
    DCMI_Stop();
    jpeg_data_ok = 0;
    uint8 *p = (uint8*)jpeg_buf;
    uint32 len = jpeg_data_len * 4;
    
    if (0xFF == p[0] && 0xD8 == p[1]) {
        for (uint32 i = 0; i < len; i++) {
            if (0xFF == p[i] && 0xD9 == p[i+1]) {
                len = i + 2;
                break;
            }
        }
        usart1_send_bytes_dma((uint8*)p, len);
    }
    void DCMI_IRQHandler(void) {  
        if(1 == DCMI->MIS.bits.FRAME) {
            if(jpeg_data_ok==0) {
                //停止当前传输
                DMA2_Stream1->CR.all &= ~(1<<0);
                //等待DMA2_Stream1可配置
                while(DMA2_Stream1->CR.all & 0X01);   
                //得到此次数据传输的长度
                jpeg_data_len = jpeg_buf_size - DMA2_Stream1->NDTR.all; 
                //标记JPEG数据采集完按成,等待其他函数处理
                jpeg_data_ok = 1;
            }
            //清除帧中断
            DCMI->ICR.bits.FRAME = 1;
        }                                        
    }
如此得到的效果就是当开发板接收到一个字节后,就会采集一帧图像并以JPEG的形式发送给上位机。

6. 总结

DCMI是一个并行的数字摄像头接口。一般由8/10/12/14位的数据线和像素时钟(PIXCLK),行同步信号(HSYNC)和帧同步信号(VSYNC)构成。 像素时钟和同步信号都是由摄像头提供的,用于指导宿主设备接收图像数据。STM32提供的DCMI也支持内嵌码的同步方式, 在本文中没有做过多的解释。

由于图像数据量很大,所以都是通过DMA的形式进行接收的。在STM32中,接收数据的缓存由DMA控制器管理与DCMI无关。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1