Concurrencia

El Concepto Clave: Paralelismo Real vs. Simulación Secuencial

El comentario más importante sobre la concurrencia en Verilog/SystemVerilog es entender que, aunque estás describiendo un hardware que es inherentemente paralelo, el simulador que ejecuta tu código es un programa de software que funciona de manera secuencial.

Esta diferencia es la fuente de los problemas más comunes, como las condiciones de carrera (race conditions), y la razón por la que existen las asignaciones blocking (=) y non-blocking (<=).

Analogía para entenderlo

Imagina que estás describiendo una coreografía de baile con 100 bailarines. En la vida real, todos los bailarines realizan su quinto paso exactamente en el mismo instante. Eso es paralelismo real.

Ahora, imagina que el director de la coreografía (el simulador) solo puede dar instrucciones a un bailarín a la vez. Para simular que todos se mueven al mismo tiempo, el director debe seguir un proceso:

  1. Fase de Evaluación (RHS): El director «mira» la posición actual de todos los bailarines y calcula cuál será el próximo movimiento de cada uno. Apunta en su libreta: «Bailarín 1 irá a la posición X, Bailarín 2 a la posición Y…», etc. Lo hace para los 100 bailarines sin que ninguno se mueva todavía.
  2. Fase de Actualización (LHS): Una vez que ha calculado todos los movimientos futuros, grita «¡AHORA!» y todos los bailarines se mueven a la nueva posición que él calculó.

¿Cómo se refleja esto en el código?

  • Las asignaciones non-blocking (<=) modelan este comportamiento correctamente. Son la forma de decirle al simulador: «Calcula todos los resultados de la derecha ahora, pero no actualices las variables de la izquierda hasta que hayas terminado de calcularlos todos». Esto es esencial para modelar registros y lógica secuencial, donde todos los flip-flops se actualizan simultáneamente con el flanco del reloj.
  • Las asignaciones blocking (=), por otro lado, son como si el director le dijera al Bailarín 1 que se mueva, luego al Bailarín 2, y así sucesivamente. El movimiento del Bailarín 2 dependerá de la nueva posición del Bailarín 1, no de la original. Esto rompe la ilusión de paralelismo y es útil para describir lógica combinacional, donde las señales se propagan en cascada.

En resumen, el concepto más crítico es que debes usar non-blocking (<=) para modelar la concurrencia real del hardware síncrono, asegurando que el simulador secuencial se comporte como el hardware paralelo que estás diseñando. Fallar en esto es la causa número uno de que una simulación no coincida con el comportamiento del hardware real.

Y para terminar una anécdota:

el Verilog original fue creado sin asignaciones non-blocking (<=).

Este no es un detalle menor; es una pieza clave de su historia que explica por qué el uso correcto de non-blocking es tan fundamental hoy en día.


El Caos del Verilog Primitivo

En sus inicios, Verilog solo contaba con la asignación bloqueante (=). Esto provocaba un «caos de simulación» al intentar modelar componentes síncronos, que son la base de casi toda la lógica digital.

El problema se puede ilustrar con el ejemplo más simple: intentar intercambiar el valor de dos registros en un flanco de reloj.

El intento fallido con el Verilog original: Si solo tienes asignaciones bloqueantes (=), el código para un intercambio se vería así:

Fragmento de código

// ESTE CÓDIGO NO FUNCIONA COMO UN INTERCAMBIO
always @(posedge clk) begin
  a = b; // 1. 'a' toma inmediatamente el valor de 'b'.
  b = a; // 2. 'b' toma el valor de 'a', ¡pero 'a' ya fue sobreescrito!
end

En este caso, ambos registros terminarían con el valor original de b. El intercambio fracasa porque el simulador ejecuta las líneas en orden estricto, una detrás de otra.

La Solución: El Nacimiento del Non-Blocking

Los ingenieros se dieron cuenta de que necesitaban una forma de decirle al simulador: «Calcula todos los resultados que deben ocurrir en este flanco de reloj primero, y luego actualiza todas las señales a la vez».

Así nació la asignación non-blocking (<=). Fue introducida específicamente para resolver este defecto de nacimiento del lenguaje.

El mismo intercambio, ahora funcionando correctamente:

Fragmento de código

// ESTE CÓDIGO SÍ FUNCIONA
always @(posedge clk) begin
  a <= b; // Planifica que 'a' tome el valor antiguo de 'b'.
  b <= a; // Planifica que 'b' tome el valor antiguo de 'a'.
end

Aquí, el simulador primero lee los valores antiguos de a y b y, solo después, actualiza ambos registros. Esto imita perfectamente el comportamiento del hardware real, donde todos los flip-flops capturan sus datos en el mismo instante.

En conclusión, la asignación non-blocking no fue una simple mejora, sino una corrección fundamental que permitió a Verilog modelar de forma fiable el hardware síncrono y convertirse en el estándar de la industria para el diseño y la síntesis.