01signal.com

Smart Zynq 보드에서 OV7670 카메라 센서의 라이브 뷰 및 영상 캡처

이 웹 페이지는 Smart Zynq 보드의 기능을 살펴보는 소규모 프로젝트 모음에 속하는 문서입니다.

이 프로젝트는 HelloFPGA에도 게시되어 있습니다. 이 사이트는 중국어 사용자에게 추천됩니다.

소개

이 튜토리얼에서는 OV7670 카메라 모듈을 Smart Zynq 보드에 연결하고, 보드 자체의 HDMI 출력으로 라이브 비디오 신호를 보는 방법을 설명합니다. 또한 간단한 Linux 명령 하나만으로 raw 비디오 스트림을 파일로 쉽게 저장하는 방법도 보여드리겠습니다.

이 튜토리얼의 내용은 다른 종류의 데이터 획득(data acquisition)에도 적용할 수 있습니다. 아래에서 소개하는 기법들은 영상 데이터뿐 아니라 다른 디지털 데이터 소스에도 사용할 수 있습니다.

카메라 모듈은 Zynq 칩의 PL(FPGA) 부분에 연결됩니다. 따라서 영상 데이터를 ARM 프로세서로 보내기 전에 이미지 처리를 수행하는 로직을 추가하기 쉽습니다. 이 튜토리얼은 Xillinux를 기반으로 하므로, 시스템의 프로세서 부분은 완전한 Linux 배포판으로 구성됩니다.

OV7670 모듈이 이 데모에 선택된 이유는 저렴하고 널리 사용되며 구하기 쉽기 때문입니다. 게다가 이 부품이 생성하는 디지털 신호는 이해하기 쉽습니다.

하지만 OV7670에는 안타까운 결점이 하나 있습니다. 기본 설정에서는 비디오 스트림의 색상이 올바르지 않다는 것입니다. 이 카메라 센서에서 잘 알려진 문제입니다. 이 결점은 카메라의 레지스터 몇 개만 변경하면 해결할 수 있습니다. 그래서 이 튜토리얼은 두 부분으로 나뉩니다.

참고로 이 튜토리얼의 상당 부분은 구현이 어떻게 동작하는지 설명합니다. 카메라를 사용하는 데 있어서 이러한 설명을 이해할 필요는 없습니다.

OV7670 모듈

이 튜토리얼은 아래 사진에 보이는 카메라 모듈을 기준으로 합니다.

The OV7670 camera sensor module used in this project

이 모듈에서는 핀 헤더의 대부분 핀이 OV7670 부품에 직접 연결되어 있습니다. 3.3V와 GND만 전압 조정기에 연결되어 있습니다. 따라서 FPGA와 모듈 사이의 모든 연결은 FPGA와 OV7670 부품 사이의 직접 연결입니다.

시중에는 동일한 기능을 가진 다른 모듈도 있습니다. 이러한 다른 모듈을 사용해도 아마 괜찮을 것입니다. 예를 들어, PCB에 "2017/3/15"라고 적힌 다른 모듈이 있는데, 이 모듈도 정상 작동합니다. 반면에 제대로 작동하지 않는 모듈도 있는데, 작동하지 않는 모듈은 PCB에 "QYF-OV7670 V3.0"이라고 적혀 있습니다.

또 한 가지 유의할 점은 OV7670 부품에도 여러 리비전이 있다는 것입니다. 모듈에 올바른 리비전이 사용되었는지 확인할 수 있습니다. 확인 방법은 이 튜토리얼의 두 번째 부분에서 설명합니다.

OV7670에 대한 주요 정보 출처는 두 가지입니다. 이 두 문서는 인터넷에서 찾을 수 있습니다.

Vivado 프로젝트 준비

데모 번들(demo bundle)의 zip 파일(부트 파티션 키트)에서 새 Vivado 프로젝트를 만드세요. 텍스트 편집기로 verilog/src/xillydemo.v를 여세요. 코드에서 "PART 2"라고 표시된 부분을 삭제하고, 그 자리에 아래 코드 조각을 삽입하세요.

   /*
    * PART 2
    * ======
    *
    * This code demonstrates a frame grabber (data acquisition) from
    * an OV7670 camera module.
    *
    */

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

   // wait_for_frame's purpose is to start getting data from the camera
   // at the beginning of a frame.
   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

   // fifo_has_been_full changes to '1' when the FIFO becomes full, so
   // that the data acquisition stops and an EOF is sent to the host.
   // This ensures that the data that arrives to the host is contiguous.

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

   // This part writes pixels from the camera to the FIFO

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

또는 변경이 완료된 xillydemo.v를 여기에서 내려받아도 됩니다.

이 변경을 마친 후 평소처럼 비트스트림(bitstream) 파일을 만드세요. 이 Verilog 코드가 어떻게 동작하는지는 이 페이지 아래에서 자세히 설명합니다.

카메라 모듈 연결

카메라 모듈과 Smart Zynq 보드를 연결할 때는 짧은 듀폰(Dupont) 점퍼 와이어를 사용하면 됩니다. 와이어 길이는 10cm 이하가 좋으며, 최적 길이는 5cm입니다. 와이어가 더 길면 누화(crosstalk)로 인해 디지털 신호 품질이 저하될 수 있습니다. 수평 동기 신호에 과도한 노이즈가 있으면 화면이 튀면서 녹색과 보라색 줄무늬가 나타납니다.

와이어 길이가 10cm인 경우, OV7670의 I/O 드라이버 전류를 줄이기 위해 레지스터를 변경해야 할 수도 있습니다. 변경 방법은 이 튜토리얼의 두 번째 부분에서 보여줍니다.

다음은 OV7670 모듈을 Smart Zynq SP 보드에 연결한 사진입니다.

OV7670 module connected to the Smart Zynq board

아래는 반대 방향에서 찍은 사진입니다. 왼쪽 위의 작은 이미지는 핀 헤더의 마지막 핀이 아무것도 연결되어 있지 않다는 점을 강조한 것입니다.

OV7670 module connected to the Smart Zynq board

위 이미지들은 와이어를 연결하는 방법을 보여줍니다. 먼저 Smart Zynq 보드 뒷면에서 "Bank 33 VCCIO Vadj"라고 적힌 부분을 찾으세요. 이 표시 근처에 있는 핀 열이 우리가 사용할 핀 헤더입니다. HDMI 커넥터 가까이에 있는 핀 헤더입니다.

카메라 센서와 Smart Zynq 보드 사이에는 16개의 와이어가 나란히 연결됩니다. 3.3V와 GND만 핀 헤더의 다른 위치에 연결됩니다. 이 두 와이어는 짧지 않아도 됩니다.

참고로 핀 헤더의 마지막 핀은 5V이므로 이 핀에는 와이어를 연결하지 마세요.

다음은 카메라 모듈과 Smart Zynq 핀 헤더 사이의 배선 사양입니다. 위 이미지에서도 이 정보를 알아낼 수 있습니다.

핀 헤더 1 3 5 7 9 11 13 15 35
모듈 핀 PWDN D0 D2 D4 D6 MCLK HS SDA GND
모듈 핀 RST D1 D3 D5 D7 PCLK VS SCL 3.3V
핀 헤더 2 4 6 8 10 12 14 16 37

다시 한번 강조하지만, 3.3V와 GND 연결에 각별히 주의하세요. 이 두 와이어를 잘못 연결하면 카메라 모듈이 손상될 수 있습니다.

프레임 캡처

위에서 보여준 대로 업데이트된 xillydemo.v를 기반으로 만든 비트스트림 파일로 Xillinux를 시작하세요.

다음 명령(shell 프롬프트에서)은 카메라 출력으로 짧은 비디오 클립을 만듭니다.

# cat /dev/xillybus_read_32 > clip.raw

이 명령은 몇 초간 실행된 후 멈춥니다. 비디오 스트림의 데이터 전송률이 SD 카드의 데이터 쓰기 속도보다 높기 때문입니다. 이로 인해 오버플로(overflow)가 발생하여 데이터 흐름이 중단됩니다. 이 동작의 메커니즘은 아래에서 설명합니다.

다음 명령으로 이 비디오 클립을 재생할 수 있습니다.

# mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw

Xillinux의 그래픽 데스크톱 안에 있는 터미널 창에서 이 명령을 실행하면 비디오가 Xillinux의 그래픽 인터페이스에 재생됩니다.

또한 별도 페이지에서 설명하는 기법을 사용하면 다른 컴퓨터의 화면에서 비디오를 재생할 수도 있습니다. 예를 들어 상대 컴퓨터의 IP 주소가 192.168.1.11이라면 명령이 다음으로 시작하도록 변경하세요.

# DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo ...

앞서 언급했듯이 비디오 클립의 색상은 올바르지 않으며, 다음 페이지에서 이 문제를 해결하는 방법을 설명합니다.

다음 명령은 비디오 프레임 하나를 읽어 frame.raw라는 파일에 저장합니다.

# dd if=/dev/xillybus_read_32 of=frame.raw bs=614400 iflag=fullblock count=1

raw 프레임의 형식은 UYVY 4:2:2입니다. 즉, 각 픽셀은 16비트로 구성됩니다. 첫 번째 바이트는 첫 번째 픽셀의 U 성분(Cb)이고, 다음 바이트는 같은 픽셀의 Y 성분입니다. 세 번째와 네 번째 바이트는 두 번째 픽셀의 V 성분과 Y 성분(Cr 및 Y)입니다.

이 파일은 다음 명령으로 PNG 파일로 변환할 수 있습니다.

# convert -size 640x480 pal:frame.raw frame.png

이미지를 보기 위한 간단한 도구가 있습니다.

# display frame.png &

라이브 뷰

카메라의 라이브 뷰를 보려면 다음과 같은 내용으로 liveview.sh 파일을 만드세요.

#!/bin/bash

while [ 1 ] ; do
  dd if=/dev/xillybus_read_32 bs=614400 iflag=fullblock count=1 2>/dev/null
done | mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400 -

이 스크립트(script)는 다음 명령으로 실행합니다.

# bash liveview.sh

이 스크립트가 왜 필요할까요? mplayer가 너무 느리기 때문에 디바이스 파일에서 데이터를 직접 읽어 재생하는 것은 불가능합니다. 다시 말해, Zynq의 ARM 프로세서는 올바른 프레임 속도(31.25fps)로 비디오를 재생할 만큼 강력하지 않습니다. 그렇게 시도하면 비디오가 아주 잠깐만 재생됩니다. FPGA 내부에서 오버플로가 발생하면 데이터 흐름이 멈춥니다.

이 스크립트는 매번 /dev/xillybus_read_32에서 raw 프레임 하나를 읽는 무한 루프에 기반합니다. 이전에 frame.raw로 프레임 하나를 읽을 때 사용한 것과 동일한 명령입니다. 다만 이번에는 dd에 출력 파일이 지정되지 않았습니다. 따라서 dd는 데이터를 표준 출력으로 대신 씁니다.

이 무한 루프의 결과는 파이프(pipe)를 통해 mplayer의 표준 입력으로 리디렉션됩니다(루프 끝에 있는 "|" 기호에 주목하세요). mplayer는 표준 입력에서 들어오는 비디오 데이터를 재생합니다.

이 스크립트는 오버플로 문제를 해결합니다. dd가 항상 비디오 프레임 하나를 완전히 읽기 때문입니다. mplayer가 더 많은 데이터를 받을 준비가 되지 않으면 파이프의 데이터 흐름이 잠시 멈춥니다. 그 결과 dd는 카메라 센서의 비디오 프레임 중 일부를 건너뜁니다. 따라서 화면에 표시되는 프레임 속도는 카메라의 프레임 속도보다 낮습니다. 더 정확히 말하면, 표시되는 프레임 속도는 mplayer가 표시할 수 있는 최대 프레임 속도입니다.

화면에 나타나는 비디오 이미지는 mplayer 자체 버퍼링 때문에 약간 지연됩니다. 낮은 레이턴시(low latency)를 얻으려면 버퍼를 추가하지 않고 이미지를 화면에 표시해 주는 간단한 프로그램을 직접 작성해야 합니다.

mplayer는 강력한 미디어 플레이어입니다. 예를 들어 잘못된 색상이 거슬린다면 비디오 클립을 흑백 영상으로 재생할 수도 있습니다. 채도를 0으로 줄이려면 명령에 다음 부분을 추가하세요.

# mplayer -saturation -100 -demuxer rawvideo ...

이 페이지의 실용 부분은 여기까지

이 페이지의 나머지 부분은 데이터 획득(data acquisition)을 수행하는 로직의 구현을 설명합니다. 실용적인 내용에만 관심이 있다면 이 튜토리얼의 다음 부분으로 계속 읽으세요.

FPGA와 호스트 사이의 통신

이 예제의 로직은 Xillybus IP 코어(IP core)를 기반으로 합니다. 이 IP 코어는 호스트와의 통신을 담당합니다.

위에서 Verilog 코드에서 교체한 부분이 다음 코드로 끝난다는 것을 기억하세요.

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

이것은 표준 FIFO의 인스턴스화(instantiation)입니다. 이 FIFO에는 FIFO에서 데이터를 읽기 위한 세 개의 포트, 즉 rd_en, dout, empty(비어 있음)가 있습니다. 이 포트들은 Xillybus IP 코어에 연결됩니다. 이로써 IP 코어가 FIFO에서 데이터를 읽어 호스트로 보낼 수 있습니다. 그 결과 FIFO에 쓰여진 모든 데이터는 /dev/xillybus_read_32라는 디바이스 파일에 도달합니다. 즉, 호스트의 일반 컴퓨터 프로그램은 /dev/xillybus_read_32를 일반 파일처럼 열 수 있습니다. 프로그램이 이 파일에서 읽으면 FPGA 내부의 애플리케이션 로직이 FIFO에 쓴 데이터를 받게 됩니다.

FIFO에는 데이터를 쓰기 위한 세 개의 포트, 즉 wr_en, din, full(가득 참)이 있습니다. 이 포트들은 카메라 센서의 픽셀 데이터를 수집하는 로직에 연결됩니다. 이 페이지의 다음 섹션들에서 이 로직이 어떻게 동작하는지 설명합니다. 지금은 로직이 @dataword와 @fifo_wr_en을 통해 픽셀 데이터를 FIFO에 쓴다는 점만 말해 두겠습니다. 이 시점부터 호스트에서 실행되는 컴퓨터 프로그램으로 데이터를 가져오는 역할은 Xillybus IP 코어가 담당합니다. 따라서 앞서 언급한 다음 명령이 이 데이터를 파일로 씁니다.

# cat /dev/xillybus_read_32 > clip.raw

FIFO가 어떻게 동작하는지에 대한 일반적인 설명은 이 페이지를 참조하세요.

데이터 흐름은 다음 다이어그램으로 요약할 수 있습니다.

Simplified diagram of data acquisition with Xillybus

이 웹사이트에는 Xillybus에 관한 섹션이 있으며, 그 섹션에는 데이터 획득(data acquisition)을 다루는 페이지가 있습니다. 그 페이지를 통째로 읽어 보면 도움이 될 수 있습니다.

user_r_read_32_open이 FIFO의 srst 포트에 연결되어 있음에 유의하세요. 호스트에서 /dev/xillybus_read_32가 열리면 이 신호는 high로 바뀝니다. 이 신호는 인버터를 거쳐 연결되므로, 디바이스 파일이 닫히면 FIFO는 리셋 상태를 유지합니다. 이로써 파일이 닫힐 때마다 FIFO의 모든 데이터가 삭제됩니다.

카메라 센서와의 인터페이스

이제 위 Verilog 코드의 시작 부분을 살펴보겠습니다.

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

@bus_clk의 주파수는 100MHz입니다. 이 클록은 @clkdiv에 의해 4분주됩니다. 따라서 카메라 모듈은 25MHz 기준 클록을 받습니다. 카메라 데이터시트에 따르면 이는 허용되는 주파수입니다. 하지만 이 카메라는 기준 클록 주파수가 24MHz일 때 30fps 비디오 스트림을 출력하도록 설계되었습니다. 따라서 실제 비디오 프레임 속도는 약간 더 높은 31.25fps입니다.

여기서 이 방법은 일반적으로 클록을 만드는 방법으로는 부적절하다는 점을 언급해야겠습니다. 올바른 방법은 PLL이나 이와 유사한 리소스를 사용하는 것입니다. 이 특별한 경우에는 이 방법으로도 문제가 없습니다. @clkdiv는 출력 신호를 만들 때만 사용되며, FPGA 자체 로직은 이 신호를 사용하지 않기 때문입니다.

Verilog 코드의 다음 부분은 다음과 같습니다.

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

J6[0]은 카메라 모듈의 PWDN 핀에 연결됩니다. 카메라는 절대로 전원이 꺼지지 않습니다.

J6[1]은 카메라의 RESET# 핀에 연결됩니다. 이 핀이 low이면 카메라가 리셋됩니다. @user_w_write_32_open은 호스트의 프로그램이 /dev/xillybus_write_32를 열면 high가 됩니다. 따라서 일반적으로 카메라는 리셋되지 않습니다. @user_w_write_32_open이 low이고 결과적으로 J6[1]이 high이기 때문입니다. 이 구성 덕분에 다음 명령으로 카메라를 리셋할 수 있습니다.

# echo 1 > /dev/xillybus_write_32

이 명령은 디바이스 파일을 잠시 열었다 닫으므로 원하는 결과를 얻을 수 있습니다.

지금까지 FPGA에서 카메라 센서로 가는 신호를 만드는 방법을 보여드렸습니다. 이제 카메라 센서에서 FPGA로 들어오는 신호를 살펴보겠습니다.

OV7670은 FPGA에서 들어오는 기준 클록과 같은 주파수의 픽셀 클록을 생성합니다. 즉, PCLK의 주파수는 25MHz입니다. 이 신호는 Verilog 코드에서 @pclk_in에 연결됩니다.

카메라 센서는 또한 비디오 데이터를 담고 있는 세 개의 신호를 생성합니다. Verilog 코드에서 이 신호들의 이름은 @D_in, @hsync_in, @vsync_in입니다. 카메라 센서는 @pclk_in이 high에서 low로 변하는 시점(하강 에지)에 이 신호들의 값을 변경합니다. 더 정확히 말하면, @D_in, @hsync_in, @vsync_in의 변화는 @pclk_in의 하강 에지에 정렬됩니다. FPGA의 관점에서 이것을 소스 동기 입력(source synchronous input)이라고 합니다.

이제 Verilog 코드의 관련 부분을 살펴보겠습니다.

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

카메라 센서의 모든 신호는 @bus_clk를 이용해 샘플링(sampling)된다는 점에 유의하세요. 카메라의 PCLK도 다른 신호와 같은 방식으로 샘플링됩니다. 즉, PCLK는 클록으로 취급되지 않고 데이터 신호로 취급됩니다. 이 기법은 아래에서 간단히 설명하겠습니다.

또한 @bus_clk와 카메라 센서 신호 사이의 타이밍 관계는 알 수 없다는 점에 유의하세요. 따라서 이 신호들을 받는 플립플롭의 출력은 신뢰할 수 없습니다. 이 플립플롭들의 타이밍 요구사항을 보장할 수 없기 때문에 짧은 시간 동안 불안정해질 수 있습니다. 이는 클록 도메인 교차(clock domain crossing)와 관련하여 잘 알려진 문제입니다.

이 문제에 대한 해결책은 별도 페이지에서 논의됩니다. 즉, 메타안정성(metastability) 가드를 사용하는 것입니다. 이는 직렬로 연결된 두 개의 플립플롭을 사용하는 것을 의미합니다. 첫 번째 플립플롭(예: @pclk_guard)은 외부 신호에 연결됩니다. 두 번째 플립플롭은 첫 번째 플립플롭에 연결됩니다. 따라서 첫 번째 플립플롭이 잠시 불안정해지더라도 두 번째 플립플롭의 타이밍 요구사항은 보장됩니다. 따라서 두 번째 플립플롭의 출력은 신뢰할 수 있습니다.

결론적으로, @D, @pclk, @hsync, @vsync는 @bus_clk와 동기화된 신뢰할 수 있는 레지스터입니다.

@bus_clk의 주파수는 100MHz임을 기억하세요. 반면 PCLK의 주파수는 25MHz입니다. 따라서 @D, @hsync, @vsync의 값은 4클록 사이클마다 한 번만 사용해야 합니다. 그렇다면 네 사이클 중 어떤 클록 사이클에서 사용해야 할까요?

답은 Verilog 코드의 다음 행들에 있습니다.

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

이 코드 조각의 의미는 간단합니다. @pclk가 지금 high이고, 이전 클록 사이클에서 low였다면 @D, @hsync, @vsync의 값을 사용하세요. 카메라 센서의 신호들은 카메라의 PCLK가 high에서 low로 변할 때 값을 변경한다는 점을 기억하세요. 따라서 PCLK가 low에서 high로 변할 때 다른 신호들은 안정적입니다.

하지만 @pclk, @D, @hsync, @vsync는 레지스터입니다. 이 레지스터들은 @bus_clk와 동기화되어 있으며, 특정 시점의 카메라 센서 신호 스냅샷을 나타냅니다. 로직은 PCLK 자체의 상승 에지를 감지하는 대신 @pclk로 비슷한 일을 수행합니다. @pclk의 값이 low에서 high로 변할 때, 그 시점이 다른 레지스터의 값을 사용하기에 올바른 시점입니다.

이 기법을 01 신호 샘플링(01-signal sampling)이라고 합니다. 이 기법의 배경 아이디어는 01 신호 샘플링에 관한 별도 페이지에서 자세히 설명합니다. 그 페이지는 또한 이 방법이 FPGA의 타이밍 요구사항을 어떻게 보장하는지도 설명합니다. 이렇게 함으로써 로직은 @D, @hsync, @vsync의 값이 올바르도록 보장합니다.

데이터 흐름의 시작과 중지

이제 호스트로 데이터를 전송하지 않도록 하기 위한 두 개의 레지스터를 살펴보겠습니다.

이제 이 두 레지스터를 각각 자세히 설명하겠습니다. 먼저 @wait_for_frame입니다.

   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

@wait_for_frame의 값은 디바이스 파일이 열려 있지 않을 때 high입니다. 이 레지스터의 값은 카메라 센서의 vsync 신호에 응답하여 low로 변합니다. 이 신호는 프레임 사이의 시간 동안 high입니다. 즉, @vsync가 high인 동안에는 카메라에서 픽셀 데이터가 전송되지 않습니다.

결론적으로, @wait_for_frame은 카메라의 픽셀 데이터를 무시해야 할 때 high입니다. 디바이스 파일이 닫혀 있거나, 디바이스 파일이 막 열렸지만 카메라가 아직 프레임 중간에 있는 경우입니다.

이제 @fifo_has_been_full을 설명하겠습니다. 호스트에 도달하는 데이터가 카메라 센서가 생성하는 데이터와 동일해야 한다는 것이 중요합니다. 하지만 컴퓨터 프로그램이 디바이스 파일에서 데이터를 충분히 빨리 읽지 않으면 FIFO에서 오버플로가 발생할 수 있습니다. DMA 버퍼가 결국 가득 차게 되어 FIFO의 내용을 복사할 곳이 없어집니다. 결과적으로 Xillybus IP 코어는 FIFO에서 데이터를 읽을 수 없게 됩니다. 그런 일이 발생하면 FIFO가 가득 차게 되어 새 데이터를 쓸 수 없습니다.

로직은 이 상황을 막기 위해 할 수 있는 일이 없습니다. 그러나 로직은 호스트에 도달하는 데이터의 연속성을 보장할 수 있습니다. FIFO가 가득 차면 로직은 FIFO에 데이터 쓰기를 중단합니다. 또한 FIFO가 가득 찼던 적이 있고 나중에 비게 되면, 로직은 EOF를 호스트로 보내도록 요청합니다. 그 결과 컴퓨터 프로그램은 FIFO가 가득 차기 전에 FIFO에 쓰여진 모든 데이터를 받게 됩니다. 그 데이터 이후에 컴퓨터는 EOF를 받습니다. 일반 파일의 끝에 도달했을 때와 같은 동작입니다.

이 메커니즘은 컴퓨터 프로그램이 도착하는 데이터가 정확하고 연속적임을 신뢰할 수 있게 해줍니다. 연속성이 끊기면 EOF로 인해 컴퓨터 프로그램이 디바이스 파일을 닫게 됩니다. 프로그램이 디바이스 파일을 다시 열면 @wait_for_frame 덕분에 데이터가 새 프레임에서 시작됩니다.

관련 Verilog 코드는 다음과 같습니다.

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

@fifo_has_been_full은 FIFO가 가득 찬 적이 있을 때 high입니다. 이 레지스터는 디바이스 파일이 열려 있지 않으면 low로 변합니다. @fifo_has_been_full은 @fifo_full과 @fifo_has_been_nonfull이 모두 high일 때 high로 변합니다.

@fifo_full은 FIFO의 full(가득 참) 포트에 연결됩니다. 그런데 @fifo_has_been_nonfull이 왜 필요할까요? 그 이유는 FIFO가 리셋 상태에 있는 동안에는 종종 "full(가득 참)" 신호를 high로 유지하기 때문입니다. 이 기능의 목적은 FIFO가 아직 데이터를 받을 준비가 되지 않았음을 애플리케이션 로직에 알려주는 것입니다. @fifo_has_been_nonfull의 목적은 이런 상황에서 @fifo_has_been_full이 잘못 high가 되는 것을 방지하는 것입니다.

@user_r_read_32_eof는 @fifo_has_been_full과 @user_r_read_32_empty가 모두 high일 때 high가 됩니다. 즉, FIFO가 이전에 가득 찼고 지금은 비어 있을 때 호스트로 EOF가 전송됩니다. 이 상황에서는 어차피 새 데이터가 FIFO에 쓰이지 않는다는 점에 유의하세요.

별도 페이지에는 데이터 연속성을 보장하기 위한 유사한 해결책이 설명되어 있습니다. 그 페이지에서 제시하는 해결책은 FIFO의 양쪽이 서로 다른 클록 도메인(clock domain)에 속할 때 필요합니다. 이 페이지의 코드에서 FIFO는 하나의 클록에만 동기화되어 있습니다. 따라서 @fifo_has_been_full의 구현은 이 페이지에서 더 간단합니다.

FIFO에 데이터 쓰기

Verilog 코드의 다음 부분은 픽셀 데이터를 FIFO에 씁니다.

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

카메라 센서의 픽셀 데이터는 8비트 너비의 데이터 요소로 도착합니다. 이 로직 부분은 이 데이터 요소들을 32비트로 재구성하여 FIFO에 쓸 수 있게 합니다. 8비트 Xillybus 스트림(/dev/xillybus_write_8)은 두 가지 이유로 이 목적에 사용되지 않습니다.

@wait_for_frame이 high이면 다음 두 가지 가능성 중 하나 때문에 FIFO에 아무것도 쓰이지 않습니다. 디바이스 파일이 열려 있지 않거나, 디바이스 파일이 열려 있지만 아직 새 프레임의 시작에 도달하지 않았거나.

카메라 센서의 HSYNC가 high이면 데이터 신호에 유효한 픽셀이 포함되어 있음을 의미합니다. "sample_valid && hsync" 수식의 값은 두 가지 기준을 결합합니다. @sample_valid가 high이면 @hsync와 @D에 유효한 값이 들어 있습니다. 따라서 @hsync가 high이면 @D의 값이 @dataword의 일부에 복사됩니다. 또한 @D가 @dataword의 마지막 부분(즉 @byte_position이 3)에 복사되면 @fifo_wr_en이 high가 됩니다. 그 결과 @dataword가 FIFO에 쓰여집니다. 더 정확히 말하면, @fifo_wr_en의 수식은 다음과 같습니다.

fifo_wr_en <= !fifo_has_been_full;

따라서 @fifo_has_been_full이 high이면 앞서 언급했듯이 FIFO에 아무것도 쓰이지 않습니다.

Verilog 코드와 실제 핀의 관계

위의 Verilog 코드는 J6이라는 inout 포트를 사용합니다. 그런데 이 포트의 연결이 어떻게 핀 헤더에 도달할까요? 답은 xillydemo.xdc에 있습니다. 이 파일은 비트스트림(bitstream)을 생성하는 Vivado 프로젝트의 일부입니다("vivado-essentials" 디렉터리).

xillydemo.xdc에는 FPGA가 전자 부품으로서 올바르게 동작하기 위해 필요한 다양한 정보가 들어 있습니다. 그중에서 이 파일에는 다음과 같은 행이 포함됩니다.

[ ... ]

## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22  [get_ports {J6[0]}];   #J6/1  = IO_B33_LN2
set_property PACKAGE_PIN T22  [get_ports {J6[1]}];   #J6/2  = IO_B33_LP2
set_property PACKAGE_PIN W22  [get_ports {J6[2]}];   #J6/3  = IO_B33_LN3
set_property PACKAGE_PIN V22  [get_ports {J6[3]}];   #J6/4  = IO_B33_LP3
set_property PACKAGE_PIN Y21  [get_ports {J6[4]}];   #J6/5  = IO_B33_LN9
set_property PACKAGE_PIN Y20  [get_ports {J6[5]}];   #J6/6  = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}];   #J6/7  = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}];   #J6/8  = IO_B33_LP7

[ ... ]

첫 번째 행은 J6[0] 신호를 U22에 연결해야 한다고 말합니다. U22는 FPGA 물리적 패키지의 위치입니다. Smart Zynq의 회로도에 따르면 이 FPGA 핀은 핀 헤더의 첫 번째 핀에 연결됩니다. 다른 포트들의 위치도 같은 방식으로 정의됩니다.

결론

이 페이지에서는 Xillybus IP 코어를 사용하여 OV7670으로부터 픽셀 데이터를 얻고 이 데이터를 호스트로 보내는 방법을 보여드렸습니다.

이 튜토리얼의 다음 부분에서는 SCCB(즉, I2C)를 이용해 카메라 센서의 레지스터를 변경하기 위해 Xillybus IP 코어를 사용하는 방법을 설명합니다. 이는 카메라의 파라미터를 변경하는 데 유용합니다. 특히 올바른 색상의 이미지를 얻기 위해 필요합니다.

이 페이지는 영어 원문을 기계 번역한 것입니다. 의문이 드는 부분이 있으면 원문을 참조하시기 바랍니다.
Copyright © 2021-2026. All rights reserved. (dcc38493)