01signal.com

Vista en directo y captura de vídeo de un sensor de cámara OV7670 con Smart Zynq

Esta página web pertenece a un grupo de pequeños proyectos que exploran las características de la placa Smart Zynq.

Este proyecto está también publicado en HelloFPGA, un sitio recomendado para los lectores chinos.

Introducción

En este tutorial se explica cómo conectar un módulo de cámara OV7670 a la Smart Zynq y ver la señal de vídeo en directo por la salida HDMI de la propia placa. También mostraré cómo el flujo de vídeo en bruto se guarda fácilmente en un archivo con un sencillo comando de Linux.

La información de este tutorial también es aplicable a otros tipos de adquisición de datos (data acquisition): las técnicas que se muestran a continuación pueden utilizarse con otras fuentes de datos de imagen, así como con otras fuentes de datos digitales.

El módulo de cámara está conectado a la parte PL (FPGA) del chip Zynq. Por tanto, es fácil añadir lógica que realice procesamiento de imagen antes de enviar los datos de imagen al procesador ARM. Como este tutorial se basa en Xillinux, la parte procesador del sistema consiste en una distribución Linux completa.

El módulo OV7670 se eligió para esta demostración porque este hardware es barato, popular y fácil de conseguir. Además, las señales digitales que genera este componente son sencillas de entender.

Sin embargo, el OV7670 tiene un defecto desafortunado: por defecto, los colores del flujo de vídeo son incorrectos. Es un problema conocido de este sensor de cámara. Es posible corregir este defecto cambiando un número reducido de registros de la cámara. Por lo tanto, este tutorial se divide en dos partes:

Nótese que una gran parte de este tutorial explica cómo funciona la implementación. No es necesario entender estas explicaciones para usar la cámara.

El módulo OV7670

Este tutorial se basa en el módulo de cámara que se muestra en la imagen siguiente:

The OV7670 camera sensor module used in this project

En este módulo, la mayoría de los pines del conector de pines están conectados directamente al componente OV7670. Solo 3.3V y GND están conectados a reguladores de tensión. En consecuencia, todas las conexiones entre la FPGA y el módulo son conexiones directas entre la FPGA y el componente OV7670.

En el mercado hay otros módulos con la misma funcionalidad. Probablemente esté bien usar también esos otros módulos. Por ejemplo, hay un módulo distinto que tiene «2017/3/15» escrito en la placa de circuito impreso (PCB). Ese módulo también funciona correctamente. Por otro lado, hay un módulo que no funciona bien. El módulo que no funciona tiene «QYF-OV7670 V3.0» escrito en el PCB.

Otra cosa a tener en cuenta es que hay distintas revisiones del componente OV7670. Es posible verificar que en el módulo se usa la revisión correcta. Cómo hacerlo se explica en la segunda parte de este tutorial.

Hay dos fuentes principales de información sobre el OV7670. Estos dos documentos se pueden encontrar en Internet:

Preparación del proyecto Vivado

Cree un nuevo proyecto Vivado a partir del archivo zip del paquete de demostración (demo bundle), es decir, el kit de la partición de arranque. Abra verilog/src/xillydemo.v en un editor de texto. Elimine la parte del código etiquetada como «PART 2». En lugar de esa parte, inserte este fragmento de código:

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

Alternativamente, puede descargar xillydemo.v con este cambio desde aquí.

Después de hacer este cambio, cree un archivo de flujo de bits (bitstream) como de costumbre. El funcionamiento de este código Verilog se explica en detalle más abajo en esta página.

Conexión del módulo de cámara

Se pueden usar cables puente Dupont cortos para conectar el módulo de cámara con la placa Smart Zynq. La longitud de los cables debe ser de 10 cm o menos. La longitud óptima es de 5 cm. Si los cables son más largos, la calidad de las señales digitales puede verse afectada por diafonía (crosstalk). El exceso de ruido en la sincronización horizontal provocará una imagen de vídeo saltarina con franjas verdes y moradas.

Si la longitud de los cables es de 10 cm, puede ser necesario cambiar un registro para reducir la corriente de los drivers de E/S del OV7670. La segunda parte de este tutorial muestra cómo hacer este cambio.

Esta es una imagen del módulo OV7670 conectado a una placa Smart Zynq SP:

OV7670 module connected to the Smart Zynq board

Abajo hay una imagen tomada desde la dirección opuesta. La imagen más pequeña de la esquina superior izquierda resalta que el último pin del conector de pines no está conectado a nada.

OV7670 module connected to the Smart Zynq board

Las imágenes de arriba muestran cómo conectar los cables: primero, busque donde pone «Bank 33 VCCIO Vadj» en el reverso de la placa Smart Zynq. La fila de pines cercana a esta marca es el conector de pines con el que trabajaremos. Ese es el conector de pines que está cerca del conector HDMI.

Hay 16 cables que se conectan en paralelo entre el sensor de cámara y la placa Smart Zynq. Solo 3.3V y GND se conectan a un lugar distinto del conector de pines. Estos dos cables no tienen que ser cortos.

Tenga en cuenta que el último pin del conector de pines es de 5V, así que no conecte un cable a ese pin.

Esta es la especificación de cableado entre el módulo de cámara y el conector de pines de la Smart Zynq. Esta información también se puede deducir de la imagen de arriba:

Conector de pines 1 3 5 7 9 11 13 15 35
Pin del módulo PWDN D0 D2 D4 D6 MCLK HS SDA GND
Pin del módulo RST D1 D3 D5 D7 PCLK VS SCL 3.3V
Conector de pines 2 4 6 8 10 12 14 16 37

Una vez más, preste especial atención a la conexión de 3.3V y GND. Una conexión incorrecta de estos dos cables puede destruir el módulo de cámara.

Captura de fotogramas

Arranque Xillinux con un archivo de flujo de bits (bitstream) basado en el xillydemo.v actualizado (como se muestra arriba).

Este comando (en el indicador de shell) crea un clip de vídeo corto a partir de la salida de la cámara:

# cat /dev/xillybus_read_32 > clip.raw

Este comando se ejecuta durante unos segundos y luego se detiene. Esto se debe a que la velocidad de datos del flujo de vídeo es mayor que la velocidad de la tarjeta SD para escribir datos. Esto provoca un desbordamiento (overflow) que detiene el flujo de datos. El mecanismo que hay detrás de este comportamiento se explica más abajo.

Es posible reproducir este clip de vídeo con el siguiente comando:

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

Si este comando se usa en una ventana de terminal dentro del escritorio gráfico de Xillinux, el vídeo se reproduce en la interfaz gráfica de Xillinux.

También es posible reproducir el vídeo en la pantalla de otro ordenador con la técnica que se describe en una página aparte. Por ejemplo, si la dirección IP del otro ordenador es 192.168.1.11, cambie el comando para que empiece así:

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

Como ya se ha mencionado, los colores del clip de vídeo son incorrectos, y la página siguiente explica cómo corregir este problema.

Este comando lee un fotograma de vídeo en un archivo llamado frame.raw:

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

El formato del fotograma en bruto es UYVY 4:2:2. En otras palabras, cada píxel consta de 16 bits. El primer byte es el componente U del primer píxel (o Cb). El byte siguiente es el componente Y del mismo píxel. El tercer y cuarto bytes son los componentes V e Y del segundo píxel (Cr e Y).

Este archivo se puede convertir a un archivo PNG con:

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

Hay una herramienta sencilla para visualizar la imagen:

# display frame.png &

Vista en directo

Para obtener una vista en directo de la cámara, cree un archivo llamado liveview.sh que contenga lo siguiente:

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

Ejecute este script con

# bash liveview.sh

¿Por qué se necesita este script? Es imposible leer los datos directamente del archivo de dispositivo porque mplayer es demasiado lento. En otras palabras, el procesador ARM del Zynq no tiene suficiente potencia para reproducir el vídeo a la velocidad de fotogramas correcta (31.25 fps). Si lo intenta, el vídeo se reproduce solo durante un breve instante. El flujo de datos se detiene cuando hay un desbordamiento dentro de la FPGA.

Este script se basa en un bucle infinito que lee un fotograma en bruto de /dev/xillybus_read_32 en cada iteración. Es el mismo comando que se usó antes para leer un fotograma en frame.raw. Pero esta vez no se define ningún archivo de salida para dd. Por lo tanto, dd escribe los datos en la salida estándar.

El resultado de este bucle infinito se redirige a la entrada estándar de mplayer mediante una tubería (pipe) (preste atención al «|» al final del bucle). mplayer reproduce los datos de vídeo que llegan por la entrada estándar.

El script resuelve el problema del desbordamiento porque dd siempre lee un fotograma de vídeo completo. Cuando mplayer no está listo para aceptar más datos, el flujo de datos de la tubería se detiene momentáneamente. Como resultado, dd se salta fotogramas del sensor de cámara. Por tanto, la velocidad de fotogramas mostrada en pantalla es menor que la del sensor de cámara. Más concretamente, la velocidad mostrada es la máxima que mplayer es capaz de mostrar.

La imagen de vídeo que aparece en pantalla está ligeramente retardada debido al almacenamiento en búfer que hace mplayer. Para obtener una latencia baja, es necesario escribir un programa sencillo que muestre la imagen en pantalla sin añadir búfer.

mplayer es un reproductor multimedia potente. Por ejemplo, si los colores incorrectos resultan molestos, es posible reproducir el clip de vídeo en blanco y negro. Añada la siguiente parte al comando para reducir la saturación a cero:

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

Fin de la parte práctica de esta página

El resto de esta página explica la implementación de la lógica que realiza la adquisición de datos. Si solo le interesan los temas prácticos, continúe en la siguiente parte de este tutorial.

Comunicación entre la FPGA y el host

La lógica de este ejemplo se basa en el núcleo IP (IP core) de Xillybus. Este núcleo IP se encarga de la comunicación con el host.

Recuerde que la parte que se reemplazó en el código Verilog termina con esto:

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

Esta es una instanciación (instantiation) de un FIFO estándar. Este FIFO tiene tres puertos destinados a leer datos del FIFO: rd_en, dout y empty (vacío). Estos puertos están conectados al núcleo IP de Xillybus. Esto hace posible que el núcleo IP lea datos del FIFO y los envíe al host. El resultado es que todo lo que se escribe en el FIFO llega al archivo de dispositivo /dev/xillybus_read_32. En otras palabras, un programa normal en el host puede abrir /dev/xillybus_read_32 como un archivo normal. Cuando el programa lee de este archivo, recibe los datos que la lógica de aplicación dentro de la FPGA ha escrito en el FIFO.

El FIFO tiene tres puertos destinados a escribir datos: wr_en, din y full (lleno). Estos puertos están conectados a la lógica que recoge los datos de píxeles del sensor de cámara. Las siguientes secciones de esta página explican cómo funciona esta lógica. Por ahora, solo señalaré que la lógica escribe los datos de píxeles en el FIFO con la ayuda de @dataword y @fifo_wr_en. A partir de ahí, el núcleo IP de Xillybus se encarga de llevar esos datos al programa que se ejecuta en el host. Por eso este comando (ya mencionado arriba) escribe estos datos en un archivo:

# cat /dev/xillybus_read_32 > clip.raw

Para una explicación general sobre cómo funciona un FIFO, consulte esta página.

El flujo de datos se puede resumir con este diagrama:

Simplified diagram of data acquisition with Xillybus

Hay una sección en este sitio web sobre Xillybus, y esa sección tiene una página que trata la adquisición de datos (data acquisition). Puede ser útil leer esa página.

Nótese que user_r_read_32_open está conectado al puerto srst del FIFO. Cuando se abre /dev/xillybus_read_32 en el host, esta señal pasa a alto. La señal está conectada con una NOT, así que cuando el archivo de dispositivo se cierra, el FIFO se mantiene en estado de reset. Esto asegura que cada vez que el archivo se cierra, todos los datos del FIFO se eliminan.

Interfaz con el sensor de cámara

Ahora veremos el comienzo del código Verilog de arriba:

   reg [1:0]  clkdiv;

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

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

La frecuencia de @bus_clk es 100 MHz. Este reloj se divide por cuatro con la ayuda de @clkdiv. En consecuencia, el módulo de cámara recibe un reloj de referencia de 25 MHz. Según la hoja de datos de la cámara, esta es una frecuencia permitida. Sin embargo, la cámara está diseñada para producir un flujo de vídeo de 30 fps cuando la frecuencia del reloj de referencia es 24 MHz. Por tanto, la velocidad de fotogramas real es ligeramente superior: 31.25 fps.

Debo mencionar que este suele ser un método incorrecto para crear un reloj. El método correcto es usar una PLL o un recurso similar. No hay problema con este método en este caso concreto, porque @clkdiv solo se usa para crear una señal de salida: la lógica de la propia FPGA no usa esta señal.

La siguiente parte del código Verilog es esta:

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

J6[0] está conectado al pin PWDN del módulo de cámara. La cámara nunca se apaga.

J6[1] está conectado al pin RESET# de la cámara. Cuando este pin está bajo, la cámara se resetea. @user_w_write_32_open está alto cuando /dev/xillybus_write_32 está abierto por un programa en el host. Así que normalmente la cámara no se resetea, porque @user_w_write_32_open está bajo y, en consecuencia, J6[1] está alto. Esta disposición permite resetear la cámara con el siguiente comando:

# echo 1 > /dev/xillybus_write_32

Este comando abre el archivo de dispositivo durante un breve período de tiempo, y con ello se consigue el resultado deseado.

Hasta ahora he mostrado cómo se crean las señales desde la FPGA hacia el sensor de cámara. Ahora pasemos a las señales que van del sensor de cámara a la FPGA.

El OV7670 genera un reloj de píxeles con la misma frecuencia que el reloj de referencia de la FPGA. En otras palabras, la frecuencia de PCLK es 25 MHz. Esta señal está conectada a @pclk_in en el código Verilog.

El sensor de cámara también genera tres señales que contienen los datos de vídeo. Los nombres de estas señales en el código Verilog son @D_in, @hsync_in y @vsync_in. El sensor de cámara cambia los valores de estas señales en el mismo momento en que @pclk_in cambia de alto a bajo (flanco de bajada). Más precisamente, los cambios de @D_in, @hsync_in y @vsync_in están alineados con el flanco de bajada de @pclk_in. Desde la perspectiva de la FPGA, esto se llama una entrada síncrona de fuente (source synchronous).

Ahora veamos la parte relevante del código 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

Nótese que todas las señales del sensor de cámara se muestrean con la ayuda de @bus_clk. Incluso el PCLK de la cámara se muestrea de la misma manera que las demás señales. En otras palabras, PCLK no se trata como un reloj, sino como una señal de datos. Explicaré brevemente esta técnica más abajo. (En inglés, este tipo de muestreo se denomina 01-signal sampling).

Observe también que se desconoce la relación de temporización entre @bus_clk y las señales del sensor de cámara. Por tanto, las salidas de los flip-flops que reciben estas señales no son fiables: es imposible garantizar los requisitos de temporización de esos flip-flops, así que pueden volverse inestables durante breves períodos de tiempo. Este es un problema conocido en relación con el cruce entre dominios de reloj (clock domain crossing).

La solución a este problema se analiza en una página aparte: las guardas de metaestabilidad (metastability guards). Esto significa que hay dos flip-flops conectados en serie entre sí. El primer flip-flop (@pclk_guard, por ejemplo) está conectado a la señal externa. El segundo flip-flop está conectado al primero. Así, aunque el primer flip-flop se vuelva inestable durante un breve tiempo, los requisitos de temporización del segundo flip-flop están garantizados. Por tanto, la salida del segundo flip-flop es fiable.

En conclusión: @D, @pclk, @hsync y @vsync son registros fiables (síncronos con @bus_clk).

Recuerde que la frecuencia de @bus_clk es 100 MHz. La frecuencia de PCLK es 25 MHz, en cambio. Así que los valores de @D, @hsync y @vsync deben consumirse solo una vez de cada cuatro ciclos de reloj. Pero, ¿qué ciclo de reloj de los cuatro?

La respuesta está en estas líneas del código Verilog:

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

Este fragmento de código significa simplemente: si @pclk está alto ahora, y estaba bajo en el ciclo de reloj anterior, entonces use los valores de @D, @hsync y @vsync. Recuerde que las señales del sensor de cámara cambian de valor cuando el PCLK de la cámara cambia de alto a bajo. Así que cuando PCLK cambia de bajo a alto, las demás señales están estables.

Pero @pclk, @D, @hsync y @vsync son registros. Estos registros son síncronos con @bus_clk y representan una instantánea de las señales del sensor de cámara en un momento concreto. En lugar de detectar el flanco de subida del propio PCLK, la lógica hace algo similar con @pclk: cuando el valor de @pclk cambia de bajo a alto, ese es el momento correcto para usar los valores de los otros registros.

Esta técnica se llama muestreo de señal 01 (01-signal sampling). La idea que hay detrás de esta técnica se explica en detalle en una página aparte sobre el muestreo de señal 01. Esa página también explica cómo este método garantiza los requisitos de temporización de la FPGA. Al hacerlo, la lógica se asegura de que los valores en @D, @hsync y @vsync son correctos.

Puesta en marcha y detención del flujo de datos

Ahora veremos dos registros cuya finalidad es evitar la transmisión de datos al host:

Ahora analizaré cada uno de estos dos registros en detalle. Primero, @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;

El valor de @wait_for_frame está alto cuando el archivo de dispositivo no está abierto. El valor de este registro cambia a bajo en respuesta a la señal vsync del sensor de cámara. Esta señal está alta durante un período de tiempo entre fotogramas. En otras palabras, cuando @vsync está alto, el sensor de cámara no transmite datos de píxeles.

En conclusión, @wait_for_frame está alto cuando deben ignorarse los datos de píxeles de la cámara: cuando el archivo de dispositivo está cerrado, o cuando el archivo de dispositivo se ha abierto recientemente pero la cámara todavía está en medio de un fotograma.

Ahora pasaré a explicar @fifo_has_been_full: es importante asegurar que los datos que llegan al host son los mismos que produce el sensor de cámara. Sin embargo, puede producirse un desbordamiento (overflow) en el FIFO si el programa informático no lee los datos del archivo de dispositivo con la suficiente rapidez: los búferes DMA acabarán llenándose, así que no habrá sitio donde copiar el contenido del FIFO. En consecuencia, el núcleo IP de Xillybus no podrá leer datos del FIFO. Cuando eso ocurre, el FIFO se llena, haciendo imposible escribir nuevos datos en él.

La lógica no puede hacer nada para evitar esta situación. Sin embargo, la lógica puede garantizar que los datos que llegan al host sean contiguos: si el FIFO se llena, la lógica deja de escribir datos en el FIFO. Además, cuando el FIFO queda vacío después de haberse llenado, la lógica solicita que se envíe un EOF (fin de archivo) al host. Como resultado, el programa informático recibe todos los datos que se escribieron en el FIFO antes de que este se llenara. Después de esos datos, el programa recibe un EOF. Esto es lo mismo que ocurre cuando se llega al final de un archivo normal.

Este mecanismo garantiza que el programa informático pueda confiar en que los datos que llegan son correctos y contiguos. Si se pierde la contigüidad, el EOF obliga al programa a cerrar el archivo de dispositivo. Si el programa abre de nuevo el archivo, los datos empezarán desde un fotograma nuevo, gracias a @wait_for_frame.

Esta es la parte correspondiente del código 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 está alto cuando el FIFO ha estado lleno. Este registro cambia a bajo cuando el archivo de dispositivo no está abierto. @fifo_has_been_full cambia a alto cuando @fifo_full y @fifo_has_been_nonfull están ambos altos.

@fifo_full está conectado al puerto «full» (lleno) del FIFO. Pero, ¿por qué es necesario @fifo_has_been_nonfull? La razón es que un FIFO a menudo mantiene su señal «full» (lleno) en alto mientras el FIFO se mantiene en el estado de reset. La finalidad de esta característica es decir a la lógica de aplicación que el FIFO aún no está listo para recibir datos. La finalidad de @fifo_has_been_nonfull es evitar que @fifo_has_been_full se ponga en alto erróneamente en este escenario.

@user_r_read_32_eof se pone en alto cuando @fifo_has_been_full y @user_r_read_32_empty están ambos altos. En otras palabras, se envía un EOF al host cuando el FIFO ha estado lleno en el pasado y ahora está vacío. Nótese que en esta situación, de todos modos no se escribirán nuevos datos en el FIFO.

Hay una página aparte que analiza una solución similar para garantizar la contigüidad de los datos. La solución presentada en esa página es necesaria cuando los dos lados del FIFO pertenecen a dominios de reloj (clock domains) diferentes. En el código presentado en esta página, el FIFO es síncrono con un solo reloj. Por tanto, la implementación de @fifo_has_been_full es más sencilla en esta página.

Escritura de datos en el FIFO

La siguiente parte del código Verilog escribe los datos de píxeles en el 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;

Los datos de píxeles del sensor de cámara llegan como elementos de datos de 8 bits de ancho. Esta parte de la lógica reorganiza esos elementos en palabras de 32 bits para poder escribirlos en el FIFO. El flujo Xillybus de 8 bits (/dev/xillybus_write_8) no se usa para este propósito por dos razones:

Cuando @wait_for_frame está alto, no se escribe nada en el FIFO debido a una de dos posibilidades: el archivo de dispositivo no está abierto, o el archivo de dispositivo está abierto pero aún no se ha alcanzado el comienzo de un fotograma nuevo.

Cuando HSYNC del sensor de cámara está alto, significa que las señales de datos contienen píxeles válidos. El valor de la expresión «sample_valid && hsync» combina dos criterios: cuando @sample_valid está alto, @hsync y @D contienen valores válidos. Así que si @hsync está alto, el valor de @D se copia en una parte de @dataword. También, si @D se copia en la última parte de @dataword (es decir, @byte_position es igual a 3), @fifo_wr_en se pone en alto. Como resultado, @dataword se escribe en el FIFO. Más concretamente, la expresión para @fifo_wr_en es así:

fifo_wr_en <= !fifo_has_been_full;

Así que si @fifo_has_been_full está alto, no se escribe nada en el FIFO, como ya se ha mencionado antes.

La relación entre el código Verilog y los pines reales

El código Verilog anterior usa el puerto inout llamado J6, pero, ¿cómo llegan las conexiones de este puerto al conector de pines? La respuesta se encuentra en xillydemo.xdc. Este archivo forma parte del proyecto Vivado que crea el flujo de bits (bitstream) (en el directorio «vivado-essentials»).

xillydemo.xdc contiene diversa información necesaria para que la FPGA funcione correctamente como componente electrónico. Entre otras cosas, este archivo contiene estas líneas:

[ ... ]

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

[ ... ]

La primera línea dice que la señal J6[0] debe conectarse a U22. Esta es una posición en el encapsulado físico de la FPGA. Según los esquemas de Smart Zynq, este pin de la FPGA está conectado al primer pin del conector de pines. Las posiciones de los demás puertos se definen de la misma manera.

Conclusión

Esta página ha mostrado cómo obtener los datos de píxeles del OV7670 y enviarlos al host mediante el núcleo IP de Xillybus.

La siguiente parte de este tutorial explica cómo usar el núcleo IP de Xillybus para cambiar los registros del sensor de cámara con la ayuda de SCCB (es decir, I2C). Esto es útil para cambiar los parámetros de la cámara. En particular, es necesario para obtener una imagen con los colores correctos.

Esta página se ha traducido del inglés mediante traducción automática. En caso de duda, consulta el texto original.
Copyright © 2021-2026. All rights reserved. (dcc38493)