Teach Lecciones con laboratorios remotos

Lección Teach

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

El alumnado implementa contadores y registros de desplazamiento en Verilog, observa patrones secuenciales en LEDs y explica actualizaciones con reloj.

  • Altera DE1-SoC
  • 65 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.

  • Construir un registro de desplazamiento y razonar sobre el orden de bits frente al orden físico de los LED.

  • 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 circuitos que se mueven solos en cada flanco: un contador que sube 0, 1, 2, 3, ... y 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.

Como en cada lección, los diseños secuenciales deben declarar el reloj. La plantilla inicial de leds_mirror.v lista solo interruptores, botones y LED, y no tiene reloj, así que cada módulo de esta lección añade input CLOCK_50; a la lista de puertos. Las señales de la placa siguen siendo las mismas: SW[9:0] (interruptor arriba = 1), KEY[3:0] (activo en bajo: pulsado lee 0), LEDR[9: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: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.

// Lección 4 - Ejercicio 2-C: contador de 4 bits con divisor de reloj
// KEY[0]=reset (activo en bajo). LEDR[3:0]=cuenta, HEX0=cuenta como dígito hex.
// DIV fija la velocidad. Con DIV=33,333,333 un flanco de subida de slow_clk
// (un paso de cuenta) ocurre aprox. cada 1.33 s. El registro tiene 27 bits, así
// puedes probar DIV = 8,333,333 (más rápido) o 100,000,000 (más lento).
module leds_mirror(CLOCK_50, KEY, LEDR, HEX0);
    input        CLOCK_50;
    input  [3:0] KEY;       // KEY[0] = reset_n
    output [9:0] LEDR;
    output [6:0] HEX0;

    localparam [26:0] DIV = 27'd33_333_333;   // 27 bits: cabe hasta 134,217,727
    reg [26:0] div_count;
    reg        slow_clk;
    always @(posedge CLOCK_50) begin
        if (!KEY[0]) begin div_count <= 0; slow_clk <= 0; end
        else if (div_count == DIV) begin div_count <= 0; slow_clk <= ~slow_clk; end
        else div_count <= div_count + 1'b1;
    end

    reg [3:0] count;
    always @(posedge slow_clk or negedge KEY[0]) begin
        if (!KEY[0]) count <= 4'd0;
        else         count <= count + 1'b1;   // vuelve 15 -> 0 automaticamente
    end

    assign LEDR[3:0] = count;
    assign LEDR[9:4] = 6'b0;

    function [6:0] seg;        // activo en bajo {g,f,e,d,c,b,a}
        input [3:0] d;
        case (d)
            4'h0: seg=7'b1000000; 4'h1: seg=7'b1111001; 4'h2: seg=7'b0100100; 4'h3: seg=7'b0110000;
            4'h4: seg=7'b0011001; 4'h5: seg=7'b0010010; 4'h6: seg=7'b0000010; 4'h7: seg=7'b1111000;
            4'h8: seg=7'b0000000; 4'h9: seg=7'b0010000; 4'hA: seg=7'b0001000; 4'hB: seg=7'b0000011;
            4'hC: seg=7'b1000110; 4'hD: seg=7'b0100001; 4'hE: seg=7'b0000110; 4'hF: seg=7'b0001110;
            default: seg=7'b1111111;
        endcase
    endfunction
    assign HEX0 = seg(count);
endmodule

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, resetéalo y cambia su velocidad

12 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 Verilog de la DE1-SoC. El editor muestra leds_mirror.v.

  2. Selecciona todo el contenido de leds_mirror.v y reemplázalo por el contador de 4 bits. Mantén el nombre leds_mirror. Observa que declara input CLOCK_50;: 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: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: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. Cambia DIV a 8,333,333 (escribe 27'd8_333_333), sintetiza, carga y observa que el contador va claramente más rápido. Después cambia DIV a 100,000,000 (escribe 27'd100_000_000), sintetiza, carga y observa que va más lento. Ambos caben porque el registro tiene 27 bits.

Con DIV = 33,333,333, observa el contador y registra algunos valores. Para cada uno, escribe el patrón binario LEDR[3: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: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: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

Modifica: 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 bloque always del contador por esta forma:

    reg [3:0] count;
    always @(posedge slow_clk or negedge KEY[0]) begin
        if (!KEY[0])            count <= 4'd0;
        else if (count == 4'd9) count <= 4'd0;   // vuelve a 0 después de 9
        else                    count <= count + 1'b1;
    end

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 módulo contador para que el bloque always 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

Ejercicio 2-D: 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:0]. En cada flanco de subida de slow_clk, mientras el desplazamiento está habilitado, hace:

        shreg <= {shreg[6:0], SW[0]};   // desplaza a la IZQUIERDA e introduce SW[0] en el bit 0

Lee con cuidado el lado derecho: toma los siete bits bajos shreg[6: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: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.

// Lección 4 - Ejercicio 2-D: registro de desplazamiento de 8 bits (izquierda)
// SW[0]=dato serial, KEY[0]=reset (activo en bajo), KEY[1]=habilita shift (activo en bajo, mantener pulsado).
// LEDR[7:0]=contenido del registro. shreg <= {shreg[6:0], SW[0]} desplaza a la IZQUIERDA e introduce SW[0] en bit 0.
// Tras cuatro ticks con SW0=1 y luego cuatro ticks con SW0=0, shreg[7:0] es 1111_0000.
// En la placa los LED encendidos pueden verse espejados; registra SIEMPRE el valor de bits y lo que ves.
module leds_mirror(CLOCK_50, KEY, SW, LEDR);
    input        CLOCK_50;
    input  [3:0] KEY;       // KEY[0]=reset_n, KEY[1]=shift_enable_n
    input  [9:0] SW;        // SW[0] = dato serial de entrada
    output [9:0] LEDR;

    localparam [26:0] DIV = 27'd33_333_333;
    reg [26:0] div;
    reg        slow_clk;
    always @(posedge CLOCK_50) begin
        if (!KEY[0]) begin div <= 0; slow_clk <= 0; end
        else if (div == DIV) begin div <= 0; slow_clk <= ~slow_clk; end
        else div <= div + 1'b1;
    end

    reg [7:0] shreg;
    always @(posedge slow_clk or negedge KEY[0]) begin
        if (!KEY[0])      shreg <= 8'h00;
        else if (!KEY[1]) shreg <= {shreg[6:0], SW[0]};   // desplaza izquierda, entra SW[0]
    end

    assign LEDR[7:0] = shreg;
    assign LEDR[9:8] = 2'b0;
endmodule

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:0] después de los cuatro ticks.

6

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 leds_mirror.v por el módulo del registro de desplazamiento de 8 bits. Mantén el nombre leds_mirror. El módulo declara input CLOCK_50;.

  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: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 SW0=1 y luego cuatro ticks con SW0=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:0] (8 bits) Camara: que LED se ven / espejado?

Sube tu evidencia. Tras cuatro ticks con SW0=1 y cuatro ticks con SW0=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

Modifica: 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:

        else if (!KEY[1]) shreg <= {SW[0], shreg[7:1]};   // desplaza derecha, introduce SW[0] arriba

Solo cambia esa línea. Ahora SW[0] entra en shreg[7], y los siete bits altos shreg[7:1] bajan hacia shreg[6: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 8'h01 (no 8'h00) y una línea de desplazamiento shreg <= {shreg[6:0], shreg[7]};. Mantén KEY[1] y observa cómo un LED gira continuamente.

  1. Cambia una sola línea de tu módulo 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 8'h01 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: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:0] después de los cuatro ticks?

En tres frases, explica: (1) qué hizo el divisor de reloj para que pudieras ver moverse el contador y el registro de desplazamiento, (2) una evidencia concreta que capturaste (el valor del contador en HEX0 o el patrón 1111_0000) y (3) una trampa que evitaste (una constante que no cabe en un registro de 26 bits, o valor de bits frente a orden de LED espejado).