Luz de fogata para el belén con CPLD 
Un año más, decorando el belén con nuevos inventos. En esta ocasión he vuelto a los orígenes y he implementado una luz, pero esta vez una luz especial que simule un fuego encendido utilizando un CPLD.

Introducción

A la hora de simular el crepitar de una llamas se ha optado por hacer que un led varíe de luminosidad de forma aleatoria varias veces por segundo.



Con una luz que varíe su intensidad lumínica varias veces por segundo (entre 5 y 6 veces por segundo, por ejemplo) más una buena escenografía (color de la luz, decorados, etc.) se consigue un razonable efecto de fuego encendido.

Diseño técnico

Como vamos a hacer el diseño utilizando un circuito totalmente digital, la intensidad lumínica se modulará utilizando una señal cuadrada modulada en anchura (PWM). Cuanto mayor sea el semiciclo a 1 y menor el semiciclo a 0 más brillará el led y a la inversa: cuanto mayor sea el semiciclo a 0 y menor el semiciclo a 1 menos brillará.



Si hacemos que la frecuencia sea lo suficientemente alta no se apreciará ningún tipo de parpadeo y la luz se percibirá como que brilla de forma continuada pero con diferente intensidad. El CPLD utilizado tiene conectado un reloj a 50 MHz, por tanto usando un contador de 10 bits estándar conseguiremos un desbordamiento a una frecuencia de

$${50000000 \over {2^{10}}} = 48828.125 Hz$$

Si el valor de este contador lo comparamos con un valor determinado, el resultado de esta comparación será la salida PWM que necesitamos para el led de nuestra fogata:



El valor de intensidad lo generaremos mediante un LFSR maximal de 10 bits. Dicho LFSR ha sido utilizado en otros montajes anteriores:



Genera una secuencia maximal de 1023 valores pseudoaleatorios (el valor 0 no aparece en la secuencia) que se puede usar como valor de intensidad lumínica:



En este circuito el bloque combinacional "OP" es el que implementa el polinomio maximal de 10 bits. A continuación sólo falta implementar la temporización. Como queremos el que valor de intensidad lumínica cambie unas 5 ó 6 veces por segundo, bastará con poner un contador estándar de:

$$\lceil log_2\left({50000000 \over 6}\right) \rceil = 23 bits$$

Con un contador de 23 bits a 50 MHz tendremos una frecuencia de desbordamiento de casi 6 veces por segundo:

$${50000000 \over {2^{23}}} = 5.96 Hz$$

A continuación puede verse cómo quedaría el diagrama completo:



El bloque combinacional "CM" (Control del Multiplexor) se encarga de controlar la selección del multiplexor del LFSR en función del timer contador de 23 bits y del valor del propio LFSR. La tabla de verdad de este bloque sería la siguiente:

EntradasSalidas
Timer == 0LFSR == 0MUX
X1"1"
10Salida de OP
00Salida del LFSR


Cuando el valor del LFSR es 0, lo que hace es seleccionarla entrada "1" del multiplexor para que el LFSR se cargue con un valor distinto de cero (el 1) y poder así arrancar la generación de números aleatorios. Cuando el timer (contador de 23 bits) se desborda (pasa por cero) el LFSR se carga con el siguiente valor de la secuencia de números pseudoaleatorios y el resto del tiempo (valor del contador de 23 bits diferente de 0) el registro LFSR permanece inalterado.

Como se puede apreciar, se ha prescindido de circuitería de reset. Teniendo en cuenta que el objetivo es minimizar la circuitería y que los fabricantes siempre te garantizan que en el arranque, todos los biestables están a 0, se puede "abusar" de esta característica y ahorrar así parte de la circuitería de reset.

Posibles mejoras

Como se puede apreciar, del contador que hace de timer (el de 23 bits) sólo nos interesa cuando pasa por un valor concreto (el 0), no es como el contador que se utiliza para comparar con el LFSR y generar la señal PWM. Teniendo esto presente, dicho contador de 23 bits podría implementarse utilizando también un LFSR maximal de 23 bits: en lugar de un sumador, se puede implementar un bloque combinacionar consistente tan solo en una única puerta xor (ver polinomio a aplicar aquí), lo que supone un ahorro considerable en circuitería.

Hay que tener presente que los LFSRs no pasan nunca por cero, por lo que habría que elegir cualquier otro valor (cualquiera) como valor de "desbordamiento" (el valor 1, por ejemplo).

Implementación

A continuación puede verse la implementación de este diseño en VHDL.

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

entity ChristmasFire is
    port (
        Clk : in std_logic;
        Led : out std_logic
    );
end entity;

architecture RTL of ChristmasFire is
    signal CounterDBus : std_logic_vector(9 downto 0);
    signal CounterQBus : std_logic_vector(9 downto 0);
    signal LFSRDBus : std_logic_vector(9 downto 0);
    signal LFSRQBus : std_logic_vector(9 downto 0);
    signal TimerDBus : std_logic_vector(22 downto 0);
    signal TimerQBus : std_logic_vector(22 downto 0);
    signal TimerOverflow : std_logic;
begin
    -- pwm counter
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            CounterQBus <= CounterDBus;
        end if;
    end process;
    
    CounterDBus <= std_logic_vector(signed(CounterQBus) + to_signed(1, 10));
    
    -- lfsr
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            LFSRQBus <= LFSRDBus;
        end if;
    end process;
    
    LFSRDBus <= std_logic_vector(to_signed(1, 10)) when (signed(LFSRQBus) = to_signed(0, 10)) else
                ((LFSRQBus(3) xor LFSRQBus(0)) & LFSRQBus(9 downto 1)) when (TimerOverflow = '1') else
                LFSRQBus;
    
    -- timer
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            TimerQBus <= TimerDBus;
        end if;
    end process;
    
    TimerDBus <= std_logic_vector(signed(TimerQBus) + to_signed(1, 23));
    TimerOverflow <= '1' when (signed(TimerQBus) = to_signed(0, 23)) else
                     '0';
    
    -- output
    Led <= '1' when (signed(CounterQBus) > signed(LFSRQBus)) else
           '0';
end architecture;


Se trata de un único fichero que puede descargarse desde la sección soft.



¡Feliz Navidad a todos!

[ añadir comentario ] ( 1076 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 9219 )
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 ] ( 1118 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2530 )
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 ] ( 2336 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2536 )
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 ] ( 1267 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2497 )
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 ] ( 4246 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 3768 )

<< <Anterior | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Siguiente> >>