An AHB agent: the pipelined driver and monitor
Drive two transfers at once, one in each phase, rebuild every transfer from the edge its address was taken and the edge its data phase ended, and keep both right through wait states and an ERROR.
- Lesson
- P4.11
- Time
- 40 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
An AHB monitor logged a transfer at every edge where HTRANS showed NONSEQ, and for months it matched the driver beat for beat. Then a slower memory inserted two wait states on a read, and the write waiting behind it was logged three times, each copy with the wrong data. The fix was one rule: an address phase counts only at an edge where HREADY is high, and the transfer ends at the next such edge.
acc is 1 at an edge where an address phase is taken (a transfer on the wires and HREADY high); pub at an edge where the monitor publishes.
- Cycle 0
The write to 0x100 is taken at 35 ns. The monitor remembers its address and control in
pend; nothing is published yet. - Cycle 1
At 45 ns HWDATA carries 11, the data of 0x100, while the address phase of the write to 0x104 is taken. The monitor publishes write 0x100 = 11 and remembers 0x104.
- Cycle 2
The read of 0x100 is taken and the write to 0x104 is published with 22. Two transfers on the wires, one in each phase, every cycle.
- Cycle 3
The memory holds HREADY low. The write to 0x200 waits in the address phase; it is not a transfer yet, and the monitor counts nothing.
- Cycle 5
HREADY rises at 85 ns: the read ends with 11 and the write to 0x200 is taken at the same edge. One edge, one transfer finished, one started.
Measured on the lesson's bench, 35 ns to 95 ns, with its waveform line switched on. The rows acc and pub mark the edges where an address phase is taken and where the monitor prints.
Two items on the bus at once
An AHB manager that waits for one transfer to finish before it starts the next wastes half the bus. The driver on this page keeps two items. aph is the item whose address phase is on the wires, dph the one whose data phase is running. At every edge where HREADY is high three things happen at once. The data phase of dph ends, so the driver reads HRESP and, for a read, HRDATA, and hands the item back with put(). aph moves into the data phase. The next item from the queue takes the address phase.
- get()
take()gets the item and queues it.finish_itemin the sequence returns at once, so the next item follows straight away.Follow the calls, values and waits
- data write 0x104: Sequence → take()
- The address phase
At an edge where HREADY is high the item leaves the queue and becomes
aph. Its HTRANS and HADDR are on the wires for the next cycle, while the item before it is in its data phase.Follow the calls, values and waits
- data write 0x104: take() → aph
- call write 0x104: aph → ahb_mem
- The data phase
At the next edge with HREADY high the memory takes the address phase and the item becomes
dph. The driver puts its data on HWDATA and keeps it there through every wait state.Follow the calls, values and waits
- data write 0x104: aph → dph
- data write 0x104: dph → ahb_mem
- Two witnesses
When the data phase ends the driver hands the item back with
put(), and the monitor publishes the same write from the wires. The scoreboard updates the memory it expects.Follow the calls, values and waits
- return OKAY: dph → Sequence
- data write 0x104 = 22: ahb_mem → Monitor
- data write 0x104 = 22: Monitor → Scoreboard
Read the diagram as text
- Sequence: finish_item returns at get()
- take(): get(); push to the queue
- aph: the address phase
- dph: the data phase; put() at its end
- ahb_mem: the DUT
- Monitor: pend: address until data
- Scoreboard: the memory it expects
- Sequence → take(): item
- take() → aph: queue
- aph → ahb_mem: HTRANS, HADDR
- aph → dph: HREADY high
- dph → ahb_mem: HWDATA
- dph → Sequence: put()
- ahb_mem → Monitor: the wires
- Monitor → Scoreboard: transfer
The driver pipelines because get() returns the sequence at once; an item belongs to the address phase for one HREADY and to the data phase until the next.
The driver changes its wires with nonblocking assignments at the edge, after the memory has sampled them, which is the drive-after-the-edge rule of every bench in this track. While HREADY is low nothing advances. HWDATA stays on the wires, and the waiting address phase stays put, the rule P4.4 and P4.10 checked.
The monitor joins the phases
A transfer on AHB exists in two places a cycle or more apart. The address and the control are on the wires when the address phase is taken, and the data and the response when the data phase ends. The monitor keeps one transfer in pend between those two edges. At an edge where HREADY is high it first completes pend. It reads HWDATA for a write or HRDATA for a read, and HRESP for both, and publishes. Then, if HTRANS shows NONSEQ or SEQ, it starts the next pend from the address phase taken at that same edge. A transfer that waits on the wires while HREADY is low is not taken, so the monitor never sees it twice.
| ns | On the wires | HREADY | The monitor |
|---|---|---|---|
| 35 | NONSEQ write 0x100 | 1 | pend = write 0x100 |
| 45 | NONSEQ write 0x104, HWDATA 11 | 1 | publishes write 0x100 = 11; pend = write 0x104 |
| 55 | NONSEQ read 0x100, HWDATA 22 | 1 | publishes write 0x104 = 22; pend = read 0x100 |
| 65 | NONSEQ write 0x200 waiting | 0 | nothing |
| 75 | NONSEQ write 0x200 waiting | 0 | nothing |
| 85 | NONSEQ write 0x200, HRDATA 11 | 1 | publishes read 0x100 = 11; pend = write 0x200 |
| 95 | SEQ write 0x204, HWDATA a0 | 1 | publishes write 0x200 = a0; pend = write 0x204 |
Measured from the example. The write to 0x200 is on the wires at three edges and becomes one transfer.
Bursts need no special case. Every beat is a transfer with its own address phase, NONSEQ for the first and SEQ for the rest, so each INCR4 comes out as four lines. A monitor that wants the burst as one item groups the beats from NONSEQ to the last SEQ.
An ERROR, a withdrawn read and the scoreboard
The write to 0x8000 lies above the memory's err_above. Its data phase gets the two-cycle ERROR. At 175 ns the first cycle, HRESP high with HREADY low, warns the driver. The read of 0x104 waiting in the address phase will be taken at the next edge unless it acts. The driver drives IDLE and pushes the read back to the front of its queue, and the log says withdrew. At 185 ns the second cycle ends the write with ERROR and the monitor publishes it that way. The read goes out again and completes at 205 ns with 22, so order and data are kept (IHI 0033C 5.1).
The scoreboard is a uvm_subscriber with an associative array for the memory it expects. An OKAY write updates it, an OKAY read is compared with it, and a transfer with ERROR is counted apart. This memory writes nothing on an error, so the scoreboard does not model one. A DUT that might write before it signals ERROR needs a scoreboard that allows both outcomes.
Asked in interviews: AHB agents
How does an AHB driver pipeline?
It holds two items: one in the address phase and one in the data phase. At every edge where HREADY is high the data phase ends, the address-phase item moves into the data phase, and the next item takes the address phase. While HREADY is low it changes nothing.
When does the monitor publish a write, and where does it get the data?
At the edge where the write's data phase ends, HREADY high, the edge after its address phase was taken or later with wait states. The data is HWDATA at that edge, because write data belongs to the data phase.
What breaks a monitor that logs at the address phase?
Wait states: a transfer waiting on the wires shows NONSEQ at several edges and is logged several times, and the data it records belongs to another transfer. An address phase counts only at an edge with HREADY high.
What does the driver do in the first cycle of an ERROR?
If a transfer waits in the address phase it may cancel it by driving IDLE in the second cycle; this driver does, and issues the transfer again afterwards. It may also continue; the specification allows both.
Why use get() and put() in the driver?
Because two items are in flight at once. get() returns the sequence immediately, and put() returns each response when its data phase ends.
Try it
Read the log by time. Thirteen items, thirteen transfers, one line each, ten ns apart except where the read of 0x100 waits two cycles (55 to 85 ns) and where the ERROR takes two cycles and the withdrawn read goes out again (165 to 205 ns). The scoreboard checks six reads against six writes and counts the ERROR apart.
Then break it. In the monitor, drop the vif.hready test and publish at every edge where HTRANS shows a transfer: the write to 0x200 appears three times. Or delete the else if branch of the driver that withdraws the waiting item: the read of 0x104 is then taken during the ERROR, which the specification allows, and its response arrives one edge earlier.
45 ns monitor write NONSEQ 0x100 data=11 OKAY 55 ns monitor write NONSEQ 0x104 data=22 OKAY 85 ns monitor read NONSEQ 0x100 data=11 OKAY 95 ns monitor write NONSEQ 0x200 data=a0 OKAY 105 ns monitor write SEQ 0x204 data=a1 OKAY 115 ns monitor write SEQ 0x208 data=a2 OKAY 125 ns monitor write SEQ 0x20c data=a3 OKAY 135 ns monitor read NONSEQ 0x200 data=a0 OKAY 145 ns monitor read SEQ 0x204 data=a1 OKAY 155 ns monitor read SEQ 0x208 data=a2 OKAY 165 ns monitor read SEQ 0x20c data=a3 OKAY 175 ns driver withdrew read 0x104: the transfer before it got ERROR 185 ns monitor write NONSEQ 0x8000 data=dead ERROR 205 ns monitor read NONSEQ 0x104 data=22 OKAY 205 ns sequence 13 responses, 1 ERROR 235 ns scoreboard 6 writes, 6 reads, 1 errors, 0 mismatches UVM_ERROR : 0Read included file: ahb_pkg.sv
`timescale 1ns/1ns
// ahb_pkg.sv: the encodings and the arithmetic of a burst (read-only).
// The memory, the driver, the checker and the lesson text all use these functions, so they never
// disagree about the address of a beat or the lanes it uses.
package ahb_pkg;
localparam logic [1:0] IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
localparam logic [2:0] SINGLE = 3'b000, INCR = 3'b001, WRAP4 = 3'b010, INCR4 = 3'b011,
WRAP8 = 3'b100, INCR8 = 3'b101, WRAP16 = 3'b110, INCR16 = 3'b111;
localparam logic OKAY = 1'b0, ERROR = 1'b1;
function automatic string trans_name(input logic [1:0] t);
case (t) IDLE: return "IDLE"; BUSY: return "BUSY"; NONSEQ: return "NONSEQ"; default: return "SEQ"; endcase
endfunction
function automatic string burst_name(input logic [2:0] b);
case (b)
SINGLE: return "SINGLE"; INCR: return "INCR"; WRAP4: return "WRAP4"; INCR4: return "INCR4";
WRAP8: return "WRAP8"; INCR8: return "INCR8"; WRAP16: return "WRAP16"; default: return "INCR16";
endcase
endfunction
function automatic string resp_name(input logic r);
return r ? "ERROR" : "OKAY";
endfunction
// s when c is 1, else the empty string (a string-typed result prints the same on every simulator).
function automatic string opt(input bit c, input string s);
if (c) return s;
return "";
endfunction
// Beats in a burst: 1 for SINGLE, 4, 8 or 16 for the fixed bursts, 0 for INCR (undefined length).
function automatic int beats_of(input logic [2:0] b);
case (b) SINGLE: return 1; INCR: return 0; WRAP4, INCR4: return 4; WRAP8, INCR8: return 8; default: return 16; endcase
endfunction
function automatic bit is_wrap(input logic [2:0] b);
return b == WRAP4 || b == WRAP8 || b == WRAP16;
endfunction
// The address of the beat after the one at a: the size in bytes further on; a wrapping burst
// returns to the start of its window (beats x size bytes, aligned to that total) at the window's end.
function automatic int unsigned next_addr(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
int unsigned nb = 1 << size;
int unsigned window = nb * beats_of(b);
int unsigned base;
if (!is_wrap(b)) return a + nb;
base = (a / window) * window;
if (a + nb >= base + window) return base;
return a + nb;
endfunction
// The address of beat n of a burst that starts at start.
function automatic int unsigned beat_addr(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
int unsigned a = start;
for (int k = 0; k < n; k++) a = next_addr(a, size, b);
return a;
endfunction
// The lanes a transfer at address a uses on a bus of bus_bytes bytes, packed as {upper, lower}.
// Every AHB transfer is aligned to its size, so it uses size bytes from its own lane upwards.
function automatic logic [15:0] beat_lanes(input int unsigned a, input logic [2:0] size, input int bus_bytes);
int lower = a % bus_bytes;
int upper = lower + (1 << size) - 1;
if (upper > bus_bytes - 1) upper = bus_bytes - 1;
return {upper[7:0], lower[7:0]};
endfunction
// The strobe of a transfer: a 1 for every lane it uses (bit l is lane l).
function automatic logic [63:0] beat_strb(input int unsigned a, input logic [2:0] size, input int bus_bytes);
logic [15:0] lanes = beat_lanes(a, size, bus_bytes);
logic [63:0] s = 0;
for (int l = lanes[7:0]; l <= lanes[15:8]; l++) s[l] = 1'b1;
return s;
endfunction
function automatic bit aligned(input int unsigned a, input logic [2:0] size);
return (a % (1 << size)) == 0;
endfunction
// True when an incrementing burst of n beats (n is used only for INCR) crosses a 1 KB boundary.
function automatic bit crosses_1k(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
int beats = beats_of(b) ? beats_of(b) : n;
if (is_wrap(b) || beats < 2) return 0;
return (start / 1024) != ((start + beats * (1 << size) - 1) / 1024);
endfunction
endpackage
Read included file: ahb_mem.sv
`timescale 1ns/1ns
// ahb_mem.sv: an AHB subordinate with a byte-addressed memory behind it (read-only in this lesson).
// It registers an address phase at an edge where HSEL is 1, HTRANS is NONSEQ or SEQ and HREADY is 1,
// then runs the data phase: wait states with HREADYOUT low, a write takes HWDATA (and HWSTRB) at the
// edge that ends the data phase, a read drives HRDATA for the cycle that ends it. IDLE and BUSY get a
// zero-wait OKAY and change nothing. The switches below are plain variables: the stimulus sets them by
// name (mem.waits = 2;).
// waits wait states inserted in every data phase
// waits_at[k] wait states for the k-th transfer this memory takes (from 0; -1 = use waits)
// err_above transfers at or above this address get the two-cycle ERROR (and write nothing)
// excl_on AHB5 exclusive access: one monitor per HMASTER, HEXOKAY on success
// trace print every transfer the memory takes and every byte it writes
// Broken twins, each off by default:
// ignore_hready registers an address phase while HREADY is low (another subordinate is stretching),
// so it acts on a transfer the manager may still change or cancel
// wdata_in_addr takes HWDATA in the address phase, so a write stores the previous transfer's data
// act_on_idle treats IDLE and BUSY as transfers (waits, reads, writes HWDATA); this twin is an
// AHB-Lite design without an HWSTRB input, so it writes the lanes HSIZE selects
// one_cycle_error answers ERROR in one cycle (HRESP high with HREADYOUT high)
// ignore_size writes every lane of the bus word, whatever HSIZE and HWSTRB say
module ahb_mem #(parameter int DATA_W = 32, parameter int DEPTH = 65536) (
input logic hclk,
input logic hresetn,
input logic hsel,
input logic [31:0] haddr,
input logic [1:0] htrans,
input logic hwrite,
input logic [2:0] hsize,
input logic [2:0] hburst,
input logic [3:0] hprot,
input logic hmastlock,
input logic [DATA_W-1:0] hwdata,
input logic [DATA_W/8-1:0] hwstrb,
input logic hexcl,
input logic [3:0] hmaster,
input logic hready,
output logic hreadyout,
output logic hresp,
output logic [DATA_W-1:0] hrdata,
output logic hexokay
);
import ahb_pkg::*;
localparam int BUS = DATA_W / 8;
// ---- switches ----
int waits = 0;
int waits_at [0:63];
int unsigned err_above = 32'hffff_ffff;
bit excl_on = 0, trace = 0;
bit ignore_hready = 0, wdata_in_addr = 0, act_on_idle = 0, one_cycle_error = 0, ignore_size = 0;
// ---- the memory ----
logic [7:0] mem [0:DEPTH-1];
int cycle = -1;
int taken = 0; // transfers taken so far
// ---- the data phase in progress ----
bit d_valid = 0, d_write = 0, d_err = 0, d_exok = 0, d_xfail = 0;
int unsigned d_addr = 0;
logic [2:0] d_size = 0;
logic [1:0] d_trans = 0;
logic [DATA_W-1:0] d_wd = 0; // HWDATA seen in the address phase (the wdata_in_addr twin)
int d_cnt = 0; // wait states still to insert
bit e_two = 0; // the ERROR response is in its second cycle
// ---- exclusive monitors, one per HMASTER ----
bit mon_use [0:15];
int unsigned mon_addr [0:15];
int mon_bytes [0:15];
initial begin
for (int i = 0; i < DEPTH; i++) mem[i] = 8'h00;
for (int k = 0; k < 64; k++) waits_at[k] = -1;
for (int m = 0; m < 16; m++) mon_use[m] = 0;
hreadyout = 1; hresp = 0; hrdata = 0; hexokay = 0;
end
function automatic logic [DATA_W-1:0] read_word(input int unsigned a);
logic [DATA_W-1:0] w = 0;
int unsigned word = (a / BUS) * BUS;
for (int l = 0; l < BUS; l++) w[8*l +: 8] = mem[(word + l) % DEPTH];
return w;
endfunction
// Write the lanes of one transfer: the lanes its address and size select, where the strobe is 1.
task automatic write_bytes(input int unsigned a, input logic [2:0] size, input logic [DATA_W-1:0] data, input logic [BUS-1:0] strb);
logic [15:0] lanes = beat_lanes(a, size, BUS);
int unsigned word = (a / BUS) * BUS;
int lo = ignore_size ? 0 : lanes[7:0];
int hi = ignore_size ? BUS - 1 : lanes[15:8];
for (int l = lo; l <= hi; l++)
if (ignore_size || act_on_idle || strb[l]) begin
mem[(word + l) % DEPTH] = data[8*l +: 8];
if (trace) $display("mem e%0d: byte 0x%0h <= %02h (lane %0d)", cycle, word + l, data[8*l +: 8], l);
end
// any write ends the exclusive monitors that cover one of its bytes
for (int m = 0; m < 16; m++)
if (mon_use[m] && a < mon_addr[m] + mon_bytes[m] && mon_addr[m] < a + (1 << size)) mon_use[m] = 0;
endtask
always @(posedge hclk) begin
cycle = cycle + 1;
if (!hresetn) begin
d_valid = 0; e_two = 0;
hreadyout <= 1; hresp <= 0; hexokay <= 0;
end else begin
// 1. the data phase that ends or waits at this edge
if (d_valid) begin
if (hreadyout && hready) begin
if (d_write && !d_err && !d_xfail)
write_bytes(d_addr, d_size, wdata_in_addr ? d_wd : hwdata, hwstrb);
d_valid = 0;
end else if (!hreadyout) begin
if (d_cnt > 0) d_cnt = d_cnt - 1;
else if (d_err) e_two = 1;
end
end
// 2. the address phase on the wires at this edge
if (hsel && (htrans[1] || act_on_idle) && (hready || (ignore_hready && !d_valid))) begin
d_valid = 1; d_addr = haddr; d_write = hwrite; d_size = hsize; d_trans = htrans; d_wd = hwdata;
d_cnt = (taken < 64 && waits_at[taken] >= 0) ? waits_at[taken] : waits;
taken = taken + 1;
d_err = (haddr >= err_above); e_two = 0;
d_exok = 0; d_xfail = 0;
if (excl_on && hexcl && htrans[1] && !d_err) begin
if (!hwrite) begin
mon_use[hmaster] = 1; mon_addr[hmaster] = haddr; mon_bytes[hmaster] = 1 << hsize; d_exok = 1;
end else if (mon_use[hmaster] && mon_addr[hmaster] == haddr && mon_bytes[hmaster] == (1 << hsize)) begin
d_exok = 1; mon_use[hmaster] = 0;
end else d_xfail = 1;
end
if (trace) $display("mem e%0d: %0s %0s 0x%0h size %0d%0s%0s%0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr, hsize,
opt(!hready, " (taken while HREADY is low)"), opt(d_err, " -> ERROR"),
opt(d_xfail, " -> exclusive failed"));
end
// 3. the response for the cycle that starts now
if (d_valid && d_cnt > 0) begin
hreadyout <= 0; hresp <= 0; hexokay <= 0;
end else if (d_valid && d_err) begin
hreadyout <= one_cycle_error ? 1'b1 : e_two; hresp <= 1; hexokay <= 0;
end else begin
hreadyout <= 1; hresp <= 0; hexokay <= d_valid && d_exok;
end
if (d_valid && !d_write) hrdata <= d_err ? '0 : read_word(d_addr);
end
end
endmodule
Check yourself
Remember
- An AHB driver holds two items: one in the address phase and one in the data phase; both move at every edge where HREADY is high.
- An address phase is taken only at an edge with HREADY high; the transfer ends at the next such edge, where its data and response are on the wires.
- The monitor joins the two edges in
pend: a transfer that waits on the wires is counted once, and its data comes from the data phase. - In the first cycle of an ERROR the driver may cancel the waiting transfer by driving IDLE; this one withdraws it and issues it again, in order.
- The scoreboard keeps the memory it expects and must model what the DUT does on an ERROR; this memory writes nothing.