01signal.com

Writing to OV7670 camera sensor's registers through I2C with Smart Zynq

This web page belongs to a group of small projects that explore the features of the Smart Zynq board.

Introduction

This tutorial explains how to access the OV7670 camera sensor's registers with a Smart Zynq board. This is a follow-up to a previous page, which describes how to receive the video data from the camera sensor.

The OV7670 has a Serial Camera Control Bus interface (SCCB) for the purpose of configuring the camera sensor's parameters. The SCCB protocol is defined in Omnivision's document named "OmniVision Serial Camera Control Bus (SCCB) Functional Specification". This protocol is compatible with the well-known I2C protocol.

The main motivation for accessing the camera sensor's registers is to make the camera produce an image with correct colors. There are however other advantages to controlling the camera through its registers: Controlling the electrical current of the digital signals, controlling and possibly stopping the camera's automatic adjustment of brightness and color, requesting a test pattern and much more.

The Zynq's processor has two built-in units that implement an I2C bus master each. It's possible to use one of these units in order to communicate with the camera sensor. Unfortunately, an attempt to use these built-in I2C units revealed that they don't work well with the OV7670. The reason is probably the large amount of noise on the wires. The lack of dedicated pull-up resistors is another possible reason (the FPGA's internal pull-ups were used for this purpose).

As the built-in I2C units can't be used, a Verilog module is added to project instead. This logic is designed to cope better with noisy signals.

Changes to the Vivado project

The instructions below are based upon the Vivado project that is already created according to the previous page.

Download the Verilog implementation of the I2C bus master from this link. Copy this file into the verilog/src/ directory. Then add the file to the Vivado project: Click on File > Add Sources… and choose "Add or create design sources". Then click Next. Click on the "Add Files" button and choose the file named "i2c_if.v" in the verilog/src/ directory. Then click the "Finish" button.

The processor controls this module with the help of two Xillybus streams. Open verilog/src/xillydemo.v in a text editor. Delete the part of the code that is labeled "PART 3". Instead of that part, insert this code snippet:

   /*
    * 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)
      );

Alternatively, you may download xillydemo.v after this change from here.

Create a bitstream file as usual after making this change.

Note that the i2c_if module connects to J6[15] and J6[14]. These ports are connected through the pin header to the camera module's SCL and SDA.

Changing the camera sensor's registers

The computer program that sends I2C commands to the camera can be downloaded from this link. Copy this program into Xillinux' file system (for example, with scp or by copying the file directly into the TF card).

Change directory to where the file is located. Type this command at shell prompt in order to perform compilation:

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

This command should complete silently.

This is how to run the program. The output that the program normally generates is also shown:

# ./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

First, the program reads the registers that contain the camera sensor's product ID. There are different versions of the OV7670 camera sensor. This tutorial is based upon the camera sensor that identifies itself with the number 0x7673 (as the product ID). It's not likely to encounter an OV7670 camera sensor with a different product ID. If the product ID is different, it's possible that another model of the camera sensor is installed on the camera module.

The program makes the minimal necessary changes so that the colors of the camera sensor's image are correct. Three registers are changed for this purpose.

Before making any changes, the program reads the registers' existing values. These are the next three rows in the program's output. Then the program writes the correct values to these registers.

The meaning of these three registers

Unfortunately, the meaning of the camera sensor's registers is only partly documented. Many of the OV7670's registers are defined as "reserved". Therefore, there is no explanation to why some of the changes in the registers is necessary. There are many sources of information about the OV7670's registers. The best place to look for hints about the registers is the camera sensor's Linux driver: ov7670.c. In particular, ov7670_default_regs[] (a variable that is defined in the driver) contains a lot of valuable hints.

These are the explanations that are available for why the i2c.c program changes the three registers. Unfortunately, the reasons for some of these changes are not known, even though it's clear that these changes are necessary.

Writing to other registers

This is the definition of @writelist, which can be found close to the beginning of the i2c.c program:

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
};

The elements of the @writelist array consist of two numbers: The first number is the address of the register. The second number is the value that should be written to this register.

For example, the first element is { 0x3d, 0x81 }. This means that the value 0x81 is written to COM13. This register's address is 0x3d.

The last element in the array must be { -1, -1 }.

Reducing the camera sensor's drive current

When the wires between the camera sensor and the Smart Zynq board are too long, it's possible that the video image is unstable: The video frame jumps and there are green and purple stripes across the image. This happens often because of crosstalk that occurs between the wires.

It may be possible to solve this problem by reducing the electrical current that the camera sensor applies to the wires. In order to do this, write the value 0x00 to COM2. This register's address is 0x09.

In other words, change @writelist's definition to this:

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

Then perform a compilation of this program and run the program as before.

Other possibilities

The camera sensor's documentation (the OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide in particular) offers information about several other registers. As mentioned above, the driver of the Linux kernel can also provide important hints.

Recall from the first part of this tutorial that it's possible to reset the camera sensor with this command:

# echo 1 > /dev/xillybus_write_32

All registers return to their default values as a result of this command.

Printing out the values of all registers

This is a part of the main() function in the i2c.c program:

  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;
  }

The purpose of this part is to show the values of all registers. This part is normally never reached because of the "if (0)" condition. Change this to "if (1)" in order to obtain a printout of all registers' values.

Printing out all registers should take less than a second. If the execution of the program pauses momentarily or if the program becomes stuck, the reason is communication errors on the I2C bus. When this happens, the output of the program may be incorrect or incomplete. Re-run the program until it runs quickly and smoothly.

A printout of all registers can be downloaded at this link. This printout reflects the registers when the camera sensor produces an image with correct colors. The printout of the default values (immediately after the camera sensor has been reset) can be downloaded here. Note that the camera continuously changes some registers as a result of automatic brightness control, white balance etc.

How an I2C write operation is performed

This section requires being familiar with the basics of the I2C protocol.

The i2c.c program communicates with the i2c_if.v module inside the FPGA through two Xillybus streams: /dev/xillybus_write_8 and /dev/xillybus_read_8.

An I2C write operation takes place as follows:

These steps are implemented by the i2c_write() function:

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

  allwrite(sendbuf, sizeof(sendbuf));
}

This function prepares a buffer that consists of 3 bytes:

The FPGA sends these three bytes to the camera sensor through the I2C bus. The i2c_write() function opens /dev/xillybus_write_8, writes the data from the buffer, and closes the file.

According to the I2C protocol, the recipient must acknowledge each byte that is sent on the bus: For each byte (which consists of 8 bits), there is a ninth bit for this purpose. This ninth bit has a special time slot during the transmission. The side that received the byte must pull the SDA wire to '0' during this time slot in order to confirm that the byte has been received.

If the camera sensor doesn't respond this way, the i2c_if module inside the FPGA refuses to accept more bytes through the device file. This doesn't cause an error, but the function call to close() will return only after a delay of 1000 ms. The reason is that Xillybus' driver waits for all remaining data to reach the FPGA before closing the file. But if the I2C slave hasn't acknowledged a byte, the FPGA will refuse to accept the next byte. In this situation, the driver waits for 1000 ms, and then closes the file anyway, and adds this message to the kernel log:

Timed out while flushing. Output data may be lost.

The messages of the kernel log can be viewed with the "dmesg" command.

In conclusion, if a function call to i2c_write() takes one second to complete, the reason is probably that the camera sensor didn't respond correctly to the I2C bus operations. It's possible that the camera sensor is incorrectly connected to the FPGA, or not connected at all.

How an I2C read operation is performed

The read operation is more complicated, because it consists of two separate operations:

The i2c_read() function is shown below:

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);
}

This function begins with sending two bytes (@cmdbuf) to the slave:

i2c_read() then opens /dev/xillybus_read_8. Note that this is not done by allread(), in contrast to allwrite().

Next, i2c_read() writes two bytes (@dummybuf) to the bus by virtue of allwrite():

The i2c_if module in the FPGA examines bit 0 of the first byte that it receives. Based upon this, the FPGA deduces whether a write operation or a read operation should take place on the bus. If a read operation is required, the content of all other bytes is ignored. These bytes are only intended to inform the FPGA how many bytes should be received.

The i2c_if module reads the requested number of bytes from the bus and sends them to the host through /dev/xillybus_read_8. This device file must be open before the read operation on the bus is initiated by writing @dummybuf. After this, allread() reads the register's value. allread() doesn't open and close the file, because the file must be open early.

Bus restart

This section isn't relevant to the OV7670 camera sensor. But this information may be useful if i2c_if is used with a different slave.

Note that i2c_read() performs a function call to allwrite() twice. Each time, /dev/xillybus_write_8 is opened and closed. As a result of that, there is an I2C start condition before the data is sent, and there is an I2C stop condition afterwards.

In other words, there is a stop condition after the register's address has been sent to the slave. Then there is a start condition before the master begins the read operation.

The camera sensor expects this sequence of events. However, other electronic components that function as I2C slaves do not operate properly if a read operation is attempted like this: These components forget the register's address in response to the stop condition. It is therefore necessary to generate a restart condition between the first and second actions on the bus.

The i2c_if module supports this possibility: If /dev/xillybus_write_8 is closed and then reopened while /dev/xillybus_read_8 is continuously open, there will be a restart condition on the bus instead. In other words, if the slave requires a restart condition, the function call to allwrite() needs to be moved. i2c_read() will then be like this:

  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);

Once again, this code is not suitable for OV7670.

Conclusion

It's possible to use the Xillybus IP core for accessing the camera sensor's registers. An additional module is required for interfacing with the I2C bus: i2c_if. This module is also useful for communicating with other I2C slaves.

The information available about the OV7670 camera module's registers is unfortunately lacking. It may therefore be necessary to search for solutions on the Internet or take help from the camera sensor's Linux driver.

Copyright © 2021-2024. All rights reserved. (6f913017)