Menu

Asignación bloqueante vs no bloqueante en Verilog: cuándo usar = vs <=

El tema más confuso para principiantes en Verilog. Qué significan realmente = y <= dentro de un bloque always, y la regla que previene la mayoría de las condiciones de carrera.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Los dos operadores

Dentro de los bloques always e initial, Verilog tiene dos operadores de asignación:

  • = es una asignación bloqueante. Actualiza el LHS ahora, antes de pasar a la siguiente sentencia.
  • <= es una asignación no bloqueante. Evalúa el RHS ahora, programa que el LHS se actualice al final del paso de tiempo actual.

Fuera de los bloques procedurales (en assign) solo existe = - assign a <= b es un error de sintaxis. Dentro de los bloques procedurales, ambos son legales, y elegir el correcto es la decisión más importante en Verilog para principiantes.

Qué hace blocking

Blocking es lo que esperarías de un lenguaje de software: línea por línea, de arriba abajo. La sentencia N se completa antes de que empiece la sentencia N+1:

Esto es puramente secuencial. Cada sentencia ve los efectos de las anteriores. Eso coincide con la intuición del software.

Qué hace non-blocking

Non-blocking tiene la forma del hardware. Todos los lados derechos se evalúan usando los valores al inicio del paso de tiempo. Todos los lados izquierdos se actualizan al final del paso de tiempo. El orden de las sentencias en el código fuente no cambia las dependencias entre señales:

Ese fragmento implementa una rotación de 3 elementos a → b → c → a en un paso. Con blocking, necesitarías una variable temporal para evitar machacar uno de ellos. Con non-blocking, el intercambio sucede atómicamente porque cada RHS lee los valores previos al paso.

Esto es exactamente cómo se comportan tres flip-flops en un flanco de reloj: todos capturan sus entradas en el mismo instante, sin importar las dependencias entre ellos.

La regla que te salva

Usa <= en bloques sincronizados. Usa = en bloques combinacionales.

Eso es todo. Memorízalo. Codifícalo sin pensar. La mayoría de las condiciones de carrera, las discrepancias entre simulación y síntesis y los bugs de "me funciona pero no sintetiza" vienen de violar esta regla.

La regla tiene una razón hardware: los bloques sincronizados modelan flip-flops que muestrean todas sus entradas simultáneamente. Los bloques combinacionales modelan lógica que propaga tan rápido como puede. La semántica del operador de asignación coincide con esos comportamientos.

Cada <= lee el valor actual del registro fuente y programa al registro destino para que tome ese valor al final del paso de tiempo. El efecto neto es exactamente lo que hace un shift register de hardware: cada flip-flop captura el valor de su vecino, todos en el mismo flanco de reloj, sin carrera.

Ahora considera lo que habría pasado con asignación bloqueante:

// WRONG - this is not a shift register!
always @(posedge clk) begin
    out[3] = out[2];   // out[3] becomes out[2]
    out[2] = out[1];   // out[2] becomes out[1], which we just set above
    out[1] = out[0];
    out[0] = in;
end

Cada sentencia machaca la fuente antes de que la siguiente sentencia la lea. En un único ciclo de reloj, in se propagaría todo el camino hasta out[3], porque cada línea ve el valor recién escrito de la anterior. El comportamiento en hardware real (que usa semántica non-blocking) sería completamente distinto del que mostró el simulador.

Bloques combinacionales: blocking es lo correcto

Para always @(*), la asignación bloqueante es correcta. No hay flip-flops, ni regla de captura simultánea que imponer, y las variables intermedias son útiles:

sum se calcula primero, luego result usa el valor recién calculado. La lógica combinacional se aplana a una única pieza de hardware: result = ~(a + b). No aparecen flip-flops porque no hay reloj.

Si usaras <= aquí, el simulador seguiría actualizando sum antes de que result se evaluase contra él (porque ambas actualizaciones suceden al final del paso), pero el orden sería sutilmente distinto y muchas herramientas de síntesis se quejan. No mezcles; elige el operador que coincida con el tipo de bloque.

El error que más duele

Aquí está: un bloque sincronizado con asignación bloqueante.

// BUG: race condition waiting to happen
always @(posedge clk) begin
    a = b;
    b = c;
    c = a;
end

En simulación, el simulador puede dar a primero, luego b, luego c, produciendo un conjunto de valores. El hardware producirá otro conjunto, porque los flip-flops reales capturan simultáneamente. Las dos divergen en silencio y perderás un día encontrando el bug. Usa <= en bloques sincronizados.

Por qué existen dos operadores

Los diseñadores de Verilog podrían haber elegido una única semántica de asignación y quedarse con ella. No lo hicieron, porque el lenguaje tiene que modelar dos comportamientos hardware distintos:

  • Lógica combinacional: las señales propagan continuamente, las dependencias importan, "qué calcula esta puerta" tiene sentido.
  • Lógica secuencial: ocurre un flanco de reloj, cada flip-flop captura simultáneamente, las dependencias entre las entradas y salidas de los flip-flops están desacopladas.

Blocking es para el primero. Non-blocking es para el segundo. El operador elige la semántica; el simulador hace el resto.

Qué viene a continuación

Ya tienes las reglas para escribir cualquier bloque procedural correctamente. El siguiente capítulo sube desde bloques individuales a las construcciones de control de flujo que van dentro de ellos: if/else, case y loops for. Las reglas de blocking vs non-blocking siguen aplicándose dentro de todas ellas.

Preguntas frecuentes

¿Cuál es la diferencia entre asignación bloqueante y no bloqueante en Verilog?

Blocking (=) actualiza el destino inmediatamente, antes de que se ejecute la siguiente sentencia. Modela ejecución secuencial. Non-blocking (<=) programa la actualización para el final del paso de tiempo actual - cada RHS no bloqueante se evalúa usando los valores antiguos de todas las señales, luego cada LHS se actualiza en un único paso coordinado. Non-blocking es cómo se comportan realmente los flip-flops.

¿Cuándo debería usar = vs <= en Verilog?

Regla: usa <= en bloques sincronizados always @(posedge clk), y usa = en bloques combinacionales always @(*). Esa única regla previene toda la clase de condiciones de carrera que producen las asignaciones mezcladas. Dentro de los bloques initial de testbench, = es la opción normal; <= aparece raramente ahí.

¿Por qué se necesita la asignación non-blocking en Verilog?

Porque los flip-flops del hardware todos capturan sus entradas simultáneamente en un flanco de reloj. Si usaras = (blocking) en código sincronizado, el orden de las sentencias en tu archivo cambiaría qué señal ve el nuevo valor de qué otra señal - una condición de carrera entre la simulación y el hardware real. <= coincide con el comportamiento del hardware al evaluar todos los RHS primero, y luego hacer todas las actualizaciones.

¿Qué pasa si mezclas = y <= en el mismo bloque always de Verilog?

Obtienes una condición de carrera. La mezcla produce hardware cuyo comportamiento depende de los internos del simulador (orden de planificación de eventos), y peor aún, la simulación puede no coincidir con lo que produce la síntesis. La mayoría de las herramientas de lint marcan esto como un error. La solución es comprometerse con uno u otro basándose en el rol del bloque - non-blocking para sincronizado, blocking para combinacional.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR