Lección Teach
Lógica digital en la DE1-SoC con VHDL (6/6): capstone de FSM, una máquina expendedora
El alumnado construye una FSM de máquina expendedora en VHDL, prueba caminos de crédito en hardware y prueba checkpoints opcionales de devolución de cambio si hay tiempo.
¿Primera vez en LabsLand? Crea tu cuenta de docente
Resultados de aprendizaje
Ejecutar y explicar una máquina de estados finitos no trivial en hardware real.
Construir un controlador de máquina expendedora cuyo estado es el crédito acumulado.
Reconocer devolución de cambio, selección de producto y display decimal como checkpoints opcionales.
Explicar por qué el diseño base necesita estados de crédito recordados; si se intenta 3-C2, razonar sobre estados y biestables.
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.
Del semáforo al controlador: una FSM que guarda tu dinero
8 min
En la Lección 5 construiste una máquina de estados finitos cuyo estado era una posición abstracta en un ciclo: un semáforo que recorría verde -> amarillo -> rojo. Si hiciste el detector opcional, también viste un estado que recuerda parte de un patrón. En este capstone, el estado significa algo concreto y físico: cuánto dinero se ha introducido hasta ahora. Vas a construir una pequeña máquina expendedora; los checkpoints opcionales pueden ampliarla función por función.
Recuerda que una máquina de estados finitos tiene tres partes: un conjunto de estados, una regla de siguiente estado dada la situación actual y las entradas, y una salida que depende del estado (y, en una máquina Mealy, también de las entradas). La habilidad nueva de esta lección es el diseño: decides cuáles deben ser los estados, dibujas la tabla de transiciones y solo después escribes VHDL.
Para una máquina de monedas, lo natural es que cada estado *sea* un nivel de crédito. Si la máquina acepta monedas de 5 y 10 céntimos y entrega el producto a 15 céntimos, el crédito puede ser 0, 5, 10 o 15 céntimos: cuatro estados. Los llamaremos S0, S5, S10, S15. Insertar una moneda te mueve a un estado de más crédito; alcanzar el precio dispensa el producto y vuelve a S0.
Las señales de la placa son las de siempre. Como este diseño tiene reloj, la entidad declara el reloj: la plantilla inicial de blink.vhd no tiene reloj, así que añadimos CLOCK_50 : in std_logic. Como en la Lección 5, un interruptor arriba es lógica 1, KEY es activo en bajo (pulsado lee 0), LEDR es activo en alto (encendido con 1) y HEX0-HEX5 son displays de 7 segmentos activos en bajo (un segmento se enciende con 0).
La máquina expendedora como diagrama de estados de crédito. Cada estado es un nivel de crédito. Una moneda de 5c (SW(0)) avanza un paso; una de 10c (SW(1)) avanza dos. Los caminos de precio exacto entran primero en S15; en el tick siguiente S15 activa dispense y vuelve a S0. Los puntos de control opcionales posteriores añaden devolución de cambio y selección de producto.
La máquina base usa cuatro estados separados S0, S5, S10, S15. Por qué necesita un estado distinto para cada nivel de crédito en vez de un único estado? Qué no podría hacer la máquina si solo tuviera un estado?
LEDR(7) (dispense) está enclavado y permanece encendido hasta pulsar KEY(0). Después de dispensar y volver a S0, qué limpia el latch, y qué problema habría si nada lo limpiara nunca?
Ejercicio 3-C: la FSM de la máquina expendedora
14 min
Este es el diseño base completo. Léelo como una máquina de estados con los bloques ya conocidos: un divisor de reloj para que las monedas se registren a ritmo humano, un bloque combinacional de siguiente estado/salida, y un registro de estado con reloj y reset asíncrono activo en bajo.
Estados. S0, S5, S10, S15 son los cuatro niveles de crédito con nombre. El crédito actual en céntimos se deriva explícitamente de comparaciones con el estado y se muestra de dos formas: en LEDR(3 downto 0) como número binario de céntimos, y en HEX0 como un carácter: 0 para 0c, 5 para 5c, A para 10c y F para 15c (son los dígitos hexadecimales cuyos valores son 0, 5, 10 y 15).
Transiciones (esta es la tabla que dibujarías primero al diseñar):
Resumen de transiciones:
- S0 (0c): moneda de 5 -> S5; moneda de 10 -> S10; sin dispensar.
- S5 (5c): moneda de 5 -> S10; moneda de 10 -> S15; sin dispensar.
- S10 (10c): moneda de 5 -> S15; moneda de 10 -> S0 y dispensa a 20c, sin cambio en la versión base.
- S15 (15c): en el siguiente tick -> S0 y dispensa, independientemente de la entrada.
Dos detalles importan. Desde S10, insertar una moneda de 10 llega a 20 céntimos, más que el precio de 15c; por tanto la máquina dispensa y vuelve a S0, pero en la versión base no devuelve cambio. El checkpoint 3-C1 lo corrige. Además, S15 es un estado de "dispensar y resetear": en el siguiente tick activa dispense y vuelve a S0, sin importar las entradas. Así que si las monedas llegan exactamente a 15c, verás el crédito llegar a F y en el tick siguiente se dispensará.
La salida dispense está enclavada. dispense en sí dura un tick. Como en el detector de la Lección 5, un pulso de un tick es fácil de perder en una cámara remota; por eso lo enclavamos en dispensed_latch, lo llevamos a LEDR(7) y solo lo limpiamos con reset. Así, cuando la máquina ha dispensado, LEDR(7) se queda encendido hasta que pulses KEY(0).
Insertar una moneda: temporización. El divisor usa DIV = 16,666,666, así que slow_clk cambia aproximadamente cada 0,33 s, pero la FSM solo toma monedas en flancos de subida. En la práctica, trata un tick de moneda como unos 0,67 s. Para insertar una moneda, sube un interruptor, espera al menos un tick de flanco de subida y vuelve a bajarlo. Si mantienes el interruptor arriba durante varios ticks, se contará como varias monedas. Inserta monedas de una en una.
-- Plantilla de estudiante: completa las lineas TODO antes de sintetizar.
-- Lección 6 - Ejercicio 3-C: diseño base de FSM de máquina expendedora.
-- SW(0)=moneda de 5c, SW(1)=moneda de 10c, KEY(0)=reset. LEDR(7)=dispensa enclavada.
-- Inserta una moneda manteniendo SW(0) o SW(1) a 1 durante un flanco de subida de slow_clk, unos 0,67 s.
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);
HEX0 : out std_logic_vector(6 downto 0)
);
end entity blink;
architecture rtl of blink is
type state_t is (S0, S5, S10, S15);
signal state, next_state : state_t := S0;
signal dispense, dispensed_latch : std_logic := '0';
signal credit : unsigned(3 downto 0);
constant DIV : unsigned(24 downto 0) := to_unsigned(16666666, 25);
signal div_count : unsigned(24 downto 0) := (others => '0');
signal slow_clk : std_logic := '0';
function seg(d : unsigned(3 downto 0)) return std_logic_vector is
begin
case d is
when "0000" => return "1000000"; -- 0
when "0101" => return "0010010"; -- 5
when "1010" => return "0001000"; -- A = 10
when "1111" => return "0001110"; -- F = 15
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 = DIV then
div_count <= (others => '0'); slow_clk <= not slow_clk;
else
div_count <= div_count + 1;
end if;
end if;
end process;
process(state, SW)
begin
next_state <= state;
dispense <= '0';
case state is
when S0 => TODO_FROM_0_CENTS;
when S5 => TODO_FROM_5_CENTS;
when S10 => TODO_FROM_10_CENTS;
when S15 => TODO_DISPENSE_AND_RESET;
end case;
end process;
process(slow_clk, KEY)
begin
if KEY(0) = '0' then
state <= S0; dispensed_latch <= '0';
elsif rising_edge(slow_clk) then
state <= next_state;
if TODO_DISPENSE_CONDITION then dispensed_latch <= '1'; end if;
end if;
end process;
credit <= to_unsigned(0, 4) when state = S0 else
to_unsigned(5, 4) when state = S5 else
to_unsigned(10, 4) when state = S10 else
to_unsigned(15, 4);
LEDR(7) <= TODO_DISPENSE_LED;
LEDR(3 downto 0) <= std_logic_vector(credit);
LEDR(6) <= '0'; -- reemplaza esta línea por change_latch en la extensión 3-C1
LEDR(5 downto 4) <= (others => '0');
LEDR(9 downto 8) <= (others => '0');
HEX0 <= seg(credit);
end architecture rtl;Partiendo de S0, predice antes de ejecutarlo la secuencia de caracteres que verás en HEX0 y cuándo se encenderá LEDR(7) (dispense) si insertas una moneda de 10c y luego una de 5c (una por tick).
Ejecuta la expendedora: monedas, dispensación y reset
14 min
Construye el controlador en la placa real, introduce monedas por dos caminos y confirma que el LED de dispensación se enclava.
Abre el laboratorio VHDL de la DE1-SoC. El editor muestra
blink.vhd.Selecciona todo el contenido de
blink.vhdy reemplázalo por la entidad de la expendedora. Mantén el nombreblink. Observa que declaraCLOCK_50 : in std_logic: la plantilla no añade el 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). Corrige cualquier error y vuelve a sintetizar. Continúa solo cuando el estado final diga 0 errores.
Pulsa Enviar a la FPGA y espera a la placa y la cámara.
Pulsa y suelta
KEY(0)para resetear.HEX0debe mostrar0yLEDR(7)debe estar apagado.Tres monedas de 5c. Inserta una moneda de 5c: sube
SW(0), espera unos 0,67 s para un tick de moneda y vuelve a bajarlo.HEX0pasa a5. Repite para llegar aA(10c), y una vez más: el crédito llega aF(15c) y en el siguiente tick se dispensa el producto.LEDR(7)se enciende yHEX0vuelve a0.LEDR(7)queda encendido.Pulsa
KEY(0)para resetear (LEDR(7)se apaga yHEX0muestra0).5c + 5c + 10c. Inserta dos monedas de 5c (
HEX0:0->5->A), y luego una moneda de 10c (SW(1)): desde S10, una de 10c llega a 20c, por lo que la máquina dispensa inmediatamente y vuelve a S0.LEDR(7)se enciende.Pulsa
KEY(0)para resetear antes del siguiente checkpoint.
Resetea primero. Inserta monedas una por tick y registra HEX0 y LEDR(7). En caminos exactos de 15c, registra la fila cuando el crédito llega a F y la dispensación sigue apagada; después espera un tick extra y registra la fila de dispensación. Luego resetea y haz 5c+5c+10c.
| Moneda insertada | Credito HEX0 (0/5/A/F) | LEDR(7) dispensa? (sí/no) |
|---|---|---|
Sube tu evidencia. Después de que tres monedas de 5c hagan dispensar la máquina, captura una imagen de la cámara con LEDR(7) encendido y adjúntala abajo.
Cuando el crédito llega a 15c (HEX0 muestra F), el producto no se dispensa en ese mismo tick: se dispensa en el siguiente. Mirando el caso S15 del código, explica en una frase por qué.
En el diseño base, insertar una moneda de 10c desde S10 llega a 20c y dispensa, pero el cliente pierde 5c. Qué salida tendrías que añadir para corregirlo, y en qué único caso se aplica?
Checkpoint opcional 3-C1: añadir devolución de cambio
12 min
Checkpoint opcional si tu docente quiere una extensión: corrige el caso injusto. Cuando la máquina llega a 20 céntimos, el único camino con sobrepago (S10 seguido de moneda de 10c), debe encender un segundo LED, LEDR(6), que significa "devolver 5c de cambio", además de dispensar en LEDR(7). En todas las demás dispensaciones, exactamente 15c, no hay cambio.
Es un cambio pequeño en la FSM. Necesitas una segunda salida enclavada, change_latch, llevada a LEDR(6), que se active cuando la máquina dispensa *desde el camino de 20c*. La forma más limpia es añadir una señal combinacional change junto a dispense, activarla solo en la rama S10 + moneda de 10c, enclavarla igual que dispensed_latch y reemplazar la línea base LEDR(6) <= '0'; por LEDR(6) <= change_latch;.
Comportamiento esperado:
- Tres monedas de 5c (5+5+5): crédito 15c; espera un tick extra, entonces LEDR(7) dispense = sí, LEDR(6) cambio = no, volver a S0.
- 5c + 10c: crédito 15c; espera un tick extra, entonces LEDR(7) = sí, LEDR(6) = no, volver a S0.
- 10c + 5c: crédito 15c; espera un tick extra, entonces LEDR(7) = sí, LEDR(6) = no, volver a S0.
- 5c + 5c + 10c: crédito 20c, LEDR(7) = sí, LEDR(6) = sí, volver a S0.
- Dos monedas de 10c (10+10): crédito 20c, LEDR(7) = sí, LEDR(6) = sí, volver a S0.
Impleméntalo y verifica en la placa que LEDR(6) se enciende solo en un camino de 20c y nunca en uno exacto de 15c.
Edita tu entidad de expendedora para añadir la devolución de cambio: declara y enclava
change_latch, y reemplaza la línea baseLEDR(6) <= '0';porLEDR(6) <= change_latch;. Debe encenderse solo cuando la máquina dispensa desde el camino de 20c (S10 + moneda de 10c), junto alLEDR(7)existente.Sintetiza, corrige cualquier typo y carga en la FPGA.
Resetea. Haz un camino exacto de 15c, por ejemplo tres monedas de 5c, espera el tick extra de dispensación y confirma que
LEDR(7)se enciende yLEDR(6)queda apagado.Resetea. Haz un camino de 20c, por ejemplo dos monedas de 5c y una de 10c, o dos de 10c: confirma que ambos
LEDR(7)yLEDR(6)se encienden.
Tras tu cambio 3-C1, ejecuta cada camino y lee los dos LED de salida. En caminos exactos de 15c, espera el tick extra de dispensación antes de leer. Registra si LEDR(7) (dispensa) y LEDR(6) (cambio) quedan encendidos al final.
| Camino de monedas | Credito alcanzado (15c/20c) | LEDR(7) dispensa? (sí/no) | LEDR(6) cambio? (si/no) |
|---|---|---|---|
Sube tu evidencia. En un camino de 20c, por ejemplo dos monedas de 5c y una de 10c, captura una imagen de la cámara mostrando ambos LEDR(7) (dispense) y LEDR(6) (cambio) encendidos.
Captura tu código del laboratorio. Guarda tu blink.vhd modificado en el laboratorio y usa el control de evidencia de código de abajo para entregar la versión del checkpoint 3-C1 con devolución de cambio. Si tu docente pide un diagrama de estados, entrégalo por separado.
En tu FSM modificada, en exactamente qué caso de estado y entrada se activa la salida de cambio (LEDR(6)), y por qué es el *único* caso de sobrepago de esta máquina?
Checkpoint 3-C2 (opcional / bonus): selección de producto
8 min
Este bonus hace que la máquina venda dos productos: un refresco de 15 céntimos (seleccionado con SW(2)) y un café de 20 céntimos (seleccionado con SW(3)). La máquina debe dispensar solo cuando el crédito alcance el precio del producto seleccionado: con refresco dispensa a 15c, y con café espera hasta 20c.
La idea importante es que esto aumenta el espacio de estados. La máquina ya no se describe solo por crédito, porque su comportamiento con, por ejemplo, 15c depende de *qué producto* se seleccionó: dispensar si es refresco, seguir esperando si es café. La máquina debe recordar el crédito y la selección. Combínalos, por ejemplo con estados como el par *(producto seleccionado, nivel de crédito)*, y decide qué combinaciones pueden ocurrir y qué hace cada una.
Dibuja primero el diagrama de estados ampliado, decide cuántos estados necesitas realmente y luego implementa. No hay una única codificación correcta, pero sí un número mínimo de estados distintos que exige el comportamiento; de ahí sale el mínimo de biestables.
(Bonus) Entrega tu diseño. Adjunta el diagrama de estados ampliado para la máquina de dos productos y pega tu VHDL modificado, o adjunta el código como .txt.
(Bonus) Para tu máquina de dos productos, cuántos estados distintos necesita, y por tanto cuál es el número mínimo de biestables requerido? Recuerda que el mínimo de biestables para codificar N estados es ceil(log2(N)). Indica tu N, muestra el cálculo ceil(log2(N)) y justifica brevemente tu conteo de estados.
Checkpoint 3-C3 (opcional / bonus): display decimal de dos dígitos
7 min
La máquina base muestra el crédito como un solo carácter hexadecimal (0, 5, A, F), compacto pero poco parecido a una máquina real. Este bonus muestra el crédito como dos dígitos decimales en HEX1:HEX0: 00, 05, 10 y 15. Muestra 20 solo si construyes una extensión separada con un estado o latch de display explícito para 20c; el diseño 3-C1 de devolución de cambio sigue devolviendo el display de crédito a 00 mientras LEDR(6) indica el cambio. HEX0 muestra unidades y HEX1 decenas.
Reutiliza la idea del decodificador de siete segmentos de la Lección 2: una función decodificadora, llamada aquí seg_dec para que pueda convivir con la función base seg, que convierte un dígito decimal 0..9 al patrón activo en bajo {g,f,e,d,c,b,a}. Separa el crédito en decenas y unidades, decodifica cada una y controla HEX1 y HEX0. Recuerda declarar HEX1 : out std_logic_vector(6 downto 0) en la lista de puertos de la entidad: la plantilla inicial solo expone HEX0, así que HEX1 debe declararse, igual que hiciste con el decodificador de dos dígitos en la Lección 2. Además, reemplaza la línea original HEX0 <= seg(credit);; no dejes activas dos asignaciones a HEX0.
Una función decimal seg_dec (con la misma forma que la función seg de la Lección 2) y una separación para los valores de crédito base (0, 5, 10, 15) es suficiente:
-- Add HEX1 to the entity port list, then complete the display helper.
HEX0 : out std_logic_vector(6 downto 0);
HEX1 : out std_logic_vector(6 downto 0)
-- ...
function seg_dec(d : unsigned(3 downto 0)) return std_logic_vector is
begin
case d is
when "0000" => return "1000000";
when "0001" => return TODO_PATTERN_FOR_1;
when "0010" => return TODO_PATTERN_FOR_2;
when others => return "1111111";
end case;
end function;
signal cents : unsigned(4 downto 0);
signal tens : unsigned(3 downto 0);
signal ones : unsigned(3 downto 0);
-- ...
cents <= TODO_CREDIT_AS_CENTS;
tens <= TODO_TENS_DIGIT;
ones <= TODO_ONES_DIGIT;
HEX0 <= TODO_ONES_DISPLAY;
HEX1 <= TODO_TENS_DISPLAY;(Bonus) Edita tu entidad: añade un puerto
HEX1 : out std_logic_vector(6 downto 0)a la lista de puertos, añade el decodificador decimalseg_dec, separa el crédito en decenas y unidades y controlaHEX1:HEX0.Sintetiza, corrige cualquier typo y carga en la FPGA.
Resetea e introduce monedas. Confirma que
HEX1:HEX0muestra00,05,10y15para los estados base. Muestra20solo si construiste una extensión separada con un estado o latch de display explícito para 20c; el diseño 3-C1 de devolución de cambio sigue devolviendo el display de crédito a00mientrasLEDR(6)indica el cambio.
(Bonus) Sube tu evidencia. Captura una imagen de la cámara de HEX1:HEX0 mostrando un crédito decimal de dos dígitos, por ejemplo 10 o 15, y adjúntala abajo.
(Bonus) Para un crédito de 15 céntimos, qué dígito de decenas y qué dígito de unidades calcula el decodificador (los valores que alimentan HEX1 y HEX0), y qué display muestra 5?
Cierre del capstone: qué construiste en el curso
8 min
Has construido en hardware real el recorrido base por la lógica digital:
- Puertas (Lección 1): AND, OR, NOT, NAND, NOR, XOR; los átomos de todo circuito y la idea de que VHDL *describe hardware* en vez de ejecutarse como un programa.
- Diseño combinacional y minimización (Lección 2): tablas de verdad, SOP, mapas de Karnaugh y extensiones opcionales; convertir una especificación en el circuito *más simple* que la cumple.
- Memoria (Lección 3): biestable y registro de carga paralela; el momento en que un circuito puede *recordar*.
- Contadores (Lección 4): un bloque secuencial movido por un divisor de reloj, más la disciplina de comprobar que una constante cabe en su registro; las extensiones opcionales introdujeron BCD y registros de desplazamiento.
- Máquinas de estados finitos (Lección 5): un semáforo Moore que actúa según un estado recordado, con detector Mealy opcional como extensión.
- Capstone FSM (esta lección): una máquina expendedora que *ejecutaste y explicaste* desde una tabla de transiciones; los checkpoints opcionales permiten añadir devolución de cambio, selección de producto o display decimal y, si se intentan, razonar sobre estados y biestables.
Ese es el núcleo de un curso introductorio de lógica digital, y ejecutaste la ruta obligatoria en una FPGA real.
A dónde seguir. Algunas direcciones opcionales:
- Temporización: setup y hold. Los biestables reales exigen que su entrada sea estable durante una pequeña ventana alrededor del flanco de reloj; cumplir esos tiempos permite alcanzar una frecuencia objetivo. Es la puerta a análisis estático de temporización.
- Aritmética y ALU. Combinar sumadores, comparadores y lógica en una unidad aritmético-lógica, corazón computacional de un procesador.
- Pipelining. Dividir un camino combinacional largo en etapas con reloj para aumentar rendimiento.
- Otros HDL y simulación. Temas avanzados de VHDL y la *simulación* con testbenches y formas de onda permiten verificar un diseño antes de tocar la placa.
Lecturas opcionales. Los cursos gratuitos de FPGA Academy (<https://fpgacademy.org/courses.html>) cubren esta familia DE1-SoC en profundidad, y Harris & Harris, *Digital Design and Computer Architecture*, capítulos 1-3, es un texto excelente para el recorrido de puertas a FSM. No son necesarios para completar la actividad; están aquí si quieres seguir.
En unas frases, recorre cómo creció la idea de *memoria* en este curso: desde un biestable (Lección 3), a un registro y contador (Lecciones 3-4), hasta una FSM cuyo estado tenía significado (Lecciones 5-6). Qué permitió hacer la memoria que las puertas puras no podían?
En tres frases, resume tu capstone: (1) qué representaba el *estado* de tu máquina expendedora y cómo lo cambiaba una moneda, (2) la evidencia que capturaste para la dispensación base o para un checkpoint opcional, y (3) un lugar donde la polaridad activa en bajo o una salida enclavada importó para ver el resultado en la cámara.