Neste trabalho não se descreve nem perto de toda linguagem Verilog, para isso remete-se o leitor para a documentação. O objectivo é dar ao leitor umas noções básicas de Verilog. O suficiente para criar alguns módulos básicos e procurar a restante informação ao nível das necessidades.
Começamos a nossa incursão em Verilog com um
programinha simples. Pelas instruções de atribuição podemos perceber que a
linguagem é muito parecida com C. Os comentários têm um sabor de C++, e. g. são
delimitados por “//” par uma linha, ou por “/*” e “*/” para diversas linhas. A
linguagem Verilog descreve um sistema digital como um conjunto de módulos, mas
para este exemplo teremos um único módulo chamado “simple”.
//A first digital model in Verilog
module simple;
//Exemplo simples do
nível de transferência do registo (RTL) em Verilog.
//O registo A é
incrementado por um. Depois os primeiros quatro bits de B são
//alterados para a
negação do últimos quatro bits de A.
//C é a redução AND
dos últimos dois bits de A.
// declaração de
registos e flip-flops
reg [0:7] A, B;
reg C;
//Os dois
"initials" e "always" executam concurrencialmente
initial begin: stop_at
//Para a
execução depois de 20 unidades de tempo.
#20; $stop;
end
initial begin:
Init
//Inicializa
o registo A, outros valores ficam como x
A = 0;
//Cabeçalho
$display("Time A B C");
//Exibe a
mensagem, de cada vez que A, B ou C mudam
$monitor(" %0d %b %b %b", $time, A, B, C);
end
//main_process
repete até a simulação acabar
always begin: main_process
// #1 significa
esperar uma unidade de tempo
#1 A = A + 1;
#1 B[0:3] =
~A[4:7]; // ~ negação entre-bits
#1 C =
&A[6:7]; //redução AND dos dois últimos bits de A
end
endmodule
No módulo simple, declarámos A e B como registos de 8-bit e C com um
registo de 1-bit também chamado flip-flop. Dentro do módulo, o always e os dois
initial descrevem três threads de controlo, isto é, funcionam ao mesmo tempo ou
concurrencialmente. Dentro da construção initial, as instruções são executadas
sequencialmente tal como em C ou outras línguas de programação imperativas
tradicionais. A construção always comporta-se como a construção initial, mas
com diferença que executa durante toda a simulação.
A notação #1 significa executar a indicação após esperar de uma unidade
do tempo simulado. Assim, a thread de controlo causada pelo primeiro construção
initial atrasa 20 unidades de tempo antes de chamar o $stop e parar a
simulação.
A instrução de sistema $display permite que o programador imprima uma
mensagem tal como o printf faz na linguagem C e o println em Java. Para cada
unidade de tempo em que as variáveis listadas mudam, a função $monitor exibe
uma mensagem. A função de sistema $time retorna o valor actual do tempo
simulado.
Abaixo temos o output esperado da simulação:
Time A B C
0 00000000 xxxxxxxx x
1 00000001 xxxxxxxx x
2 00000001 1110xxxx x
3 00000001 1110xxxx 0
4 00000010 1110xxxx 0
5 00000010 1101xxxx 0
7 00000011 1101xxxx 0
8 00000011 1100xxxx 0
9 00000011 1100xxxx 1
10 00000100 1100xxxx 1
11 00000100 1011xxxx 1
12 00000100 1011xxxx 0
13 00000101 1011xxxx 0
14 00000101 1010xxxx 0
16 00000110 1010xxxx 0
17 00000110 1001xxxx 0
19 00000111 1001xxxx 0
Stop at simulation time 20
Recomenda-se que o leitor analise
atentamente o código e perceba os outputs antes de continuar, visto que esta é
a estrutura típica de um programa em Verilog. Um bloco initial para delimitar o
tempo de simulação, outro bloco initial para inicializar os registos e
descrever os que devem ser monitorizados e finalmente um bloco always para o
sistema digital a modelar. Note que
todas as indicações no segundo initial são feitas com o tempo = 0, visto que
não há nenhum instrução da forma #<inteiro> para atrasar.
Léxico da Linguagem
O léxico da linguagem é parecido com C++. Assim, os comentários são
precedidos por “//” ou delimitados por “/*” e “*/”. Palavras reservadas, como
module, são invariavelmente todas em minúsculas. A linguagem é case-sensitive,
o que diferencia as letras minúsculas das maiúsculas. Os espaços são também
importantes, porque são estes que delimitam os tokens da linguagem
Os números podem ser especificados de forma normal, como uma série de
dígitos com ou sem sinal, ou então na forma:
<tamanho><base><número>
onde o campo <tamanho>, opcional, contém os dígitos decimais que
especificam o tamanho da constante em bits. O campo <base> forma-se com o
caracter ' (plica) seguido por um dos seguintes caracteres b, d, o e h, que
representam, binário, decimal, octal e hexadecimal, respectivamente. O campo
<número> contem os dígitos legais para a <base>. Alguns exemplos:
549 // número
decimal
'h 8FF // número
hexadecimal
'o765 // número
octal
4'b11 // número
binário de 4-bits: 0011
3'b10x // número
binário de 3-bits com último bit desconhecido
5'd3 // número
decimal de 3-bits
-4'b11 //
complementos de dois de 4-bits de 0011 ou 1011
Atenção: o sinal dever colocado apenas antes do campo <tamanho>.
Uma string é uma sequência de caracteres separada por caracteres “ (aspas):
"isto é uma string"
Os operadores são um, dois ou três caracteres utilizados nas expressões e são especificados mais à frente neste documento.
Identificadores são especificados por uma letra ou
underscore seguido de zero ou mais letras, dígitos, sinais dólar ou underscores
até um máximo de 1024 caracteres.
Estrutura do programa
A linguagem Verilog descreve um sistema digital como um conjunto dos
módulos. Cada um destes módulos tem uma relação com outros módulos que descreve
como estão ligados. Normalmente coloca-se um módulo por ficheiro mas é apenas
convenção e não é obrigatório. Os módulos podem funcionar simultaneamente, mas
normalmente especificamos um módulo do nível superior que especifica um sistema
fechado contendo dados de teste bom como modelos de hardware. A este módulo
chamaremos módulo de topo. O módulo de topo invoca instâncias de outros módulos.
Os módulos podem representar as partes de hardware que podem ir desde
portas simples até sistemas complexos como microprocessadores. Os módulos podem
ser especificados por comportamento ou estruturalmente (ou uma combinação dos
dois). Uma especificação por comportamento define o comportamento de um sistema
digital (módulo) que usa as construções tradicionais duma linguagem de
programação, e. g., ifs, instruções de atribuição. Uma especificação estrutural
expressa o comportamento de um sistema digital (módulo) como uma interconexão
hierárquica dos módulos secundários. No fundo da hierarquia os componentes
devem ser primitivas ou especificados por comportamento. As primitivas de
Verilog incluem portas, e.g., nand, bem como transístores de passagem (switches).
A estrutura de um módulo é a seguinte:
module <nome_módulo> (<lista_portas>);
<declarações>
<elementos_módulo>
endmodule
<nome_módulo> é um identificador que descreve univocamente o
módulo. <lista_portas> é uma lista das portas de input, de inout e de
output que são usados na ligação a outros módulos. A parte <declarações>
especifica estruturas de dados como registos ou memórias bem outras construções
executáveis como funções.
Os <elementos_módulo> podem ser blocos initial, blocos always
,atribuições contínuas ou instâncias de módulos.
A semântica do bloco module em Verilog é muito diferente de funções ou
procedimentos noutras linguagens. Ao invés de ser chamado, o módulo é
instanciado no início do programa e dura até ao fim do programa. Isto faz sentido,
pois o Verilog representa modelos de hardware e faz sentido assumir que ninguém
adiciona, remove ou altera qualquer dos módulos depois de serem postos a
funcionar. De cada vez que um módulo é instanciado, dá-se à instância um nome.
Por exemplo, NAND1 e NAND2 são os nomes das instâncias de nossa porta do NAND
no exemplo seguinte.
Abaixo estão as especificações para um módulo NAND. A saída out é a
negação da conjunção das entradas in1 e in2.:
//Modelo comportamental de uma porta NAND
module
NAND(in1, in2, out);
input
in1, in2;
output
out;
//atribuição contínua
assign
out = ~(in1 & in2);
endmodule
As portas in1, in2 e out não são mais que “etiquetas” em fios. A
atribuição contínua assign monitoriza constantemente as variáveis na expressão
da direita e quando se dão alterações reavalia a expressão e propagas o
resultado para o lado esquerdo(out). A instrução assign é utilizada para
modelar circuitos combinacionais, onde as saídas mudam quando muda uma das
entrada.
Abaixo estão a especificação de um
módulo AND à custa de dois módulos NAND. Note que se liga a saída de um
dos módulos a ambas as entradas do outro:
module
AND(in1, in2, out);
//Modelo estrutural de uma porta AND a partir de duas NANDs
input
in1, in2;
output
out;
wire
w1;
//duas instâncias de NAND
NAND
NAND1(in1, in2, w1);
NAND
NAND2(w1, w1, out);
endmodule
Este módulo tem duas instâncias do módulo NAND: NAND1 e NAND2, ligados internamente por um fio interno, w1.
A fórmula geral para instanciar um módulo é:
<nome_módulo> <lista_parâmetros> <nome_instância> (<lista_portas>);
onde <lista_parâmetros> são valores para os parâmetros a passar à instância.
Abaixo está um módulo de alto nível que inicializa algumas variáveis e começa a monitorização de variáveis.
module
test_AND;
// High
level module to test the two other modules
reg a, b;
wire
out1, out2;
initial
begin //Valores de
teste
a = 0; b = 0;
#1 a = 1;
#1 b = 1;
#1 a = 0;
end
initial begin //Começa a monitorizar entradas e saídas
$monitor("Time=%0d a=%b b=%b out1=%b
out2=%b",
$time, a, b, out1, out2);
end
//Instâncias de AND e NAND
AND
gate1(a, b, out2);
NAND
gate2(a, b, out1);
endmodule
Note que é necessário guardar os valores a e b ao longo do tempo, para
isso usam-se registo de 1-bit. As variáveis reg, guardam o último valor que lhes
foi procedimentalmente atribuído, tal como noutras linguagens imperativas.
Linhas no entanto não têm capacidade de armazenamento. Assim, têm que ter um
valor constantemente associado, e. g., a uma instrução assign ou a saída de um
módulo.
Atenção: se as linhas não forem correctamente conectadas, assumem o valor especial x (desconhecido).
Atribuições contínuas usam a instrução assign ao passo que atribuições procedimentais têm a forma <variavel_reg> = <expressão> sendo que<variavel_reg> deve ser um registo ou memória. Este tipo de atribuições(procedimentais) só pode aparecer em blocos initial ou always.
As instruções no bloco do primeiro initial são executadas
sequencialmente, sendo que algumas são atrasadas por #1, isto é, uma unidade do
tempo simulado. O bloco always comporta-se tal como o bloco initial mas,
repete-se indefinidamente até a simulação terminar. Os blocos initial e always
são usados para modelar lógica sequencial (isto é, autómatos de estados
finitos).
Verilog faz uma distinção relevante entre a atribuição procedimental e
a atribuição contínua assign. A atribuição processual muda o estado de um
registo, isto é, lógica sequencial, ao contrário da atribuição contínua que é
para usada modelar a lógica combinacional. As atribuições contínuas mantém
variáveis tipo linha e alteram-nas quando existe uma modificação nas entradas.
Ao correr os três módulos no simulador produz-se a saída seguinte:
Time=0
a=0 b=0 out1=1 out2=0
Time=1
a=1 b=0 out1=1 out2=0
Time=2
a=1 b=1 out1=0 out2=1
Time=3
a=0 b=1 out1=1 out2=0
Como o simulador não tem mais nada para executar, não é necessário
terminar a simulação explicitamente.
Tipos de dados
Tipos de dados físicos
Como o propósito do Verilog é modelar hardware, os principais tipos de
dados são registos(reg) e linhas (wire). As variáveis reg armazenam o último
valor que lhes foi procedimentalmente atribuído, ao contrário das linhas que
representam ligações físicas entre entidades estruturais tais como portas. Uma
variável wire representa apenas uma “etiqueta” num fio e não armazena valores.
(Existem outros tipos de dados relacionados com wire, mas excedem o espectro
desta leitura.)
As variáveis reg e wire podem ter os seguintes valores:
0 zero lógico ou falso
1 um lógico ou verdadeiro
x valor lógico desconhecido
z alta impedância em portas tri-estado
As variáveis reg são inicializadas a x e qualquer linha não conectada tem também o valor x.
O tamanho deste tipo de variáveis pode ser especificado na declaração. Por exemplo, as declarações seguintes:
reg [0:7] A, B;
wire [0:3] Dataout;
reg [7:0] C;
especificam os registos A e B com 8-bits cada e o bit mais significativo é o bit 0, ao passo que no registo C é o bit 7. A linha Dataout tem 4-bits.
Os bits num registo ou linha podem ser endereçados pela notação [<bit-inicialt>:<bit-final>].
Por exemplo, na segunda linha do código abaixo:
initial
begin: int1
A =
8'b01011010;
B = {A[0:3] | A[4:7], 4'b0000};
end
o valor de B é alterado para a disjunção dos quatro
primeiros bits de A com os quatro últimos concatenada com 0000. B fica com o
valor 11110000. As {} chavetas representam a concatenação.
Qualquer argumento pode ser replicado especificando um número na fórmula:
{numero_repetições{exp1, exp2, ... , expn}}
Eis alguns exemplos:
C = {2{4’b1011}}; //C é alterado para 8’b10111011
C = {{4{A[4]}}, AA[4:7]}; // primeiros 4 bit são
extensões de sinal
Os limites destas referências numa expressão têm que ser valores constantes. No entanto, um bit isolado pode ser endereçado com uma variável. Tomemos o exemplo:
reg [0:7] A, B;
B =
3;
A[0: B] = 3'b111; // ILEGAL – os indiques devem ser constantes!!
A[B] = 1'b1; // A referência um bit isolado é LEGAL
Este tipo exótico de limitação justifica-se de novo com o objectivo do Verilog: descrever hardware, por isso necessitamos apenas de expressões realizáveis por hardware.
Em Verilog as memórias são declaradas como vectores de registos. Para obter uma memória com 1K de Words cada uma a 32 bits tem-se:
reg [31:0] Mem [0:1023];
A notação Mem[0] endereça a primeira word da memória e o índice para este tipo de operações pode ser um registo.
Atenção: Verilog HDL não suporta referências à memória bit-a-bit. Assim, para utilizar uma série de bits em memória é necessário carregar primeiro a word inteira para um registo temporário.
Tipos de dados abstractos
Para além da modelação de hardware, existem outros usos para as variáveis num modelo da hardware. Por exemplo, o programador pode precisar de usar um inteiro para contar o número de vezes que executa um ciclo ou executar outras operações de debug. Para maior comodidade, o Verilog HDL inclui alguns tipos de variáveis sem representação no hardware. Esses tipos são real, integer e time e são semelhantes aos correspondentes noutras linguagens.
Atenção: uma variável reg não tem sinal e um integer de 32 bits tem. Isto tem consequências não negligenciáveis ao subtrair.
Variáveis do tipo time armazenam valores de 64-bits e usam-se em conjunto com a função de sistema $time. Arrays unidimensionais de variáveis integer e time podem ser utilizados.
Atenção: arrays do tipo real e arrays multi dimensionais são ilegais.
Alguns exemplos:
integer Count; // inteiro simples de 32-bits com sinal
integer K[1:64]; // array de 64 inteiros
time
Start, Stop; // duas variáveis time de 64-bits
Operadores
Operadores binários aritméticos
Operadores binários aritméticos operam sobre dois operandos. Registos e linhas são tratados como sendo sem sinal. No entanto, reais e inteiros podem ter sinal. Se qualquer um dos bits de qualquer operando for desconhecido, então o resultado é esse mesmo: x.
Operador | Nome | Detalhes |
+ | Adição | |
- | Subtracção | |
* | Multiplicação | |
/ | Divisão | Divisão por 0 resulta em x |
% | Modulus |
Operadores unários aritméticos
Operador | Nome | Detalhes |
- | Simétrico | Muda o sinal do operador |
Operadores relacionais
Operadores relacionais comparam dois operando e devolvem TRUE(1) ou FALSE(0). Se qualquer dos bits é desconhecido, o resultado é também desconhecido (x).
Operador | Nome | Detalhes |
> | Maior | |
>= | Maior ou igual | |
< | Menor | |
<= | Menor ou igual | |
== | Igual | |
!0 | Diferente |
Operadores lógicos
Operadores lógicos operam sobre operandos lógicos e devolvem TRUE(1) ou FALSE(0).
Atenção: Não confundir operadores lógicos com operadores booleanos entre-bits. Por exemplo, ! é a negação lógica e ~ é a negação entre-bits. O primeiro nega, e. g., !(5==6) é TRUE. O segundo complementa os bits, e. g., ~{1,0,1,1} é 0100.
Operador | Nome | Detalhes |
! | Negação lógica | |
&& | Conjunção lógica | |
|| | Disjunção lógica |
Operadores operam sobre os bits dos operandos. Por exemplo, o resultado de A&B é o AND dos bits correspondentes de A em B. A operação sobre um bit desconhecido(x) resulta no valor esperado. Ou seja, (x AND FALSE) devolve x e (x OR TRUE) retorna TRUE.
Operador | Nome | Detalhes |
~ | Negação entre-bits | |
& | Conjunção entre-bits | |
| | Disjunção inclusiva entre-bits | |
^ | Disjunção exclusiva entre-bits | |
~& | NAND entre-bits | |
~| | NOR entre-bits | |
~^ ou ^~ | Equivalência entre-bits |
Operadores unários de redução
Estes operadores produzem um único bit, resultado da aplicação do operador a todos os bits do intervalo. Assim, &A fará a operação AND sobre todos os bits de A.
Operador Nome Detalhes
Operador | Nome | Detalhes |
& | Redução AND | |
| | Redução Or | |
^ | Redução XOR | |
~& | Redução NAND | |
~| | Redução NOR | |
~^ | Redução XNOR |
Outros operadores
Outros operadores um pouco mais
exóticos.
Operador | Nome | Detalhes |
=== | Igualdade | Comparação entre bits, incluindo x e z. Todos os bits têm que coincidir para haver igualdade. Retorna True ou False. |
!== | Desigualdade | Comparação entre bits, incluindo x e z. Se um bit difere produz desigualdade. Retorna True ou False. |
{ , } | Conatenação | Concatena os bits de 2 ou mais expressões separadas por vírgulas. |
<< | Shift à esquerda |
Espaços de bits vazios enchem-se com 0s. |
>> | Shift à Direita | Espaços de bits vazios enchem-se com 0s. |
?: | Condicional | Devolve um de
dois valores de acordo com uma expressão.
E. g., A = C>D ? B+3 : B-2 significa que se o valor de C é maior que D, então A toma o valor B+3, senão A toma o valor B-2. |
Precedência de operadores
A precedência de operadores está esquematizada na tabela seguinte. Operadores no topo da tabela têm a precedência mais alta e operadores na mesma linha têm a mesma precedência em qualquer expressão, executando-se primeiro os mais à esquerda. Parênteses podem e DEVEM ser utilizados para clarificar a situação e aumentar a legibilidade do código.
operadores unários: ! & ~& | ~| ^ ~^ + - (precedência mais alta)
* / %
+ -
<< >>
< <= > >+
== != === ~==
& ~& ^ ~^
| ~|
&&
||
?:
Blocos de controlo
Verilog HDL tem diversas instruções de controlo que podem ser usadas nas partes procedimentais do código, ou seja, nos blocos initial e always. A maior parte destes blocos são comuns a quase todas as linguagens procedimentais. Ao contrário de linguagens como C e Java, que usam chavetas, Verilog usa begin e end para delimitar os blocos tal como Pascal.
Selecção
Um if apresenta a seguinte forma:
if (A ==
4)
begin
B = 2;
end
else
begin
B = 4;
end
Ao contrário de C e Java, a instrução case em Verilog não necessita de
break. Ao invés disso, assim que <expressão> igualar a <valor>, as
<instruções> correspondentes são executadas e a execução prossegue a
partir do endcase:
case (<expressão>)
<value1>: <instruções>
<value2>: <instruções>
default: <instruções>
endcase
O exemplo seguinte verifica o valor de uma linha de 1-bit:
case
(sig)
1'bz: $display("O sinal está a flutuar");
1'bx: $display("O sinal é desconhecido");
default:
$display("Signal is %b", sig);
endcase
Repetição
De novo, verifica-se a proximidade da expressão for entre Verilog e Java. No entanto, os operadores ++ e –- não existem, devendo por isso utilizar-se i = i + (ou -) 1;
for(i = 0; i < 10; i = i + 1)
begin
$display("i= %0d", i);
end
O bloco while tem a forma mais comum:
i = 0;
while(i
< 10)
begin
$display("i= %0d", i);
i = i + 1;
end
A instrução repeat repete o bloco associado um número determinado de vezes.
repeat
(5)
begin
$display("i=
%0d", i);
i = i + 1;
end