一、简单介绍

笔者使用带FIFO的OV7670模块,主控选择F411CEU6,将采集的图像显示在一块由ST7789芯片驱动的240*240分辨率的彩屏上。

最终效果

图片

OV7670传感器芯片框图如下:

D[7:0]是输出的图片信息,宽度8的数据总线。

SCCB(Serial Camera Control Bus)接口,类似I2C,用于操作芯片内部寄存器,可以用HAL库的I2C接口读写。

左下角是一些视频时序信号。

芯片内有一个656*488大小的像素阵列,实际用到的是640*480。

可选VGA到CIF等大小的图片输出。

可选3种测试图案:8彩条,带灰度的彩条,移位1。

    和别的模块相比,摄像头模块会涉及一些术语,如果不了解这些名词的含义,开发的时候就会很被动。部分如下:

YUY:一种颜色编码方法,RGB也是一种色彩编码方法。YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度(Luminance、Luma),而 U 和 V 表示色度(Chrominance、Chroma)。

HREF:指示当前这一行的像素数据是有效的,在一整行像素期间保持高电平

PCLK:(Pixel Clock,像素时钟)是摄像头并行接口里最核心的时钟信号,PCLK 每跳变一次,就有一份像素数据要被采样。用来同步数据总线上的数据

VGA即“Video Graphics Array”。是IBM在1987年随PS/2机一起推出的一种视频传输标准,具有分辨率高、显示速率快、颜色丰富等优点,在彩色显示器领域得到了广泛的应用。VGA最早指的是显示器640X480。VGA的英文全称是Video GraphicArray,也叫显示绘图阵列。

QVGA即"Quarter VGA"。中VGA的四分之一尺寸,亦即在液晶屏幕(LCD)上输出的分辨率是240×320像素。QVGA支持屏幕旋转,可以开发出相应的程序,以显示旋转90°、180°、270°屏幕位置。由HandEra公司发布。多用于手持/移动设备。

CIF是常用的标准化图像格式(Common Intermediate Format)。在H.323协议簇中,规定了视频采集设备的标准采集分辨率。CIF = 352×288像素

QCIF全称Quarter common intermediate format。QCIF也是常用的标准化图像格式。在H.323中,规定QCIF = 176×144像素。

AGC (Automatic Gain Control)自动增益控制

AWB(Automatic White Balance )自动白平衡

AEC(Automatic Exposure Control)自动曝光控制

当图像信息有效时,HREF输出高电平,每来一个像素时钟PCLK,摄像头输出一个字节的图像信息。

    如图为摄像头输出VGA的时序,VSYNC从高到低后等待17个tline(784tp)HREF被拉高,有效像素字节开始输出,在一行像素(640个)输出期间HREF一直保持高电平,消耗640个tp。输出完一帧(480行)的像素后,再等待10个tline,VSYNC引脚会被拉高。

    在VSYNC的下降沿后,HREF会频繁变化,且两两VSYNC之间有480个HREF上升沿,代表480行像素。当HREF为高时,表明摄像头正在传输一行的内容,每行有640个像素点。

    对于RGB565而言,一个像素包含2字节信息,因此一个tp等于2个像素时钟tpclk。

    笔者使用的是带FIFO的OV7670模组,FIFO芯片的型号是AL422B,引脚定义如下:

    OV7670传感器将采集到的数据D0~D7引脚连接到FIFO芯片的数据输入引脚,开发重点就是MCU从FIFO中把摄像头采集的图片信息取出,并显示到TFT彩屏上。

二、cubeMX配置

I2C引脚设置上拉

SPI用于刷新屏幕

开启DMA并使能SPI中断

GPIOA0~A7设置输入模式,用于读取FIFO数据

用于控制FIFO的引脚均设置低电平,低速率

用于控制屏幕的引脚设置输出模式。笔者为减少杜邦线的使用,将控制屏幕背光的BL引脚短接至VCC,读者若是使用GPIO控制,请开启代码中注释的相关接口和函数。

PB12作为外部中断,下降沿触发

使用HSE时钟

三、驱动开发

    摄像头使用的SCCB协议很相似,实测可以用I2C控制读写。

读数据

HAL_StatusTypeDef OV7670_RD(uint8_t addr, uint8_t* val)
{
   HAL_I2C_Master_Transmit(&hi2c1, 0x42, (uint8_t*)(&addr), 1, 0xFF);
   HAL_I2C_Master_Receive(&hi2c1, 0x43, val, 1, 0xff);
}

写数据

HAL_StatusTypeDef OV7670_WR(uint8_t addr, uint8_t val)
{
   uint8_t send[2] = {addr, val};
   HAL_I2C_Master_Transmit(&hi2c1, 0x42, send, 2, 0xFF);
}

    读OV7670的ID

uint8_t OV7670_CheckID()
{
   uint8_t re[2];
   OV7670_RD(0xA, re);
   OV7670_RD(0xB, re + 1);
   if (re[0] == 0x76 && re[1] == 0x73)
   {
      return 1;
   }
   return 0;
}

    读到0x76和0x73说明通信正常。

    摄像头的寄存器配置较为繁琐,笔者使用网上大神分享的寄存器表格。

    输出像素点数量选择QVGA(240*320),格式选择RGB565。

    COM7寄存器0x12写入0x14。写入后可以发现每个VSYNC之间的上升沿变成240个,表明有240行像素。

    摄像头输出的窗口大小也可以设置,分别有四个:水平起始,水平结束,垂直起始,垂直结束。

寄存器地址0x17~0x1A分别是它们的高8位,剩余的低3位和低2位在HREF寄存和VREF寄存器中,如下:

图片

图片

    0x70和0x71寄存器用于显示测试图案,建议刚开始调试的时候开启来,方便查找问题。

图片

    0x70的最高位和0x71的最高位共同决定显示的测试图案。

0x70

0x71

图案

0x80

0x80

带灰度的彩条

0x80

0x00

移位1

0x00

0x80

彩条

FIFO写入时序

    当/WE是低电平时,WCK的上升沿来到,就写入FIFO,写地址自增。

    WCK被连接到摄像头的OV_PLCK像素时钟,摄像头输出的OV_HREF和排针FIFO_WEN经过一个与非门,输出到FIFO的写使能引脚FIFO_WE。摄像头输出数据总线被连接至FIFO数据输入总线。这样当排针拉高时,FIFO由摄像头OV_HREF控制写入,排针拉低时,FIFO_WE一直是高,FIFO被禁止写入。用这种方式,FIFO可以存放摄像头高速输出的图片信息,同时也提供给我们一个控制是否允许摄像头写入的接口。

FIFO读取时序

    /RE控制能否从FIFO中读出数据,模块已经将/RE接地了就不用考虑。

    /OE拉低时,在RCK(读时钟)的上升沿就可以对数据总线引脚读取,如果/OE是高电平,此时数据总线是高阻态。除了基本的读写时序,还要注意读地址和写地址的控制:应先让FIFO的读地址复位至0,使摄像头输出字节从FIFO的0地址依次存放,存放完一帧的内容。再将读地址复位至0,从0地址开始把刚才写入的图像数据读出。

写地址复位

    WCK被连接到PCLK,一直有信号,我们只需要控制WRST引脚从高变低再变高就完成写地址的复位。

void FIFO_WReset()
{
   HAL_GPIO_WritePin(WRST_GPIO_Port, WRST_Pin, GPIO_PIN_RESET);
   HAL_GPIO_WritePin(WRST_GPIO_Port, WRST_Pin, GPIO_PIN_SET);
}

读地址复位

void FIFO_RDReset()
{
   HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_RESET);
   FIFO_RCK(0);
   FIFO_RCK(1);
   FIFO_RCK(0);
   FIFO_RCK(1);
   HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_SET);
}

    四、总体设计

        对于摄像头来说,FIFO很快就被填满,而MCU读取很慢,所以在摄像头填满FIFO后,禁止摄像头继续写入FIFO,直到FIFO内容被MCU读出。

        VSYNC的下降沿来临时,表明摄像头要开始传输一帧的数据,MCU拉低WR,允许摄像头写入FIFO。当下降沿再次到来时,表明刚才的一帧数据写入完毕,拉高WR,复位写指针。读取FIFO内容到RAM并显示,读取时,应先复位读指针,再用for循环读取。

    主函数

    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
      /* USER CODE END 1 */
      /* MCU Configuration--------------------------------------------------------*/
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
      /* USER CODE BEGIN Init */
      /* USER CODE END Init */
      /* Configure the system clock */
      SystemClock_Config();
      /* USER CODE BEGIN SysInit */
      /* USER CODE END SysInit */
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_I2C1_Init();
      MX_SPI1_Init();
      MX_TIM10_Init();
      /* USER CODE BEGIN 2 */
      HAL_TIM_Base_Start_IT(&htim10);
       OV7670_Reset();
       // HAL_Delay(5);
       ST7789_Init();
       ST7789_Clear(RED);
       OV7670_READY = OV7670_CheckID();
       if (OV7670_READY)
       {
          HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
          OV7670_Config();
          OV7670_SetWindow(20,176,240,240);
       }
      /* USER CODE END 2 */
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
          if (OV7670_READY)
          {
             if (gather == 1)
             {
                /* read it*/
                FIFO_OE(0);
                FIFO_RDReset();
                for (uint32_t i = 0; i < 240 * 240 * 2; i++)
                {
                   FIFO_RCK(0);
                   FIFO_RCK(1);
                   TFT[i] = GPIOA->IDR & 0xFF;
                }
                ST7789_Address_Set(0, 0, 239, 239);
                for (uint16_t k = 0; k < 2; k++)
                {
                   ST7789_SendMultiByte(TFT + k * 240 * 240, 240 * 240);
                }
                FIFO_OE(1);
                gather = 0;
             }
             sprintf(str, "%d", show);
             ST7789_ShowString(0, 0, str, Font_16x26, RED, WHITE);
          }
          else
          {
             ST7789_ShowString(0, 110, "dont detect OV7670", Font_16x26, RED, WHITE);
          }
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

    显示帧率

        使用一个变量计算一秒钟有多少图像显示到彩屏上就可以得到帧率。开启一个定时周期是1s的定时器中断,中断触发时,赋值计数值给显示值并复位计数值。

    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
       show = cnt;
       cnt = 0;
    }

    外部中断

    每完成一帧图像的读取,LED就翻转一次,图像计数器自增。

    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    {
       if (gather == 0)
       {
          FIFO_WReset();
          FIFO_WR(1);
          gather = 1;
          HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
          cnt++;
       }
       else
       {
          /* FIFO stored a whole frame */
          FIFO_WR(0);
       }
    }

    五、后记

    注意点

    • 窗口寄存器0x17~0x1A,0x03,0x32的设置比较关键。

    • 如果彩条可以正常显示,但关闭彩条后摄像头白茫茫的一片,是焦距问题,拧几圈镜头就可以变得清晰。

    • 如果彩条无法显示,大概率是摄像头寄存器配置问题,关注窗口的配置。当没有头绪的时候也不要灰心,保持耐心,请相信自己。

    • 使用杜邦线连接易受干扰,注意数据总线和VSYNC线,FIFO控制线保持一定距离,减少干扰,可以试试把GPIO配低速。

        用逻辑分析仪监测VSYNC引脚,每隔大约30ms跳变一次,理论上如果摄像头输出的数据直接显示在屏幕上,帧率约30。

        对FIFO数据线和屏幕片选线采样,完成一次屏幕刷新和FIFO读取耗时大约134ms,也就是一秒钟7帧。读取FIFO占用较多时间,接近100ms;屏幕刷新大约32ms。

        在读取FIFO的代码中使用寄存器操作RCK引脚而非使用HAL库可以大幅提高读取速度,但也会变得极为敏感,用杜邦线极易受到干扰。笔者测试可以达到每秒10帧,提升50%,后续设计PCB再测试。使用5V供电,整体电流约100mA。

        完整工程可在笔者的公众号“田甲电子”私信 15_OV7670_FIFO  获取。

        提前祝大家马年快乐,万事如意,马到成功!

    Logo

    智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

    更多推荐