本文根据 Xillinux (xillybus)官方的子网站 www.01signal.com 中的内容转载整理而来,如对原始内容感兴趣的可访问下列链接进行查看 使用 Smart Zynq通过 I2C 写入 OV7670 camera sensor的 registers 或者英文原内容Writing to OV7670 camera sensor’s registers through I2C with Smart Zynq
也感谢xillybus 官方对原始内容的梳理, 本文仅在上述内容中对文中部分翻译内容做了简单微调,以便更好的理解。
介绍
本教程解释了如何使用 Smart Zynq 开发板访问 OV7670 相机传感器的寄存器。这是上一节内容的补充内容,上一节描述了如何从相机传感器接收视频数据。
OV7670 有一个串行摄像头控制总线接口(SCCB)用于配置相机传感器的参数。SCCB 协议在 Omnivision 的名为“OmniVision Serial Camera Control Bus (SCCB) Functional Specification”的文档中定义。该协议与众所周知的的 I2C 协议兼容。
访问相机传感器寄存器的主要动机是使相机产生具有正确颜色的图像。然而,通过其寄存器控制相机还有其他优点:控制数字信号的电流,控制并可能停止相机的亮度和颜色的自动调整,请求测试图案(test pattern)等功能。
Zynq处理器有两路内置的I2C单元,每路都可以充当I2C总线 master的角色,可以使用其中任一单元与相机传感器通信。不幸的是,当尝试使用这些内置的I2C单元时发现它们不能与 OV7670很好地配合使用。原因可能是线路上存在大量噪音(当使用杜邦线连接的情况下)。并且缺乏专用的上拉电阻是另一个可能的原因( FPGA的内部上拉电阻可以解决此问题)。
由于无法使用内置的I2C单元,因此在项目中添加了一个Verilog模块。此逻辑设计得更好地应对噪声信号。
对Vivado 项目进行更改
下面的内容是基于上一节内容(Xillinux 章节十六 使用 Smart Zynq 从 OV7670 摄像头模块进行实时取景和视频捕捉 )中创建的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 module 连接到 J6[15] 和 J6[14]。这些端口通过排针连接到相机模块的 SCL 和 SDA。
更改相机传感器的寄存器
可以从此链接下载向相机发送 I2C 命令的计算机程序。将此程序复制到 Xillinux的文件系统中(复制方式可以通过TF卡直接复制,或者用scp命令传输(Xillinux 章节十 Windows 通过 SCP 命令 远程传输文件给Xillinux 系统 ),也可以用CIFS的方式传输文件(Xillinux 章节十二 在Xillinux 系统上搭建 CIFS服务(samba),实现与Windows 文件共享 ))
假设我们用CIFS的方式将i2c文件放到home文件夹下,如下图所示
![](http://www.hellofpga.com/wp-content/uploads/2024/06/image-4.png)
将目录更改为文件所在的位置。在命令行中键入以下命令以执行编译:
gcc -Wall -O3 -o i2c i2c.c
![](http://www.hellofpga.com/wp-content/uploads/2024/06/image-5.png)
该编译过程应该静默完成。编译成功后,可以看到目录下多了个i2c程序文件
![](http://www.hellofpga.com/wp-content/uploads/2024/06/image-6.png)
该程序可以通过 ./i2c 命令来运行运行,下面是该程序的运行方式和程序正常生成的输出:
# ./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
![](http://www.hellofpga.com/wp-content/uploads/2024/06/image-7.png)
首先,程序读取包含相机传感器 product ID的寄存器 。 OV7670 相机传感器有不同版本。本教程使用的(product ID)编号是 0x7673 的相机传感器。不太可能会遇到不同product ID标识的 OV7670 相机传感器。如果 product ID 不同,则很可能相机模块上可能安装了其他型号的相机传感器。
该程序进行最小限度的必要更改,以确保相机传感器图像的颜色正确无误。为了此目的,我们仅更改了三个寄存器的值。
在进行任何更改之前,程序会读取寄存器的现有值。这些是程序输出中的接下来的三行。然后程序将正确的值写入这些寄存器。
经过上述配置后再根据上一章节的操作来查看,就能发现显示的颜色已经变得正常了。如使用杜邦线的情况下图像不是很稳定 ,可以根据下文中提到的内容通过修改寄存器的方式来减少相机输出的驱动电流,再来看效果。
![](http://www.hellofpga.com/wp-content/uploads/2024/06/image-8-1024x743.png)
这三个寄存器的功能
不幸的是,相机传感器寄存器的功能介绍仅被部分记录。很多 OV7670的寄存器都被定义为“reserved”。因此,没有解释为什么寄存器中的一些改变是必要的。关于 OV7670和寄存器配置的信息来源有很多。寻找有关寄存器提示的最佳位置是相机传感器的 Linux driver: ov7670.c(来源于github,需要科学上网)。特别是, ov7670_default_regs[] ( driver中定义的 variable )包含许多有价值的提示。
这些是关于为什么 i2c.c 程序改变三个寄存器的解释。不幸的是,尽管这些改变显然是必要的,但其中一些改变的原因尚不清楚。
- COM13 (0x3d): 首先,这个寄存器的 bit 0 改为 ‘1’。因此, U 和 V 的位置在相机传感器的输出中交换。这是必需改变的,以便输出格式为 UYVY。这是 mplayer 和其他软件所期望的格式。另外,这个寄存器的 bit 3 改成了 ‘0’。这个 bit 的含义并没有写在相机传感器的文档中。
- Reserved register (0xb0): 没有关于此寄存器的文档。
- AWBCTR0 (0x6f): 这个寄存器与相机传感器的 white balance(白平衡)有关。根据 OV7670的 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 数组的元素内容由两个数字组成: 第一个数字是寄存器的地址。第二个数字是应写入该寄存器的值。
例如,第一个元素是 { 0x3d, 0x81 }。这意味着值 0x81 被写入 COM13寄存器中。这个寄存器的 地址就是 0x3d。
数组中的最后一个元素必须是 { -1, -1 }。
减少相机传感器的驱动电流
当摄像头传感器与 Smart Zynq 板之间的连线过长时(此处指用杜邦线进行连接的情况下),可能会导致视频图像不稳定: 视频帧跳跃,图像上出现绿色和紫色条纹。这种情况经常发生是因为电线之间出现串扰。
通过减少相机传感器施加到信号线缆上的电流(驱动电流)可以解决这个问题。为此,请将值 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 (640×480) CameraChip Implementation Guide )提供了有关其他几个寄存器的信息。如上所述, Linux kernel的driver 也可以提供重要提示。
回想一下本教程的第一部分(Xillinux 章节十六 使用 Smart Zynq 从 OV7670 摄像头模块进行实时取景和视频捕捉 ),可以使用以下命令重置相机传感器:
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 写操作的执行过程如下:
- 当 host 打开 /dev/xillybus_write_8时, FPGA 生成 I2C start condition(I2C起始信号)。
- 写入此 device file(设备文件) 的字节将会出现在 I2C 总线上(未经任何修改)。
- 当 host 关闭 /dev/xillybus_write_8时, FPGA 生成 I2C stop condition(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));
}
该函数准备一个由 3 个字节组成的buffer :
- I2C slave 地址。 ov7670的I2C从地址是0x42。每次i2c通讯时master端都需要发送该地址
- 代操作的寄存器地址。
- 写入该寄存器的值。
FPGA 通过 I2C 总线将这三个字节发送到相机传感器。 i2c_write() 函数打开 /dev/xillybus_write_8,从 buffer写入数据,然后关闭文件。
根据 I2C 协议,接收方必须对总线上所接收到的每个字节进行应答(ack): 每个字节(由 8 位组成)传输后,会有一个第9位数据用于应答的目的。这第9位在传输期间有一个特殊的时隙。接收字节的一侧必须在该时隙期间将 SDA 信号线线拉至 ‘0’ ,以向发送方告知确认接收到该完整字节。
如果相机传感器不以这种方式响应, FPGA 内部的 i2c_if 模块将拒绝通过device file(设备文件)接受更多字节。这不会导致错误,但对 close() 函数的调用只有在延迟 1000 ms后才会返回。原因是 Xillybus的驱动在关闭文件之前一直在等待所有剩余数据到达 FPGA 。但如果 I2C slave 没有对其中的一个字节进行应答(ack), 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 slave地址。ov7670是 0x42,和之前的写操作一样。
- 我们要读取的寄存器的地址 。
i2c_read() 然后打开 /dev/xillybus_read_8。请注意,与 allwrite()相比, allread()没有这样做。
接下来, i2c_read() 借助 allwrite()向 bus 写入两个字节(@dummybuf):
- I2C address。这是 0x43,它告诉 slave 这是对总线的读操作。
- 一个值是0的字节。 i2c_if 模块将忽略该字节的内容。
FPGA 中的 i2c_if 模块将检查它接收到的第一个字节的 bit 0 。 FPGA 根据此位推断应该对总线进行写操作还是读操作。如果需要读取操作,则忽略所有其他字节的内容。这些字节仅用于通知 FPGA 应接收多少字节。
i2c_if 模块从总线读取请求的字节数,并通过 /dev/xillybus_read_8将其发送到 host 。在通过写入 @dummybuf启动总线上的读操作之前,必须打开该 device file (设备文件)。此后, allread() 读取寄存器的值。 allread() 不会打开和关闭文件,因为文件必须被提前打开。
Bus restart
本节与 OV7670 相机传感器无关。但如果 i2c_if 与不同的 slave一起使用,此信息可能有用。
请注意, i2c_read() 对 allwrite() 执行了两次函数调用。每次, /dev/xillybus_write_8 都会被打开和关闭。这样一来,数据发送之前有一个 I2C start condition(I2C起始信号) ,之后有一个 I2C stop condition(I2C结束信号) 。
也就是说, 寄存器的地址发送到 slave之后,还有一个 stop condition(I2C结束信号) 。然后在 master 开始读操作之前还有一个 start condition(I2C起始信号) 。
相机传感器预计会发生这一系列事件。但是,如果尝试执行如下读取操作,则充当 I2C slaves 的其他电子组件将无法正常工作: 这些组件忘记了寄存器的地址以响应 stop condition(停止信号)。因此,有必要在总线上的第一个和第二个操作之间生成一个 restart condition(重新启动的信号)。
i2c_if 模块支持这种可能性: 如果 /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 核访问相机传感器的寄存器。与 I2C 总线连接需要一个附加模块: i2c_if。该模块对于与其他 I2C slaves通信也很有用。
不幸的是,缺乏有关 OV7670 相机模块寄存器的可用信息。因此,可能需要在互联网上搜索解决方案或向相机传感器的 Linux driver寻求帮助。