01signal.com

在 Smart Zynq 上通过 I2C 写入 OV7670 摄像头传感器的寄存器

本页面属于一个小项目系列,这些项目用于探索 Smart Zynq 开发板的各种功能。

这个项目也发布在 HelloFPGA 网站,推荐中文读者阅读。

简介

本教程介绍如何使用 Smart Zynq 开发板访问 OV7670 摄像头传感器的寄存器。这是上一页的续篇,上一页描述了如何从该摄像头传感器接收视频数据。

OV7670 带有一个串行摄像头控制总线接口(Serial Camera Control Bus,SCCB),用于配置摄像头传感器的参数。SCCB 协议定义在 Omnivision 公司的文档《OmniVision Serial Camera Control Bus (SCCB) Functional Specification》中。该协议与广为人知的 I2C 协议兼容。

访问摄像头传感器寄存器的主要动机,是让摄像头产生颜色正确的图像。不过,通过寄存器控制摄像头还有其他好处:控制数字信号的驱动电流大小、控制甚至停止摄像头对亮度和色彩的自动调节、让摄像头输出测试图形,等等。

Zynq 的处理器内部有两个内置单元,分别实现了一个 I2C 总线主控制器。可以使用其中一个单元与摄像头传感器通信。遗憾的是,尝试使用这些内置 I2C 单元后发现,它们与 OV7670 配合得不好。原因很可能是导线上的噪声较大。另一个可能的原因是缺少专用的上拉电阻(这次是使用 FPGA 内部上拉电阻来充当的)。

由于无法使用内置的 I2C 单元,便在工程中加入了一个 Verilog 模块来代替。这个逻辑经过专门设计,能更好地应对带有噪声的信号。

对 Vivado 工程的修改

下面的说明基于上一页中已经创建好的 Vivado 工程。

这个链接下载 I2C 总线主控制器的 Verilog 实现。把这个文件复制到 verilog/src/ 目录下。然后把它添加到 Vivado 工程中:点击 File > Add Sources…,选择 “Add or create design sources”,再点击 Next。点击 “Add Files” 按钮,在 verilog/src/ 目录下选择名为 i2c_if.v 的文件,然后点击 “Finish” 按钮。

处理器通过两条 Xillybus 数据流来控制这个模块。用文本编辑器打开 verilog/src/xillydemo.v。删除代码中标有 “PART 3” 的部分,用下面的代码片段替换它:

   /*
    * PART 3
    * ======
    *
    * The instantiation of i2c_if demonstrates how to use two Xillybus
    * streams to implement an I2C interface with the camera sensor module.
    *
    */

   i2c_if i2c_if_ins
     (
      .bus_clk(bus_clk),
      .quiesce(quiesce),

      .i2c_clk(J6[15]),
      .i2c_data(J6[14]),

      .user_w_write_8_open(user_w_write_8_open),
      .user_w_write_8_wren(user_w_write_8_wren),
      .user_w_write_8_data(user_w_write_8_data),
      .user_w_write_8_full(user_w_write_8_full),

      .user_r_read_8_open(user_r_read_8_open),
      .user_r_read_8_rden(user_r_read_8_rden),
      .user_r_read_8_data(user_r_read_8_data),
      .user_r_read_8_empty(user_r_read_8_empty),
      .user_r_read_8_eof(user_r_read_8_eof)
      );

或者,你也可以从这里下载修改后的 xillydemo.v。

完成上述修改后,照常生成比特流(bitstream)文件。

注意,i2c_if 模块连接 J6[15] 和 J6[14]。这些端口通过排针连接到摄像头模块的 SCL 和 SDA。

修改摄像头传感器的寄存器

用于向摄像头发送 I2C 命令的计算机程序可以从这个链接下载。把这个程序复制到 Xillinux 文件系统中(例如使用 scp,或直接把文件复制到 TF 卡中)。

切换到文件所在目录。在 shell 提示符下执行下面的命令进行编译:

# gcc -Wall -O3 -o i2c i2c.c

这条命令应该静默完成,不产生任何输出。

运行方法如下,同时给出了程序正常运行时的输出:

# ./i2c
Camera sensor's product ID is 0x7673
Reg 0x3d = 0x88 (to be altered)
Reg 0xb0 = 0x00 (to be altered)
Reg 0x6f = 0x9a (to be altered)
Wrote 0x3d = 0x81
Wrote 0xb0 = 0x84
Wrote 0x6f = 0x9f

程序首先读取包含产品 ID 的寄存器。OV7670 摄像头传感器有不同的版本。本教程所基于的摄像头传感器,其产品 ID 为 0x7673。一般而言,不太可能会遇到产品 ID 不同的 OV7670。如果读到的产品 ID 不同,那很可能是模块上安装了另一种型号的摄像头传感器。

程序只做了让图像颜色正确所必需的最少量修改。为此总共修改了三个寄存器。

在改动之前,程序会先读取这些寄存器当前的值。这些就是程序输出中接下来的三行。随后,程序把正确的值写入这些寄存器。

这三个寄存器的含义

遗憾的是,摄像头传感器寄存器的含义只有一部分有文档说明。OV7670 的许多寄存器都被定义为 “reserved”(保留)。因此,对于某些寄存器修改为什么是必需的,并没有给出解释。关于 OV7670 寄存器的信息来源有很多。寻找寄存器相关提示的最佳去处,是摄像头传感器的 Linux 驱动:ov7670.c。特别是 ov7670_default_regs[](驱动中定义的一个变量)包含许多有价值的线索。

下面给出 i2c.c 程序修改这三个寄存器时可以利用的解释。遗憾的是,其中一些修改的原因仍然未知,尽管这些修改显然是必要的。

向其他寄存器写入数据

下面这一段就是可以在 i2c.c 程序开头附近找到的 @writelist 定义:

static const struct {
  int addr;
  int value;
} writelist [] = {
  { 0x3d, 0x81 }, // COM13, swap UV, turn off reserved bit 3
  { 0xb0, 0x84 },
  { 0x6f, 0x9f }, // AWBCTR0, crucial for white balance

  { -1, -1 }, // Terminate
};

@writelist 数组的每个元素由两个数字组成:第一个数字是寄存器地址,第二个数字是要写入该寄存器的值。

例如,第一个元素是 { 0x3d, 0x81 }。这表示把值 0x81 写入 COM13。这个寄存器的地址是 0x3d。

数组的最后一个元素必须是 { -1, -1 }。

降低摄像头传感器的驱动电流

当摄像头传感器与 Smart Zynq 开发板之间的导线过长时,视频图像可能会不稳定:画面会跳动,并出现绿色和紫色的条纹。这通常是由导线之间的串扰引起的。

一种可能的解决方法,是降低摄像头传感器施加在导线上的电流。为此,向 COM2 写入值 0x00。这个寄存器的地址是 0x09。

换句话说,把 @writelist 的定义改成下面这样:

static const struct {
  int addr;
  int value;
} writelist [] = {
  { 0x09, 0x00 }, // Drive current to 1x level
  { -1, -1 }, // Terminate
};

然后重新编译这个程序,并像之前一样运行它。

其他可能性

摄像头传感器的文档(尤其是《OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide》)提供了关于其他几个寄存器的信息。如前所述,Linux 内核驱动也能提供重要的线索。

回顾本教程的第一部分,可以用下面的命令复位摄像头传感器:

# echo 1 > /dev/xillybus_write_32

执行这条命令后,所有寄存器都会恢复为默认值。

打印所有寄存器的值

下面是 i2c.c 程序中 main() 函数的一部分:

  if (0) { // Change this in order to print out registers instead
    for (i=0; i<=0xc9; i++) {
      i2c_read(i, &value);

      printf("Reg 0x%02x = 0x%02x\n", i, value);
    }

    return 0;
  }

这段代码的用途是打印所有寄存器的值。由于 “if (0)” 这个条件,正常情况下永远不会执行到这段代码。如果想打印所有寄存器的值,就把它改成 “if (1)”。

打印所有寄存器的时间应该不到一秒。如果程序在运行中出现短暂停顿,或者卡住不动,原因可能是 I2C 总线上发生了通信错误。发生这种情况时,程序的输出可能不正确或不完整。请重新运行程序,直到它能快速顺畅地完成为止。

可以在这个链接下载一份全部寄存器的打印结果。这份打印结果对应的是摄像头传感器产生颜色正确图像时的寄存器状态。默认值(摄像头传感器复位后立即读到的值)的打印结果可以从这里下载。注意,由于自动亮度控制、白平衡等功能,摄像头会持续改变一些寄存器的值。

I2C 写操作的执行方式

本节要求读者熟悉 I2C 协议的基本知识。

i2c.c 程序通过两条 Xillybus 数据流与 FPGA 内的 i2c_if.v 模块通信:/dev/xillybus_write_8 和 /dev/xillybus_read_8。

一次 I2C 写操作按如下方式进行:

这些步骤由 i2c_write() 函数实现:

static void i2c_write(int addr, unsigned char data) {
  unsigned char sendbuf[3] = { i2c_addr << 1, addr, data };

  allwrite(sendbuf, sizeof(sendbuf));
}

这个函数准备一个由三个字节组成的缓冲区:

FPGA 通过 I2C 总线把这三个字节发送给摄像头传感器。i2c_write() 函数会打开 /dev/xillybus_write_8,写入缓冲区中的数据,然后关闭文件。

根据 I2C 协议,接收方必须确认(acknowledge)总线上发送的每一个字节:对于每个由 8 位组成的字节,还专门有第 9 位用于确认。这个第 9 位在传输过程中有专门的时隙。接收字节的一方必须在这个时隙把 SDA 线拉低到 `0`,以确认该字节已被收到。

如果摄像头传感器没有这样应答,FPGA 内的 i2c_if 模块就会拒绝通过设备文件接收更多字节。这本身并不会造成错误,但调用 close() 时会在延迟 1000 ms 之后才返回。原因是 Xillybus 驱动在关闭文件之前,会等待所有剩余数据到达 FPGA。但如果 I2C 从机(slave)没有确认某个字节,FPGA 就会拒绝接收下一个字节。在这种情况下,驱动会等待 1000 ms,然后仍然关闭文件,并在内核日志中加一条消息:

Timed out while flushing. Output data may be lost.

可以用 dmesg 命令查看内核日志中的消息。

总之,如果一次对 i2c_write() 的函数调用需要一秒钟才能返回,原因很可能是摄像头传感器没有正确响应 I2C 总线操作。这可能是摄像头传感器没有正确连接到 FPGA,或者根本没有连接。

I2C 读操作的执行方式

读操作比较复杂,因为它由两个独立的操作组成:

下面给出 i2c_read() 函数:

static void i2c_read(int addr, unsigned char *data) {
  int fdr;

  unsigned char cmdbuf[2] = { i2c_addr << 1, addr };
  unsigned char dummybuf[2] = { (i2c_addr << 1) | 1, 0 };

  allwrite(cmdbuf, sizeof(cmdbuf));

  // We open xillybus_read_8 only now. Had it been open during the first
  // operation, there would have been a restart condition rather than a
  // stop condition after the first command.

  fdr = open("/dev/xillybus_read_8", O_RDONLY);

  if (fdr < 0) {
    perror("Failed to open /dev/xillybus_read_8 read-only");
    exit(1);
  }

  allwrite(dummybuf, sizeof(dummybuf));

  allread(fdr, data, sizeof(*data));

  close(fdr);
}

这个函数首先向从机发送两个字节(@cmdbuf):

接着,i2c_read() 会打开 /dev/xillybus_read_8。注意,这里不像 allwrite() 那样由 allread() 负责打开这个文件。

接下来,i2c_read() 借助 allwrite() 向总线写入两个字节(@dummybuf):

FPGA 内的 i2c_if 模块会检查它收到的第一个字节的第 0 位。据此,FPGA 推断总线上应该进行写操作还是读操作。如果需要进行读操作,其他所有字节的内容都会被忽略。这些字节的作用,仅仅是告诉 FPGA 需要接收多少个字节。

i2c_if 模块从总线读取所需数量的字节,并通过 /dev/xillybus_read_8 发送给主机。这个设备文件必须在总线上的读操作(即写入 @dummybuf)开始之前保持打开。之后,allread() 读取寄存器的值。allread() 不会打开和关闭文件,因为这个文件必须提前打开。

总线重启

本节与 OV7670 摄像头传感器无关。但如果你把 i2c_if 用于其他从机,这些信息可能会很有用。

注意,i2c_read() 会两次调用 allwrite()。每一次调用都会打开并关闭 /dev/xillybus_write_8。因此,在数据发送之前会出现一个 I2C 起始条件,数据发送之后会出现一个停止条件。

换句话说,在把寄存器地址发送给从机之后会有一个停止条件;然后,在主控制器开始读操作之前,又会有一个起始条件。

摄像头传感器期望的是这样的事件顺序。然而,其他有些作为 I2C 从机(slave)的电子元器件,如果以这种方式尝试读操作,就不能正常工作:这些元器件会因为停止条件而忘记寄存器地址。因此,必须在总线上的第一个操作和第二个操作之间产生一个重启条件(restart condition)。

i2c_if 模块支持这种可能:如果 /dev/xillybus_write_8 被关闭,然后在 /dev/xillybus_read_8 一直保持打开的情况下重新打开,那么总线上将出现一个重启条件(restart condition)。换句话说,如果从机要求使用重启条件,就需要相应调整对 allwrite() 的调用位置。这样,i2c_read() 将变成下面这样:

  fdr = open("/dev/xillybus_read_8", O_RDONLY);

  if (fdr < 0) {
    [ ... ]
  }

  allwrite(cmdbuf, sizeof(cmdbuf));
  allwrite(dummybuf, sizeof(dummybuf));

  allread(fdr, data, sizeof(*data));

  close(fdr);

再次强调,这段代码不适用于 OV7670。

结论

可以把 Xillybus IP 核(IP core)用于访问摄像头传感器的寄存器。为此,需要增加一个专门与 I2C 总线对接的模块:i2c_if。这个模块也可以用于与其他 I2C 从机通信。

遗憾的是,能获得的关于 OV7670 摄像头模块寄存器的信息并不充分。因此,可能有必要在互联网上寻找解决方案,或者从摄像头传感器的 Linux 驱动中获取帮助。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)