VHDL

Metrónomo en VHDL (2 de 3): ¿Alguien dijo variable?

Metrónomo [(cc)(by)(sa) Paco Vila]

Aquí estamos en la segunda parte, ¿no están emocionados? Ahora ya tenemos la base matemática que dice que es posible y una ROM con los datos necesarios para implementar 512 frecuencias distintas, ¿cómo lo pegamos? ¿qué hay que hacer para crear un divisor de frecuencia variable?

Primero revisaremos un poco los cálculos, porque una cosa es tener la escala y otra muy diferente que ésta pueda ser implementada en el divisor de frecuencia. Posteriormente, realizamos la unión de la ROM con el divisor, para crear un “divisor de frecuencia variable”, con ayuda de una nueva instrucción que conoceremos. Finalmente, simularemos e implementaremos el sistema para comprobar que funciona como se espera.

[wpdm_file id=16]

Calculos para el divisor, de nuevo

En el ejemplo del divisor de frecuencia de 50MHz a 200Hz la escala fue de 250000, con 125000 ciclos en alto y 125000 ciclos en bajo. Pero, ¿y si deseáramos que la frecuencia de salida fuese 150Hz? La escala entonces sería

\( Escala = \dfrac{f_{in}}{f_{out}} = \dfrac{50MHz}{150Hz} = 333333.33333333 \)

Así que necesitamos un ajuste al entero más próximo, que en este caso es 333333. Dejando de lado la diferencia en la frecuencia de salida, ¿qué hacemos si tenemos una escala impar? El código para el caso par es simple, cambiamos de 0 a 1 o de 1 a 0 cada 125000 ciclos como se muestra en el listado 1.

if (contador = 124999) then    -- el único límite, a la mitad de la escala.
    temporal <= NOT(temporal); -- cambio en la señal, de 0 a 1 o de 1 a 0.
    contador <= 0;             -- Reinicio del contador.
else
    contador <= contador + 1;  -- Seguimos en el mismo estado, hay que contar.
end if;

Para el caso impar necesariamente uno de los dos estados, alto o bajo, tiene mayor tiempo que el otro. Se podría ajustar a un valor par, pero se tendría un poco más de diferencia respecto a la frecuencia deseada (hay que recordar que ya perdimos 0.33333333) y es lo que se trata de reducir. Por lo tanto, para el caso impar tendremos algo similar a lo mostrado en el listado 2.

if (contador = 333332) then     -- Este es el límite superior, la escala en 333333.
    contador <= 0;              -- Aquí se produce el regreso a cero.
else
    if (contador < 166666) then -- La mitad es 166666.5, no posible en enteros.
        temporal <= '1';        -- Así que se elije:
    else                        --   De      0 a 166665 (166666 estados) en alto.
        temporal <= '0';        --   De 166666 a 333332 (166667 estados) en bajo.
    end if;
    contador <= contador + 1;
end if;

Así que tenemos dos casos diferentes, ¿cómo se implementarán ambos en el mismo divisor de frecuencia? Dejemos en pausa un momento tal problema y veamos otro pequeño detalle.

Mentí (o ¿qué haces cuando el entero es muy grande?)

En la primera parte del metrónomo calculamos las escalas necesarias para lograr frecuencias de 1 a 300 BPM en base a un reloj de entrada de 50MHz. El factor de escalamiento mayor es 3,000,000,000 (¿se lee como tres mil millones?), y para su implementación se necesitan 32 bits.

Pero, ¿no es este un número muy grande para un entero? Primero realizaremos una pequeña comprobación de los límites gracias al listado 3, que contiene un pequeño contador que va de 0 a 2,999,999,999.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity int32b_test is
    Port (
        clk     : in  STD_LOGIC -- Reloj de entrada de 50MHz.
    );
end int32b_test;

architecture Behavioral of int32b_test is
    -- Escala necesaria para bajar la frecuencia de 50MHz a 1BPM.
    signal contador: integer range 0 to 2999999999 := 0;
begin
    prueba_contador: process (clk) begin
        if rising_edge(clk) then
            if (contador = 2999999999) then
                contador <= 0;
            else
                contador <= contador + 1;
            end if;
        end if;
    end process;
end Behavioral;

Al realizar el proceso de síntesis lógica, ¡sorpresa, sorpresa!, no funciona:

ERROR:HDLParsers:414 - "D:/Estado Finito/Proyectos/Xilinx/PruebaContador/int32b_test.vhd" Line 11. The integer value of 2999999999 is greater than integer'high.
ERROR:HDLParsers:414 - "D:/Estado Finito/Proyectos/Xilinx/PruebaContador/int32b_test.vhd" Line 15. The integer value of 2999999999 is greater than integer'high.

Lo que nos lleva a la pregunta, ¿entonces cuánto es lo máximo que soporta el tipo de dato entero? Una busqueda superficial reveló que un entero se representa en 32 bits (confirmado en este otro foro). De esos 32 bits, uno es el signo, lo que implica que su rango está entre los valores:

\( -2^{31} \leq integer \leq +2^{31}-1 \therefore -2~147~483~648 \leq integer \leq +2~147~483~647 \)

Así que, ¿qué hacemos? La primera idea es simple: utilizamos un reloj de entrada de 25MHz en lugar de los 50MHz y utilizamos dicha frecuencia para realizar los cálculos nuevamente. ¿Por qué? Eso implicaría que la escala se reduce a la mitad, por lo tanto,

\( 3~000~000~000 > 2~147~483~647 > 1~500~000~000, \)

se crea una escala que se puede implementar con el rango de un entero. Hay dos problemas con esta implementación:

  1. el error en cada frecuencia del metrónomo se duplica, y
  2. el valor en la ROM deberá ser escrito en notación binaria dado que el número de bits no es múltiplo de cuatro (a pesar de que existen algunos trucos para acortar el número).

En esta fase se debe tomar una decisión de diseño: ¿creamos cadenas de 31 bits y nos quedamos con el doble de error? ¿buscamos otra forma de implementar el contador que pueda soportar la longitud de 32 bits? ¿o simplemente utilizamos un vector de 28 bits, múltiplo de cuatro, y aceptamos un error dieciséis veces mayor?

Si pusieron atención, sabrán que esa decisión fue tomada desde la creación de la ROM: una ROM de 512 registros de 28 bits cada uno, calculados con una frecuencia de entrada de 3.125MHz (reducción de 4 bits, o división entre 16 de la frecuencia original).

Así que ya lo tenemos, aunque ahora el error máximo por hora es de 2.30ms. Aún sigue siendo bajo, ¿no?

Hola, GENERIC

Ahora bien, esta implementación va a estar hecha con 28 bits. Algunos no estarán de acuerdo, otros sí. Asi que es mejor buscar una manera de hacer un componente modificable de una manera más fácil, sin ser tan intrusivos en todo el código.

En otras palabras: si después se decide incrementar la resolución del entero a 32 bits (de alguna forma), queremos que se haga desde un único lugar.

Por suerte, en el lenguaje VHDL existe GENERIC. Como se define en el enlace anterior:

[quote]
La instrucción GENERIC define y declara propiedades o constantes del módulo. Las constantes declaradas en esta sección son como los parámetros en las funciones de cualquier otro lenguaje de programación, por lo que es posible introducir valores, en caso contrario tomará los valores por defecto.
[/quote]

Para nuestros propósitos, podemos declarar como una constante de la entidad el número de bits que se reciben de la ROM y utilizarla a través de todo el módulo. Así será más fácil aumentar o disminuir el número de bits recibidos de la ROM. Pero, suficientes cálculos y letras, veamos más codigo.

El divisor, con un toque de ROM

En el listado 4 comenzamos la declaración de la entidad. Primero declaramos un elemento genérico gracias a nuestra nueva palabra clave GENERIC. Todo lo contenido en GENERIC es similar a lo declarado en PORT, aunque para uso interno de la entidad. Respecto al listado 4:

  • En la línea 3 se declara un entero, NBITS, mismo que representará la cantidad de bits provenientes de la ROM.
  • El parámetro de entrada escala recibe NBITS, facilitando un posterior cambio en el número de bits.

El listado 4 también muestra la estructura básica del nuevo componente, un divisor de frecuencia con una nueva entrada: la escala ahora vendrá de la ROM.

entity clk_rom is
    GENERIC (
        NBITS   : integer := 28 -- Cantidad de bits que tiene cada registro en la ROM.
    );
    PORT (
        clk     : in  STD_LOGIC; -- Reloj de entrada de 3.125MHz.
        reset   : in  STD_LOGIC;
        escala  : in  STD_LOGIC_VECTOR(NBITS-1 downto 0);
        clk_out : out STD_LOGIC  -- Reloj de salida, dependiente del valor en memoria.
    );
end clk_rom;

Volviendo a la primera sección del artículo, sobre escalas pares e impares, sabemos que necesitaremos el valor de la escala y su mitad, para definir alto y bajo del reloj. Para ello se crearán dos señales, escala_num (que será el valor de la ROM convertido a número) y escala_mitad (la mitad del valor recibido), mismas que serán utilizadas para las operaciones de conteo (contador irá de 0 a escala_num).

-- Señal utilizada para procesamiento interno de la señal de salida.
signal temporal    : STD_LOGIC;
-- Señal que cubre el rango que puede alcanzar la ROM.
signal contador    : integer range 0 to (2**(NBITS-4))-1 := 0;
-- Transformación de la escala de entrada a tipo numérico para el uso de operadores aritméticos.
signal escala_num  : UNSIGNED(NBITS-1 downto 0) := (others => '0');
-- La mitad de la escala de entrada, que marca el tiempo en alto.
signal escala_mitad: UNSIGNED(NBITS-1 downto 0) := (others => '0');

Lo único que queda es el proceso de conteo, de 0 a escala_mitad en alto y de escala_mitad a escala_num en bajo. Pero, ¿cómo calculamos la mitad? ¿cómo se realiza una división en el FPGA? Por fortuna, no es necesario realizar ninguna división. Simplemente hay que recorrer todos los bits a la derecha mediante una operación de corrimiento de bits, con una simple línea de código (línea 3 del listado 6):

-- Actualización de los datos de escala.
escala_num   <= UNSIGNED(escala);           -- Conversión de vector a número.
escala_mitad <= shift_right(escala_num, 1); -- División entre 2, eliminando decimales.

Y, por último, pegamos todo e incluimos el proceso para cambiar el valor del estado en base al contador y nuestros dos límites (siguiendo el ejemplo del listado 2).

----------------------------------------------------------------------------------
-- Compañía:            Estado Finito
-- Ingeniero:           Carlos Ramos
-- 
-- Fecha de creación:   2014/04/24 17:41:30
-- Nombre del módulo:   clk_rom - Behavioral 
-- Comentarios adicionales: 
--   Este divisor de frecuencia toma sus valores de una memoria ROM que contiene
--   los valores de los contadores. Por lo tanto, el rango de frecuencias depende
--   de la ROM.
--
-- Comentarios adicionales:
--   Se puede encontrar más información en la siguiente dirección:
--   https://www.estadofinito.com/metronomo-en-vhdl-2/
--
-- Revisión:
--   Revisión 0.01 - Archivo creado.
----------------------------------------------------------------------------------
 
library IEEE;
use IEEE.NUMERIC_STD.ALL;
use IEEE.STD_LOGIC_1164.ALL;
 
entity clk_rom is
    GENERIC (
        NBITS   : integer := 28 -- Cantidad de bits que tiene cada registro en la ROM.
    );
    PORT (
        clk     : in  STD_LOGIC; -- Reloj de entrada de 3.125MHz.
        reset   : in  STD_LOGIC;
        escala  : in  STD_LOGIC_VECTOR(NBITS-1 downto 0);
        clk_out : out STD_LOGIC  -- Reloj de salida, dependiente del valor en memoria.
    );
end clk_rom;
 
architecture Behavioral of clk_rom is
    -- Señal utilizada para procesamiento interno de la señal de salida.
    signal temporal    : STD_LOGIC;
    -- Señal que cubre el rango que puede alcanzar la ROM.
    signal contador    : integer range 0 to (2**(NBITS-4))-1 := 0;
    -- Transformación de la escala de entrada a tipo numérico para el uso de operadores aritméticos.
    signal escala_num  : UNSIGNED(NBITS-1 downto 0) := (others => '0');
    -- La mitad de la escala de entrada, que marca el tiempo en alto y bajo.
    signal escala_mitad: UNSIGNED(NBITS-1 downto 0) := (others => '0');
begin
    -- Actualización de los datos de escala.
    escala_num   <= UNSIGNED(escala);           -- Conversión de vector a número.
    escala_mitad <= shift_right(escala_num, 1); -- División entre 2, eliminando decimales.

    -- Procesamiento para el divisor de frecuencia.
    divisor_frecuencia: process (clk, reset) begin
        if (reset = '1') then
            temporal <= '0';
            contador <= 0;
        elsif rising_edge(clk) then
            if (contador = escala_num) then
                contador <= 0;
            else
                if (contador < escala_mitad) then
                    -- Primera mitad, o casi mitad, en alto.
                    temporal <= '1';
                else
                    -- Segunda mitad, o poco más, en bajo.
                    temporal <= '0';
                end if;
                contador <= contador + 1;
            end if;
        end if;
    end process;
 
    -- Asignación de la señal de salida.
    clk_out <= temporal;
end Behavioral;

[wpdm_file id=15]

Así que ahí está, el código que recibe un dato de la ROM y produce una frecuencia distinta según el dato recibido… o al menos eso debería hacer si se uniera con la ROM mediante un PORT MAP

La ROM y el divisor

En el listado 8 se une la ROM creada en una entrada pasada con el nuevo componente del divisor con valores de la ROM. Además, se utiliza un divisor de frecuencia de 50MHz a 3.125MHz como entrada para el nuevo divisor, con el fin de implementar las frecuencias de 1 a 512 BPM.

El código del divisor de 50 a 3.125MHz no se incluye en la entrada, pero se incluye junto con los demás archivos para su descarga.

----------------------------------------------------------------------------------
-- Compañía:            Estado Finito
-- Ingeniero:           Carlos Ramos
-- 
-- Fecha de creación:   2014/05/08 22:15:22
-- Nombre del módulo:   clk_rom_1_512BPM - Behavioral 
-- Comentarios adicionales: 
--   Este componente une la ROM de 512 localidades de 28 bits, con el divisor de
--   frecuencia que toma valor de la ROM, con un reloj de 3.125MHz para lograr un
--   "divisor de frecuencia variable" de 1 a 512 BPM.
--
-- Comentarios adicionales:
--   Se puede encontrar más información en la siguiente dirección:
--   https://www.estadofinito.com/metronomo-en-vhdl-2/
--
-- Revisión:
--   Revisión 0.01 - Archivo creado.
----------------------------------------------------------------------------------

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity clk_rom_1_512BPM is
    GENERIC (
        NBITS   : integer := 28 -- Cantidad de bits que tiene cada registro en la ROM.
    );
    PORT (
        clk     : in  STD_LOGIC; -- Reloj de entrada de 50MHz.
        reset   : in  STD_LOGIC; -- Señal de reset.
        addr    : in  STD_LOGIC_VECTOR(8 downto 0); -- Dirección de la ROM.
        clk_out : out STD_LOGIC  -- Reloj de salida.
    );
end clk_rom_1_512BPM;

architecture Behavioral of clk_rom_1_512BPM is
    -- Señal de 3.125MHZ para entrar en divisor de frecuencia con ROM.
    signal clk3M125:  STD_LOGIC := '0';
    -- Señal intermedia para pasar de la ROM al componente del divisor.
    signal escala:    STD_LOGIC_VECTOR(NBITS-1 downto 0);
    -- Señal para habilitar lectura de la ROM.
    signal rom_en:    STD_LOGIC := '0';
begin
    -- La ROM se habilita siempre que no está el estado de reset.
    rom_en <= NOT reset;

    clk3M125Hz_i: entity work.clk3M125Hz(Behavioral)
        PORT MAP(clk, reset, clk3M125);
    rom512_28b_i: entity work.rom512_28b(Behavioral)
        PORT MAP(clk, rom_en, addr, escala);
    clk_rom_i: entity work.clk_rom(Behavioral)
        PORT MAP(clk3M125, reset, escala, clk_out);
end Behavioral;

En el listado 8 existe una pequeña diferencia al declarar los PORT MAP. En primera, el código del componente no está en ningún lado. Esto se debe a que automáticamente todos los componentes se añaden a la biblioteca work, misma que podemos utilizar para hacer referencia al componente. Por ejemplo, las líneas 46 y 47 crean la instancia de un reloj a 3.125Hz, por el simple hecho de llamar a work y el nombre del componente.

La figura 1 muestra el diagrama esquemático generado automáticamente por las herramientas de síntesis.

Figura 1: Diagrama esquemático de los tres componentes interconectados.

Figura 1: Diagrama esquemático de los tres componentes interconectados.

Ahora vamos a realizar una simulación, para ver cómo funciona el nuevo componente todo unido. Pero antes, despejemos una pregunta que quizá tengan.

¿Es en realidad un divisor de frecuencia variable?

Respuesta corta: No. En realidad, estamos cargando una constante (entre las 512 disponibles) para hacer un divisor de frecuencia, sólo que este divisor puede tener muchos más valores.

El sistema es totalmente determinista, el divisor de frecuencia recibe un numerito, y lo utiliza para producir una salida. ¡Ni siquiera la ROM es una memoria! Si no hay memoria, si no hay circuitos secuenciales, ¿cómo podría ser variable?

Respuesta larga: el divisor de frecuencia, a pesar de que puede otorgar a la salida 512 frecuencias diferentes, es un sistema combinacional y, por lo tanto, no es un divisor de frecuencia variable.

Sin embargo, lo observamos como tal debido a que, ¡oigan!, cambia su valor. La ROM no es memoria, y este nuevo componente no es un divisor de frecuencia variable. Pero, como parece uno, le dejaremos ese nombre.

Simulando para saber si funciona

El código del archivo de simulación no se incluye en la entrada, pero está en el paquete de descargas. Se realizan dos comprobaciones: una para 200 BPM y otra para 300 BPM (sería un fastidio esperar a que se complete toda la simulación para ver 1BPM). Aquí está el vínculo de descarga nuevamente:

[wpdm_file id=16]

Para 200BPM la frecuencia de salida esperada es 3.333333Hz, o el equivalente a 300ms. En el caso de los 300BPM, la frecuencia de salida esperada es 5Hz, o 200ms. En la figura 2 se observa que el periodo entre un flanco y otro corresponde, efectivamente, a 300ms.

Figura 2: Frecuencia de 300ms o 200BPM.

Figura 2: Frecuencia de 300ms o 200BPM.

Para la figura 3 la frecuencia entre un flanco y otro es de 200ms, correspondiente a los 5Hz o 300BPM (también como se esperaba).

Figura 3: Frecuencia de 200ms o 300BPM.

Figura 3: Frecuencia de 200ms o 300BPM.

Nada como un osciloscopio para estar seguros de que funciona

Para implementar el componente en la Basys2 se utilizó el archivo de implementación del listado 9.

NET  "clk"         LOC = "B8";
NET  "reset"       LOC = "G12";
# Señal de reloj de salida ############
#   Se puede asignar a un LED o a un 
#   pin para ver señal en osciloscopio.
#NET  "clk_out"     LOC = "M5"; # LED.
NET  "clk_out"     LOC = "B2"; # Pin.
# Entrada de BPM ######################
#   Sólo se usan 8 bits de dirección
#   debido a limitaciones en botones 
#   en la tarjeta Basys2.
NET  "addr<0>"     LOC = "P11";
NET  "addr<1>"     LOC = "L3";
NET  "addr<2>"     LOC = "K3";
NET  "addr<3>"     LOC = "B4";
NET  "addr<4>"     LOC = "G3";
NET  "addr<5>"     LOC = "F3";
NET  "addr<6>"     LOC = "E2";
NET  "addr<7>"     LOC = "N3";
NET  "clk_out"     LOC = "M5";

En la figura 4 se muestra el FPGA con diversos BPM, y su respectiva frecuencia en el osciloscopio. La tabla 1 muestra el resumen de los resultados del osciloscopio. Por lo tanto, se puede observar que el metrónomo en FPGA se comporta como se esperaba.

Tabla 1: Resumen de datos obtenidos con FPGA y osciloscopio.
BPM en FPGA Frecuencia en osciloscopio BPM calculados (Frecuencia × 60)
50 830mHz 49.8
100 1.667Hz 100.02
200 3.333Hz 199.98
250 4.17Hz 250.02
Figura 4: Diferentes frecuencias vistas en el osciloscopio.

Figura 4: Diferentes frecuencias vistas en el osciloscopio.

Quizá ya lo vieron…

En las imágenes mostradas con el osciloscopio se puede observar que en los cuatro visualizadores de siete segmentos de la Basys2 se indica la cantidad de BPM que el metrónomo está entregando. Justo ahora la señal se puede observar solamente mediante un osciloscopio, simulación o, si son muy muy buenos con la vista, calcular la frecuencia con ayuda de un LED.

En la siguiente entrada hablaremos sobre cómo convertir un número binario a su representación en BCD y, de ahí, pasarlo a visualizadores de siete segmentos. Dicho sea de paso, tal número corresponderá al número de BPM. Con la siguiente entrada, la tercera parte del metrónomo, el proyecto se dará por terminado.

Retomaremos este proyecto el día 23 de mayo, en la entrada titulada Metrónomo en VHDL (3 de 3): Ahora me ves. Por cierto, si no quieres esperar dos semanas, puedes recibir el código del metrónomo como se ve en las imágenes justo en tu correo, solamente únete a la lista de correo de Estado Finito.

You Might Also Like

No hay comentarios

Deja tu comentario