Começando com Verilog

Opa, eae?

Demorou, mas chegamos a um dos posts que realmente tinha uma galera me cobrando para fazer… Circuitos e Verilog sem mais delongas vamos para o conteúdo!

Introdução


Se você quer aprender sobre Verilog acredito que já deve ter passado pela criação de circuitos com álgebra booleana no papel ou com ferramentas como Logisim e CircuitVerse (quem sabe até com redstone no Minecraft?).
Caso ainda não tenha estudado sobre circuitos lógicos, você ainda pode continuar lendo este post, porém a criação de bons códigos em HDLs, se dá pelo bom conhecimento de lógica booleana, então recomendo que estude isso primeiro.


Álgebra Booleana (uma breve recordação)


No princípio, um fio.


Responsive image

É isso mesmo, é só um fio.
Porém, o importante para nós é o estado dele, também chamado de nível lógico. O nível lógico em circuitos digitais se dá por um intervalo de tensões e isso pode variar entre circuitos e famílias de portas lógicas, porém sempre vão seguir um padrão como este:

Responsive image

No nosso exemplo, com uma porta da família CMOS temos um intervalo de 5v a 3.5v o qual o circuito interpretará o sinal de entrada como HIGH (Alto), ou nível lógico 1.
Seguindo o mesmo exemplo, entre 1.5v e 0v o circuito interpretará a entrada de sinal como LOW (Baixo), descrito também como nível lógico 0.

Contudo, por mais que seja importante entender de onde surgem os níveis lógicos, quando estamos trabalhando com circuitos digitais, não nos preocuparemos com a tensão em momento algum, iremos apenas tratar dos estados (Altos(1) e Baixos(0)).
Assim iremos descrever algo como: “esses fios ou as entradas dessa porta, estão com nível lógico Alto ou Baixo”


Portas Lógicas!


Responsive image

Descrevendo de maneira rápida, elas são um amontoado de componentes eletrônicos tendo como principal os transistores.
Possuem N entradas que se unificam em uma só saída, e tem o seu comportamento descrito por tabelas verdade.


Circuitos Lógicos!


A partir do momento que temos portas lógicas podemos agrupá-las em circuitos e solucionar problemas lógicos mais complexos.

Vamos supor um problema:
Você possui um escritório com duas janelas e uma porta, em cada abertura está instalado um sensor que indica quando a mesma está aberta retornando o estado 1 (e 0 quando está fechada).

Responsive image

Quando você está no escritório sempre deixa a porta aberta, e como está dentro do comodo as janelas podem estar em qualquer posição, pois ninguém tentará entrar pela janela enquanto você está por lá.
Porém, você é uma pessoa um pouco esquecida e saiu algumas vezes do seu local de trabalho fechando a porta, porém deixando ao menos uma janela aberta.
Como sabe eletrônica, resolveu colocar um alarme (que reproduz som caso receba 1 em sua entrada e fique ocioso caso receba 0) para que isso não ocorra mais. Assim, caso esteja fechando a porta, contudo ao menos uma das janelas esteja aberta, o alarme soará o fazendo entrar novamente, fechar a(s) janela(s) aberta(s) e por fim sair deixando o local em segurança.

A princípio, para quem ainda está começando com lógica de boole, este problema pode parecer complexo, mas já te adianto que com uma unidade de 3 das portas da imagem a cima ele pode ser solucionado, vejamos:

Devemos saber quando uma OU outra janela está aberta (ou até ambas), assim podemos unificar o sinal dos dois sensores das janelas na saída de uma por OR.

Responsive image

Bom, agora sabemos quando alguma janela está aberta, caso isso aconteça E a porta esteja fechada devemos acionar o alarme! Assim, caso a saída do nosso OR seja 1 E nossa porta esteja fechada (portanto, emitindo o estado 0) temos que ter o sinal 1 para nosso alarme.

Podemos Inverter a saída do sensor da nossa porta, com uma porta NOT, agora quando for fechada temos como resultado 1.


Responsive image

E quando tivermos 1 vindo da nossa porta OR E 1 vindo da nossa porta NOT temos nosso caso de acionamento do alarme, logo um AND entre esses dois sinais pode resolver nosso problema!


Responsive image

E por fim temos nosso circuito! Mesmo com poucas portas conseguimos descrever um problema maior e real!

Lógico, há muitas outras formas de se chegar neste circuito além da dedução, o ideal inclusive é utilizar formar mais elegantes, como a criação de tabelas verdades e a simplificação por mintermos ou maxtermos, ou até a utilização de mapas de karnaugh.
Porém, aqui neste post estou apenas relembrando alguns conceitos de álgebra booleana, o foco aqui será o Verilog.


Linguagens de Descrição de Hardware (HDLs)


Linguagens de Descrição e Hardware são, como o próprio nome já diz, linguagens utilizadas para descrever como são hardwares.
Ok?... Mas o que exatamente é isso?

Quando se está aprendendo sobre eletrônica digital, e assim, desenvolvendo circuitos como o exemplo anterior, é comum desenhar em papéis ou utilizar ferramentas gráficas (como as já citadas Logisim e CircuitVerse), onde pelo “arrasta e solta” de portas lógicas criam-se circuitos maiores.
Para pequenos projetos, essa forma de criação é válida e ajuda muito o entendimento pelo visual. Porém, projetos mais complexos exigiriam folhas e folhas ou ficariam confusos em pequenas telas. Além disso, às vezes necessita-se replicar componentes a exaustão com a instanciação de N módulos, desenhar tudo isso levaria tempo de mais e a replicação por ctrl+c, além de ser deselegante, criaria os mesmos problemas de manutenibilidade de replicação de códigos em programas.

Em resposta a tudo isso, surgem as HDLs, vindo como linguagens próximas em sintaxe as linguagens de programação e tendo como vantagem extra a simulação com testes em computadores e hardwares (FPGAs).

As HDLs ainda podem descrever o hardware pela descrição formal de seus circuitos, isto é, descrever os componentes e portas lógicas os conectando um a um, ou também informar como o circuito deve funcionar e deixar a encargo dos compiladores e interpretadores criarem um circuito para uma resposta equivalente. (Falaremos mais disso em Verilog e suas formas Comportamentais, Estruturais e Funcionais).


HDLs VS Linguagens de Programação


Se tem algo que deve ficar claro aqui e se você tiver que levar apenas um conhecimento deste grande post leve esta informação:
“Linguagens de Descrição de Hardware NÃO são Linguagens de Programação!”.

Elas de fato parecem linguagens de programação, possuem sintaxes parecidas e até copiadas, mas não são!

Por que isso deve ficar claro?
Quando se está aprendendo HDLs e não se associa o pensamento a hardware e circuitos, é muito comum, pela próxima sintaxe, cair no pensamento imperativo de linguagens comuns de programação.
Isto pode fazer você pensar que as portas e módulos ali descritos estão funcionando de maneira sequencial, ou seja, um após o outro conforme estão descritos no arquivo. Porém, isto não acontece na realidade, os módulos ali descritos estão equivalentemente a circuitos fixos em uma protoboard ou PCB, não existe quem é ativado ou está funcionando primeiro (a não ser de circuitos acionados por clock), todos ali estão ao mesmo tempo, funcionando, recebendo suas entradas e acionando suas saídas.
Isto tanto é verdade que, na prática, tanto faz a ordem que as linhas do seu código aparecem, você pode alterá-las mudando de ordem no arquivo e isso não mudará o circuito final feito.
Lembre-se, você apenas está descrevendo uma placa dizendo o que está nela e como estão ligados.

Dito isto, não programamos em Verilog ou em qualquer outra linguagem de descrição de hardware. Eu sei, isso é triste e decepcionante, mas como não estamos tratando de linguagens de programação, não estamos programando.
O que estamos fazendo, então? Descrevendo!
Quando se está utilizando HDLs você está descrevendo hardware, logo, quando alguém olhar sua tela e o perguntar “O que você está fazendo?”, você já tem a resposta “Eu estou descrevendo hardware!”, aposto que irá impressionar todos e fazer você ter um excelente encontro depois 🤓.

Um adendo: Pela minha lógica, salvo algum equívoco, não podemos programar, mas podemos sim “codar” em HDLs. Veja bem, “codar” vem da nossa forma de abrasileirar “coding” que nada mais é que “codificação”, assim, se estamos fazendo código, não interessa de qual tipo, estamos codificando ou “codando” e como HDLs são códigos, logo, podemos dizer que estamos codando em Verilog ou demais linguagens de descrição de hardware.


Verilog


Chegamos finalmente no Verilog!
Verilog é simplesmente uma das primeiras e acredito que a linguagem de descrição de hardware mais utilizada.

Nela tudo o que fazemos está dentro de módulos, assim o início de um projeto se dá pela instanciação do primeiro módulo, que pode receber qualquer nome, vamos ao código:

module meuModulo(input entrada, output saida);

    assign saida = entrada;

endmodule

Temos inicialmente a palavra reservada module, esta instancia nosso modulo, após ela vem o nome que daremos ao nosso módulo “meuModulo”.
Em seguinte vem entre parenteses a listagem das entradas e saídas com as palavras reservadas input e output, e após elas os nomes das portas que damos aos nossos módulos.
No fim temos a palavra reservada endmodule a qual fecha nosso bloco de módulo e ao meio que desenvolveremos a lógica deste.

Como conteúdo do nosso módulo temos apenas uma linha assign entrada = saida;, nela estamos atribuindo a entrada a saída diretamente, pois não estamos fazendo nada com o sinal, ou seja, aqui criamos apenas um fio, tudo que vem pela entrada sai pela saída sem nenhuma lógica adicional.

Representação visual do módulo:

Responsive image

Sintaxe



-Ligações com Fios, Registradores e Lógicos (Wires, Regs and Logics):

Todos os inputs e outputs dos módulos em Verilog são fios (Wires), além disso, wires auxiliares podem ser declarados dentro de módulos com a sintaxe “wire nome_do_fio;”.

Fios funcionam como fios na vida real, isto é, ligam coisas e o estado que eles representam depende de naquele mesmo momento estarem recebendo este estado.
Um fio só irá “devolver” 1 se no mesmo momento da “consulta” ele estiver recebendo 1 de alguma fonte.

Muito parecido aos fios, registradores também recebem e emitem valores, porém estes guardam os valores recebidos até que recebam um valor diferente, ou seja, se um reg em algum momento receber o estado 1, irá emitir esse estado até que receba 0, caso nunca receba 0 irá emitir para sempre 1.

A sintaxe para descrever um registrador é: “reg nome_do_registrador;

Verilog recebeu diversas atualizações ao longo do tempo, porém sempre manteve compatibilidade com as versões antigas; por isso, há às vezes, muitas sintaxes e modos de se descrever a mesma coisa.
Uma das grandes atualizações do Verilog e a mais recente é a SystemVerilog, e nela foi introduzido um novo tipo de ligação a Lógica.

Em muitos códigos verilog a diferença prática entre regs e wires passa a ser nula, podendo se usar qualquer um dos dois e tendo como final um circuito que resolve o mesmo problema, além disso, essa diferença acabava por complicar um pouco o desenvolvimento em verilog sobretudo para iniciantes. Assim, na versão SystemVerilog a ligação lógica foi desenvolvida como uma abstração para substituir ambos (regs e wires) deixando a encargo do interpretador decidir como logic irá se comportar.

A sintaxe para descrever um lógico é: “logic nome_do_registrador;


-Vetores ou Barramentos (Vectors/Buses):

Até agora tratamos de wires, regs e logics com apenas 1 bit, porém cada um destes podem ser um vetor que representa um barramento de N bits a sintaxe para barramentos será:
O tipo (wire, reg, logic) seguido do tamanho representado pelo último valor do barramento seguido de dois pontos e o primeiro valor (entre colchetes) e o nome do nosso vetor.
Ex: Um vetor de 8 bits do tipo wire será descrito assim:

wire [7:0] vector;

Da mesma forma um vetor de 16 bits do tipo reg será descrito:

reg [15:0] vector;

Também pode-se querer separar apenas um bit ou alguns bits de um vetor, neste caso se utiliza a mesma notação dos colchetes, porém após o nome do vetor, ex:

assign a = vector[2];

Neste exemplo atribui-se a “a” o terceiro bit do vetor.

assign b = vector[4:2];

Neste exemplo atribui-se a “b” os bits da posição 3 a 5 (lembrando que “b” deve ser declarado antes como um vetor de pelo menos 3 bits).


-Operadores Lógicos (Logic Operators):

Operadores lógicos são, como o próprio nome já, diz operadores que exercem funções lógicas, ou nesse caso funções de portas lógicas.
Assim eles irão receber dois bits e retornar a resposta de um bit coerente a ponta lógica que o símbolo representa. São eles:

Símbolo Função
&& And
|| Or
! Not

Um exemplo utilizando o operador And, seria:

wire a, b;
wire result; 

assign result = a && b;

Aqui o wire, result recebe constantemente o valor resultante da porta AND com as entras “a” e “b”.


-Operadores Bit a Bit (Bitwise Operators):

Operadores bit a bit irão performar em cada bit de um vetor separadamente, ex:

wire [2:0] a, b, result; //Três vetores de 3 bits cada

assign result = a & b;

Aqui atribui-se a operação And dos vetores “a” e “b” bit a bit no resultado.

Este código seria equivalente a:

wire [2:0] a, b, result; //Três vetores de 3 bits cada

assign result[0] = a[0] & b[0];
assign result[1] = a[1] & b[1];
assign result[2] = a[2] & b[2];

Onde cada bit é atribuído individualmente.

Tabela de operadores bit a bit:

Símbolo Função
& And
| Or
~ Not
^ Xor

Operadores bit a bit ainda podem se comportar como operadores de redução de vetores, isso acontece caso sejam colocados juntos a um único vetor, realizando assim a operação do símbolo com todos os valores deste barramento, retornando um único bit resultado dessa operação. Ex:

wire [2:0] a;
wire result; 

assign result = &a;

Este código seria equivalente a:

wire [2:0] a;
wire result; 

assign result = a[0] & a[1] & a[2];

-Operadores Aritméticos (Arithmetic Operators):

Operadores aritméticos, são os mais parecidos com linguagens de programação, eles podem ser utilizados para bits únicos, porém são bem mais comuns serem utilizados junto a vetores, a tabela que os representa é:

Símbolo Função
+ Adição
- Subtração
* Multiplicação
/ Divisão
% Mod (Resto da divisão)
** Exponenciação

Um código que representa a soma binária é:

assign result = a + b;


-Representação numérica

Verilog pode automaticamente converter números em diferentes bases para realizar comparações ou atribuições a notação para isso é:
O tamanho necessário para armazenar o número em binário, seguido de aspas simples, uma letra reservada para representar em que base está escrita o número (d = decimal, h = hexadecimal, b = binário, o = octal) e o número propriamente dito. Ex:

Atribuindo a um vetor o número 12 em decimal:

assign vector = 4’d12;

Atribuindo a um vetor o número 1F em hexadecimal:

assign vector = 8’h1f;

-Concatenação e Replicação

Ainda na manipulação de vetores tem-se a concatenação e replicação.
A concatenação se dá por juntar bits e/ou vetores, ex:

assign vector0 = {a, c}; //Concatenação de “a” e “c” (podem representar vetores ou bits únicos)

assign vector1 = {a, b, 2’b10, d}; // Concatenação de “a”, “b”, 10 (em binário) e “d”

assign vector2 = {a[2], b, c, d[3:1]}; // Concatenação do bit na segunda posição de “a”, “b”, “c” e do intervalo de bits desde a posição 3 até a posição 1 do vetor “d”

Aqui atribuímos aos vetores diferentes concatenações que utilizam bits, outros vetores ou partes de outros vetores.

A replicação possuí uma sintaxe muito parecida a concatenação, coloca-se quantas vezes irá se repetir o valor seguida do valor entre chaves. Ex:

assign vector0 = 10{1’b1}; //10 bits “1” seguidos

assign vector1  = 12{2’b10}; //12 vezes a replicação dos bits “10”

assign vector2 = 6{a[15]}; //6 vezes o bit da posição 15 do vetor “a”

Concatenações e replicações muitas vezes são utilizadas juntas:

assign vector = {a[15], 15{1’b0}}; //Bit da posição 15 do vetor a seguido de 15 zeros

-Verilog Estrutural e Funcional

As formas Estrutual e Funcional de Verilog muitas vezes são descritas apenas como Funcional, isso vai variar de autores e sites, por isso irei explicar as duas separadamente e caso você queira, pode entende-las como uma só.


Verilog Funcional:

Para todo efeito, é o verilog que estamos usando até agora, dentro dos módulos para representar as portas e circuitos utilizamos atribuições (assign), seguidas de operadores que criam a lógica do nosso circuito, um exemplo do nosso circuito das janelas e portas descrito no começo desse post é:

module acionaAlarme(
input janela0,
input janela1,
input porta0,
output saida
);

    assign saida = (!porta0) && (janela0 || janela1);

endmodule

Verilog Estrutural:

O verilog Estrutural cria sua lógica chamando portas como módulos.
A sintaxe padrão é: o nome da porta e entre parenteses seu output (sempre em primeiro) e seus inputs logo após (separados por virgulas).
Ex:

and (output, inut1, input2);
not (output, input1);

E um código equivalente ao apresentado anteriormente seria:

module acionaAlarme1(
input janela0,
input janela1,
input porta0,
output saida
);

    wire portaN, janelas;

    not (portaN, porta0);
    or (janelas, janela0, janela1);
    and (saida, portaN, janelas);

endmodule

Verilog Comportamental:

Não estamos trando de Elixir, mas de fato é aqui que a mágica e alquimia acontece.

Até agora descrevemos o circuito como ele é, ou seja, quais portas deve utilizar, quais segmentos de vetores deve utilizar, como estão as ligações ente módulos e etc.

No verilog comportamental não faremos isso, e sim descreveremos como o circuito deve se comportar, a criação do circuito fisicamente com as portas lógicas será dada pelo interpretador.

Aqui a criação de códigos Verilog se torna muito mais próxima da criação de códigos em linguagens de programação, porém volto a lembrar, por mais que você efetivamente não esteja fazendo o circuito o interpretador estará, e tudo que você cria representa um circuito em uma placa e não um código em uma máquina.

A tag que marca os módulos comportamentais é a tag “always” que normalmente será sucessida de um “@” (at) e alguma entrada, como clock do circuito em circuitos com clock, alguma saida de outro módulo, ou até mesmo “*” que representa qualquer mudança possível em qualquer lugar do circuito.
A entrada do always irá ditar quando este bloco é executado.

Dentro da tag always, pode-se usar comparações com ifs ou cases ou atribuições que só ocorrem caso a entrada do always seja confirmada.

O código equivalente a os dois já apresentados aqui, porém no modelo Comportamental é:

module acionaAlarme2(
input janela0,
input janela1,
input porta0,
output saida
);

    always @(*) begin
        if (porta0 == 1"b0 && (janela0 || janela1)) begin
            saida = 1"b1;
        end else begin
            saida = 1"b0;
        end
    end

endmodule

O modelo comportamental é de fato uma linguagem a parte e é bastante complexo, sua utilização é muito comum em circuitos com armazenamento (flip-flops) como as máquinas de estado.
Irei deixar apenas a introdução desse tipo de módulos nesse post, podendo futaramente abordar mais sobre ele em publicações futuras.


Ferramentas


Para quem está começando acredito que a melhor forma de aprender é se deparar e se submeter à problemas, para isso o site HDLBits é ótimo!
Quem é de programação já deve ter visto sites de desafios como Exercism ou o Beecrowd (antigo URI Jungle), o HDLBits é a versão deles para hardware e Verilog, lá há vários exercícios, os quais você submete sua resposta e ela é avaliada por testes para indicar o bom funcionamento.

Caso precisem de ajuda eu tenho esse repositório com resoluções de boa parte dos exercícios do site em: https://github.com/MarlonHenq/Verilog-HDLBits-solutions

Outra forma muito bacana de aprender Verilog e de visualizar seus códigos é utilizar o DigitalJs que possui sua versão online no site e também é uma extensão no VSCode, todas as imagens dos circuitos deste post foram feitos com essa ferramenta.


Isso é tudo!


Ufa! Esse post demorou muito para ser feito e foi muito complexo, mas espero que você tenha gostado e aprendido muito!
Caso reste dúvidas deixe ai embaixo (para o pessoal do Dev.To) ou me chamem no twitter @MarlonHenq. Me sigam lá para demais atualizações e posts e caso esse post tenha te ajudado não deixe de compartilhar nas suas redes.

Flw!