Você está na página 1de 50

Apostila Aleatória de VHDL e GHDL do Borges

Capítulo 0: Introdução

Oi. Eu sou o Borges. Pode me chamar de Borges. Este é o primeiro de uma série
de documentos na qual eu procuro ensinar VHDL “do zero”, pra ajudar a galera que tá
começando por causa de Sistemas Digitais ou mesmo Lab. Digital. A primeira parte
consistirá em uma introdução aos conceitos-chave, e eu vou assumir que você tá caindo
completamente de paraquedas e fazendo (ou fez) SD1.

O que é o VHDL? De verdade. Você saberia explicar pra um parente confuso com
tecnologia? Sei não. Bem, vamos fingir que eu sou famoso dando uma TED Talk, e usar
um clichê tão batido que o Google pode muito bem #cancelar minha conta hoje:

O que o VHDL não é?

● O VHDL não é um programa ou ferramenta.


● O VHDL não é uma linguagem de programação.
● O VHDL não é tão difícil quanto a Poli faz parecer.

Agora que sabemos o que ele não é, fica mais fácil discutir o que ele é. O VHDL é
uma linguagem de descrição de hardware. Ele é uma especificação enorme, rígida, e
completa de uma maneira de escrever texto que pode ser inequivocamente interpretado
como uma descrição de um sistema digital, e inclui uma biblioteca de funções e tipos
padrão que aproximam essa linguagem do mundo real. Na verdade, ele é uma série que
estreou nos anos 80, a temporada mais recente é de 2019, mas a mais popular é a de ‘93.

Ou seja, é uma especificação. Só isso. Isso puxa a próxima questão: o que eu faço
com ele? Bem, tudo depende das ferramentas que você tem. Algumas, como o Quartus,
podem pegar seu código, simular, fazer umas ondinhas da hora, ou mesmo compilar e
botar numa plaquinha FPGA!

Página 1 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


O Quartus é enorme, extenso, complexo, e você vai aprender a sentar na ponta
desse iceberg computacional no Lab. Digital I. Aqui, vamos apenas discutir a linguagem,
e como escrever código correto que faz o que tem que fazer. Se você joga ele no Quartus
ou não é um problema exclusivamente seu. Mas como vamos apenas fazer simulações, o
Quartus é overkill. Por isso, vamos usar um programa menor, mais rápido, e mais
direto-ao-ponto, o GHDL. Ele compila VHDL em executáveis nativos rápidos, os quais
rodam simulações que podem ser analisadas depois. Falaremos mais disso depois.

Vamos falar um pouco da linguagem. Um projeto em VHDL tem aspectos


estruturais parecidos com um programa em C… pera, calma. CALMA. Não estou falando
de segfaults, microgerenciamento de memória, nem de ponteiros, mas da estrutura das
coisas. Por exemplo, no C, uma função pode ser declarada e definida:

Declaração Definição

● Entradas e saídas ● Explicita o funcionamento interno


● Não dá nenhuma informação ● Pode ser “puxada” de bibliotecas
sobre o funcionamento interno externas
● Necessária para compilar ● Necessária para linkar programas
programas que usam funções de que usam funções de outra
outra biblioteca/arquivo biblioteca/arquivo
● “Caixa-preta” ● Implementa um protótipo

Mantenha esse padrão em mente: caixas-pretas e implementações do que tem


dentro. No VHDL, acontece algo bem semelhante.

No VHDL, uma entity é a caixa-preta, tal qual a declaração (protótipo) de uma


função em C, apenas define os tipos de entrada e de saída, sem afirmar nada sobre o
funcionamento interno.

Analogamente, uma architecture em VHDL é como a definição (implementação)


de uma função em C, definindo o funcionamento interno de uma função com um
protótipo conhecido. É na definição que você declara variáveis internas, faz contas,
chama outras funções, e relata informações ao usuário. Na architecture, declaramos
sinais internos, instanciamos componentes auxiliares, e fazemos associações
estruturais.

Página 2 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Veja como a analogia com VHDL bate certinho (não tente entender o código):

Entity/component Architecture

● Entradas e saídas ● Explicita o funcionamento interno


● Não dá nenhuma informação ● Pode ser “puxada” de bibliotecas
sobre o funcionamento interno ● Pode instanciar outros
● Necessária para compilar (análise) componentes
quando puxamos componentes de ● Necessária para linkar
outra biblioteca/arquivo (elaboração) quando puxamos de
● “Caixa-preta” outra biblioteca/arquivo
● Implementa um protótipo

Ou seja, de longe, seu projeto em VHDL vai ser isso: um amontoado de entities e
suas architectures, implementando funcionamento como definido na fase de projeto.
Nas architectures, você vai instanciar outros componentes, fazer atribuições
combinatórias, implementar lógica sequencial, trabalhar com sinais intermediários e
tipos numéricos, e até mesmo reportar informações na tela (no caso de uma testbench).

Pra finalizar, vamos falar mais rapidinho sobre as simulações. Em VHDL, o jeito
mais comum de se operar uma simulação é escrever (com ou sem ajuda de ferramentas,
como o editor de waveforms do Quartus) uma testbench, que é uma entity sem entradas
nem saídas, feita exclusivamente pra enviar sinais pros seus componentes, testar as
saídas, e te avisar caso dê ruim.

O GHDL consegue compilar seu componente e sua testbench em executáveis


nativos, que executam a simulação da testbench e opcionalmente produzindo um
arquivo VCD (variable change dump), que contém os valores de todos os sinais a cada
instante, que você pode abrir num programa bonitinho como o GTKWave.

Absorva essa informação com carinho, e quanto estiver confortável, vamos pro
próximo capítulo.

~ Borges

Página 3 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 1: Circuitos combinatórios simples

Bem, na última aula eu falei muito e não escrevemos quase nada. Hoje vai ser um
pouco diferente. Vamos escrever bastante código! Vamos aprender o básico da sintaxe
de entities e architectures de circuitos combinatórios simples. Isso é a base para fazer
coisas mais complexas, então instale GHDL (tutorial para Windows 10) e acompanhe
com o código se puder!

MAS (sempre tem um “mas”) vamos discutir rapidamente o que é bit e o que é
std_logic. No VHDL, temos vários tipos de dados, e para representar estados lógicos,
temos esses dois como principais. O bit é exatamente o que o nome diz, um tipo que
pode assumir sempre um de dois valores, ‘0’ ou ‘1’. Por ser extremamente simples, é o
que mandam você usar em SD1. Em disciplinas subsequentes, muito provavelmente vão
te mandar usar o std_logic. Esse tipo, além de ‘0’ e ‘1’, pode assumir valores como ‘U’
(não-inicializado), ‘X’ (desconhecido) e ‘-’ (“don’t care”). Ele é mais complexo, mas é mais
útil para encontrar problemas no seu código do que o bit. Aqui, por enquanto, vamos
brincar só com bits.

Uma última coisa: vai ter muito exemplo de código, e isso vai introduzir sintaxe
nova. Recomendo manter o adendo sobre operadores e símbolos aberto, especialmente
no começo, pra não se enrolar muito com detalhes de sintaxe. Não tente lê-lo inteiro
agora nem no fim, deixe aberto para consultar quando precisar.

Isso estando resolvido, bora fazer nosso primeiríssimo componente: uma porta
XOR de 4 bits! Assim como numa função em C primeiro decidimos o protótipo e depois a
implementação, vamos primeiro escrever a entity, a caixa-preta, e depois uma
architecture com seu funcionamento. A entity é a parte mais fácil: por ora, ela só vai
conter uma lista de entradas e saídas com seus nomes e tipos:

Página 1 - Revisão 2 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Leia e internalize essa sintaxe com calma. Também recomendo fortemente deixar
o adendo de símbolos aberto, ele pode ser bem útil pra consultar nesse começo.

Se acalmou? Boa. Bem, eu admito que tentei te pegar de surpresa. Tem bit_vector
aí. O que é isso? No VHDL, costuma-se trabalhar não apenas com os tipos-base, mas
vetores contendo várias instâncias desse tipo-base. O tipo bit_vector é só um vetor com
bits. Para instanciar um bit_vector, temos que, no mínimo, especificar um range. Um
range é algo da forma “A downto B” ou “A to B”. Qual a diferença entre “to” e “downto”?
Não vou entrar muito nos detalhes, mas o “to” funciona do jeito que você tá
acostumado(a): o item #0 do vetor corresponde ao mais significativo (à esquerda). Com
“downto” é ao contrário, bem mais natural quando estamos trabalhando com números.

Ok… agora a caixa-preta faz um pouco mais sentido. Duas entradas de quatro
bits cada, uma saída de quatro bits, e nossa intenção é que a saída seja o XOR das
entradas correspondentes. Como implementamos isso? Na architecture, claro:

Parece simples… mas não se deixe enganar pela aparência familiar de código. O
que fica entre o begin e o end não é “código” no sentido ao qual você está acostumado.
Entre o begin e o end rolam ações concorrentes. Uma ação concorrente é qualquer coisa
no VHDL que representa algo que pode ser feito simultaneamente. Parece meio fora do
nosso mundo, né? Vou tentar fazer uma analogia idiota com reformas.

Numa reforma, talvez uma das coisas que você vá querer fazer seja pintar as
quatro paredes do cômodo. Você então faz uma lista como: “parede A verde, parede B
verde-claro, parede C branca, parede D azul-bebê”. Nessa lista, obviamente, a ordem não
importa; as paredes são objetos distintos, tanto que se você tiver três pessoas dispostas
a te ajudar, vocês podem cada um pintar uma parede simultaneamente, e o resultado de
nenhum vai interferir no do outro.

Seguindo na analogia, além da ordem não fazer diferença, há coisas que não
fariam sentido nessa lista, como listar uma parede duas vezes. Você não vai pintar a
mesma parede duas vezes… No VHDL, atribuições concorrentes seguem as mesmas

Página 2 - Revisão 2 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


regras. Você estabelece uma série de relações que conectam entradas, sinais internos, e
saídas; não faria sentido atribuir à mesma coisa duas vezes.

É essa ideia de concorrência que quebra os novatos. Você não pode somar 1 a um
inteiro e depois dobrá-lo como em C. A ordem não importa, não pode importar. Ações
concorrentes não são como um programa normal. Sendo assim, nelas, você geralmente
está limitado a definições, sem ações. Para introduzir ações e estados, você vai precisar
usar um process. Dentro deles, as coisas lembram um pouco mais um programa normal,
com “if”, “else”, “for”, etc. Mas não vamos falar de process por enquanto.

Enfim… esse projetinho funciona. Mas como simulá-lo? Isso eu vou ensinar mais
pra frente, quando eu falar de testbenches, e isso só depois de falar de circuitos
sequenciais e processes. Por enquanto, você vai ter que confiar em mim quando eu falo
que funciona. Bora pro próximo!

Esse vai ser um pouco mais cabeludo: um somador completo de um bit. Você está
familiarizado com esse circuitinho? Ele faz um pedaço vertical de um processo de soma,
com as parcelas, “vem um”, “vai um”, a coisa toda. Se necessário, faça uma pausa e
refresque sua memória com respeito a esse circuito. Agora, vamos declarar a entity dele:

Até aí, só uma caixa preta mesmo. Nada de muito espetacular. Bits entram, bits
saem. A verdadeira mágica, como você vai ver, rola sempre na architecture! Como vamos
fazê-la? Bora dar uma olhada no diagrama lógico de um somador completo:

Página 3 - Revisão 2 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Vamos fazer algo que não fizemos no outro sisteminha digital: declarar um signal.
Um signal, ou sinal, é um valor interno ao funcionamento do circuito. Ele pode ser um
bit, um vetor de bits (efetivamente, poder acabar operando como um registrador!), ou
um valor “virtual”, como um número inteiro, que só é assim para nossa comodidade…
no mundo real, ele vira um registrador como qualquer outro. Assim como variáveis num
programa, você pode tanto declarar um sinal por clareza, simplificando suas expressões
quanto por necessidade. No caso do nosso somador, faremos por clareza.

Esse sinal vai armazenar um valor intermediário, algo que usamos mais de uma
vez na hora de expressar nossas saídas como função das entradas. Isso fica evidente
quando olhamos o diagrama: o valor de A XOR B é usado tanto na expressão de S quanto
na de cout, então podemos armazená-la num signal!

Manjou? Como discutimos antes, podemos trocar essas atribuições de ordem que
nada irá mudar. É isso que torna VHDL uma linguagem diferente das demais, esse
elemento concorrente que te força a expressar relações entre variáveis do jeito mais
essencial e não-ambíguo possível, permitindo que isso possa ser interpretado não
apenas como um programa para simular posteriormente, mas de fato como uma
descrição de hardware.

Enfim, esses dois projetos funcionam, se você jogar numa pasta e mandar o GHDL
analisar, vai produzir os arquivos .o certinhos, sem erro nenhum. E pra simular? Isso a
gente discute mais pra frente. Lá pra frente...

Por este capítulo é só. Absorva com carinho.

Página 4 - Revisão 2 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 2: Instanciando componentes

Neste capítulo, vamos aprender uma das coisas mais legais do VHDL: usar um
componente dentro de outro. Isso, juntamente com generics (o próximo assunto), é uma
das coisas mais poderosas em termos de design. Você pode criar componentes
incrementalmente maiores, usando pecinhas menores e conectando-as de acordo.
Eventualmente, a sua entidade “final”, seu objetivo, vai ter mais cara de um dataflow
ligando várias pecinhas do que de um circuito lógico de verdade.

Como isso funciona na prática? Vamos começar voltando na analogia com C.


Lembra dela? Que eu expliquei entidades e architectures do VHDL são mais ou menos
análogas a protótipos e implementações de funções em C? Então, vamos voltar nisso.
Num programa em C, quando você quer usar uma função definida numa biblioteca
externa, você geralmente tem que importar um arquivo .h contendo os protótipos das
funções que você quer usar. Por exemplo, para poder usar o printf, você tem que
incluir o stdio.h, que é um arquivo enorme contendo, entre outras coisas, o protótipo
da função printf. Isso só serve para tornar o resto do seu código válido (ele “sabe” que
tem uma função printf com aquela “cara”), e cria uma pendência: a não ser que você
forneça uma, o compilador vai ter que procurar uma implementação nas bibliotecas
disponíveis.

No VHDL, acontece algo parecido. Para um componente poder instanciar outro,


ele tem que incluir, em si, a entity do subcomponente. Sim, eu sei que você já a escreveu
no outro arquivo. Eu não vou entrar nos detalhes do porquê. O ponto é, quando você tá
escrevendo uma architecture e tá a fim de instanciar outro componente pra te ajudar,
você vai ter que copiar e colar a caixa-preta dele naquela architecture.

Chega de papo furado e bora pro exemplo! Vamos começar com algo simples.
Vamos instanciar dois somadores completos do capítulo anterior e fazer um somador
completo de dois bits! Como sempre, começaremos pela entity:

Página 1 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Como você pode ver, eu declarei os vetores com “downto”: estamos tratando de
números, e a primeira operação é a sobre o bit menos significativo. Você pode fazer
como se sentir mais confortável, mas quando estamos trabalhando com números, o
“downto” é mais natural.

Prosseguindo… Agora, você vai ver, pela primeira vez, uma descrição estrutural:
ao invés de mexermos nas operações entre os bits e vetores, vamos apenas instanciar
componentes e conectá-los de acordo. Isso vai ser um tema recorrente em SD2.

Página 2 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Ok, nada de pânico. Vamos por partes. Primeiro, eu declarei o componente, lá
antes do begin, junto dos sinais e tudo o mais. e então declarei um sinal intermediário
para o carry que vai do primeiro pro segundo somador (por necessidade). Por fim, na
ações concorrentes, instanciei dois daquele componente, ligando suas entradas e saídas
de acordo usando port map. Uma particularidade que você talvez tenha notado é que
acessamos vetores e arrays com parênteses, e não com colchetes como normalmente.

Antes de cantar vitória, calma. O arquivo que estamos escrevendo está…


incompleto, por assim dizer. Vamos ter que mergulhar um pouco mais fundo na analogia
do C. Lembra que eu falei do printf? Quando você “compila” seu hello world em C
usando o GCC, ele passa por várias etapas. Resumidamente, temos duas: compilação, que
é pegar seu .c e produzir um .o, e a linkagem, que resolve as pendências no .o (como, por
exemplo, a função printf, que você só declarou ao importar printf, mas nunca definiu), e
produz um executável bonitão.

No VHDL, algo semelhante ocorre. Se você analisar o componente acima com algo
do tipo “ghdl -a soma2.vhd”, não vai ter erro nenhum, e um .o será produzido. Contudo,
da mesma maneira que o .o do seu hello world em C possui referências pendentes ao
printf, que foi declarado mas nunca implementado, o soma2.o vai possuir uma
referência pendente à entidade soma1. Portanto, para ser linkado corretamente num
executável, precisará resolver essa pendência, procurando num tal de “soma1.o” a
implementação da entity.

No GCC, essas intenções são declaradas em tempo de invocação: você especifica


nas opções do comando “gcc” onde procurar bibliotecas pra resolver pendências (fora as
bibliotecas-padrão, obviamente). Já no VHDL, você especifica no código onde o VHDL
deve procurar por funções externas, componentes, etc. No caso, como é um componente
do seu projeto, ele vai estar na biblioteca “work”. Então você vai ter que falar pro GHDL,
“use o soma1 da work, fazendo favor aê”. No começo do soma2.vhd:

Sem isso, você não vai poder elaborar o soma2 futuramente, por exemplo, para
produzir um executável, uma testbench. Sem isso, o GHDL sabe que o componente
soma2 usa um “tal de soma1” que “tem aquela cara”, mas sem a menor ideia do que ele
faz até você falar pro GHDL como descobrir.

Antes de terminarmos, vamos introduzir só mais um conceito interessante, ele


vai parecer meio jogado aqui, mas ele vai brilhar muito quando unido ao próximo
capítulo.

Página 3 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Imagina que você quer fazer um somador com os mesmo princípios desse, mas
quer que ele tenha 64 somadores encadeados. Bem, ferrou, né? Vamos ter que copiar,
colar e editar os 64 somadores na mão?! Relaxa, tem um jeito melhor, e ele se chama
for-generate. Não se deixe iludir, ele não é como o for mais conhecido. Ele é uma ação
concorrente, mais especificamente, ele é um jeito de “repetir” outras ações concorrentes
em termos de um range.

Por exemplo, se quiséssemos fazer o nosso somador de 64 bits com for-generate,


tem dois jeitos de fazer, dependendo de como queremos encadear os carry-ins e
carry-outs. Um jeito mais “claro” é fazer três etapas de instanciação de componentes: um
na ponta, recebendo o carry-out de fora e fazendo a conta do bit mais significativo, 62
contadores no “miolo” com seus carries encadeados, e um outro na ponta, para o dígito
mais significativo, que cospe o carry-out do contadorzão inteiro. Manjou? Olha a entity:

Nada de novo por enquanto. Só mudamos uns números. Já na architecture, você


vai ver umas coisas meio diferentes. Mantenha a calma, vai dar tudo certo, e se não der,
vai dar quando você ler o texto que vem depois.

Página 4 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


correção: cin => cmiolo(63)
eu sou imbecil, corrijo dps

Página 5 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Bastante coisa, né? Mas se você prender a respiração e ler com calma, vai ver que
é basicamente o que eu escrevi lá na outra página. Um somador para o dígito/bit menos
significativo (LSD = least significant digit), 62 para o “miolo”, e mais um na outra ponta.
Apesar desse código ser bem elegante, tem como ser mais ainda. Podemos
simplesmente gerar os 64 contadores, com todos os seus carries num signal,
encadeando, e extrair o cin e o cout globais desse vetor de 64. Olha que legal:

Esse jeito é mais conciso, de certa forma. Sim, menos código, mas a
expressividade depende muito mais de comentários… Bem, faça como achar melhor.
Agora, você deve estar se perguntando sobre esses nomes com dois-pontos antes das
coisas, tipo “somador:” e “gen_somadores:”. Esses nomes são as labels, você geralmente
vai vê-las antes de for-generates, instâncias de componentes, processes, etc.

Labels são mandatórias antes de certas coisas, como for-generates. Você pode por
uma antes de um monte de coisa, e eles ajudam a organizar seu código; como ao usar
com if num process, você fecha o “nome: if … then” com “end if nome”. Acostume-se, elas
tornam as mensagens de erro mais fáceis de interpretar!

Página 6 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Por este capítulo é só. Absorva com carinho.

Página 7 - Revisão 1 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 3: Generics

No capítulo anterior, falamos sobre uma das coisas mais poderosas do VHDL, que
é instanciar componentes pra usar dentro de outro. Essa capacidade já é muito poderosa
por si só, mas ela fica ainda mais top quando introduzimos o conceito de generic.

Generics são um jeito de “passar informação” para um componente que você


instancia. Eles não são como as entradas, pois elas influenciam a saída e o estado de um
componente instanciado apenas. Um generic é um parâmetro fixo e acessível ao
componente a partir do instante em que ele é criado, e pode afetar não apenas seu
comportamento, mas sua estrutura. É tiiiipo um parâmetro de construtor, se você se
sente confortável com a analogia com programação orientada a objetos.

Eles podem mudar apenas o comportamento de um elemento, como um


parâmetro inteiro para um contador que fala o valor após um reset (deixando 0 quando
não especificado). Mas os generics realmente brilham quando você percebe que pode
usá-los para alterar a estrutura interna de um componente. Um exemplo batido é o
somador completo de N bits.

Volte seu pensamento pro somador completo de 64 bits que fizemos no capítulo
anterior… o princípio é o mesmo, só que ao invés de trabalharmos com valores fixos nos
nosso ranges, vamos trabalhar com um valor “n” e definir os ranges de tudo em termos
de “n”. A sintaxe na entity e instanciação é parecida com a das entradas e saídas (port):

Página 1 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Com a architecture, a sintaxe vai ser largamente a mesma, substituindo os
“números mágicos” por funções de n:

Viu? Generics não são tão algo tão absurdo na hora de definir, e são algo muito
podersoso, especialmente unidos ao for-generate. Mas e na hora de instanciar? Bem,
lembra que instanciamos pegando um port map com uma lista associativa
nome-expressão pra tratar as entradas e saídas? Analogamente, antes do port map,
escrevemos um generic map, que tem o exato mesmo formato que um port map: pares
“nome: valor” pra cada parâmetro genérico. Não esqueça de botar o generic na “cópia”
da entity na declaração do component!

Por este capítulo é só. Absorva com carinho.

Página 2 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 4: Circuitos sequenciais simples

Antes de continuarmos a aventura, eu tenho que deixar algo bem claro. Este
provavelmente vai ser o capítulo mais importante de todos. Sério. Com tudo que você
sabe até agora, você consegue implementar qualquer lógica combinatória… ou seja,
peças. Circuitos combinatórios são peças. Sistemas são e exigem partes combinatórias,
com memória, com estado interno. Moscar nessa seção é proibido, beleza? Bora.

Reiterando, até agora você sabe fazer lógicas combinatórias. Ou seja, circuitos
cujas saídas são função exclusiva das entradas... eles não contêm estado interno. Pra
brincar com circuitos combinatórios, vamos introduzir um conceito meio poderoso, mas
que você tem que tomar muito cuidado: o process. Um process é uma ação concorrente,
ele reside dentro do begin de uma architecture, mas dentro dele, não rolam ações
concorrentes, mas sim ações sequenciais.

Dentro de um process, as regras são outras, e elas lembram mais as linguagens de


computação às quais você tá acostumado(a), com controle de fluxo (if-elsif-else), loops
(while, for), etc. A ordem das coisas que você escreve ali dentro importa. O que torna isso
tão especial? Para implementar lógicas sequenciais, você precisa que seu circuito possa
reagir a um estímulo, como um clock, e um ótimo jeito de conseguir isso é usar um
process. O negócio é tão poderoso que dá pra implementar quase tudo só com process,
tanto que em EPs de lógica combinatória, às vezes tem uma regra tipo “proibido usar
process; sujeito a porrada”.

Agora, uns termos mais práticos. Um process é uma série de ações sequenciais
que rolam quando ele é ativado. A ativação de um process pode rolar de dois jeitos: uma
sensitivity list ou chamadas a wait. A primeira é mais comum, especialmente nos
exemplos e soluções que vemos na Poli, e significa especificar uma lista de sinais ao
declarar o process. Quando o valor de um ou mais desses sinais mudar, o process é
acionado. Esse é um jeito bem simples, permitindo fazer um processo sensível a um
clock, que vai ser o caso na maioria das vezes.

Um jeito alternativo e, na minha opinião, mais expressivo, é controlar a ativação


usando o idioma wait da linguagem. O wait é um controle muito versátil, se
manifestando em várias formas. Geralmente, você vai usar ele em duas situações:
ativação de processes e testbenches. Por enquanto, só vamos falar do wait para controlar
a ativação do process, que é o conceito central que vamos introduzir neste capítulo.

Note-se que mesmo eu gostando mais do wait, os exemplos de código aqui não o
usarão. O motivo? Como eu disse, nos exemplos da Poli usam mais sensitivity list por
algum motivo, então vou escrever focando nessa familiaridade.

Página 1 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Sem wait Com wait
process (a, b) process
begin begin
-- ações sequenciais -- ações sequenciais
end process; wait on a, b;
end process;

process (clk) process


begin begin
if clk’event and clk=’1’ then wait until clk’event and clk=’1’;
-- ações sequenciais -- ações sequenciais
end if; end process;
end process;

Na tabela acima, os códigos lado a lado são equivalentes. O primeiro exemplo é a


sensibilidade mais simples: acionar o processo quando um ou mais sinais mudarem de
valor. Já o segundo exemplo é uma coisa que você vai escrever um bilhão de vezes: um
processo que é acionado na borda (‘event) de subida (=’1’) de um clock. Mentira, às
vezes vai ter que botar um reset na sensibilidade também, caso eles peçam um reset
assíncrono. Sim, o termo ‘event (mudança de valor) é redundante, mas alguns
programas podem reclamar, então acostume-se a incluí-lo.

Vamos colocar tudo que aprendemos até agora em prática, e vamos fazer um
contador simples, oito bits, de 0 a 255, que com reset assíncrono para um valor
fornecido via generic (padrão = 0), e que volta para o valor do “reset” quando atinge um
máximo (também fornecido via generic, com padrão = 255). Como sempre, a entity:

Página 2 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Tem algumas coisas novas. Tá vendo esses “:=”? Numa declaração de generic, ele
especifica um valor padrão, permitindo que esse valor nem apareça na hora de
instanciar. Numa declaração de signal, elas dão o valor “inicial”, antes de qualquer
atribuição. Beleza? Não? O library/use ali em cima? Já explico. Eis a architecture:

Tem bastante coisa nova aqui, então vamos andar com calma. Muita sintaxe nova
aí, eu sei, mas não tem jeito bonito de introduzir essas coisas, desculpa. O máximo que
eu posso fazer é andar de parte em parte e tratar as coisas novas. Bora? Bora.

A primeira coisa surpreendente deve ser a declaração do único sinal interno, “n”.
Ele é um inteiro. Quando você declara inteiros, pode especificar um range, e isso é
geralmente considerado uma boa prática quando esse inteiro for limitado por alguma
outra coisa de tamanho fixo (como, por exemplo, o contador, que é sempre de 8 bits). Eu
também especifiquei um valor inicial pra ele, assim, não temos que resetar ao instanciar
(cuidado que na vida real não tem “valor inicial”). Esse valor inteiro vai servir como a
contagem atual. Na prática, o GHDL vai criar um registrador pra ele, mas é mais fácil
trabalhar com ele como número (e.g. pra somar e atribuir).

Página 3 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Em seguida, mais sintaxe nova. Introduzimos um process sensível a mudanças em
dois sinais de entrada: clock e reset. Ou seja, quando esses sinais mudam de valor, o
process é executado. A primeira coisa que fazemos é verificar se estamos numa borda de
subida do reset, no caso, testamos se ele é ‘1’. O teste de ‘event (borda), como dissemos,
é redundante, mas todo mundo coloca, então você vai colocar também. Enfim, se
estivermos numa borda de subida do reset, voltamos o valor do contador para o valor
declarado como “inicial”.

Caso não estejamos numa borda de subida do reset, podemos muito bem estar
numa do clock, então fazemos o mesmo teste. Se for uma borda de subida do clock,
incrementamos (ou voltamos) a contagem de acordo se e somente se a entrada conta
também for ‘1’. Como você pode ver, as coisas que rolam dentro de um process
geralmente lembram mais o tipo de programação que a gente vê normalmente, então
acho que apesar de ser algo mais complexo, é um complexo ao qual estamos
acostumados. A lógica concorrente mais simples geralmente causa mais confusão no
começo…

E pra terminar esse show de horror, um último segredinho. Bem, você viu que
estamos tratando muito bem o nosso sinal inteiro, mas como jogamos ele em binário na
saída “valor”, que é claramente um bit_vector? É aí que entram as bibliotecas numéricas.
Você viu que na entity eu coloquei duas linhas a mais, né? A primeira declara que
estamos usando a biblioteca do IEEE, e a segunda importa um pacote muito específico
de funções: a NUMERIC_BIT. Esse pacote define uma pá de funções pra fazer
manipulações numéricas com bits e bit_vectors. Caso estivéssemos usando std_logic ao
invés de bit (uma possibilidade concreta pós-SD1), importaríamos a NUMERIC_STD, mas
as funções são largamente as mesmas.

Dentre as funções que você mais vai usar figuram as de manipulação de


unsigneds. Esse tipo unsigned é meio intermediário, ele reside num lugar meio estranho
entre binários e inteiros, e você vai tratá-lo mais como um intermediário, uma forma de
interpretar binários em números e vice-versa. No caso, temos nosso número “n” sem
sinal, e queremos convertê-lo numa representação binária sem sinal. Primeiro,
convertemos ele para um unsigned de 8 bits, usando a função to_unsigned(n, L) (o
membro ‘length de tipos array-like e vector-like retorna seu tamanho). Por fim,
queremos converter esse unsigned meio doidão em bit_vector, e isso pode ser feito
simplesmente instanciando um bit_vector na hora ali a partir do unsigned! Como esses
tipos de dados são “próximos”, a conversão rola numa boa. Mesma coisa se estivéssemos
usando std_logic.

Ufa! Tudo isso pra falar que aquela atribuição concorrente ali no final da
architecture simplesmente diz: “quero que, num dado instante, a saída valor seja sempre
o valor de n em binário sem sinal”. Pode respirar, acabou, seu contador funciona. Chega.

Página 4 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Agora, a fim de sanar possíveis frustrações da sua parte, vou sair numa tangente,
me acompanhe se quiser, o capítulo acabou. Se você leu todos os capítulos com
carinho, você já sabe fazer circuitos combinatórios e uns sequenciais simples. Contudo,
provavelmente nos exemplos da internet ainda vai ter um monte de sintaxe maluca que
você nunca viu e, caramba, eu nem te ensinei a fazer ou rodar testbenches!

O fato é, eu escolhi essa ordem porque isso aqui não é uma speedrun, e meu
objetivo não é fazer você sentir a satisfação de conseguir fazer tudo porque seguiu uma
lista de instruções no primeiro capítulo e deu certo, e apareceram ondas bonitinhas no
GTKWave. Quando se faz isso, o conhecimento é como uma casa de cartas, e apesar de
você conseguir escrever muita coisa que possam vir a pedir, você dificilmente vai se
sentir confortável escrevendo VHDL.

O conforto vem de um alicerce forte, uma base devidamente construída, e isso


tem relação direta com a ordem dos tópicos. Eu não quero te dar a ilusão de que você é
um deus do VHDL após ler um ou dois capítulos… a longo prazo, isso só traz frustração
e insegurança. Eu quero que você faça os EPs confortavelmente, sem pressão, porque
você não apenas leu exemplos até aprender a sintaxe por força-bruta igual tantos
infelizmente fazem, mas sim porque você se preocupou em de fato construir, e não
empilhar, o conhecimento.

Por causa dessa intenção minha, a ordem das coisas aqui prioriza um
entendimento da essência do que tem por trás. Qualquer zé consegue ler uns exemplos
na internet em uma ou duas horas, interpolar a sintaxe, e escrever uns códigos
rocambolescos por tentativa e erro. No presente momento, tive que ver a maioria dos
meus colegas fazer isso: mentes altamente inteligentes e disciplinadas sofrem com isso,
e a culpa é do imediatismo, do jeito jogado que esse tópico é tratado na maioria dos
anos, salvo célebres exceções.

Eu estou deixando de dormir e escrevendo este docs à 01:26 da manhã porque


esse vai ser o terceiro ano em que eu tenho que ver ótimas mentes sofrendo com isso, e
quero fazer algo a respeito. Nem sei se vai dar certo, mas eu tenho que tentar. Aguenta
mais um pouco, e eu creio que o conforto que vai se seguir em SD2 e no Lab. Digital vai
compensar todo esse tempo lendo.

Não deu certo? Ainda tá frustrado(a)?

O próximo capítulo é sobre testbenches e GHDL.

Obrigado por confiar em mim até aqui, e prepare os ânimos.

Página 5 - Revisão 1 - 16/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 5: Testbenches (ft. GHDL)

Agora que você sabe o básico de circuitos combinatórios e sequenciais, podemos


começar a falar de testbenches e, finalmente, começar a simular coisas, ver ondinhas, a
brincadeira toda. Se você ainda não tem o GHDL e o GTKWave instalado, eu tenho um
pequeno guia. Volte quando tiver o GHDL e o GTKWave instalados. Bora? Bora.

Você sabe o que é uma testbench? Basicamente, ela é um componente de


mentirinha, sem entradas nem saídas, que manda sinais de entrada pra um componente
a ser testado, testa as saídas, e usa coisas da linguagem para te avisar caso tenha dado
ruim. Eu digo que ela é um componente de mentirinha porque ela não costuma ser
sintetizável, no sentido que você vai usar coisas da linguagem que não têm
correspondentes no mundo real (como wait for, assert e report).

Ou seja, ela não passa de uma entity vazia cuja arquitetura consiste em instanciar
um componente a ser testado, e usar um process pra ficar enviando sinais pra ele. Não
tem muito conceito novo em termos de código… o verdadeiro desafio é fazer e rodar
uma testbench decente que consiga pegar problemas no seu código.

Pra começar a brincadeira, vamos pegar o primeiro circuito que fizemos: a porta
XOR. Como é um circuito razoavelmente simples, a testbench vai ser similarmente
simples. Vamos simplesmente enviar umas bitstrings pra ele e ver se o resultado bate. A
entity é a parte mais fácil, pois não há entradas, saídas, ou generics, basta importar o
componente a ser testado e as bibliotecas que formos usar (e.g. numeric_bit).

Agora, vamos elaborar um plano de ação para a testbench. Vamos começar com
algo simples: na parte concorrente, tudo que precisamos fazer é instanciar o
componente e seu port map. Para conseguirmos acessar os valores, precisaremos, antes
do begin, declarar uns signals pra conseguir atribuir e testar seu valores. Aí, num
process, enviaremos as entradas “0011” e “0101” e vamos testar e a saída é “0110”.

As duas únicas coisas novas que você vai ver são wait for, report e assert. O wait
for é exatamente o que ele soa, ele pausa um process por um dado tempo. O report
imprime uma string para a saída padrão. O assert executa um report se uma dada
condição falhar.

Página 1 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Eis uma architecture simples:

Não tem nada que você não tenha visto. Talvez as exceções sejam a expressão de
bit strings com aspas e o wait no final, que meramente pausa um processo
indefinidamente. Por que esperei 5 femtossegundos? Eu tenho que esperar algum tempo
maior que zero… peguei um valor qualquer (por padrão, a resolução temporal máxima
do GHDL é o femtossegundo). Ah, é. GHDL. Nem falei dele ainda. Vamos aprender a
rodar essas testbenches e ver os valores!

Página 2 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Então, se você leu todo meu documento de “como instalar o GHDL minimizando o
total de sofrimento”, é meio caminho andado. Lá, eu expliquei que o GHDL tem três
etapas: análise, elaboração, e execução. Na análise, os arquivos são compilados
isoladamente, produzindo binários com pendências. Na elaboração, os .o cheios de
pendências viram um executável, que roda a simulação instanciando a entity
especificada. Por fim, na execução, esse executável é invocado, a simulação é executada,
testando todos nossos asserts, imprimindo os reports e, opcionalmente, produzindo um
arquivo VCD como saída.

Esse arquivo VCD (variable change dump) contém os valores de todos os sinais
no sistema a cada instante. Se você tentar abrir esse arquivo como texto, vai sofrer um
pouco. Para simulações pequenas como a que acabamos de fazer, ele vai ser pequeno,
mas ele pode chegar a vários megabytes de tamanho. Um programa legal pra ver os
conteúdos de um VCD é o GTKWave, que você instalou junto com o GHDL, né?

Bom, vamos de fato rodar nossa simulação. Primeiro, navegue o terminal para a
pasta com seus arquivos .vhd. No meu caso, a pasta contém inicialmente apenas o
xor4.vhd e o xor4_tb.vhd. A primeira coisa que vamos fazer é checar se os arquivos,
isoladamente, são corretos. Para isso, vamos analisá-los na ordem de dependência. Isso
quer dizer que vamos “de baixo pra cima” na árvore de dependência entre os
componentes. Como xor4_tb.vhd depende de xor4.vhd, começamos pelo xor4.vhd; se
tentarmos analisar o xor4_tb.vhd primeiro, o GHDL vai chorar. Invocamos então o
seguinte comando: “ghdl -a xor4.vhd xor4_tb.vhd”. O parâmetro -a significa
“análise”, e ele vem seguido de arquivos a serem analisados nessa ordem.

Ao executarmos esse comando, aparecem três novos arquivos na pasta: xor4.o,


xor4_vhd.o, e work-obj93.cf. Os arquivos .o, como falamos, são cada um dos .vhd
compilados e com pendências (e.g. o xor4_tb.vhd contém uma pendência, que é uma
architecture para xor4). O arquivo work-obj93.cf é um arquivo de texto auxiliar do
GHDL, lá ele guarda a informação de tudo que é definido em cada arquivo, como entities
e architectures, e suas datas de modificação. É graças a ele que o GHDL sabe onde
procurar quando você fala “use work.xor4”.

Bem, agora que sabemos que nossos arquivos compilam numa boa, podemos
elaborar a testbench. O comando é mais simples dessa vez: “ghdl -e xor4_tb”. Ele vai
olhar na pasta atual, ver que essa entity existe (graças ao work-obj93.cf), e criar um
executável nativo que simula essa entity (instanciando-a). Isso pode produzir alguns
outros arquivos, mas na prática, o que importa é um binário, chamado apenas de
xor4_tb (no Linux, executáveis não têm extensão).

Já estamos quase lá, aguenta firme.

Página 3 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


A parte final é executar o comando a seguir: “ghdl -r xor4_tb
--vcd=simul.vcd” (se ficar em loop infinito, mate-o com Ctrl-C). Isso recebe o nome
do binário e umas opções. No caso, só passamos uma opção: pedimos para a simulação
gerar um VCD. Esse comando produzirá uma saída mais ou menos assim:

O report do assert não executou, indicando que a condição passou. Ufa! E agora,
podemos abrir o VCD no GTKWave e ver nossa testbench em ação:

O GTKWave é cheio de opções e utilidades. No início, ele não mostra nada. Ele
começa com uma hierarquia de entities e seus sub-componentes ali em cima. Podemos
abri-las, e eventualmente aparece uma lista de sinais. Eu selecionei os três, e cliquei em
insert. A lista com fundo branco (sinais carregados), antes vazia, agora possui três
sinais, mas eles estavam aparecendo como números na waveform. Pra consertar isso,
selecionei os três na lista de sinais carregados, botão direito, “Data format”, e “Binary”.
Dica: Alt+Shift+F redimensiona pra simulação toda caber na tela.

Bastante informação, mas eu juro que você vai se acostumar rapidinho. Agora,
vamos supor que seu projeto tenha dado algum erro, seja na análise, na elaboração, sei
lá. Vai ter que apagar todos os .o, editar e analisar tudo de novo? Nah, relaxa! Se você
tentar elaborar de novo, O GHDL vai olhar o work-obj93.cf, comparar as datas de
modificação, descobrir quais os arquivos modificados, e vai te mandar reanalisar só os
que mudaram. Enfim, vamos ver uma testbench bem básica pro somador a seguir!

Página 4 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Página 5 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado
Pra rodar ela, eu não sou muito fã de ter que ficar digitando comandos, só quero
apertar a setinha pra cima (volta nos comandos anteriores) e apertar enter. Sabia que no
bash (shell padrão do Linux), você pode encadear comandos? Dependendo do que você
coloca entre eles, o comportamento muda: “;” entre comandos significa “execute o
próximo”, “||” significa “execute o próximo só se o anterior falhar, e “&&” significa
“execute o próximo só se o anterior não falhar. Então, pra rodar essa testbench, eu
simplesmente rodo o comando “ghdl -a soma1.vhd soman.vhd soman_tb.vhd &&
ghdl -e soman_tb && ghdl -r soman_tb --vcd=simul.vcd”. Analisar sempre,
elaborar só se a análise for bem-sucedida, e executar só se a elaboração for bem
sucedida. Pra rodar de novo, só setinha pra cima e enter.

Enfim, vamos dar uma olhada no código, tem algumas coisas que você talvez não
tenha visto ainda… Sabe, maioria das coisas aí eu já mostrei. As únicas coisas meio
diferenciadas são uns valores no port map e as conversões numéricas. Tá vendo que no
port map, eu deixei o carry-in global do contador em zero sempre? Nem precisei criar
um sinal pra ele, dado que só vou enviar o mesmo valor sempre… E depois, eu descarto
o carry-out global dele também, ligando-o a open, que é exatamente o que parece, é
como deixar um pino de saída em aberto porque eu não dou a mínima pro valor dele.

A outra coisa talvez-nova são as conversões numéricas. Se você olhar com


atenção, vai ver que em nenhum momento eu dou a mínima pro valor em binário dos
sinais de entrada e saída. Dentro do process, eu só mexo com valores numéricos. Como?
O segredo está fora do process. Mais especificamente, linhas 34 a 36. As duas primeiras
falam pros sinais binários que o somador recebe serem sempre a representação binária
dos inteiros que eu declarei; essa conversão você já viu no contador. A terceira faz o
contrário: ela manda o outro valor inteiro, o da soma, receber sempre o valor da saída
binária do somador quando interpretada como um inteiro sem sinal; essa conversão é
nova, guarde pro coração. Se seu componente opera com números mas tem binário na
sua testbench, você não guardou pro coração…

Como tarefa, tente rodar essa testbench e abrir a forma de onda, no GTKWave,
mudar uns valores, ver o que acontece. Cuidado na hora de selecionar os sinais pra
adicionar na tela. Alguns minutinhos de prática e você já pega o essencial, como mudar a
representação (binária, decimal, hexadecimal, com ou sem sinal, etc).

Pra finalizar, vamos ver juntos uma testbench pro contador que fizemos no
capítulo anterior! Essa vai ser um pouco mais cascuda, porque tem um clock. Tem vários
jeitos de colocar um clock na sua testbench. Eu pessoalmente prefiro declarar um
componente que faz isso, assim podemos rotear o sinal com mais facilidade. Então, no
começo da testbench, declaramos um componente que gera clock.

Página 6 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Esse gerador de clock tem uma entrada adicional, que faz ele “parar”, do
contrário, a simulação andaria ad eternum. Também tem uma sintaxe em cujos detalhes
eu não vou entrar muito (assign after). Quer dizer que você não consegue fazer um?
Muito pelo contrário! Só usar process e wait, conceitos que já discutimos:

Página 7 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Aproveitei essa oportunidade pra introduzir o while-loop. Hehe. Esse usa só
coisas que eu expliquei ou que você deve ter uma noção intuitiva, e recomendo usá-lo
caso resolvam impor restrições malucas sobre o que você pode ou não usar e te peçam
uma testbench. Oh well. Agora só fazer a testbench. Vou quebrar em duas partes pra
conversa ficar mais dinâmica. A entity você já sabe, e importaremos numeric_bit.all, e
work.conta8. Vamos começar pelas declarações da architecture:

Nada de muito novo. No máximo, declarei umas constantes, e usei o tipo time pra
não termos que ficar muito preocupados com unidades de tempo. Ele também faz parte
do protótipo do “clocker”. Fora constant e time, tudo isso você já viu!

Página 8 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Finalmente, o comportamento da architecture, o prato principal:

Olha como é legal aprender as coisas na ordem e não ir à base do “nunca vi isso,
vou usar sem perguntar”, o famoso aprendizado O(n²). Eu garanto que não tem nada
nesse miolo de testbench que eu não tenha te contado antes: atribuições concorrentes,
um process mixuruca, umas conversões numéricas e um assert só pra falar que tem. E
pra rodar? Exatamente igual antes. E o VCD? Só abrir no GTKWave.

Página 9 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Se você mexer nuns valores, vai ver o contador voltando como programado:

Página 10 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Calma, tá acabando. O pior já passou faz tempo! O resto só vai construir em cima
do que você já sabe. É sério. Se você instalou tudo, acompanhou os exemplos, rodou
umas simulações, deu uma brincada com os códigos que eu passei, os próximos
capítulos vão ser os mais fáceis de todos! =D

E mesmo que você só esteja lendo no desespero porque tem um testinho de SD2
amanhã, ainda acho que vale a pena ler até o final. Muitos exemplos de código
fornecidos nas matérias usam essas coisas. Por que eu deixei isso pro final? Ué. Só
porque é fácil pendurar um quadro na parede, quer dizer que você pendura ele antes de
terminar a parede?! Pois é… esses assuntos são fáceis… se e somente se você estiver
confortável com tudo que já conversamos até agora.

De qualquer maneira, se lhe parecer melhor ir dormir e deixar pro semestre que
vem, deixe um feedback no forms! Ele está nesta pasta também, marcado em roxo. Eu
uso ele pra corrigir erros e sanar sugestões. Não tiro dúvidas por lá, nem tenta.

Faça uma pausa se quiser. Quando estiver confortável… próximo capítulo!

Página 11 - Revisão 1 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 6: Mais condicionais

Com tudo que discutimos nos capítulos 0 a 5, dá pra resolver a maioria dos
desafios que jogarem na sua direção. Minha intenção com eles foi construir uma base
forte e chegar o mais rápido possível em simulações pra evitar frustração, aquela
sensação chata de “li pra porra e até agora não consigo fazer nada”.

Agora que você tem uma base forte, podemos expandir um pouco em alguns
detalhes da linguagem que não apenas são úteis, mas podem ser necessários pra resolver
alguns desafios. Eu não vou segurar tanto na sua mão e explicar o código tintim por
tintim porque você é inteligente (afinal, chegamos até aqui!) e já tem uma base forte.

Vamos largamente explorar algumas gostosuras sintáticas do VHDL!


Começaremos estendendo nosso conhecimento sobre a atribuição concorrente (i.e. “<=”
fora de um process). Sabia que ele tem duas formulações, e as duas permitem usarmos
condicionais? Pois é. Imagina que você tem que fazer um circuito “seletor” simples, que
recebe um bit “b”, e retorna “01” se receber ‘0’, e “10” se receber ‘1’. Pense um pouco em
como você faria, e só leia o parágrafo abaixo quando conseguir ou se cansar.

Com o que você sabe até agora, você provavelmente ia fazer alguma coisa do tipo,
criar um sinal de dois bits, e atribuir a ele “[b][not b]”. Não tá errado! De fato, isso
funciona 100%! Se você pensou nisso, parabéns pela engenhosidade! Você sabe que é
um circuito combinatório, e as saídas são meramente funções booleanas das entradas!

Bem, agora pensemos em outro circuito combinatório bem simples, um


decodificador para display de sete segmentos. Se não quiser pesquisar no Google, é um
treco que você vai ter que usar 3 bilhões de vezes no Lab. Dig.: ele recebe um número de
0 a 9 em binário (um BCD -- binary coded decimal -- , quatro bits), e cospe sete saídas,
uma pra cada segmento, indicando se ele acende ou não com aquele número.

Se você for escrever as expressões booleanas pra isso, você vai ter que usar, no
mínimo, 4 NOTs, 7 ORs, e 9 ANDs, e 12 sinais internos se quiser minimizar as portas
lógicas. Ou isso, ou muita, muita redstone.

Essa patifaria de reduzir expressões lógicas como uns “if” a expressões


booleanas é coisa de co...mputador. O VHDL faz isso pra você. “MAS BORGES, NÃO PODE
IF NUM BLOCO CONCORRENTE, VOCÊ MENTIU PRA MIM”, dirão os descrentes, e eles
estão certos. É por isso que a atribuição concorrente suporta condicionais. Veja uma
cacetada de jeitos diferentes de fazer o “seletor” do qual eu tava falando antes na página
a seguir. Coulrofóbicos: trigger warning.

Página 1 - Revisão 2 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Estamos diante de quatro jeitos de resolver a mesma coisa. O primeiro é o mais
básico. O segundo é parecido, só que usamos o operador de concatenação ‘&’ pra
economizar uma linha, mas não mudamos de atitude. As outras duas que são
interessantes. Elas usam os dois jeitos de fazer atribuições concorrentes condicionais:
assign when, e with-select. O assign when é mais geral, porque você pode colocar
qualquer condição dentro dele. Já o with-select faz jus ao nome: você usa o valor de uma
expressão para atribuir a outras. Você vai usar with-select toda hora pra fazer máquina
de estados.

Página 2 - Revisão 2 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Como exercício pra internalizar o with-select, recomendo fortemente você tentar
implementar um decodificador de display de sete segmentos. Pesquise a pinagem do
display na internet, leia-se “a saída é abcdefg, mas qual letra é cada segmento?”. Se você
olhar pro seu código e não tiver A MENOR IDEIA do diagrama lógico por trás, parabéns,
você fez certíssimo. Escreve suas atribuições condicionais numa boa, e deixa o
simulador/sintetizador decidir as operações lógicas. Ponto bônus se incluir os casos
“hexadecimais”, leia-se, as strings binárias equivalentes a 10 (A) até 15 (F).

Tem uma última coisa que eu quero te mostrar antes de fecharmos este capítulo
sobre outras formas de se expressar condicionais. Dentro de um process, as coisas
parecem mais com programação normal, por exemplo, temos o if. Mas em programação
normal, alguns ifs ficam mais elegantes na forma de um switch-case! Temos um
equivalente em VHDL? Sim, e chama case-when.

A estrutura dele lembra muito a de um switch-case: um valor a ser testado,


opções, uma opção “padrão”, e, ligadas a cada opção, uma série de coisas pra executar
quando batermos naquela opção:

case expr is
when valor_1 => coisa1; coisa2; coisa3;
when valor_2 => coisa1; coisa4;
when valor_3 => coisa5;
when others => coisa6; coisa7;
end case;

Acho que aprofundamos o suficiente nesse assunto para o escopo desta


apostila… pelo menos por enquanto. Eu tento seguir uma ordem de escopo primeiro,
profundidade depois, breadth-first, senão ia ficar impossível de ler.

Por este capítulo é só!

Página 3 - Revisão 2 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Capítulo 7: Type, subtype, e array

Agora vamos mergulhar num assunto mais abstrato do VHDL, mas que não deixa
de ser super útil, especialmente na hora de fazer circuitos mais complexos, como
máquinas de estado, por exemplo. O assunto da vez são tipos e subtipos definidos pelo
usuário.

O VHDL é uma linguagem fortemente tipada. Isso significa que uma variável
nasce com um tipo e morre com o mesmo tipo, ou seja, ele restringe os valores que ela
pode assumir durante toda sua existência. Por um lado, isso tira do nosso alcance a
aparente mágica de conversões implícitas entre tipos e o dinamismo ao que muitos
estão acostumados. Afinal, quem nunca se impressionou com os tipos do JavaScript, um
lugar mágico onde ("3"+2 == "32") mas ("3"-2 == 1)?

Mas por outro lado, isso torna a linguagem mais previsível. Você não precisa se
preocupar com uma linha que dá erro porque não sabe se quando vc soma A com B, por
acaso um deles veio como texto e o outro é uma array. Você vê um nome, olha a
definição, e sabe o tipo daquele nome durante 100% da execução. Hardware é assim…
fios não se multiplicam e portas lógicas não mudam seu comportamento.

Outra vantagem da tipagem forte é que muitos erros são pegos em tempo de
compilação. Se você sem querer atribuir um bit_vector de tamanho 4 a um bit_vector de
tamanho 2, não precisa rodar e esperar a explosão, durante a própria análise o GHDL vai
te perguntar se você usou drogas e quais. É aí que entra o assunto de hoje: podemos
definir nossos próprios tipos e subtipos para tirar vantagem de checagens em tempo de
compilação e tornar nosso código mais expressivo.

Existem três jeitos de definir seus próprios tipos no VHDL, e vamos falar
primeiro do que você vai ver o tempo todo: enumerações. Você pode definir um tipo de
enumeração, por exemplo, antes do begin de uma architecture, com a seguinte sintaxe:

type gente is (fulano, beltrano, sicrano);


type trit is (‘T’, ‘0’, ‘1’);

Dentro dos parênteses, você enumera os valores possíveis que variáveis desse
tipo pode assumir, eles podem ser tanto nomes livres quanto literais de caractere
(leia-se: caractere entre aspas simples). Sim, o bit é só um tipo enumerado que suporta
operações especiais por também ser um tipo lógico, assim como o std_logic e vetores
com esses caras.

Página 1 - Revisão 1 - 18/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Seus tipos customizados são, de fato, Gente Como a Gente™, a atribuição
funciona como de costume:

signal eu: gente := beltrano;


signal tr: trit := ‘0’;

“Mas Borges, isso não deveria dar erro? Você tá atribuindo ‘0’ ao tipo trit, mas ‘0’ é
um bit! E ‘beltrano’ nem é um símbolo globalmente visível!”, gritariam os descrentes. O
VHDL é esperto… quando você atribui a um tipo enumerado, você não precisa falar
quem é o quê, ele infere pelo contexto. Se estou atribuindo a um trit, ele vai olhar na
enumeração do trit. Um literal de caractere não tem restrições de atribuição apenas por
existir, toda atribuição é inseparável do contexto.

Esses tipos enumerados são literalmente enumerados, sendo o primeiro da lista o


tipo #0, o segundo #1 e, como já disse um tal de Peano, “por aí vai”. Sendo assim, é
recomendável escrever seus tipos “na ordem” caso essa ordem exista. Geralmente,
assume-se que o valor “padrão”/“base” é o primeiro, por isso ele é acessível através do
atributo mágico 'left:

signal tu: gente := tu'left;

Esses tipos são muito úteis para fazer máquinas de estado e unidades de
controle. Por quê? Flexibilidade. Se você declarar o estado da sua máquina como um
bit_vector(2 downto 0) e ela tiver oito estados, tudo bem. Mas e se ela tiver seis?
Imagine os pesadelos envolvendo a variável de estado acidentalmente receber “111”.
Sem contar que ficar escrevendo em binário é chato, estado <= “101”; é chato. Sabe
o que é legal? Isso: estado <= E_FIM;.

“Mas Borges! Não existe um jeito de armazenar uma informação com exatamente
cinco valores possíveis em binário! Ela vai ser representada internamente com três
bits!”, clamam os de pouca fé. E isso é verdade! Se você jogar sua maquininha de cinco
estados no Quartus e mandar sintetizar, o estado muitíssimo provavelmente vai ser
armazenado num registrador de três bits.

Mas você tá sintetizando algo? Provavelmente não. No GHDL, a simulação por


trás é um binário altamente otimizado. Em tempo de simulação, tudo pode acontecer. Se
a sua máquina de estados, por algum motivo tenebroso de introspecção maliciosa vinda
diretamente das catacumbas do Laboratório Digital, tentar chegar num estado que
numericamente não existe no tipo definido, o simulador provavelmente vai te avisar
e/ou explodir. Ou seja, ainda temos a vantagem de detectar comportamentos indevidos.

Sobre tipos enumerados, é isso. Pratique!

Página 2 - Revisão 1 - 18/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Vamos pro próximo tipo “customizado”, arrays. Você vai ver arrays com bem
menos frequência do que enumerações e subtipos, mas vale a pena mencionar. Você
pode fazer arrays de qualquer coisa e tamanho, mas como não é um assunto super
importante, só vou falar rapidamente das arrays de tamanho fixo. Você pode definir um
tipo de array com a seguinte sintaxe:

type sua_array is array(range) of tipo;


type reg is array(63 downto 0) of bit;

A atribuição e acesso funciona do mesmo jeito que com bit_vectors, ou qualquer


arraylike, mas com algumas limitações. Tome o seguinte exemplo: eu tenho sinais
bit_vector, um chamado “dword” de 16 bits, e um “word” de 8 bits. Eu posso fazer
atribuições parciais (slices) como uma das duas seguir:

dword(7 downto 0) <= word;


word <= dword(15 downto 8);

Essas atribuições são ok porque os tipos envolvidos são compatíveis. Um pedaço


(slice) de bit_vector é um bit_vector. Agora vamos imaginar que as variáveis foram
definidas como tipos array, como a seguir:

type word_t is array(7 downto 0) of bit;


type dword_t is array(15 downto 0) of bit;
signal word: word_t;
signal dword: dword_t;
[...]
begin
word <= dword(15 downto 8);

O código acima vai explodir. Por quê? Porque uma slice de uma array não muda
seu tipo. Uma slice de uma dword_t continua sendo uma dword_t, então a atribuição a
uma variável de tipo word_t nem vai compilar, consagrado(a).

Essa incompatibilidade não as torna menos úteis! Uma array pode ser de
qualquer coisa, até mesmo de outras arrays, e eu nem falei (nem vou falar) de arrays de
tamanho indefinido. Contudo, em muitas situações você vai querer fazer atribuições
entre coisas… existe uma facilidade de tipagem que permita impor restrições sobre
coisas do mesmo tipo? Afinal, é disso que queremos nos beneficiar: impor restrições, via
tipagem, pra podermos escrever código com mais calma e mais expresivo.

Tem como? Tem. Subtipos ao resgate!

Página 3 - Revisão 1 - 18/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


O terceiro método é uma solução para o problema com arrays que discutimos
acima. Queremos definir um tipo restrito como, por exemplo, inteiros de 0 a 255 ou
vetores de quatro bits, mas sem perder a compatibilidade para fazermos atribuições
entre coisas compatíveis. Para isso, o VHDL oferece uma facilidade chamada subtype. Um
subtipo é um tipo definido em cima de um tipo-base e uma restrição, um range.

Isso resolve nossos problemas! Antes, não conseguíamos atribuir dword_t a


word_t porque eles são tipos distintos: um é “arrays de 16 bits” e outro é “arrays de 8
bits”. Seus equivalentes em subtipos seriam algo como “a família dos bit_vectors de 16
bits” e “a família dos bit_vectors de 8 bits”. Eles são compatíveis, e podemos atribuir
entre eles! Se definíssemos os tipos do exemplo anterior assim...

subtype word_t is bit_vector(7 downto 0);


subtype dword_t is bit_vector(15 downto 0);

...não teria rolado nenhum erro de compilação, e o código ia funcionaria numa


boa! Se você tiver memória realmente aguçada, deve lembrar que no capítulo 4, no
contador, definimos um integer com um range fixo. Similarmente, podemos definir um
tipo de inteiro assim, isso evita repetição e facilita na hora de mudar parâmetros:

subtype digito is integer range 0 to 9;

Também existem outras facilidades de tipagem no VHDL, como records,


composites, e umas coisas muito loucas como access. Não vou falar delas aqui, não por
enquanto, são tópicos bem mais avançados, e se eu for falar disso, fica pra um capítulo
bem mais pra frente.

As enumerações são onipresentes em máquinas de estado, e os subtipos ajudam


bastante na hora de deixar seu código mais claro, e evitar repetições. Essas facilidades
ajudam na detecção de erros em tempo de compilação e, com o tempo, evitam que esses
erros sejam meramente escritos, então use-as!

Chegamos ao final… mas isso não quer dizer que eu não vou adicionar mais
documentos ou fazer revisões! A apostila é um work-in-progress…? Este parágrafo é
temporário, e é movido sempre para a última página do último capítulo.

Seja como for, seu feedback é essencial!

~ Borges

Página 4 - Revisão 1 - 18/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Como instalar o GHDL no Windows 10 usando o WSL
(e também o básico de como usar o GHDL)
[pelo amor de deus, eu sei que tem Linux no meio, mas confia em mim, é o jeito mais fácil]
Um guia de aventura do Borges

Se você chegou aqui, é porque vai ter que escrever uns EPs em VHDL, e ainda não
chegou no Lab. Digital, logo ainda não entrou pro clubinho secreto dos usuários de
Quartus. Ou quem sabe você usou o Quartus, mas como o Pelicano usa GHDL, seu EP
explodiu, gastou um envio, e você resolveu se render aos métodos. As opções
canonicamente disponíveis para se rodar o GHDL são as seguintes:

1. GHDL no Linux nativo


2. GHDL numa máquina virtual Linux
3. GHDL no Windows com MinGW
4. GHDL no WSL

A primeira deve soar aterrorizante para usuários de Windows. A segunda é


overkill, pesada, e demorada. A terceira, eu te garanto, vai doer a cada instante. Resta a
quarta. Que diabos é WSL? É o resultado da Microsoft percebendo que o Windows é
meia-bomba demais pra certas coisas, então resolveram dar um jeito de meter um
ambiente Linux dentro do Windows 10. Sim, LINUX. Não precisa ter medo. Você
provavelmente já aprendeu coisas mais mundanas e inúteis na Poli e não reclamou.

Antes da gente começar a brincadeira, vamos ativar o WSL e instalar um Linux. O


WSL é um componente de baixo-nível do Windows, então o jeito de instalar “varia”
(Microsoftês para “quebra de tempos em tempos sem motivo aparente”). Basicamente,
tudo que você precisa fazer é rodar um ou dois comandos como administrador no
PowerShell. Tenebroso. Siga as instruções em https://aka.ms/wslinstall. Volte
aqui quando terminar. Não esqueça de reiniciar o sistema.

Se você fez tudo certo, o PowerShell deve ter dito algo assim. Reinicie.

Página 1 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


Pronto. Agora que o WSL está ativado, podemos meter um Linux aí dentro. Abre a
Microsoft Store, e procura lá o Ubuntu 18.04 LTS. Ah. Lembre-se que a revisão atual
deste documento é de 2020. Talvez nem exista mais o Ubuntu 18.04 lá. Em que ano
estamos? Procure o representante mais próximo do poder público/ditatorial, conselho
intergalático, agente da matrix, professor do PCS, ou seja lá o que atualmente mandar
em você, e pergunte. Achou o Ubuntu 18.04? Clica em instalar e espera um bocadinho.

Achou!

Instalou o Ubuntu? Brilhante. Ótimo. Agora, quando você digitar “ubuntu” no seu
Menu Iniciar, vai aparecer a opção de abrir o terminal do Ubuntu 18.04 LTS. Sim.
Terminal. TERMINAL. AAAAAAAHHHH BORGES VOCÊ FALOU QUE IA SER FÁCIL AAAA--

Calma, porra.

O terminal do Linux não é um bixo de sete cabeças. Se eu disse que esse é o


caminho mais fácil e você chegou até aqui, provavelmente você confia em mim. Vamos
manter desse jeito e seguir o passo-a-passo com calma.

Página 2 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


Se você for algum tipo de autodidata maluco, é uma boa hora pra fazer uma
pausa, quem sabe pesquisar uns tutoriais duvidosos no Google com o básico do shell. Do
contrário, relaxa. Você dificilmente vai usar quaisquer comandos além de ls, cd, e pwd.
Nessa ordem, eles listam os arquivos na pasta atual, mudam a pasta atual, falam a pasta
atual. Yep. Assim como uma janela do gerenciador de arquivos tem uma pasta atual, uma
sessão do terminal também. Esses três comandos permitem que você navegue até a
pasta que contém o seu projetinho em VHDL.

Afinal, tudo que estamos fazendo aqui é só pra ter o GHDL e executar simulações
bonitinhas, lembra? Mantenha o objetivo final em mente, e vamos continuar nossa
aventura. Bem, você acabou de clicar em Ubuntu e entrou em pânico, porque viu algo
tipo assim:

Escolha um nome de usuário simples, igual num site. Nome ou sobrenome sem
acento nem espaço é perfeito pra isso. Ele também vai te pedir uma senha, e depois
disso, você cai num terminal e pode fazer o que quiser.

Bem, não temos tempo a perder. Vamos começar instalando o GHDL. Num
terminal, você digita um comando, dá enter, e ele executa. Alguns comandos exigem
privilégios administrativos; antes deles, você digita “sudo” (Super User DO). Vamos
rodar alguns comandos de superusuário pra instalar o GHDL.

Página 3 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


Só pra deixar claro: isto não é Windows, beleza? Rodar como admin não
magicamente resolve nada, na verdade, muito pelo contrário. Só rode comandos com
“sudo” antes se tiver muita certeza do que está fazendo ou, melhor ainda, se um imbecil
num docs tiver mandado. Por conta própria, só se você já estiver bem preparado.

Bem, bora instalar o GHDL? Não. Vamos instalar os requisitos antes. Por quê?
Surpresa. Eu te conto assim que acabarmos essa parte. Execute o comando: “sudo
apt-get update”. Assim que ele acabar, execute “sudo apt-get install -y git
make gnat zlib1g-dev”. Isso pode demorar um pouco. Comece a preparar os ânimos,
a coisa vai ficar muito louca em breve. Aproveite o calor produzido pelo seu PC
descompactando o .zip paquidérmico do gnat e faça um chá. Prepare os nervos.

Seu terminal após uma execução OK dos comandos acima.

Agora vamos começar a parte realmente tenebrosa. Só manter a calma e seguir as


instruções. O que vamos fazer agora é baixar a última revisão do GHDL e, sim, compilar
e instalar com uns 2 comandos! Comece executando o seguinte comando: “git clone
https://github.com/ghdl/ghdl”. Isso vai baixar a última revisão estável do GHDL.
Vamos entrar na pasta na qual baixamos o código-fonte GHDL com o comando “cd
ghdl”. Agora, vamos rodar uns comandos para preparar, compilar, e instalar o GHDL no
seu Ubuntu de mentirinha:

1. ./configure --prefix=/usr/local
2. make
3. sudo make install

Esses três comandos falam um monte de coisa maluca, e o make demora um


monte. Mas relaxa, a não ser que apareça um FATAL ERROR no meio, acho que tá tudo
bem. Ao final do terceiro comando, meus parabéns! Você tem GHDL instalado, e pode
seguir todos os tutoriais de uso como se estivéssemos num sistema Linux. Afinal, todos
os tutoriais decentes do GHDL são no Linux… Teste sua instalação, execute “ghdl”, veja
ele reclamar!

Página 4 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


Agora, um breve adendo: vamos falar um pouco sobre a invocação do GHDL. Eu
pretendo falar mais sobre isso nos docs de VHDL de fato, mas enquanto isso não for uma
realidade, bem, vamos falar rapidamente sobre como o GHDL opera.

Basicamente, o GHDL tem três etapas de uso: análise, elaboração, e execução. Na


análise, seus .vhd, com seus componentes e, quem sabe, uma testbench, são verificados
e compilados em arquivos .o. Na elaboração, esses .o são unidos às bibliotecas-padrão
e compilados num executável. Na execução, ele é invocado, ou seja, é uma simulação,
produzindo um .vcd, que pode ser analisado num software como o GTKWave. (Baixe o
GTKWave pro Windows, o de Linux não vai abrir.)

Imagina que você escreveu um componente e uma testbench pra ele, e quer
executar a simulação dessa testbench. Assumindo que você já navegou o terminal pra
pasta onde seus arquivos estão (dica: os discos rígidos do PC residem na /mnt, como em
“cd /mnt/c”), os comandos podem ser como a seguir:

1. ghdl -a coisa.vhd
2. ghdl -a tb.vhd
3. ghdl -e tb
4. ghdl -r tb --vcd=simul.vcd
5. rm *.o *.cf *.lst

Os dois primeiros comandos analisam os componentes, no caso, o seu


componente e a testbench. É nessa fase que muitos erros são detectados, e mesmo sem
uma testbench é bem bom você analisar seu componente. O terceiro elabora os arquivos
num executável, e alguns erros de compatibilidade entre os componentes são revelados
aqui. O quarto executa a simulação, salvando um VCD. Esse arquivo contém os valores de
cada sinal a cada instante. É uma boa abrir ele no GTKWave. O quinto comando é
opcional, ele remove arquivos intermediários. Bom pra evitar mandar um .o pro Judge
quando acabar tudo.

Acho que o resto você consegue aprender na internet e nos tutoriais pela
internet, ou mesmo nos do eDisciplinas. Como disse antes, também pretendo fazer umas
videoaulas ensinando VHDL de fato, e invocações mais complexas do GHDL.

Por hoje é só. Não dê like. Não se inscreva. Se precisar de ajuda, pode me
procurar, mas eu não garanto que vou responder rápido, tô cheio de coisa pra fazer.

Espero que dê tudo certo. Boa sorte com seu EP aí.

Página 5 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


~ Bruno Borges Paschoalinoto

Página 6 - Revisão 2 - 17/06/2020 - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Adendo 1: Operadores

Neste adendo, vou deixar uma tabelinha de referência para os operadores e seus
significados. Não vai fazer parte dos capítulos principais, mas recomendo consultar
sempre que necessário. A lista a seguir relaciona operadores aos seus significados,
separando estes pelos contextos nos quais são relevantes.

● + - * /: em expressões, comportam-se numericamente como de costume nas


linguagens de programação que você deve conhecer.
● **: idem (exponenciação), o expoente deve ser um integer.
● abs: calcula o valor absoluto de um número (e.g.: “abs -3” reduz a “3”).
● mod e rem: ambos calculam restos de divisão (e.g. “4 rem 3” reduz a “1”), mas
o resultado de mod tem o sinal do divisor, e o do rem tem o sinal do dividendo.
● =: não é atribuição, ele retorna true se seus operandos são iguais (vide acima).
● /=: não é atribuição por divisão, ele retorna false se seus operandos são iguais.
● < e >: são comparação matemática, funcionam como de costume.
● <=: esse é complicado… depende do contexto:
○ numa expressão, ele faz a comparação: [...] if n <= t’length [...].
○ fora de uma expressão, ele é uma atribuição a sinal: s(0) <= not m(0);
● =>: não é um operador comum, depende do contexto:
○ uma delas é ser parte da sintaxe associativa para port map e generic
map… por exemplo, se eu tenho uma uma entidade comp1 que recebe “a”
e “b” e retorna “c”:
■ c1: comp1
port map(reg_a, reg_b, reg_c);
essa sintaxe não é boa porque ela depende da ordem na entity.
■ c1: comp1
port map(
a => reg_a,
b => reg_b,
c => reg_c
);
essa independe de ordem e é mais clara!
○ ele também está presente em blocos case-when, conectando o valor de
teste à lista de ações, como a seguir:

case vec is
when "0011" => s <= '1'; f <= f + 1;
when "1100" => s <= '0'; f <= f - 1;
when others => s <= '0'; f <= 0;
end case;

Página 1 - Revisão 2 - 17/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


● :=: esse também depende do contexto.
○ no contexto da declaração de um signal, ela exprime o valor inicial dele,
como em signal tmp: bit := '0';.
○ no contexto de declaração de uma entity, mais especificamente, em seu
bloco generic, ele exprime o valor padrão de um parâmetro genérico,
como em “entity clock is generic (periodo := 5 ps);
[...] end entity;”.
○ em operações com variables, ele tanto dá um valor inicial quando atribui a
uma variable.
○ ele define o valor numa declaração constant, similarmente a variables.
● and, or, nand, nor, xor, e ror: são operadores lógicos, operam sobre
booleanas, arrays lógicas, ou tipos lógicos, e fazem a operação correspondente.
○ bits: '1' and '0' → '0'.
○ vectors: "10" or "01" → "11".
○ booleanas: [...] if ck=1 and ck'event [...].
● &: não é um AND, ele concatena coisas, você pode usar para juntar vários bits
num vector, ou mesmo juntar vectors, ou mesmo juntar strings:
○ bits: '1' & '0' & '1' → "101".
○ vectors também: "10" & "01" & '1' → "10011".
○ até strings: report "bor" & "ges"; → imprime “borges” na tela.
● sll, srl, sla, sra, rol, e ror: são operadores de shift, operam sobre arrays
lógicas (como bit_vector ou std_logic_vector); uso: “vec sll 3”.
○ os dois primeiros são shift lógico (shift left/right logical), empurrando as
coisas na array e preenchendo com zeros.
○ os dois do meio são shift aritmético (shift right/left arithmetic), trata a
array lógica como complemento de dois, preservando o bit no topo.
○ os dois últimos são rotações (rotate left/right), permutam ciclicamente a
array lógica.

Eu obviamente não cobri 100% de tudo que há. Mas acho que esse resumo já é
bom pra tirar umas incertezas que forem surgindo. Consulte-o sempre que ficar confuso
com a sintaxe. Não dá pra ler ele todo no começo, porque vai estar tudo meio fora de
contexto, mas se deixar pra ler no final, ele vai ser bem menos útil.

Procure deixá-lo aberto sempre que possível no começo, especialmente


com os primeiros exemplos de código.

Página 2 - Revisão 2 - 15/06/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Apostila Aleatória de VHDL e GHDL do Borges
Adendo 2: Integer, signed, unsigned...
ou “A sutil dança dos tipos numéricos”
ou “Meu integer me traiu com um unsigned, e agora?”

Quando se ensina VHDL, uma coisa que geralmente é deixada de lado é essa
confusão entre signed, unsigned, integer, e sabe-se lá o que mais. Eu, inclusive, sou
culpado disso ao estar incluindo esse detalhe num “adendo”. Mas eu tenho uma boa
desculpa: 90% das vezes, as duas conversões (bit_vector → integer e vice-versa) que eu
coloquei nos exemplos vão ser tudo que você precisa, mesmo sem ter a MENOR IDEIA
do que seja um unsigned no VHDL.

Por outro lado, pode ser que algum EP seu dê ruim, e a linha marcada com
vermelho vai ter uns to_unsigned ou to_integer no meio e você vai entrar em pânico pois
só digitou esses nomes uma vez, e foi pra copiar e colar. Vamos resolver isso! Como?
Aprendendo de uma vez por todas como viver em bons termos com os tipos envolvidos.

Se você já leu um começo razoável da apostila, deve ter visto que eu falei pra
porra sobre o VHDL ser uma linguagem fortemente tipada, e enunciei uns prós e contras
dessa escolha de design. Este adendo só existe por causa de um grande “contra”: as
conversões podem ser um inferno. Mas só porque elas ficam rocambolescas não quer
dizer que não possam fazer sentido depois de ler umas páginas escritas por um doido.

Ainda operando na asserção de que você já leu um pouco, você sabe que eu deixei
claro que tipos numéricos e lógicos são totalmente INCOMPATÍVEIS. Eles não se
conhecem, não se conversam. Se você tem um numérico numa mão e um lógico na outra,
tá proibido atribuir, somar, subtrair, comparar, testar igualdade, qualquer coisa. Não
estamos no mundo mágico do JavaScript onde “5”>6 e “5”>4 são coisas válidas.

Espero que tenha ficado claro que vivemos no mundo pragmático e previsível da
tipagem forte. Mas como fazemos coisas nesse mundo? “Como que eu subtraio 5 de uma
entrada, se ela vem como bit_vector, Borges?”

Agora eu quero que você pare um pouco pra pensar nessa pergunta. Que
Diabos™ quer dizer isso? “Ué, eu recebo um número como complemento de dois, e quero
subtrair 5…”

Aí sim sua pergunta fez sentido. Antes, faltava informação. Afinal, “subtrair 5” é
uma operação aritmética. Você não quer subtrair 5 de uma bitstring porque isso não faz
porra de sentido nenhum. Você quer subtrair 5 do [número que resulta quando
interpretamos a bitstring como complemento de dois] e fazer algo com a bitstring
resultante [de codificar o resultado da subtração como complemento de dois].

Página 1 - Revisão 1 - 14/07/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Ufa, voltamos pro mundo onde as coisas fazem sentido. Sabemos o que queremos
fazer. Mas como fazemos? Bem, ficaram evidentes duas etapas quando queremos
converter entre tipos lógicos e numéricos. Quando queremos converter de um tipo
lógico para um numérico, rola uma interpretação seguida de uma conversão. No sentido
contrário, rola uma conversão seguida de uma interpretação. No VHDL, essas duas
etapas ficam explícitas nos tipos signed e unsigned.

Ao contrário do que sua possível experiência com C/C++ possa indicar, signed e
unsigned não são tipos numéricos. Eles são tipos lógicos, idênticos a um bit_vector.
Você pode fazer casting entre eles livremente. “Ué, então pra quê eles existem?”

Eles existem para declarar sua intenção. Uma função opera diferentemente de
acordo com os tipos de seus parâmetros. Quando “renomeamos” nosso bit_vector para
unsigned, deixamos clara nossa intenção para que “funções que tratam de tipos lógicos
como se representassem números” que aquela coisa deve ser tratada de um tal jeito.

É nessa ideia de “overloading” das funções que reside a mágica do signed e do


unsigned: eu posso, por exemplo, construir uma função to_integer que, quando recebe
um signed, retorna o valor numérico que resulta da interpretação daquela bitstring aos
olhos do complemento de dois, mas quando recebe um unsigned, interpreta como
inteiro sem sinal. Por sorte, essa função já existe, e pode ser importada do pacote
IEEE.numeric_bit para brincar com bit, ou IEEE.numeric_std para std_logic & cia.

Sim, você poderia fazer uma função que recebe um bit_vector e algum booleano
indicador de “interpreta essa merda como complemento de dois”. Mas aí perdemos
clareza, nossa intenção de interpretação desaparece pra dentro da documentação da
função. Quando explicitamos isso no tipos dos nossos parâmetros, fica imediatamente
claro pra quem vê o código (mas nunca viu sua função na vida) a intenção de
interpretação daquela bitstring.

Bem, chega de explicar a valsa do signed e do unsigned. Como que brincamos com
essa galera no código? Dois tipos de funções serão nossas amigas: casts e conversões.

No VHDL, um cast pode ser feito entre tipos compatíveis simplesmente


chamando o nome do tipo “como se fosse uma função”. Por exemplo, se temos um
bit_vector chamado s, podemos chamar unsigned(s) sem problema algum! Essa
pequena expressão será do tipo unsigned, podendo ser atribuída sem medo de ser feliz.
Analogamente, podemos atribuir a s como tal: s <= bit_vector(u) para algum sinal
do tipo unsigned chamado u.

Um cast não tem efeito nenhum sobre os valores, pois ele só existe entre tipos
compatíveis, tipos que guardam o mesmo tipo de coisa. Unsigned é só outro nome…

Página 2 - Revisão 1 - 14/07/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado


Uma conversão é algo mais complexo do que um cast. Ela é uma função. Assim
sendo, ela pode receber mais de um parâmetro (um cast não, pois nem função é), e pode
se comportar diferentemente de acordo com os tipos desses parâmetros (um cast nem
tem comportamento de fato, é tudo compatível…). As funções de conversão geralmente
vão começar com “to_” para deixar bem claro o tipo de retorno e que não são casts.

No contexto dos inteiros, geralmente vamos brincar com apenas três funções de
conversão, a depender da direção que estamos tomando:

● A função to_integer(m), para m (signed ou unsigned), interpreta a bitstring


como (complemento de dois ou sem sinal) e retorna o valor numérico.
● A função to_signed(s, L) codifica o valor numérico s como complemento de
dois e retorna essa codificação como uma bitstring de tipo signed de L bits.
● A função to_unsigned(s, L) codifica o valor numérico s como binário sem
sinal e retorna essa codificação como uma bitstring de tipo unsigned de L bits.

Note que essas funções podem produzir erros de valor. Por exemplo, as duas
últimas podem produzir warnings em tempo de simulação caso sejam invocadas com
um valor que não cabe em L bits naquela codificação. Isso pode ser especialmente
perigoso quando a variável/sinal do valor numérico não foi declarada com um range
explícito. A presença de um range ajuda a deixar as coisas mais claras, use-o sempre que
puder, especialmente quando o valor do integer vier de uma chamada a to_integer…

Pra fechar essa parte com chave de ouro… e as conversões que você copiou e
colou à exaustão? Elas são o resumo de tudo que a gente viu até agora! Por exemplo, o
eterno “to_integer(unsigned(v))” significa “explicite que queremos interpretar v
como um inteiro sem sinal e, sabendo disso, converta-o para um valor numérico inteiro”.
Na outra direção, temos o eterno “q <= bit_vector(to_unsigned(n, 8))”, que
significa “codifique o valor numérico de n em complemento de dois com 8 bits, e mude o
tipo do unsigned resultante para bit_vector, pois queremos guardá-lo em q, que é do tipo
bit_vector”.

Você sempre usou essas conversões, só nunca enxergou que signed e unsigned
eram tipos porque nunca teve a necessidade de declarar um sinal do tipo unsigned, por
exemplo. Ele é só um outro nome pra bit_vector mesmo… qual seria a necessidade?

O mundo cartesiano da tipagem forte é meio chato, mas é melhor do que


conversões implícitas mágicas e cheias de surpresas desagradáveis.

Respire. A explicação em si acabou. Em breve vou adicionar um exemplo!

Página 3 - Revisão 1 - 14/07/2020 - Bruno Borges Paschoalinoto - Nenhum direito reservado

Você também pode gostar