Sintetizador monofónico basado en FPGA: Parser MIDI mejorado y filtro paso-bajo de segundo orden 
Partiendo del montaje realizado en el post anterior, se han realizado varias modificaciones y mejoras. El parser MIDI de esta segunda iteración genera ahora 3 señales de control, de 7 bits cada una, que se utilizan para controlar la frecuencia de corte, la resonancia y la ganancia de la entrada de un filtro paso bajo de segundo orden:

Este sería el diagrama de bloques de esta segunda iteración:




Parser MIDI mejorado


En la versión iniciar el parser MIDI no se tuvieron en cuenta algunas características "raras" que se dan el algunos teclados controladores y al mismo tiempo se asumía que un "note off" posterior a un "note on" siempre era de la misma tecla, lo cual es demasiado suponer, sobre todo cuando quien toca es un humano. Cuando un humano toca una secuencia de notas en un teclado (por ejemplo: La, Mi, Do) uno puede pensar que los mensaje que manda el teclado controlador son los siguientes:
noteOn(La), noteOff(La), noteOn(Mi), noteOff(Mi), noteOn(Do), noteOff(Do)

Sin embargo lo cierto es que a veces un humano pulsa la siguiente tecla al mismo tiempo o antes de soltar la anterior:
noteOn(La), noteOn(Mi), noteOff(La), noteOff(Mi), noteOn(Do), noteOff(Do)

Con la anterior versión del parser, que asumía que un noteOff se correspondía siempre con el noteOn inmediatamente anterior, lo que ocurría era que cuando al sinte le llegaba el noteOff(La) callaba la nota Mi disparada justo antes porque asumía que ese noteOff se correspondía con dicha nota Mi. En la nueva versión del parse este noteOff(Mi) es ignorado por la máquina de estados por lo que la respuesta del sintetizador es más natural.

Para mejorar el comportamiento y la funcionalidad del parser MIDI se ha optado por un diseño basado en máquinas de estado en serie y en paralelo en lugar de una única máquina de estados grande. El parser MIDI se ha divido en dos etapas (Stage1 y Stage2), la primera etapa genera señales "KeyOn" y "KeyOff" limpias por cables separados y además implementa en paralelo una máquina de estados aparte para procesar los mensajes de "Control Change". En la segunda etapa se implementa la lógica anteriormente descrita de ignorar los "Note Off" que no se corresponden con el mensaje "Note On" inmediatamente anterior.







De esta forma, aunque aparentemente se ha complicado el diseño, se han separado los problemas y es más sencillo introducir modificaciones y depurar errores en las máquinas de estado. Cada una por separado es más sencilla y fácil de trazar que una hipotética máquina de estados única para todo.

Además de la mejora en el procesado de los mensajes "Note On" y "Note Off", este parser ya reconoce mensajes de tipo "Control Change", en concreto para tres valores prefijados de controlador: 71, 74 y 16, que se asignarán en el sintetizador a la frecuencia de corte del filtro, la resonancia del filtro y la ganancia de entrada del filtro.


Filtro paso bajo de segundo orden


Se ha optado por la implementación estándar de un filtro de estado variable (state variable filter). Se trata de un filtro de segundo orden (dos polos) que genera simultáneamente 3 salidas:

- paso bajo (con pendiente de filtrado de 12 dB/octava)
- paso alto (con pendiente de filtrado de 12 dB/octava)
- paso banda (con pendiente de filtrado de 6 dB/octava)

No son grandes pendientes de filtrado pero siempre se pueden mejorar poniendo varios filtros en cascada. La implementación que se ha utilizado es la descrita en el libro "Musical Applications of Microprocessors" de Hal Chamberlin (dicha implementación ya fue usada sobre un microcontrolador en este post). El filtro de estado variable viene determinado por el siguiente sistema de ecuaciones en diferencias finitas:

$$pasoAlto[n] = entrada - ({r \times pasoBanda[n-1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$
$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$

Siendo:

$$f = 2\sin\left({\pi F_c \over F_s}\right)$$
$$r = {1 \over Q}$$

Siendo $F_c$ la frecuencia de corte del filtro, $F_s$ la frecuencia de muestreo y $Q$ la Q del filtro (la resonancia).

Si se reordenan las ecuaciones en diferencias:

$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$
$$pasoAlto[n] = entrada - ({r \times pasoBanda[n - 1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$

Podemos olvidarnos de los índices:
pasoBajo += f * pasoBanda
pasoAlto = entrada - (r * pasoBanda) - pasoBajo
pasoBanda += f * pasoAlto

Como se puede apreciar es preciso mantener en memoria (registro) al menos las variables pasoBajo y pasoBanda entre que se procesa una muestra y la siguiente (se trata de un filtro digital de segundo orden).

Para implementar dicho filtro sobre FPGA lo que necesitaremos serán básicamente los siguientes elementos:

- Al menos tres registros en los que almacenaremos los valores "pasoBajo", "pasoBanda" y "pasoAlto" (aunque realmente podríamos no gastar un registro para "pasoAlto", lo vamos a incluir para poder disponer de esa salida en el módulo).
- Una unidad de suma con multiplicación: Un módulo combinacional que realiza la operación: A = (B * C) + D (en muchos casos D = A, por lo que se puede ver como A += B * C)
- Una máquina de estados para controlar qué operandos y operaciones se hacen en cada momento.

Con estos elemento y teniendo en cuenta las ecuaciones anteriores, podemos hacer una propuesta de secuenciación de operaciones como sigue:

1. LP := (cutoff * BP) + LP
2. HP := (0 * x ) + IN
3. HP := (-reso * BP) + HP
4. HP := (-1 * LP) + HP
5. BP := (cutoff * HP) + BP

Cada paso requiere un único ciclo de reloj por lo que bastará con implementar una máquina de estados que, por cada muestra que llegue, pase por los 5 estados de forma secuencial para que los registros LP, BP y HP (LowPass, BandPass y HighPass) tengan los valores de salida del filtro que necesitamos. Nótese que será preciso utilizar aritmética de punto fijo y en nuestro caso se ha optado por un formato Q16.16 (16 bits de parte entera y 16 bits de parte fraccionaria).

A continuación puede verse como quedaría la implementación del filtro en VHDL:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity StateVariableFilter is
    port (
        Reset       : in std_logic;
        Clk         : in std_logic;
        EnableIn    : in std_logic;
        SampleIn    : in std_logic_vector(15 downto 0);
        CutOffIn    : in std_logic_vector(31 downto 0);    -- 0..1  fixed point Q16.16
        ResonanceIn : in std_logic_vector(31 downto 0);    -- 0..1  fixed point Q16.16
        SampleOut   : out std_logic_vector(15 downto 0);
        EnableOut   : out std_logic
    );
end entity;

architecture RTL of StateVariableFilter is
    signal LPDBus : std_logic_vector(31 downto 0);
    signal LPQBus : std_logic_vector(31 downto 0);
    signal HPDBus : std_logic_vector(31 downto 0);
    signal HPQBus : std_logic_vector(31 downto 0);
    signal BPDBus : std_logic_vector(31 downto 0);
    signal BPQBus : std_logic_vector(31 downto 0);
    signal MultOperandA : std_logic_vector(31 downto 0);
    signal MultOperandB : std_logic_vector(31 downto 0);
    signal MultResult64 : std_logic_vector(63 downto 0);
    signal MultResult : std_logic_vector(31 downto 0);
    signal AddOperandB : std_logic_vector(31 downto 0);
    signal AddResult : std_logic_vector(31 downto 0);
    signal NegResonance : std_logic_vector(31 downto 0);
    signal FSMDBus : std_logic_vector(2 downto 0);
    signal FSMQBus : std_logic_vector(2 downto 0);
begin
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            LPQBus <= LPDBus;
        end if;
    end process;

    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            HPQBus <= HPDBus;
        end if;
    end process;

    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            BPQBus <= BPDBus;
        end if;
    end process;

    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            FSMQBus <= FSMDBus;
        end if;
    end process;

    NegResonance <= std_logic_vector(to_signed(-to_integer(signed(ResonanceIn)), 32));
    MultOperandA <= CutOffIn when ((FSMQBus = "001") or (FSMQBus = "101")) else
                    NegResonance when (FSMQBus = "011") else
                    std_logic_vector(to_signed(-65536, 32)) when (FSMQBus = "100") else  -- -65536 es -1 en notación Q16.16
                    std_logic_vector(to_signed(0, 32));
    MultOperandB <= LPQBus when (FSMQBus = "100") else
                    BPQBus when ((FSMQBus = "001") or (FSMQBus = "011")) else
                    HPQBus;
    AddOperandB <= LPQBus when (FSMQBus = "001") else
                   BPQBus when (FSMQBus = "101") else
                   HPQBus when ((FSMQBus = "011") or (FSMQBus = "100")) else
                   std_logic_vector(to_signed(to_integer(signed(SampleIn)), 32));
    --MultResult64 <= std_logic_vector(to_signed(to_integer(signed(MultOperandA)) * to_integer(signed(MultOperandB)), 64));
    MultResult64 <= std_logic_vector(signed(MultOperandA) * signed(MultOperandB));
    MultResult <= MultResult64(47 downto 16);
    --AddResult <= std_logic_vector(to_signed(to_integer(signed(MultResult)) + to_integer(signed(AddOperandB)), 32));
    AddResult <= std_logic_vector(signed(MultResult) + signed(AddOperandB));
    LPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else
              AddResult when (FSMQBus = "001") else
              LPQBus;
    HPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else
              AddResult when ((FSMQBus = "011") or (FSMQBus = "100") or (FSMQBus = "010")) else
              HPQBus;
    BPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else
              AddResult when (FSMQBus = "101") else
              BPQBus;

    -- fsm
    --    LP += cutoff * BP
    --    HP = in - (resonance * BP) - LP
    --    BP += cutoff * HP
    FSMDBus <= "000" when ((Reset = '1') or (FSMQBus = "110")) else       --       MultOperandA   MultOperandB   AddOperandB
               "001" when ((FSMQBus = "000") and (EnableIn = '1')) else   -- LP := cutoff       * BP           + LP
               "010" when (FSMQBus = "001") else                          -- HP := 0            * x            + IN
               "011" when (FSMQBus = "010") else                          -- HP := -reso        * BP           + HP
               "100" when (FSMQBus = "011") else                          -- HP := -1           * LP           + HP
               "101" when (FSMQBus = "100") else                          -- BP := cutoff       * HP           + BP
               "110" when (FSMQBus = "101") else
               "000";
    EnableOut <= '1' when (FSMQBus = "110") else
                 '0';
    SampleOut <= std_logic_vector(to_signed(-32768, 16)) when (to_integer(signed(LPQBus)) < -32768) else
                 std_logic_vector(to_signed(32767, 16)) when (to_integer(signed(LPQBus)) > 32767) else
                 LPQBus(15 downto 0);
end architecture;


La máquina de estados espera hasta que la entrada "EnableIn" se ponga a "1", dicho evento es la señal que indica al filtro que debe realizar una iteración (i.e. calcular la siguiente muestra a partir de la entrada "SampleIn").



Todo el código está disponible en la sección soft.

[ 1 comentario ] ( 150 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 307 )
Sintetizador monofónico basado en FPGA: parser MIDI, oscilador y DAC básicos 
Tradicionalmente, la síntesis y el procesado de sonido digital siempre se ha delegado a nivel hardware en el uso de DSPs. El uso de FPGAs para sustituir DSPs es una tendencia actual derivada del abaratamiento de las FPGAs y de la incursión de las mismas dentro del mundo de la electrónica amateur y DIY. Actualmente una FPGA media tiene suficiente potencia para llevar a cabo múltiples operaciones DSP a una velocidad incluso mayor. El problema con las FPGAs es la forma de programarlas, que requiere un pensamiento abstracto de tipo diferente al razonamiento algorítmico tradicional que se utiliza para programar CPUs y DSPs estándar. Este post se introducirá en el diseño y la implementación de un sintetizador monofónico muy simple sobre una FPGA.

La idea

La idea de esta primera versión es implementar un sintetizador monofónico con un único oscilador de diente de sierra, que sólo lea mensajes de tipo NoteOn y NoteOff y que reproduzca el sonido a través de un DAC I2S.



Como se puede apreciar se trata del típico circuito de entrada MIDI con optoacoplador más un PCM5102A como DAC I2S de alta calidad. Los mensajes MIDI de NoteOn se traducen en tonos que genera el oscilador.

El interfaz de salida I2S para el DAC externo

El protocolo I2S es un estándar definido para transportar sonido digital a muy cortas distancias (dentro de una misma placa, por ejemplo). Es estándar de facto en casi la totalidad de los conversores DAC y ADC de alta calidad del mercado de todos los fabricantes y se trata de un protocolo relativamente ligero y fácil de implementar.


(imagen © Texas Instruments Incorporated, extraida con permiso de la hoja de datos del PCM5102A)

Existe una variante del I2S denominada "Left Justified" que simplifica el uso del reloj LR, evitando el desfase de un bit entre el envío de cada palabra para el canal izquierdo y el canal derecho:


(imagen © Texas Instruments Incorporated, extraida con permiso de la hoja de datos del PCM5102A)

Y que es la variante I2S que se ha usado en este proyecto ya que es más fácil de implementar que el estándar original y actualmente todos los DACs del mercado la soportan. A continuación puede verse lo que sería el diagrama de bloques de la interfaz I2S-LJ dentro de la FPGA:



Las diferentes tablas de verdad de cada uno de los bloques combinacionales serían las siguientes:

EntradasSalidas
ClkOutDivider == 22ResetMUXcod
00+
010
1X0


EntradasSalidas
ClkOutDivider == 22ClkOutDivider == 10ResetMUXco
1010
0101
en otro casoClkOut


EntradasSalidas
ClkOutDivider == 22ResetMUXbc
X10
10+ mod 32
en otro casoBitCounter


EntradasSalidas
BitCounter < 16ResetMUXlrco
000
011
1X1


EntradasSalidas
ClkOutDivider == 22BitCounter == 31MUXdata
0Xdata
10<<
11muestra izq + der


Como se puede apreciar el mecanismo se basa en meter en un registro de desplazamiento de 32 bits las dos palabras de 16 bits de cada canal (izquierdo + derecho) e ir emitiendo bit a bit ese registro cambiando la polaridad de la señal LRCLK cada 16 bits para indicar canal izquierdo o canal derecho.

El oscilador

El oscilador se ha implementado como un sencillo acumulador de fase.



Como lo que se busca es un oscilador de diente de sierra, lo más sencillo es aprovechar el comportamiento natural de cualquier acumulador que, cuando se desborda "da la vuelta". Esto simplifica enormemente todo el diseño ya que, de forma natural, la señal resultante tiene forma de diente de sierra.


(imagen de dominio público extraida de Wikipedia)

Por cada nueva muestra que debe ser calculada, el acumulador es incrementado en una cantidad determinada, lo que provoca que su valor crezca de forma lineal (la rampa del diente de sierra). Al cabo de una cantidad suficiente de muestras, el acumulador se desbordará y "dará la vuelta" empezando de nuevo desde abajo (el "pico" del diente de sierra).

La cantidad que se use para ir incrementando el acumulador de fase determinará la frecuencia de la señal del oscilador:

$$DivisorFrecuenciaRelojI2S = {{32000000 Hz \over 44100 Hz} \over 32 bits}$$

$$inc = {{f \times 65536} \over {{32000000Hz \over DivisorFrecuenciaRelojI2S} \over 32 bits}} \times 65536$$

El incremento (inc) debe estar en formato Q16.16 (punto fijo de 16 bits de parte entera y 16 bits de parte fraccionaria), que es el formato usado por el acumulador de fase del oscilador.

Nótese que el oscilador no se incrementa en cada ciclo de reloj de la FPGA, sino cada vez que se requiere una nueva muestra por parte de la interfaz I2S-LJ para emitirla al DAC.

El parser MIDI

El módulo de procesamiento MIDI se encarga de implementar un receptor UART sencillo a 31250 baudios y una máquina de estados que vaya leyendos los datos MIDI de entrada y determinando en cada momento si hay que reproducir una nota en el oscilador (y con qué frecuencia) o no.

La UART se implementa de forma muy sencilla usando un registro de desplazamiento y un contador para medir el tiempo equivalente a 1.5 bits y a 1 bit.



Y usando la siguiente máquina de estados:



En una entrada anterior de este blog se abordó este proyecto de forma separada. Lo que se ha hecho en este caso ha sido simplificar aquel esquema para que cupiese todo dentro de un único fichero VHDL.

Una vez implementado el receptor UART, el parser MIDI se puede implementar mediante una sencilla máquina de estados que sólo detecte eventos NoteOn y NoteOff.



El parser MIDI en este caso no sólo determina qué nota debe ser reproducida, sino que usando una ROM interna, determina el valor de incremento que debe ser usado por el módulo oscilador para generar el tono correspondiente.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity NotesRom is
    port (
        AddressIn : in std_logic_vector(6 downto 0);
        DataOut   : out std_logic_vector(31 downto 0)
    );
end entity;

architecture RTL of NotesRom is
    type RomType is array (0 to 127) of std_logic_vector(31 downto 0);
    constant Data : RomType := (
        x"00000000",   -- note 0
        x"000cdf51",   -- note 1
        x"000da345",   -- note 2
        x"000e72df",   -- note 3
        x"000f4ed1",   -- note 4
        x"001037d7",   -- note 5
        x"00112eb9",   -- note 6
        x"00123449",   -- note 7
        x"00134966",   -- note 8
        x"00146efe",   -- note 9
        x"0015a60b",   -- note 10
        x"0016ef97",   -- note 11
        . . .
        . . .
        . . .
        x"368d1251",   -- note 122
        x"39cb7a59",   -- note 123
        x"3d3b4348",   -- note 124
        x"40df5cc9",   -- note 125
        x"44bae33a",   -- note 126
        x"48d12253"    -- note 127
    );
begin
    DataOut <= Data(to_integer(unsigned(AddressIn)));
end architecture;

Para generar este conjunto de valores se hizo un pequeño programa en C++ que convirtió el valor de cada nota MIDI en el valor de incremento correspondiente para que el oscilador emita a esa frecuencia:

#include <iostream>
#include <iomanip>
#include <stdint.h>
#include <math.h>

using namespace std;

double getFreq(uint8_t midiNote) {
    const double A4_FREQ = 440;
    const int32_t A4_MIDI_NOTE = 69;
    return A4_FREQ * pow(2.0, ((double) (((int32_t) midiNote) - A4_MIDI_NOTE)) / 12.0);
}

uint32_t getInc(uint8_t midiNote) {
    const uint32_t CLK_FREQ = 32000000;
    const uint32_t SAMPLE_RATE = 44100;
    double freq = getFreq(midiNote);
    double div = (((double) CLK_FREQ) / SAMPLE_RATE) / 32;
    double inc = (freq * 65536) / ((CLK_FREQ / div) / 32);
    uint32_t ret = round(inc * 65536);
    return ret;
}

int main() {
    for (uint8_t n = 0; n < 128; n++)
        cout << "\t\tx\"" << hex << setw(8) << setfill('0') << getInc(n) << "\",   -- note " << dec << setw(0) << setfill(' ') << ((int) n) << endl;
    return 0;
}

Compilando este programa y ejecutándolo, genera en la salida estándar los valores de incremento de todas las 127 notas MIDI posibles:

g++ -c -o notes_rom_generator.o notes_rom_generator.cc
g++ -o notes_rom_generator notes_rom_generator.o
./notes_rom_generator


Todo junto

A la hora de ponerlo todo junto, basta con interconectar los tres bloques:





Implementación sobre cualquier FPGA

La implementación se ha desarrollado sobre una Spartan3E de Xilinx a 32 MHz pero el proyecto se puede meter en cualquier FPGA siempre y cuando se ajusten las ecuaciones y las constantes para tener en cuenta las diferentes frecuencias de reloj. En caso de que queramos meter el sintetizador en una FPGA que vaya a otra frecuencia de reloj habría que realizar los siguientes cambios:

1. Las constantes CLK_OUT_DIV y CLK_OUT_DIV_BITS de LJI2SOutput.vhd deben se recalculadas.

2. Las constantes TIME_COUNTER_BITS, TIME_COUNTER_1BIT y TIME_COUNTER_1_5BIT de UartRx.vhd deben ser recalculadas.

3. La constante CLK_FREQ dentro de notes_rom_generator.cc debe ser cambiada, hay que recompilar el programa y colocar la salida generada como los nuevos valores de NotesRom.vhd.

Todo el código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 527 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 431 )
Programación de una FPGA Spartan 6 
Publico este post a modo de mini tutorial sobre cómo programar la FPGA Spartan 6 de Xilinx usando un programador de bajo coste basado en el chip FT232H desde Linux.

Placa de ejemplo

Como placa de ejemplo he usado una placa recién adquirida por AliExpress, en concreto un clon de la QMTech XC6SLX16 SDRAM Core Board, una placa que incluye una FPGA Spartan 6 de Xilinx, un oscilador a 50 MHz, una SDRAM de 32 Mb, una flash SPI de 8 Mbit (para almacenar la configuración no volátil de la FPGA), varios leds y múltiples puestos de entrada/salida. El clon que se puede adquirir por AliExpress es exactamente igual que la placa original. Me costó unos 19¤ con los gastos de envío incluidos.

Programador de ejemplo

Como interface de programación se ha optado por usar un conversor USB a UART/SPI/I2C/JTAG basado en el chip FT232H. En concreto he usado este por ser una opción barata y de buena calidad de construcción. Costó unos 9¤ con los gastos de envío incluidos.

Prueba de concepto

Instalamos el entorno ISE WebPack de Xilinx (la última versión disponible con soporte para Spartan 6 es la 14.7. No es necesario instalar los drivers de programación), lo abrimos y creamos un nuevo proyecto para la FPGA XC6SLX16, con encapsulado FTG256 y velocidad -2.

Yo llamé al proyecto "Spartan6Blinker" y dentro de él creé un único módulo VHDL al que llamé "Spartan6Blinker.vhd" con el siguiente código:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity Spartan6Blinker is
    Port (
        Clk   : in std_logic;
        D1Led : out std_logic
    );
end entity;

architecture A of Spartan6Blinker is
    constant COUNTER_WIDTH : integer := 23;
    signal CounterDBus : std_logic_vector((COUNTER_WIDTH - 1) downto 0);
    signal CounterQBus : std_logic_vector((COUNTER_WIDTH - 1) downto 0);
begin
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            CounterQBus <= CounterDBus;
        end if;
    end process;

    CounterDBus <= std_logic_vector(to_unsigned(to_integer(unsigned(CounterQBus)) + 1, COUNTER_WIDTH));
    D1Led <= CounterQBus(COUNTER_WIDTH - 1);
end architecture;


Como se puede ver es un sencillo contador incremental de 23 bits sin control de desbordamiento (se va incrementando desde 0 hasta (2^23 - 1) y vuelta a empezar) y lo único que hacemos es conectar el bit más significativo del registro contador a la salida del led D1.

A continuación añadimos un nuevo fichero fuente de tipo UCF (Implementation Constraints File) al que llamamos "QMTechSpartan6Board.ucf" (podemos ponerle el nombre que queramos) y le metemos el siguiente contenido:

NET Clk   LOC = A10 | IOSTANDARD = LVCMOS33;
NET D1Led LOC = T9 | IOSTANDARD = LVCMOS33;


Lo que hemos hecho es definir en qué pines concretos está la entrada de reloj y la salida hacia el led D1. Estos datos están disponibles en el repositorio de Github de QMTech.

Hacemos doble click en "Generate Programming File" y esperamos a que termine todo el proceso de compilación y síntesis del VHDL. Este proceso generará un fichero llamado "Spartan6Blinker.bit" en la carpeta del proyecto, que es el fichero que se manda a la FGPA o se tosta en la flash SPI.

Por ultimo, nos vamos a una consola, nos descargamos el código fuente del programa xc3sprog en una carpeta aparte:

mkdir -p /opt/src
cd /opt/src
git clone https://github.com/buserror/xc3sprog.git


Y seguimos las instrucciones del fichero README para compilarlo.

Programar directamente la FPGA

Para programar la FPGA lo que hacemos es conectarle el programador basado en FT232H de la siguiente manera:

FT232H      Spartan 6
AD0 --------- TCK
AD1 --------- TDI
AD2 --------- TDO
AD3 --------- TMS
GND --------- GND

Conectamos a continuación la placa Spartan 6 a la alimentación de 5 voltios, la placa FT232H al USB de nuestro ordenador y ejecutamos el xc3sprog de la siguiente manera:

cd /opt/src/xc3sprog/build
./xc3sprog -c ft232h /RUTA_CARPETA_PROYECTO/Spartan6Blinker.bit

Programar la flash SPI

Con el programador FT232H conectado de la misma forma, grabamos en la RAM de la FPGA una configuración que nos permitirá transferir datos entre JTAG y la flash SPI:

cd /opt/src/xc3sprog/build
./xc3sprog -c ft232h ../bscan_spi/xc6slx16_cs324.bit

Y a continuación transferimos nuestro fichero bit indicando que es para la flash:

./xc3sprog -c ft232h -I /RUTA_CARPETA_PROYECTO/Spartan6Blinker.bit

De esta forma la FPGA, nada más arrancar, cargará nuestro blinker (sin necesidad de que esté conectada por JTAG).



[ añadir comentario ] ( 222 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 2.9 / 940 )
Cielo artificial y luces para el Belén basados en CPLD 
Como todos los años cuando se acercan las fechas navideñas siempre trato de revisitar el concepto de luces del Belén aprovechando los conocimientos adquiridos en el último año. En este caso, entendiendo que el concepto de luces a secas ya hay que superarlo :-), se ha introducido una componente móvil en el Belén de este año: un cielo artificial con ciclo día-noche.

La rueda del cielo

Para simular un cielo que cambia entre día y noche se ha optado por una solución muy sencilla basada en un disco de cartón de medio metro de diámetro, aproximadamente, a cuyo eje se conecta directamente un motor paso a paso con una reductora. El motor girará lentamente a razón de una vuelta cada día.

Colocando el motor encima del mueble sobre el que se va a colocar el belén, con el eje apuntando a la pared, se puede colocar el disco de cartón de tal manera que sólo sea visible la mitad superior del mismo. De esta manera se puede pintar medio disco de cartón como si fuese de día (azul celeste, por ejemplo) y la otra mitad del disco de negro con el firmamento y la estrella de Belén, por ejemplo.



Electrónica de control de la rueda del cielo

El motor elegido para acoplar la rueda del cielo es el conocido y asequible 28BYJ-48 (que venden en AliExpress junto con la placa controladora a unos 2 ¤ en el momento de escribir estas líneas). Se trata de un motor paso a paso de 4 bobinas (4 pasos enteros u 8 medios pasos) y con una reductora interna que nos da una resolución teórica de 4096 medios pasos por vuelta (por cuestiones mecánicas, en realidad son medios 4076 pasos por vuelta, según comenta Luis Llamas en su blog).

La placa controladora tiene cuatro entradas digitales correspondientes cada una a una de las 4 bobinas del motor. Activando alternativamente las entradas 1, 2, 3, 4, 1, 2, 3, 4... hacemos girar el motor en un sentido, mientras que activando alternativamente las entradas 4, 3, 2, 1, 4, 3, 2, 1... hacemos girar el motor en el sentido opuesto. A aquellas personas que no estén familiarizadas con los motores paso a paso o con este motor paso a paso en particular les recomiendo esta entrada del blog de Luis Llamas, donde está magníficamente explicado.

La idea es hacer un circuito que haga que el motor paso a paso se mueva lentamente de manera que de una vuelta entera cada 24 horas (para simular el ciclo día-noche en el disco de cartón). Al diseño hay que añadirle botones de avance y retroceso rápido para que el disco pueda "calibrarse" o "sincronizarse" manualmente de forma sencilla (hay que recordar que no es necesaria una precisión milimétrica, es para un Belén). A continuación un diagrama del circuito a implementar en el CPLD:



El circuito consta de tres registros:

- Uno que actúa como latch de salida.

- Otro que actúa de registro de rotación.

- Otro que actúa como contador.

El multiplexor que controla el valor del latch de salida (MUXo) permite elegir entre la salida del registro de rotación o todo ceros, el multiplexor que controla el valor del registro de rotación (MUXs)permite elegir entre mantener el valor (realimentación directa), cargar un valor "0001" (para cuando se inicializa el circuito) o cargar una versión rotada de la salida actual del registro de rotación (en un sentido u otro dependiento de otro multiplexor, MUXd). El multiplexor que controla el registro contador (MUXc) permite elegir entre cargar el valor de reset del contador (T) o cargar el valor decrementado. La constante con la que se decrementa el registro contador debe variar en función de la velocidad a la que queramos que se mueva el disco y está controlada por otro multiplexor (MUXic).

El resto de bloques que aparecen en el esquema son circuitos combinacionales:

- El bloque "<=0" genera un 1 si el valor del contador (con signo) es menor o igual a cero.

- El bloque "DIC" (Dentro Intervalo Cuenta) genera un 1 a su salida si el valor del registro contador está dentro de un intervalo de valores. Esto se utiliza para evitar que las bobinas del motor paso a paso consuman mucho ya que para girar el rotor un paso basta con generar un pulso lo suficientemente ancho en la bobina correspondiente y luego dejar el motor en reposo (la reductora hace que el motor rotor esté prácticamente frenado en ausencia de pulsos).

- El bloque ">=1" es una puerta OR. Cuando se está haciendo avance o retroceso rápido, el multiplexor de salida hace de buffer del registro de rotación, pero cuando no estamos girando rápido, hay que activar las bobinas el motor sólo el tiempo necesario para evitar que el circuito consuma mucha corriente. De esta forma aunque el registro de rotación tenga el valor "0100" el registro de salida sólo tendrá el valor "0100" el tiempo necesario para excitar la bobina correspondiente y, a continuación, emitirá un "0000" aunque en el registro de rotación siga estando el valor "0100".

- El bloque "+" es un bloque sumador estándar. El encargado de ir decrementando el registro contador.

Funcionamiento

Los otros dos bloques combinacionales (abajo a izquierda y derecha) son los encargados de controlar todo el conjunto. Veamos primero el bloque combinacional de abajo a la izquierda:

EntradasSalidas
DIC/RESET<=0MUXsMUXcMUXo
X0X110
010000
110001
X11210


Cuando la entrada /RESET se pone a 0, MUXs selecciona la entrada "0001", MUXc selecciona la entrada del valor de reset del contador (T) y MUXo selecciona la entrada "0000", por lo que en el siguiente ciclo de reloj el registro de salida se pondrá a cero, el registro contador se cargará con el valor T y el registro de rotación se cargará con el valor "0001".

Una vez que /RESET se pone a 1, como el registro contador vale T (se trata de un contador decremental), tanto las entradas C como DIC están a cero por lo que MUXs selecciona la entrada de realimentación (para mantener el valor actual del registro de rotación), MUXc selecciona la entrada procedente del sumador y MUXo sigue seleccionando la entrada con el valor "0000" (la salida del latch que va al motor sigue siendo "0000") por ahora.

Hay que tener en cuenta que, como el MUXc está selecionando la entrada procedente del sumador, cada ciclo de reloj que pasa, el valor del contador se decrementa. En algún momento el valor del contador entrará dentro del intervalo configurado para el bloque combinacional DIC y este bloque empezará a emitir un 1. Esto provoca que el MUXo seleccione la entrada procedente del registro de rotación por lo que se emitirá el valor almacenado en dicho registro hacia el motor durante el tiempo que el valor del contador genere un 1 a la salida del bloque DIC. Cuando el contador baje por debajo del umbral inferior del bloque comparador DIC, la salida de este bloque será de nuevo 0 y la salida del registro de salida volverá a ser "0000" de nuevo. El tiempo que el bloque comparador DIC emite un 1 debe ser suficiente como para que se exciten adecuadamente las bobina del motor (en mi caso lo he puesto para que las active durante un segundo, más que suficiente).

Una vez que el registro de salida ha vuelto a "0000" el registro contador continúa su camino hacia el cero. Cuando llega a cero (o lo sobrepasa hacia el negativo), el bloque "<=0" emite un 1. Esta condición hace que el MUXs seleccione la entrada de rotación (para que se active la siguiente bobina del motor y el rotor gire un poquito), que el MUXc seleccione la entrada del valor iniciar T (para que se cargue el contador con el valor inicial) y que el MUXo seleccione la entrada "0000" (para seguir emitiendo ceros).

Esto provoca que todo el ciclo empiece de nuevo por lo que tendremos que, calculando bien el valor de T y los valores umbral del bloque comparador DIC conseguiremos un disco dia-noche que de una vuelta entera una vez cada 24 horas.

Si el motor paso a paso da una vuelta completa cada 4076 medios pasos y nosotros vamos a utilizarlo con pasos enteros, cará una vuelta cada ${4076 \over 2} = 2038$ pasos enteros. Por tanto si queremos que de una vuelta entera cada día tendrán que pasar:

$${{24 \times 60 \times 60} \over 2038} = 42.3945 \ \ segundos/paso$$

Como el reloj va a 50 MHz el valor de T será de:

$${{24 \times 60 \times 60} \over 2038} \times 50000000 \approx 2119725221 \ \ ciclos/paso$$

Con este valor de T podemos hacer que el bloque DIC emita un 1 cuando:

$$2119725000 > contador > 2069725220$$

50000000 de ciclos de diferencia (1 segundo). Y un 0 en el resto de los casos.

El bloque combinacional de abajo a la derecha es el encargado de controlar el avance y retroceso rápidos.

EntradasSalidas
Avance rápidoRetroceso rápidoMUXdRápido
0000
0111
1X01


En función de los valores de las entradas de avance rápido y retroceso rápido, el multiplexor MUXd seleccionará un sentido de rotación u otro. Además, en caso de que se pulse cualquiera de los dos botones, el registro latch de salida selecciona siempre la entrada proveniente del registro de rotación: cuando estamos haciendo avance y retroceso rápido los pulsos de activación serán tan cortos que no será necesario usar el mecanismo del bloque DIC para controlar la anchura de los pulsos de activación de las bobinas.



Luces para el cielo nocturno

Para rizar el rizo y aprovechando que tenía por aquí un CPLD chico de 64 macroceldas (el EPM3064A de Altera, unos 6 ¤ por aliexpress) me aventuré a colocar unas luces en la parte "nocturna" del disco giratorio. Una pila de botón de tipo CR2032 es más que suficiente para alimentar el CPLD y los 5 leds que se usan para simular las estrellas.



En este caso se ha realizado una implementación simplificada del diseño publicado en este post. En lugar de incluir un comparador y un latch se ha optado por emitir directamente hacia los leds, cinco de los bits del registro LFSR de 10 bits.



Si el reloj del CPLD va a 50 MHz (como en nuestro caso) y queremos que las luces cambien cada segundo, T debe valer 50000000. La descripción del resto de bloques combinacionales es la siguiente:

- El bloque "P" es el bloque que aplica el polinomio de realimentación maximal para 10 bits al valor actual del registro LFSR (una puerta XOR más un desplazamiento). Al ser un polinomio maximal de 10 bits el registro LFSR generará una secuencia de números pseudoaleatoria comprendida entre los valores 1 y 1023 (el valor 0 está fuera de la secuencia y en caso de que se alcance dicho valor, el LFSR se "para").

- El bloque "X" es un bloque que, en caso de que la entrada valga "0000000000" en la salida emite "0000000001", en caso contrario emite la entrada sin cambiar. Este bloque se coloca para garantizar que si el LFSR se pone totalmente a 0 (por ruido, reinicio, encendido, etc.) vaya a un valor que sí esté dentro de la secuencia pseudoaleatoria de números y pueda así seguir generando números dentro de dicha secuencia.

- El bloque "=0" es un bloque que emite un 1 si el valor del registro contador es 0 y un 0 en caso contrario.

Como se puede observar el comportamiento del generador de destellos para el firmamento nocturno es muy sencillo:

Si asumimos que el momento del arranque los registros están todos a cero, la salida del bloque "=0" será 1 por lo que se seleccionará la entrada T del multiplexor del contador y la entrada P del multiplexor del LFSR. Aunque el registro LFSR está a 0, la salida del bloque X será "0000000001" por lo que la salida de P será el siguiente valor de la secuencia maximal de P después del valor "0000000001". En el momento que llega el siguiente flanco de subida del reloj se carga el registro LFSR con el nuevo valor de la secuencia pseudoaleatoria y se carga el registro contador con el valor T (50000000).

A partir de ahora, como el registro contador contiene un valor diferente de 0, la salida del bloque "=0" será un 0 por lo que el multiplexor del registro LFSR mantendrá el valor actual del registro LFSR y el multiplexor del contador seleccionará la entrada que proviene del sumador. Esta condición se mantendrá durante el tiempo que el contador sea mayor que cero (para T = 50000000 a 50 MHz, tenemos un segundo de tiempo) y en el momento que el contador llegue a cero, el bloque combinacional "=0" emitirá de nuevo un uno y el proceso se reanudará de nuevo (carga del LFSR con el siguiente valor de la secuencia pseudoaleatoria y carga del contador con el valor T).

Si sacamos hacia fuera 5 de los 10 bits del registro LFSR (no tienen por qué ser consecutivos) obtendremos un razonable efecto de "cielo estrellado aleatorio" que cambia cada segundo. Ahora podemos colocar todo el montaje en la parte trasera del disco de cartón dejando que se asomen hacia adelante sólo los leds y poniendo en la cara no visible la plaquita con el CLPD y la pila de botón.

Todo el código VHDL está disponible en la sección soft.

A continuación puede verse una foto de todo el conjunto pintado y montado simulando el cielo nocturno:



Y simulando el clieno diurno ("amaneciendo"):



¡Feliz Navidad a todos! :-)

[ añadir comentario ] ( 3270 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2752 )
Implementación de una interfaz VGA sobre FPGA 
A lo largo de este post se abordará el diseño y la implementación en VHDL de una interfaz de salida VGA para FPGA. La interfaz lee una imagen de 64x48 pixels de una memoria (por ahora una ROM) interna y la renderiza usando el modo VGA estándar de 640x480 a 60Hz.

Señal VGA

Las señales más importantes que viajan por un cable VGA son:

- R (nivel de rojo, mínimo 0 voltios, máximo 0.7 voltios)

- G (nivel de verde, mínimo 0 voltios, máximo 0.7 voltios)

- B (nivel de azul, mínimo 0 voltios, máximo 0.7 voltios)

- Sincronismo horizontal (HSync, señal digital TTL que puede ser de 3.3 voltios)

- Sincronismo vertical (VSync, señal digital TTL que puede ser de 3.3 voltios)

Un flanco de bajada en HSync determina el fin de una línea horizontal de imagen y un flanco de bajada en VSync determina el fin de un fotograma, o de un cuadro). Hay unos "márgenes de seguridad" antes y después (front y back porch) de cada flanco de bajada de HSync de la misma forma que hay unos "márgenes de seguridad" (en forma de líneas en negro) antes y después (front y back porch) de cada flanco de bajada de VSync.

A continuación puede verse un diagrama de tiempos sobre cómo funcionan las señales de color y de sincronismo en un cable VGA:

(imagen copyright © 2017 Scott Larson, extraida de este artículo)


Si asumimos una resolución VGA estándar de 640 x 480 pixels a 60 Hz, tendremos los siguientes valores:
Pixel clockAnchura (pixels)Altura (líneas)
VisiblesNo visiblesVisiblesNo visibles
Front porchHSyncBack porchFront porchHSyncBack porch
25.175 MHz64016964848010233


Con estos valores vemos que, en efecto tenemos una tasa de refresco de:

$${1 \over {{{640+16+96+48} \over {25175000}} \times {\left(480+10+2+33\right)}}}=59.940\ Hz \approx 60\ Hz$$

Como los valores RGB son analógicos (entre 0 y 0.7 voltios), será necesario implementar un DAC (aunque sea de forma rudimentaria) por cada componente de color. Asumiremos una imagen de 8 colores con un bit por cada componente:

000001010011100101110111
negroazulverdecyanrojorosaamarilloblanco


De esta forma tendremos un DAC de 1 bit por cada color. Si la impedancia de entrada de las entradas RGB es de 75 Ohm vemos que con una sencilla resistencia de 270 Ohm en serie construimos un divisor de tensión que genera 0 voltios con un valor lógico 0 (0 voltios de salida digital) y 0.7 voltios con un valor lógico 1 (3.3 voltios de salida digital):

$${0 \times {75 \over {270+75}}}=0\ voltios$$
$${3.3 \times {75 \over {270+75}}}=0.71739\ voltios$$

Por cada píxel hay que almacenar 3 bits pero como las anchuras de bus de 3 bits son raras asumimos que cada pixel ocupa un byte del cual (por ahora) sólo se usan los 3 bits menos significativos.

Como el reloj de nuestra FPGA va a 50 MHz y el pixel clock es de 25.175 MHz se ha optado por asumir una imagen de 64 x 48 pixels. Esto es: cada pixel de la imagen en la memoria se corresponde con un cuadrado de 10 x 10 pixels en la pantalla. Usando esta aproximación, una imagen de 64 x 48 pixels necesita 64 x 48 = 3072 bytes de almacenamiento y el pixel clock se reduce de 25.175 MHz a 2.5175 MHz, que es una frecuencia más fácil de manejar por la FPGA.

Tenemos, por tanto, una memoria de 64 x 48 = 3072 bytes en la que cada byte posee los valores RGB en sus tres bits menos significativos. En cada línea de imagen se deben pintar 64 pixels (con un pixel clock de 2.5175 MHz en lugar de 25.175 MHz para que cada pixel de la memoria ocupe 10 pixels VGA de ancho) y cada línea debe ser pintada 10 veces de idéntica manera.

Ruta de datos y máquina de estados

A continuación puede verse una propuesta de ruta de datos a implementar para la interfaz VGA:


Leyenda:

PW = Pixel width
FP = Front porch (tiempo "en negro" antes del pulso HSync)
BP = Back porch (tiempo "en negro" después del puslto HSync)
LS = Line size, anchura total en pulsos de una línea en blanco de 640 pixels

El registro LineAddr almacena la dirección de comienzo de la línea actual. Es un registro que se incrementa de 64 en 64 y que ayuda a hacer el repetido de líneas (cada una de las 48 líneas de la imagen debe ser repetida 10 veces para renderizar las 480 líneas VGA).

El registro PixelAddr almacena la dirección de memoria del píxel actual que está siendo pintado. Este registro se inicializa siempre con el valor del registro LineAddr y es incrementado de uno en uno, 64 veces por cada línea (64 * 10 pixels = 640 pixels de anchura de la señal VGA).

El registro Pixel almacena el byte de cuyos tres bits más bajos se sacan las señales RGB de forma directa. Está gobernado por un multiplexor que decide si carga datos de la memoria o de la constante 0. Nótese que durante los márgenes de seguridad (front y back porch), durante los intervalos de sincronismo (tanto vertical como horizontal) y durante las lineas de retrazo (las que no se ven, de la 480 a la 524, 45 en total) debe emitirse una señal en negro por los pines RGB.

Se han habilitado además 3 contadores de propósito general que son utilizados por la máquina de estados para controlar los tiempos de cada fase de la señal VGA. La máquina de estados puede verse a continuación:

Los diferentes valores que se aplican a los contadores para controlar los tiempos del protocolo VGA se calculan teniendo en cuenta la frecuencia de reloj usada (50 MHz) y la frecuencia de pixel VGA (25.175 MHz). Por ejemplo el valor de PW (PIXEL_WIDTH) se usa para esperar un tiempo equivalente a 10 pixels:

$$PW={{1 \over 25175000} \times 50000000 \times 10}=20\ pulsos$$

De la misma forma se calcula el resto de valores:

$$FP={{1 \over 25175000} \times 50000000 \times 16}=32\ pulsos$$
$$HSYNC={{1 \over 25175000} \times 50000000 \times 96}=191\ pulsos$$
$$BP={{1 \over 25175000} \times 50000000 \times 48}=95\ pulsos$$
$$LS+BP={{1 \over 25175000} \times 50000000 \times \left(640+16\right)}=1303\ pulsos$$

Estos valores teóricos son luego ajustados ya que en esas ecuaciones sólo se tienen en cuenta los estados de espera y no se contabiliza el resto de estados, que también consumen ciclos de reloj.

Una técnica muy usada para compensar los tiempos de espera en las máquinas de estado es incluir estados que no hacen nada con transiciones vacías (épsilon). Como se puede ver en la imagen anterior, el estado 14 se alcanza siempre al final del renderizado de una línea horizontal VGA. Si Contador3 = 0 entonces se ha terminado una repetición de 10 líneas consecutivas y toca avanzar de línea, pero si Contador3 <> 0 entonces hay que volver a pintar la línea actual (la que está apuntada por el registro LA, LineAddress). En este último caso, la máquina de estados pasa por tres estados que no hacen nada (26, 27 y 28) pero que se han colocado ahí para que no haya diferencia entre la cantidad de ciclos que tarda una línea repetida y la cantidad de ciclos que tarda una línea nueva.

Buffers de salida para los sincronismos

Aunque la máquina de estados ya genera directamente las señales HSync y VSync, es necesario hacerlas pasar por un latch (biestable D) para garantizar que quedan libres de gitches.

    -- acondicionador de señales HSync y VSync
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            HSyncQBus <= HSyncDBus;
        end if;
    end process;

    HSyncDBus <= HSyncIn;
    HSyncOut <= HSyncQBus;

    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            VSyncQBus <= VSyncDBus;
        end if;
    end process;

    VSyncDBus <= VSyncIn;
    VSyncOut <= VSyncQBus;


Si no se colocasen estos latches y se cableasen directamente las salidas HSync y VSync a los pines de la PFGA podría ocurrir que transiciones intermedias espúreas entre estados generasen pulsos "fantasma" en dichas salidas. Haciendo pasar a estas señales por un latch se garantiza una carga atrasada y limpia que disminuye la probabilidad de que se produzcan estos glitches.

Por claridad, estos latches no se muestran en el diagrama con la ruta de datos mostrado anteriormente.

Código fuente e implementación

La máquina de estados se ha implementado, como otras veces, siguiendo el modelo estándar de máquina de Moore:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity FSM is
    port (
        Clk               : in std_logic;
        Reset             : in std_logic;
        Counter1IsZero    : in std_logic;
        Counter2IsZero    : in std_logic;
        Counter3IsZero    : in std_logic;
        LineAddrIsVisible : in std_logic;
        Counter2IsVSync   : in std_logic;
        Counter1Mux       : out std_logic_vector(2 downto 0);
        Counter2Mux       : out std_logic_vector(1 downto 0);
        Counter3Mux       : out std_logic_vector(1 downto 0);
        LineAddrMux       : out std_logic_vector(1 downto 0);
        PixelAddrMux      : out std_logic_vector(1 downto 0);
        PixelMux          : out std_logic_vector(1 downto 0);
        HSync             : out std_logic;
        VSync             : out std_logic
    );
end entity;

architecture A of FSM is
    signal DBus : std_logic_vector(4 downto 0);
    signal QBus : std_logic_vector(4 downto 0);
begin
    -- lógica de estado siguiente
    DBus <= "00000" when (Reset = '1') else
            "00001" when ((QBus = "00000") or ((QBus = "11101") and (Counter2IsZero = '1'))) else
            "00010" when ((QBus = "00001") or (QBus = "11100") or (QBus = "11001")) else
            "00011" when ((QBus = "00010") or ((QBus = "00110") and (Counter2IsZero = '0'))) else
            "00100" when ((QBus = "00011") or ((QBus = "00100") and (Counter1IsZero = '0'))) else
            "00101" when ((QBus = "00100") and (Counter1IsZero = '1')) else
            "00110" when (QBus = "00101") else
            "00111" when ((QBus = "00110") and (Counter2IsZero = '1')) else
            "01000" when ((QBus = "00111") or ((QBus = "01000") and (Counter1IsZero = '0'))) else
            "01001" when ((QBus = "01000") and (Counter1IsZero = '1')) else
            "01010" when ((QBus = "01001") or ((QBus = "01010") and (Counter1IsZero = '0'))) else
            "01011" when ((QBus = "01010") and (Counter1IsZero = '1')) else
            "01100" when ((QBus = "01011") or ((QBus = "01100") and (Counter1IsZero = '0'))) else
            "01101" when ((QBus = "01100") and (Counter1IsZero = '1')) else
            "01110" when (QBus = "01101") else
            "01111" when ((QBus = "01110") and (Counter3IsZero = '1')) else
            "10000" when (QBus = "01111") else
            "10001" when ((QBus = "10000") and (LineAddrIsVisible = '0')) else
            "10010" when ((QBus = "10001") or ((QBus = "11101") and (Counter2IsZero = '0'))) else
            "10011" when ((QBus = "10010") or ((QBus = "10011") and (Counter1IsZero = '0'))) else
            "10100" when ((QBus = "10011") and (Counter1IsZero = '1')) else
            "10101" when ((QBus = "10100") or ((QBus = "10101") and (Counter1IsZero = '0'))) else
            "10110" when ((QBus = "10101") and (Counter1IsZero = '1')) else
            "10111" when ((QBus = "10110") or ((QBus = "10111") and (Counter1IsZero = '0'))) else
            "11000" when ((QBus = "10111") and (Counter1IsZero = '1')) else
            "11001" when ((QBus = "10000") and (LineAddrIsVisible = '1')) else
            "11010" when ((QBus = "01110") and (Counter3IsZero = '0')) else
            "11011" when (QBus = "11010") else
            "11100" when (QBus = "11011") else
            "11101" when (QBus = "11000") else
            "00000";

    -- lógica de salida
    Counter1Mux <= "001" when (QBus = "00011") else   -- cargar anchura de pixel
                   "010" when (QBus = "00111") else   -- cargar front porch
                   "011" when ((QBus = "01001") or (QBus = "10100")) else   -- cargar HSYNC
                   "100" when ((QBus = "01011") or (QBus = "10110")) else   -- cargar back porch
                   "101" when (QBus = "10010") else   -- cargar anchura línea + front porch
                   "110" when ((QBus = "00100") or (QBus = "01000") or (QBus = "01010") or (QBus = "01100") or (QBus = "10011") or (QBus = "10101") or (QBus = "10111")) else   -- decrementar
                   "000";
    Counter2Mux <= "01" when (QBus = "00010") else   -- cargar 64 (la anchura de línea)
                   "10" when ((QBus = "00101") or (QBus = "11000")) else   -- decrementar
                   "11" when (QBus = "10001") else   -- cargar 45 (cantidad de líneas de blanqueo)
                   "00";
    Counter3Mux <= "01" when ((QBus = "00001") or (QBus = "11001")) else   -- cargar 10 (líneas a repetir)
                   "10" when (QBus = "01101") else   -- decrementar
                   "00";
    LineAddrMux <= "01" when (QBus = "00001") else   -- cargar 0
                   "10" when (QBus = "01111") else   -- incrementar en 64
                   "00";
    PixelAddrMux <= "01" when (QBus = "00010") else
                    "10" when (QBus = "00101") else
                    "00";
    PixelMux <= "01" when ((QBus = "00001") or (QBus = "00111")) else  -- cargar 0
                "10" when (QBus = "00011") else                        -- cargar dato de la ROM
                "00";
    HSync <= '0' when ((QBus = "01001") or (QBus = "01010") or (QBus = "10100") or (QBus = "10101")) else
             '1';
    VSync <= '0' when (((QBus = "10010") or (QBus = "10011") or (QBus = "10100") or (QBus = "10101") or (QBus = "10110") or (QBus = "10111") or (QBus = "11000") or (QBus = "11101")) and (Counter2IsVSync = '1')) else
             '1';

    -- biestables
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            QBus <= DBus;
        end if;
    end process;
end architecture;


El resto de la implementación consiste en codificar lo registros y los multiplexores:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity AllButFSM is
    port (
        Clk               : in std_logic;
        LineAddrMux       : in std_logic_vector(1 downto 0);
        PixelAddrMux      : in std_logic_vector(1 downto 0);
        PixelMux          : in std_logic_vector(1 downto 0);
        PixelValue        : out std_logic_vector(7 downto 0);
        HSyncIn           : in std_logic;
        HSyncOut          : out std_logic;
        VSyncIn           : in std_logic;
        VSyncOut          : out std_logic;
        Counter1Mux       : in std_logic_vector(2 downto 0);
        Counter1IsZero    : out std_logic;
        Counter2Mux       : in std_logic_vector(1 downto 0);
        Counter2IsZero    : out std_logic;
        Counter2IsVSync   : out std_logic;
        Counter3Mux       : in std_logic_vector(1 downto 0);
        Counter3IsZero    : out std_logic;
        LineAddrIsVisible : out std_logic
    );
end entity;

architecture A of AllButFSM is
    component Rom is
        generic (
            Log2NumRows : integer := 12   -- 4096 bytes
        );
        port (
            AddressIn : in std_logic_vector((Log2NumRows - 1) downto 0);
            DataOut   : out std_logic_vector(7 downto 0)
        );
    end component;
    component Counter1 is
        port (
            Clk    : in std_logic;
            Mux    : in std_logic_vector(2 downto 0);
            IsZero : out std_logic
        );
    end component;
    component Counter2 is
        port (
            Clk     : in std_logic;
            Mux     : in std_logic_vector(1 downto 0);
            IsZero  : out std_logic;
            IsVSync : out std_logic
        );
    end component;
    component Counter3 is
        port (
            Clk     : in std_logic;
            Mux     : in std_logic_vector(1 downto 0);
            IsZero  : out std_logic
        );
    end component;
    signal LineAddrDBus : std_logic_vector(11 downto 0);   -- 12 bits = 4096 bytes (sólo se usan los 64 * 48 = 3072 primeros bytes)
    signal LineAddrQBus : std_logic_vector(11 downto 0);
    signal PixelAddrDBus : std_logic_vector(11 downto 0);
    signal PixelAddrQBus : std_logic_vector(11 downto 0);
    signal RomOut : std_logic_vector(7 downto 0);
    signal PixelDBus : std_logic_vector(7 downto 0);
    signal PixelQBus : std_logic_vector(7 downto 0);
    signal HSyncDBus : std_logic;
    signal HSyncQBus : std_logic;
    signal VSyncDBus : std_logic;
    signal VSyncQBus : std_logic;
    constant FIRST_NO_VISIBLE_LINE_ADDRESS : integer := 3072;     -- Dirección de memoria de la primera línea no visible
begin
    -- dirección de inicio de la línea actual de pantalla
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            LineAddrQBus <= LineAddrDBus;
        end if;
    end process;

    LineAddrDBus <= std_logic_vector(to_signed(0, 12)) when (LineAddrMux = "01") else
                    std_logic_vector(to_signed(to_integer(signed(LineAddrQBus)) + 64, 12)) when (LineAddrMux = "10") else    -- 64 bytes (pixels) por línea
                    LineAddrQBus;
    LineAddrIsVisible <= '0' when (to_integer(unsigned(LineAddrQBus)) = FIRST_NO_VISIBLE_LINE_ADDRESS) else
                         '1';

    -- dirección del actual pixel
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            PixelAddrQBus <= PixelAddrDBus;
        end if;
    end process;

    PixelAddrDBus <= LineAddrQBus when (PixelAddrMux = "01") else
                     std_logic_vector(to_signed(to_integer(signed(PixelAddrQBus)) + 1, 12)) when (PixelAddrMux = "10") else    -- 1 byte = 1 pixel
                     PixelAddrQBus;

    -- ROM con la imagen de 64 x 48 pixels (1 byte por pixel, 64 * 48 = 3072 bytes)
    R : Rom generic map (
        Log2NumRows => 12
    )
    port map (
        AddressIn => PixelAddrQBus,
        DataOut => RomOut
    );

    -- buffer de salida de la ROM (pixel actual)
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            PixelQBus <= PixelDBus;
        end if;
    end process;

    PixelDBus <= RomOut when (PixelMux = "10") else
                 std_logic_vector(to_signed(0, 8)) when (PixelMux = "01") else
                 PixelQBus;
    PixelValue <= PixelQBus;

    -- contadores
    C1 : Counter1 port map (
        Clk => Clk,
        Mux => Counter1Mux,
        IsZero => Counter1IsZero
    );

    C2 : Counter2 port map (
        Clk => Clk,
        Mux => Counter2Mux,
        IsZero => Counter2IsZero,
        IsVSync => Counter2IsVSync
    );

    C3 : Counter3 port map (
        Clk => Clk,
        Mux => Counter3Mux,
        IsZero => Counter3IsZero
    );

    -- acondicionador de señales HSync y VSync
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            HSyncQBus <= HSyncDBus;
        end if;
    end process;

    HSyncDBus <= HSyncIn;
    HSyncOut <= HSyncQBus;

    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            VSyncQBus <= VSyncDBus;
        end if;
    end process;

    VSyncDBus <= VSyncIn;
    VSyncOut <= VSyncQBus;
end architecture;


La imagen se aloja en una ROM cuyos datos se especifican directamente en el código fuente de Rom.vhd. Para generar la imagen en formato VHDL usando el GIMP se hicieron los siguientes pasos:

- Se creó una nueva imagen de 64 x 48 pixels.

- Menú Ventanas -- Diálogos empotrables -- Paletas -- Botón derecho dentro del listado de paletas -- Importar paleta -- En "Seleccionar origen" se marcó "Archivo de la paleta" y se seleccionó el fichero "vga_fpga.gpl" que se ha incluido dentro del proyecto. Esto creó dentro del GIMP una nueva paleta de 8 colores que se correspondía con los 8 colores que genera nuestra FPGA.

- Se trabajó la imagen de 64x48 con esa paleta.

- Cuando se terminó de trabajar con la imagen, menú Imagen -- Modo -- Indexado -- En "Mapa de colores" se marcó la opción "Usar paleta personal", se seleccionó la paleta acabante de crear a partir del fichero "vga_fpga.gpl", se desmarcó la opción "Eliminar los colores sin usar de la paleta final" y "Aceptar".

- Menú Archivo -- Exportar como -- Se seleccionó como tipo de archivo "Cabecera de código fuente en C (.h)" -- Exportar

Esto creó un fichero .h con un array con el mapa de color (la paleta) y otro array de 3072 bytes con la imagen completa. Afortunadamente el formato de datos de array de C y VHDL es relativamente similar por lo que simplemente hubo que trabajar un poco con el comando "sed" para adaptar los datos. Por ejemplo, un bloque de texto de esta forma:

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,
7,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,
7,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,3,
3,0,0,3,3,3,0,0,0,0,3,3,3,0,0,0,


Puede convertirse a formato de datos VHDL de esta forma:

$ cat datos.txt | sed -e 's/0/x"00"/g' | sed -e 's/1/x"01"/g' | sed -e 's/2/x"02"/g' | sed -e 's/3/x"03"/g' | sed -e 's/4/x"04"/g' | sed -e 's/5/x"05"/g' | sed -e 's/6/x"06"/g' | sed -e 's/7/x"07"/g'


Generando la salida:
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"07",
x"07",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"03",x"03",
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"07",
x"07",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"03",x"00",x"00",
x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"00",x"03",x"03",x"03",
x"03",x"00",x"00",x"03",x"03",x"03",x"00",x"00",x"00",x"00",x"03",x"03",x"03",x"00",x"00",x"00",


Que es fácilmente incluible en un fichero VHDL como un array (ver Rom.vhd).

Circuito

El circuito externo a la FPGA sólo requiere las líneas de reset, de reloj y de sincronismo conectadas directamente y cada una de las tres líneas de componentes de color (RGB) conectada con una resistencia en serie de 270 Ohm.



El resultado:





Como siempre, todo el código fuente está disponible en la sección soft.

[ añadir comentario ] ( 692 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2405 )

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Siguiente> >>