Lección Teach
Lógica digital en la DE1-SoC (3/6): memoria, biestables D y registros
El alumnado construye biestables D y registros en Verilog, prueba reloj y reset, y distingue lógica combinacional de estado almacenado.
Resultados de aprendizaje
Explicar por qué la lógica secuencial necesita memoria y la combinacional no.
Describir un biestable D como muestreo y retención en el flanco del reloj, con reset síncrono.
Construir un registro de carga paralela de 8 bits a partir de biestables D que comparten reloj y reset.
Usar asignaciones no bloqueantes en bloques con reloj y reconocer entradas KEY activas en bajo.
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.
Por qué los circuitos necesitan memoria
10 min
Hasta ahora todos tus diseños han sido combinacionales: los LED eran una función pura de los interruptores *en ese momento*. Cambias un interruptor y la salida cambia en cuanto la señal atraviesa las puertas. Un circuito combinacional no tiene estado: no sabe qué entradas había hace un instante y no puede recordar nada.
Pero la mayoría de los sistemas útiles necesitan memoria. Un contador debe conocer su valor actual para producir el siguiente. Una CPU debe guardar operandos y resultados entre instrucciones. Un semáforo debe saber qué color está mostrando ahora para decidir qué viene después. Nada de eso es posible solo con puertas, porque las puertas reflejan únicamente sus entradas presentes.
La lógica secuencial es la lógica que recuerda. Su salida depende de las entradas actuales y también de un estado almacenado: lo que ocurrió antes. El bloque básico que guarda un bit de estado es el biestable (*flip-flop*), y el encargado de decirle a cada biestable *cuándo* actualizarse es el reloj.
La DE1-SoC proporciona un reloj libre de 50 MHz llamado CLOCK_50: una onda cuadrada que sube y baja 50.000.000 veces por segundo. A partir de esta lección, tus diseños secuenciales deben incluirlo como entrada. La plantilla inicial de leds_mirror.v solo lista interruptores, botones y LED, y no tiene reloj, así que debes añadir input CLOCK_50; tú mismo. Olvidarlo es el error más común en diseños secuenciales.
Cuál de estos circuitos es secuencial (su salida puede depender del pasado), y no solo combinacional?
Los diseños combinacionales de las Lecciones 1-2 solo listaban interruptores y LED, pero un diseño con reloj como un biestable debe listar también input CLOCK_50;. Por qué necesita el reloj un biestable o registro, y qué suele fallar si lo dejas fuera? Responde en una o dos frases.
El biestable D: muestrear y retener en el flanco de reloj
11 min
Un biestable D es el elemento de memoria más sencillo. Tiene una entrada de datos D, una entrada de reloj y una salida Q. Su regla completa es:
> En cada flanco de subida del reloj, Q captura el valor de D y lo mantiene hasta el siguiente flanco de subida.
Entre flancos, lo que haga la entrada no importa: Q queda congelada. Eso es muestreo y retención: el biestable toma una muestra de D en el instante en que el reloj pasa de 0 a 1, y después mantiene esa muestra estable. Así es como un circuito gana memoria: Q puede seguir valiendo 1 mucho después de que D haya vuelto a 0, porque recuerda la muestra.
Muestreo y retención en el flanco de reloj: Q se actualiza solo en cada flanco de subida de CLOCK_50, copiando el valor que tenga D en ese instante, y mantiene ese valor hasta el siguiente flanco. Los cambios de D entre flancos se ignoran.
Cómo se escribe en Verilog: dos ideas nuevas.
Primero, el bloque con reloj. El comportamiento disparado por flanco se describe con always @(posedge CLOCK_50): las instrucciones internas se evalúan en cada flanco de subida, no de forma continua. Una señal asignada dentro de ese bloque debe declararse como reg, porque conserva un valor entre flancos.
Segundo, la asignación no bloqueante <=. Dentro de un bloque con reloj usa siempre <=, no =. El operador <= significa "tomar ahora todos los valores de la derecha y actualizar las señales de la izquierda juntas en el flanco", que es exactamente cómo se comportan los biestables reales. Usar = (bloqueante) dentro de un bloque con reloj es un error clásico de Verilog: las asignaciones se ejecutan en secuencia dentro del flanco, por lo que un biestable puede ver el valor *nuevo* de otro en vez del anterior, generando hardware distinto al que querías. Regla práctica: <= en bloques con reloj; = solo en assign o always @(*) combinacional.
Reset síncrono. También necesitamos forzar Q a un valor conocido. Un reset síncrono se comprueba *dentro* del bloque con reloj, así que solo actúa en un flanco de reloj:
always @(posedge CLOCK_50) begin
if (!KEY[0]) Q <= 1'b0; // reset síncrono, evaluado en el flanco
else Q <= SW[0]; // si no, captura D
end
Como KEY es activo en bajo, el botón lee 0 cuando está pulsado; por eso !KEY[0] es verdadero mientras se mantiene pulsado el reset.
Supón que escribieras Q = SW[0]; con = bloqueante, en lugar de <=, dentro de always @(posedge CLOCK_50). Qué operador deberías usar, y qué puede salir mal cuando varios biestables de un mismo bloque con reloj se actualizan juntos usando =?
Supón que D sube a 1 solo durante un instante *entre* dos flancos de subida y vuelve a 0 antes del siguiente flanco. Llega Q a valer 1? Explica la respuesta usando la regla de muestreo y retención.
Ejercicio 2-A: biestable D (reset visible)
12 min
Construye un biestable D en la placa real. Conexiones: SW[0] = D, KEY[0] = reset (activo en bajo), LEDR[0] = Q, y LEDR[1] = ~Q.
Lee esto antes de ejecutarlo: es importante. A 50 MHz, Q sigue a D 50.000.000 veces por segundo, muchísimo más rápido de lo que pueden ver tus ojos o la cámara. Por eso, aunque este circuito *sí* almacena D, no podrás *ver* el almacenamiento: al mover SW[0], LEDR[0] parece seguirlo de inmediato. Este ejercicio demuestra sobre todo el reset, no la memoria visible. Lo que observarás:
- Con SW[0] arriba (D=1), LEDR[0] está encendido y LEDR[1], que muestra ~Q, está apagado.
- Si mantienes pulsado KEY[0] (pulsado lee 0, así que el reset está activo), LEDR[0] se apaga aunque SW[0] siga arriba. El reset gana al dato.
- Al soltar KEY[0], LEDR[0] vuelve a seguir a SW[0].
Verás memoria de forma clara en el siguiente ejercicio (2-B). Aquí céntrate en que el reset domina la entrada de datos.
// Lección 3 - Ejercicio 2-A: biestable D
// SW[0]=D, KEY[0]=reset (activo en bajo), LEDR[0]=Q, LEDR[1]=~Q.
// Nota: a 50 MHz, Q sigue a D demasiado rápido para la vista/cámara; aqui
// el comportamiento claramente visible es el RESET. La memoria se ve en 2-B.
module leds_mirror(CLOCK_50, KEY, SW, LEDR);
input CLOCK_50;
input [3:0] KEY; // KEY[0] = reset_n (activo en bajo)
input [9:0] SW; // SW[0] = D
output [9:0] LEDR;
reg Q;
always @(posedge CLOCK_50) begin
if (!KEY[0]) Q <= 1'b0; // reset sincrono: actua en el flanco de reloj
else Q <= SW[0]; // captura D en cada flanco de subida
end
assign LEDR[0] = Q;
assign LEDR[1] = ~Q;
assign LEDR[9:2] = 8'b0;
endmoduleAbre 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 módulo del biestable D de arriba. Mantén el nombreleds_mirror. Observa que ahora declarainput CLOCK_50;: esa línea es obligatoria para cualquier diseño con reloj.Pulsa Sintetizar. Si una guía tapa el botón, ciérrala o sáltala primero. Espera a que termine (aprox. 1-3 minutos). Si da error, las causas más comunes son olvidar
CLOCK_50en la lista de puertos o un punto y coma. Corrige y vuelve a sintetizar. Continúa solo cuando el estado final diga que terminó con 0 errores.Pulsa Enviar a la FPGA y espera a la placa y la cámara.
Pon
SW[0]arriba (D=1). Confirma queLEDR[0](Q) está encendido yLEDR[1](~Q) está apagado.Demuestra el reset: con
SW[0]todavía arriba, mantén pulsadoKEY[0]. Observa cómoLEDR[0]se apaga aunque el interruptor no cambie: el reset domina al dato. SueltaKEY[0]y confirma queLEDR[0]vuelve a encenderse.
Coloca SW[0] y KEY[0] como en cada fila y registra qué muestran LEDR[0] (Q) y LEDR[1] (~Q) en la cámara (1 = encendido, 0 = apagado). Recuerda que KEY es activo en bajo: pulsado = 0, suelto = 1.
| SW[0] (D) | KEY[0] (0=pulsado, 1=suelto) | LEDR[0] = Q | LEDR[1] = ~Q |
|---|---|---|---|
Cuál es la diferencia entre este biestable y un simple cable de SW[0] a LEDR[0]? Responde en términos de memoria.
Este diseño usa un reset síncrono. Señala la línea de código que realiza el reset y explica qué significa "síncrono" respecto a *cuándo* actúa. (En la Lección 5 verás el reset asíncrono.)
Por qué LEDR[1] siempre muestra lo contrario de LEDR[0]?
El Ejercicio 2-A sí almacena D, pero en la cámara solo pudiste ver funcionar el reset, no el almacenamiento. Usa la frecuencia de 50 MHz para explicar por qué el almacenamiento es invisible mientras el reset sí se ve claramente.
Ejercicio 2-B: registro de carga paralela de 8 bits (memoria visible)
12 min
Un registro son N biestables D colocados en paralelo, todos con el mismo reloj y el mismo reset. Un registro de 8 bits almacena 8 bits. Una carga paralela copia los 8 bits de entrada al registro a la vez en un flanco de reloj: cada biestable captura su propio bit simultáneamente.
El truco que hace visible la memoria es el control de load. En lugar de capturar los datos en *cada* flanco, cómo hacía el biestable simple, este registro solo captura cuando se lo ordenas. Así puedes cambiar los interruptores todo lo que quieras y el valor almacenado se mantiene hasta que vuelves a cargar. Conexiones: SW[7:0] = datos, KEY[0] = reset (activo en bajo), KEY[1] = load (activo en bajo).
Como KEY es activo en bajo, debes pulsar y soltar KEY[1] para cargar: al pulsar, KEY[1] lee 0, y el código comprueba !KEY[1]. Observa que KEY[1] se *muestrea con el reloj*; no es una señal de reloj con antirrebote por hardware. El diseño simplemente comprueba el nivel del botón en cada flanco de subida, suficiente para este ejercicio.
// Lección 3 - Ejercicio 2-B: registro de carga paralela de 8 bits
// SW[7:0]=datos, KEY[0]=reset (activo en bajo), KEY[1]=load (activo en bajo). LEDR[7:0]=valor guardado.
// Como KEY es activo en bajo, "pulsar y soltar KEY[1]" carga los interruptores; el código comprueba !KEY[1].
module leds_mirror(CLOCK_50, KEY, SW, LEDR);
input CLOCK_50;
input [3:0] KEY; // KEY[0]=reset_n, KEY[1]=load_n
input [9:0] SW; // SW[7:0] = datos a cargar
output [9:0] LEDR;
reg [7:0] Q;
always @(posedge CLOCK_50) begin
if (!KEY[0]) Q <= 8'h00; // reset domina a load
else if (!KEY[1]) Q <= SW[7:0]; // carga paralela mientras KEY[1] está pulsado
// else: mantener (Q conserva su valor aunque cambien los interruptores)
end
assign LEDR[7:0] = Q;
assign LEDR[9:8] = 2'b0;
endmoduleEn
leds_mirror.v, reemplaza todo el contenido por el módulo del registro de 8 bits. Mantén el nombreleds_mirrory conservainput CLOCK_50;.Pulsa Sintetizar, espera a que termine correctamente y después pulsa Enviar a la FPGA.
Coloca los interruptores en
SW[7:0] = 10101010(SW[7]es el bit más significativo). Los LED todavía no tienen por qué mostrarlo: no has cargado nada.Carga: pulsa y suelta
KEY[1]. AhoraLEDR[7:0]muestra10101010. El registro ha capturado los interruptores.Observa la memoria: cambia ahora los interruptores a
SW[7:0] = 01010101sin tocarKEY[1]. Mira los LED: no cambian. El registro mantiene el valor almacenado aunque las entradas sean distintas. Eso es memoria.Actualiza: pulsa y suelta
KEY[1]de nuevo. AhoraLEDR[7:0]cambia a01010101.Reset: pulsa y suelta
KEY[0]. Los ocho LED se apagan (Q = 00000000) sin importar los interruptores.
Ejecuta la secuencia de carga y registra los 8 LED (LEDR[7:0]) en cada etapa. La observación clave es la fila 'interruptores cambiados, sin carga': el valor almacenado debe quedarse igual que en la fila anterior.
| Etapa | SW[7:0] colocado en | Accion sobre KEY | LEDR[7:0] observado |
|---|---|---|---|
Sube tu evidencia. Captura una imagen de la cámara que demuestre que el registro está *manteniendo* un valor: LEDR[7:0] debe leer 10101010 (el valor cargado antes) mientras los interruptores en pantalla SW[7:0] están en un patrón diferente, por ejemplo 01010101, y no has pulsado KEY[1]. La captura debe mostrar tanto los LED retenidos como las posiciones distintas de los interruptores.
En el paso 5 cambiaste los interruptores, pero los LED no cambiaron. Explica por qué usando las palabras "almacenar" y "cargar".
Si mantuvieras pulsados reset (KEY[0]) y load (KEY[1]) al mismo tiempo en un flanco de reloj, qué valor tomaría el registro y por qué? Señala la línea de código que lo decide.
Cuál es la utilidad práctica de un registro de carga paralela dentro de una CPU? Da un ejemplo concreto de algo que podría guardar.
Qué construiste y una nota sobre estilos de reset
8 min
Construiste las dos bases de toda memoria en sistemas digitales: el biestable D (un bit, muestreo y retención en el flanco de reloj) y el registro (varios biestables cargados en paralelo, manteniendo su valor hasta recibir otra orden). Usaste always @(posedge CLOCK_50) con asignaciones no bloqueantes <=, y viste como una entrada KEY activa en bajo actúa como control.
Nota para las próximas lecciones: dos estilos de reset. Los dos diseños de esta lección usaron reset síncrono: el reset se comprobaba *dentro* del bloque con reloj, por lo que solo actuaba en un flanco de subida. Es limpio y predecible, y lo seguiremos usando para elementos de almacenamiento sencillos. En las lecciones de máquinas de estados (5 y 6), en cambio, usaremos un reset asíncrono activo en bajo, escrito como always @(posedge CLOCK_50 or negedge KEY[0]), que fuerza el circuito a un estado seguro de inmediato, sin esperar al reloj. Eso ayuda a recuperar una máquina de estados bloqueada en la placa. Allí se explicará la diferencia con detalle. Por ahora, quédate con que existen ambos estilos y que esta lección usó deliberadamente el reset síncrono.
Qué secuencia coincide con lo que hiciste en cada ejercicio de esta lección?
En tres o cuatro frases: explica cómo el Ejercicio 2-B *demostró* que el registro tiene memoria (cita tus datos), por qué el Ejercicio 2-A solo permitía ver el reset y no el almacenamiento, y nombra un momento donde importó que KEY fuera activo en bajo.