Digital blocks typically communicate using bus protocols — AMBA AXI, WishBone, OCP, and similar. A bus master sending data provides control signals telling the slave when a packet is valid, whether it's a read or write, and how many bytes are being sent, followed by an address and the data to be stored there. Here's a worked example where a testbench acts as the master, constraining a bus transaction class to valid data for a specific target.

12 min read | Intermediate Level

What You'll Learn

  • How multiple constraints — class-level and inline — combine to model a real bus protocol's addressing and burst rules
  • Why an address-range constraint alone isn't enough to guarantee a burst transaction stays within a slave's memory window
  • How to verify by hand that a specific randomized result actually satisfies every constraint in play

Modeling a Bus Transaction


// Burst [ 0 -> 1 byte, 1 -> 2 bytes, 2 -> 3 bytes, 3 -> 4 bytes]
// Length -> max 8 transactions per burst
// Protocol expects to send only first addr, and slave should calculate all
// other addresses from burst and length properties

class BusTransaction;
  rand int 			m_addr;
  rand bit [31:0]	m_data;
  rand bit [1:0] 	m_burst; 	// Size of a single transaction in bytes (4 bytes max)
  rand bit [2:0] 	m_length; 	// Total number of transactions
  
  constraint c_addr { m_addr % 4 == 0; } // Always aligned to 4-byte boundary
  
  function void display(int idx = 0);
    $display ("------ Transaction %0d------", idx);
    $display (" Addr 	= 0x%0h", m_addr);
    $display (" Data 	= 0x%0h", m_data);
    $display (" Burst 	= %0d bytes/xfr", m_burst + 1);
    $display (" Length  = %0d", m_length + 1);
  endfunction
endclass

m_burst and m_length are both encoded as one less than their real meaning — a common convention that lets a 2-bit field represent 4 possible sizes (1 through 4 bytes) and a 3-bit field represent 8 possible counts (1 through 8 transfers), rather than wasting an encoding on "0 bytes" or "0 transfers." m_addr is constrained to always be 4-byte aligned, at the class level, so this rule applies to every transaction regardless of anything else layered on top later.

Constraining a Transaction to a Slave's Address Window

Now the class is randomized with inline constraints targeting a specific slave's address range:


module tb;
  int 				slave_start;
  int  				slave_end;
  BusTransaction	bt;
  
  // Assume we are targeting a slave with addr range 0x200 to 0x800
  initial begin
  	slave_start = 32'h200;
    slave_end 	= 32'h800;
    bt = new;
  
    bt.randomize() with { m_addr >= slave_start;
                          m_addr < slave_end;
                         (m_burst + 1) * (m_length + 1) + m_addr < slave_end;
                        };
    bt.display();
  end
endmodule

Output

Output
ncsim> run
------ Transaction 0------
 Addr 	= 0x6e0
 Data 	= 0xbbe5ea58
 Burst 	= 4 bytes/xfr
 Length  = 5
ncsim: *W,RNQUIE: Simulation is complete.

The first two inline constraints bound where the transaction starts: 0x6e0 (1760 in decimal) falls between slave_start (512) and slave_end (2048). But a burst transaction doesn't just touch its starting address — it spans (burst size) × (number of transfers) bytes total. With 4 bytes per transfer and 5 transfers, this transaction covers 20 bytes, ending at 1760 + 20 = 1780 — still comfortably under 2048, thanks to the third constraint. Without it, nothing would stop a burst from starting inside the valid window and then running past its end.

The class's own c_addr constraint is still in force too, even though it isn't repeated inline — 0x6e0 is divisible by 4, confirming the 4-byte alignment rule from the class definition combined seamlessly with the inline range constraints.

Quick Check

Q: If m_addr were 2040 — still comfortably under slave_end (2048) on its own — with a burst of 4 bytes/xfr and 5 transfers (20 bytes total), would this transaction pass the third constraint?

Show Answer

No. 2040 + 20 = 2060, which is not less than 2048 — this would violate the third constraint, even though the starting address alone (2040) satisfies both of the simpler range checks. This is exactly the scenario the third constraint exists to catch.

Where Is This Used?

This exact pattern — an address-range constraint combined with a burst-aware end-of-transfer check — is standard for testing any memory-mapped peripheral, regardless of the specific bus protocol (AXI, APB, WishBone, and others all share this same basic shape). It guarantees generated stimulus never targets memory outside a peripheral's decoded address range, even when a burst's last byte would otherwise spill past the boundary.

Common Beginner Mistakes

Mistake #1: Forgetting the class-level constraint still applies

As this example shows, c_addr's alignment rule stays in force even though the inline constraint never mentions it. Why? Inline constraints add to a class's existing constraints, they don't replace them — every rule declared in the class is still active during this randomization.

Mistake #2: Bounding only the starting address of a burst

As the Quick Check shows, checking just m_addr >= slave_start and m_addr < slave_end isn't enough on its own. Why? A burst spans multiple bytes starting from that address — without a constraint accounting for the full span, the transaction's later bytes could still land outside the target window.

Mistake #3: Miscounting the burst/length encoding

m_burst + 1 gives bytes per transfer, and m_length + 1 gives the number of transfers — not the raw field values themselves. Why? Forgetting the "off by one" encoding convention leads to computing a span that's smaller than the transaction actually uses, silently undermining the whole boundary check.

Mistake #4: Assuming a simple range constraint is sufficient stimulus coverage

Real protocol constraints usually need several rules working together — alignment, starting range, and burst span, in this case — not just one broad bound. Why? A single simplistic constraint can look correct while still allowing scenarios (like a burst overrunning its target) that a real protocol would never actually permit.

Practice Exercise

Challenge: Check Two Candidate Transactions

Using a slave range of slave_start = 0x100 (256) and slave_end = 0x400 (1024), evaluate whether each of these transactions would satisfy all three inline constraints from this article: Candidate A — addr 900, burst 2 bytes/xfr, length 4 transfers. Candidate B — addr 1010, burst 4 bytes/xfr, length 8 transfers.

Hint

For each candidate, compute the total span (bytes per transfer × number of transfers) and check whether addr + span stays under slave_end, in addition to the simple starting-address bounds.

Solution

Candidate A passes. 900 falls within [256, 1024). Its span is 2 × 4 = 8 bytes, ending at 908 — still under 1024.

Candidate B fails. 1010 is within [256, 1024) on its own, but its span is 4 × 8 = 32 bytes, ending at 1042 — past slave_end. This is exactly the same failure mode demonstrated in this article's own Quick Check: a starting address that looks fine in isolation, undone by the burst's own span.

Summary

In this tutorial, you learned:

  • A realistic bus transaction constraint combines class-level rules (like address alignment) with inline constraints targeting a specific test scenario
  • Bounding only a burst's starting address isn't enough — the transaction's full span has to be accounted for to guarantee it stays within a target window
  • Encoded fields like burst size and transfer length often use an "off by one" convention, worth double-checking before computing a span
  • Real protocol stimulus typically needs several constraints working together, not one broad range check alone
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5