Verilog HDL es uno de los dos lenguajes de descripción de hardware (HDL, del inglés Hardware Description Language) más comunes que utilizan los diseñadores de circuitos integrados (IC), con una sintaxis derivada del lenguaje de programación C. Se corresponde inicialmente con el IEEE standard 1364-1995, extendido posteriormente en IEEE standard 1364-2001 y SystemVerilog.
Un HDL permite simular el diseño en etapas tempranas del ciclo de diseño para corregir errores o experimentar con diferentes arquitecturas, llevar a cabo la síntesis lógica del mismo y facilitar la documentación del mismo. Los diseños descritos en HDL son independientes de la tecnología, fáciles de diseñar y depurar, y suelen ser más legibles que los esquemas, especialmente para circuitos grandes y complejos, lo que permite además un prototipado rápido y acelerar el tiempo de salida del producto final (TTM, del inglés Time-To-Market).
Verilog se puede utilizar para describir diseños en cuatro niveles de abstracción:
if
, case
y loop
). Los últimos tres niveles se corresponden con una descripción también denominada estructural, si bien el lenguaje permite llevar a cabo descripciones mixtas que combinan parte comportamental y estructural, así como las construcciones que pueden utilizarse para controlar la entrada y salida de la simulación.
Es común actualmente utilizar descripciones Verilog como entrada para herramientas de síntesis lógica que generan descripciones a nivel de puerta o netlist del circuito, si bien algunas construcciones de Verilog no son sintetizables y es importante diferenciar ambos tipos. También la manera de escribir el código afectará en gran medida al tamaño y la velocidad del circuito sintetizado. Como en la mayoría de los casos se pretenderá sintetizar los circuitos, las construcciones no sintetizables deben usarse sólo para bancos de pruebas (testbenches). Se trata de módulos de programa utilizados para generar las E/S necesarias para simular el resto del diseño normalmente bajo el modelo estímulo/respuesta.
Como en la mayoría de HDLs se distinguen dos tipos/estilos de codificación/modelado/descripción que tienen correspondencia con determinados niveles de abstracción señalados:
assign a = b & c | d; assign d = e & (~c);
always @(posedge clk) count <= count+1;
El estilo procedural es preferido por los diseñadores por la facilidad de codifificación y semejanza con los lenguajes de programación de alto nivel (HLL, del inglés High Level Language), si bien es importante tener en cuenta que la mayoría de construcciones procedurales con asignación implicarán la generación de almacenamiento en síntesis y esto puede no ser deseable en algunos casos donde se desea evitar lógica superflua atendiendo a restricciones de área o de temporización, por lo que se recomienda hacer uso del estilo estructural en circuitos puramente combinacionales.
Los archivos de código fuente Verilog constan de los siguientes componentes (tokens) léxicos:
Los espacios en blanco separan las palabras y pueden ser: espacios, tabuladores, caracteres de nueva línea y de retorno de carro. Así, una sentencia puede extenderse sobre múltiples líneas sin caracteres de continuación especiales.
Los comentarios se pueden especificar de dos maneras (exactamente de la misma manera que en C/C++):
//
. Todo el texto entre estos caracteres y el final de la línea será ignorado por el compilador Verilog./*
y */
. El uso de este método permite continuar los comentarios en más de una línea. Esto es bueno para “comentar” código de muchas líneas, o para comentarios breves intercalados en la línea.a = c+d; // este es un comentario simple /* sin embargo, este comentario continua en en más de una línea */ assign y = temp_reg; assign x = ABC /*más su complemento*/ + ABC_
El almacenamiento de números se define con un número de bits y el correspondiente valor que puede especificarse en binario, octal, decimal o hexadecimal.
Ejemplos
3'b001
, un número de 3 bits5'd30
, (= 5'b11110
) 16'h5ED4
, (= 16'd24276
)
Los identificadores son palabras definidas por el usuario para variables, nombres de funciones, nombres de módulos, nombres de bloques y nombres de instancia.
Los identificadores comienzan con una letra o subrayado (no con un número o $) y pueden incluir cualquier número de letras, dígitos y subrayados.
Los identificadores en Verilog distinguen entre mayúsculas y minúsculas.
ABCDE…abcdef…1234567890 _$
- & # @
Ejemplos
adder // usa subrayados para hacer by_8_shifter // los identificadores más legibles _ABC_ /* no es lo mismo que */ _abc_ Read_ // suele usarse para indicar NOT Read
Los operadores son uno, dos, y a veces tres caracteres con símbolos especiales, p.ej. >
, +
, ~
, &
, !
, =
, utilizados para realizar operaciones sobre variables.
El conjunto de operadores y su uso se describen en detalle en apartados posteriores.
Estas son palabras que tienen un significado especial en Verilog. Algunos ejemplos son assign
, case
, while
, wire
, reg
, and
, or
, nand
, y module
, la lista completa se verá en lo sucesivo.
No deben utilizarse como identificadores.
Las palabras clave de Verilog también incluyen directivas del compilador, tareas y funciones del sistema.
El diseño de circuitos y sistemas digitales con Verilog HDL pasa siempre por utilización jerárquica de módulos como elemento principal del lenguaje y la descripción a distintos niveles de abstracción y estilos del mismo.
directivas_compilador module nombre_de_modulo(…); // interfaz (puertos) input … output … inout … // conexiones internas, variables, ... wire … reg … integer … real … time … // componentes (submódulos) instancias_de_modulos instancias_de_primitivas // estructura/comportamiento assign initial always endmodule
Verilog consta de sólo cuatro valores básicos. Casi todos los tipos de datos de Verilog almacenan todos estos valores:
Estos dos últimos valores x y z tienen un uso limitado para la síntesis.
La representación numérica de valores en las distintas bases de numeración responde a las siguientes reglas que permite los siguientes caracteres:
Sintaxis valor
<size>'<base format><number>
Tipo | Prefijo | Caracteres permitidos |
---|---|---|
binario | 'b | 01xXzZ_? |
octal | 'o | 0-7xXzZ_? |
decimal | 'd | 0-9_ |
hexadecimal | 'h | 0-9a-fA-FxXzZ_? |
Ejemplos
334
Número decimal de 32 bits32'b101
Número binario en 32 bits (ceros a la izquierda)3'b11
Número binario de 3 bits (es decir, 011
)20'hf_ffff
Número hexadecimal de 20 bits10'bZ
Número de 10 bits, todos ellos en tercer estado-4'b11
Número binario de 4 bits, complemento a 2 de 0011
= 1101
'h8FF
Número hexadecimal de 32 bitsLos tipos de datos que tienen valores asociados como los indicados anteriormente necesitan declararse y responden a la siguiente sintaxis:
Sintaxis definición dato
<data_type> [<msb>:<lsb>] <list_of_identifiers>;
Después de declarar un vector, puede ser referenciado:
Sintaxis selección de bit o parte de vector
<variable_name>[index] <variable_name>[msb:lsb]
Ejemplos
wire [7:0] result; reg [0:3] cuenta; reg [23:16] rega, regb; reg [7:0] a, b; reg [3:0] ls; reg c; c = a[7] & b[7]; // selecciones de bit ls = a[7:4] + b[3:0]; // selección de parte de vector
Un wire
representa un cable/hilo/nodo físico en un circuito y se utiliza para conectar (net) puertas o módulos, así como modelar nodos combinacionales en sentencias de asignación continua. No almacena su valor, pero debe ser generado por una instrucción de asignación continua o por conexión a la salida de una puerta o módulo. Dentro de una función o bloque,
puede leerse su valor pero no asignarlo. El tipo por defecto es de 1 bit.
Otros tipos específicos de cables son:
wand
(cable AND): el valor deriva de una puerta AND y de las entradas (drivers) de la misma (modela open-collector)wor
(cable OR): el valor depende de lógica OR y drivers conectadas a la misma (modela ECL)tri
(triestado): todos los drivers conectados a un tri
deben ser z
, excepto uno (que determina el valor del tri
)tri0
(pull-down), tri1
(pull-up): cuando no están conectados se asocian a la correspondiente resistencia.Sintaxis
wire [msb:lsb] wire_variable_list; wand [msb:lsb] wand_variable_list; wor [msb:lsb] wor_variable_list; tri [msb:lsb] tri_variable_list;
Ejemplo
wire c; // simple wire wand d; assign d = a; // el valor de d es el AND lógico de assign d = b; // a y b wire [9:0] A; // un cable (vector) de 10 hilos (wires)
Este tipo de datos sirven para modelar tanto nodos lógicos combinacionales como secuenciales, aunque normalmente se asocia con aquellos que mantienen su valor hasta la siguiente asignación y aparecen en el lado izquierdo de las expresiones en procedimientos initial
y always
, o function
.
Un reg
es el tipo de datos que debe utilizarse para latches, flip-flops y memorias, sin embargo a menudo se sintetiza también como conductor (“combinacional”) en lugar de almacenamiento.
En los registros de varios bits (vectores), los datos se almacenan por defecto como números sin signo y no se realiza ninguna extensión de signo, por lo que deberá tener en cuenta si se necesita operar en complemento a 2.
Sintaxis
reg [msb:lsb] reg_variable_list;
Ejemplo
reg a; // variable registro de 1-bit reg [7:0] tom; // un vector de 8-bit; un banco de 8 registros de 1-bit reg [5:0] b, c; // dos variables de 6-bit
Estas palabras clave declaran puertos de entrada (input
), salida (output
) y bidireccionales (inout
) de un módulo (module
) o tarea (task
).
Los puertos input
e inout
son de tipo wire
, los output
pueden configurarse de tipo wire
, reg
, wand
, wor
o tri
. El valor predeterminado es wire
.
Sintaxis
input [msb:lsb] input_port_list; output [msb:lsb] output_port_list; inout [msb:lsb] inout_port_list;
Ejemplo
module ejemplo_puertos ( input a, // una entrada, por defecto wire simple 1-bit output b, e, // dos salidas, por defecto wires de 1-bit output reg [1:0] c // una salida de tipo reg de 2-bit );
Los enteros son variables de propósito general. En síntesis se utilizan principalmente para los índices de bucles, parámetros y constantes. Son implícitamente del tipo reg
, sin embargo, almacenan datos como números con signo mientras que los tipos declarados explícitamente como reg
se almacenan sin signo.
Si contienen números que no están definidos en tiempo de compilación, su tamaño predeterminado será de 32-bit. Si contienen constantes, el sintetizador las ajusta al ancho mínimo necesario en la compilación.
Sintaxis
integer integer_variable_list; ... integer_constant ... ;
Ejemplo
integer a; // entero simple de 32-bit assign b = 63; // 63 se considera un entero de 7-bit
También existe un tipo derivado (real
) para representar números reales en doble precisión (64-bit) con objeto fundamental de simulación y con conversión con redondeo al entero más cercano en caso de síntesis.
supply0
y supply1
definen cables conectados a lógica 0 (ground) y lógica 1 (power), respectivamente.
Sintaxis
supply0 logic_0_wires; supply1 logic_1_wires;
Ejemplo
supply0 my_gnd; // equivalente a un wire asignado a 0 supply1 a, b;
El tiempo es una cantidad de 64-bit que puede utilizarse en combinación con la tarea de sistema $time
para almacenar el tiempo de simulación.
time
no está soportado en síntesis y, por lo tanto, se utiliza sólo para fines de simulación.
Sintaxis
time time_variable_list;
Ejemplo
time c; c = $time; // c = tiempo de simulación actual
Modelan valores constantes (no nodos), lo que permite p.ej. que las constantes como la longitud de la palabra se definan simbólicamente en un lugar. Esto hace que sea fácil cambiar la longitud de la palabra más tarde, cambiando sólo el parámetro. Una forma alternativa al uso de parámetros es utilizar macros sustituibles en tiempo de preprocesado.
Sintaxis
parameter par_1 = valor, par_2 = valor, .....; parameter [rango] parm_3 = valor
Ejemplo
parameter ancho_de_bus = 7; parameter add = 2’b00, sub = 3’b111; parameter n=4; parameter [3:0] st4 = 4’b1010; . . . reg [n-1:0] harry; /* Un registro de 4-bit cuya longitud se establece por el parámetro n anterior */ always @(x) y = {{(add - sub){x}}; // Operador de repetición if (x) begin state = st4[1]; else state = st4[2]; end
Tanto wire
como reg
permiten modelar nodos combinacionales, si bien deben utilizarse distintos tipos de asignaciones en cada caso, p.ej. si queremos construir un sumador de 8 bits:
module sum ( input [7:0] sum1, sum2, // por defecto, los puertos son wire output [7:0] res ); assign res = sum1 + sum2; endmodule // sum
Un circuito equivalente al anterior se obtendría con la siguiente descripión:
module sum ( input [7:0] sum1, sum2, output reg [7:0] res // ahora es un registro ); always @(sum1 or sum2) begin res = sum1 + sum2; // esto no se puede hacer con wire end endmodule // sum
Observaciones
reg
se trata como un número no negativo (sin signo) en operaciones aritméticas (+, *).X
o Z
, entonces el resultado es X
.Continuando con el ejemplo del sumador anterior, podríamos introducir un parámetro para determinar el ancho de palabra de datos con el que opera, y un nuevo puerto que permita seleccionar la operación a realizar con los datos y ampliar su funcionalidad a sumador/restador:
module sum #(parameter no_bits = 8) ( input sel_op, input [no_bits-1:0] sum1, sum2, output reg [no_bits-1:0] res ); always @(sel_op or sum1 or sum2) begin if (sel_op) res = sum1 + sum2; // asignación procedural else res = sum1 - sum2; // asignación procedural end endmodule // sum
Realizan operaciones aritméticas.
+
(suma)
-
(resta)
*
(multiplicación)
/
(división)
%
(resto/módulo)
+
y -
pueden ser operadores unarios (-z) o binarios (x-y)
Ejemplo
parameter n = 4; reg [3:0] a, c, f, g, count; f = a + c; g = c - n; count = (count + 1) % 16; // puede contar de 0 a 15
Comparan dos operandos y devuelven un único bit 1
o 0
.
Se sintetizan como comparadores.
Los valores de wire
y reg
se consideran positivos, p.ej. (-3’b001)==3’b111
y (-3d001)>3d110
mientras que los enteros se consideran números con signo, p.ej. -1<6
.
<
(menor que)
>
(mayor que)
<=
(menor o igual que)
>=
(mayor o igual que)
==
(igual a)
!=
(no igual a)
Ejemplo
if (x == y) e = 1; /* equivalente: e = (x == y); */ else e = 0; // Comparar en complemento a 2 a>b reg [3:0] a, b; if (a[3] == b[3]) a[2:0] > b[2:0]; else b[3];
Comparan bit a bit dos operandos.
Notación idéntica a operadores reductores (unarios).
~
(NOT bitwise)
&
(AND bitwise)
|
(OR bitwise)
^
(XOR bitwise)
^~
, ~^
(XNOR bitwise)
Ejemplo
module and2 ( input [1:0] a, b, output [1:0] c ); assign c = a & b; endmodule //and2
Operan de forma similar a los bitwise sólo para operandos de un solo bit, y devuelven un solo bit 1
ó 0
.
Pueden trabajar con expresiones, números enteros o grupos de bits y tratan todos los valores distintos de cero como 1
.
Normalmente se utilizan en sentencias condicionales (if … else
) ya que funcionan con expresiones.
!
(NOT lógico)
&&
(AND lógico)
||
(OR lógico)
==
(igualdad lógica)
!=
(desigualdad lógica)
===
(igualdad case
)
!==
(desigualdad case
)
En ===
y !==
se tienen en cuenta bits con valores x
o z
para la comparación bitwise, en cambio cuando aparecen ==
y !=
el resultado de la comparación es siempre x
y estos últimos son sintetizables por contra de los primeros.
Ejemplo
wire [7:0] x,y,z; // variables multibit reg a; ... if ((x==y)&&(z)) a=1; // a = 1 si x igual que y, y z no es cero else a = !x; // a = 0 si x es algo distinto a cero
Operan sobre todos los bits de un vector y devuelven un valor de un solo bit.
Son la versión unaria (un solo argumento) de los operadores bitwise anteriores.
&
(AND reductor)
|
(OR reductor)
^
(XOR reductor)
~&
(NAND reductor)
~|
(NOR reductor)
~^
, ^~
(XNOR reductor)
Ejemplo
module chk_zero ( input [2:0] a, output z ); assign z = ~|a; // NOR reductor endmodule //chk_zero
Desplazan el primer operando el número de bits especificado por el segundo operando.
Las posiciones sobrantes se rellenan con ceros, tanto en desplazamentos a derecha como a izquierda (no hay extensión de signo).
«
(desplazamiento a izquierda)
»
(desplazamiento a derecha)
Ejemplo
assign c = a << 2; /* c = a deplazado a la izquierda 2 bits; las posiciones vacantes se rellenan con 0’s */
Similar al utilizado en C/C++. Se evalúa una de la las dos expresiones basadas en una condición (operador ternario).
Se sintetiza a base de multiplexores.
Sintaxis
conditional_expression ? true_expression : false_expression
Ejemplo
assign a = (inc == 2) ? a+1 : a-1; */ se incrementa o decrementa a si el valor de inc coincide */ assign a = (g) ? x : y;
Combina dos o más operandos para formar un vector mayor.
{ }
(concatenación)
Ejemplos
wire [1:0] a, b; wire [2:0] x; wire [3:0] y; assign x = {1’b0, a}; // x[2]=0, x[1]=a[1], x[0]=a[0] assign y = {a, b}; /* y[3]=a[1], y[2]=a[0], y[1]=b[1], y[0]=b[0] */ assign foo = { a[4:3], 1’b0, c[1:0] }; assign foo = { a[4], a[3], 1’b0, c[1], c[0] }; // equivalente a la anterior assign {cout, sum} = a + b + cin; // concatenación de un resultado
Hace múltiples copias de un item.
{n{item}}
(n réplicas de item)
Ejemplos
wire [1:0] a, b; wire [4:0] x, y; assign x = {2{1’b0}, a}; // equivalente a x = {0,0,a} assign y = {2{a}, 3{b}}; // equivalente a y = {a,a,b,b,b} wire [3:0] z = {4{1’b1}}; // equivalente a z = 4’b1111
Se debe evitar realizar repeticiones nulas ya que ocasionan errores en algunos sintetizadores, p.ej.
parameter n=5, m=5; assign x = {(n-m){a}}
La siguiente tabla muestra la precedencia de los operadores de mayor a menor. Los operadores del mismo nivel evalúan de izquierda a derecha.
Se recomienda utilizar paréntesis para definir el orden de precedencia y mejorar la legibilidad del código.
Operador | Descripción |
---|---|
[ ] | selección de bits ó parte |
( ) | paréntesis |
! ,~ | NOT lógico y bit-wise |
& , | , ~& , ~| , ^ , ~^ , ^~ | reducción AND, OR, NAND, NOR, XOR, XNOR |
+ ,- | (unario) signo positivo o negativo |
{ } | concatenación |
{{ }} | repetición |
* ,/ ,% | (binario) multiplicación, división y resto (no siempre sintetizable) |
+ ,- | (binario) suma y resta |
« ,» | desplazamientos a izquierda y derecha |
< ,> ,⇐ ,>= | comparaciones |
== ,!= | igualdad y desigualdad |
& | AND bit-wise |
^ ,^~ ,~^ | XOR bit-wise, XNOR bit-wise |
| | OR bit-wise |
&& | AND lógico |
|| | OR lógico |
? : | (ternario) condicional |
* Otras consideraciones importantes:
4'hc > 10 (TRUE) 8'd12 && 9'h1 (TRUE) 8'h0 || 0 (FALSE) !8'h6 (FALSE) ~4'b0111 (4'b1000) 2'b01 & 2'b11 (2'b01) 4'h8 | 4'h1 (4'h9) &16'h324d (1'b0) |16'h1 (1'b1) 8'hfe << 3 (8'hf0)
Un módulo es la entidad de diseño principal de Verilog. La primera línea de una declaración de módulo especifica el nombre y la lista de puertos (argumentos), así como el tipo de entrada/salida y ancho de cada puerto (por defecto el ancho es 1-bit).
Las variables del puerto deben ser declaradas como wire
, wand
, …, reg
, aunque por defecto toma el valor wire
. Normalmente las entradas suelen ser wire
, ya que se registran fuera del módulo, las salidas en cambio suelen ser de tipo reg
si se almacenan dentro de bloques always
o initial
.
Sintaxis
module module_name ( input [msb:lsb] input_port_list, output [msb:lsb] output_port_list, inout [msb:lsb] inout_port_list ); ... statements ... endmodule
Ejemplo
module small_block ( // Interfaz del módulo input a, b, c, output o1, o2 ); wire s; // Estructura y comportamiento assign o1 = s | c; // or assign s = a & b; // and assign o2 = s ^ c; // xor endmodule
Las declaraciones de módulo son plantillas a partir de las cuales se crean objetos reales (instanciaciones). Los módulos se instancian dentro de otros módulos, y cada instancia crea un objeto único a partir de la plantilla. La excepción es el módulo de nivel más alto (top) que es su propia instancia.
Sintaxis básica para instanciación
nombre_módulo nombre_instancia_1(lista_conexión_puertos), nombre_instancia_2(lista_conexión_puertos), ...... nombre_instancia_n(lista_conexión_puertos);
Argumentos del módulo
Los puertos del módulo instanciado deben coincidir con los definidos en la plantilla. Esto se especifica de dos formas posibles:
.template_port_name (name_of_wire_connected_to_port)
”Ejemplo
// MODULE DEFINITION module and4( input [3:0] a, b, output [3:0] c ); assign c = a & b; endmodule //and4
// MODULE INSTANTIATIONS wire [3:0] in1, in2; wire [3:0] o1, o2; /* C1 is an instance of module and4 C1 ports referenced by position */ and4 C1 (in1, in2, o1); /* C2 is another instance of and4. C2 ports are referenced to the declaration by name. */ and4 C2 ( . c(o2), . a(in1), . b(in2));
Los módulos no pueden instanciarse dentro de bloques procedurales.
Pueden crearse módulos parametrizados y especificar el valor de los parámetros en cada instancia del módulo. Las puertas primitivas tienen parámetros que han sido predefinidos como retrasos.
Sintaxis básica para instanciación
nombre_módulo #(valor_parámetro_1, valor_parámetro_2, ...) nombre_instancia(lista_conexión_puertos);
Ejemplos
// Definición de módulo module shift_n // usado en módulo test_shift #(parameter N = 2) // el valor por defecto de N es 2 ( input [7:0] it, output [7:0] ot ); assign ot = (it << N); // se desplaza a la izquierda n veces (bits) endmodule //shift_n
El módulo anterior podría usarse (instanciarse) y conectar sus puertos en otros módulo, p.ej.:
// Instanciaciones parametrizadas wire [7:0] in1, ot1, ot2, ot3; // Conexiones implícitas shift_n shft2(in1, ot1); // desplazamiento de 2; por defecto shift_n #(5) shft5(in1, ot3); // desplazamiento de 5; reeemplaza parámetro 2. // Conexiones explícitas shift_n #(.N(3)) shft3(.it(in1), .ot(ot2)); // desplazamiento de 3; reeemplaza parámetro 2.
Otro ejemplo de un contador:
module counter ( input clk, rst, output reg [7:0] count ); parameter tpd_clk_to_count = 1; parameter tpd_reset_to_count = 1; //... endmodule // counter
module test_counter; reg clk_t, rst_t; wire [7:0] count_t; // conexión implícita: counter #(5,10) dut(clk_t, rst_t, count_t); /* conexión explícita: counter #(5,10) dut(.count(count_t), .clk(clk_t), .rst(rst_t)); */ //... endmodule // test_counter
Sintaxis alternativa para instanciación
defparm nombre_instancia.parámetro = valor_parámetro; nombre_módulo nombre_instancia(lista_conexión_puertos);
Ejemplo
// Instanciaciones parametrizadas wire [7:0] in1, ot1, ot2, ot3; defparm shft3.N=3, shift5.N=5; shift_n shft2(in1, ot1), // desplazamiento de 2; por defecto shift_n shft3(in1, ot2); // desplazamiento de 3; reeemplaza parámetro 2. shift_n shft5(in1, ot3); // desplazamiento de 5; reeemplaza parámetro 2.
No todos los sintetizadores permiten la palabra clave defparam
como forma alternativa de cambio de parámetros.
Uso de macros como parámetros
Las macros hacen sustituciones de cadenas y pueden funcionar como parámetros, especialmente útiles para parámetros sin necesidad de pasar por los módulos.
Ejemplo
// Uso de macros como parámetros `define M 8 // ancho de palabra module top wire [`M-1:0] x, y, z; // equivalente a: wire [8-1:0] x,y,z;
El estilo de descripción HDL comportamental o procedural se utiliza para modelar un diseño en el más alto nivel de abstracción de entre los posibles con Verilog HDL. Proporciona gran potencia para diseños complejos, sin embargo, pequeños cambios en los métodos de codificación implican grandes cambios en el hardware generado.
Las sentencias procedurales sólo pueden usarse en procedimientos asociados a bloques always
e initial
.
wire
)reg
)always
, initial
, task
, y function
Tipo de sentencia | Destino (LHS) |
---|---|
Asignación continua | net (vector o escalar) |
selección de bit constante de una net vector | |
selección de una parte constante de una net vector | |
concatenación de cualquiera de la 3 anteriores | |
Asignación procedural | registro (vector o escalar) |
selección de bit constante de un registro vector | |
selección de una parte constante de un registro vector | |
elemento de memoria | |
concatenación de cualquiera de la 4 anteriores |
wire
):assign sal = a & b;
assign c = a & b, d = a | b, e = a ^ b;
wire
a la salida de un módulo, éste también realiza una asignación de este tipo.begin … end
se usan para agrupar varias sentencias en el lugar donde una sentencia se permite sintácticamente.always
e initial
, sentencias condicionales if
, case
, y sentencias de bucle como for
.enable
o disable
.reg
, integer
y parameter
.begin : nombre_bloque reg [msb:lsb] lista_variables_reg; integer [msb:lsb] lista_enteros; parameter [msb:lsb] lista_parametros; ... sentencias ... end
always @(data) begin:named_block integer i; parity = 0; for(i=0; i<11; i=i+1) parity = parity ^ data[i]; end // named_block
Las sentencias if … else
ejecutan una sentencia o bloque de sentencias dependiendo del resultado de la expresión que sigue al if
. Si las expresiones condicionales en todos los if
se evaluan como falsas, entonces se ejecutan, si están presentes, las sentencias del bloque else
.
Pueden incluir tantas sentencias intermedias else if
como se requieran, pero sólo puede habr un solo bloque if
y un solo bloque else
.
Si sólo hay una sentencia en un bloque, las sentencias begin … end
pueden omitirse.
Tanto las sentencias else if
como else
son opcionales. Sin embargo, si todas las posibilidades no están específicamene cubiertas, la síntesis generará latches extra, que no son deseables en la mayoría de los casos.
if (expresion) begin ... sentencias ... end else if (expresion) begin ... sentencias ... end ... más bloques else if ... else begin ... sentencias ... end
if (alu_func == 2’b00) aluout = a + b; else if (alu_func == 2’b01) aluout = a - b; else if (alu_func == 2’b10) aluout = a & b; else // alu_func == 2’b11 aluout = a | b;
if (a == b) // Este if sin else generará begin // latches para 'x' y 'ot' que x = 1; // almacenarán sus valores antiguos si (a != b). ot = 4’b1111; end
always @(intreg or selector) begin if (selector == 2’b00) result_int = intreg[0]; else if (selector == 2’b01) result_int = intreg[1]; else if (selector == 2’b10) result_int = intreg[2]; else if (selector == 2’b11) result_int = intreg[3]; end
La sentencia case
genera una ramificación multicamino basada en la comparación de la expresion con una lista de opciones.
Las sentencias enel bloque default
se ejecutará cuando ninguna de las comparaciones con las opciones se evalue como cierta. Sin defult
, si no hay comparaciones ciertas, la síntesis generará latches no desebles en la mayoría de los casos. Las buenas prácticas de diseño invitaan al hábito de poner la sentencia default
tanto si es necesaria como si no.
Si los valores que se asignan (RHS) en la clausula default
son indeterminados o irrelevantes, se definen como 'X' para que el proceso de minimización lógica los trate como irrelevantes y se ahorre cierta área en el circuito final.
Las opciones del case
pueden ser una simple constante, una expresión, o una lista separada por comas de las mismas.
case (expresion) case_opcion1: begin ... sentencias ... end case_opcion2: begin ... sentencias ... end ... más bloques de opciones case ... default: begin ... sentencias ... end endcase
Ejemplos:
case (alu_ctr) 2’b00: aluout = a + b; 2’b01: aluout = a - b; 2’b10: aluout = a & b; default: aluout = 1’bx; // Tratado como irrelevante endcase // mínima generación de lógica
case ({w, y}) 2’b00: aluout = a + b; //si 'w' e 'y' is 2’b00. 2’b01: aluout = a - b; 2’b10: aluout = a & b; default: $display(“Valores no válidos w,y = %b %b ”, w, y); endcase // muestra un error si w,y son 11, o contienen ‘x’s.
La sentencia casex
sirve para expresar indiferencias en las ramas del case
.
En casex(a)
las opciones de la constante “a” pueden contener Z
, X
o ?
que pueden utilizarse como irrelevantes para la comparación. Con case
la correspondiente variable de simulación tendría que corresponder con un triestado, desconocido u otra señal. En definitiva, 'case' usa X
para comparar con una señal desconocida, mientras que casex
lo trata como irrelevante para reducir lógica.
La sintáxis es similar a la de case
.
input [2:0] sel; casex (sel) 3’b10x: ... 3’bx10: ... 3’bx11: ... 3’b00x: ... default: ... endcase
casex (a) 2’b1x: msb = 1; // msb = 1 si a = 10 o a = 11 // si fuese case(a) sólo coincidiría cuando a=1x default: msb = 0; endcase
casez
es similar a casex
excepto que sólo pueden usarse ?
o Z
(no X
) en las opciones de caso constantes como irrelevantes.
Su uso favorece la simulación, en el caso que aparezca una señal x
, que no coincidirá con un 0 o 1 en las opciones del caso.
La sintáxis es similar a la de case
.
casez (d) 3’b1??: b = 2’b11; // b = 11 si d = 100 o superior 3’b01?: b = 2’b10; // b = 10 si d = 010 o 011 default: b = 2’b00; endcase
always @(<eventos>) begin // algoritmo de procesamiento end
always @(a or b or c or d) // sensible a: a, b, c, d
always @(posedge clk) // sensible a flanco + de clk always @(negedge clk) // sensible a flanco -de clk
// flip-flop always@(posedge clk) begin q = d; end
// multiplexor always @(sel or ent1 or ent2) begin if(!sel) sal = ent1; else sal = ent2; end
// flip-flop con reset asíncrono always @(posedge clk or negedge rstn) begin if(!rstn) q = 1’b0; else q = d; end
// flip-flop con reset síncrono always @(posedge clk) begin if(!rstn) q = 1’b0; else q = d; end
always @(a) // a pasa a valer 1 begin b = a; // b vale 1 c = b; // c vale 1 end
always @(a) // a pasa a valer 1 begin b <= a; // b valdrá 1 al salir c <= b; // c valdrá 0 al salir end
always @(posedge clk) begin if(ce) begin rega = ent1; regb = ent2; end end
always @(intreg or selector) case(selector) 2’b00: result_int = intreg[0]; 2’b01: result_int = intreg[1]; 2’b10: result_int = intreg[2]; 2’b11: result_int = intreg[3]; endcase
module asram ( input [3:0] address, input [7:0] data_in, output reg [7:0] data_out ); reg [7:0] mem[3:0]; always @(address or data_in or we) if(we) mem[address] = data_in; else data_out = mem[address];
module mux_case (source, ce, wrclk, selector, result); input [3:0] source; input ce, wrclk; input [1:0] selector; output result; reg [3:0] intreg; // intreg y result_int son señales locales reg result, result_int; always @(posedge wrclk) begin if(ce) intreg = source; result = result_int; end always @(intreg or selector) case(selector) 2’b00: result_int = intreg[0]; 2’b01: result_int = intreg[1]; 2’b10: result_int = intreg[2]; 2’b11: result_int = intreg[3]; endcase endmodule // mux_case
En Verilog hay bucles for
, while
, repeat
e indefinidos (forever
), poco recomendables para síntesis.
Similar a los bucles for
en C/C++, se utilizan para ejecutar repetidamente una sentencia o bloque de sentencias.
Si el bucle contiene sólo una sentencia pueden omitirse las palabras clave begin … end
.
for (count = valor1; count </<=/>/>= valor2; count = count +/- step) begin ... sentencias ... end
integer i; input clk; reg [4:0] input_signal, result; reg enable; always @(posedge clk) for (i=0; i<5; i=i+1) result[i] = enable & input_signal[i];
El bucle while
ejecuta repetidamente una sentencia o bloque de sentencias hasta que la expresión condicional se evalua como falsa.
Para evitar realimentación combinacional en síntesis, un bucle de este tipo debe poder romperse con una sentencia del tipo @(posedge/negedge clock)
.
En simulación basta con incluir un retardo dentro del bucle.
Si el bucle contiene sólo una sentencia pueden omitirse las palabras clave begin … end
.
while (expresion) begin ... sentencias ... end
while (!overflow) begin @(posedge clk); a = a + 1; end
La sentencia forever
ejecuta un bucle inifinito de una sentencia o bloque de sentencias.
Para evitar realimentación combinacional en síntesis, un bucle de este tipo debe poder romperse con una sentencia del tipo @(posedge/negedge clock)
.
En simulación basta con incluir un retardo dentro del bucle.
Si el bucle contiene sólo una sentencia pueden omitirse las palabras clave begin … end
.
forever begin ... sentencias ... end
forever begin @(posedge clk); // o p.ej.: a = #9 a+1; a = a + 1; end
La sentencia repeat
ejecuta una sentencia o bloque de sentencias un número fijo de veces.
repeat (numero_veces) begin ... sentencias ... end
repeat (2) begin // después de 50, a = 00, #50 a = 2’b00; // después de 100, a = 01, #50 a = 2’b01; // después de 150, a = 00, end // después de 200, a = 01
always @(i) begin o = i; repeat (4'b1011) o = ~o; // o = ~i end
La ejecución de una sentencia disable
termina un bloque y pasa el control a la siguiente sentencia después de ese bloque.
Es como una sentencia break
en C, excepto que puede terminar cualquier bucle, no sólo en el que aparece.
* Sintáxis:
disable nombre_bloque;
always begin @(posedge clk) out = 0; begin: for_ever forever begin: name @(posedge clk) if (reset) disable for_ever; out = out + 1; end end end
En Verilog se pueden definir funciones y tareas (task
), que son procedimientos.
assign y = func(a,b); // Las funciones son valores my_task(a, b, c, d); // Las tareas son sentencias.
Las funciones y tareas se definen dentro de los módulos (dentro de module-endmodule
) con las palabras reservadas function-endfunction
y task-endtask
respectivamente de modo similar a los módulos.
Su uso principal es para simulación y creación de testbenches. No es aconsejable utilizarlas para síntesis salvo que sus límites o rango sean fijos tanto en datos como en iteraciones de bucles.
El valor de retorno de una función se puede utilizar directamente en una expresión sin asignarla antes a una variable de tipo registro o cable, utilizando la llamada de función como uno de los operandos, teniendo en cuenta el ancho en bits de dicho valor.
Ejemplos
// Una función de utilidad para calcular el logaritmo en base2 function integer clogb2; input [31:0] value; for (clogb2=0; value>0; clogb2=clogb2+1) value = value>>1; endfunction //clogb2
// Multiplicador secuencial suma-desplazamiento function [15:0] mult; input [7:0] a, b; reg [15:0] r; integer i; begin if (a[0] == 1) r = b; else r = 0; for (i=1; i<7; i=i+1) begin if (a[i] == 1) r = r + b << 1; end mult = r; end endfunction
// Oscilador !! no síntesis task T; inout io; output o; begin o = io; io = ~io; end endtask
Hay 2 formas de describir circuitos combinacionales:
assign
);always
.Por ejemplo, un multiplexor:
assign sal = (sel) ? ent1 : ent0;
o:
always @(sel or ent0 or ent1) if (sel) sal = ent1; else sal = ent0;
El uso de asignaciones continuas es recomendable cuando se trata de un combinacional sencillo del que se conoce la ecuación, una operación aritmética simple o una asignación condicional en la que se puede compactar la descripción.
Hay que recordar que las asignaciones de este tipo se realizan sobre wires
. Por lo demás, no hay que tomar “precauciones especiales”.
Ejemplos:
assign a = b + c; assign sal = (a & b) | (c & d); assign sal = (sel) ? (a + b) : (a – b); assign sal = ena_i & sal_i, saln = ena_i & (!sal_i), ena = ena_i;
El uso de bloques always
está indicado cuando el combinacional tiene una cierta complejidad.
Hay que tener en cuenta lo siguiente:
reg
.if
y case
, y utilizando las operaciones aritméticas básicas y los operadores lógicos. Conviene utilizar siempre “patrones” probados y sencillos, es decir, no se trata de describir algoritmos de procesamiento utilizando bucles, funciones, etc, sino de repetir fórmulas probadas, aunque puedan resultar, a veces, menos compactas. De esta manera, se evitarán problemas.wire [3:0] dato_in; wire enable; reg [1:0] dato_out; always @(enable or dato_in) if (enable) casex (dato_in) 4'bxxx1: dato_out = 2'b00; 4'bxx10: dato_out = 2'b01; 4'bx100: dato_out = 2'b10; 4'b1000: dato_out = 2'b11; endcase else // si no lo ponemos => latch dato_out = 2'b00;
Para expresar condiciones de indiferencia en las salidas, se utiliza el valor “X”.
always @(ent) casex (ent) 4'bxx01: sal = 4'b1110; 4'bx010: sal = 4'b1010; default: sal = 4'hx; endcase
En las sentencias case
es importante incluir la rama default
cuando no se definen todas las combinaciones de entrada.
always @(ent) casex (ent) 4'bxx01: sal = 4'bxx10; 4'bx010: sal = 4'b10xx; default: sal = 4'b0000; endcase
Intentar simplificar la asignación de valores a salidas puede llevar a cometer errores.
always @(ent) casex (ent) 4'bxx01: sal = 4'b0010; 4'bx010: sal = 4'b1000; default: sal = 4'b0000; endcase
¡Ojo!No es equivalente a esta descripción:
always @(ent) casex (ent) 4'bxx01: sal[1:0] = 2'b10; 4'bx010: sal[3:2] = 2'b10; default: sal = 4'b0000; endcase // LATCH!
Hay una técnica para evitar este tipo de errores (poco utilizada):
always @(ent) begin sal = 4'b0000; casex (ent) 4'bxx01: sal[1:0] = 2'b10; 4'bx010: sal[3:2] = 2'b10; default: sal = 4'b0000; endcase end
En la descripción de circuitos aritméticos, el lenguaje considera que el bit más significativo de los objetos es el primero que aparece en la definición del rango.
reg [3:0] a; reg [0:3] b; wire[3:0] c; // si a vale 4'b0001 y b vale 4'b1100 assign c= a + b; // c valdrá 4'hd
Es muy aconsejable definir siempre los rangos con el mismo criterio (preferiblemente descendentes).
Además, Verilog considera que los vectores representan números binarios. Esto puede corregirse con la directiva ‘signed
; afecta a las operaciones de comparación.
// a = 4'b1000; b = 4'b0001; c = a > b; // c vale 1 c = 'signed a > 'signed b; // c vale 0
En ocasiones hay que describir buffer tri-estado. Se hace de la siguiente manera:
assign sal = (enable) ? sal_i : 1'hz;
Si se trata de un pin bidireccional:
inout pin_bidir; // pin bidireccional (wire) wire ent_bidir; // el nodo interno de entrada reg sal_i; // el nodo interno de salida assign pin_bidir = (enable) ? sal_i : 1'hz; assign ent_bidir = pin_bidir;
A la hora de codificar el hardware hay que prever cómo va a ser sintetizado. Un detalle importante que hay que tener presente es si el sintetizador que se va a utilizar es capaz de detectar recursos compartidos. Por ejemplo:
s1 = a + b - c; s2 = d - c + b; //Esto se puede hacer con dos sumadores y un restador
Lo mejor es codificar para que el resultado no dependa de las peculiaridades del sintetizador.
module sum4 (a, b, c, d, e, sum1, sum2, sum3); input [31:0] a, b, c, d, e; output [31:0] sum1, sum2, sum3; assign sum1 = a + b + c; assign sum2 = a + b + d; assign sum3 = a + b + e; // Genera 6 sumadores endmodule
module sum4 (a, b, c, d, e, sum1, sum2, sum3); input [31:0] a, b, c, d, e; output [31:0] sum1, sum2, sum3; wire [31:0] a_plus_b; assign a_plus_b = a + b; assign sum1 = a_plus_b + c; assign sum2 = a_plus_b + d; assign sum3 = a_plus_b + e; // Genera 4 sumadores endmodule
La forma de codificar también puede repercutir en la velocidad de operación.
Los circuitos secuenciales se modelan siempre utilizando bloques always
sensibles al flanco de reloj y, si es necesario, a una señal asíncrona de inicialización o reset.
always @(posedge clk) // flanco positivo always @(negedge clk) // flanco negativo
El modo que se muestra es el más utilizado para describir reset asíncronos, aunque resulte “algo chocante”.
always @(posedge clk or posedge reset) // reset alto always @(posedge clk or negedge reset) // reset bajo
Si el circuito tiene reset asíncrono, primero se establece la operación del reset y luego, alternativamente, la operación síncrona del circuito.
always @(posedge clk or posedge reset) // reset alto begin if (reset) // operación de reset else // operación síncrona end
La condición que consulta el nivel de la señal de reset, la “transforma” en señal activa “por nivel”, aunque el bloque sea sensible a su flanco, porque “elimina” el funcionamiento síncrono cuando está activa.
Ejemplo: Circuito síncrono sin reset
module dffe(clk, a, b, c, sel); input a, b, sel, clk; output c; reg c; always @(posedge clk) if (sel) c = a; else c = b; endmodule
Ejemplo: Circuito síncrono con reset asíncrono y clock-enable
module dffr (input clk, reset, a, ce, output reg c); always @(posedge clk or negedge reset) if (!reset) c = 0; else if (ce) c = a; endmodule // dffr
Ejemplo: Circuito síncrono con reset asíncrono y síncrono
module dffrs (input clk, reset, reset_syn, a, ce output reg c); always @(posedge clk or negedge reset) if (!reset) c = 0; else begin if (reset_syn) // reset síncrono activo a nivel alto c = 0; else if (ce) c = a; end endmodule // dffrs
Al igual que en el caso de los circuitos combinacionales, la descripción de la mayor parte de los secuenciales puede realizarse combinando sentencias case
, if
y operadores aritméticos y lógicos.
En la descripción de circuitos secuenciales hay que tener cuidado con las asignaciones blocking y non-blocking. En general, es recomendable que todas sean non-blocking (<=
).
Para describir autómatas (una de las tareas que más frecuentemente hay que abordar durante el desarrollo de un sistema digital), se pueden utilizar hasta tres estilos distintos:
always
síncrono,always
síncrono y otro combinacional,El primero, que por lo demás es el más frecuentemente utilizado, sólo sirve para autómatas de Moore o Mealy con las salidas registradas. El tercero viene en todos los manuales de estilo de los sintetizadores, pero nadie (que yo conozca) lo utiliza. El segundo es apropiado si (es raro) se quiere desarrollar un autómata de Mealy (puro y duro) o se está implementando uno de Moore a partir del dibujo del diagrama de estados.
Desde el punto de vista metodológico, quizás el segundo sea el mejor, pero es un poco incómodo de utilizar si se va desarrollando el autómata al tiempo que se escribe el código, de modo que el más utilizado es el primero.
En el primer método, con un único bloque always
, se describe, mediante una sentencia case, donde cada rama es un estado, las transiciones y el valor de las salidas en cada estado. La estructura genérica de este tipo de autómatas es:
always @(posedge clk or negedge reset) if (!reset) begin estado <= estado_inicial; salidas <= valor_estado_inicial; end else case (estado) s0: case (entradas) e1: estado <= estado_i; salidas <= salidas_i; e2: ... s1: case (entradas) ... sn: ...
Ejemplo: Autómata simple con variable estado igual a las salidas
module fsm1 (input clk, reset, c, output reg [1:0] status); always @(posedge clk or negedge reset) if (!reset) status = 0; else case (status) 0: if (c) status = 1; 1: status = 2; 2: if (c) status = 1; else status = 0; endcase endmodule // fsm1
Ejemplo: Autómata de Mealy con salidas registradas
module fsm2 (input clk, reset, c, output reg [3:0] salida); reg [1:0] status; // estado always @(posedge clk or negedge reset) if (!reset) begin status <= 0; salida <= 3'b010; end else case (status) 0: if (c) begin status <= 1; salida <= 3'b100; end 1: begin status <= 2; salida <= 3'b111; end 2: if (c) begin status <= 1; salida <= 3'b100; end else begin status <= 0; salida <= 3'b010; end endcase endmodule // fsm2
Ejemplo: Autómata de Moore con dos bloques always
module fsm3 (input clk, reset, c, output reg [3:0] salida); reg [1:0] status; // estado // bloque secuencial always @(posedge clk or negedge reset) if (!reset) status <= 0; else case (status) 0: if (c) status <= 1; 1: status <= 2; 2: if (c) status <= 1; endcase // bloque combinacional always @(status) case (status) 0: salida <= 3'b010; 1: salida <= 3'b100; 2: salida <= 3'b111; default: salida <= 3'bxxx; endcase endmodule // fsm3
Si fuera de Mealy, con las salidas sin registrar, el segundo proceso sería sensible al estado y a las entradas.
En el diseño de autómatas hay que tener en cuenta algunas consideraciones adicionales:
reg
tasks
, a modo de macros, para compactar el código y facilitar su comprensión e implementación.Los módulos estructurales Verilog son muy fáciles de elaborar con copy&paste a partir de la declaración de los módulos insertados, tomando una precaución:
module control (input ent_ctrl, output sal_ctrl); ... module controlado (input sal_ctrl, input sal_ctrldo); ... module top (input ent_ctrl, output sal_ctrldo); ... wire sal_ctrl; // instanciación control U0 (ent_ctrl, sal_ctrl); controlado U1 (sal_ctrl, sal_ctrldo);
Si el módulo tiene definidos parámetros, se puede particularizar su valor para cada instancia: entre paréntesis con #
, o dejar que tomen el valor de la declaración si no se pone nada.
module sumador #(parameter SIZE = 4) (input [SIZE-1:0] a, b); ... module top (...); ... wire [7:0] a, b, sal; ... // instanciación sumador #(8) U0 (a, b, sal); ...
Las memorias se modelan en Verilog como arrays de 2 dimensiones mediante variables tipo registros a las que puede acceder por palabras. Si se desea obtener uno o varios bits concretos debe asignarse la salida a un registro o cable y seleccionar la parte deseada de esta nueva variable.
Sintaxis
reg [wordsize:0] array [0:arraysize]
Ejemplo
reg [7:0] mem [0:255];
En Verilog no hay arrays multi-dimensionales, ni es posible definir ni conectar puertos de tipo arrays, estas facilidades están incluidas en SystemVerilog.
Las memorias pueden inicializarse de forma secuencial, p.ej.:
reg [15:0] mem[0:255]; integer i; initial for (i=0; i<256; i=i+1) mem[i] = 0; // initial each location with 0
O a partir del contenido de ficheros ASCII, que definen su contenido numérico en base binaria o hexadecimal, mediante system tasks $readmemb
y $readmemh
respectivamente.
El archivo de datos consta de direcciones y datos. Una dirección escrita en hexadecimal como @hhh…
indica la dirección de la primera palabra en un bloque de datos. A continuación aparecerán las palabras binarias de datos separadas por espacios en blanco. Los bits binarios legales son 0 1 x X z Z _
. Los datos no incluidos en el
fichero tomarán valores xxx…
. Los datos pueden ubicarse en bloques no contiguos si una dirección precede a cada bloque. Si no se especifica ninguna dirección al comienzo del archivo, se asume @000
para la primera palabra de datos. Los comentarios también se permiten en los archivos de datos.
Sintaxis
readmemb("file_name", array_name); readmemb("file_name", array_name, start_addr); readmemb("file_name", array_name, start_addr, finish_addr); readmemh("file_name", array_name); // start_addr and finish_addr son opcionales
Si se le da start_addr, la matriz de memoria se llenará comenzando en esa dirección y continuará hasta finish_addr (o
final de la matriz). Debe tener start_addr < @hhh…
, la dirección inicial en el archivo.
Ejemplo
reg [7:0] memry [0:31]; // memoria de 32 bytes wire [7:0] memwrd; wire x; initial begin // Inicializa el contenido de la memoria desde archivo $readmemb("init.dat", memry, 8); // las palabras 8 y 9 no están en el archivo y por defecto serán x end // Ejemplos de operaciones tras la inicilaización: // Extraer la última palabra en memoria assign memwrd= memry[31]; // Extraer el bit más significativo en la palabra 31 assign x= memwrd[7]; ------------------------------- file init.dat------------------------------- // Como start_addr=8 en memry[0:9] se almacenarán xxxxxxxx @00A // 10101100 11110000 1x000x11 11110101 01011010 01001100 XxxxZzzz 00000000 @01E // 5'h1E = 5'd30, los guiones bajos dan mejor legilibilidad 1100_1010 00011_0001
Ejemplo
module rom_32x4 (input [4:0] addr, output reg [3:0] dout); always @(addr) case (addr) 0:dout = 4'b1110; 1:dout = 4'b0100; 2:dout = 4'b1110; 3:dout = 4'b1001; 4:dout = 4'b1111; 5:dout = 4'b0011; 6:dout = 4'b1000; 7:dout = 4'b0001; 8:dout = 4'b0110; 9:dout = 4'b0001; 10:dout = 4'b1100; 11:dout = 4'b0000; 12:dout = 4'b0110; 13:dout = 4'b0000; 14:dout = 4'b0100; 15:dout = 4'b0110; 16:dout = 4'b1110; 17:dout = 4'b0100; 18:dout = 4'b1110; 19:dout = 4'b1001; 20:dout = 4'b1111; 21:dout = 4'b0011; 22:dout = 4'b1000; 23:dout = 4'b0001; 24:dout = 4'b0110; 25:dout = 4'b0001; 26:dout = 4'b1100; 27:dout = 4'b0000; 28:dout = 4'b0110; 29:dout = 4'b0000; 30:dout = 4'b0100; 31:dout = 4'b1111; endcase endmodule // rom_32x4
Ejemplo de memoria SRAM-1P
module sync_ram_singleport #(parameter addr_width = 8, parameter data_width = 8) (input clk, we, input [addr_width - 1:0] addr, input [data_width - 1:0] data_in, output[data_width - 1:0] data_out); reg [addr_width - 1:0] addri; reg [data_width - 1:0] mem [(32’b1<<addr_width):0]; always @(posedge clk) begin if (we) mem[addr] = data_in; addri = addr; end assign data_out = mem[addri]; endmodule //sram1p
Ejemplo de memoria SRAM-2P
module sync_ram_dualport #(parameter data_width = 16, parameter addr_width = 32) (input clk_in, clk_out, we, input [addr_width - 1:0] addr_in, input [addr_width - 1:0] addr_out, input [data_width - 1:0] data_in, output reg [data_width - 1:0] data_out); reg [data_width - 1:0] mem [2^(addr_width -1):0]; always @(posedge clk_in) begin if (we)mem[addr_in] <= data_in; end always @(posedge clk_out) begin data_out <= mem[addr_out]; end endmodule //sram2p
La generación de bloques es una facilidad del lenguaje para la generación de instanciaciones en bucle. Se utilizan para ello las palabras clave generate
y endgenerate
que acotan el ámbito de generación apoyados en alguna variable declarada mediante la palabra clave genvar
.
Las instanciaciones generadas pueden ser de uno o más de los siguientes tipos:
initial
y always
Existen 3 métodos para crear declaraciones de generar:
Ejemplos
// a bit-wise XOR of two N-bit bases module bitwise_xor #(parameter N=32) //bus width ( output [N-1:0] out, input [N-1:0] i0, i1 ); genvar j; // temp loop variable; does not exist during simulation generate for (j=0;j<N;j=j+1) begin :xor_loop xor g1(out[j],i0[j],i1[j]); end //xor_loop endgenerate // alternative method /* reg [N-1 :0] out; generate for (j=0;j<N;j=j+1) begin :bit always @(i0[j] or i1[j]) out [j]=i0[j]^i1[j]; end endgenerate */ endmodule //bitwise_xor
a = #5 b; // Equivalente a: begin temp = b; #5 a = temp; end
a = @(posedge clk) b; // Equivalente a: begin temp = b; @(posedge clk) a = temp; end
a = repeat (3) @(posedge clk) b; // Equivalente a: begin temp = b; @(posedge clk); @(posedge clk); @(posedge clk) a = temp; end
Para simular los modelos hardware Verilog hay que ejecutar, en un simulador Verilog, un banco de pruebas o TestBench, es un módulo donde se instancia el modelo que se desea probar (diseño o unidad bajo test) y se le aplican una serie de vectores de test para poder analizar su comportamiento y comprobar su correcto funcionamiento.
Se invocan además una serie de tareas del sistema para visualizar/formatear los resultados de la simulación.
Sintaxis
module test_bench; ... // Declaración de estímulos ... // Declaración de salidas ... // Instanciación del modelo, conectado a // estímulos y salidas ... // Definición de estímulos ... endmodule
Ejemplo
module ffd_tb; reg clk, reset, a, ce; // Estímulos wire c; // Salida dffr dut (clk, reset, a, ce, c); // Modelo bajo pruebas always #100 clk = ~clk; // Reloj initial begin // Inicialización reset = 1'b1; clk = 1'b0; a = 1'b0; ce = 1'b0; @(negedge clk) reset = 1'b0; // reset @(negedge clk) reset = 1'b1; @(negedge clk) ce = 1'b1; // pongo el ce a = 1'b1; @(negedge clk); @(posedge clk) #10 ce = 1'b0; // cambio el dato @(posedge clk) #1000 $finish; // se acabó end endmodule // ffd_tb
`define MAX 100000
`ifdef SYNTH <verilog code> `endif
<verilog code>
`include <verilog file>
`timescale 1ns / 10ps
`timescale
unidad_de_tiempo_de_referencia / precisión
`timescale
por diseño.s
= segundosms
= milisegundosus
= microsegundosns
= nanosegundosps
= picosegundosfs
= femtosegundos
Ejemplo Uso de `timescale
`timescale 100ns/1ns module ffd (input clk, reset, d, output reg q); always @(posedge clk or negedge reset) if (reset==0) q <= 0; else q <= d; endmodule //ffd
`define
macro_de_texto [valor] `include
path/archivo
Ejemplo Uso de `define
e `include
// Autómata de control lectura/escritura // (Memory Manager) // status `define ST0 2 `define ini0 2'b00 `define esc1 2'b01 `define lec1 2'b10 // salidas `define ackfrommgr_activo 1'b1 `define ackfrommgr_inactivo 1'b1 `define ce_activo 1'b1 `define ce_inactivo 1'b0 `define readext 1'b1 `define writeext 1'b0 `define enack_activo 1'b1 `define enack_inactivo 1'b0 `define enreg_activo 1'b1 `define enreg_inactivo 1'b0 ... `include "../DEFINES/defs_mem_mgr.v" ... case (status) `ini0: begin en_ack <= `enack_inactivo; canal_dec_reg <= canal_dec; canal_reg <= canal; if (canal_dec == `canal_ninguno) status <= `ini0; else if (canal[`log2NC-1]==`ST0) status <= `lec1; else status <= `esc1; end `esc1: begin status <= `ini0; ce <= `ce_inactivo; en_ack <= `enack_activo; end `lec1: begin status <= `ini0; ce <= `ce_inactivo; en_reg <= `enreg_inactivo; en_ack <= `enack_activo; end endcase
`ifdef
macro_de_texto
........................
`else
........................
`endif
Ejemplo Uso de ifdef
`ifdef dimm4 $display("INFO(TEST): Using DIMM4 (4 banks, 9 b/col, 12 b/row)"); `else $display("INFO(TEST): Using DIMM2 (2 banks, 8 b/col, 11 b/row)"); `endif
Las “System Tasks” permiten realizar funciones complementarias para la simulación como visualización o volcado de valores de salida por la consola del simulador o a archivos.
Las tareas que extraen datos, como $monitor
necesitan estar en bloques initial
o always
.
Estos comandos tienen la misma sintaxis y muestran texto en la pantalla durante la simulación. $display
y $strobe
muestran salida cada vez que se ejecutan, mientras que $monitor
aparece cada vez que cambia uno de sus parámetros. La diferencia entre $display
y $strobe
es que $strobe
muestra los parámetros al final de la unidad de tiempo de simulación actual en lugar de exactamente donde se ejecuta.
La cadena de formato es similar a C/C++: %h
(hexadecimal), %b
(binario), %c
(carácter), %s
(cadena) y %t
(tiempo), %m
(nivel de jerarquía). %5d, %5b, etc. darían exactamente 5 espacios para el número en lugar del espacio necesario.
Sintaxis
$display("texto_con_formato", par_1, par_2, ... ); $strobe("texto_con_formato", par_1, par_2, ... ); $monitor("texto_con_formato", par_1, par_2, ... ); $displayb // similar al anterior pero por defecto en binario.. $strobeh // similar al anterior pero por defecto en hex.. $monitoro (// similar al anterior pero por defecto en octal..
Ejemplo
initial begin // c below is in submodule submod1 $displayh (b, d, submod1.c); //No format, display in hex $monitor ("time=%t, d=%h, c=%b", $time, a, submod1.c); end
Estos devuelven el tiempo de simulación actual como un entero de 64 bits, un entero de 32 bits y un número real, respectivamente.
Sintaxis
variable_integer=$time; // tiempo de simulación
$reset
restablece la simulación al tiempo 0; $stop
detiene el simulador y lo pone en el modo interactivo donde
el usuario puede introducir comandos; $finish
sale del simulador de nuevo al sistema operativo.
Sintaxis
$finish;
$stop;
Pone un valor particular en un elemento net, sobrescribiendo lo que se puso allí desde el “circuito”. Bueno para bancos de pruebas.
Sintaxis
$deposit(nombre_net, valor);
Ejemplo
$deposit (b, 1'b0); $deposit (outp, 4'b001x);// outp es un bus 4-bit
Genera un entero aleatorio cada vez que se invoca. Si se necesita puede forzase la secuencia a partir de una semilla dada o tomar por defecto el reloj del sistema.
Sintaxis
xzz = $random[(integer)];
Ejemplo
reg [3:0] xyz; initial begin xyz= $random (7); // Seed the generator so number // sequence will repeat if simulation is restarted. forever xyz = #20 $random; // The 4 lsb bits of the random integers will transfer into the // xyz. Thus xyz will be a random integer 0 ≤ xyz ≤ 15.
Volcado de variables para la visualización en herramientas de simulación gráficas.
Sintaxis estándar (VCD)
$dumpfile("nombre_de_archivo");
$dumpvars([profundidad, módulo]);
$dumpon;
$dumpoff;
Sintaxis de Cadence
$shm_open("nombre_de_archivo");
$shm_probe(módulo, "profundidad");
$shm_close("nombre_de_archivo");
Escritura genérica en archivos.
Sintaxis
identificador $fopen("nombre_de_archivo");
$fclose(identificador);
$fdisplay(identificador, ("texto_con_formato", señal, señal, …);
Ejemplo
module testbench: reg [15:0]a; reg clk; integer hand1; initial begin hand1=$fopen(“datastuff.txt”); forever @(posedge clk) begin $fstrobe (hand1, “time=%5t, a=%h, c=%b”, $time, a, submod1.c);. end // Never put statements after a forever block. end initial begin clk=0; a=8’h2b; forever #5 clk=~clk; end // Never put statements after a forever block initial begin a=a+8; #3000 $fclose (hand1); // Close the file $finish; end submod submod1(a, clk); // with internal variable c. endmodule -------------------------------- Output --------------------------- time= 5, a=2b, c=0 time= 10, a=2c, c=1
Las puertas lógicas “primitivas” son parte del lenguaje de Verilog. Se pueden especificar dos propiedades:
drive_strength
(intensidad en las salidas de la puerta). La salida más fuerte es una conexión directa a una fuente, después una conexión a través de un transistor conductor, y después un pull-up/down resistivo. Normalmente no se especifica esta propiedad, en cuyo caso las intensidades predeterminadas son strong1
y strong0
. delay
(retardos de propagación). Si no se especifica ningún retardo, entonces la puerta no tiene retardo de propagación; si se especifican dos retardos, el primero representa el retardo de la subida, el segundo el retardo de la caída; si sólo se especifica un retraso, entonces el de subida y caida son iguales. Los retrasos se modelan en simulación, pero se ignoran en síntesis. Este método de especificación de retardo es un caso especial de parámetros predefinidos en módulos parametrizados para el caso de las puertas primitivas.
Implementan las puertas lógicas básicas. Tienen una salida y una o más entradas. En la sintaxis de instanciación de la puerta que se muestra a continuación, GATE representa una de las palabras clave and
, nand
, or
, nor
, xor
, xnor
.
Sintaxis
GATE (drive_strength) #(delays) instance_name1(output, input_1, input_2, ..., input_N), instance_name2(outp, in1, in2, ..., inN);
donde delays se corresponde con una de las siguientes especificaciones:
Ejemplo
and c1(o,a, b,c, d); // puerta AND de 4-entradas llamada c1 y c2(p, f, g); // puerta AND de 2-entradas llamada c2. or #(4, 3) ig (o, a, b); /* puerta OR llamada ig (nombre de instancia); tiempo de subida = 4, tiempo de bajada = 3 */ xor #(5) xor1 (a, b, c); // a = b XOR c después de 5 unidades de tiempo xor (pull1, strong0) #5 (a, b, c); /* puerta idéntica a la anterior con pull-up de intensidad pull1 y pull-down de intensidad strong0. */
Estos implementan buffers e inversores, respectivamente. Tienen una entrada y una o más salidas. En la sintaxis de la instancia de puerta que se muestra a continuación, GATE representa la palabra clave buf
o not
Sintaxis
GATE (drive_strength) #(delays) instance_name1(output_1, output_2, ..., output_n, input), instance_name2(out1, out2, ..., outN, in);
Ejemplo
not #(5) not_1 (a, c); // a = NOT c después de 5 unidades de tiempo buf c1 (o, p, q, r, in); // buffers de 4-salidas y 2-salidas respectivamente c2 (p, f, g);
Implementan buffers e inversores de 3 estados, propagan Z
(triestado o alta impedancia) si su señal de control no está asertada.
Pueden tener tres especificaciones de retardo: un tiempo de subida, un tiempo de caída, y un tiempo para entrar en triestado.
Ejemplo
bufif0 #(5) not_1 (BUS, A, CTRL); /* BUS = A 5 unidades de tiempo después de bajar CTRL */ notif1 #(3,4,6) c1 (bus, a, b, cntr); /* el bus pasa a triestado 6 unidades de tiempo después de bajar CTRL */
Técnica de modelado con la que el usuario puede diseñar nuevos elementos primitivos a modo de módulos:
Reglas
Ejemplo
primitive carry(carryOut, carryIn, aIn, bIn); output carryOut; input carryIn, aIn, bIn; table 0 00 : 0; 0 01 : 0; 0 10 : 0; 0 11 : 1; 1 00 : 0; 1 01 : 1; 1 10 : 1; 1 11 : 1; endtable endprimitive
Recomendaciones
Eficiencia en simulación
Eficiencia de síntesis