Escolar Documentos
Profissional Documentos
Cultura Documentos
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:
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!
Declaração Definição
Entity/component Architecture
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.
Absorva essa informação com carinho, e quanto estiver confortável, vamos pro
próximo capítulo.
~ Borges
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:
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
É 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:
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...
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.
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:
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.
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.
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.
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!
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.
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):
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!
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.
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.
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.
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:
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).
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.
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.
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.
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.
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.
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!
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.
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).
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!
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.
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.
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!
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.
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.
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.
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!
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.
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.
case expr is
when valor_1 => coisa1; coisa2; coisa3;
when valor_2 => coisa1; coisa4;
when valor_3 => coisa5;
when others => coisa6; coisa7;
end case;
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:
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.
“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 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.
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.
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.
~ 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:
Se você fez tudo certo, o PowerShell deve ter dito algo assim. Reinicie.
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.
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.
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.
1. ./configure --prefix=/usr/local
2. make
3. sudo make install
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
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.
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.
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;
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.
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].
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.
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.
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…
No contexto dos inteiros, geralmente vamos brincar com apenas três funções de
conversão, a depender da direção que estamos tomando:
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?