Primera parte - Sincronización
Para empezar creamos un proyecto nuevo con el
System Builder, debe tener las siguientes características:
Dentro del proyecto crearemos un módulo verilog llamado "
mi_vga", con las siguientes entradas y salidas:
Se trabajará en este caso con una resolución de 640x480 pixeles, pero es posible que trabajemos hasta con 1024x7868 pixeles por lo que los puertos de salida correspondientes al barrido de los pixeles tienen de 11 bits de longitud cada uno.
La entrada de reloj como se calculó durante la explicación teórica será de 25MHz y son necesarios pulsos de sincronización vertical y horizontal como también se menciona allí.
Luego se agregan unos parámetros locales según se demostraron, que indican las constantes que definen las etapas de las señales de sincronización:
Para el caso del barrido horizontal tenemos un periodo de señal, luego con la señal en alto un borde frontal izquierdo con 48 pixeles de ancho, el área visible de 640 pixeles, y un borde final derecho de 16. La señal finaliza con un tramo de retraso de 96 pixeles.
La señal de sincronización vertical tiene la misma forma de la horizontal, pero con rangos diferentes, el borde inferior tiene 33 pixeles de ancho, el área visible unos 480 pixeles, el borde derecho 10 y el retraso es de 2 pixeles.
Ahora podemos empezar con la generación de las señales de sincronización, para empezar necesitamos un contador que haga un barrido completo de la pantalla. Para obtenerlo usamos un bloque always, que funciona a cada flanco positivo del reloj de nuestro módulo.
Con el registro que habíamos declarado previamente, contamos horizontal y vertical siempre que estemos dentro de los rangos de la señal completa, y reiniciamos el contador cada vez que lleguemos al borde derecho para el barrido horizontal, y en el borde inferior para el barrido vertical .
Ahora, teniendo un registro que nos almacena el valor del pixel actual dentro de la pantalla, es posible generar la señal de sincronización. Estas estarán en alto siempre que hallamos recorrido los pixeles entre el borde inicial y el área visible, y aún no hallamos llegado al final de la línea en la que nos encontramos.
Asignamos estas señales a las salidas de sincronización y creamos una bandera que indica que estamos en el área visible, usando la constante que contiene el ancho en pixeles de nuestra pantalla.
Ya listo el módulo
mi_vga revisamos la sintaxis y lo instanciamos en el módulo principal, pero como se mencionó antes, se requiere de un reloj de 25MHz para su funcionamiento, por lo que se requiere una señal de oscilación de esta frecuencia. La mejor forma de generar este reloj es usando un PLL, ya que la salida que genera este a diferencia de la de un divisor de frecuencia mantendrá la fase del sincronizador VGA.
Usaremos un IP Core disponible en el IP Catalog (Para acceder a este seguir el
manual de IP Cores). Buscamos el "Altera PLL" ubicado en Clocks. PLLs and Resets > PLL
Al seleccionarlo se debe indicar el nombre para el PLL al final de la ruta de la carpeta del proyecto en el que se está trabajando y el HDL que se debería utilizar al generar el nuevo IP, en este caso Verilog.
Clic en OK.
Ahora se abrirá una ventana como la que se muestra en la imagen, se colocan los parámetros ahí indicados:
Reference Clock Frecuency: 50MHz y Desired Frecuency: 25Hz
Se da clic en finish, con lo cual se pide la confirmación sobre agregar el nuevo módulo al proyecto que tenemos abierto, lo cual debemos aceptar. Con esto aparecerán estos nuevos archivos en el proyecto. Abriremos el
pll_vga.v
Copiamos la instanciación que aparece allí descrita y la añadimos en el módulo principal, junto a la instanción de mi_vga
Ya contamos con el módulo de sincronización VGA y con el clock con el que funcionará, podemos ahora trabajar en la descripción del módulo principal.
Se declaran dos cables, que permitirán usar los contadores que se habían descrito en el módulo de sincronización, al igual que el estado de video_on que permite saber si se está barriendo la parte visible de la pantalla y las señales de sincronización recientemente obtenidas.
Se usan tres registros que contendrán la cantidad de color en el esquema RGB. Cada registro tiene 4 bits, ya que este esquema contempla 255 posibles tonalidades de cada color.
Se asignan las entradas, para cada color los 4 pines de salida, Se declara un cable para conectar el reloj de salida del PLL al módulo mi_vga.
y para cada señal de sincronización 1 pin. Se hacen las dos instanciaciones con sus respectivas conexiones.
Se define un multiplexor de video para asignar los valores de salida de color y así probar el funcionamiento del hardware que acabamos de describir, probaremos poniendo la pantalla en color rojo, lo que en RGB es R=255, G=0, B=0.
Procedemos a programar el
proyecto en la FPGA que estamos utilizando. Ahora es necesario conectar la FPGA a una pantalla para probar el diseño, pero como la que estamos utilizando no tiene entrada para cable VGA es necesario hacer las conexiones desde la FPGA hasta el cable VGA que conectaremos al monitor para eso se usa el siguiente circuito:
Lo que sucede aquí es que al enviar la cantidad de color a través de los pines GPIO los diferentes valores de resistencias resultan en variaciones a la entrada del puerto VGA, lo que se interpreta nuevamente como color, así para los tres colores RGB, se envía por dos pines las dos señales de sincronización, con resistencias necesarias para el acople. Los pines marcados en verde comparten tierra.
Cabe aclarar que la vista que está en la imagen está invertida respecto a lo que se vería en el cable. En esta parte es conveniente usar la tarjeta
multimedia ADT, para no tener dificultades con las conexiones.
Después de conectar se vería algo como esto:
Segunda Parte - Cuadro con movimiento en la pantalla
Sabiendo con funciona la generación de video desde la FPGA, las posibilidades son grandes, más si tenemos en cuenta herramientas como los IP Cores, el PicoCtrl, NIOS II, entre otros que esteremos viendo en esta misma página
Para empezar probaremos algo sencillo, un cuadrado que se desplace a través de la pantalla detectando sus bordes.
Primero, declara dos registros que almacenarán el valor de la posición con su coordenada horizontal y vertical en pixeles, y dos banderas para indicar cuando el cuadrado está en una posición que nos interesa. También hay un cable que transmitirá un estado lógico alto siempre que nos encontremos al final de cada frame o fotograma.
Ahora, hay que definir los estados de las banderas, los cuales se definirán cada vez que pase un frame, por lo que todos los bloques always dependerán de esta señal.
En primer lugar se revisa la posición, si estamos a 100 pixeles horizontal y 50 pixeles vertical de completar la línea que estamos barriendo, la bandera se pondrá en alto, lo que le indicará al hardware que ya estamos barriendo el área del rectángulo que queremos ver. Con este dato conocemos que el rectángulo ha llegado al límite de la pantalla y que es necesario trasladarlo en dirección opuesta a los bordes de la pantalla, en caso de que la señal esté en bajo, le daremos movimiento al rectángulo de forma perpendicular a la dirección anterior.
Después de definir el movimiento de nuestro rectángulo queremos darle color para visualizarlo en pantalla, para esto debemos saber que estamos en los pixeles que le corresponden, para ello está el cable box_on, el cual lleva la señal que está en 1 siempre que el pixel barrido sea del rectángulo, y 0 cuando no lo es.
Se modifica el multiplexor que teníamos en la primera parte para que siempre que la señal esté en uno, el pixel se ilumine de un color diferente al fondo, en este caso, azul.
Se compila el diseño, se programa, y se visualiza así:
Tercera parte - Poniendo una imagen
Durante esta parte pondremos una imagen en pantalla, para lo cual usaremos una de
ejemplo. Es necesario tener Matlab a la mano.
Para usar la imagen necesitamos que esté en formato .mif el cual permite al hardware a través de una ROM tomada de los IP Cores hacer la lectura de los pixeles de la imagen para así mostrarlos en la pantalla.
Tener la imagen en el formato .mif es sencillo. Descargue el
script de matlab, y asegúrese de que la imagen está en la misma carpeta del script o que la ruta está indicada correctamente, ejecutelo y revise que en la misma carpeta aparezca un archivo llamado "
imagen.mif". Si lo abre con bloc de notas se verá así:
Ahora copiamos el archivo recién creado a la carpeta del proyecto que estamos trabajando. Como se dijo, almacenaremos la información de la imagen contenida en el archivo .mif dentro de una ROM. Para crearla vamos al catálogo de IP Cores, y escogemos: Basic Functions > On Chip Memory > ROM: 1-PORT
Al seleccionarla se escoge un nombre, y se genera como un archivo verilog.
Dentro de la ventana es necesario cambiar los parámetros resaltados, ya que las características de la ROM dependen del tamaño de la imagen, como se vió en el archivo .mif. El bus de salida 'q' será tan ancho como la imagen y la cantidad de palabras de memoria dependen de la profundidad de ella. Estos datos se encuentran como WIDTH y DEPTH respectivamente.
Luego, le indicamos a la ROM de dónde sacar los datos de la imagen, por lo que en la ventana de inicialización de la memoria (Mem Init) agregaremos el archivo .mif que creamos y damos clic en finalizar, confirmando que queremos agregar la ROM al proyecto.
En el navegador de archivos aparecerá el archivo rom_imagen.qip o como la hallamos llamado en el paso anterior y un archivo verilog con el mismo nombre. Abrimos el archivo verilog.
Estando en el archivo verilog creamos la instanciación y la abrimos para copiarla al módulo principal. Ahora regresamos al módulo principal.
De la misma manera en que habíamos hecho para la segunda parte, creamos una señal que indica cuando estamos barriendo los pixeles de la imagen, la cual tiene una resolución de 82x100.
Se declaran dos cables adicionales que permitirán conectar a la entrada de la rom la dirección, que será el dato exacto que queremos leer, y un cable para la salida del dato que guardamos.
Al instanciar la ROM se conectan los dos cables que mencionamos y el reloj para leerla.
También tenemos que modificar el mux de video para que siempre que estemos en los pixeles de la imagen coloque los niveles de cada color salidos de la ROM en la pantalla.
El nivel de rojo está entre los bits 11 y 8 del dato rgb_image, el color verde entre los bits 7 y 4 y el azul entre los 4 menos significativos.
Al probarlo en una pantalla, se puede ver la imagen que teníamos de ejemplo en la esquina superior izquierda.
Cuarta parte - Animación
El siguiente experimento será con otra
imagen, la cual también es de mapa de bits (BMP), para usarla tendremos que seguir el mismo procedimiento de la tercera parte para obtener el archivo "
image_render.mif", no olvidar en imread cambiar el archivo a 'render.bmp'.
Abrimos el archivo y revisamos los dos parámetros requeridos para la ROM de acceso. En este caso WIDTH= 15064 y DEPTH= 12. Creamos otra ROM para estos parámetros.
Agregamos el archivo creado al proyecto de igual manera.
Creamos la instanciación, y la colocamos en el módulo principal. Las señales que se conectan a la nueva instanciación, serán las mismas que para la otra ROM por lo que se debe eliminar la anterior.
Se modifica la señal image_on para el nuevo tamaño de la imagen, y de igual manera la señal de dirección.
Probándolo hasta este punto se puede ver la imagen render en el borde de la pantalla, tal como lo hicimos con la imagen anterior. El proyecto se puede descargar en la sección de archivos con el nombre
Practica_VGA_Parte2_3.qar.
Ahora, algo más divertido. Haremos una animación con la imagen que acabamos de poner.
Para ellos, haremos algunas modificaciones. Primero, se definen algunos parámetros locales, la altura de la imagen y las dos diferentes anchos que puede tomar, también un registro para definir el cambio entre las diferentes posibilidades que va a tener el frame esta vez. Un bloque always se encarga de que todas ocurran.
Se cambia la definición de la señal image_on la cual depende del nuevo tamaño de la imagen. Hay ahora una nueva señal llamada offset que tomará un valor de 11 bits, según el source_frame que halla definido el always anterior.
También se cambia la señal adrress_image, ahora, ésta depende de la señal offset.
Por último se elimina la condición del box_on dentro del mux de video para evitar que tape la imagen.
En la pantalla se puede ver así:
Quinta parte - Explorando posibilidades.
Si se quiere agregar el efecto de Alpha-blending como el que se ve en la imagen de abajo (Imagen tomada de commons.wikimedia.org/w/index.php?curid=25900468), se puede agregar el código de la izquierda al proyecto en el que trabajamos
Otra opción es cambiar la resolución de la pantalla en la etapa de sincronización, esto se hace modificando el archivo que trabajamos en la primera parte llamado mi_vga.
Teniendo en cuenta la teoría vista al empezar, lo que cambian son las señales para el barrido horizontal y vertical, por lo que es necesario definir los intervalos de la señal nuevamente.
Los nuevos parámetros locales para una sincronización de una pantalla de 1280x800 a 60Hz.
A partir de lo que hemos aprendido ya es posible la generación de cualquier gráfico en la FPGA sólo haciendo una descripción del comportamiento en hardware de nuestra pantalla.
Referencias