이 웹 페이지는 Smart Zynq 보드의 기능을 살펴보는 소규모 프로젝트 모음에 속하는 문서입니다.
이 프로젝트는 HelloFPGA에도 게시되어 있습니다. 이 사이트는 중국어 사용자에게 추천됩니다.
소개
이 튜토리얼에서는 Smart Zynq 보드로 OV7670 카메라 센서의 레지스터에 접근하는 방법을 설명합니다. 이 글은 카메라 센서에서 비디오 데이터를 수신하는 방법을 설명한 이전 페이지의 후속편입니다.
OV7670에는 카메라 센서의 파라미터를 설정하기 위한 SCCB(Serial Camera Control Bus) 인터페이스가 있습니다. 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 프로그램이 세 레지스터를 변경하는 이유에 대해 현재 알려진 설명입니다. 이 변경들이 필요하다는 것은 분명하지만, 그중 일부의 이유는 알려져 있지 않습니다.
- COM13(0x3d): 먼저 이 레지스터의 비트 0이 '1'로 변경됩니다. 그 결과 카메라 센서 출력에서 U와 V의 위치가 바뀝니다. 이는 출력 형식을 UYVY로 맞추기 위해 필요합니다. UYVY는 mplayer와 다른 소프트웨어가 기대하는 형식입니다. 또한 이 레지스터의 비트 3은 '0'으로 변경됩니다. 이 비트의 의미는 카메라 센서 문서에 기록되어 있지 않습니다.
- 예약된(reserved) 레지스터(0xb0): 이 레지스터에 대한 문서는 없습니다.
- AWBCTR0(0x6f): 이 레지스터는 카메라 센서의 화이트 밸런스와 관련이 있습니다. OV7670의 Implementation Guide에 따르면 이 레지스터에 0x9f를 쓰면 두 가지 변경이 일어납니다. 고급 AWB 모드가 활성화되고 최대 컬러 게인(color gain)이 2x에서 4x로 변경됩니다. 이 레지스터를 변경하지 않으면 화이트 밸런스가 제대로 작동하지 않습니다.
다른 레지스터에 값 쓰기
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 }입니다. 즉, COM13에 값 0x81을 씁니다. 이 레지스터의 주소는 0x3d입니다.
배열의 마지막 요소는 반드시 { -1, -1 }이어야 합니다.
카메라 센서의 구동 전류 줄이기
카메라 센서와 Smart Zynq 보드 사이의 와이어가 너무 길면 비디오 이미지가 불안정해질 수 있습니다. 비디오 프레임이 튀고 이미지에 녹색과 보라색 줄무늬가 나타납니다. 이는 와이어 사이에서 발생하는 누화(crosstalk) 때문에 자주 나타납니다.
카메라 센서가 와이어에 공급하는 전류를 줄이면 이 문제를 해결할 수 있을 수도 있습니다. 이렇게 하려면 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)"로 바꾸세요.
모든 레지스터를 출력하는 데는 1초 미만이 소요됩니다. 프로그램 실행이 잠시 멈추거나 프로그램이 멈춰 버리면 I2C 버스에서 통신 오류가 발생한 것입니다. 이 경우 프로그램 출력이 잘못되거나 불완전할 수 있습니다. 프로그램이 빠르고 원활하게 실행될 때까지 다시 실행하세요.
모든 레지스터의 출력 결과는 이 링크에서 내려받을 수 있습니다. 이 출력 결과는 카메라 센서가 올바른 색상의 이미지를 생성할 때의 레지스터 상태를 반영합니다. 기본값(카메라 센서를 리셋한 직후)의 출력 결과는 여기에서 내려받을 수 있습니다. 카메라는 자동 밝기 제어, 화이트 밸런스 등으로 인해 일부 레지스터를 지속적으로 변경한다는 점에 유의하세요.
I2C 쓰기 동작의 수행 방식
이 섹션을 이해하려면 I2C 프로토콜의 기본 사항을 알고 있어야 합니다.
i2c.c 프로그램은 두 개의 Xillybus 스트림, 즉 /dev/xillybus_write_8과 /dev/xillybus_read_8을 통해 FPGA 내부의 i2c_if.v 모듈과 통신합니다.
I2C 쓰기 동작은 다음과 같이 진행됩니다.
- 호스트가 /dev/xillybus_write_8을 열면 FPGA는 I2C 시작(start) 조건을 생성합니다.
- 이 디바이스 파일에 쓰여지는 바이트들은 수정 없이 그대로 I2C 와이어에 나타납니다.
- 호스트가 /dev/xillybus_write_8을 닫으면 FPGA는 I2C 종료(stop) 조건을 생성합니다.
이 단계들은 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입니다.
- 레지스터 주소.
- 레지스터에 쓰는 값.
FPGA는 I2C 버스를 통해 이 세 바이트를 카메라 센서로 보냅니다. i2c_write() 함수는 /dev/xillybus_write_8을 열고 버퍼의 데이터를 쓴 다음 파일을 닫습니다.
I2C 프로토콜에 따르면 수신자는 버스로 전송되는 각 바이트에 대해 응답(ACK)해야 합니다. 각 바이트(8비트로 구성)마다 이 목적을 위한 아홉 번째 비트가 있습니다. 이 아홉 번째 비트는 전송 중 특별한 시간 슬롯을 갖습니다. 바이트를 수신한 쪽은 이 시간 슬롯 동안 SDA 와이어를 '0'으로 끌어내려 바이트를 수신했음을 확인해야 합니다.
카메라 센서가 이렇게 응답하지 않으면 FPGA 내부의 i2c_if 모듈은 디바이스 파일을 통해 더 이상 바이트를 받아들이지 않습니다. 이로 인해 오류가 발생하지는 않지만, close() 함수 호출은 1000ms의 지연 후에야 반환됩니다. 그 이유는 Xillybus 드라이버가 파일을 닫기 전에 남은 데이터가 모두 FPGA에 도달할 때까지 기다리기 때문입니다. 그런데 I2C 슬레이브(slave)가 바이트에 대해 응답하지 않으면 FPGA는 다음 바이트를 받아들이지 않습니다. 이 상황에서 드라이버는 1000ms를 기다린 후 어쨌든 파일을 닫고, 커널 로그에 다음 메시지를 추가합니다.
Timed out while flushing. Output data may be lost.
커널 로그 메시지는 "dmesg" 명령으로 볼 수 있습니다.
결론적으로, i2c_write() 함수 호출이 완료되는 데 1초가 걸린다면 카메라 센서가 I2C 버스 동작에 올바르게 응답하지 않았을 가능성이 큽니다. 카메라 센서가 FPGA에 잘못 연결되었거나 전혀 연결되지 않았을 수도 있습니다.
I2C 읽기 동작의 수행 방식
읽기 동작은 두 가지 별도 동작으로 구성되므로 더 복잡합니다.
- 쓰기 동작이지만 데이터 바이트는 없습니다. 이 동작의 목적은 레지스터 주소를 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 주소. 쓰기 동작과 같은 0x42입니다.
- 읽으려는 레지스터의 주소.
그런 다음 i2c_read()는 /dev/xillybus_read_8을 엽니다. allwrite()와 달리 이 작업은 allread()로 수행되지 않는다는 점에 유의하세요.
다음으로 i2c_read()는 allwrite()를 통해 @dummybuf의 두 바이트를 버스에 씁니다.
- I2C 주소. 0x43은 버스에서 읽기 동작을 수행한다는 것을 슬레이브에 알려줍니다.
- 0을 포함하는 바이트. i2c_if 모듈은 이 바이트의 내용을 무시합니다.
FPGA의 i2c_if 모듈은 수신하는 첫 번째 바이트의 비트 0을 검사합니다. 이를 바탕으로 FPGA는 버스에서 쓰기 동작을 수행해야 하는지 읽기 동작을 수행해야 하는지를 판단합니다. 읽기 동작이 필요한 경우 다른 모든 바이트의 내용은 무시됩니다. 이 바이트들은 단지 몇 바이트를 수신해야 하는지를 FPGA에 알려주기 위한 것입니다.
i2c_if 모듈은 버스에서 요청된 수만큼 바이트를 읽고, /dev/xillybus_read_8을 통해 호스트로 보냅니다. 이 디바이스 파일은 @dummybuf를 써서 버스에서 읽기 동작을 시작하기 전에 반드시 열려 있어야 합니다. 그 후 allread()가 레지스터 값을 읽습니다. allread()는 파일을 열고 닫지 않습니다. 파일이 미리 열려 있어야 하기 때문입니다.
버스 재시작(restart)
이 섹션은 OV7670 카메라 센서와는 관련이 없습니다. 하지만 다른 슬레이브와 함께 i2c_if를 사용한다면 이 정보가 유용할 수 있습니다.
i2c_read()는 allwrite()를 두 번 호출한다는 점에 유의하세요. 호출할 때마다 /dev/xillybus_write_8이 열리고 닫힙니다. 그 결과 데이터가 전송되기 전에 I2C 시작(start) 조건이 있고, 전송이 끝난 후에 I2C 종료(stop) 조건이 있습니다.
즉, 레지스터 주소가 슬레이브로 전송된 후 종료(stop) 조건이 발생합니다. 그런 다음 마스터가 읽기 동작을 시작하기 전에 시작(start) 조건이 발생합니다.
카메라 센서는 이러한 이벤트 순서를 기대합니다. 하지만 I2C 슬레이브로 동작하는 다른 전자 부품들은 이렇게 읽기 동작을 시도하면 제대로 작동하지 않습니다. 그러한 부품들은 종료(stop) 조건에 반응하여 레지스터 주소를 잊어버리기 때문입니다. 따라서 버스에서 첫 번째 동작과 두 번째 동작 사이에 재시작(restart) 조건을 생성해야 합니다.
i2c_if 모듈은 이러한 경우도 지원합니다. /dev/xillybus_read_8이 계속 열려 있는 동안 /dev/xillybus_write_8을 닫았다가 다시 열면 버스에 재시작(restart) 조건이 대신 발생합니다. 즉, 슬레이브가 재시작(restart) 조건을 요구한다면 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 드라이버에서 도움을 얻어야 할 수도 있습니다.