Skip to content

Assignment Ordering

Nothing stops you from assigning the same register twice inside one pipeline stage — both writes are built, both are emitted, and the write-priority system decides which one lands. The rule is:

  1. Higher priority wins. Every assignment carries a priority; for one target, writes are emitted in ascending priority order inside the register’s always block, so under non-blocking (<=) semantics the highest-priority write is emitted last and dominates.
  2. Ties fall back to program order. Writes at the same priority keep their declaration order (the update pool sorts stably), so the last-declared write is emitted last and wins.

Both cases are pinned down by pipeline test models built on the three-stage counter from Pipeline Basics.

Test model tc20_pip_zync_multi_assign_order. Stage 1 writes a twice in the same clocked block, both at the default user priority:

self.v = val(8, 1, "v") # first write's addend
self.v2 = val(8, 2, "v2") # second write's addend
with pip(self.pip_cons[0], auto_req=True):
with zync(self.pip_cons[1]):
self.a |= self.a + self.v # first write: a + 1
self.a |= self.a + self.v2 # second write: a + 2 ← wins

The per-grant step of a reveals the winner unambiguously:

observed stepmeaning
+2 per grantthe last write overrides (a + v2) — this is what happens
+1 per grantthe first write would have won
+3 per grantboth writes would have accumulated

The test bench asserts every non-stall delta of a equals 2: the second a |= ... overrides the first, exactly like the last non-blocking assignment winning in a hand-written Verilog always block. Note that the losing write is not merged or accumulated — a steps by v2, not v + v2.

flowchart TB
  w1["a |= a + v<br/>(first, default pri)"] --> emit
  w2["a |= a + v2<br/>(second, default pri)"] --> emit
  emit["always block:<br/>ascending priority order,<br/>last-declared emitted last"] --> res["a steps by +2 (v2 wins)"]

Different priorities: priority overrides order

Section titled “Different priorities: priority overrides order”

Test model tc21_pip_zync_multi_assign_priority is the same pipeline, but the two writes are wrapped in with priority(...) at different levels — and the higher priority is deliberately placed on the first-declared write:

PRI_HIGH = DEFAULT_UE_PRI_USER + 3
PRI_LOW = DEFAULT_UE_PRI_USER + 1
with pip(self.pip_cons[0], auto_req=True):
with zync(self.pip_cons[1]):
with priority(PRI_HIGH):
self.a |= self.a + self.v # +1, declared FIRST, highest → wins
with priority(PRI_LOW):
self.a |= self.a + self.v2 # +2, declared LAST, lowest → loses

Now a steps by +1 per grant, not +2. If program order still decided, the last-declared +2 write would win as in tc20 — instead the high-priority write dominates even though it appears first in the source. Priority, not declaration order, picks the winner.

flowchart LR
  hi["PRI_HIGH<br/>a |= a + v<br/>(declared first)"] -->|emitted last| win["dominates → a steps +1"]
  lo["PRI_LOW<br/>a |= a + v2<br/>(declared last)"] -->|emitted first| lose["overwritten"]

Writes in different stages of the same pipeline target different registers in the idiomatic style (a, b, c …), so no conflict arises. When two stages — or a stage and some parallel flow — do write the same register, the resolution is exactly the same as above: each write is guarded by its own stage’s grant condition, and if both guards fire on the same clock edge, priority (then program order) decides. The pipeline adds no extra ordering rule of its own.

The same mechanics apply outside pipelines too — the par variants of these experiments (tc12/tc13) and the emitted-Verilog view are on the Write Priority page.