Hola Habr! Quiero hacer mi contribución al avance de los FPGA. En este artículo intentaré explicar cómo describir en VHDL un dispositivo que controla una pantalla de siete segmentos. Pero antes de comenzar, quiero hablar brevemente sobre cómo llegué a FPGA y por qué elegí el lenguaje VHDL.
Hace aproximadamente medio año decidí probar suerte en la programación de FPGA. Antes de eso, nunca había encontrado circuitos. Había poca experiencia en el uso de microcontroladores (Atmega328p, STM32). Inmediatamente después de la decisión de sentirme cómodo con los FPGA, surgió la cuestión de elegir el idioma que usaría. La elección recayó en VHDL debido a su mecanografía estricta. Como principiante, quería detectar la mayor cantidad de problemas posibles en la etapa de síntesis, y no en un dispositivo que funcionara.
¿Por qué exactamente una pantalla de siete segmentos? El parpadeo del LED ya está cansado y la lógica de hacerlo no representa nada interesante. La lógica de controlar la pantalla es, por un lado, más complicada que parpadear un LED (es decir, escribirlo es más interesante) y, por otro lado, es bastante simple de implementar.
Lo que usé en el proceso de creación del dispositivo:
- FPGA Altera Cyclone II (Sé que está desactualizado, pero los chinos pueden comprarlo por un centavo)
- Quartus II versión 13.0.0 (que yo sepa, esta es la última versión compatible con Cyclone II)
- Modelo de simulador: Sim
- Pantalla de siete segmentos con registro de desplazamiento
Desafío
Cree un dispositivo que muestre los números 0 - 9 en un ciclo. Una vez por segundo, el valor que se muestra en la pantalla debería aumentar en 1.
Hay muchas formas de implementar esta lógica. Dividiré este dispositivo en módulos, cada uno de los cuales realizará alguna acción y el resultado de esta acción se transmitirá al siguiente módulo.
Módulos
- Este dispositivo debería poder contar el tiempo. Para contar el tiempo, creé un módulo de "retraso". Este módulo tiene 1 señal entrante y 1 saliente. El módulo recibe la señal de frecuencia FPGA y, después de un número específico de períodos de la señal entrante, cambia el valor de la señal saliente a lo opuesto.
- El dispositivo debe leer de 0 a 9. El módulo bcd_counter se utilizará para esto.
- Para iluminar un segmento en la pantalla, debe establecer el bit correspondiente al segmento en 0 en el registro de desplazamiento de la pantalla, y para borrar el segmento en bits, escriba 1 (mi pantalla tiene lógica invertida). El decodificador bcd_2_7seg se ocupará de la instalación y restablecimiento de los bits deseados.
- El módulo transmisor será responsable de la transferencia de datos.
El dispositivo host controlará la transmisión correcta de señales entre los módulos, así como generará una señal rclk al finalizar la transferencia de datos.
Para mayor claridad, doy un diagrama de este dispositivo Como puede ver en el diagrama, el dispositivo tiene 1 señal entrante (clk) y 3 señales salientes (sclk, dio, rclk). La señal clk viene en 2 divisores de señal (sec_delay y transfer_delay). Una señal saliente con un período de 1 s abandona el dispositivo sec_delay. En el borde ascendente de esta señal, el contador (bcd_counter1) comienza a generar el siguiente número para mostrar. Después de que se genera el número, el decodificador (bcd_2_7seg1) convierte la representación binaria del número en segmentos iluminados y no iluminados en la pantalla. Que, utilizando el transmisor (transmisor1), se transmiten a la pantalla. El transmisor se sincroniza utilizando el dispositivo transfer_delay.
Código
Para crear un dispositivo en VHDL, se utiliza un diseño de dos componentes entidad y arquitectura. La entidad declara una interfaz para trabajar con el dispositivo. La arquitectura describe la lógica del dispositivo.
Así es como se ve la entidad de un dispositivo de retraso A través del campo genérico, podemos configurar el dispositivo con el retraso deseado. Y en el campo de puertos describimos las señales entrantes y salientes del dispositivo.
La arquitectura del dispositivo de retraso es la siguiente El código dentro de la sección del proceso se ejecuta secuencialmente, cualquier otro código se ejecuta en paralelo. Entre paréntesis, después de la palabra clave del proceso, se indican las señales, cambiando qué proceso se iniciará (lista de sensibilidad).
El dispositivo bcd_counter, en términos de lógica de ejecución, es idéntico al dispositivo de retraso. Por lo tanto, no me detendré en ello en detalle.
Aquí está la entidad y la arquitectura del decodificador. entity bcd_to_7seg is port(bcd: in std_logic_vector(3 downto 0) := X"0"; disp_out: out std_logic_vector(7 downto 0) := X"00"); end entity bcd_to_7seg; architecture bcd_to_7seg_arch of bcd_to_7seg is signal not_bcd_s: std_logic_vector(3 downto 0) := X"0"; begin not_bcd_s <= not bcd; disp_out(7) <= (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) and bcd(0)); disp_out(6) <= (bcd(2) and not_bcd_s(1) and bcd(0)) or (bcd(2) and bcd(1) and not_bcd_s(0)); disp_out(5) <= not_bcd_s(2) and bcd(1) and not_bcd_s(0); disp_out(4) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) and bcd(0)) or (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or (bcd(2) and bcd(1) and bcd(0)); disp_out(3) <= (bcd(2) and not_bcd_s(1)) or bcd(0); disp_out(2) <= (not_bcd_s(3) and not_bcd_s(2) and bcd(0)) or (not_bcd_s(3) and not_bcd_s(2) and bcd(1)) or (bcd(1) and bcd(0)); disp_out(1) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1)) or (bcd(2) and bcd(1) and bcd(0)); disp_out(0) <= '1'; end bcd_to_7seg_arch;
Toda la lógica de este dispositivo se ejecuta en paralelo. Hablé sobre cómo obtener fórmulas para este dispositivo en uno de los videos de mi canal. A quién le importa, aquí hay un enlace al
video .
En el dispositivo transmisor, combino lógica serial y paralela entity transmitter is port(enable: in boolean; clk: in std_logic; digit_pos: in std_logic_vector(7 downto 0) := X"00"; digit: in std_logic_vector(7 downto 0) := X"00"; sclk, dio: out std_logic := '0'; ready: buffer boolean := true); end entity transmitter; architecture transmitter_arch of transmitter is constant max_int: integer := 16; begin sclk <= clk when not ready else '0'; send_proc: process(clk, enable, ready) variable dio_cnt_v: integer range 0 to max_int := 0; variable data_v: std_logic_vector((max_int - 1) downto 0); begin
A la señal sclk, redirijo el valor de la señal clk que ingresa al transmisor, pero solo si el dispositivo está transmitiendo datos actualmente (señal lista = falso). De lo contrario, el valor de la señal sclk será 0. Al comienzo de la transferencia de datos (habilitar = señal verdadera), combino los datos de dos vectores de 8 bits (digit_pos y digit) que ingresan al dispositivo en un vector de 16 bits (data_v) y transmito los datos de este vector son un bit por reloj, configurando el valor del bit transmitido en el diodo de señal saliente. De las cosas interesantes sobre este dispositivo, quiero señalar que los datos en dio se configuran en el borde posterior de la señal clk, y los datos de pin dio se escribirán en el registro de desplazamiento de la pantalla cuando llegue el borde delantero de la señal sclk. Al finalizar la transmisión, al configurar la señal ready <= true, señalo a otros dispositivos que la transmisión se ha completado.
Así es como se ve la entidad y la arquitectura de un dispositivo de visualización entity display is port(clk: in std_logic; sclk, rclk, dio: out std_logic := '0'); end entity display; architecture display_arch of display is component delay is generic (delay_cnt: integer); port(clk: in std_logic; out_s: out std_logic := '0'); end component; component bcd_counter is port(clk: in std_logic; bcd: out std_logic_vector(3 downto 0)); end component; component bcd_to_7seg is port(bcd: in std_logic_vector(3 downto 0); disp_out: out std_logic_vector(7 downto 0)); end component; component transmitter is port(enable: in boolean; clk: in std_logic; digit_pos: in std_logic_vector(7 downto 0); digit: in std_logic_vector(7 downto 0); sclk, dio: out std_logic; ready: buffer boolean); end component; signal sec_s: std_logic := '0'; signal bcd_counter_s: std_logic_vector(3 downto 0) := X"0"; signal disp_out_s: std_logic_vector(7 downto 0) := X"00"; signal tr_enable_s: boolean; signal tr_ready_s: boolean; signal tr_data_s: std_logic_vector(7 downto 0) := X"00";
Este dispositivo controla otros dispositivos. Aquí, antes de declarar señales auxiliares, declaro los componentes que usaré. En la arquitectura misma (después de la palabra clave begin) creo instancias de dispositivo:
- sec_delay: una instancia del componente de retraso. La señal de salida se enruta a sec_s.
- transfer_delay: una instancia del componente de retraso. La señal de salida se envía a la señal transfer_clk.
- bcd_counter1: una instancia del componente bcd_counter. La señal de salida se enruta a bcd_counter_s.
- bcd_to_7seg1: una instancia del componente bcd_to_7seg. La señal de salida se enruta a disp_out_s.
- Transmisor1 es una instancia del componente transmisor. Las señales salientes se envían a señales sclk, dio, tr_ready_s.
Después de las instancias de componentes, se declara un proceso. Este proceso resuelve varios problemas:
Si el transmisor no está ocupado, el proceso inicializa el inicio de la transferencia de datos. if(tr_ready_s) then if(not (prev_disp = disp_out_s)) then prev_disp := disp_out_s;
- Si el transmisor está ocupado (tr_ready_s = false), el proceso establece el valor de la señal disp_refresh_s <= true (esta señal indica que después de que se complete la transferencia, los datos en la pantalla deben actualizarse). El valor de la señal tr_enable_s <= false también se establece, si esto no se hace antes de que se complete la transmisión, se transmitirán los datos descargados al transmisor
Establece y restablece la señal rclk después de que se complete la transferencia de datos if(rclk_v = '1') then disp_refresh_s <= false; end if; if(tr_ready_s and disp_refresh_s) then rclk_v := '1'; else rclk_v := '0'; end if; rclk <= rclk_v;
Tabla de tiempos
Aquí está el diagrama de tiempos para transferir el número 1 a la primera posición de la pantalla Primero, se transmiten los datos "10011111". Entonces, la posición del número en la pantalla es "00010000" (este parámetro llega al transmisor como constante X "10"). En ambos casos, el bit más a la derecha (lsb) se transmite primero.
Todo el código se puede ver en
github . Los archivos con un subíndice * _tb.vhd son archivos de depuración para los componentes correspondientes (por ejemplo ,ransmisor_tb.vhd es un archivo de depuración para un transmisor). Por si acaso, también los subí a github. Este código fue descargado y funcionó en un tablero real. A quién le importa, puede ver una ilustración del código
aquí (a partir de las 15:30). Gracias por su atencion