VHDL

Metrónomo en VHDL (3 de 3): Ahora me ves

Figura 1: Simulación del metrónomo a 1 BPM.

En esta entrada final se completa el primer diseño del metrónomo en VHDL. Para ello, se unen los módulos anteriormente creados para generar la frecuencia y mostrarla en los visualizadores de siete segmentos. Además, se añade un nuevo componente, un contador con dos botones, y se corrijen unos detalles previos, en específico:

  • qué pasa con el valor 512 en los visualizadores, y
  • la señal de reloj para indicar el ritmo.

Así que iniciamos pegando todo lo que ya tenemos, para posteriormente ir atacando cada uno de los detalles adicionales.

Descargar codigo metronomo

Pegándolo todo (desde la ROM hasta binario a siete segmentos)

Para llegar aquí hemos creado unos cuantos componentes, que corresponden a:

¡Ahora es tiempo de unirlos todos y disfrutar del resultado! Sin más preámbulo, el código del listado 1 es el que une todos los componentes previamente diseñados (mediante instrucciones PORT MAP), con explicación en comentarios.

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

entity metronomo is
	PORT (
        clk      : in  STD_LOGIC; -- Reloj de entrada de 50MHz.
        reset    : in  STD_LOGIC; -- Señal de reset.
	    direccion: in  STD_LOGIC_VECTOR(8 downto 0); -- Dirección a leer en la ROM
		d7s      : out STD_LOGIC_VECTOR(7 downto 0);
		MUX      : out STD_LOGIC_VECTOR(3 downto 0);
        clk_out  : out STD_LOGIC  -- Reloj de salida.
    );
end metronomo;

architecture Behavioral of metronomo is
    -- Señal para comunicación entre ROM y divisor con ROM.
	signal escala:    STD_LOGIC_VECTOR(27 downto 0);
    -- Señal para el reloj de 3.125MHz (como entrada a divisor con ROM).
	signal clk3M125:  STD_LOGIC := '0';
	-- Señal para el reloj de 5Hz (como entrada al contador).
	signal rom_en:    STD_LOGIC := '0';
    -- Señal para almacenar la cantidad de BPM.
	signal bpm:       STD_LOGIC_VECTOR(8 downto 0);
    -- Señal para pasar un número binario a BCD.
	signal num_bcd:   STD_LOGIC_VECTOR(10 downto 0);
    -- Dígitos BCD para ser mostrados como siete segmentos.
	signal D0, D1, D2, D3: STD_LOGIC_VECTOR(3 downto 0);
begin
    -- Reloj de 3.125MHz, que será la entrada para el divisor
    -- de frecuencia implementado con la ROM.
	clk3M125Hz_i: entity work.clk3M125Hz(Behavioral)
		PORT MAP(clk, reset, clk3M125);
	
    -- Divisor de frecuencia que entrega una salida de 1 a 512
    -- pulsos por minuto, según la dirección de la ROM.
    -- En general: BPM = DIRECCION + 1.
	clk_rom_i: entity work.clk_rom(Behavioral)
		PORT MAP(clk3M125, reset, escala, clk_out);
	rom512_28b_i: entity work.rom512_28b(Behavioral)
		PORT MAP(clk, rom_en, direccion, escala);
	
    -- Convertidor de binario a BCD a siete segmentos.
    -- Se encarga de recibir la cantidad de BPM en binario,
    -- convertirla a tres dígitos en BCD, y enviar esos datos
    -- a los visualizadores de siete segmentos.
    bin2bcd9_i: entity work.bin2bcd9(Behavioral)
		PORT MAP(bpm, num_bcd);
	d7s_i: entity work.siete_segmentos_4bits_completo(Behavioral)
		PORT MAP(clk, reset, D0, D1, D2, D3, d7s, MUX);

    -- La cantidad de BPM es igual a la dirección más uno.
	bpm <= direccion + 1;
    -- La ROM está habilitada siempre y cuando no esté en reset.
	rom_en <= NOT reset;
    -- Se asignan las señales que representarán los datos en siete segmentos.
	D3 <= "0000";
	D2 <= "0" & num_bcd(10 downto 8);
	D1 <= num_bcd(7 downto 4);
	D0 <= num_bcd(3 downto 0);
end Behavioral;

Y su archivo de implementación correspondiente se muestra en el listado 2.

NET  "clk"        LOC = "B8";
NET  "reset"      LOC = "G12";
# Segmentos del visualizador. #########
NET  "d7s<7>"     LOC = "N13"; # dp
NET  "d7s<6>"     LOC = "M12"; # g
NET  "d7s<5>"     LOC = "L13"; # f
NET  "d7s<4>"     LOC = "P12"; # e
NET  "d7s<3>"     LOC = "N11"; # d
NET  "d7s<2>"     LOC = "N14"; # c
NET  "d7s<1>"     LOC = "H12"; # b
NET  "d7s<0>"     LOC = "L14"; # a
# Multiplexor #########################
NET  "MUX<3>"     LOC = "F12";
NET  "MUX<2>"     LOC = "J12";
NET  "MUX<1>"     LOC = "M13";
NET  "MUX<0>"     LOC = "K14";
# Entrada de BPM ######################
NET  "num_bin<0>" LOC = "P11";
NET  "num_bin<1>" LOC = "L3";
NET  "num_bin<2>" LOC = "K3";
NET  "num_bin<3>" LOC = "B4";
NET  "num_bin<4>" LOC = "G3";
NET  "num_bin<5>" LOC = "F3";
NET  "num_bin<6>" LOC = "E2";
NET  "num_bin<7>" LOC = "N3";
NET  "num_bin<8>" LOC = "A7";
# Salida del metrónomo ################
NET  "clk_out"    LOC = "M5";

El comportamiento del sistema según el código anterior se muestra en el video colocado debajo:

En el video se muestran dos problemas:

  1. al hacer un cambio de valor, el metrónomo tarda en responder y
  2. para el valor de 512 BPM, el cual sí se implementa correctamente, se muestra un valor de 0000 en los visualizadores.

Veamos primeramente el segundo problema.

¿Por qué el último valor es “0000”?

Tras ver la demostración, debemos hacer un poco de memoria para recordar dos cosas:

  1. Las direcciones en la ROM van de 0 a 511 (un total de 512 elementos).
  2. La cantidad de BPM van de 1 a 512 (también un total de 512 elementos también).

La pequeña diferencia está en ese valor inicial en 1, pues las direcciones están en el rango de

\( 0b0~0000~0000 \leq \mbox{direccion} \leq 0b1~1111~1111, \)

mientras que la cantidad de pulsos por minutos está en

\( 0b0~0000~0001 \leq \mbox{BPM} \leq 0b10~0000~0000, \)

y, como nuestro contador de BPM tiene solamente 9 bits, el décimo bit (que indica que el valor es 512) simplemente no se ve, no existe, desaparece. Para este pequeño problema existen tres “soluciones” posibles (que se me ocurren en este momento):

  1. Dejarlo ser, pues sinceramente dudo que alguien llegue a utilizar el ritmo de 512 BPM.
  2. Limitar el contador hasta 510 en lugar de 511, para evitar que se despliegue el cero (equivalente a metrónomo de 1 a 511 BPM).
  3. Asignar un bit más a la señal de bpm, para mostrar el resultado correctamente. Esto implicaría añadir un bit más también al módulo bin2bcd9.

La primera solución no sería buena, pues sería un tanto extraño ver una frecuencia de 0 BPM y un LED parpadeando bastante. La tercera solución nos llevaría a cambiar un tanto dos módulos ya hechos (si vamos a cambiar bin2bcd9 para que acepte 10 bits, deberíamos cambiarlo para que pueda mostrar cuatro dígitos pues ya podría contar hasta 1023).

Por lo tanto, la decisión de diseño tomada es la segunda: disminuir el valor máximo en el contador. Y, dado que 511 no parece un buen número, redondeamos en 500 BPM (un máximo de 499 en el contador). Es hora de diseñar el componente.

Contando hacia arriba, contando hacia abajo

Antes habíamos realizado un contador de 0 a 127 para el controlador del servomotor, y este contador es similar, con las siguientes dos diferencias:

  1. el límite es 499, mismo que se establece en las línea 22 (y su declaración en la línea 16 para asignar suficientes bits).
  2. el valor predeterminado es 59, como se establece en las líneas 16 y 20.
library IEEE;
use IEEE.NUMERIC_STD.ALL;
use IEEE.STD_LOGIC_1164.ALL;

entity contador_up_down_0_499 is
    PORT(
        clk     : IN  STD_LOGIC;
        reset   : IN  STD_LOGIC;
        cnt_up  : IN  STD_LOGIC;
        cnt_down: IN  STD_LOGIC;
        contador: OUT STD_LOGIC_VECTOR(8 DOWNTO 0)
    );
end contador_up_down_0_499;

architecture Behavioral of contador_up_down_0_499 is
    signal temporal: UNSIGNED(8 DOWNTO 0) := "000111011";
begin
    proceso_contador: process (clk, reset, cnt_up, cnt_down) begin
        if (reset = '1') then
            temporal <= "000111011";
        elsif rising_edge(clk) then
            if (cnt_up = '1' AND temporal < 499) then
                temporal <= temporal + 1;
            elsif (cnt_down = '1' AND temporal > 0) then
                temporal <= temporal - 1;
            end if;
        end if;
    end process;
 
    contador <= STD_LOGIC_VECTOR(temporal);
end Behavioral;

Y para la implementación de este nuevo módulo hacemos unos ligeros cambios al listado 1. Dichos cambios se indican en el listado 4.

-- En la lista de puertos, entre las líneas 6 y 10 del listado 1:
--     Eliminar la entrada *direccion* e insertar las siguientes:
btn_inc : in  STD_LOGIC; -- Incrementa la cantidad de BPM.
btn_dec : in  STD_LOGIC; -- Decrementa la cantidad de BPM.

-- Ahora que ya no existe la dirección como entrada, declararla como señal, entre las líneas 16 y 27.
signal direccion: STD_LOGIC_VECTOR(8 downto 0);

-- En la arquitectura comportamental, entre las líneas 29 y 59 del listado 1, añadir el PORT MAP.
contador_dir_i: entity work.contador_up_down_0_499(Behavioral)
    PORT MAP(clk, reset, btn_inc, btn_dec, direccion);

El código del listado 3 está disponible para su descarga individual:

Descargar codigo 3

Y el archivo de implementación sería sustituido completamente al contenido del listado 5.

NET  "clk"         LOC = "B8";
NET  "reset"       LOC = "G12";
NET  "btn_inc"     LOC = "M4";
NET  "btn_dec"     LOC = "C11";
# Segmentos del visualizador. #########
NET  "d7s<7>"   LOC = "N13"; # dp
NET  "d7s<6>"   LOC = "M12"; # g
NET  "d7s<5>"   LOC = "L13"; # f
NET  "d7s<4>"   LOC = "P12"; # e
NET  "d7s<3>"   LOC = "N11"; # d
NET  "d7s<2>"   LOC = "N14"; # c
NET  "d7s<1>"   LOC = "H12"; # b
NET  "d7s<0>"   LOC = "L14"; # a
# Multiplexor #########################
NET  "MUX<3>"      LOC = "F12";
NET  "MUX<2>"      LOC = "J12";
NET  "MUX<1>"      LOC = "M13";
NET  "MUX<0>"      LOC = "K14";
# Salida del metrónomo ################
NET  "clk_out"     LOC = "M5";

Y lo que eso hace es algo como lo que se muestra en el siguiente video:

Donde solamente aparecen dos números: el 1 y el 500. Eso está bien, es lo esperado, ¿por qué?

Sólo veo 0001 y 0500, ¿cómo está contando?

Está contando normal, sólo que el conteo va muy rápido. De hecho, ante nuestros ojos mortales, se teletransporta. Pero créeme: estamos contando. Sólo hay un ligero problemita: lo que para nosotros es probablemente una milésima de segundo, para el FPGA de 50MHz son alrededor de unos 50000 ciclos. Recordemos que:

\( 1 ms = 1~000 \mu s = 1~000~000 ns \)

Y si tenemos un reloj de 50MHz (o 20ns),

\( \mbox{ciclos} = \dfrac{\mbox{tiempo de boton presionado}}{\mbox{tiempo entre pulso de reloj}} = \dfrac{1~000~000 ns}{20 ns} = 50~000 ~ \mbox{ciclos} \)

Así es como, mientras nosotros muy apenas y tocamos el botón por un milisegundo, el conteo se realiza desde un extremo hasta otro sin siquiera poder darnos cuenta.

De nuevo, existen muchas formas de solucionar ese problema. Por ejemplo:

  1. Contar una vez cada que se presione el botón (es decir, almacenamos el valor del botón y no volvemos a contar hasta que pasemos nuevamente por cero).
  2. Contar solamente si detectamos el botón presionado durante cinco ciclos (almacenando el valor en flip-flops).
  3. Simplemente disminuimos la frecuencia del reloj. Al ser sistema síncrono, el contador cambia al recibir la señal de reloj.

La más fácil, por supuesto, es utilizar un divisor de frecuencia para disminuir la velocidad del contador. Aquí depende de qué tanto queramos avanzar cada vez que presionamos el botón. Personalmente creo que generar un divisor con una frecuencia de salida de 5Hz es suficiente para satisfacer las demandas de conteo. Al incluir el nuevo componente en el diseño, el conteo se realiza de una forma que podemos ver y disfrutar:

El código de esta parte no lo incluyo aquí en la página, pero corresponde a la implementación que se encuentra en el paquete de descarga:

[wpdm_package id=”1361″]

¿Cómo dijimos que era la señal de salida?

Cuadrada. Mismo tiempo en bajo, mismo tiempo en alto.

Y. Eso. Es. Un. Error. Nefasto.

Vamos al caso extremo: 1 BPM. En la figura 1 se muestra la imagen de la simulación, treinta segundos en alto, treinta segundos en bajo.

Figura 1: Simulación del metrónomo a 1 BPM.

Figura 1: Simulación del metrónomo a 1 BPM.

Si escuchamos un metrónomo, nos daremos cuenta de que el sonido ocurre durante un breve periodo. Un pequeño tic, tic, tic, cada cambio.

Es decir, no queremos un largo tiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiic.

Entonces, ¿de cuánto debe ser ese sonido? ¿qué frecuencia debe tener? De entrada, debe ser mayor a 500 BPM (mayor a 8.53 Hz). Dado que soy amante de la base 10 (tanto como de la base 2), seleccionaré la frecuencia de 10Hz. Eso implica un pequeño cambio en el componente del divisor de frecuencia basado en la ROM: la cantidad de tiempo en alto será una constante, siempre igual para cualquier frecuencia (el tiempo en bajo es el que incrementará).

El cambio se realiza en el módulo del divisor de frecuencia, motivo por el cual crearemos una segunda revisión del módulo (preservando el archivo anterior en el repositorio). El código modificado se muestra en el listado 6, donde las modificaciones fueron:

  • La señal escala_mitad ya no es necesaria, motivo por el cual es removida (líneas 24 y 25 solamente muestran un comentario).
  • Dado que ya no existe escala_mitad, ya no realizamos procesamiento alguno con ella (líneas 29 y 30)
  • El contador de tiempo en alto ahora es constante para producir la señal de 10Hz (línea 41).
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 señal de la mitad de la escala ya no es necesaria pues ya tenemos una frecuencia
    -- constante de 10Hz.
begin
	-- Actualización de los datos de escala.
	escala_num   <= UNSIGNED(escala);           -- Conversión de vector a número.
    -- Ya no hay escala_mitad, ni necesidad de calcularla.
	--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 < 312500) then
					-- Tiempo de 10Hz en alto.
					temporal <= '1';
				else
					-- Todo lo demá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;

El final… ¿o lo es?

Aquí termina el desarrollo del metrónomo, al menos de momento. Algunas veces me digo, “si Harry Potter, Crepúsculo, y Los Juegos del Hambre han tenido dos partes en la etapa final, ¿por qué no hacer algo como Metrónomo en VHDL (3 de 3 [Parte 2]): Colofón“. La verdad es que de momento no sé por qué ocurre el fenómeno de “quedarse atascado” o “tardarse en reaccionar”.

Tan pronto como tenga una respuesta, y estoy totalmente abierto a sugerencias, hablaré sobre arreglo de diversos bugs. Estoy seguro que otros problemas se presentarán, y entonces será el momento de volver a la acción, de crear Metrónomo en VHDL (Repercusiones): Arreglo de bugs. Hasta entonces, cuento con su apoyo para encontrar desperfectos y cazarlos sin piedad.

You Might Also Like

2 Comentarios

  • Responder
    JOSE EUSEBIO LOPEZ
    diciembre 1, 2016 at 11:38 am

    Buenas tardes profesor:

    Me gustaría que subiera un ejemplo donde se pueda ir guardando los tiempos
    ejemplo ir guardando los tiempo de llegada de unos atletas y después mostrarlo

    • Responder
      Carlos Ramos
      diciembre 2, 2016 at 1:44 pm

      José, ¿cómo planeas que éso suceda? ¿cómo lo imaginas? Dime más sobre tu idea.

    Deja tu comentario