Teach Lecciones con laboratorios remotos

Lección Teach

Lógica digital en la DE1-SoC con VHDL (4/6): contadores y registros opcionales de desplazamiento

El alumnado construye un contador y un divisor de reloj en VHDL, y prueba extensiones opcionales BCD y de desplazamiento si hay tiempo.

  • Altera DE1-SoC
  • 40 min
  • Secundaria (16–18 años)
  • Español
  • Sistemas digitales y FPGA
Altera DE1-SoC
Altera DE1-SoC

Resultados de aprendizaje

  • Explicar qué hace un divisor de reloj y usarlo para que el comportamiento de 50 MHz sea visible en cámara.

  • Construir un contador de 4 bits y leer su valor en LED y en un display de 7 segmentos.

  • Reconocer BCD y registros de desplazamiento como extensiones opcionales si hay tiempo.

  • Reconocer problemas de anchura de registros, como una constante que no cabe en el registro declarado.

Vista previa de la actividad del estudiante

Contenido de la actividad

Solo vista previa. En una sesión de clase, los estudiantes pueden completar respuestas y entregar su trabajo al docente.

1

De almacenar a moverse: por qué necesitamos un divisor de reloj

10 min

En la Lección 3 construiste circuitos que recuerdan un bit o un byte: un biestable y un registro de carga paralela. Cada uno actualiza el valor almacenado en un flanco de reloj. En esta lección reutilizarás esa idea para construir un circuito que se mueve solo en cada flanco: un contador que sube 0, 1, 2, 3, .... Si hay tiempo, una extensión opcional usa la misma idea para un registro de desplazamiento que desliza sus bits lateralmente.

El reloj de la placa es CLOCK_50, una onda cuadrada de 50 MHz: 50.000.000 flancos de subida cada segundo. Si un contador avanzara una vez por cada flanco de CLOCK_50, recorrería los 16 valores de un contador de 4 bits unos tres millones de veces por segundo. No verías pasos legibles, solo un borrón de LED, y la cámara remota, que captura solo unos pocos fotogramas por segundo, no tendría ninguna posibilidad de seguirlo.

Por eso lo ralentizamos con un divisor de reloj. Un divisor de reloj es, en el fondo, otro contador: cuenta flancos de CLOCK_50 y, cuando llega a un valor terminal DIV, invierte una señal más lenta llamada slow_clk y vuelve a empezar. Cuanto mayor sea DIV, más lento será slow_clk. Después usamos slow_clk, en lugar de CLOCK_50, para mover el circuito principal: el contador de 4 bits, o el registro de desplazamiento opcional.

Como en cada lección, los diseños secuenciales deben declarar el reloj. La plantilla inicial de blink.vhd lista solo interruptores, botones y LED, y no tiene reloj, así que cada entidad de esta lección añade CLOCK_50 : in std_logic a la lista de puertos. Las señales de la placa siguen siendo las mismas: SW(9 downto 0) (interruptor arriba = 1), KEY(3 downto 0) (activo en bajo: pulsado lee 0), LEDR(9 downto 0) (activo en alto: encendido con 1) y HEX0-HEX5 (siete segmentos activos en bajo).

Un divisor de reloj toma el CLOCK_50 rápido de 50 MHz y lo introduce en un contador que cuenta hasta un valor terminal DIV; cada vez que llega a DIV cambia una salida lenta slow_clk, convirtiendo millones de flancos por segundo en un flanco lento que los ojos y la cámara pueden seguir.

Un divisor de reloj cuenta flancos rápidos de CLOCK_50 e invierte una señal mucho más lenta, slow_clk, cuando llega al valor terminal DIV. Aumentar DIV hace que slow_clk sea más lento. El circuito real se mueve con slow_clk, de modo que su movimiento se vuelve visible.

Un contador de 4 bits conectado directamente a CLOCK_50 avanzaría unas 3 millones de veces por segundo, mientras la cámara remota captura solo unos pocos fotogramas por segundo. Qué verías realmente en los LED, y por qué?

Si duplicaras el valor terminal DIV (por ejemplo de 33,333,333 a 66,666,666), cómo cambiaría aproximadamente la *velocidad* de avance del contador, y por qué? Responde en una frase.

2

Ejercicio 2-C: contador de 4 bits con divisor de reloj

12 min

Un contador es un registro cuyo siguiente valor es simplemente el valor actual más uno. Un contador de 4 bits guarda valores de 0 a 15; después de 15 vuelve automáticamente a 0, porque 15 + 1 = 16 = 10000 en binario y solo se conservan los cuatro bits bajos (0000). No hace falta escribir código especial para ese retorno: lo produce la anchura del registro.

El diseño de abajo tiene dos partes. El divisor cuenta flancos de CLOCK_50 hasta DIV e invierte slow_clk. El contador avanza una unidad en cada flanco de subida de slow_clk, y KEY(0) lo resetea a 0 (activo en bajo: pulsado significa reset). La cuenta se muestra en LEDR(3 downto 0) en binario y también en HEX0 como un dígito hexadecimal (0-9, luego A-F para 10-15).

El tiempo, dicho con precisión. Con DIV = 33,333,333, el divisor invierte slow_clk después de unos 0,67 segundos: 33.333.333 flancos sobre 50.000.000 por segundo son aproximadamente dos tercios de segundo. Un paso del contador ocurre solo en un flanco de subida de slow_clk, y una onda cuadrada tiene un flanco de subida por *periodo completo* (sube y baja). Por tanto, el contador avanza aproximadamente cada 1,33 segundos, alrededor de 0,75 Hz. Espera ver un valor nuevo un poco más de una vez por segundo.

Un problema típico de anchura de registros. El registro del divisor aquí tiene 27 bits. Es deliberado. Un registro de 26 bits solo puede guardar hasta 2^26 - 1 = 67,108,863. Un valor como DIV = 100,000,000 no cabe en 26 bits, así que un diseño con esa anchura estaría roto antes de ejecutarse. Con 27 bits, el registro llega a 2^27 - 1 = 134,217,727, por lo que tanto DIV = 100,000,000 (más lento) como DIV = 8,333,333 (más rápido) caben y funcionan. Comprueba siempre que una constante cabe en el registro que declaraste.

Una advertencia honesta. Usar slow_clk como reloj de otro registro, como hacemos aquí, es un atajo didáctico frecuente porque hace visible la idea. En diseños de producción normalmente se mantiene todo en el único reloj rápido CLOCK_50 y se usa una habilitación de reloj (*clock enable*): un pulso de un ciclo que dice "actualiza en este flanco". Así todo el diseño queda en un solo reloj real, que las herramientas de síntesis analizan mejor. Aquí usamos el reloj dividido solo porque es la forma más clara de *ver* el contador moverse.

-- Plantilla de estudiante: completa las lineas TODO antes de sintetizar.
-- Lección 4 - Ejercicio 2-C: contador de 4 bits con divisor de reloj.
-- KEY(0)=reset. LEDR(3 downto 0)=count, HEX0=muestra la cuenta como dígito hexadecimal.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity blink is
    port (
        CLOCK_50 : in  std_logic;
        KEY      : in  std_logic_vector(3 downto 0);
        LEDR     : out std_logic_vector(9 downto 0);
        HEX0     : out std_logic_vector(6 downto 0)
    );
end entity blink;

architecture rtl of blink is
    constant DIV : unsigned(26 downto 0) := TODO_VISIBLE_DIVIDER;
    signal div_count : unsigned(26 downto 0) := (others => '0');
    signal slow_clk  : std_logic := '0';
    signal count     : unsigned(3 downto 0) := (others => '0');

    function seg(d : unsigned(3 downto 0)) return std_logic_vector is
    begin
        case d is
            when "0000" => return "1000000"; when "0001" => return "1111001";
            when "0010" => return "0100100"; when "0011" => return "0110000";
            when "0100" => return "0011001"; when "0101" => return "0010010";
            when "0110" => return "0000010"; when "0111" => return "1111000";
            when "1000" => return "0000000"; when "1001" => return "0010000";
            when "1010" => return "0001000"; when "1011" => return "0000011";
            when "1100" => return "1000110"; when "1101" => return "0100001";
            when "1110" => return "0000110"; when "1111" => return "0001110";
            when others => return "1111111";
        end case;
    end function;
begin
    process(CLOCK_50)
    begin
        if rising_edge(CLOCK_50) then
            if KEY(0) = '0' then
                div_count <= (others => '0');
                slow_clk <= '0';
            elsif div_count = TODO_DIVIDER_LIMIT then
                div_count <= (others => '0');
                slow_clk <= not slow_clk;
            else
                div_count <= div_count + 1;
            end if;
        end if;
    end process;

    process(slow_clk, KEY)
    begin
        if KEY(0) = '0' then count <= (others => '0');
        elsif rising_edge(slow_clk) then count <= TODO_NEXT_COUNT;
        end if;
    end process;

    LEDR(3 downto 0) <= TODO_LED_COUNT;
    LEDR(9 downto 4) <= (others => '0');
    HEX0 <= TODO_HEX_COUNT;
end architecture rtl;

Supón que usarás un registro divisor de 24 bits. Cuál es el mayor valor que puede guardar (muestra 2^24 - 1), y cabría todavía DIV = 33,333,333? Explícalo en una frase.

3

Ejecuta el contador y resetéalo

10 min

Construye el contador en la placa real, observa cómo sube, resetéalo y prueba los dos valores alternativos de divisor.

  1. Abre el laboratorio VHDL de la DE1-SoC. El editor muestra blink.vhd.

  2. Selecciona todo el contenido de blink.vhd y reemplázalo por el contador de 4 bits. Mantén el nombre blink. Observa que declara CLOCK_50 : in std_logic: la plantilla no añade el reloj por ti.

  3. Pulsa Sintetizar. Si una guía tapa el botón, ciérrala o sáltala primero. Espera a que termine (aprox. 1-3 minutos). Corrige cualquier typo y vuelve a sintetizar. Quartus puede mostrar muchas advertencias de temporización por este divisor didáctico; continúa solo cuando el estado final diga 0 errores.

  4. Pulsa Enviar a la FPGA y espera a la placa y la cámara.

  5. Observa LEDR(3 downto 0) y HEX0. Debe aparecer un valor nuevo algo más de una vez por segundo (aprox. cada 1,33 s). Confirma que los LED binarios coinciden con el dígito hexadecimal de HEX0 (por ejemplo, cuando HEX0 muestra C, LEDR(3 downto 0) debe leer 1100).

  6. Mantén pulsado KEY(0) para resetear la cuenta a 0, suéltalo y observa cómo vuelve a subir desde 0.

  7. Extensión opcional si queda tiempo: cambia DIV a 8,333,333 (to_unsigned(8_333_333, 27)), sintetiza, carga y observa que el contador va más rápido. Después prueba 100,000,000 (to_unsigned(100_000_000, 27)) y observa que va más lento. Esta extensión no es necesaria para la evidencia obligatoria.

Con DIV = 33,333,333, observa el contador y registra algunos valores. Para cada uno, escribe el patrón binario LEDR(3 downto 0) y el caracter que ves en HEX0 (0-9 y después A-F). Luego resetea con KEY(0) y confirma que la cuenta vuelve a 0.

Observación # LEDR(3 downto 0) (binario) Caracter en HEX0 Valor decimal (0-15)

Sube tu evidencia. Espera un momento en que HEX0 muestre el dígito C (decimal 12), captura una imagen de la cámara (LEDR(3 downto 0) debe leer 1100 al mismo tiempo) y adjúntala abajo.

Probaste DIV = 8,333,333 y DIV = 100,000,000. Qué valor hizo que el contador avanzara más rápido y, usando esos dos números, aproximadamente cuántas veces más rápido fue que el otro?

Supón que quisieras usar un contador como reloj que se mantuviera preciso dentro de más o menos un segundo durante un día completo de 24 horas. Además del contador, qué tendrías que hacer bien? Nombra el ingrediente clave y explica por qué importa.

4

Extensión opcional: modifica un contador BCD de 0 a 9

7 min

Un contador normal de 4 bits recorre 0-15 y muestra A-F en los seis valores superiores. Muchos displays reales quieren un único dígito decimal: contar 0, 1, 2, ..., 9 y volver directamente a 0. Un contador que cuenta solo 0-9 se llama BCD (*binary-coded decimal*).

El cambio es pequeño: en vez de dejar que el registro de 4 bits vuelva solo a 0 después de 15, detectas cuando la cuenta llega a 9 y fuerzas el siguiente valor a 0. Reemplaza el proceso del contador por esta forma:

process(slow_clk, KEY)
begin
    if KEY(0) = '0' then count <= TODO_RESET_COUNT;
    elsif rising_edge(slow_clk) then
        if count = TODO_LAST_BCD_DIGIT then count <= TODO_WRAP_VALUE;
        else                              count <= TODO_NEXT_COUNT;
        end if;
    end if;
end process;

Deja todo lo demás igual: divisor, asignaciones a LEDR/HEX0 y función seg. Ahora HEX0 debe recorrer 0 a 9 y nunca mostrar A-F.

  1. Edita tu entidad contador para que el proceso del contador vuelva a 0 después de 9, como se muestra arriba.

  2. Sintetiza, corrige cualquier typo y carga en la FPGA.

  3. Observa que HEX0 recorre 0, 1, 2, ... 9, 0, 1, .... Confirma que nunca llega a A. Pulsa KEY(0) para volver a 0 si quieres comprobar desde el inicio.

En el contador BCD, cuál es el valor de count en el flanco de reloj inmediatamente después de que HEX0 muestre 9, y qué línea de código lo fuerza?

5

Extensión opcional: registro de desplazamiento de 8 bits

9 min

Un registro de desplazamiento es una fila de biestables donde, en cada flanco de reloj, cada bit se mueve a su vecino y un bit nuevo entra por un extremo. Es la base de cómo funcionan muchos enlaces seriales: protocolos como UART, SPI e I2C envían datos bit a bit, y un registro de desplazamiento ensambla esos bits en un byte o separa un byte en bits.

El diseño de abajo es un registro de 8 bits shreg(7 downto 0). En cada flanco de subida de slow_clk, mientras el desplazamiento está habilitado, hace:

shreg <= TODO_SHIFT_LEFT_UPDATE;  -- TODO: shift left and bring SW(0) into bit 0

Lee con cuidado el lado derecho: toma los siete bits bajos shreg(6 downto 0), los mueve una posición hacia arriba e introduce SW(0) en el bit 0. Cada bit se mueve hacia el extremo alto, así que es un desplazamiento a la izquierda. Controles: SW(0) = bit serial que entra, KEY(0) = reset (activo en bajo), KEY(1) = habilitación de desplazamiento (activa en bajo: mantén KEY(1) pulsado para desplazar en cada tick).

Patrón esperado. Razónalo en vez de confiar en la orientación de la cámara. Empieza en 0000_0000. Pon SW(0) = 1 y deja pasar cuatro ticks: el registro se llena desde el bit 0 hacia arriba, dando 0000_0001, luego 0000_0011, 0000_0111, 0000_1111. Ahora pon SW(0) = 0 y deja pasar cuatro ticks más: entran ceros por el bit 0 y empujan los unos hacia arriba y fuera, dando 0001_1110, 0011_1100, 0111_1000 y finalmente 1111_0000. Así que después de cuatro 1 seguidos de cuatro 0, shreg(7 downto 0) es 1111_0000.

Valor de bits frente a lo que ves. Registra ambas cosas: el valor binario del registro y lo que muestra la cámara. El orden físico de los LED en la placa puede verse espejado respecto a cómo escribes los bits (shreg(7) en un lado, shreg(0) en el otro), así que los LED encendidos pueden parecer invertidos de izquierda a derecha frente al número 1111_0000. Eso es un efecto de etiqueta/orientación, no un error lógico: por eso anotarás el valor calculado junto a la foto.

-- Plantilla de estudiante: completa las lineas TODO antes de sintetizar.
-- Lección 4 - Ejercicio 2-D: registro de desplazamiento de 8 bits hacia la izquierda.
-- SW(0)=dato serie de entrada, KEY(0)=reset, KEY(1)=habilitar desplazamiento. Las entradas KEY son activas en bajo.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity blink is
    port (
        CLOCK_50 : in  std_logic;
        KEY      : in  std_logic_vector(3 downto 0);
        SW       : in  std_logic_vector(9 downto 0);
        LEDR     : out std_logic_vector(9 downto 0)
    );
end entity blink;

architecture rtl of blink is
    constant DIV : unsigned(26 downto 0) := TODO_VISIBLE_DIVIDER;
    signal div_count : unsigned(26 downto 0) := (others => '0');
    signal slow_clk  : std_logic := '0';
    signal shreg     : std_logic_vector(7 downto 0) := (others => '0');
begin
    process(CLOCK_50)
    begin
        if rising_edge(CLOCK_50) then
            if KEY(0) = '0' then
                div_count <= (others => '0'); slow_clk <= '0';
            elsif div_count = DIV then
                div_count <= (others => '0'); slow_clk <= not slow_clk;
            else
                div_count <= div_count + 1;
            end if;
        end if;
    end process;

    process(slow_clk, KEY)
    begin
        if KEY(0) = '0' then
            shreg <= (others => '0');
        elsif rising_edge(slow_clk) then
            if KEY(1) = '0' then shreg <= TODO_SHIFT_UPDATE;
            end if;
        end if;
    end process;

    LEDR(7 downto 0) <= TODO_SHIFT_OUTPUT;
    LEDR(9 downto 8) <= (others => '0');
end architecture rtl;

Partiendo de 0000_0000, con KEY(1) mantenido, aplica la secuencia de entrada SW(0) = 1, 1, 0, 1 durante cuatro ticks (el primer bit es el más antiguo). Hazlo tick a tick y escribe shreg(7 downto 0) después de los cuatro ticks.

6

Extensión opcional: haz marchar el bit por los LED

9 min

Construye el registro de desplazamiento y observa cómo un LED encendido avanza por la fila; después observa cómo lo siguen los ceros.

  1. Reemplaza el contenido de blink.vhd por la entidad del registro de desplazamiento de 8 bits. Mantén el nombre blink. La entidad declara CLOCK_50 : in std_logic.

  2. Sintetiza, corrige cualquier typo y carga en la FPGA.

  3. Pulsa y suelta KEY(0) una vez para resetear el registro a todo ceros (ningún LED encendido entre LEDR(7 downto 0)).

  4. Pon SW(0) arriba (entrada serial = 1) y mantén KEY(1) pulsado. En cada tick (aprox. cada 1,33 s) entra un nuevo LED encendido por un extremo y los encendidos avanzan. Deja pasar cuatro ticks: deberías tener cuatro LED encendidos (0000_1111).

  5. Ahora pon SW(0) abajo (entrada serial = 0), sigue manteniendo KEY(1), y deja pasar cuatro ticks más. Los ceros siguen a los unos hasta que el registro vale 1111_0000.

  6. Suelta KEY(1) cuando quieras para congelar el patrón y poder leerlo o fotografiarlo.

Resetea y luego mantén KEY(1). Registra el valor tick a tick: cuatro ticks con SW(0)=1 y luego cuatro ticks con SW(0)=0. Escribe shreg como ocho bits (shreg(7) a la izquierda, shreg(0) a la derecha). En la última columna, anota si los LED en la cámara parecen espejados respecto a los bits escritos.

Tick # SW(0) entrada `shreg(7 downto 0)` (8 bits) Cámara: ¿qué LED se ven / espejado?

Sube tu evidencia. Tras cuatro ticks con SW(0)=1 y cuatro ticks con SW(0)=0, suelta KEY(1) para congelar el patrón (shreg = 1111_0000), captura una imagen de la cámara, anota si los LED parecen espejados y adjunta la captura abajo.

Compara el valor binario que calculaste (1111_0000) con los LED encendidos en tu captura. Si parecen espejados, explica por qué es un efecto de etiqueta/orientación y no un fallo en la lógica de desplazamiento.

7

Opcional: desplazar a la derecha y contador en anillo

6 min

Desplaza en el otro sentido. Para hacer que el registro se desplace a la derecha en vez de a la izquierda, cada bit se mueve hacia el extremo bajo y el nuevo bit entra por arriba (shreg(7)). Solo cambia una línea. Sustituye la asignación de desplazamiento por:

shreg <= TODO_SHIFT_RIGHT_UPDATE; -- TODO: shift right and bring SW(0) into bit 7

Solo cambia esa línea. Ahora SW(0) entra en shreg(7), y los siete bits altos shreg(7 downto 1) bajan hacia shreg(6 downto 0).

Extensión: contador en anillo. Un contador en anillo realimenta la propia salida del registro hacia su entrada, de modo que un patrón fijo circula sin fin. Haz un anillo con desplazamiento a la izquierda que realimente el bit superior shreg(7) hacia el bit 0, e inicializa el registro a 0000_0001 para que un único LED encendido persiga al resto por la fila. Usa un reset que cargue "00000001" (no "00000000") y una línea de desplazamiento shreg <= shreg(6 downto 0) & shreg(7);. Mantén KEY(1) y observa cómo un LED gira continuamente.

  1. Cambia una sola línea de tu entidad por la versión de desplazamiento a la derecha de arriba. Sintetiza y carga.

  2. Resetea, mantén KEY(1), pon SW(0)=1 durante cuatro ticks y luego SW(0)=0 durante cuatro ticks. Confirma que los bits se mueven en sentido contrario por los LED.

  3. (Extensión opcional) Construye el contador en anillo: el reset carga "00000001" y la línea de desplazamiento realimenta shreg(7) hacia el bit 0. Mantén KEY(1) y observa cómo circula un único LED encendido.

En la versión de desplazamiento a la derecha shreg <= SW(0) & shreg(7 downto 1);, parte de 0000_0000 e introduce SW(0) = 1 durante cuatro ticks. Qué extremo del registro se llena y cuál es shreg(7 downto 0) después de los cuatro ticks?

8

Qué construiste

4 min

En tres frases, explica: (1) qué hizo el divisor de reloj para que pudieras ver moverse el contador, (2) una evidencia obligatoria concreta que capturaste del contador en HEX0 y (3) una trampa que evitaste, como una constante que no cabe en el registro divisor de 27 bits. Si también probaste una extensión BCD o de desplazamiento, añade esa evidencia en una frase extra.