Simulación de salida de video en un diseño que genera una señal VGA

Diseño HDL con este lenguaje. Módulos y testbenchs. Estilos y trucos de codificación, etc. NOTA: dado que hay entornos como ISE que soportan Verilog pero no SystemVerilog, señalad dentro de un post que de lo que se va a tratar es SystemVerilog si es el caso.
Avatar de Usuario
mcleod_ideafix
Site Admin
Mensajes: 80
Registrado: 14 Ago 2018, 01:15

Simulación de salida de video en un diseño que genera una señal VGA

Mensaje por mcleod_ideafix » 02 Sep 2018, 03:01

La siguiente explicación usa únicamente herramientas libres y multiplataforma. Para recrearlo por vuestra cuenta necesitareis instalar:
- Icarus Verilog. http://iverilog.icarus.com/
- GTKWave. http://gtkwave.sourceforge.net/
- Imagemagick. https://www.imagemagick.org/script/download.php
(en los comandos de ejemplo asumo que el comando es "magick", no "convert", como venía siendo habitual en esta herramienta)
- ffmpeg (la utilidad de línea de comandos, no el codec). https://www.ffmpeg.org/download.html
Si trabajais en Linux u OS X, necesitareis el compilador de C

Ah! y no se necesita ninguna FPGA!

Habitualmente, cuando uno está depurando un diseño, escribe sus testbenchs y tal, lo habitual es enfrentarse con el simulador (iSim en ISE, ModelSim en Quartus, GTKWave en entornos libres) y pantallas como ésta:

gtkwave.png
gtkwave.png (75.17 KiB) Visto 5054 veces
La información que te da un cronograma es muy útil, y el ingeniero habitualmente encuentra errores que habían pasado desapercibidos, o señales que no se terminan de comportar como deben.

Supongamos que estamos depurando el core de ejemplo del fantasmita rebotando por la pantalla (ver viewtopic.php?f=32&t=40 ). Este core da una salida VGA a 640x480 en donde se muestra un fondo y un sprite animado rebotando por la pantalla. De hecho el cronograma de la figura anterior es de ese core. Sólo con ver unos cuantos microsegundos de tiempo (si sabemos qué es lo que esperamos encontrarnos) se puede comprobar que las señales más importantes, como los sincronismos, la señal de display enable, se generan a su debido tiempo, y que la señal RGB pasa a valer negro durante los periodos en los que display_enable está a nivel bajo. Vamos, que tiene toda la pinta de que esto generará una imagen VGA "legal".

Pero... ¿y el contenido de esa pantalla? Las señales RGB están en el cronograma, eso es cierto, pero claro, inferir de ahí la imagen que está generando el core (salvo que ésta sea muy sencilla), es como leer Matrix directamente sin descodificar.

¿Y si hubiera alguna forma de poder construir la imagen que se está genernado? Pues la hay. Sólo hay que añadir a nuestro testbench un módulo que se comporte como un monitor. Es decir, un módulo que acepte como entrada los sincronismos y RGB, y vaya guardando, bien en una matriz en memoria, o en un fichero, los valores RGB que forman la pantalla.

Un ejemplo de módulo testbench que hace eso es el siguiente:

Código: Seleccionar todo

module framegrabber (
  input wire clk,
  input wire [10:0] hc,
  input wire [10:0] vc,
  input wire [7:0] r,
  input wire [7:0] g,
  input wire [7:0] b,
  input wire hsync,
  input wire vsync
  );

  parameter HTOTAL = 800;
  parameter VTOTAL = 525;
  parameter HSYNCPOL = 0;  // 0 = polaridad negativa, 1 = polaridad positiva
  parameter VSYNCPOL = 0;  // 0 = polaridad negativa, 1 = polaridad positiva

  integer nframes;
  integer f;
  initial begin
    f = $fopen ("fotograma.rgb","wb");
    nframes = 0;
  end

  always @(posedge clk) begin
    if (vc == VTOTAL-1 && hc == HTOTAL-1)
      nframes <= nframes + 1;
    if (nframes > 0) begin
      if (hsync == HSYNCPOL || vsync == VSYNCPOL)
        $fwrite (f, "%c%c%c", 64, 64, 64);  // SYNC de color gris
      else begin
        $fwrite (f, "%c%c%c", r, g, b);
        if (vc == VTOTAL-1 && hc == HTOTAL-1) begin
          $fclose (f);
          $display ("Fotograma volcado. Fin de la simulacion");
          $finish;
        end
      end
    end
  end
endmodule
Este módulo "framegrabber" funciona de la siguiente manera:

- Primero, por simplificarlo al máximo, el módulo recibe más información de la que habitualmente recibiría un monitor físico. La información de más que se recibe son las coordenadas de pantalla que se están enviando y el reloj de pixel (este último dato sí que lo recibe un monitor si se usa DVI o HDMI.

- Así, el frame grabber, en cada ciclo de reloj va leyendo los datos RGB y los va volcando en un fichero binario. Verilog, en los testbenchs, dispone de una serie de tareas del sistema que son funciones con el símbolo $ delante. Hay toda una colección de ellas dedicada a la E/S, al estilo de las que hay en C.

- Pero por la salida VGA siempre se está enviado información, aunque no se vea en pantalla: después de cada línea visible hay un intervalo llamado "blanking" en donde no se debe enviar ninguna señal RGB, es decir, debe estar la pantalla a negro. Dentro de ese intervalo de blanking hay otro en donde la señal de sincronismo se activa. En nuestro frame grabber incluiremos toda esta información, es decir, la zona "activa" (los 640x480 píxeles), la región de blanking, y dentro de ella, la región de sincronismos. Se puede observar en el código que se pregunta si alguna de las dos señales de sincronismo están activas, y si es así, el pixel que se pinta es de color gris oscuro, en otro caso, el pixel se pinta del color leído.

- La imagen así grabada es por tanto más de los 640x480 píxeles que vemos en pantalla. En realidad, es de 800x525 pixeles. Estos números vienen de la especificación de este modo de pantalla, en concreto, los parámetros HTOTAL y VTOTAL (lo que se ve en pantalla corresponde a HACTIVE y VACTIVE y por tanto el blanking es el resto).

Es habitual, al explicar un formato de pantalla con sus timings, el usar un esquema como éste:

VGA_crtc.gif
VGA_crtc.gif (15.12 KiB) Visto 5054 veces
En donde se ve todo lo que se genera por la salida VGA, especificando qué cosas son las que se ven en pantalla y qué cosas corresponden al blanking, sincronismos, etc. Como en el framegrabber estamos recogiendo todo eso, lo que se guardará en el fichero tendrá ese aspecto.

Pero ¿desde qué momento empezamos a grabar datos RGB en el fichero? Si quisiéramos simular un monitor má realístico, el módulo debería esperar a que terminase un retrazo vertical (que vsync se desactivara), esperar algunas líneas (= esperar algunos pulsos de hsync) y entonces empezar a recoger píxeles hasta el siguiente puso de hsync. Esperar, y volver a coger píxeles.

Nosotros optaremos por algo más simple: aprovechando que este módulo estará dentro del diseño (que no dentro de la FPGA, porque estamos simulando), usaremos los valores de los contadores horizontal (coordenada X) y vertical (coordenada Y) para saber en todo momento dónde está el pixel que se está enviando. Estos contadores van, el horizontal desde 0 a 799, y el vertical, de 0 a 524 (los 800x525 píxeles totales antes mencionados), y por tanto, contaremos fotogramas de salida de video cada vez que ambos contadores estén a punto de valer 0, que corresponde al pixel activo (visible) de la esquina superior izquierda.

El módulo necesita comenzar desde el primerísimo pixel, así que descarta el primer frame y comienza realmente a grabar en fichero a partir del siguiente. De eso se encarga la variable nframes que cuenta los frames recibidos (sabe que ha terminado un frame cuando los contadores toman sus valores máximos). Cuando el frame se ha grabdo por completo, se cierra el fichero y se termina la simulación.

Vamos entonces a usar Icarus Verilog para compilar el diseño y prepararlo para la simulación. Desde el directorio donde se encuentran los ficheros que adjunto para este artículo, ejecutad el comando iverlog, así:

Código: Seleccionar todo

iverilog -ofantasma.out *.v
Esto coge todos los ficheros Verilog y los compila, obteniendo un fichero "fantasma.out" que implementa la versión simulada del diseño en un pseudo lenguaje máquina. La compilación para simulación es muchísimo más rápida que la síntesis para FPGA.

Para ejecutar la simulación, que es como decir que ejecutamos el programa que implementa la versión simulada del diseño, ejecutamos el programa vvp, que viene junto con iverilog, así:

Código: Seleccionar todo

vvp fantasma.out
Este comando tardará bastante más que el anterior en ejecutarse. Está ejecutando un código que simula el comportamiento del circuito con todas sus señales. No digo "emula", porque como vereis, no lo hace en tiempo real. Al cabo de unos 13 segundos (es lo que tarda en mi PC), se obtiene este mensaje:

33.60 ms : Fotograma volcado. Fin de la simulacion

Es decir: la ejecución del programa ha simulado 33.6 milisegundos de tiempo desde el punto de vista del diseño que estamos considerando, pero le ha llevado muchísimo más en tiempo "de verdad". Lo que la FPGA a 25 MHz "ejecuta" en 33.6 milisegundos exactos, a un PC con un Pentium 4 a unos cuantos gigahercios le lleva varios segundos.

Tras la ejecución de la simulación, obtenemos un fichero llamado "fotograma.rgb" . Este fichero contendrá 800x525 pixeles, siendo cada pixel 3 bytes. En total, 800x525x3 = 1.260.000 bytes. El ponerle extensión .rgb es para que ImageMagick lo detecte como un fichero sin formato, pero con datos RGB, no en escala de grises. Para poder convertirlo a un formato que podamos ver cómodamente, basta con usar ImageMagick así:

Código: Seleccionar todo

magick -size 800x525 -depth 8 fotograma.rgb fotograma.png
Y el resultado es éste:

fotograma.png
fotograma.png (9.69 KiB) Visto 5054 veces
Salta a la vista que este gráfico tiene el mismo formato que el anterior, el que se usa para timings. De hecho en este otro gráfico están esos mismos timings y se pueden extraer, sin más que recordar que cada pixel dura un ciclo de reloj de pixel. Así, contando la anchura del pulso de sincronismo horizontal (perfectamente visible a la derecha) podemos saber cuántos ciclos de reloj dura. Cuando se cuenten pixeles en vertical, recordar que de un pixel vertical al que está justo por debajo de él han pasado 800 ciclos de reloj (la cantidad de píxeles -ciclos- en una linea).

Si notais algo "raro" en el fantasmita, es simplemente porque he variado ligeramente el módulo de sprite para que lo pinte con un zoom x4, ocupando ahora 64x64 pixeles (es muy sencillo hacer zoom a un sprite x2, x4, x8 o en general, a una potencia de 2. Mirad el pequeño cambio en sprite.v respecto del original que os di en el otro post)

En el gráfico se aprecia perfectamente la parte de blanking, con la pantalla apagada, y las dos zonas de sincronismos, el horizontal, a la derecha, y el vertical, abajo. Si os fijais bien, el sincronismo vertical ocupa dos lineas de la imagen. Esta misma información se puede obtener del cronograma de la primera figura de este post, que está tomada justo en el momento en que se produce un pulso de sincronismo vertical, en donde se puede comprobar que dicho pulso abarca exactamente dos líneas (dos pulsos de sincronismo horizontal).

El siguiente paso es obvio: si podemos grabar un fotograma, ¿por qué no grabar varios uno detrás de otro y recrear la animación, durante N frames, que genera este core? De ello se encarga otro módulo de testbench, animgrabber, que no es más que una versión ligeramente modificada del anterior:

Código: Seleccionar todo

module animgrabber (
  input wire clk,
  input wire [10:0] hc,
  input wire [10:0] vc,
  input wire [7:0] r,
  input wire [7:0] g,
  input wire [7:0] b,
  input wire hsync,
  input wire vsync
  );

  parameter HTOTAL = 800;
  parameter VTOTAL = 525;
  parameter HSYNCPOL = 0;  // 0 = polaridad negativa, 1 = polaridad positiva
  parameter VSYNCPOL = 0;  // 0 = polaridad negativa, 1 = polaridad positiva
  parameter MAXFRAMES = 60;

  integer nframes;
  integer f, res;
  initial begin
    $timeformat(-3, 2, " ms", 10);   // mostrar tiempo escalado a ms (10^-3), con 2 decimales de precision, usando " ms" como sufijo, y ocupando todo 10 caracteres
    f = $fopen ("anim.raw","wb");
    nframes = 0;
  end

  always @(posedge clk) begin
    if (vc == VTOTAL-1 && hc == HTOTAL-1)
      nframes <= nframes + 1;
    if (nframes > 0) begin
      if (hsync == HSYNCPOL || vsync == VSYNCPOL) begin
        res = $fputc(8'h40, f);
        res = $fputc(8'h40, f);  // SYNC de color gris oscurillo
        res = $fputc(8'h40, f);
      end
      else begin
        res = $fputc(r, f);
        res = $fputc(g, f);
        res = $fputc(b, f);
        if (vc == VTOTAL-1 && hc == HTOTAL-1) begin
          $display ("%t : Fotograma %4d", $realtime, nframes);
          if (nframes == MAXFRAMES) begin
            $fclose (f);
            $finish;
          end
        end
      end
    end
  end
endmodule
Este módulo toma un parámetro, MAXFRAMES, desde el que indicamos cuántos frames queremos grabar en el fichero de salida (anim.raw). Para usar este módulo en lugar del anterior, editar tb_fantasma.v y quitar/poner comentarios donde se indique. Recordar que la simulación es lenta: si 33.6 milisegundos me lleva simularlos 13 segundos, 1 segundo de animación (1000 milisegundos, 60 fotogramas) me llevará 387 segundos, es decir, casi 6 minutos y medio de tiempo real.

Compilamos con iverilog y ejecutamos la simulación con vvp, todo igual que en el primer ejemplo, y la simulación tiene esta pinta (las primeras 10 líneas)

> vvp fantasma.out 33.60 ms : Fotograma 1 50.40 ms : Fotograma 2 67.20 ms : Fotograma 3 84.00 ms : Fotograma 4 100.80 ms : Fotograma 5 117.60 ms : Fotograma 6 134.40 ms : Fotograma 7 151.20 ms : Fotograma 8 168.00 ms : Fotograma 9 184.80 ms : Fotograma 10 ... ... 9979.20 ms : Fotograma 593 9996.00 ms : Fotograma 594 10012.80 ms : Fotograma 595 10029.60 ms : Fotograma 596 10046.40 ms : Fotograma 597 10063.20 ms : Fotograma 598 10080.00 ms : Fotograma 599 10096.80 ms : Fotograma 600

Si no habeis tocado el valor de MAXFRAMES en tb_fantasma.v , habreis generado 600 fotogramas tardando 10096.80 milisegundos de tiempos simulado, que probablemente hayan sido un buen puñado de minutos de tiempo real (en mi caso, alrededor de 1 hora).

El resultado, anim.raw, es un fichero que contiene concatenados, tantos frames de pantalla como hayamos indicado en MAXFRAMES.

Convertir esto a un video es tarea de un fichero batch que se incluye, convierte.bat. Este batch necesita para funcionar de ImageMagick, ffmpeg y una utilidad de la que se da el código fuente en C y un ejecitable para Windows. En Linux y OS X se genera el ejecutable con gcc -o anim2frame anim2frame.c

El video se genera en tres fases:
- anim2frame trocea el fichero anim.raw en frames discretos con una numeración. Cada frame tiene el mismo formato que "fotograma.rgb", o sea, es una imagen de 800x525
- Imagemagick toma todos esos frames discretos y crea un video MPG a 25 fps
- Con ffmpeg remuestreamos el video para que se reproduzca a 60 fps, y así concuerde con la velocidad de refresco del original. El resultado final puede reproducirse en el PC.

Si lo quereis mejor en MP4, cambiad la llamada a ffmpeg por esta otra:

Código: Seleccionar todo

ffmpeg -i temp.mpeg -r 60 -b:v 4000k -c:v libx264 filter:v "setpts=0.416666667*PTS,crop=800:524:0:0" simulacion.mp4
El video tendrá mejor calidad (4Mbps). Por otra parte, el codec H264 necesita que el tamaño horizontal y vertical sea múltiplo de 2, así que con el filtro "crop" me como la última línea de pantalla, dejando al video con 800x524. Os dejo en el post el video en este formato, que pesa menos. Asimismo se puede generar un video con sólamente la región activa usando crop=640:480:0:0


¿Y con el sonido se puede hacer algo parecido? Se puede, por supuesto. Jotego ya lo mostró hace tiempo en su blog. Nosotros aquí usaremos un ejemplo similar, y la utilidad sox, también libre y multiplataforma, para procesar el fichero que genere el testbench y convertirlo a un formato de audio más estándar. Será tema para un próximo post.
Adjuntos
ejemplo_simulacion_video.zip
(44.65 KiB) Descargado 440 veces
simulacion.mp4
(491.07 KiB) Descargado 363 veces

Responder

Volver a “Verilog / SystemVerilog”