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