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.
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.
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 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.
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);
endmoduleSupó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.
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.
Abre el laboratorio Verilog de la DE1-SoC. El editor muestra
leds_mirror.v.Selecciona todo el contenido de
leds_mirror.vy reemplázalo por el contador de 4 bits. Mantén el nombreleds_mirror. Observa que declarainput CLOCK_50;: la plantilla no añade el reloj por ti.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.
Pulsa Enviar a la FPGA y espera a la placa y la cámara.
Observa
LEDR[3:0]yHEX0. 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 deHEX0(por ejemplo, cuandoHEX0muestraC,LEDR[3:0]debe leer1100).Mantén pulsado
KEY[0]para resetear la cuenta a0, suéltalo y observa cómo vuelve a subir desde0.Cambia
DIVa8,333,333(escribe27'd8_333_333), sintetiza, carga y observa que el contador va claramente más rápido. Después cambiaDIVa100,000,000(escribe27'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.
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.
Edita tu módulo contador para que el bloque
alwaysvuelva a0después de9, como se muestra arriba.Sintetiza, corrige cualquier typo y carga en la FPGA.
Observa que
HEX0recorre0, 1, 2, ... 9, 0, 1, .... Confirma que nunca llega aA. PulsaKEY[0]para volver a0si 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?
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;
endmodulePartiendo 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.
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.
Reemplaza el contenido de
leds_mirror.vpor el módulo del registro de desplazamiento de 8 bits. Mantén el nombreleds_mirror. El módulo declarainput CLOCK_50;.Sintetiza, corrige cualquier typo y carga en la FPGA.
Pulsa y suelta
KEY[0]una vez para resetear el registro a todo ceros (ningún LED encendido entreLEDR[7:0]).Pon
SW[0]arriba (entrada serial =1) y manténKEY[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).Ahora pon
SW[0]abajo (entrada serial =0), sigue manteniendoKEY[1], y deja pasar cuatro ticks más. Los ceros siguen a los unos hasta que el registro vale1111_0000.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.
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.
Cambia una sola línea de tu módulo por la versión de desplazamiento a la derecha de arriba. Sintetiza y carga.
Resetea, mantén
KEY[1], ponSW[0]=1durante cuatro ticks y luegoSW[0]=0durante cuatro ticks. Confirma que los bits se mueven en sentido contrario por los LED.(Extensión opcional) Construye el contador en anillo: el reset carga
8'h01y la línea de desplazamiento realimentashreg[7]hacia el bit0. ManténKEY[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).