- 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:
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
- 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:
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
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
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
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
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
¿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.