Implementación sobre una FPGA de un transmisor de radio en la banda comercial de FM 
En anteriores entradas se realizó la implementación, tanto de un transmisor FM como de un conversor analógico digital delta-sigma, sobre FPGA y como proyectos separados. A lo largo de esta entrada se aborda el desarrollo de un transmisor FM en la banda de la radio comercial para transmitir música, combinando ambos proyectos en uno solo.

Conversión analógica digital delta-sigma

Como se puede comprobar en el post anterior relacionado, la conversión analógica digital de tipo delta-sigma tiene la ventaja de requerir muy pocos componentes externos: un comparador analógico (para lo que puede usarse un amplificador operacional normal), un condensador y una resistencia; y permite implementar un ADC de precisión arbitraria (la precisión sólo está limitada por los recursos disponibles en la FPGA).



Recordemos que la conversión analógico digital de tipo delta-sigma lo que hace es, mediante un biestable de tipo D, hacer que la salida que carga el condensador C a través de la resistencia R "siga" a la entrada analógica:


- Si la entrada + del comparador está por encima de la entrada -, el comparador emite un 1, que es cargado en el siguiente ciclo de reloj por el biestable de entrada, haciendo que la salida que va a la resistencia R tome el valor de 1 y trate de "acercar" el valor de la entrada - del comparador al valor de la entrada + del comparador.

- Si la entrada + del comparador está por debajo de la entrada -, el comparador emite un 0, que es cargado en el siguiente ciclo de reloj por el biestable de entrada, haciendo que la salida que va a la resistencia R tome el valor de 0 y trate de "acercar" (esta vez hacia "abajo") el valor de la entrada - del comparador al valor de la entrada + del comparador.


Como se puede ver, este comportamiento hace que para valores próximos a 0 voltios en la entrada + del comparador, el biestable de entrada emite muchos más 0s que 1s, mientras que para valores próximos a 3.3 voltios en la entrada + del comparador, el biestable de entrada emite muchos más 1s que 0s. También se puede ver que para valores próximos a Vcc / 2 = 1.65 voltios, el biestable emitirá una cantidad aproximadamente igual de 0s que de 1s.

Si lo que hacemos ahora es asociar al valor 0 de la salida del biestable, el valor numérico -1, y, al valor 1 de la salida del biestable, el valor numérico +1, lo que tenemos es un circuito digital que emitirá:


- Más +1 que -1 para valores próximos a 3.3 voltios.

- Más -1 que +1 para valores próximos a 0 voltios.

- Una cantidad aproximadamente igual de -1 y +1 para valores próximos a 1.65 voltios.


Si vamos acumulando estos -1 y +1 sobre un acumulador que se pone a cero cada cierto tiempo, lo que tendremos es que en dicho acumulador tendremos un valor proporcional al voltaje de entrada del ADC:


- Valores próximos a 3.3 voltios, al generar más +1 que -1, dan como resultado un valor de conversión muy alto y positivo.

- Valores próximos a 0 voltios, al generar más -1 que +1, dan como resultado un valor de conversión muy bajo y negativo.

- Valores próximos a 1.65 voltios, al generar una cantidad aproximadamente similar de -1 y de +1, dan como resultado un valor de conversión próximo a 0.


Debido a esta necesidad de contar -1s y +1s necesitamos sobremuestreo: Si queremos obtener una resolución de 16 bits, eso significa que el valor máximo de conversión tendrá que ser -32767 mientras que el valor mínimo de conversión tendrá que ser -32768. Si asumimos una escala simétrica tendremos un valor entre -32767 y +32767, eso significa que debemos hacer 32767 sumas (de -1s y +1s) antes de leer el valor de conversión. Dichas sumas las tenemos que hacer en un registro, que llamaremos "acumulador de conversión".

Para el caso de 16 bits necesitaríamos un contador de 15 bits que, en el momento de que valga 0 haga que el "acumulador de conversión" copie su valor en el registro de salida del ADC y se ponga a cero, y, durante los 32767 pulsos restantes (hasta el siguiente desbordamiento), se sumen los -1s y +1s que van entrando.

Así vemos que, por ejemplo, para 12 MHz y 16 bits de resolución, como necesitaríamos un contador de 15 bits, al final la frecuencia de muestreo podrá ser, como máximo de:

$$ {12000000 \over {2^{15}}} = 366.21 \: Hz $$

Por ejemplo, si queremos hacer una conversión en calidad CD necesitaríamos un reloj de sistema (sobremuestreo) de:

$$ {44100 \times {2^{15}}} = 1445068800 \: Hz $$

En el caso que nos ocupa se ha decidido implementar el transmisor en una FPGA MAX10 de las que viene en una placa MAX1000 de Arrow, a 12 MHz, cuyo reloj puede ser subido mediante PLLs hasta unos 400 MHz. Debido a esta limitación se ha decidido subir el reloj a 300 MHz y, para trabajar con esa frecuencia de sobremuestreo, se han tenido que bajar un poco las especificaciones del ADC, usando un contador de 13 bits y un acumulador de conversión de 14 bits. De esta forma tenemos una frecuencia de muestreo de:

$$ {300000000 \over {2^{13}}} = 36621.09375 \: Hz $$

Por lo que el ancho de banda es de unos 18 KHz (buen ancho de banda para música) y la resolución de conversión es de 14 bits (valores de conversión entre -8191 y +8191). No es calidad CD pero tampoco está mal.

Consideraciones entorno a la entrada de sonido analógica

Hay que tener en cuenta que las señales de sonido tal cual salen de un amplificador, son señales simétricas (con semiciclos positivos y negativos) mientras que nuestro ADC mide voltajes entre 0 y 3.3 voltios (no mide voltajes negativos). Es necesario, por tanto, acondicional la señal de sonido de entrada para que quede "desplazada" hacia arriba y un valor de 0 voltios de entrada se traduzca en 1.65 voltios a la entrada + del comparador. Es por esto por lo que se coloca el divisor de tensión entre 3.3 y 0 voltios a la entrada de audio, que desplaza el "0" de la señal de sonido hasta los 1.65 voltios.



El nivel de señal que entrega la salida de auriculares de un ordenador es suficiente para la entrada del ADC y no requiere amplificación adicional, al menos para esta prueba.

Generador de señal de antena

La frecuencia de transmisión elegida es 87.5 MHz (el extremo inferior de la banda de radiodifusión de sonido). La señal de antena que se quiere generar debe estar centrada, por tanto, en dicha frecuencia y dicha frecuencia deberá variarse en un rango máximo de +-75 KHz (estándar de radiodifusión). Para la generación de frecuencias arbitrarias que sean inferiores a la frecuencia de reloj de un sistema digital lo lógico es utilizar un acumulador de fase.

Un acumulador de fase no es más que un registro que se incrementa en un valor constante (no tiene por qué incrementarse de 1 en 1) con desbordamiento. Por ejemplo, si a partir de un reloj de 300 MHz queremos generar un reloj de 75 MHz lo que podemos hacer es incrementar un registro de 2 bits de 1 en 1 a 300 MHz:

...
Pulso: 00 --> 01
Pulso: 01 --> 10
Pulso: 10 --> 11
Pulso: 11 --> 00
Pulso: 00 --> 01
Pulso: 01 --> 10
Pulso: 10 --> 11
Pulso: 11 --> 00
...

En este ejemplo se puede ver que si la frecuencia de pulso es de 300 MHz, la frecuencia del bit 0 será de ${300 \over 2} = 150 \: MHz$ mientras que la frecuencia del bit 1 será de ${150 \over 2} = 75 \: MHz$. Un acumulador de fase con incrementos potencia de 2 es, formalmente, un divisor de frecuencia. Si se aplica este mismo principio para registros con mayor cantidad de bits y usando incrementos arbitrarios, conseguimos frecuencias diferentes.

Asumamos que la señal de salida de nuestro "oscilador" será siempre el bit más significativo de un registro de 16 bits y nuestro reloj va a 300 MHz. Eso significará que, incrementando el registro de 1 en 1, el bit más significativo cambiará a razón de:

$$ {300000000 \over {2^N}} = 4577 \: Hz $$

Si en lugar de incrementar el registro de 1 en 1, lo incrementamos de 2 en 2, la cantidad de pulsos que tarde en desbordarse el registro será menor, por tanto, la frecuencia del bit más significativo será mayor, es decir, la frecuencia del bit más significativo es proporcional al valor de incremento del registro. El máximo valor de incremento será $2^{N-1}$ para N bits (es decir un 1 seguido de N-1 ceros) que hará que el registro se comporte de la siguiente manera:

...
Pulso: 0000000000000000 --> 1000000000000000
Pulso: 1000000000000000 --> 0000000000000000
Pulso: 0000000000000000 --> 1000000000000000
Pulso: 1000000000000000 --> 0000000000000000
...

En este caso extremo tenemos que la frecuencia del bit más significativo es de 150 MHz (para una frecuencia de pulso de 300 MHz). De forma genérica tenemos que el valor de incremento para una frecuencia dada puede calcularse de la siguiente forma:

$$ I = {{f_{deseada}} \over 300000000} \times 2^{N} $$

En nuestro caso, si queremos emitir a 87.5 MHz hay que usar un valor de N=54 bits para que el valor de incremento no sea fraccionario:

$$ I_{central} = {87500000 \over 300000000} \times 2^{54} = 5254199565265579 $$

Ese valor es el que habría que usar como valor de incremento en cada pulso de reloj para que en el bit más significativo del registro de 54 bits (bit 53) tengamos una señal a 87.5 MHz. Nótese que para valores de incremento que no sean potencias de 2, obtendremos señales no cuadradas o de fase algo irregular (en el anterior post dedicado a la transmisión FM se profundiza en este tema), pero para el caso que nos ocupa, la "calidad" de la señal resultante no es significativa, lo importante es que la frecuencia fundamental sea la correcta. Para hacer modulación en frecuencia hemos de modificar esta frecuencia en el rango de -75 KHz y +75 KHz, lo que nos genera los siguiente incrementos mínimos y máximos:

$$ I_{min} = {(87500000 - 75000) \over 300000000} \times 2^{54} = 5249695965638208 $$

$$ I_{max} = {(87500000 + 75000) \over 300000000} \times 2^{54} = 5258703164892949 $$

Si calculamos la diferencia entre los incrementos y la dividimos entre dos nos dará el valor 4503599627370.5 que es la amplitud máxima que deberá tener la señal del ADC para que modifique el valor del incremento y que este, a su vez, genere una variación máxima de +- 75 KHz en la frecuencia portadora.

Como la salida del ADC da valores entre -8191 y +8191 a este valor hay que multiplicarle el valor ${4503599627370.5 \over 8191} = 549822930$ para que valores próximos a -8191 en la salida del ADC generen una portadora de 87.5 MHz - 75 KHz = 87.425 MHz y valores próximos a +8191 en la salida del ADC generen una portadora de 87.5 MHz + 75 KHz = 87.575 MHz. El valor del incremento del acumulador de fase que genera la señal de la antena será el siguiente:

$$ I = I_{central} + (ADC \times 549822930) = 5254199565265579 + (ADC \times 549822930) $$

A continuación puede verse el código fuente completo (cabe en un único fichero VHDL).

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


entity MAX10FMTransmitter is
    port (
        Clk              : in std_logic;
        AnalogComparator : in std_logic;
        PWM              : out std_logic;
        Antenna          : out std_logic;
        Led              : out std_logic_vector(7 downto 0)
    );
end entity;

architecture A of MAX10FMTransmitter is
    component PLL
        port (
            inclk0 : in std_logic := '0';
            c0        : out std_logic 
        );
    end component;
    signal Clk300 : std_logic;
    signal ADCFollowerD : std_logic;
    signal ADCFollowerQ : std_logic;
    signal ADCScalerD : std_logic_vector(12 downto 0);
    signal ADCScalerQ : std_logic_vector(12 downto 0);
    signal ADCAccumulatorD : std_logic_vector(13 downto 0);
    signal ADCAccumulatorQ : std_logic_vector(13 downto 0);   -- signed value: -8191 to +8191
    signal ADCOutputD : std_logic_vector(13 downto 0);
    signal ADCOutputQ : std_logic_vector(13 downto 0);
    signal PhaseAccumulatorD : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorQ : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorIncD : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorIncQ : std_logic_vector(53 downto 0);
    -- increment for phase accumulator to transmit at 87.5 MHz: (87500000 / 300000000) * (2^54) = 5254199565265579
    --constant TxCentralFrequencyInc : integer := 5254199565265579;   -- must fit in 54 bits
    constant TxCentralFrequencyIncL : integer := 2863311531;   -- 32 bits
    constant TxCentralFrequencyIncH : integer := 1223338;      -- 22 bits
    constant TXCentralFrequencyInc : signed(53 downto 0) := to_signed(TxCentralFrequencyIncH, 22) & to_signed(TxCentralFrequencyIncL, 32);
    -- gain for ADC output 
    constant ADCGain : integer := 549822930;   -- ADCGain needs 30 bits
begin
    -- PLL to obtain 300 MHz from external 12 MHz
    P : PLL port map (
        inclk0 => Clk,
        c0 => Clk300
    );

    -- ADC
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCFollowerQ <= ADCFollowerD;
        end if;
    end process;
    
    ADCFollowerD <= AnalogComparator;
    PWM <= ADCFollowerQ;
    
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCScalerQ <= ADCScalerD;
        end if;
    end process;
    
    ADCScalerD <= std_logic_vector(unsigned(ADCScalerQ) + 1);

    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCAccumulatorQ <= ADCAccumulatorD;
        end if;
    end process;

    ADCAccumulatorD <= std_logic_vector(to_signed(0, 14)) when (unsigned(ADCScalerQ) = 0) else
                       std_logic_vector(signed(ADCAccumulatorQ) + to_signed(1, 14)) when (ADCFollowerQ = '1') else
                       std_logic_vector(signed(ADCAccumulatorQ) - to_signed(1, 14)) when (ADCFollowerQ = '0') else
                       ADCAccumulatorQ;
    
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCOutputQ <= ADCOutputD;
        end if;
    end process;
    
    ADCOutputD <= ADCAccumulatorQ when (unsigned(ADCScalerQ) = 0) else
                  ADCOutputQ;
    Led <= ADCOutputQ(13 downto 6);
    
    -- phase accumulator
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            PhaseAccumulatorQ <= PhaseAccumulatorD;
        end if;
    end process;
        
    PhaseAccumulatorD <= std_logic_vector(unsigned(PhaseAccumulatorQ) + unsigned(PhaseAccumulatorIncQ));
    Antenna <= PhaseAccumulatorQ(53);
    
    -- phase accumulator increment control (output frequency control)
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            PhaseAccumulatorIncQ <= PhaseAccumulatorIncD;
        end if;
    end process;
    
    -- 14 bits * 40 bits = 54 bits
    PhaseAccumulatorIncD <= std_logic_vector((signed(ADCAccumulatorQ) * to_signed(ADCGain, 40)) + TxCentralFrequencyInc) when (unsigned(ADCScalerQ) = 0) else
                            PhaseAccumulatorIncQ;
end architecture;


El bit 53 del registro acumulador de fase se saca por un pin de la FPGA y en dicho pin se puede colocar un simple trozo de cable. No es necesario hacer ningún circuito que acondicione la señal de salida.



Si a corta distancia del circuito ponemos un receptor de radio FM comercial sintonizado a 87.5 MHz podremos escuchar la señal que está leyendo el ADC de la FPGA y que está siendo transmitida en FM.



Código fuente disponible en la sección soft.

[ añadir comentario ] ( 8 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 5 )
Efectos de sonido con un CPLD 
¿Se pueden generar efectos de sonido rudimentarios sin un chip de sonido y utilizando muy pocas macroceldas de un CPLD o una FPGA?

Motivación

En la anterior entrada del blog se realizó un pequeño proyecto para generar dos efectos de sonido utilizando un sencillo chip PSG, el SN76489. Los sonidos generados fueron muy buenos pero a nivel hardware, como el objetivo del montaje es una futura integración en un pequeño robot con mando a distancia, me encontré con que, el hecho de tener que cablear todo un bus de 8 bits junto con los pines OE y READY, consumía muchos pines del STM32 (el robot consume ya GPIO y PWM para las ruedas, UART para la comunicación e I2C para una pequeña pantalla OLED).

Una primera aproximación válida sería un conversor serie a paralelo de tipo I2C o algo así pero instalar DOS chips sólo para generar dos ruidos rudimentarios me pareció excesivo, por lo que opté por una solución basada en un CPLD.

64 macroceldas

Ese es el reto: hacer un generador de sonido que consuma, como mucho, 64 macroceldas en un CPLD de la serie MAX3000A de Intel, en concreto el EPM3064.



64 macroceldas son 64 biestables con la lógica combinatoria asociada. No es un reto sencillo y es probable que los resultados no tengan tan buena calidad como con un PSG, pero el beneficio que se consigue en reducción de pines, miniaturización y reducción de consumo (Un CPLD consume mucho para los estándares actuales, pero el SN76489 consume más) hace que valga la pena intentarlo.

El circuito

Se plantea un circuito sencillo con dos entradas configuradas en lógica negativa y circuitería antirrebote básica y una salida con condensador de desacoplo y divisor de tensión para evitar sobretensiones en el amplificador de audio de la salida. La placa equipada con el CPLD EPM3064 incluye un oscilador a 50 MHz conectado a uno de los pines de reloj del CPLD y un led con su cátodo conectado a otro de los pines del CPLD (se enciende cuando se emite un 0 por ese pin).



Diagrama de bloques

A continuación puede verse el diagrama del bloques que se ha implementado en el CPLD.



El bloque $x^{18} + x^{11} + 1$ se corresponde con el LFSR maximal de grado 18 que permite generar ruido blanco (aproximado) en el registro de arriba.

L1 es un bloque combinacional que emite un 1 si la entrada vale 0 y la entrada sin cambiar en caso contrario.
L1
EntradaSalida
01
xx

L2 es un bloque combinacional que emite 8193 si la entrada vale 0 y la entrada sin cambiar en caso contrario.
L2
EntradaSalida
08193
xx

B es el bloque combinacional encargado de controlar los multiplexores en función de las señales de entrada y del cruce por cero del registro de 23 bits:
B
EntradasSalidas (MUX)
/Laser/NoiseNS123
11dc111
0dcdc00dc
100010
101210
(dc = don't care)

Descripción funcional

Cuando se activa la entrada /Laser (se pone a nivel bajo), se carga en el registro de 23 bits el valor "01110000000000000000000", este valor se carga para que los 4 bits más significativos tengan el valor "0111". Si nos fijamos los 4 bits más significativos del registro de 23 bits se utlizan para incrementar el valor del registro de 18 bits. Dicho registro de 18 bits actúa como acumulador de fase para una señal de onda cuadrada correspondiente al bit más significativo (bit 17) de este registro.

Si tenemos un registro de 18 bits como acumulador de fase y una frecuencia de reloj de 50 MHz (la de la placa que estamos usando) tendremos una frecuencia del bit más significativo de:

$$f_{out} = {f_{clk} \over 2^{18}}$$

De forma general, en caso de que apliquemos incrementos arbitrarios a este registro de desplazamiento, obtendremos una frecuencia en el bit más significativo de:

$$f_{out} = Inc \times {f_{clk} \over 2^{18}} = Inc \times {50000000 \over 2^{18}}$$

Para simular el sonido de un disparo láser lo que generamos es una caida rápida en frecuencia por lo que empezamos con un $Inc = 7$ cuando /Laser = 0 (de ahí los 4 bits más significativos del valor 01110000000000000000000), este valor de incremento genera una frecuencia en el bit más significativo del registro de 18 bits de:

$$Inc = 7 \Rightarrow f_{out} = 7 \times {50000000 \over 2^{18}} = 1335.1 Hz$$

Cuando /Laser vuelve al valor 1, vamos bajando el valor de Inc (los 4 bits más significativos del registro de 23 bits) hasta que vale 0:

$$Inc = 6 \Rightarrow f_{out} = 1144.4 Hz$$
$$Inc = 5 \Rightarrow f_{out} = 953.67 Hz$$
$$Inc = 4 \Rightarrow f_{out} = 762.94 Hz$$
$$Inc = 3 \Rightarrow f_{out} = 572.2 Hz$$
$$Inc = 2 \Rightarrow f_{out} = 381.47 Hz$$
$$Inc = 1 \Rightarrow f_{out} = 190.73 Hz$$
$$Inc = 0 \Rightarrow f_{out} = 0 Hz$$

Como el registro de 23 bits también actúa como un acumulador de fase (pues se decrementa en bloque, no solo los bits más significativos), la caida es lo suficientemente lenta como para ser audible (que es lo que queremos). Nótese que una vez cae a 0, el registro de 23 bits se queda ahí estancado gracias al circuito combinacional L1 que actúa como limitador, lo que, en la práctica, provoca que el registro de 18 bits "pare" de oscilar (pues $Inc = 0$ siempre). Nótese también que, aunque pare de oscilar, es posible que a la salida que va hacia el amplificador (el bit 17) se quede un "1" de forma pemanente, es por ello por lo que se hace necesario colocar siempre un condensador de desacoplo a la salida.

Si lo que se pone a 0 es la entrada /Noise y la entrada /Laser permanece a 1, lo que se hace es seleccionar como realimentación del registro de 18 bits la salida del polinomio LFSR, que provocará una secuencia de números pseudoaleatorios (ruido) en el bit 17 (salida del amplificador). La carga del valor del LFSR no se produce en cada ciclo del reloj de 50 MHz, pues provocaría ruido no audible, sino que se aprovecha el registro de desplazamiento de 23 bits y, a través del circuito combinacional L2, se hace que "desborde" en 8192, por lo que el registro de 18 bits cambiará con una frecuencia de:

$$f_{muestreo} = {50000000 \over 8192} = 6103.5 Hz$$

por lo que el ruido resultante ocupará aproximadamente hasta la banda de los 3 KHz. No es un ruido blanco pero a efectos audibles es muy parecido al ruido generado por un PSG.

Código fuente

Todo el código fuente en VHDL puede meterse dentro de una sola entidad:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Epm3064GunSound is
    port (
        Clk               : in std_logic;
        TrigLaserIn       : in std_logic;
        TrigNoiseIn       : in std_logic;
        SpeakerOut        : out std_logic;
        LedOut            : out std_logic
    );
end entity;

architecture Architecture1 of Epm3064GunSound is
    signal LFSRD : std_logic_vector(17 downto 0);
    signal LFSRQ : std_logic_vector(17 downto 0);
    signal LFSRRawOut : std_logic_vector(17 downto 0);
    signal LFSROut : std_logic_vector(17 downto 0);
    signal LFSRMux : std_logic_vector(1 downto 0);
    signal TimerD : std_logic_vector(22 downto 0);
    signal TimerQ : std_logic_vector(22 downto 0);
    signal TimerMux : std_logic;
    signal Limited1TimerQ : std_logic_vector(22 downto 0);
    signal Limited2TimerQ : std_logic_vector(22 downto 0);
    signal LimiterMux : std_logic;
    signal LimiterMuxOut : std_logic_vector(22 downto 0);
    signal NoiseSample : std_logic;
begin
    -- LFSR
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            LFSRQ <= LFSRD;
        end if;
    end process;
    
    --LFSRRawOut <= (LFSRQ(0) xor LFSRQ(3)) & LFSRQ(19 downto 1);    20 bits
    LFSRRawOut <= (LFSRQ(0) xor LFSRQ(7)) & LFSRQ(17 downto 1);    -- 18 bits
    LFSROut <= LFSRRawOut when (unsigned(LFSRRawOut) /= 0) else
               std_logic_vector(to_unsigned(1, 18));
    LFSRD <= LFSROut when (LFSRMux = "10") else
             std_logic_vector(unsigned(LFSRQ) + unsigned(TimerQ(22 downto 19))) when (LFSRMux = "01") else
                LFSRQ;
    SpeakerOut <= LFSRQ(17);
    
    -- timer
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            TimerQ <= TimerD;
        end if;
    end process;

    Limited1TimerQ <= std_logic_vector(to_unsigned(1, 23)) when (signed(TimerQ) = 0) else
                      TimerQ;
    Limited2TimerQ <= std_logic_vector(to_unsigned(8192 + 1, 23)) when (signed(TimerQ) = 0) else
                      TimerQ;
    LimiterMuxOut <= Limited1TimerQ when (LimiterMux = '1') else
                     Limited2TimerQ;
    TimerD <= std_logic_vector(signed(LimiterMuxOut) - 1) when (TimerMux = '1') else
              "01110000000000000000000";
    NoiseSample <= '1' when (signed(TimerQ) = 0) else
                   '0';
    
    -- operation logic
    LFSRMux <= "01" when (((TrigLaserIn = '1') and (TrigNoiseIn = '1')) or (TrigLaserIn = '0')) else
               "10" when ((TrigLaserIn = '1') and (TrigNoiseIn = '0') and (NoiseSample = '1')) else
                  "00";
    TimerMux <= '0' when (TrigLaserIn = '0') else
                '1';
    LimiterMux <= '1' when ((TrigLaserIn = '1') and (TrigNoiseIn = '1')) else
                  '0';
    LedOut <= TrigLaserIn and TrigNoiseIn;
end architecture;


Resultados

Con este circuito se consiguen unos resultados similares a los obtenidos utilizando el chip SN76489, con menos circuitería, ocupando menos pines y menos tiempo de procesamiento en el microcontrolador: nótense que ahora sólo necesitamos dos pines GPIO del microcontrolador (uno para /Laser y otro para /Noise).



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

[ añadir comentario ] ( 143 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 2.9 / 134 )
Programación embebida no bloqueante mediante máquinas de estado en C++. Caso de uso con el chip de sonido SN76489 
El uso de máquinas de estado en un recurso casi obligatorio a la hora de implementar código no bloqueante, para dotar a los sistemas de algún tipo de funcionalidad multi-tarea o simplemente como mecanismo de ahorro de energía. En este post se estudiará un caso de uso: la programación del chip de sonido SN76489, mediante esta técnica de desarrollo y utilizando parte del potencial del lenguaje C++ moderno para ello.

Descripción funcional

El objetivo es hacer un pequeño montaje basado en el ARM Cortex-M3 STM32F103C8T6 como MCU y en el chip de sonido SN76489 (3 canales de tono más un canal de ruido, utilizado en las antiguas consolas de Sega y en algunas placas de recreativas de los años 80 y 90).



Dicho montaje incluye dos pulsadores externos: uno para generar un sonido de disparo "láser" y otro para generar un sonido de metralleta (o parecido, dentro de las limitaciones del SN76489).

Sonido

El SN76489 es un chip de sonido extremadamente sencillo de programar: un bus de 8 bits con señales /WE y /CE (que en este caso se han cortocircuitado) y una señal READY de salida. Como se puede ver en el circuito anterior, el SN76489 se alimenta a 5 voltios, mientras que el STM32 se alimenta a 3.3 voltios, lo que, a priori puede resultar un problema de cara a la señal READY. Dicho problema, sin embargo, no lo es tal ya que, según la hoja de datos del fabricante, el pin READY del SN76489 es una salida en colector abierto, por lo que puede atacar perfectamente a una entrada del STM32 siempre y cuando la resistencia de pull-up correspondiente esté a 3.3 voltios.

Como se puede ver en su hoja de datos, para enviarle un byte, es necesario hacer los siguiente pasos:

1. Esperar a que la salida READY se ponga a 1 (colector abierto), lo que indica que el chip puede recibir un comando.

2. Colocar el byte que se quiere enviar en el bus de 8 bits.

3. Poner /CE y /WE a 0.

4. Esperar a que la salida READY se ponga a 0 (a masa), lo que significa que está procesando el comando

5. Esperar a que la salida READY se ponga de nuevo a 1 (colector abierto), lo que significa que el comando ha sido procesado y el chip está preparado para recibir más comandos.

Esta secuencia hay que realizarla por cada comando (byte) que se le quiera enviar al SN76489.

A la hora de diseñar la máquina de estados se ha utilizado el formalismo de la máquina de Mealy. En este formalismo, los estados están caracterizados sólo por sus etiquetas, mientras que las transiciones están caracterizadas por el par (entrada | salida), en este caso, "entrada" será la condición de entrada para que el proceso tome por ese arco y la "salida" serán las operaciones a realizar en ese salto:



"condición" puede ser vacía, lo que significará que siempre se toma ese arco, y "operaciones" también podrá ser vacía, lo que significará que no se ejecuta ninguna operación al tomar ese arco.



En la máquina de estados se han introducido estados adicionales al final que permiten introducir retardos entre en envío de un comando y el siguiente. Esta funcionalidad es muy útil para controlar los tiempos: tiempo de caida de los tonos de "láser", tiempo entre "balas" de la metralleta, etc. "tc" es la variable miembro "tickCounter" usada para esperas forzosas (el STM32 va más rápido que el SN76489) mientras que "dt" se corresponde con la variable miembro "delayTicks", usada para hacer las esperas entre el envío de un comando y el envío del siguiente.

Cada objeto SN76489 incluye una cola de comandos: las funciones miembro que hacen de API se encargan de preparar y meter en la cola (push) los comandos que será posteriormente enviados por la función miembro "tick" (la encargada de ejecutar la máquina de estados):

void SN76489::setToneGeneratorFrequency(uint8_t index, uint16_t frequency, int16_t sweepFrequencyIncrement, int16_t sweepTicksPerIncrement, int16_t sweepNumIncrements) {
    if (index > 2)        // 0, 1 or 2
        index = 2;
    frequency &= 0x03FF;  // 10 bit value
    this->txQueue.push({(uint8_t) (0x80 | (index << 5) | (frequency & 0x0F))});
    this->txQueue.push({(uint8_t) ((frequency >> 4) & 0x3F), sweepTicksPerIncrement});
    while (sweepNumIncrements > 0) {
        frequency += sweepFrequencyIncrement;
        sweepNumIncrements--;
        this->txQueue.push({(uint8_t) ((frequency >> 4) & 0x3F), sweepTicksPerIncrement});
    }
}


void SN76489::setToneGeneratorAttenuation(uint8_t index, uint8_t attenuation) {
    if (index > 2)        // 0, 1 or 2
        index = 2;
    this->txQueue.push({(uint8_t) (0x80 | (index << 5) | 0x10 | (attenuation & 0x0F))});
}


void SN76489::setNoiseGeneratorFrequency(NoiseGeneratorFrequency frequency, NoiseGeneratorFeedback feedback) {
    this->txQueue.push({(uint8_t) (0xE0 | ((static_cast<uint8_t>(feedback) & 0x01) << 2) | (static_cast<uint8_t>(feedback) & 0x03))});
}


void SN76489::setNoiseGeneratorAttenuation(uint8_t attenuation, int32_t delayTicks) {
    this->txQueue.push({(uint8_t) (0xF0 | (attenuation & 0x0F)), delayTicks});
}

Mientras que la función miembro "tick" deberá ser invocada desde el SYSTICK del microcontrolador (que se ejecuta varios cientos de veces por segundo). Dicha función miembro "tick" es la encargada de ejecutar la máquina de estados y de enviar los comandos al SN76489 de forma correcta.

Para separar la implementación de esta máquina de estados del hardware se define una clase abstracta "SN76489Interface", que deberá ser implementada por el "usuario" y que es la que en última instancia escribe en el bus de datos del SN76489, escribe el bit /WE y lee el bit READY del SN76489.

class SN76489Interface {
    public:
        virtual void sn76489SetByte(uint8_t v) = 0;
        virtual void sn76489SetWE(uint8_t v) = 0;
        virtual bool sn76489GetReady() = 0;
};

Cuando se invoca la función miembro "init" del objeto "SN76489", se le pasa un puntero a un objeto de tipo "SN76489Interface", para que el objeto "SN76489" pueda comunicarse con un hardware real a través de este interface.

Cada elemento de la cola de comandos es un par (comando, ticksRetardo), de tal manera que la máquina de estados (la función miembro "tick") envía el comando y espera una cantidad de ticks equivalente a "ticksRetardo" antes de enviar el siguiente comando de la cola (si lo hubiese). De esta manera es muy sencillo hacer los sonidos propuestos:

- Láser: Se configura el generador de tono 0 con una frecuencia alta, se espera un par de ticks, se envía una frecuencia más baja, se espera otro par de ticks, se envía otra fecuencia más baja, etc. Esta caida de frecuencia en el tiempo genera un efecto psicoacústico "percutido", como de disparo. Al final de la secuencia se pone el volumen el mínimo del generator tono 0.

- Metralleta: Se configura el generador de ruido y se manda 17 veces la secuencia: volumen máximo con espera de 20 ticks y silencio con espera de 30 ticks. Esta alternancia de ruido/silencio repetidas veces genera el sonido de "metralleta".

Nótese que los comandos descritos para cada sonido se empujan en la cola de comandos de forma simultánea uno detrás de otro, es la máquina de estados (función miembro "tick" de la clase "SN76489") la que se encarga de enviar los comandos y realizar la espera entre comando y comando a medida que va sacando comandos de la cola.

Teclado

De forma adicional, es necesario implementar un pequeño teclado de dos botones, de manera de un botón dispare el "láser" y el otro la "metralleta". Para este menester creamos también una clase Keyboard pero en este caso, como dicha clase sí va a estar vinculada directamente con un recurso hardware (esta es una decisión arbitraria), será una clase estática (con todas sus funciones miembro estáticas).

La función miembro "init" configurará dos pines GPIO y el controlador de interrupciones externas ETXI del STM32 para que genere una interrupción cada vez que se produzca un flanco de bajada en esos dos pines del microcontrolador. Como se puede comprobar en la declaración de la clase Keyboard, la función miembro estática "interrupt" se coloca en la sección ".exti" dentro del código objeto: esto hará que el linker script la coloque en el lugar adecuado para que se invoque al producirse una interrupción de GPIO.

#ifndef  __KEYBOARD_H__
#define  __KEYBOARD_H__


#include <stdint.h>


extern "C++" {
    namespace avelino {
        using namespace std;

        class KeyboardListener {
            public:
                virtual void buttonPressed(int8_t key) = 0;
        };

        class Keyboard {
            protected:
                static KeyboardListener *listener;
                static int8_t currentKey;
                static int32_t tickCounter;
                enum class Status : int8_t { IDLE, WAIT_AFTER_PRESSED };
                static Status status;
            public:
                static void init(KeyboardListener *listener);
                static void interrupt()  __attribute__ ((section(".exti")));
                static void tick();
        };
    }
}


#endif  // __KEYBOARD_H__

La función miembro "interrupt" hace muy poco: simplemente actualiza la variable miembro "key" y limpia los "pending bits" del microcontrolador para que la interrupción pueda volver a dispararse en el futuro. Uno puede pensar que con esto ya sería, de por sí, suficiente pero lo cierto es que no es así, ya que al tratarse de entradas desde el exterior, son propensas a experimentar rebotes y ruido, por lo que se hace necesario hacer "limpieza" de esa señal mediante una sencilla máquina de estados. Es la función miembro "tick" (invocada, como su homónima de SN76489, periódicamente desde el SYSTICK del microcontrolador) la encargada de ejecutar la máquina de estados que procesa los valores de esta variable "key".



Como la interrupción se dispara sólo ante flancos de bajada, lo único que hace la máquina de estados es simplemente esperar un tiempo prudencial (200 ejecuciones de ticks) antes de volver al estado de espera a que se detecte una nueva pulsación. De esta forma se eliminan las pulsaciones espúreas que saldrían su hubiese rebote mecánico.

La función miembro "buttonPressed" de la clase "ButtonListener" (que hereda de "KeyboardListener") es invocada cada vez que se detecta la pulsación de uno de los botones y el parámetro "key" indica qué botón se ha pulsado. Es aquí donde se realizan las llamadas a la API del objeto de clase "SN76489" para encolar los comandos que generan el sonido "láser" o encolar los comandos que generan el sonido de "metralleta", en función de la tecla pulsada.

void systick() __attribute__ ((section(".systick")));


void systick() {
    sn76489.tick();
    Keyboard::tick();
}


class ButtonListener : public KeyboardListener {
    public:
        virtual void buttonPressed(int8_t key);
};


void ButtonListener::buttonPressed(int8_t key) {
    if (key == 1) {   // láser
        sn76489.setToneGeneratorAttenuation(0, 0);   // full volume
        sn76489.setToneGeneratorFrequency(0, 477, 100, 5, 10);
        sn76489.setToneGeneratorAttenuation(0, 0x0F);   // min volume
    }
    else if (key == 2) {   // metralleta
        sn76489.setNoiseGeneratorFrequency(SN76489::NoiseGeneratorFrequency::N_512, SN76489::NoiseGeneratorFeedback::WHITE);
        for (int i = 0; i < 17; i++) {
            sn76489.setNoiseGeneratorAttenuation(0, 20);
            sn76489.setNoiseGeneratorAttenuation(0xFF, 30);
        }
    }
}

Como se puede apreciar, la función global "systick", que es invocada de forma periódica por el timer SYSTICK del microcontrolador, es la encargada a su vez de invocar las funciones miembro "tick" de "SN76489" y "Keyboard".

El hecho de que todo el software esté basado en interrupciones permite generar un código más elegante y más eficiente desde un punto de vista energético: la función main sólo tiene que inicializar los objetos y variables y dormirse a la espera de que se produzcan interrupciones (ya sea por GPIO como de SYSTICK):

int main() {
    // keyboard
    Keyboard::init(&buttonListener);
    // enable systick timer
    SYST_RVR = 0x0001FFFF;
    SYST_CVR = 0;
    SYST_CSR = 7;
    // sn76489
    sn76489GPIOInterface.init();
    sn76489.init(&sn76489GPIOInterface);
    sn76489.setToneGeneratorAttenuation(0, 0xFF); // min volume for tone generator 0
    sn76489.setToneGeneratorAttenuation(1, 0x0F); // min volume for tone generator 1
    sn76489.setToneGeneratorAttenuation(2, 0x0F); // min volume for tone generator 2
    sn76489.setNoiseGeneratorAttenuation(0x0F);   // min volume for noise generator
    while (true) {
        // WFI (wait for interrupt) instruction, enters low power mode
        asm volatile ("wfi");
    }
}

Nótese el bucle infinito con la instrucción que duerme la CPU "wfi" (Wait For Interrupt).

Características de C++ aprovechadas

- Plantillas estáticas en lugar de memoria dinámica: En sistemas embebidos hay que evitar el uso de memoria dinámica y es por ello que se ha creado la plantilla "StaticQueue" que define una cola circular estática cuyos parámetros de plantilla son el tipo base y la cantidad de elementos: Esta cola estática es la utilizada para almacenar los pares (comando, ticksRetardo) que utiliza la clase SN76489.

- Enumeradores fuertemente tipados ("enum class"): Los estados en las máquinas de estado no están definidos como constantes enteras sino como tipos "enum class" que es un tipo de enumerado fuertemente tipado introducido en C++11. El uso de este tipo de datos impide, por ejemplo, que se le asigne un entero aunque su tipo base sea un entero. Además, el código generado es igual de eficiente que si se utilizasen constantes enteras y se gana mucho en claridad en el código.

A continuación puede verse un vídeo del montaje en acción (con sonido):


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

[ añadir comentario ] ( 143 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 115 )
Implementación de un conversor analógico-digital delta-sigma sobre FPGA 
El uso de conversores analógico-digitales de tipo delta-sigma permite conseguir una conversión con una muy buena calidad usando sólo tres componentes externos y una circuitería analógica mínima. La resolución del conversor es arbitraria y depende de los recursos digitales que se tengan (biestables) y de la velocidad de reloj (sobremuestreo).

Esquema

Como se puede apreciar la parte analógica del conversor se reduce a un comparador, una resistencia y un condensador. En la "zona digital" tenemos todo el meollo: un biestable, un acumulador y un filtro paso bajo. Veamos el funcionamiento con una traza sencilla.

Funcionamiento

Asumamos que empezamos con la salida que va al filtro RC a 0 y el condensador totalmente descargado y que en la entrada analógica tenemos un voltaje entre 0 y Vcc (3.3 voltios) que llamaremos $V_{in}$. En estas condiciones iniciales, como $V_{in} > 0$, el comparador emite 3.3 voltios (un 1 lógico) y por tanto, en el siguiente ciclo de reloj, el biestable carga ese 1 en su salida Q.

Tras esta primera carga la salida que va conectada al filtro RC (la Q del biestable) se pone a 1, lo que significa que el condensador empieza a cargarse a través de R. Para cuando llegue el siguiente ciclo de reloj lo más probable es que la carga del condensador aún sea muy baja y el comparador siga emitiendo a su salida un 1 (su entrada + tiene un voltaje aún mayor que su entrada -). Tendremos, por tanto que el biestable seguirá emitiendo 1s hasta que el voltaje en la entrada - del comparador sea superior a $V_{in}$, en el momento que ocurra eso la salida del comparador será de 0 voltios (un 0 lógico). El biestable en el siguiente ciclo de reloj cargará ese 0 en su Q y el condensador empezará ahora a descargarse a través de la R.

Como se puede apreciar, el sistema en lazo cerrado lo que hace es, mediante 0s y 1s tratar de hacer que el voltaje en la entrada - del comparador (el voltaje en el condensador) "siga" al voltaje de entrada $V_{in}$ y esto, en la práctica, significa que a la salida del biestable lo que tenemos es una especie de señal PWM cuyo ciclo de trabajo será proporcional al voltaje $V_{in}$:

1.- Cuanto mayor es el valor de $V_{in}$, mayor cantidad de 1s emite el biestable para tratar de "alcanzar" $V_{in}$.

2.- Cuanto menor es el valor de $V_{in}$, mayor es la cantidad de 0s que emite el biestable para tratar de "alcanzar" $V_{in}$.

3.- Se puede ver, además, que para un valor próximo a ${Vcc \over 2}$ el biestable emitirá aproximadamente la misma cantidad de 0s que de 1s por unidad de tiempo.

Ahora supongamos que pasamos la salida del biestable por un circuito combinacional que ante un 0 en la entrada emite un -1 en un bus de N bits y ante un 1 en la entrada emite un +1 en esa misma salida de N bits. Si durante $2^{N-1}$ ciclos de reloj vamos sumando en un acumulador con signo los valores +1 y -1 que van saliendo de este circuito combinacional tendremos lo siguiente:

1.- Para valores altos de $V_{in}$, que generan muchos más 1s que 0s en la salida del biestable, tendremos que el acumulador cada $2^{N-1}$ ciclos de reloj tendrá un valor próximo a $2^{N-1}$.

2.- Para valores bajos de $V_{in}$, que generan muchos más 0s que 1s en la salida del biestable, tendremos que el acumulador, cada $2^{N-1}$ ciclos de reloj, tendrá un valor próximo a $-2^{N-1}$.

3.- Para valores próximos a ${Vcc \over 2}$, que generan una cantidad muy similar de 0s y de 1s por unidad de tiempo, tendremos que el acumulador, cada $2^{N-1}$ ciclos de reloj, tendrá un valor próximo a 0.

Si cada $2^{N-1}$ ciclos de reloj cargamos el valor de este acumulador sobre un registro (que podemos llamar de salida), justo antes de poner de nuevo el acumulador a 0 (y volver a acumular los +1 y -1 que llegan del biestable), tenemos que este registro de salida será el resultado de la conversión analógico-digital del voltaje de entrada $V_{in}$ en N bits, que era nuestro objetivo. En la implementación recomendada se introduce una tercera etapa que realiza un sobremuestreo adicional para suavizar la señal (actúa como filtro paso bajo) pero que no se ha implementado en este caso por simplicidad.

Otra característica importante en este tipo de conversores es que la resolución en bits del conversor viene determinada por N (la anchura en bits del acumulador y del filtro paso bajo), que es un parámetro relativamente sencillo de cambiar, y que no requiere cambios en la electrónica externa.

Implementación

A continuación puede verse un esquema detallado de lo que sería la implementación en una FPGA o en un CPLD del conversor descrito:

Cada vez que el contador pasa por 0 hace que en acumulador se cargue en el registro de salida y al mismo tiempo se reinicie con el valor 0. Los valores de la resistencia R y del condensador C deben ser elegidos acorde a la frecuencia de reloj del sistema. Por ejemplo, Lattice Semiconductor recomienda que la constante de tiempo del filtro $t = R \times C$ cumpla que $200 < t \times f_{clk} < 1000$. En este caso concreto se ha hecho una implementación sobre un CPLD de Altera (Intel) que va a 50 MHz y se han elegido los valores de C = 1.5 nF y R = 10 K, que cumplen dicha desigualdad.

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

entity DeltaSigmaADC is
    port (
        Reset        : in std_logic;
        Clk          : in std_logic;
        DataOut      : out std_logic_vector(7 downto 0);
        ComparatorIn : in std_logic;
        RCChargeOut  : out std_logic
    );
end entity;

architecture Architecture1 of DeltaSigmaADC is
    signal DBus : std_logic;
    signal QBus : std_logic;
    signal FollowerOut : std_logic_vector(15 downto 0);
    signal CounterDBus : std_logic_vector(14 downto 0);
    signal CounterQBus : std_logic_vector(14 downto 0);
    signal IntegratorDBus : std_logic_vector(15 downto 0);
    signal IntegratorQBus : std_logic_vector(15 downto 0);
    signal LatchDBus : std_logic_vector(15 downto 0);
    signal LatchQBus : std_logic_vector(15 downto 0);
begin
    -- biestable de seguimiento de voltaje
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            QBus <= DBus;
        end if;
    end process;

    DBus <= ComparatorIn;
    RCChargeOut <= QBus;
    FollowerOut <= std_logic_vector(to_signed(-1, 16)) when (QBus = '0') else
                   std_logic_vector(to_signed(1, 16));

    -- contador
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            CounterQBus <= CounterDBus;
        end if;
    end process;

    CounterDBus <= std_logic_vector(to_signed(0, 15)) when (Reset = '1') else
                   std_logic_vector(signed(CounterQBus) + 1);

    -- integrador
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            IntegratorQBus <= IntegratorDBus;
        end if;
    end process;

    IntegratorDBus <= std_logic_vector(to_signed(0, 16)) when (signed(CounterQBus) = 0) else
                      std_logic_vector(signed(FollowerOut) + signed(IntegratorQBus));

    -- latch
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            LatchQBus <= LatchDBus;
        end if;
    end process;
    
    LatchDBus <= IntegratorQBus when (signed(CounterQBus) = 0) else
                 LatchQBus;

    -- salida
    DataOut <= LatchQBus(15 downto 8);
end architecture;



El código fuente está disponible en la sección soft.

[ añadir comentario ] ( 876 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2564 )
Estudio del uso de la sobrecarga de operadores para aritmética de punto fijo en C++ 
La aritmética de punto fijo es un mecanismo muy útil para la implementación de funciones matemáticas en procesadores sin unidad de coma flotante como microcontroladores y procesadores pequeños de 8 o 16 bits. A lo largo de este post se plantea el uso de la sobrecarga de operadores de C++ para facilitar las tareas de programación y mejorar la legibilidad del código cuando se usan tipos de punto fijo.

Introducción

La aritmética de punto fijo permite realizar operaciones con números fraccionarios mediante tipos enteros y operaciones enteras. En anteriores posts de este blog se ha hablado de forma extensa acerca de este tema, por lo que se remite a ellos a la persona interesada. A lo largo de este post usaré siempre valores de punto fijo Q16.16 (32 bits con 16 bits para la parte entera y 16 bits para la parte fraccionaria).

Implementación tradicional mediante macros

Tradicionalmente siempre he implementado la aritmética de punto fijo con un fichero de cabecera en el que defino "fixedpoint_t" como un "int32_t" y unas macros especiales para las operaciones de conversión de entero a punto fijo, de multiplicación y de división (las más "complejas"):

typedef int32_t fixedpoint_ct;

#define  FP_MUL(x, y)  ((int32_t) ((((int64_t) (x)) * ((int64_t) (y))) >> 16))
#define  FP_DIV(x, y)  ((int32_t) ((((int64_t) (x)) << 16) / ((int64_t) (y))))
#define  TO_FP(x)      (((int32_t ) (x)) << 16)

Como se puede apreciar, se trata de una implementación extremadamente sencilla y si lo que queremos es escribir una función que calcule:
$$\left({a \times b}\right) + \left({a \over b}\right)$$
Introduciríamos el siguiente código:

fixedpoint_ct fWithMacros(fixedpoint_ct a, fixedpoint_ct b) {
    return FP_MUL(a, b) + FP_DIV(a, b);
}

Y para los valores $a = 4$ y $b = 3$ la invocaríamos de la siguiente forma:

fixedpoint_ct v = fWithMacros(TO_FP(4), TO_FP(3));

Se trata de una implementación perfectamente válida aunque adolece de falta de claridad en el código: hay que leer con cuidado las operaciones aritméticas para no confundirse. Por otro lado es una implementación que tiene la ventaja de que en todo momento está claro que no estamos trabajando con un tipo "trivial".

Implementación basada en sobrecarga de operadores

Buscando un código más legible que el anterior, lo lógico es recurrir a la sobrecarga de operadores de C++. Definimos una clase "fixedpoint_t" en la que metemos un entero de 32 bits y definimos las cuatro operaciones básicas como "operator" dentro de la propia clase:

class fixedpoint_t {
    public:
        int32_t v;
        fixedpoint_t(int32_t x = 0) : v(x << 16) { };
        inline fixedpoint_t &operator = (const int32_t &x) { this->v = x << 16; return *this; };
        inline fixedpoint_t operator + (const fixedpoint_t &x) { fixedpoint_t ret; ret.v = this->v + x.v; return ret; };
        inline fixedpoint_t operator - (const fixedpoint_t &x) { fixedpoint_t ret; ret.v = this->v - x.v; return ret; };
        inline fixedpoint_t operator * (const fixedpoint_t &x) { fixedpoint_t ret; ret.v = (((int64_t) this->v) * ((int64_t) x.v)) >> 16; return ret; };
        inline fixedpoint_t operator / (const fixedpoint_t &x) { fixedpoint_t ret; ret.v = (((int64_t) this->v) << 16) / ((int64_t) x.v); return ret; };
};

Esta implementación nos permite ahora escribir la misma función de antes de una forma más legible:

fixedpoint_t fWithOperators(fixedpoint_t a, fixedpoint_t b) {
    return (a * b) + (a / b);
}

Y, de la misma manera, también nos permite invocarla de forma más legible:

fixedpoint_t v = fWithOperators(4, 3);

Sin embargo se podría pensar que una implementación así generaría mucho más código que la implementación basada en macros. Hagamos unas pruebas.

Comparativa

Si compilamos con gcc el código de ambas funciones sin opciones de optimización:

g++ -std=c++11 -o fp fp.cc

la diferencia es abismal:

_Z11fWithMacrosii:
    push   %rbp
    mov    %rsp,%rbp
    push   %rbx
    mov    %edi,-0xc(%rbp)
    mov    %esi,-0x10(%rbp)
    mov    -0xc(%rbp),%eax
    movslq %eax,%rdx
    mov    -0x10(%rbp),%eax
    cltq   
    imul   %rdx,%rax
    sar    $0x10,%rax
    mov    %eax,%ecx
    mov    -0xc(%rbp),%eax
    cltq   
    shl    $0x10,%rax
    mov    -0x10(%rbp),%edx
    movslq %edx,%rbx
    cqto   
    idiv   %rbx
    add    %ecx,%eax
    pop    %rbx
    pop    %rbp
    retq   

_Z14fWithOperators12fixedpoint_tS_:
    push   %rbp
    mov    %rsp,%rbp
    sub    $0x40,%rsp
    mov    %edi,-0x30(%rbp)
    mov    %esi,-0x40(%rbp)
    lea    -0x40(%rbp),%rdx
    lea    -0x30(%rbp),%rax
    mov    %rdx,%rsi
    mov    %rax,%rdi
    callq  4009cc <_ZN12fixedpoint_tdvERKS_>
    mov    %eax,-0x20(%rbp)
    lea    -0x40(%rbp),%rdx
    lea    -0x30(%rbp),%rax
    mov    %rdx,%rsi
    mov    %rax,%rdi
    callq  40098a <_ZN12fixedpoint_tmlERKS_>
    mov    %eax,-0x10(%rbp)
    lea    -0x20(%rbp),%rdx
    lea    -0x10(%rbp),%rax
    mov    %rdx,%rsi
    mov    %rax,%rdi
    callq  400952 <_ZN12fixedpoint_tplERKS_>
    leaveq 
    retq   

El compilador ha hecho caso omiso del "inline" de las funciones miembro de la clase "fixedpoint_t" y las ha implementado como funciones aparte en ensamblador. En este caso la implementación usando macros es más eficiente. Activemos ahora el primer nivel de optimización "-O1":

g++ -std=c++11 -O1 -o fp fp.cc

Voilà, ahora apenas notamos la diferencia en el código generado:

_Z11fWithMacrosii:
    movslq %edi,%rax
    movslq %esi,%rsi
    mov    %rax,%rcx
    imul   %rsi,%rcx
    sar    $0x10,%rcx
    shl    $0x10,%rax
    cqto   
    idiv   %rsi
    add    %ecx,%eax
    retq   

_Z14fWithOperators12fixedpoint_tS_:
    movslq %edi,%rdi
    movslq %esi,%rsi
    mov    %rdi,%rax
    shl    $0x10,%rax
    cqto   
    idiv   %rsi
    imul   %rdi,%rsi
    sar    $0x10,%rsi
    add    %esi,%eax
    retq   

La implementación utilizando la clase "fixedpoint_t" con los operadores sobrecargados genera un código igual de eficiente que la implementación basada en macros.

Como conclusión podemos sacar que no es necesario sacrificar legibilidad en aras de la velocidad de ejecución, siempre y cuando usemos correctamente los elementos del lenguaje y compilemos usando las opciones de optimización adecuadas.

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

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

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