VHDL Episódio 2 - Some quantos bits quiser

@ 2011-07-25 by João Paulo Pizani Flor

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”:

Ripple-carry adder
Ripple-carry adder

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:

Entei
Entei