Sistemas Informáticos Curso 2002-2003 Procesado de imágenes digitales sobre un sistema hardware dinámicamente reconfigurable Isaac Álvarez Gallego Sergio Bernabé Villalobos Gerardo Robledillo Gómez Mª Pilar Rodríguez Crespo Dirigido por: Prof. Hortensia Mecha López Prof. José M. Mendías Cuadros Dpto. de Arquitectura de Computadores y Automática Facultad de Informática Universidad Complutense de Madrid Índice 1. RESUMEN DEL PROYECTO ........................................................................ 3 2. OBJETIVO DEL PROYECTO......................................................................... 5 3. CONTEXTO ................................................................................................... 6 3.1 Hardware .................................................................................................. 6 ¿Qué es una FPGA?................................................................................... 6 Modo directo de Vídeo ................................................................................ 7 Control un monitor VGA .............................................................................. 7 La RAM ....................................................................................................... 8 La RAMDAC................................................................................................ 9 Configuración de la RAMDAC en formato TARGA y single-edge-clock 10 Reconfiguración ........................................................................................ 12 Configuración de la FPGA desde la Flash RAM.................................... 12 Proceso de arranque de la placa........................................................... 12 3.2 Software.................................................................................................. 14 3.2 Software.................................................................................................. 14 El formato .PACA ...................................................................................... 14 El formato .HEX ........................................................................................ 14 El formato .XES......................................................................................... 15 4. MÓDULOS INICIALES................................................................................. 16 4.1 Control de VGA....................................................................................... 16 Cálculo de valores de las constantes VGA ............................................... 16 Módulo de visualización de patrón fijo ...................................................... 18 Controlador de monitor con refresco de memoria ..................................... 19 Carga de datos en la RAM........................................................................ 22 4.2 Carga de imagen en memoria ................................................................ 22 GXSLoad ............................................................................................... 23 5. VISUALIZACIÓN Y FILTRADO DE IMAGEN EN RAM ................................ 24 5.1 Carga de una imagen en RAM............................................................ 24 Conversión y mostrado.......................................................................... 24 5.2 Visualización de una imagen en la RAM............................................. 25 5.3 Filtros de imágenes ................................................................................ 30 Introducción............................................................................................... 30 Filtros Píxel a Píxel: ............................................................................... 31 Filtro de Negativizado: ....................................................................... 31 Filtro de Blanco y Negro:.................................................................... 33 Filtro de Binarización:......................................................................... 35 Filtro de 8 Colores:............................................................................. 37 Filtros de Convolución de matrices:....................................................... 39 Filtro de Suavizado: ........................................................................... 40 Filtro de detección de bordes: ............................................................ 45 5.4 Integración .......................................................................................... 48 Proceso ................................................................................................. 48 6. RECONFIGURACIÓN.................................................................................. 52 6.1 Objetivo buscado ................................................................................ 52 6.2 Desarrollo............................................................................................ 52 7. MÓDULOS SOFTWARE DE CARGA Y VISUALIZACIÓN DE IMÁGENES. 55 7.1 Aplicación BMP2PACA ....................................................................... 55 1 7.2 Aplicación DIBUPACA......................................................................... 58 Apéndice A : ..................................................................................................... 62 uso de la herramienta Xilinx.......................................................................... 62 Utilización.................................................................................................. 62 Generación de archivos para el CPLD y la Flash...................................... 68 Apéndice B....................................................................................................... 71 Uso de la herramienta GXSLOAD ................................................................ 71 Utilización: ............................................................................................. 71 Apéndice C :..................................................................................................... 73 Hardware ...................................................................................................... 73 C.1. Código del módulo q muestra un patrón por pantalla : contador.vhd 73 C.2. Código dl módulo q guarda y lee un patrón de memoria: rayash.vhd 75 C.3. Código del subsistema Cargador:...................................................... 77 C.3.1 CargadorMem.vhd ....................................................................... 77 C.3.2 CtrlPar.vhd ................................................................................... 78 C.3.3 escribidor.vhd............................................................................... 81 C.3.4 contador2.vhd .............................................................................. 83 C.3.5 biestd_r1.vhd................................................................................ 84 C.3.6 reggen.vhd ................................................................................... 84 C.4. Código del módulo que lee una imagen de memoria y la muestra: ... 86 C.5. Código de la arquitectura (módulo de control de la pantalla, cargador y filtro blanco y negro):................................................................................. 89 C.6. Código de los filtros ........................................................................... 95 C.6.1 Filtro de 8 colores : fil8co.vhd....................................................... 95 C.6.2 Filtro de binarización : filbin.vhd................................................. 100 C.6.3 Filtro de escala de grises: filbyn.vhd .......................................... 104 C.6.4 Filtro de negativización: filneg.vhd ............................................. 109 C.6.5 Filtro de suavizado: filsua.vhd.................................................... 113 C.6.6 Filtro de obtención de bordes: filbor.vhd .................................... 124 C.7. Primer archivo de reconfiguración de la FPGA:............................... 133 C.8. Módulo que elimina rebotes de una señal: ...................................... 138 Software...................................................................................................... 141 C.9. BMP2PACA ..................................................................................... 141 C.9.1 matrizRGB.h............................................................................... 141 C.9.2 matrizRGB.cpp........................................................................... 141 C.9.3 RGBPixmap.h ............................................................................ 142 C.9.4 RGBPixmap.cpp......................................................................... 142 C.9.5 GlSkel.h ..................................................................................... 153 C.9.5 GlSkel.cpp.................................................................................. 155 C.10 DIBUBIF ......................................................................................... 159 C.10.1 Pixel.h ...................................................................................... 159 C.10.2 GlSkel.h ................................................................................... 160 C.10.3 GlSkel.cpp................................................................................ 161 Apéndice D:.................................................................................................... 172 Forma de uso.............................................................................................. 172 9. Bibliografía ................................................................................................. 173 2 1. RESUMEN DEL PROYECTO El objetivo principal de nuestro proyecto era implementar sobre la FPGA de una placa XSV-800, un circuito de visualización y procesamiento de imágenes. Este procesamiento consistía en aplicar una serie de filtros a las imágenes. Como complemento al circuito de visualización, fue necesario crear una herramienta software que transformara imágenes de formato .BMP a un formato propio. Estas imágenes en el nuevo formato, se guardan en la RAM mediante otra herramienta software (heredada de un proyecto anterior) de comunicación entre el PC y la XSV-800 usando el puerto paralelo. Luego las imágenes se leerán de la RAM para ser mostradas en la pantalla por medio de una RAMDAC, un dispositivo contenido en la placa. En cuanto al procesamiento de las imágenes, se carga en la FPGA el filtro deseado, que trata la imagen, y la almacena de nuevo en la RAM para su posterior visualización. Los filtros disponibles son binarización, escala de grises, negativización, reducción del espectro de color a 8, suavizado y cálculos de los bordes una imagen. Como objetivo secundario también estaba conseguir reconfigurar dinámicamente la placa de una forma total. Para ello, guardamos en la memoria Flash de la placa dos configuraciones, con distintos filtros, y permitimos configurar la FPGA en cualquier momento con cualquiera de éstas, en función de las peticiones del usuario. En esta documentación se detalla el proceso de desarrollo seguido, las herramientas utilizadas, los conocimientos necesarios para trabajar con imágenes sobre FPGAs, además de incluir y explicar todos los circuitos y código fuente desarrollados por nosotros durante este proyecto. 3 Project Outline The main goal of this project is to implement on the FPGA of a XSV-800 board, a visualization and image processing circuit. This processing is based on the application of several image filters. As a complement to the visualization circuit, it was necessary to create a software tool that transforms .BMP images into an own format. Images in this new format will be stored inside the RAM through another software tool (inherited from a past SI project) that communicates the PC and the XSV-800 by the parallel port. The images stored inside the RAM memory will afterwards be showed in a VGA monitor through a RAMDAC, which is embedded in the board. For the image processing, the desired filter is downloaded into the FPGA, and then the image is managed and stored again into the RAM memory for its further visualization. The different filters that are available include: binarization, grey scale, negative effect, reduction of the colour spectrum to 8, softening of the image, and border settings. A secondary goal was making a total reconfiguration of the board dynamically. Therefore, two settings, with two filters, to reconfigure the FPGA are saved inside the Flash RAM, allowing configuring the FPGA with one or the other of the configurations at any point in time, depeding on user’s requests. This document shows a detailed description of the process, all the tools used to achieve the goals, and the required knowledge to manage images on a FPGA, as well as showing and explaining all the different circuits and codes developed during the project execution. 4 2. OBJETIVO DEL PROYECTO El enunciado del proyecto original era: “El objetivo fundamental de este proyecto es implementar sobre una tarjeta de FPGAs dinámicamente reconfigurable una tarjeta de visualización y procesamiento de imágenes. Se implementarán distintos circuitos hardware que realicen diferentes procesos sobre una imagen, de forma que en la tarjeta de la FPGA siempre se mantenga el algoritmo de visualización y puedan cambiarse, a gusto del usuario, los algoritmos de procesamiento. Se utilizará la posibilidad que proporciona el hardware reconfigurable de modificar parte del circuito mientras el resto sigue ejecutando un algoritmo. La implementación se realizará en una placa de prototipado APX-240 que dispone de una FPGA virtex 300, utilizando software de Xilinx Foundation, Model Technology y Synopsys.” Sobre este esquema inicial se han ido haciendo modificaciones sobre la marcha, la primera de las cuales consiste en el modelo de FPGA utilizado, que finalmente fue una XSV800-240HQ. 5 3. CONTEXTO 3.1 Hardware ¿Qué es una FPGA? Una FPGA es uno de los muchos circuitos prefabricados que existen de lógica programable, como también lo son la PLA, PROM, PAL y los CPLD, en los que se pueden implementar distintos circuitos hardware. Nuestra placa, además del modelo FPGA antes reseñado, tiene entre otros dispositivos un CPLD, que no es más que una PLA a gran escala con lógica adicional. Centrándonos en lo que concierne a la FPGA, esta es un array de puertas programable que consta de una serie de componentes: • Un array de celdas regularmente dispuestas sobre el silicio cuya funcionalidad es programable, denominados CLB’s • Una colección de celdas de entrada / salida dispuestas perimetralmente cuyas características también son programables, denominados IOB’s • Y una colección de bloques de interconexión, que bajo programación permiten conectar CLB’s e IOB’s entre sí, denominados PSM. Por tanto, los diseños que se implementan sobre la FPGA no se fabrican sino que se realizan programando adecuadamente los CLB’s, IOB’s y los bloques de interconexión. fig. 1 - Modo directo de vídeo de la Ramdac 6 Modo directo de Vídeo Las placas XSV pueden generar señales para controlar un monitor VGA [MEND01] bien directamente o bien a través de un chip BT481A RAMDAC incluido en la placa. En una primera aproximación no se utiliza la RAMDAC, sino que se genera directamente las señales de color en la FPGA, usando 6 bits. Para esto, las líneas de datos de la RAMDAC (p0-p5) se entienden como 6 bits de color, siempre que los jumpers J5, J6 y J7 estén en la posición correcta (evitando la RAMDAC y en configuración directa, ver fig. 1). Así, con dos bits para el rojo, dos para el verde y dos para el azul p0 r0 p1 r1 p2 g0 p3 g1 p4 b0 p5 b1 tendríamos 64 colores distintos. Por otro lado, las señales de sincronización del monitor, horizontal y vertical (hSync y vSync) deben generarse también en la FPGA. Esto implica decidir a qué frecuencia de reloj se va a trabajar, para ajustar los valores de número de líneas y columnas que se pintan, numero de ciclos hasta los hSync y vSync, ... El significado de estas señales se detalla en el siguiente apartado. Control un monitor VGA En este proyecto, las imágenes se muestran en una pantalla de tubo de rayos catódicos (CRT). En estas pantallas, la imagen se forma mediante un haz de electrones que barre la superficie fluorescente de una pantalla siguiendo un determinado patrón de movimiento (ver fig. 2). fig. 2 - Patrón de movimiento del haz de electrones CRT 7 Del control de este barrido se encargan las señales de hSync y vSync, y las se on esto, se entiende que la sincronización entre la posición del haz en cada m La RAM Las placas XSV tienen dos bancos de memoria SRAM formado cada g. 3 - Configuración de los bancos de memoria RAM. Por otro lado, las líneas d0-d7 del banco derecho de memoria (si s por esta razón (la existencia de dos módulos por banco de memoria) por la ñales de intensidad roja, verde y azul del haz de electrones. La señal de hSync marca el comienzo y final de una línea, mientras que por el contrario vSync determina el final de una pantalla completa (fin de la última línea de una pantalla). La intensidad de cada una de las señales de color del haz (RGB) deciden el color del punto (o píxel) que se dibuja en esa zona de la pantalla. C omento y los valores de intensidad generados es muy importante. Debe conocerse la velocidad del haz para, en conjunción con las señales hSync y vSync, conocer en cada momento la posición del haz sobre la pantalla y decidir los valores de intensidad a enviar (los del píxel correspondiente). uno por dos módulos de 4 Megabits, organizados en 219 bytes, y accesibles por byte. Ambos bancos funcionan de la misma manera. Disponen de señales oe*, ce* y we* (cuya funcionalidad se explicará al comentar la fig. 5). Dentro de cada uno de los bancos, ambos módulos comparten las líneas de direcciones y de oe*, ce* y we*. Por esto, puede entenderse cada uno de los bancos de memoria como una única SRAM de 8 Megabits, con 219 palabras de 16 bits. fi entendemos que todo el banco derecho tiene sus 16 bits de datos nombrados d0-d15, como vienen nombrados en el manual de las placas XSV [XSVB01]), están directamente conectadas a las líneas p0-p7 de la RAMDAC. E que cuando se trabaje con datos de 16 bits, se tendrá que enviar a la RAMDAC en un ciclo el byte menos significativo y en el siguiente el más 8 significativo, porque sólo el módulo inferior (la parte menos significativa) tiene conexión directa con la RAMDAC. La RAMDAC La RAMDAC es un dispositivo hardware de la placa, diseñado para el desarrollo de gráficos de color de alta resolución y que permite mostrar las imágenes con una mayor gama de colores de la que se dispone cuando se hace de forma directa. Conexión entre la FPGA, RAM y RAMDAC El modelo Bt481A de que disponemos nos permite cuatro modos de lor: 1. El formato TARGA, que son 15 bits de la forma 5:5:5 (5 bits para . El formato XGA, que presenta 16 bits de la forma 5:6:5. 3. El formato RGB/BGR que presenta 24 bits de color de la forma 4. Por último suministra un formato de 8 bits de pseudo color. as estas posibles configuraciones, se decide trabajar con el fig. 4 - co el rojo, 5 para el verde y otros tantos para el azul) 2 8:8:8 . De tod formato TARGA con 15 bits de color, lo que nos da 215 = 32.768 colores distintos, suficientes para tener una buena calidad de imagen, y en el modo single-edge clock que determina que los datos son cargados en el flanco de 9 subida del reloj, porque si se escoge dual-edge se realiza tanto en el flanco de subida como en el de bajada. Por otra parte, la RAMDAC posee una serie de registros de configuración que se explican en el siguiente apartado Configuración de la RAMDAC en formato TARGA y single-edge- clock Para configurar de este modo la RAMDAC (en TARGA, single-edge), los valores necesarios de los datos de configuración son los de la tabla 3. A 10100000 RD* 1 RS2..0 110 D7..0 10100000 BLANK* 0 TABLA 3 - VALORES DE DATOS DE CONFIGURACIÓN El registro RS[2..0] especifica el tipo de operación de lectura o escritura que se realiza y si es sobre un registro o paleta de colores. En nuestro caso, el valor “110” indica al MPU que debe acceder al registro A. La entrada de datos D representa el bus de datos para la configuración y contendrá la misma información que se desee escribir en el registro A, es decir D[7..0]=10100000. El registro A consta de 8 bits y debemos indicarlo con el valor “10100000” que indica lo siguiente: 1. A[7..4]=1010, sirve para seleccionar el modo 5:5:5 single-edge Clock. 2. A[3]=0 que siempre debe contener un cero lógico para asegurar que se opere correctamente. 3. A[2]=0 que indica que la señal RS2 dependerá del valor lógico de RS[2]. 4. A[1]=0 que indica que los bits de píxel seguirán el modo RGB. 5. A[0]=0 impide que el registro de selección extendido no pueda ser accedido. El registro RD* se pone a 0 indicando que se obtendrán datos de los registros RS y D en el flanco de bajada. El registro BLANK* ignora los valores RGB de los píxeles cuando vale 0 y los pone a negro (0 lógico), por tanto, este registro se inicializa a 0 al 10 principio, y vale 1 siempre que nos encontremos en la región válida del muestreo. A continuación se comentan otros aspectos de configuración que pueden resultar interesantes: • El formato true-color 5:5:5 de carga (formato TARGA), contiene 16 bits organizados de la siguiente forma: El bit B15 es ignorado Los bits B14..10 pasan por el conversor analógico digital del rojo (red DAC) Los bits B9..5 pasan por el conversor de verde (green DAC) Y, por último, los bits B4..0 pasan por el conversor de azul (blue DAC). • El pixelClkOut es el reloj de píxel y coincide con nuestro reloj de sistema (50Mhz aunque necesitamos refresco a 25Mhz) • El registro SYNC* deshabilita la información de sincronismo con RGB si se activa (con un 0 lógico). • OL[3..0] indica cúal es la paleta que se usa. Si se pone a 0 se utiliza la paleta por defecto. • Por último, tenemos el TRUE_COLOR* que deshabilita los modos de color real, necesario en esta parte del proyecto. Por otra parte, la RAMDAC presenta la entrada de datos P[7..0], que es la entrada de datos, conectada directamente al modulo inferior del banco de memoria, de donde se leen los bits menos significativos, como muestra la fig. 4. Cuando se envíen los más significativos, se tiene que conectar, mediante la FPGA, la parte alta de la RAM con la RAMDAC. Una vez configurada la RAMDAC, es necesario darle un pulso a WR* durante 50ns para que se quede guardada la configuración introducida. Para esto, se tiene que mantener activo WR* durante un mínimo de 3 ciclos si trabajamos a 50MHz (nosotros lo dejamos activo 4 ciclos para mayor seguridad). Además de todo esto, hay que deshabilitar el módulo de Ethernet de la placa XSV-800, porque algunas líneas que le unen a la FPGA son compartidas con líneas de configuración de la RAMDAC, en concreto con las líneas D7..0 y RS2..0. Así, mientras el modulo Ethernet esté activo, estará forzando esas líneas a algún valor, lo que impedirá que se configure correctamente la RAMDAC. Activando la señal TRSTE del Ethernet, este se deshabilita poniendo todas sus líneas a alta impedancia, con lo que se soluciona ese problema. 11 Reconfiguración Configuración de la FPGA desde la Flash RAM [DEHD02] A continuación se explica el funcionamiento del interfaz básico que automáticamente carga el contenido de la FLASH desde la dirección 0 hasta que la FPGA señale el fin de la configuración. Este interfaz es proporcionado por Xess en su página: • Se selecciona el modo SelectMap para configurar la Virtex desde la Flash RAM. En este modo, la FPGA acepta bytes de configuración en el flanco de subida del reloj de configuración. • Esto se da siempre que las señales chip-select y write-enable estén activas. • Se realiza una división de frecuencia de la señal de reloj. Esto es debido a que la Flash tiene un tiempo de acceso de 85ns, mientras que el oscilador de la placa puede alcanzar una frecuencia de 100Mhz. Por ello, se divide la frecuencia del oscilador entre 16 y se usa un reloj más lento para la configuración de la Virtex. • Tras el encendido de la placa es necesario esperar un tiempo antes de que ésta esté lista para que la configuración comience. Para ello, se implementa un contador hardware y se deshabilita la configuración hasta que éste haya finalizado. • A partir de este momento el circuito simplemente incrementa el contador de direcciones para leer el siguiente byte de la Flash RAM y lo envía a la Virtex FPGA. • Cuando la FPGA indica que la configuración ha finalizado, el CPLD cesa sus operaciones. En este momento se ponen todas las líneas usadas por el CPLD que están compartidas con la FPGA a alta impedancia para que puedan ser utilizadas por el nuevo circuito cargado en la Virtex. Proceso de arranque de la placa Tras el encendido, la configuración de la FPGA es automáticamente borrada. Un 0 lógico en la señal PROG* limpia la configuración y mantiene a la FPGA en el estado de “memoria de configuración borrada”; estado en el que se mantiene mientras PROG* continúe a 0 manteniendo el valor de la señal INIT* a 0 para indicar que la memoria está siendo limpiada. Cuando el valor de PROG* cambia, la FPGA continúa manteniendo el valor de INIT* a cero hasta que finalice el borrado de toda la memoria. El pulso 12 mínimo para PROG* es de 300ns, no habiendo un valor máximo. El valor de INIT* puede ser mantenido a cero desde el exterior de la placa para evitar su configuración. Una vez que el valor de INIT* pasa a ser 1, la configuración puede comenzar. Ya no son necesarias más esperas, pero no es necesario que la configuración comience nada más producirse la transición en el valor de INIT*. Durante la configuración se produce una comprobación del valor del CRC (palabra de paridad) del bitstream que se está cargando. Si el valor del CRC no es correcto, la señal INIT* pasa a valer 0, indicando así que se ha producido un error en el CRC. La secuencia de comienzo, si se produce esta situación, es abortada y la FPGA no pasa a estado activo. Para reconfigurar el circuito, el valor de la señal PROG* debe ser puesto a 0 lógico para borrar la configuración. Apagando y encendiendo la plca también conseguimos que la FPGA sea borrada para una nueva configuración. Cuando se ha verificado el valor del CRC, la FPGA entra en una secuencia de encendido, en la que se activa la señal DONE para indicar que la configuración se ha realizado correctamente y se activan las entradas/salidas. 13 3.2 Software El formato .PACA Este formato es realmente simple. Los archivos PACA son archivos de texto ASCII (de los generados por cualquier herramienta normal, como el notepad), donde cada entrada está en una línea distinta, acabada en un salto de línea. Hay dos posibles tipos de entrada, de dirección o de datos: - Una entrada de dirección es un número en hexadecimal terminado en dos puntos. - Una entrada de datos consiste en un número binario de 16 bits. El numero binario está formado por 1’s y 0’s ASCII (es decir, en el archivo, abierto con un editor de textos, se verán 1’s y 0’s y al generarlo se tienen que meter caracteres 1’s y 0’s, no enteros, ni bits, ni booleanos) Una entrada de dirección nos pone en esa posición de memoria, y a partir de ahí, las siguientes entradas de datos se guardan de forma consecutiva a partir de la posición de la que se partió. Por ejemplo, con el siguiente archivo PACA: 0: 0011001100110011 1100110011001100 1001011010010110 1111111100000000 0000000011111111 la palabra 0011001100110011 se almacenará en la posición 0 de memoria, la palabra 1100110011001100 en la 1, y así hasta la 0000000011111111 que se guardará en la dirección 4 de memoria. El formato .HEX Los archivos .HEX son archivos de texto en ASCII con registros en formato HEX, uno por fila. El formato HEX es el siguiente: Marca “:” Nº de Datos Desplazamiento Tipo de registro Datos Checksum 1 byte 1 byte 2 bytes 1 byte N bytes 1 byte Marca: Para indicar que lo que viene a continuación es un registro HEX, la marca es el símbolo “:” (dos puntos), y aparecerá por tanto al comienzo de cada fila. Nº de Datos: El numero de bytes de información de que constará el registro HEX. En nuestro caso es siempre 02 ya que en cada registro metemos la información de un píxel (16bits) 14 Desplazamiento: La dirección en 16 bits donde se comenzara a escribir el registro en la RAM. Tipo de registro: Puede tener dos valores fundamentales: “00” en la mayoría de los casos, indicando que el registro es de datos, y “01” para indicar que se trata del ultimo registro del archivo. Datos: Pares de dígitos en hexadecimal (tantos como se hayan indicado en el Nº de datos) con la información que se quiera guardar en la RAM. Checksum: Un byte, en hexadecimal, con los dos dígitos menos significativos del complemento a dos de la suma de los pares de dígitos de todos los campos anteriores. El formato .XES Los archivos .XES son muy parecidos a las .HEX. También son archivos de texto en ASCII con registros, y también uno por fila. El formato XESS se diferencia del HEX en la Marca y en que no tienen Checksum. Hay tres tipos de formato XESS: XESS-16, con 16 bits para direccionar la RAM y cuya Marca es “-“. Al tener 16 bits para direccionar, el campo de Desplazamiento tendrá 2 pares de dígitos en hexadecimal (8 bits cada par) XESS-24, con 24 bits y Marca “=”. XESS-32, con 32bits y Marca”+”. En el proyecto hemos usado XESS-24 ya que necesitamos direccionar direcciones de 19 bits. El formato XESS-24 es el siguiente: Marca “=” Nº de Datos Desplazamiento Tipo de registro Datos 1 byte 1 byte 3 bytes 1 byte N bytes Marca: En los registros XESS-24 la marca es el símbolo “=” (igual). Nº de Datos: El numero de bytes de información de que constará el registro. En nuestro caso se mantiene siempre a 02, ya que en cada registro metemos la información de un píxel (16bits) Desplazamiento: La dirección en 24 bits (3 pares de dígitos en hexadecimal) donde se comenzara a escribir el registro en la RAM. Tipo de registro: Puede tener dos valores fundamentales: “00” en la mayoría de los casos, indicando que el registro es de datos, y “01” para indicar que se trata del último registro del archivo. Datos: Pares de dígitos en hexadecimal (tantos como se hayan indicado en el Nº de datos) con la información que se quiera guardar en la RAM. 15 4. MÓDULOS INICIALES 4.1 Control de VGA Varios de los pasos realizados durante el proyecto, se llevaron a cabo en paralelo debido a que presentaban una cierta independencia de diseño, para posteriormente pasar a su debida integración. Al principio, y como toma de contacto con el VHDL sintetizable, se realizaron un par de pequeños diseños, como un contador, primero normal y luego ascendente descendente y un registro desplazamiento. Estos proyectos también fueron útiles para aprender a usar las distintas herramientas, como Xilinx y las utilidades GXSLoad1. Una vez familiarizados con el entorno y el lenguaje, creamos un módulo que, usando las facilidades gráficas de la tarjeta, nos permitía experimentar con ellas. Cálculo de valores de las constantes VGA La resolución final de nuestro sistema gráfico dependerá tanto del valor nominal de la pantalla (en nuestro caso 640x480) como del controlador que genera las señales de intensidad. Por tanto, lo primero que tenemos que decidir es la frecuencia de reloj a la que trabajará nuestro controlador. Para ello, es necesario saber que el estándar VGA presenta una frecuencia de reloj de muestreo del monitor de 25.175Mhz, porque la combinación de esta frecuencia con la del reloj de nuestro sistema determinará la resolución gráfica final. Teniendo en cuenta todo lo dicho, nos decidimos por una frecuencia de 50Mhz que, aunque podría permitirnos teóricamente una resolución de 1256x480, finalmente nos permitirá una resolución de 640x480, debido a que es esta la resolución del estándar VGA. Los cálculos los hemos realizado con una frecuencia de reloj del controlador de 25Mhz y hemos obtenido un tiempo de ciclo de 1/25=0,04 microsegundos. Los cálculos los realizamos con una frecuencia de 25MHz en vez de 50MHz (que es el valor real del reloj de nuestra sistema, nuestra frecuencia de trabajo) teniendo en cuenta que en un futuro usaremos la RAMDAC, y esta, cuando funciona a 15 bits de color (TARGA 5:5:5), necesita dos ciclos de acceso para cada píxel, uno para enviarle la parte alta del byte, y otro para enviarle la parte baja. Así pues, cuando trabajemos a 50MHz enviaremos píxeles a 25Mhz, porque tardaremos dos ciclos de reloj en enviar un píxel. 1 ver Apéndices A y B 16 Una vez que hemos decidido la frecuencia de nuestro controlador, transformamos los valores a ciclos (o columnas) y a líneas. Estos serán los valores que tendrá nuestro sistema grafico. Datos en MICRO sg. 1 ciclo = 0.04 microsg. Tiempo Tipo / 0.04 # ciclos Barrido completo de una fila 31,77 Exacto 794,25 794 Blanking horizontal 3,77 Exacto 94,25 94 Barrido visible de una fila 25,17 Máximo 629,25 629 Porche delantero 0,94 Mínimo 23,5 24 Porche trasero 1,89 Mínimo 47,25 48 TABLA 1 - CÁLCULO DE VALORES CONSTANTES DE LOS CICLOS O COLUMNAS DE VGA En la primera columna de la tabla 1 aparecen los tiempos del estándar VGA (que son fijos para cualquier frecuencia de controlador que queramos tener): • Barrido completo de una fila : indica los microsegundos que transcurren en el muestreo de todos los píxeles de una fila que, dividiéndolo por 0,04 microsegundos que tiene nuestro ciclo, se obtienen 794 ciclos (o columnas) por fila. • Barrido visible de una fila : No obstante, el valor anterior (barrido completo de una fila) no es el valor real que se mostrará por pantalla, porque el estándar presenta por pantalla durante un máximo de 25.17 microsegundos, lo que nos otorga la posibilidad máxima de 629 columnas de muestreo (se redondea hacia el valor entero más pequeño). • Blanking horizontal : tenemos 3.77 microsegundos como tiempo de blanking (que resultan ser 94 columnas). Este tiempo es necesario debido a que mientras se realiza la sincronización horizontal (hSync) y el haz baja de línea en diagonal, no se debe pintar la pantalla. Estos 94 ciclos son el tiempo que tarda el haz de electrones en colocarse en el comienzo de la siguiente fila. • Porche delantero y trasero : no son más que los tiempos de porche delantero y porche trasero del tiempo de sincronización. Son necesarios para evitar que se pinte antes de que el haz se haya colocado y aportan un seguro para una correcta sincronización. Las columnas o ciclos resultantes son 24 y 48 respectivamente. La suma de los cuatro últimos valores nos da como resultado un valor de 795, que si tenemos en cuenta los redondeos a entero imprescindibles, nos deja el valor 794 ya calculado, correspondiente al número de columnas por fila. Vamos ahora con las filas: 17 Datos en MILI sg. 1 línea = 794 * 0.04 = 31.76 microsg. tiempo Tipo / 0.03176 # líneas Barrido completo de la pantalla 16,784 Exacto 528,46 528 Blanking vertical 0,064 Exacto 2,02 2 Barrido visible de la pantalla 15,25 Máximo 480,16 480 Porche delantero 0,45 Mínimo 14,17 14 Porche trasero 1,02 Mínimo 32,12 32 TABLA 2 - CÁLCULO DE VALORES CONSTANTES DE FILAS DE VGA Módulo de visualización de patrón fijo fig. 5 - Módulo de visualización de patrón fijo Una vez realizados los cálculos necesarios, sólo queda implementar un módulo que, utilizando estos datos, genere directamente las señales de vídeo en la FPGA. Este módulo muestra un patrón fijo, consistente en columnas verticales de ancho fijo y distintos colores. En la fig. 5 se muestra el diseño del módulo, y su código vhdl puede encontrarse en el Apéndice C, con el nombre contador.vhd. Como se puede observar, se utiliza dos contadores de 10 bits para la generación de las columnas y filas. hSync estará activo cuando el número de columnas esté comprendido entre 653 (629 columnas a mostrar + 24 columnas del porche delantero de la 18 sincronización) y 747 (que es la suma de las columnas a mostrar, el porche delantero más el número de columnas de sincronización horizontal ). vSync esta activo cuando el número de filas esté comprendido entre 494 (480 filas a mostrar + 14 filas del porche delantero de la sincronización) y 496 (que es el valor total máximo de filas). Además, se cuenta con una señal de enable que aumenta la cuenta de filas cada vez que el contador de columnas termina. Por otro lado, los valores que se muestran en configuración directa a través de las líneas de datos p0..p5, se obtienen de los bits 8 a 3 del contador de filas (se cogen estos bits para que el tamaño de las líneas de color mostradas por pantalla tenga un mayor grosor, teniendo en cuenta que se corresponden con los 6 bits de RGB utilizados en este punto). No obstante, sólo se utilizan cuando nos encontramos en la región válida de muestreo (que no es otra que cuando el número de columnas no supera las 628 y cuando las filas no alcanzan las 480). Posteriormente se hizo una pequeña mejora consistente en mostrar el patrón de líneas vertical u horizontal dependiendo de un switch. Esto no supone más que tomar los bits 8 a 3 del contador de filas o del de columnas, en vez de siempre el de las filas, dependiendo del valor de dicho switch. Este switch viene representado en el código mediante la señal FilCol. Controlador de monitor con refresco de memoria Una vez hecho esto, se modificó el módulo de manera significativa. El objetivo de esta modificación era que en el primer barrido de la pantalla se guardara en la memoria el patrón generado a la vez que se mostrara por pantalla, para que en las siguientes pasadas se leyera de la memoria y se enviara a la salida de vídeo para mostrarse. Esto implica tener un minicontrolador de memoria y tener en cuenta si es la primera vez que se genera el patrón o si hay que leerlo de memoria. En cuanto a leerlo de memoria y mandarlo a la salida de vídeo, esto no es ningún problema, ya que en las placas XSV, la parte baja de los datos del banco derecho de la RAM2 está directamente conectada a la entrada de la RAMDAC y, por tanto, a la salida de vídeo (si los jumpers están en configuración directa). De este modo, con activar la señal oe* de la RAM y pasarle la dirección correcta, la RAM saca por sus 6 líneas menos significativas los valores de intensidad RGB que se han guardado antes y que son los que interesan. Como hemos dicho antes, debido a que directamente estos 6 bits están conectados a la RAMDAC y mediante los jumpers (si están en modo directo) a la salida de vídeo, no se necesita más lógica para generar la señal de vídeo. 2 ver sección 3.1, La Ram, pg. 8 19 Con lo único que hay que tener cuidado es con no forzar esas líneas de datos a ningún valor desde la FPGA y dejarlas flotando. Este es el caso, porque se evita la RAMDAC y sólo se usan 6 bits de datos; cuando se empiecen a usar 15 bits de color (en formato TARGA, 5:5:5) a través de la RAMDAC, la cosa se complica. En cualquier caso, y de momento, las únicas modificaciones que hicieron falta fueron añadir ese minicontrolador y también un multiplexor a la salida de los datos de intensidad de RGB para elegir si se mostraban desde la generación del patrón (en la primera pasada) o si venía de memoria. Para generar la dirección en la que guardar o de la que leer los datos, se concatenan la parte baja (9 bits) de la señal que lleva la cuenta de las filas con la señal que lleva la cuenta de las columnas. En cualquier caso, esta concatenación sólo se realiza si se está dentro de la zona de visualización (columnas <= 628 y filas <= 479). Si se está en esa zona se genera un cero. Con esto, el diseño es el mismo que en la fig. 5, pero con los añadidos de la fig. 6. El código de este diseño, con el nombre rayash.vhd se encuentra en el apéndice C. fig. 6 - Controlador de VGA usando memoria 20 En la figura 6 se pueden observar los siguientes módulos: • Por un lado tenemos un modulo concatenación, encargado de formar la dirección de la RAM en la que se escribe o lee dependiendo de lo que decida el minicontrolador. Esta dirección de 19 bits esta formada por los 10 bits del contador de columnas (que son los 10 bits menos significativos de la dirección) y los 9 bits menos significativos del contador de filas. Como ya se ha dicho, esta concatenación solo se realiza cuando nos hallemos en la zona real de muestreo. • Por otro lado, los seis bits menos significativos del contador de columnas se cargan en los seis bits menos significativos de la RAM, que son los que están conectados directamente con la RAMDAC, pero sólo la primera vez que se pinta la pantalla, puesto que después se lee de la RAM. De conseguir que en el primer barrido se escriba en la RAM y en los demás se lea de ella, se encargan oe* y we*. Además, mediante un triestado se impide que se envíen valores directamente a la salida una vez que se ha hecho la primera pasada y, excepto en esa primera pasada, deja las líneas que unen directamente la RAM y la RAMDAC flotando para que la salida de vídeo provenga de la memoria. • El minicontrolador es muy sencillo y consta de un biestable que, inicialmente, almacena un cero. Es en este caso cuando se tiene que escribir en la RAM. Para conseguir que en sucesivas pintadas se lea, se realimenta el biestable con una OR cuyas entradas serán el último valor de dicho biestable y una señal de fin de pantalla que vale 1 cuando ambos contadores (el de columnas y el de filas) lleguen a su fin. A partir de este instante siempre se lee de la RAM, y solamente se vuelve a escribir si se pulsa el reset del sistema, en cuyo caso se vuelve a almacenar un 0 en el biestable. • Las señales que se utilizan para la sincronización de las lecturas y escrituras son we*, oe* y ce*. Todas estas señales se activan cuando les entra un 0 ya que están implementadas con lógica inversa : 1. we* recibe el valor actual del biestable. Cuando éste vale 0 la señal de we* (señal de escritura o write enable) se activa y, en consecuencia, se habilita la RAM para su escritura. 2. En los siguientes pintados de la pantalla, se activa la señal oe* (señal de lectura de la RAM o read enable), ya que recibe el valor negado de lo almacenado en el biestable. 3. La señal de ce* (o chip enable) simplemente habilita la RAM para su funcionamiento. 21 Carga de datos en la RAM A partir de este momento, cuando ya se ha conseguido usar la RAM sin problemas y se lee un patrón previamente guardado, se empieza a investigar la bajada de imágenes a la RAM por el puerto paralelo. 4.2 Carga de imagen en memoria Para el proyecto se necesita bajar una imagen del ordenador a los bancos de la RAM, por el puerto paralelo. En principio, esto iba a realizarse con la herramienta GXSLoad. Por tanto era necesario crear una herramienta que trasformase un archivo imagen en otro con un formato HEX, XESS o PACA. Se decidió trabajar con imágenes en formato .BMP, ya que el formato es el más simple (comparándolo con JPEG y GIF, entre otros) Así mismo, se decidió usar archivos en formato HEX para trabajar con la placa, porque en principio parecía el mas simple. Para crear la aplicación necesaria usamos C++, puesto que ya lo estábamos usando también en Informática Gráfica y podríamos así reutilizar clases, métodos y conocimiento. De esta manera creamos la aplicación BMP2HEX, que trasforma una imagen en formato BMP a otra en formato HEX para luego bajarla a la placa. Una vez obtenidos mediante esta aplicación archivos en formato HEX, fuimos incapaces de bajarlos a la placa mediante la herramienta GXSLoad. Pero también nos dimos cuenta de que el formato HEX era insuficiente para lo que nosotros necesitábamos, puesto que sólo te deja manejar direcciones de memoria de hasta 16 bits (debido al campo desplazamiento3) y nosotros necesitábamos direcciones de hasta 19 bits, así que decidimos probar con otro formato, el XESS-24 De esta manera creamos la aplicación BMP2XESS. Esta aplicación era una evolución de la anterior, simplemente se le añadieron los métodos necesarios para trabajar con archivos con el formato XESS-24. Con los archivos en formato XESS-24 tampoco fuimos capaces de bajar imágenes a la placa mediante GXSLoad. Así probamos con el formato paca, creado hace unos años en otro proyecto con la misma placa. Creamos la aplicación BMP2PACA, evolución también de las anteriores, añadiendo los métodos para trasformar los archivos al formato paca. 3 Ver sección 3.2, El formato .HEX, pg. 14 22 Para bajar los archivos .paca a la placa no usamos la aplicación GXSLoad sino que usamos el LoadMem, otra aplicación que también fue desarrollada en el proyecto anterior. Esta vez, usando el formato .paca y el LoadMem sí que se consiguió bajar con éxito las imágenes a la memoria. GXSLoad Xess (el fabricante de las placas XSV), proporciona una herramienta, el GXSLoad4, para bajar configuraciones a la FPGA y al CPLD y subir y bajar datos a la RAM y a la FlashRam, en formato HEX, EXO y XESS. En un principio, parecía que esta herramienta iba a permitir bajar datos a la RAM sin ningún problema, por lo que se desarrolló un conversor software, en C++, para pasar imágenes de formato BMP a formato HEX5. Este conversor, BMP2PACA.exe se trata con detalle en la sección de Software. No obstante, cabe destacar que este formato no será el que finalmente se utilizará, sino que el formato final pasará a ser .PACA, de ahí que el nombre de la aplicación tenga el sufijo PACA en vez de Hex. Sin embargo, la herramienta GXSLoad daba problemas. No permitía bajar ni subir más de unos cuantos bytes (entre 0 y 2 Kbytes, aunque no era una cantidad fija, variaba de ejecución en ejecución) antes de dar error. Se pensó en un primer momento que podía ser por problemas de la configuración del puerto paralelo del ordenador, pero se probaron todas las posibles configuraciones y el problema seguía. Además, los problemas sólo aparecían cuando se intentaba subir o bajar a la RAM. No había ningún problema con el puerto si se trataba de bajar datos a la FPGA o la FlashRam. A continuación, se barajó la posibilidad de un fallo en el software de XESS o en el módulo ram800.bit (módulo que se carga en la FPGA para conectar el puerto paralelo con la RAM, pues la RAM no tiene conexión directa con el puerto paralelo). Después de estudiarlos un tiempo, se descartó que el fallo pudiera estar ahí, puesto que son herramientas ampliamente usadas y no se había informado de ningún bug en ellas. A continuación se descargó e intentó trabajar con distintos programas libres, creados por distintas universidades para bajar y subir datos de la RAM. Estos programas eran el PcToRam de la Universidad de Queensland y el SPP.exe de la universidad de Sevilla. El de la Universidad de Queensland no llegó a funcionar en ningún momento, y el de la Universidad de Sevilla estaba creado para placas XS40 y habría que modificarlo. No se llegó a modificar este software pero sí a intentar reparar el de Queensland. Este intento de reparación significó bastantes problemas (después de una cantidad considerable de trabajo ni siquiera se llegó a poder compilar el código fuente). En ese momento se decidió usar un módulo VHDL y una herramienta Software para bajar imágenes a la RAM de una placa XSV-800 desarrollada en años anteriores en otro proyecto de Sistemas Informáticos [BAPP01]. 4 Ver apéndice B 5 La especificación del formato HEX se encuentra disponible en la página http://www.intel.com/design/zapcode/Intel_HEX_32_Format.doc, o en la sección Software Archivos .HEX del presente documento 23 5. VISUALIZACIÓN Y FILTRADO DE IMAGEN EN RAM 5.1 Carga de una imagen en RAM El software utilizado finalmente para comunicar el PC con la RAM de la placa es la aplicación loadMem.exe, para Windows, que recibe un fichero en formato PACA (formato propietario, creado por los autores del proyecto [BAPP01] y que nosotros hemos heredado) y lo guarda en los bancos de la RAM6. Conversión y mostrado La forma de realizar la conversión, abreviadamente, es la siguiente: Nuestro programa BMP2PACA transforma una imagen con formato bmp en un pixmap o array de píxeles. Dicho pixmap contiene en cada celda el valor RGB correspondiente a dicho píxel y lo que se hace para la obtención del formato paca deseado es recorrer cada una de las posiciones del pixmap, obteniendo su intensidad de color y traduciendo dicha intensidad a un valor de 15 bits. Cada uno de estos valores contendrá 5 bits para cada una de las intensidades roja, verde y azul del formato RGB de color. El nuevo fichero .paca esta formado por todos estos valores de píxeles uno a continuación del otro, en serie. Primero el píxel de superior izquierdo, luego el de su derecha y seguimos así por su fila, y luego las siguientes filas de arriba abajo. Este archivo .paca tiene todos los píxeles posibles que pueda albergar la resolución de pantalla utilizada. Por tanto, todos los píxeles que no entren en la región válida (ya comentada antes) de nuestra práctica valdrán 0. Es por este motivo que todos los ficheros .paca tienen el mismo volúmen de datos a pesar de que no se vayan a necesitar muchos de ellos. Este tamaño es del orden de 9.2Mb. Una vez probado que funcionaba el proyecto original del que se iba a extraer el cargador (y por tanto que funcionaba el módulo cargador mismo), se extrajo de él el módulo de carga de la RAM, que nosotros llamamos cargador, y que es, en realidad, una arquitectura, cuya jerarquía se muestra a en la fig. 8. 6 en qué banco se guarden los datos, en el izquierdo o en el derecho, dependerá de la asignación de pines que se realice en el archivo ucf del modulo cargador que se baje a la placa. Este módulo cargador (que se carga en la FPGA para comunicar la placa con el PC) se explicará más adelante. 24 fig. 8 - Arquitectura del módulo Cargador Dentro de esta arquitectura, CrtlParalelo se encarga de controlar el puerto paralelo, comunicarse con la utilidad LoadMem.exe a través de este puerto (esta aplicación espera confirmaciones desde el puerto paralelo de que los datos han llegado) y leer los datos que llegan a través de él. Luego, escribidor se encarga de ir almacenando estos datos en la memoria RAM. Su código se encuentra disponible en el apéndice C. A partir de aquí, hay que modificar la herramienta BMP2PACA.exe para que convierta los archivos de imagen del formato BMP (sólo los de 24 bits de color) al nuevo formato PACA. A su vez, se crea un visualizador software de archivos PACA, llamado dibuPaca.exe. Ambos se encuentran comentados en la sección 7.1 y 7.2 de Software. Una vez que se consiguió bajar correctamente las imágenes a la RAM, era necesario mostrarlas, pero ya no con 6 bits de color, de forma directa, sino pasando por la RAMDAC. Para esto, primero hace falta programar la RAMDAC para trabajar en formato TARGA 5:5:5 single edge. Pasamos a explicar lo que esto significa. 5.2 Visualización de una imagen en la RAM El siguiente paso es modificar el módulo Controlador de monitor con refresco de memoria7 para que no guarde nada en la Ram, en ningún momento. Ahora, la información de color es de 16 bits, y la RAMDAC sólo tiene 8 líneas de entrada de datos, así pues, hay que leer de memoria una palabra completa y pasársela a la RAMDAC byte a byte en dos ciclos distintos. Además de esto, se tiene que crear un pequeño subsistema de configuración de la RAMDAC y algo de lógica de control para configurar primero y enviar la imagen a la RAMDAC después. El diseño final del módulo se encuentra en la fig. 9. 7 sección 4.1, pg. 19 25 Como puede apreciarse, la figura 9 consta de dos módulos, comentados a continuación: 1. Subsistema de configuración de la RAMDAC: • Empezando por la esquina inferior izquierda, vemos que se carga en Pout (entrada de datos de la RAMDAC) el byte más significativo del valor de píxel que, en ese momento, se esté leyendo de memoria. Esto se debe a que, al trabajar con datos de 16 bits, en un ciclo es necesario que leer el byte menos significativo y guardar en un registro el byte más significativo para, en el siguiente ciclo, enviar éste a la RAMDAC. Para realizar esto, se utiliza un triestado que depende de oe*. Cuando esta señal vale 0, el triestado se pone en alta impedancia y no permite la carga de datosIn, sino que se lee el byte menos significativo mediante la conexión directa entre la RAM y la RAMDAC. Cuando, por el contrario, oe* vale 1, se carga en Pout el valor de DatosInGuardados, es decir, el registro donde se guardó la parte más significativa del byte. • En los registros RD, Dout y RS se cargan los valores de inicialización8. • Para el cálculo de la señal ini (señal que indica que la configuración de la RAMDAC ya ha sido almacenada) se utiliza un contador de tres bits, es decir, módulo 8, ya que, como ya se comentó, se necesita dar un pulso de tres ciclos a WR*, pero como nosotros lo hacemos durante 4 ciclos, utilizamos el bit más significativo del contador. Cuando éste se pone a 1 por primera vez, ini pasa a valer 1, y en consecuencia también WR, puesto que están conectados. A partir de ese momento, los valores de ini y WR no cambian, debido a la realimentación del biestable con la puerta OR, salvo que pulsemos el reset del sistema, en cuyo caso volvería a iniciarse el proceso. • Lo único que queda por ver de este módulo es la señal de blankOut, que valdrá 0 mientras la configuración no se haya realizado (es decir, mientras ini valga 0) y en caso contrario, el valor de BlankOut dependerá de si estamos o no en la región válida de muestro(esto se realiza con la AND de la figura). 2. Subsistema de mostrado del contenido de la memoria: • Para empezar, tenemos, como en módulos anteriores, un par de contadores para el recuento de las columnas y las filas, pero en esta ocasión el contador de columnas llega hasta el 1578 (el doble que antes). Este aumento se debe a que necesitamos el bit menos significativo del contador de columnas para cargarlo en oe*. Este bit cambia cada ciclo y precisamente es lo que queremos para oe* 8 sección 3.1, Configuración de la RAMDAC en formato TARGA y single-edge-clock, pg. 10 26 puesto que debemos poner a alta impedancia las líneas que unen el módulo inferior de la RAM derecha con la RAMDAC9 para de este modo enviar el primer byte del píxel en un ciclo y conectar el modulo de arriba (y por tanto el segundo byte del píxel) con la RAMDAC en el siguiente ciclo. Así conseguimos mandar el píxel entero en dos ciclos. Además, oe* sólo cogerá este valor cuando ini ya valga 1 (mediante el multiplexor), porque hasta ese momento no necesitamos leer nada, puesto que se esta configurando la RAMDAC. Además, para los cálculos de hSync, regiones válidas, direcciones y demás, solo cogemos los 10 bits más significativos de dicho contador, y el último lo despreciamos. • Por otro lado, a ce* le cargamos un 0 para activarlo, para que esté activo siempre, mientras que en we* ponemos un 1 porque nunca vamos a escribir en memoria. • El cálculo de las señales de sincronización horizontal y vertical (hSync y vSync respectivamente) se realiza como en módulos anteriores, salvo la diferencia de que ahora debemos retardar dichas señales 8 ciclos ya que los píxeles sufren una demora (delay) al atravesar la DAC. • Por último, está el cálculo de las direcciones de las que se va a leer. Esto se realiza, como siempre, mediante la concatenación del contador de columnas y el de filas. El código de este módulo (BmpRamda.vhd) se puede encontrar en el apéndice C. 9 Ver sección La RAM 27 fig. 9 - Subsistema de mostrado una imagen almacenada en memoria usando la RAMDAC En el diseño y depuración de este módulo, nos encontramos con una serie de problemas, de distinto calibre : 28 - Problemas de límites de la imagen : La imagen salía torcida y boca abajo por pantalla, como en la fig. 7 .Este era un problema en el software de conversión al formato PACA, que se solucionó cambiando el sentido de un bucle (en vez de recorrer la imagen de abajo a arriba generando en ese sentido el archivo PACA, pasamos a recorrerlo de arriba a abajo), y el valor de una comparación (una vez alcanzado el último píxel de cada línea, se rellenaba con negros hasta alcanzar 102410, cuando en realidad había que aumentarlo en 1023 posiciones, por que si no se añade un píxel extra) - Problemas con la configuración de la RAMDAC : Como se ha dicho anteriormente, el módulo Ethernet puede interferir con la configuración de la RAMDAC, cosa que efectivamente pasaba al principio. La imagen salía con colores distintos cada vez que se reiniciaba la placa, y esos colores eran aleatorios. De eso concluimos que la configuración no funcionaba y la RAMDAC estaba funcionando con la tabla de colores. Una vez que nos dimos cuenta del problema con el Ethernet y se inhabilitó el módulo, la configuración, sin ningún cambio más, funcionaba a la perfección y los colores eran ya correctos. - Para decidir cuándo leíamos un byte del píxel de la RAM (y se mandaba automáticamente la otra parte11) y cuándo mandábamos a la RAMDAC ese byte leído inicialmente, usábamos el reloj, dividiendo su frecuencia por la mitad, de modo que en un ciclo leíamos y en otro mandábamos. Pero esto nos causaba problemas de sincronización y la imagen no se mostraba bien. Esto era porque no controlábamos que el primer byte, que iba directamente de la parte baja de la RAM a la RAMDAC, y el byte que enviábamos de la parte alta a continuación fueran del mismo píxel. Lo que efectivamente pasaba es que mandaba la parte alta de un byte y la baja del otro. Esto lo solucionamos haciendo que el que leyera, o mandara, dependiera del último bit de la señal de columnas, (que ahora serían 11 bits, de los cuales 10 servirían para generar la dirección y uno para estar dos ciclos en cada píxel, uno mandando la parte baja y otro mandando la parte alta, que ahora nos asegurábamos de que eran del mismo píxel, porque la dirección (generada por los 10 bits siguientes, que no cambiaban en estos dos ciclos) era la misma durante este proceso). Una vez solucionados estos problemas, el diseño queda como se ha explicado más arriba (fig. 9), y puede encontrarse su código en el apéndice C. 10 este valor sale de la forma en que generamos las direcciones en el módulo. Al concatenar las direcciones, cuando pasamos de la última útil de una línea, la siguiente que generamos es aumentando en uno el valor del contador de filas, que al ser el undécimo bit es 210=1024 posiciones delante. 11 Ver sección 4.1, Controlador de monitor con refresco de memoria, pg. 19 29 5.3 Filtros de imágenes Introducción Las redes combinacionales definidas para los filtros son de tipo Moore, ya que las salidas dependen sólo del estado en el que se encuentre en un momento determinado. En el desarrollo de cada filtro nos encontramos diferentes problemas que hubo que solucionar sobre la marcha. Estos problemas se comentarán en el apartado correspondiente a cada filtro, ya que, aunque algunos son comunes, muchos de ellos son característicos de cada filtro. En todos los filtros operamos con el criterio asumido para la RAMDAC, Targa 5:5:5, que marca que cada componente, RGB, de un píxel tendrá 5 bits. Por lo que habrá 25 tonos de rojo, de verde y azul, por tanto se conseguirá 32767 colores. Una utilidad que nos ha servido de mucha ayuda en el desarrollo de estos filtros es la definición de colores personalizados del Paint de Windows. Sobre todo en el caso del filtro Blanco y Negro, en el que pudimos ver el resultado de la formula que aplicaremos, y en el filtro 8 colores. Todos los filtros siguen un plan de ejecución general, que incluye el uso de dos botones externos, asignados a las señales “rst” y “Accion”: Estado 0 Estado 1 accion = 1 accion = 0 Estados… rst = 0 fig. 10 - Diagrama general de estados Los filtros desarrollados finalmente pueden agruparse en dos grupos, según dos criterios diferentes: • Según los bancos de memoria utilizados. Por una parte están los filtros que hacen uso de sólo el banco derecho de la memoria RAM, filtros de 8 colores, Blanco y Negro, y Binarizar; Y los filtros que 30 utilizan tanto el banco derecho como el izquierdo de la memoria RAM, siendo estos los filtros de Negativizado, Suavizado y Bordes. • Según el modo de operar con los píxeles de la imagen. Los filtros 8 colores, Blanco y Negro, Negativizado y Binarizar operan píxel a píxel, mientras que los filtros de Suavizado y Bordes realizan una convolución de la matriz 3x3 que rodea a cada píxel de la imagen. Teóricamente, el uso de los dos bancos de la memoria es necesario cuando se va a operar con matrices de píxeles en lugar de píxel a píxel. Para poder almacenar el valor de la convolución de las matrices mientras se opera con el resto de los píxeles de la imagen. Este criterio no se cumple en el filtro de Negativizado que, como veremos, utiliza los dos bancos de memoria aunque opera píxel a píxel. Para estudiar cada filtro por separado utilizamos el criterio de operación con los píxeles. Filtros Píxel a Píxel: Filtro de Negativizado: Funcionamiento: El objetivo de este filtro es convertir la imagen en su negativo, es decir, en la imagen resultante al sustituir cada píxel por el valor que se obtiene al restar al valor máximo de color que se puede obtener, con ese número de bits, el valor de dicho píxel. Este filtro lee el valor de cada píxel del banco derecho de la memoria RAM y carga este valor negado en el banco izquierdo. Dado que el módulo bmpramda lee siempre del banco derecho de la memoria, el último estado de este filtro se encargará de volcar el contenido del banco izquierdo al derecho. Este filtro utiliza los dos bancos de memoria, aunque teóricamente no sería necesario, porque fue el primer filtro desarrollado con este método. Como se comenta en el apartado de los problemas encontrados, se intentó modificar este filtro para utilizar sólo un banco de memoria, pero no se consiguió. 31 Para desarrollar este código necesitamos 4 estados: o Estado 00: Estado de espera. En este estado se entra al pulsar reset. Ambos bancos de memoria están apagados porque no se utilizan. De este estado se pasa al estado 01 cuando se pulsa botón asociado a la señal “accion”. o Estado 01: Leemos una posición del banco derecho de la memoria, negamos su valor y lo cargamos en la misma dirección de memoria del banco izquierdo. Por este estado pasaremos una vez por posición de memoria, es decir, 524286 veces. Este valor es el resultado del número de columnas (1024) por el número de filas (512). o Estado 10: Una vez recorrido todo el banco derecho se pasa al estado 10, en el cual se recorre el banco izquierdo de la memoria y se carga cada valor en la misma posición de memoria del banco derecho. o Estado 11: Estado final. Se apagan las memorias, que ya no se van a utilizar y se activa la señal “yasta”para indicar que el filtro ha terminado. Diagrama de estados: 00 Espera 01 Leer B. Dch 10 Escribir B. Dch 11 Fin accion = 1 finMem = 1 finMem = 1 accion = 0 finMem = 0 rst = 0 finMem = 0 fig. 11 - Diagrama de estados del Filtro Negativizar Código: Ver anexo C6.4 Problemas encontrados: 32 El principal problema encontrado en las pruebas de este código fue que la imagen resultante era mostrada unas posiciones más a la derecha que la imagen original. Tras realizar numerosas pruebas determinamos que el problema estaba en el incremento de la dirección de memoria. Aparentemente, incrementábamos este valor más rápidamente y más veces de las necesarias. Para solucionarlo modificamos el código, de forma que el incremento de la dirección de memoria se realizase sólo en el momento del cambio de estado. Posteriormente al desarrollo de este filtro intentamos modificarlo para que utilizase solamente el banco derecho de la memoria, pero no conseguimos que funcionase correctamente con la nueva arquitectura, ya que desplazaba algunas posiciones a la derecha. Filtro de Blanco y Negro: Funcionamiento: El objetivo de este filtro es visualizar la imagen en blanco y negro, con toda su escala de grises. Para que una imagen que use los tres colores RGB se vea solo en escala de grises hay que poner el mismo valor en los tres colores y para ello este filtro se basa en una fórmula de paso Blanco y Negro, facilitado en la asignatura optativa Robótica: Color = 0,299 × Rojo + 0,587 × Verde + 0,114 × Azul Dicha fórmula la redondeamos a los siguientes valores: Color = 0,25 * Rojo + 0,5 * Verde + 0,125 * Azul Con este redondeo se pierde información, por lo que la ejecución sucesiva de este filtro sobre la misma imagen hace que se pierdan niveles de gris. Estas multiplicaciones son equivalentes a dividir por 4, por 2 y por 8, respectivamente. Por tanto, la operación final sería realmente una operación de desplazamiento del número de posiciones necesarias en cada componente: * 0’5 (x/2) 1000000000 Desplazar una posición a la derecha el componente R del píxel. * 0’25 (x/4) 0100000000 Desplazar dos posiciones a la derecha el componente G del píxel. * 0’125 (x/8) 0010000000 Desplazar tres posiciones a la derecha el componente B del píxel. 33 En esta ocasión utilizamos 5 estados: o Estado 000: Estado de espera. En este estado se entra al pulsar reset. Ambos bancos de memoria están apagados porque no se utilizan. De este estado se pasa al estado 001 cuando se pulsa el botón asociado a la señal “accion”. o Estado 001: Se lee la información del banco derecho de la memoria y se carga cada componente del píxel leído en la señal auxiliar correspondiente. Los bits se cargan ya desplazadas las posiciones necesarias. Desde este estado se pasa ineludiblemente al estado 010. o Estado 010: Estado operación. En este estado se suman los valores de los componentes auxiliares del estado anterior. De esta forma se termina de ejecutar la fórmula anterior y obtenemos los 5 bits con el color definitivo del píxel. Estos 5 bits son guardados en la señal RGBdef. De este estado se pasa al estado 011, para escribir el píxel, ya transformado, en su posición de memoria correspondiente. o Estado 011: Se carga en el banco derecho de la memoria el valor de la señal RGBdef. En los tres componentes del píxel se repite el mismo valor contenido en RGBdef. Este píxel es cargado en la misma posición de la que fue leído en el estado 001. Este estado y los dos anteriores se ejecutarán tantas veces como posiciones de memoria hay, es decir, 524286 o Estado 100: Estado final. Se desactiva el banco de la memoria utilizada, el derecho. Y se activa la señal “yasta”, para indicar que el filtro ha finalizado. Diagrama de estados: 34 000 Espera 001 Leer B. Dch 010 Operar 011 Escribir B. Dch 100 Fin accion = 1 accion = 0 finMem = 1 finMem = 0 rst = 0 fig. 12 - Diagrama de estados del Filtro Blanco y Negro Código: Ver anexo C6.3 Problemas encontrados: Los principales problemas que encontramos fueron derivados de la operación de transformación a Blanco y Negro. En un principio operábamos con los 15 bits en vez de con cada componente por separado. Esto provocaba que la imagen resultante estuviese totalmente distorsionada. Una vez corregido este error, el siguiente problema encontrado fue que el filtro funcionaba de forma aleatoria, es decir, filtraba la imagen unas veces sí y otras no. Para solucionar este problema incluimos el módulo eliminador de rebotes. Para evitar los rebotes de los botones de reset y de “accion”. Este módulo lo tomamos de la página de J. M. Mendías12 y, dado que solucionó el problema en este filtro, lo incluimos por defecto en el resto de ellos. Filtro de Binarización: Funcionamiento: Este filtro parte del filtro de Blanco y Negro para limitar los colores a dos, negro (0) y blanco (32767). Para ello, una vez transformada la imagen a blanco y negro se compara el nivel de gris resultante en cada píxel con un umbral. Si el valor es mayor que dicho umbral se sobrescribe con el valor 32767, blanco, en caso contrario, con el valor 0, negro. Esta comparación se realiza fuera del proceso MaquinaDeEstadosFiltro. 35 12 http://www.dacya.ucm.es/mendias/143/disenos/debouncer.vhd Para desarrollar este código utilizamos también cinco estados y sólo un banco de memoria, el derecho: o Estado 000: Estado de espera. En este estado se entra al pulsar reset. No utilizamos el banco derecho de la memoria todavía, por lo que está apagado. De este estado se pasa al estado 001 cuando se pulsa botón asociado a la señal “accion”. o Estado 001: Se lee la información del banco derecho de la memoria y se carga cada componente del píxel leído en la señal auxiliar correspondiente. Los bits se cargan ya desplazadas las posiciones necesarias. Desde este estado se pasa ineludiblemente al estado 010. o Estado 010: Estado operación. En este estado se suman los valores de los componentes auxiliares del estado anterior. De esta forma se completa la transformación de la imagen a Blanco y negro, como parte del proceso de binarización. De este estado se pasa al número 011. o Estado 011: Se carga en el banco derecho de la memoria el valor de la señal RGBdef. Dado que la comparación con el umbral, y la sustitución de valor correspondiente, se realiza fuera del proceso, en este momento esta señal ya tendrá el valor adecuado. En los tres componentes del píxel se repite el mismo valor contenido en RGBdef. Este píxel es cargado en la misma posición de la que fue leído en el estado 001. Este estado y los dos anteriores se ejecutarán tantas veces como posiciones de memoria hay, es decir, 524286. De este estado pasamos al estado 100, final. o Estado 100: Estado final. Se desactiva el banco de la memoria utilizada, el derecho. Y se activa la señal “yasta”, para indicar que el filtro ha finalizado. Diagrama de estados: 36 000 Espera 001 Leer B. Dch 010 Operar 011 Escribir B. Dch 100 Fin accion = 1 accion = 0 finMem = 1 finMem = 0 rst = 0 fig. 13 - Diagrama de estados del Filtro Binarizar Código: Ver anexo C6.2 Problemas encontrados: Una vez logrado el filtro de Blanco y Negro el desarrollo de este filtro de Binarización fue relativamente sencillo. El único problema encontrado fue la definición del umbral, pero se solucionó fácilmente mediante prueba y error. El umbral seleccionado finalmente fue 16, la mitad de los niveles de gris que puede alcanzar cada componente del píxel. De cualquier manera, es posible que, dependiendo del histograma de cada imagen, haya que cambiar este umbral. Si esto fuese necesario, habría que modificar dicho umbral en el código y volver a sintetizar e implementar el mismo, para generar una nueva versión del fichero .bit. Filtro de 8 Colores: Funcionamiento: Este filtro limita a 8, mediante la utilización de un umbral, los colores de la imagen original. Para ello se realiza una comparación del valor de cada uno de los componentes de los píxeles de la imagen con un umbral, si el valor es mayor o igual, se fija el componente a 1, en caso contrario se fija a 0. Esta operación es equivalente a la realizada en el filtro de binarización, sólo que en este caso se realiza componente a componente. 37 Este filtro trabaja sólo con un banco de la memoria, el derecho, y con cinco estados: • Estado 000: Estado de espera. En este estado se entra al pulsar reset. No utilizamos el banco derecho de la memoria todavía, por lo que está apagado. De este estado se pasa al estado 001 cuando se pulsa botón asociado a la señal “accion”. • Estado 001: Leemos los bits de una posición de memoria y cargamos los correspondientes a cada componente (RGB) en su variable. Desde este estado se pasa ineludiblemente al estado 010. • Estado 010: Estado Comparación. En este estado se realiza la comparación de cada uno de los componentes del píxel leído y se le asigna el valor correspondiente según el resultado. De este estado se pasa al número 011. • Estado 011: Se carga en la misma posición del banco derecho de la memoria el valor del píxel transformado. Se escribe bit a bit para ir cargando el valor de cada variable auxiliar, una por componente. Este píxel es cargado en la misma posición de la que fue leído en el estado 001. Este estado y los dos anteriores se ejecutarán tantas veces como posiciones de memoria hay, es decir, 524286 • Estado 100: Estado final. Se desactiva el banco de la memoria utilizada, el derecho. Y se activa la señal “yasta”, para indicar que el filtro ha finalizado. Diagrama de estados: 38 000 Espera 001 Leer B. Dch 010 Operar 011 Escribir B. Dch 100 Fin accion = 1 accion = 0 finMem = 1 finMem = 0 rst = 0 fig. 14 - Diagrama de estados del Filtro 8 Colores Código: Ver anexo C6.1 Problemas encontrados: Dado que este filtro tiene una arquitectura muy similar a los anteriores, y que las operaciones a realizar con los píxeles leídos se limitaban a una comparación y sustitución, su desarrollo fue muy sencillo. En las primeras pruebas observamos que algunos píxeles eran desplazados a la derecha. Para solucionar el problema incluimos la variable dirAux, para guardar el valor de la dirección de memoria de la que se lee en el estado 001 y en la que se debe escribir en el estado 011. Filtros de Convolución de matrices: En la teoría de convolución de matrices se trata de aplicar un filtro a una imagen incluyendo en cada operación los ocho píxeles que rodean cada píxel de la imagen. Es decir, se aplica la mascara seleccionada sobre una matriz 3x3. fig. 14 En los dos filtros que hemos desarrollado con esta técnica despreciamos los bordes, es decir, los píxeles de los bordes permanecerán iguales o se 39 “filtraran” con información no útil situada en la memoria fuera de los límites de la imagen. Este problema sería fácilmente evitable conociendo las dimensiones reales de cada imagen, pero no disponemos de este dato en el momento del desarrollo del código, ya que la información sobre el formato de la imagen no se guarda en memoria, sólo la imagen. Filtro de Suavizado: Funcionamiento: En el caso del filtro de suavizado, la mascara elegida es: 9/ 111 111 111           Esta máscara es un filtro de paso-bajo, que elimina de la imagen original las distorsiones provocadas por los ruidos Gaussiano y Sistemático. El ruido Gaussiano añade o resta valores aleatoriamente al valor real de cada píxel de la imagen, mientras que el ruido Sistemático aparece como un patrón regular que no forma parte de la imagen actual. Este último tipo de ruido puede ser originado por ejemplo, por la cámara, la digitalizadora, la iluminación, etc. Una vez extraídos de la memoria los nueve píxeles que conforman la matriz, la operación que habría que realizar con ellos, convolución, sería la siguiente: a22 = (a11 + a12 + a13 +a21 + a22 + a23 + a31 + a32 + a33) / 9 Siendo A la matriz de los píxeles de la imagen:           332331 232221 312111 aaa aaa aaa El resultado de esta operación será el nuevo valor que, en la imagen transformada, tendrá el píxel del centro, a22. Realmente, en vez de dividir por nueve, dividimos por 8, para poder simplificar la división a desplazar los bits tres posiciones a la derecha. Esto implica un leve aumento en el valor de los píxeles resultantes, pero la diferencia se puede despreciar. En este filtro, como en todos los anteriores, las operaciones de transformación hay que realizarlas en cada componente (RGB) de cada píxel 40 por separado. Esto nos lleva a la utilización de numerosas señales auxiliares en la arquitectura del filtro. Este filtro necesita utilizar los dos bancos de la memoria, el derecho y el izquierdo, para poder ir almacenando el resultado de la operación de convolución sobre cada píxel sin sobrescribir ningún valor original, hasta haber recorrido toda la memoria. Para el cálculo de las direcciones, se utilizará la siguiente matriz, donde col tiene el valor 1024: ( ) ( ) ( ) ( ) ( ) ( ) ( ) (           +++−+ +− +−−−− 11 11 11 coldircoldircoldir dirdirdir coldircoldircoldir ) Para realizar todas las operaciones necesitamos 15 estados: o Estado 0000: Estado de espera. En este estado se entra al pulsar reset. Ambos bancos de memoria están apagados. De este estado se pasa al estado 0001 cuando se pulsa botón asociado a la señal “accion”. o Estado 0001: De este estado al 0110, son los que llamamos estados de inicialización, ya que los utilizamos para cargar la matriz con sus valores iniciales. En este estado leemos del banco derecho de la memoria, el píxel correspondiente a la posición 11 de la matriz. En este punto lo cargamos en la posición 12 porque en estados posteriores se desplaza una posición a la izquierda. La dirección que se lee de la memoria es la 0. De este estado se pasa ineludiblemente al estado 0010. o Estado 0010: Leemos de la dirección 1 de la memoria el píxel correspondiente a la posición 12 de la matriz. En este estado lo colocamos en la posición 13 porque, al igual que con el píxel anterior, posteriormente se desplaza. De este estado pasamos al 0011. 41 o Estado 0011: Leemos de la posición 1024 de la memoria el píxel de la posición 21 de la matriz. Como en casos anteriores, lo colocamos en la posición 22 para desplazarlo después. Leemos de la posición 1024 porque tenemos 1024 columnas en la memoria (0-1023). De este estado pasamos al número 0100. o Estado 0100: Continuamos con la inicialización, en este caso leemos el píxel de la posición 22 de la matriz, dirección de memoria 1025. Lo colocamos en la posición 23 y pasamos al siguiente estado de inicialización 0101. o Estado 0101: Inicialización de la posición 31 de la matriz. Leemos el píxel de la dirección 2048 (número de columnas x 2) de memoria y lo colocamos en la posición 32 de la matriz. o Estado 0110: Este es el último estado de inicialización. Se corresponde con el píxel 32 de la matriz, leído de la posición 2049 de la memoria y situado en la posición 33 en este estado. De aquí pasamos al primer estado de desplazamiento y actualización. o Estado 0111: En este estado y los dos siguientes se desplazan y actualizan los valores de la matriz A. De forma que en dicha matriz estén siempre contenidos los 8 píxeles que rodean al píxel de la posición de memoria que estemos leyendo. En este momento desplazamos una posición a la izquierda todos los valores de la primera fila de la matriz y cargamos el valor de la posición 13. Este valor se corresponde al píxel situado en la posición de memoria “dir” – 1023. Es decir, la dirección de memoria del píxel que queremos tratar menos el número de columnas menos 1. La primera vez que entramos en este estado fijamos el valor de “dir” a 1025, que se corresponde con la segunda columna de la segunda fila de la memoria, para comenzar las operaciones con la primera matriz, situada arriba y a la izquierda de la imagen. De este modo no realizamos la convolución de los píxeles de la primera fila y primera columna de la memoria, que operarían con basura. De este estado pasamos al número 1000. o Estado 1000: Desplazamos todos los valores de la fila 2 de la matriz y leemos de memoria el píxel de la dirección “dir” +1, para situarlo en la posición 23. Pasamos al último estado de desplazamiento y actualización, el 1001. o Estado 1001: Desplazamos todos los valores de la fila 3 de la matriz y leemos de memoria el píxel de la dirección “dir” + 1025, para situarlo en la posición 33. La posición de memoria leída se corresponde con la dirección del píxel que estamos tratando más el número de columnas de la memoria más 1. o Estado 1010: Estado de desplazamiento de componentes. En este estado dividimos por 8, desplazamos tres posiciones a la derecha, 42 cada componente, RGB, de cada una de las nueve posiciones de la matriz. El tener que desplazar cada componente de cada píxel por separado hace que tengamos que recurrir a numerosas señales auxiliares. o Estado 1011: Primer estado de operación. En este estado sumamos por separado los componentes de los píxeles de cada fila de la matriz. De forma que nos quedan tres señales con los valores de R, G y B de cada fila. En el apartado de problemas encontrados comentaremos el por que estas sumas se realizan por separado. o Estado 1100: Segundo estado de operación. En este estado sumamos los tres componentes de cada fila para dejar una sola señal por componente del píxel resultante. Estas tres señales se asignarán directamente a la señal de datos del banco izquierdo de la memoria. o Estado 1101: Escritura en el banco izquierdo. En este estado cargamos bit a bit cada componente del píxel resultante. La carga se realiza de este modo para facilitar su comprensión, dado el número de señales auxiliares utilizadas durante las operaciones. Este píxel es cargado en la posición de memoria "dir", que se corresponde con la dirección de memoria del banco derecho del que leímos el píxel cuya convolución estamos calculando. El banco derecho de la memoria está apagado, no lo utilizamos. De este estado pasaremos siempre al estado de desplazamiento 1 (0111) hasta que hayamos recorrido todo el banco derecho de la memoria y realizado, por tanto, la convolución de todos los píxeles. Una vez hecho esto y cargados los píxeles resultante en el banco izquierdo procedemos a volcar el contenido de este banco al derecho, de donde se lee para mostrar en pantalla. o Estado 1110: Volcamos el contenido del banco izquierdo de memoria en el derecho. Una vez recorrido entero dicho banco pasaremos al estado final del filtro. o Estado 1111: Estado final. Se desactivan ambos bancos de memoria. Y se activa la señal “yasta”, para indicar que el filtro ha finalizado. Diagrama de estados: 43 0000 Espera 0001 Inicia 1 0010 Inicia 2 0011 Inicia 3 0100 Inicia 4 0101 Inicia 5 0110 Inicia 6 0111 Desp 1 1000 Desp 2 1001 Desp 3 1010 Desp Op. 1011 Opera 1 1100 Opera 2 1101 Escribir B. Izq. 1110 Volcar a B. Dch. 1111 Fin accion = 1 accion = 0 finMem = 1 finMem = 0 finMem = 1 finMem = 0 rst = 0 Estado 0000 rst=0 fig. 15 - Diagrama de estados del Filtro de Suavizado 44 Código: Ver anexo C6.5 Problemas encontrados: El primer problema al que nos encontramos fue decidir qué método seguir para recorrer todas las posiciones de memoria mientras creábamos las matrices correspondientes a la convolución de cada píxel. Una vez decidido y estudiado dicho método nos encontramos con el problema del desbordamiento en las sumas. Es decir, el valor resultante excedía los bits definidos para la señal que recogía el resultado, lo que no permitía ni compilar el código. Esto nos obligó a separar en varias operaciones y estados las sumas de los componentes ya desplazados. No modificamos la definición de las señales porque estas son utilizadas para cargar datos en memoria. Cuando logramos sintetizar e implementar correctamente el código vimos que la imagen resultante no era la esperada en absoluto. De hecho estaba completamente deformada. En este punto nos dimos cuenta que habíamos realizado mal las operaciones desde el principio, ya que no habíamos hecho la convolución con cada componente de los píxeles sino con los píxeles completos. Después de redefinir de nuevo las operaciones y añadir las señales necesarias nos encontramos que las sumas eran de nuevo demasiado grandes y, aunque el código compilaba sin error, al intentar sintetizarlo llegaba a bloquear el sistema completo. Para solucionar este último problema tuvimos que añadir, nuevamente, más señales auxiliares. Filtro de detección de bordes: Funcionamiento: Los bordes en las imágenes digitales se caracterizan porque delimitan zonas o regiones que difieren significativamente en los respectivos valores de sus niveles de intensidad del gris. Los detectores de bordes, por consiguiente, buscan píxeles donde se produce un cambio en los niveles de intensidad de los colores de la imagen. Para ello hay trabajar sobre una imagen en Blanco y negro, por lo que este filtro implica aplicar primero el filtro de Blanco y Negro a la imagen original. En este filtro nosotros utilizamos el detector de bordes de la Laplaciana, con la siguiente matriz:           −−− −− −−− 111 181 111 Este detector de bordes lo hemos obtenido también de otra asignatura, Robótica. Como en el filtro anterior, recorreremos la memoria para formar la matriz de convolución de cada píxel y, siendo A tal matriz, operaremos de la siguiente manera: 45 a22 = 8 a× 22 – a11 – a12 – a13 – a21 – a23 – a31 – a32 – a33 Como se verá en la explicación de los estados correspondientes, para llevar a cabo estas operaciones se realizarán primero la suma de los componentes y, después, una resta final. Este filtro tiene, en general, el mismo desarrollo que el anterior, el filtro de suavizado. Con la diferencia fundamental de la matriz de filtrado que se aplica. Aquí utilizamos también 15 estados, de los cuales, los nueve primeros son comunes con el filtro anterior. Así mismo, utilizaremos también el banco izquierdo de la memoria para almacenar los píxeles resultantes después de cada convolución. o Estado del 0000 al estado 1001: Omitimos la explicación de estos estados dado que, como se ha comentado antes, son comunes con el filtro anterior. o Estado 1010: Desplazamos tres posiciones a la derecha, para multiplicar por ocho, los tres componentes del píxel central de la matriz. Este píxel se corresponde con la posición de memoria “dir”, es aquel del que estamos haciendo el filtrado. o Estado 1011: Sumamos por separado los tres componentes de todos los píxeles de la matriz, para restar luego el resultado al componente correspondiente del píxel central, ya multiplicado. o Estado 1100: Restamos a cada componente del píxel central, ya multiplicado, los resultados de sumar los componentes del resto de los píxeles. o Estado 1101 al estado 1111: Comunes al filtro anterior. Diagrama de estados: 46 0000 Espera 0001 Inicia 1 0010 Inicia 2 0011 Inicia 3 0100 Inicia 4 0101 Inicia 5 0110 Inicia 6 0111 Desp 1 1000 Desp 2 1001 Desp 3 1010 Multi central 1011 Suma comp. 1100 Resta comp. 1101 Escribir B. Izq. 1110 Volcar a B. Dch. 1111 Fin accion = 1 accion = 0 finMem = 1 finMem = 0 finMem = 1 finMem = 0 rst = 0 Estado 0000 rst=0 fig. 16 - Diagrama de estados del Filtro de Bordes 47 Código: Ver anexo C6.6 Problemas encontrados: Para hacer las pruebas del código de este filtro utilizamos una imagen ya transformada a blanco y negro con el Filtro de paso a Blanco y Negro. Aunque, teóricamente, este filtro de detección de bordes tiene un funcionamiento muy similar al de suavizado, en el momento de redacción de esta memoria todavía no hemos conseguido hacerlo funcionar correctamente. La imagen resultante, no sólo no mostraba las líneas de bordes, sino que, además, incluía colores distintos al blanco y al negro. Continuamos haciendo pruebas con este código para detectar y solucionar los problemas. 5.4 Integración Llegados a este punto, donde ya teníamos por separado los filtros, el módulo que mostraba una imagen en la RAM (bmpramda) y el cargador, se intentó unirlos todos en dos arquitecturas, cada una con tres filtros y el cargador y el bmpramda, para usarlos luego en la reconfiguración13. Este proceso, que en un principio consideramos que iba a resultar muy simple, ha terminado dándonos muchos problemas, hasta el punto de que únicamente se ha conseguido una arquitectura con el bmpramda y el cargador (funcionando perfectamente) y un sólo filtro (el que pasa a escala de grises). De hecho, este filtro no funciona completamente bien (cómo sí lo hace por separado) y deja ciertos residuos en forma de líneas verticales regularmente espaciadas de color más claro del debido. Hasta llegar a ese punto se han seguido distintos enfoques, cada uno de los cuales era respuesta a los problemas que se iban encontrado. Proceso • En un primer momento se unieron los módulos cargador y bmpramda en una arquitectura y se les añadió a cada uno una señal de enable para que sus salidas respectivas estuvieran en alta impedancia cuando los módulos estuvieran desactivados con el enable. Esta señal de enable tomaba para el bmpramda directamente el valor de un dipswitch y para el cargador la misma señal, pero negada. Así, nunca estarían los dos activos al mismo tiempo, con lo que las señales de salida que compartían nunca tendrían dos fuentes, porque una de las dos tendría el enable activo y el otro desactivado, con lo que la línea tendría una fuente activa y la otra en alta impedancia. En este momento, la imagen que se mostraba estaba azuleada. Es decir, el byte superior de cada píxel estaba bien, pero el byte 13 ver sección Reconfiguración 48 menos significativo estaba completamente a 1, así, todo el canal azul y parte del verde estaba a su máximo valor de intensidad, con lo que la imagen parecía vista a través de un cristal azul. • Después de intentar solucionar el error y no encontrarlo, se decidió intentar juntar el bmpramda con el cargador pero cambiando el enfoque y creando una máquina de estados. Surgieron exactamente los mismos problemas. • Después de trabajar sobre ello durante un tiempo e intentar solucionarlo, se decidió intentar juntar el bmpramda con uno de los filtros y dejar el cargador para más adelante. En este punto, el filtro (que es el de escala de grises, llamado filtroBlancoyNegro) compartía con el bmpramda las entradas y salidas de la RAM, escribiendo y leyendo ambos de ellas a la vez. Obviamente, esto creaba problemas y no hacía nada, mostraba una pantalla completamente blanca. • Entonces, se separan en el filtro y en el bmpramda las entradas de las salidas de la RAM. Hasta este momento las líneas de datos de la RAM de cada módulo eran de tipo inout. Esto se debía a que cada uno de estos módulos debían funcionar por separado y ellos sólos, con su propio archivo ucf. En estos archivos, un mismo pin no puede ir a dos señales distintas, por lo que las líneas de la RAM, de las que queríamos leer y en las que queríamos escribir, debían ser necesariamente de tipo inout. Ahora, que estaban en una arquitectura, podían tratar por separado, como si conceptualmente fueran dos líneas distintas, las señales de la RAM cuando eran de entrada y de salida. Esto hacía necesario que las líneas de la RAM de la arquitectura, que pueden tener dos fuentes distintas (filtro o bmpramda), estén multiplexadas (dependiendo del origen) y tengan fuentes que provengan de triestados, para poder separar las líneas de entrada de las de salida cuando queramos que funcionen como salida y viceversa como muestra la fig. 9. 49 fig. 17 - Comunicación entre la RAM y la Arquitectura global Llegados a este punto, ya vuelve a funcionar, pero de nuevo azulea, como antes. Después de bastante tiempo depurando y buscando los errores, nos dimos cuenta de que el error estaba (o parecía estar) en el tiempo de propagación de la señal oeaux. Esta señal, por algún motivo, se retrasaba con respecto al estado MOSTRANDO. Este problema (que la imagen saliera azuleada), se solucionó añadiendo la comprobación extra, al controlar el multiplexor de la parte baja de la RAM, (estado=MOSTRANDO and oeAux='1'). Aún así, al filtrar la imagen se generaban unas líneas verticales, regularmente espaciadas, de color más claro del que debería. Este problema no ha conseguido solucionarse. • Llegados aquí, se añade el módulo cargador, cosa que no da ningún problema, más allá de los retoques necesarios en las comparaciones de los multiplexores y los estados que hacen falta añadir. • Una vez en este punto, nos dimos cuenta de que no habíamos incluido ningún estado de espera de bajada de la señal de activación del filtro, con lo que una vez que terminaba de funcionar el filtro, si aún se estaba pulsando la señal de 50 activación, el filtro volvía a activarse. Teniendo en cuenta que nuestros filtros tienen pérdida, esto hacía que la imagen resultante fuera de mala calidad. Lo único que se hizo para solucionar esto fue añadirle un estado de espera de bajada de la señal antes de lanzar el filtro. Finalmente, la máquina de estados quedo así: g.18 - Diagrama de estados de la Arquitectura global MOSTRANDO : En este estado tenemos activo el módulo BMPRAMDA e este estado cuando el usuario solicite una CARGANDO : En este estado desactivamos el BMPRAMDA y activamos cargaOK=’0’ cargaOK=’1’ Pide ByNOK=’0’ doneByN=’0’ CARGANDO MOSTRANDO pideByNOKOK=’1’ START BYN doneByN=’1’ pideByNOKOK=’0’ BAJANDO BYN BYN fi • y desactivados todos los demás, por lo que se muestra el contenido de la RAM por pantalla. Mantenemos el contador del eliminador de rebotes a ‘0’, es decir, reseteado. Saldremos d carga o que se aplique el filtro. • el cargador, además de asociar las direcciones de la RAM con las que vienen del módulo cargador. De este estado se sale sólo bajo la petición del usuario y en el momento que él diga. Puede salir de este estado antes 51 de completar la carga (con lo que sólo se cargarían datos hasta el punto en que el usuario bajó el dipswitch). De este estado se vuelve al estado MOSTRANDO. • STARTByN : En este estado se activa el modulo de filtro, subiendo el reset y dejándo el filtro listo para actuar. De quí sepasa siempre a ByN. • ByN : En este estado se pide al filtro que empiece a actuar activando la señal accionByN (esta señal se activa a baja). Seguiremos en este estado hasta que se active la señal de doneByN que nos avisa de que el filtro ha terminado. De aquí pasamos a BAJANDOByN. • BAJANDOByN : Este estado está sólo para esperar a que el usuario deje de pulsar el switch que activa el filtro. Si no estuviera, el filtro se aplicaría continuamente hasta que el usuario deje de pulsar el switch. A todas las señales se le eliminan rebotes mediante un submódulo eliminador de rebotes llamado rebotes, cuyo código se puede ver en el Apéndice C. 6. RECONFIGURACIÓN 6.1 Objetivo buscado Para probar la reconfiguración, la idea era crear dos arquitecturas, cada una de ellas con el bmpramda (visualizador de imágenes), el cargador y tres filtros distintos. Con esto, podríamos pedir mediante un dipswitch la reconfiguración total de la placa, y con otro controlar cuál de las dos arquitecturas se cargarían, dependiendo del filtro que quisiéramos usar. En cualquier caso, en la reconfiguración se empezó a trabajar mientras aún no se habían terminado estas arquitecturas, así que para las pruebas se usaron el contador.vhd (que muestra un patrón de rayas verticales) y una modificación de este (que las muestra horizontales). 6.2 Desarrollo En un primer momento se intentó crear totalmente por nosotros el archivo de configuración de la Flash, a partir del vhd que controla la configuración del CPLD de la Virtex proporcionado por XESS [VDBO01]. En este primer intento, se crearon una serie de estados que se detallan en la fig. 19 y se añadió un contador de 100ms, para eliminar los rebotes que generaban los dipswitches al código mencionado antes. 52 fig. 19 - Diagrama de estados de la reconfiguración S0 Elimina Rebote 1 Elimina Rebote 2 S3 comienza=HI finEspera=LO finEspera=HI v_done=HI poweron_reset=LO comienza=LO BajaDip switch finEspera =LO finEspera=HI v_done=LO S2 S1 finEspera300us=LO finEspera=HI Explicación de los estados: • S0 : Estado inicial. Seguiremos en este estado hasta que llegue una subida del dipswitch que activa la reconfiguración y además hayan pasado 20 milisegundos desde que se encendió la placa (tiempo que necesita la FPGA para estar lista para reconfigurarse después del encendido).En este estado mantenemos la señal reconf y programb a low para que no se reconfigure la FPGA. Al salir de este estado, activamos un contador de 100 milisg. para eliminar rebotes del dipswitch. • EliminaRebotes1 : Nos quedamos en este estado hasta que sube la señal fin_espera_100ms. De aquí pasamos a BajaDipswitch. • BajaDipSwitch : Necesitamos esperar a que baje el dipswitch que activa la reconfiguración, porque si no después de terminar la reconfiguración volveríamos a empezarla de nuevo porque el dipswitch seguiría arriba, y así seguiríamos reconfigurando hasta que se bajara el dipswitch, con lo que se harían muchas configuraciones seguidas innecesariamente. Una vez que se detecta la bajada del dipswitch se pasa a un nu