该网页属于一组探索 Smart Zynq board功能的小项目。
介绍
本教程介绍如何使用 Smart Zynq 板访问 OV7670 camera sensor的 registers 。这是上一页的后续内容,上一页描述了如何从相机传感器接收视频数据。
OV7670 有一个 Serial Camera Control Bus 接口(SCCB),用于配置相机传感器的参数。 SCCB protocol 在 Omnivision的名为“OmniVision Serial Camera Control Bus (SCCB) Functional Specification”的文档中定义。这款 protocol 与众所周知的 I2C protocol兼容。
访问相机传感器 registers 的主要动机是让相机生成具有正确颜色的图像。然而,通过 registers控制相机还有其他优点: 控制数字信号的电流,控制并可能停止相机的亮度和颜色自动调整,要求 test pattern 等等。
Zynq的 processor 有两个内置单元,每个单元实现一个 I2C bus master 。可以使用这些单元之一来与相机传感器通信。不幸的是,尝试使用这些内置 I2C 单元后发现它们不能与 OV7670很好地配合使用。原因可能是电线上存在大量噪音。缺乏专用的 pull-up resistors 是另一个可能的原因( FPGA的内部 pull-ups 用于此目的)。
由于无法使用内置 I2C 单元,因此在项目中添加了 Verilog module 。这款 logic 旨在更好地应对噪声信号。
Vivado 项目的更改
下面的说明基于已根据上一页创建的 Vivado 项目。
从此链接下载 I2C bus master 的 Verilog 实现。将此文件复制到 verilog/src/ 目录中。然后将该文件添加到 Vivado 项目中: 单击 File > Add Sources… 并选择“Add or create design sources”。然后单击“ Next”。单击“Add Files”按钮并选择 verilog/src/ 目录中名为“i2c_if.v”的文件。然后单击“Finish”按钮。
processor 在两个 Xillybus streams的帮助下控制这台 module 。在文本编辑器中打开 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 module 连接到 J6[15] 和 J6[14]。这些 ports 通过 pin header 连接到相机模块的 SCL 和 SDA。
更改相机传感器的 registers
可以从此链接下载向相机发送 I2C 命令的计算机程序。将此程序复制到 Xillinux的 file system 中(例如使用 scp 或直接将文件复制到 TF card中)。
将目录更改为文件所在的位置。在 shell prompt 处键入以下命令以执行 compilation:
# 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
首先,程序读取包含相机传感器 product ID的 registers 。 OV7670 相机传感器有不同版本。本教程基于使用编号 0x7673 (如 product ID)标识自身的相机传感器。它不太可能遇到 OV7670 相机传感器与不同的 product ID。如果 product ID 不同,则相机模块上可能安装了其他型号的相机传感器。
该程序进行最少的必要更改,以使相机传感器图像的颜色正确。为此更换了三台 registers 。
在进行任何更改之前,程序会读取 registers的现有值。这些是程序输出中的接下来的三行。然后程序将正确的值写入这些 registers。
registers这三个的含义
不幸的是,相机传感器 registers 的含义仅被部分记录。很多 OV7670的 registers 都被定义为“reserved”。因此,没有解释为什么 registers 中的一些改变是必要的。关于 OV7670和 registers的信息来源有很多。寻找有关 registers 提示的最佳位置是相机传感器的 Linux driver: ov7670.c。特别是, ov7670_default_regs[] ( driver中定义的 variable )包含许多有价值的提示。
这些是关于为什么 i2c.c 程序改变三个 registers的解释。不幸的是,尽管这些改变显然是必要的,但其中一些改变的原因尚不清楚。
- COM13 (0x3d): 首先,这个 register 的 bit 0 改为 '1'。因此, U 和 V 的位置在相机传感器的输出中交换。这是必需的,以便输出格式为 UYVY。这是 mplayer 和其他软件所期望的格式。另外,这个 register的 bit 3 改成了 '0'。这个 bit 的含义并没有写在相机传感器的文档中。
- Reserved register (0xb0): 没有关于此 register的文档。
- AWBCTR0 (0x6f): 这个 register 与相机传感器的 white balance有关。根据 OV7670的 Implementation Guide,将 0x9f 写入这个 register 会导致两个变化: Advanced AWB mode 启用, Maximum color gain 从 2x 更改为 4x。如果这个 register 不改的话, white balance 就不好用了。
写入其他 registers
这是 @writelist的定义,可以在 i2c.c 程序的开头附近找到:
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 array 的 elements 由两个数字组成: 第一个数字是 register的地址。第二个数字是应写入该 register的值。
例如,第一个 element 是 { 0x3d, 0x81 }。这意味着值 0x81 被写入 COM13。这个 register的 address 就是 0x3d。
数组中的最后一个元素必须是 { -1, -1 }。
减少相机传感器的 drive current
当摄像头传感器与 Smart Zynq 板之间的连线过长时,可能会导致视频图像不稳定: 视频帧跳跃,图像上出现绿色和紫色条纹。这种情况经常发生是因为电线之间出现 crosstalk 。
通过减少相机传感器施加到电线上的电流可以解决这个问题。为此,请将值 0x00 写入 COM2。这个 register的 address 就是 0x09。
换句话说,将 @writelist的定义改为:
static const struct {
int addr;
int value;
} writelist [] = {
{ 0x09, 0x00 }, // Drive current to 1x level
{ -1, -1 }, // Terminate
};
然后执行该程序的 compilation 并像以前一样运行该程序。
其他可能性
相机传感器的文档(特别是OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide )提供了有关其他几个 registers的信息。如上所述, Linux kernel的driver 也可以提供重要提示。
回想一下本教程的第一部分,可以使用以下命令重置相机传感器:
# echo 1 > /dev/xillybus_write_32
所有 registers 都会因该命令而返回到其默认值。
打印出所有 registers的值
这是 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;
}
这部分的目的是展示所有 registers的数值。由于“if (0)”条件,通常永远不会到达此部分。将其更改为“if (1)”以获得所有 registers值的打印输出。
打印出所有 registers 的时间应该不到一秒。如果程序执行暂时暂停或程序卡住,则原因是 I2C bus上的通信错误。发生这种情况时,程序的输出可能不正确或不完整。重新运行该程序,直至运行快速、流畅。
所有 registers 的打印输出均可在此链接下载。当相机传感器生成具有正确颜色的图像时,此打印输出反映了 registers 。默认值的打印输出(相机传感器重置后立即)可以在此处下载。请注意,由于自动亮度控制、白平衡等,相机会不断改变一些 registers 。
I2C 写操作是如何执行的
本节需要熟悉 I2C protocol的基础知识。
i2c.c 程序通过两个 Xillybus streams与 FPGA 内部的 i2c_if.v module 进行通信: /dev/xillybus_write_8 和 /dev/xillybus_read_8。
I2C write operation 的发生过程如下:
- 当 host 打开 /dev/xillybus_write_8时, FPGA 生成 I2C start condition。
- 写入此 device file 的字节出现在 I2C 线上(未经任何修改)。
- 当 host 关闭 /dev/xillybus_write_8时, FPGA 生成 I2C stop condition。
这些步骤由 i2c_write() 函数实现:
static void i2c_write(int addr, unsigned char data) {
unsigned char sendbuf[3] = { i2c_addr << 1, addr, data };
allwrite(sendbuf, sizeof(sendbuf));
}
该函数准备一个由 3 个字节组成的 buffer :
- I2C address。这是用于写操作的 0x42 。
- register address。
- 写入 register的值。
FPGA 通过 I2C bus将这三个字节发送到相机传感器。 i2c_write() 函数打开 /dev/xillybus_write_8,从 buffer写入数据,然后关闭文件。
根据 I2C protocol,接收方必须确认 bus上发送的每个字节: 对于每个字节(由 8 位组成),有一个第九位用于此目的。这第九位在传输期间有一个特殊的时隙。接收字节的一侧必须在该时隙期间将 SDA 线拉至 '0' ,以确认已接收到该字节。
如果相机传感器不以这种方式响应, FPGA 内部的 i2c_if module 将拒绝通过 device file接受更多字节。这不会导致错误,但对 close() 的函数调用只有在延迟 1000 ms后才会返回。原因是 Xillybus的 driver 在关闭文件之前等待所有剩余数据到达 FPGA 。但如果 I2C slave 还没有确认一个字节, FPGA 将拒绝接受下一个字节。在这种情况下, driver 等待 1000 ms,然后关闭文件,并将此消息添加到 kernel log:
Timed out while flushing. Output data may be lost.
kernel log 的消息可以使用“dmesg”命令查看。
总之,如果对 i2c_write() 的函数调用需要一秒钟才能完成,原因可能是相机传感器没有正确响应 I2C bus 操作。摄像头传感器可能未正确连接到 FPGA,或者根本没有连接。
I2C 的读操作是如何执行的
读操作更复杂,因为它由两个单独的操作组成:
- 写操作,但是没有数据字节。此操作的目的是将 register address 提交给 I2C slave。
- 一个读操作。 register的值从 slave 发送到 master。
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);
}
该函数首先向 slave发送两个字节 (@cmdbuf):
- I2C address。这是 0x42,和写操作一样。
- 我们要读取的 register 的 address 。
i2c_read() 然后打开 /dev/xillybus_read_8。请注意,与 allwrite()相比, allread()没有这样做。
接下来, i2c_read() 借助 allwrite()向 bus 写入两个字节(@dummybuf):
- I2C address。这是 0x43,它告诉 slave 这是对 bus的读操作。
- 包含零的字节。 i2c_if module 忽略该字节的内容。
FPGA 中的 i2c_if module 检查它接收到的第一个字节的 bit 0 。 FPGA 据此推断 bus应该进行写操作还是读操作。如果需要读取操作,则忽略所有其他字节的内容。这些字节仅用于通知 FPGA 应接收多少字节。
i2c_if module 从 bus 读取请求的字节数,并通过 /dev/xillybus_read_8将其发送到 host 。在通过写入 @dummybuf启动 bus 上的读操作之前,必须打开该 device file 。此后, allread() 读取 register的值。 allread() 不会打开和关闭文件,因为文件必须提前打开。
Bus restart
本节与 OV7670 相机传感器无关。但如果 i2c_if 与不同的 slave一起使用,此信息可能有用。
请注意, i2c_read() 对 allwrite() 执行了两次函数调用。每次, /dev/xillybus_write_8 都会打开和关闭。这样一来,数据发送之前有一个 I2C start condition ,之后有一个 I2C stop condition 。
也就是说, register的 address 发送到 slave之后,还有一个 stop condition 。然后在 master 开始读操作之前还有一个 start condition 。
相机传感器预计会发生这一系列事件。但是,如果尝试执行如下读取操作,则充当 I2C slaves 的其他电子组件将无法正常工作: 这些组件忘记了 register的 address 以响应 stop condition。因此,有必要在 bus上的第一个和第二个操作之间生成一个 restart condition 。
i2c_if module 支持这种可能性: 如果 /dev/xillybus_write_8 关闭然后重新打开,而 /dev/xillybus_read_8 持续打开,那么 bus 上就会出现一个 restart condition 。换句话说,如果 slave 需要 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 core 访问相机传感器的 registers。与 I2C bus连接需要一个附加模块: i2c_if。该模块对于与其他 I2C slaves通信也很有用。
不幸的是,缺乏有关 OV7670 相机模块寄存器的可用信息。因此,可能需要在互联网上搜索解决方案或向相机传感器的 Linux driver寻求帮助。