Olá Habr! Quero dar minha contribuição para o avanço dos FPGAs. Neste artigo, tentarei explicar como descrever em VHDL um dispositivo que controla uma tela de sete segmentos. Mas antes de começar, quero falar brevemente sobre como cheguei ao FPGA e por que escolhi a linguagem VHDL.
Há cerca de meio ano, decidi tentar programar FPGAs. Antes disso, eu nunca havia encontrado circuitos. Havia pouca experiência com microcontroladores (Atmega328p, STM32). Imediatamente após a decisão de se familiarizar com os FPGAs, surgiu a questão de escolher o idioma que eu usaria. A escolha recaiu sobre o VHDL por causa de sua digitação estrita. Como iniciante, eu queria pegar o maior número possível de problemas no estágio de síntese, e não em um dispositivo em funcionamento.
Por que exatamente uma tela de sete segmentos? O LED piscando já está cansado e a lógica de piscar não representa nada de interessante. A lógica de controlar a tela é, por um lado, mais complicada do que piscar um LED (isto é, escrever é mais interessante) e, por outro lado, é bastante simples de implementar.
O que eu usei no processo de criação do dispositivo:
- FPGA Altera Cyclone II (eu sei que está irremediavelmente desatualizado, mas os chineses podem comprá-lo por um centavo)
- Quartus II versão 13.0.0 (até onde eu sei, esta é a versão mais recente que suporta o Cyclone II)
- Simulator ModelSim
- Visor de sete segmentos com registro de deslocamento
Desafio
Crie um dispositivo que mostre os números de 0 a 9. Em um ciclo, uma vez por segundo, o valor mostrado no visor deve aumentar em 1.
Existem muitas maneiras de implementar essa lógica. Dividirei esse dispositivo em módulos, cada um dos quais executará alguma ação e o resultado dessa ação será transmitido para o próximo módulo.
Módulos
- Este dispositivo deve poder contar o tempo. Para contar o tempo, criei um módulo de "atraso". Este módulo possui 1 sinal de entrada e 1 de saída. O módulo recebe o sinal de frequência FPGA e, após um número especificado de períodos do sinal recebido, altera o valor do sinal de saída para o oposto.
- O dispositivo deve ler de 0 a 9. O módulo bcd_counter será usado para isso.
- Para iluminar um segmento na tela, você precisa definir o bit correspondente ao segmento como 0 no registro de deslocamento da tela e, para limpar o segmento em bits, escreva 1 (minha tela tem lógica invertida). O decodificador bcd_2_7seg trata da instalação e redefinição dos bits desejados.
- O módulo transmissor será responsável pela transferência de dados.
O dispositivo host controlará a transmissão correta de sinais entre os módulos, além de gerar um sinal rclk após a conclusão da transferência de dados.
Para maior clareza, dou um diagrama deste dispositivo Como você pode ver no diagrama, o dispositivo possui 1 sinal de entrada (clk) e 3 sinais de saída (sclk, dio, rclk). O sinal clk vem em 2 divisores de sinal (sec_delay e transfer_delay). Um sinal de saída com um período de 1s sai do dispositivo sec_delay. Na borda ascendente desse sinal, o contador (bcd_counter1) começa a gerar o próximo número para exibição. Depois que o número é gerado, o decodificador (bcd_2_7seg1) converte a representação binária do número em segmentos acesos e não acesos no visor. Quais, usando o transmissor (transmissor1), são transmitidos para o display. O transmissor é cronometrado usando o dispositivo transfer_delay.
Código
Para criar um dispositivo em VHDL, um design de dois componentes é usado para entidade e arquitetura. A entidade declara uma interface para trabalhar com o dispositivo. Arquitetura descreve a lógica do dispositivo.
Veja como é a entidade de um dispositivo de atraso Através do campo genérico, podemos definir o dispositivo para o atraso desejado. E no campo de portas, descrevemos os sinais de entrada e saída do dispositivo.
A arquitetura do dispositivo de atraso é a seguinte O código dentro da seção do processo é executado sequencialmente, qualquer outro código é executado em paralelo. Entre parênteses, após a palavra-chave do processo, os sinais são indicados, alterando o início do processo fornecido (lista de sensibilidade).
O dispositivo bcd_counter, em termos de lógica de execução, é idêntico ao dispositivo de atraso. Portanto, não vou me debruçar sobre isso em detalhes.
Aqui está a entidade e a arquitetura do 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 a lógica deste dispositivo é executada em paralelo. Falei sobre como obter fórmulas para este dispositivo em um dos vídeos do meu canal. Quem se importa, aqui está um link para o
vídeo .
No dispositivo transmissor, eu combino lógica serial e 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
Para o sinal sclk, redireciono o valor do sinal clk que entra no transmissor, mas apenas se o dispositivo estiver transmitindo dados no momento (sinal pronto = falso). Caso contrário, o valor do sinal sclk será 0. No início da transferência de dados (ativar = sinal verdadeiro), eu combino os dados de dois vetores de 8 bits (digit_pos e digit) que entram no dispositivo em um vetor de 16 bits (data_v) e transmito os dados desse vetor são um bit por relógio, configurando o valor do bit transmitido no sinal de saída dio. Entre as coisas interessantes sobre esse dispositivo, quero observar que os dados no dio são definidos na borda posterior do sinal clk, e os dados do pin dio serão gravados no registro de deslocamento da tela quando a borda principal do sinal sclk chegar. Após a conclusão da transmissão, configurando o sinal pronto <= verdadeiro, sinalizo para outros dispositivos que a transmissão foi concluída.
Veja como é a entidade e a arquitetura de um dispositivo de exibição 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 outros dispositivos. Aqui, antes de declarar sinais auxiliares, declaro os componentes que vou usar. Na própria arquitetura (após a palavra-chave begin), crio instâncias de dispositivo:
- sec_delay - uma instância do componente de atraso. O sinal de saída é roteado para sec_s.
- transfer_delay - uma instância do componente de atraso. O sinal de saída é enviado para o sinal transfer_clk.
- bcd_counter1 - uma instância do componente bcd_counter. O sinal de saída é roteado para bcd_counter_s.
- bcd_to_7seg1 - uma instância do componente bcd_to_7seg. O sinal de saída é roteado para disp_out_s.
- transmitter1 é uma instância do componente transmissor. Sinais de saída são enviados para sinais sclk, dio, tr_ready_s.
Após as instâncias do componente, um processo é declarado. Este processo resolve vários problemas:
Se o transmissor não estiver ocupado, o processo inicializa o início da transferência de dados. if(tr_ready_s) then if(not (prev_disp = disp_out_s)) then prev_disp := disp_out_s;
- Se o transmissor estiver ocupado (tr_ready_s = false), o processo definirá o valor do sinal disp_refresh_s <= true (este sinal indica que após a transferência ser concluída, os dados no visor devem ser atualizados). O valor do sinal tr_enable_s <= false também é definido, se isso não for feito antes da transmissão ser concluída, os dados baixados para o transmissor serão transmitidos
Define e redefine o sinal rclk após a conclusão da transferência de dados 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;
Gráfico de tempo
Aqui está o diagrama de temporização da transferência do número 1 para a primeira posição da tela Primeiro, os dados "10011111" são transmitidos. A posição do número no visor é "00010000" (este parâmetro chega ao transmissor como constante X "10"). Nos dois casos, o bit mais à direita (lsb) é transmitido primeiro.
Todo o código pode ser visualizado no
github . Arquivos com um subscrito * _tb.vhd são arquivos de depuração para os componentes correspondentes (por exemplo, transmitter_tb.vhd é um arquivo de depuração para um transmissor). Apenas no caso, eu também os enviei para o github. Este código foi baixado e trabalhado em uma placa real. Quem se importa, você pode ver uma ilustração do código
aqui (a partir das 15:30). Obrigado pela atenção.