Los operadores de Verilog son símbolos que describen operaciones aritméticas, comparaciones y transformaciones de bits que el sintetizador debe convertir en hardware.
Hemos visto como usar puertas lógicas (AND, OR, XOR). Pero no queremos estar pensando en bits individuales todo el tiempo. Queremos decir: “Si el sensor A es mayor que 100, enciende la alarma”.
Afortunadamente, Verilog nos permite trabajar con un nivel de abstracción más alto. Entiende los números y sabe operar con ellos.
Hoy vamos a ver las matemáticas dentro de la FPGA. Y ojo, porque aunque los símbolos se parecen a los de C++, el coste en hardware es muy distinto.
Operadores aritméticos
Verilog soporta las operaciones matemáticas básicas. Cuando el sintetizador ve estos símbolos, intenta construir el circuito digital equivalente (sumadores, restadores, multiplicadores…).
| Operador | Descripción | Ejemplo (A=7, B=2) |
|---|---|---|
+ | Suma | A + B -> 9 |
- | Resta | A - B -> 5 |
* | Multiplicación | A * B -> 14 |
/ | División | A / B -> 3 (Entera) |
% | Módulo (Resto) | A % B -> 1 |
** | Potencia | 2 ** 3 -> 8 |
El coste de la aritmética en hardware
Una división también es más costosa que una suma en un procesador, pero en una FPGA la diferencia de área y retardo puede ser mucho mayor.
- Suma (
+) y Resta (-): Son “baratas”. Se implementan con lógica de acarreo (Carry logic) que las FPGAs traen optimizada. - Multiplicación (
*): Es “cara”. Consume bloques dedicados DSP. Si tu FPGA tiene pocos DSPs, se te acabarán rápido. - División (
/) y Módulo (%): Conviene evitarlos con operandos variables salvo que el coste esté justificado.
- Un circuito divisor combinacional es enorme y lentísimo.
- Si el divisor es una potencia de 2 (ej:
/ 4), el sintetizador es listo y lo convierte en un desplazamiento de bits (gratis). - Si el divisor es variable (ej:
A / B), evita usar el operador/. Necesitarás diseñar un módulo divisor secuencial que tarde varios ciclos de reloj.
Siempre que puedas, multiplica y divide por potencias de 2. Es tan simple como desplazar cables a izquierda o derecha.
Operadores relacionales (comparadores)
Sirven para comparar dos valores. El resultado de estas operaciones es siempre un solo bit: 1 (Verdadero) o 0 (Falso).
Son idénticos a C/C++:
| Operador | Descripción |
|---|---|
> | Mayor que |
< | Menor que |
>= | Mayor o igual |
<= | Menor o igual |
== | Igualdad lógica |
!= | Desigualdad lógica |
wire es_mayor;
// Si a (10) es mayor que b (5), es_mayor valdrá 1
assign es_mayor = (a > b); El operador == compara igualdad lógica, pero devuelve X cuando no puede decidir por la presencia de valores X o Z.
La variante === (case equality) trata X y Z como valores comparables y se utiliza principalmente en testbenches.
Operadores lógicos y bitwise
Este es el error número 1 de los principiantes. Confundir los operadores lógicos con los operadores bit a bit que vimos en el artículo anterior.
- Bitwise (
&,|,~): Operan bit por bit y devuelven un vector del mismo tamaño. - Lógicos (
&&,||,!): Tratan todo el vector como un valor booleano (0 es Falso, cualquier otra cosa es Verdadero) y devuelven un solo bit.
Veamos la diferencia con un ejemplo claro:
Supongamos A = 4'b1010 y B = 4'b0000.
| Operación | Tipo | Resultado | Explicación |
|---|---|---|---|
A & B | Bitwise | 4'b0000 | 1&0, 0&0, 1&0, 0&0. |
A && B | Lógico | 1'b0 (False) | ¿Es A verdadero? Sí. ¿Es B verdadero? No. -> Falso. |
~A | Bitwise | 4'b0101 | Invierte cada bit. |
!A | Lógico | 1'b0 (False) | A es “verdadero”, así que NOT A es falso. |
En sentencias if, solemos usar los lógicos (if (a && b)).
En asignaciones de señales, solemos usar los bitwise (assign y = a & b).
Operadores de desplazamiento (shift)
Son utilísimos para multiplicar o dividir por potencias de 2 y para manipulación de bits.
<<: Desplazamiento a la izquierda (Rellena con ceros). Multiplica por 2.>>: Desplazamiento lógico a la derecha (rellena con ceros). Divide un valor sin signo por 2.
assign y = a << 2; // Equivalente a multiplicar 'a' por 4.Operador de concatenación { }
Este es exclusivo de los HDLs y es maravilloso. Nos permite unir bits o buses sueltos para formar un bus más grande. Se usan las llaves { , }.
wire a; // 1 bit
wire [3:0] b; // 4 bits
wire [4:0] bus_c; // 5 bits
// Unimos 'a' (parte alta) con 'b' (parte baja)
assign bus_c = {a, b}; También podemos usarlo para replicar bits usando la sintaxis {veces{valor}}.
// Queremos extender un número de 4 bits a 8 bits rellenando con ceros
assign byte_completo = { 4'b0000, dato_4bits };
// O rellenando con unos repetidos 4 veces
assign byte_unos = { {4{1'b1}}, dato_4bits }; Ejemplo integrador: un comparador con umbral
Vamos a usar aritmética y comparaciones. Haremos un módulo que active una salida si la entrada supera un umbral. No es una histéresis real, porque no conserva estado ni utiliza dos umbrales distintos.
module comparador_simple (
input wire [7:0] sensor, // Valor de 0 a 255
input wire [7:0] umbral, // Valor de referencia
output wire alarma_critica,
output wire aviso
);
// 1. Relacional: ¿Es mayor estricto?
assign aviso = (sensor > umbral);
// 2. Aritmético + Relacional
// Alarma crítica si supera el umbral + 10 unidades
assign alarma_critica = (sensor > (umbral + 8'd10));
endmodule