Esta postagem também está disponível em: Inglês
Episódios anteriores:
Continuando o tutorial de VHDL, dessa vez acho que o código já não vai caber na tela de vocês… Em compensação, esse episódio traz TRÊS novidades \o/ Hoje vamos programar e testar um somador de n-bits. Um somador que pode somar dois números com uma quantidade arbitrária de bits. A figura abaixo mostra exatamente a arquitetura que vamos implementar, é um somador “ripple-carry”:

Pela figura já dá pra notar qual vai ser o conceito mais importante abordado nesse episódio: a modelagem hierárquica, usando componentes. Ou seja, nós vamos montar a arquitetura de um somador de n bits usando um componente mais simples já descrito, o nosso somador de 1 bit do episódio anterior. Claro, tem mais novidades, mas primeiro vamos ao código :)
library ieee;
use ieee.std_logic_1164.all;
entity nBitAdder is
Generic(nBits : integer := 8);
Port(
a : in std_logic_vector(nBits-1 downto 0);
b : in std_logic_vector(nBits-1 downto 0);
s : out std_logic_vector(nBits downto 0));
end nBitAdder;
architecture arch of nBitAdder is
component fullAdder is
Port(
a : in std_logic;
b : in std_logic;
cin : in std_logic;
s : out std_logic;
cout : out std_logic);
end component fullAdder;
for leftmost: fullAdder use entity work.fullAdder;
for rightmost: fullAdder use entity work.fullAdder;
constant N_BITS : integer := nBits;
signal sig_carryChain : array(1 to N_BITS-1) of std_logic;
begin
-- rightmost bit
rightmost: fullAdder
port map(
a => a(0),
b => b(0),
cin => '0',
s => s(0),
cout => sig_carryChain(1));
-- middle bits
middle: for i in 1 to N_BITS-2 generate
adder: fullAdder
port map(
a => a(i),
b => b(i),
cin => sig_carryChain(i),
s => s(i),
cout => sig_carryChain(i+1));
end generate middle;
-- leftmost bit
leftmost: fullAdder
port map(
a => a(N_BITS-1),
b => b(N_BITS-1),
cin => sig_carryChain(N_BITS-1),
s => s(N_BITS-1),
cout => s(N_BITS));
end arch;
O nosso somador “nBitAdder” tem uma novidade em sua seção “entity”: É a declaração de parâmetros genéricos. Os parâmetros que estão declarados nessa seção “Generic” têm seu valor definido em tempo de síntese, e, portanto, nosso somador não pode ser configurado “ao vivo” :), ou seja, enquanto o circuito funciona. Diferentes valores para os parâmetros alteram a síntese, e geram diferentes blocos resultantes. Muitas coisas úteis no design de hardware são feitas com esse tipo de parametrização, mas por enquanto o que nós queremos é simples: ter um somador com entradas e saídas de tamanho genérico.
Agora, quanto à declaração das portas do nosso somador:
Port(
a : in std_logic_vector(nBits-1 downto 0);
b : in std_logic_vector(nBits-1 downto 0);
s : out std_logic_vector(nBits downto 0));
Ele têm agora apenas duas entradas, que são os dois números a serem somados, cada um deles representado por “nBits” bits. O tipo de cada entrada é “std_logic_vector”, como nós já vimos no episódio anterior… A novidade é que a saída do nosso somador tem tamanho (nBits+1). isso, claro, pois a soma de dois números com n bits pode gerar um número de no máximo n+1 bits :)
A seção “architecture” já traz a grande novidade logo no início: a declaração do componente que vamos utilizar, nesse caso um fullAdder. Essa modelagem que utiliza componentes mais simples para montar os mais complexos é o famoso “bottom-up”, e esse estilo “Lego-like” de modelagem de hardware é um dos mais utilizados. A declaração de um componente nada mais é do que uma CÓPIA da seção entity desse componente, trocando a palavra “entity” por “component”. Isso parece repetitivo, e é mesmo :P Existem maneiras de não repetir essa declaração, mas isso virá mais para frente no tutorial, por enquanto, usar “component” é a maneira mais simples de fazer design hierárquico…
A constante N_BITS não é necessária, eu só usei pra mostrar como se declara uma constante, não merece grandes explicações… Já na linha de baixo vem a declaração do sinal “sig_carryChain”. Como nós vamos utilizar n fullAdders, vamos precisar de n-1 fios pra interconectar as entradas e saídas de carry deles. É por isso que declaramos um array de sinais std_logic:
signal sig_carryChain : array(1 to N_BITS-1) of std_logic;
Os índices do array vão de 1 até N_BITS-1, e portanto ele tem NBITS-1 elementos, onde cada elemento é do tipo std_logic.
Vamos então agora “montar” nosso LEGO, propriamente. Instanciamos o primeiro fullAdder, todos os outros serão instanciados de maneira semelhante.
rightmost: fullAdder
port map(
a => a(0),
b => b(0),
cin => '0',
s => s(0),
cout => sig_carryChain(1));
O primeiro bit da soma (de índice 0) é o mais à direita, o bit menos significativo. Conectamos as entradas desse fullAdder com as primeiras entradas do nosso nBitAdder, a saída com a primeira saída e a saída de carry desse primeiro fullAdder é o primeiro sinal na nossa cadeia de carry. De forma semelhante instanciamos o último bit, o mais à esquerda. A única diferença é que a saída de carry do último fullAdder é ligada ao bit “extra” (índice N_BITS) da saída.
Para instanciar os bits “intermediários” nós precisamos de uma abordagem mais “flexível” :) Nós precisamos instanciar mais NBITS-2 fullAdders. É esse tipo de “repetição controlada” de uma estrutura que o “generate” oferece.
middle: for i in 1 to N_BITS-1 generate
adder: fullAdder
port map(
a => a(i),
b => b(i),
cin => sig_carryChain(i),
s => s(i),
cout => sig_carryChain(i+1));
end generate middle;
Cada um dos somadores do meio tem suas entradas e saídas ligadas às entradas e saídas de índice correspondente na entidade de n bits. A entrada de carry vem do sinal correspondente na cadeia de carry, e a saída de carry vai pro próximo sinal da cadeia. Tudo se encaixa como deve :)
Em termos de estilo de código não temos nenhuma novidade! Mas o código de hoje mostra como seu usa comentários em VHDL. Em VHDL só existe comentário de uma linha: toda linha começando em “–” (dois hífens consecutivos) é ignorada e considerada comentário.
Vamos então à parte emocionante do episódio: mostrar que o somador funciona, através de um testbench. Bem, dessa vez o código do próprio somador ficou grande, mas por incrível que pareça o código do testbench ficou relativamente pequeno, pois usamos os arrays que acabamos de aprender…
library ieee;
use ieee.std_logic_1164.all;
entity nBitAdder_testbench is
end nBitAdder_testbench;
architecture arch of nBitAdder_testbench is
component nBitAdder is
Generic(nBits : integer := 8);
Port(
a : in std_logic_vector(nBits-1 downto 0);
b : in std_logic_vector(nBits-1 downto 0);
s : out std_logic_vector(nBits downto 0));
end component nBitAdder;
for dut: nBitAdder use entity work.nBitAdder;
constant N_BITS : integer := 4;
signal sig_a : std_logic_vector(N_BITS-1 downto 0);
signal sig_b : std_logic_vector(N_BITS-1 downto 0);
signal sig_s : std_logic_vector(N_BITS downto 0);
type testInputArray is array(0 to 7) of std_logic_vector(N_BITS-1 downto 0);
type testOutputArray is array(0 to 7) of std_logic_vector(N_BITS downto 0);
constant testInputsA : testInputArray := (
"0000", "0001", "0100", "0101", "1000", "1001", "1100", "1101");
constant testInputsB : testInputArray := (
"1101", "1100", "1001", "1000", "0101", "0100", "0001", "0000");
constant testOutputs : testOutputArray := (
"01101", "01101", "01101", "01101", "01101", "01101", "01101", "01101");
begin
dut: nBitAdder
generic map(nBits => N_BITS)
port map(
a => sig_a,
b => sig_b,
s => sig_s);
tb: process
begin
for i in 0 to 7 loop
sig_a <= testInputsA(i);
sig_b <= testInputsB(i);
wait for 10 ns;
assert (sig_s = testOutputs(i)) report "fail" severity failure;
end loop;
assert false report "All tests passed." severity failure;
end process;
end arch;
Pronto! Acabou! Funciona! Tá tudo bem agora.
Como sempre, pegue o código nesse pacote aqui: VHDL2 , compile o testbench, veja que tudo funciona e confira as formas de ondo no GTWake, surfe nas ondas do testbench! :P Mais uma vez, o makefile empacotado junto com os arquivos VHDL vão te ajudar… Não se preocupe, tudo vai dar certo, pois esse tutorial tem o selo Entei de segurança:

Olá, tudo bem?
Estou com um probleminha em um controlador de um conversor AD que estou descrevendo em VHDL. O que aconteceu é que depois de terminar o código ele ficou usando 3% a mais de espaço na FPGA e eu não sei mais o que fazer de otimização no código para melhorar isto. Você teria alguma dica de como otimizar o código para ocupar menos espaço na FPGA?
Abraço
Oi murilo, em geral há 2 “caminhos” pra seguir na busca por reduzir o espaço ocupado pelo código no FPGA:
1) Regular as configurações da tua ferramenta de síntese (ISE, Quartus, etc). Você pode tentar aumentar o nível de otimização pra espaço (vai aumentar bastante seu tempo de síntese), pode tentar mudar a codificação das suas máquinas de estados (usar menos bits), ou então pode tentar colocar o armazenamento em BRAMs ao invés de em flip-flops (tem uma configuração no ISE pra isso, mas eu não me lembro onde).
2) Diminuir o paralelismo no seu design: Essa alternativa já é mais difícil, mas vai resultar numa mudança maior no espaço ocupado no FPGA. O objetivo é você tentar localizar blocos no seu código VHDL em que operações são feitas em paralelo, ou seja, em que há replicação de hardware. Daí você deve tentar serializar essas operações, usando por exemplo uma máquina de estados para controlar a sequência. No exemplo do somador do post, a gente poderia ter usado apenas UM FULL-ADDER de 1 bit, ao invés de N. Isso iria diminuir o espaço ocupado na FPGA.
Espero ter te ajudado um pouco, e não confundido mais :)