Teach Lecciones con laboratorios remotos

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.

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

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.

1

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).

Diagrama de estados de crédito para una máquina expendedora: S0 0 céntimos, S5 5, S10 10 y S15 15. Una moneda de 5 céntimos SW(0) avanza un paso; una moneda de 10 céntimos SW(1) avanza dos pasos. Los caminos de 15 céntimos entran primero en S15; en el tick siguiente S15 activa la señal `dispense` y vuelve a S0. Desde S10, una moneda de 10 llega a 20 céntimos, dispensa inmediatamente y vuelve a S0.

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?

2

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).

3

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.

  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 la entidad de la expendedora. Mantén el nombre blink. Observa que declara CLOCK_50 : in std_logic: la plantilla no añade el reloj.

  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 error y vuelve a sintetizar. 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. Pulsa y suelta KEY(0) para resetear. HEX0 debe mostrar 0 y LEDR(7) debe estar apagado.

  6. 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. HEX0 pasa a 5. Repite para llegar a A (10c), y una vez más: el crédito llega a F (15c) y en el siguiente tick se dispensa el producto. LEDR(7) se enciende y HEX0 vuelve a 0. LEDR(7) queda encendido.

  7. Pulsa KEY(0) para resetear (LEDR(7) se apaga y HEX0 muestra 0).

  8. 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.

  9. 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?

4

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) = , volver a S0.
- Dos monedas de 10c (10+10): crédito 20c, LEDR(7) = sí, LEDR(6) = , 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.

  1. Edita tu entidad de expendedora para añadir la devolución de cambio: declara y enclava change_latch, y reemplaza la línea base LEDR(6) <= '0'; por LEDR(6) <= change_latch;. Debe encenderse solo cuando la máquina dispensa desde el camino de 20c (S10 + moneda de 10c), junto al LEDR(7) existente.

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

  3. 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 y LEDR(6) queda apagado.

  4. 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) y LEDR(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?

5

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.

6

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;
  1. (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 decimal seg_dec, separa el crédito en decenas y unidades y controla HEX1:HEX0.

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

  3. Resetea e introduce monedas. Confirma que HEX1:HEX0 muestra 00, 05, 10 y 15 para los estados base. Muestra 20 solo 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 a 00 mientras LEDR(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?

7

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.