Two processors share a bus. One reads from a slow flash controller that takes a long time to answer. While it waits, the other processor, which only wants a word from fast on-chip RAM, cannot use the bus at all. The bus is "busy", yet for most of that time no data is moving on it. Everyone is waiting for one slow target.
Must a transaction hold the bus until it is answered, and what does letting go cost?
The obvious bus protocol keeps a transaction on the bus from request to response: the requester drives the address, the target stretches the transfer until it has the data, and then the bus is free. It is simple, because the bus itself remembers who asked what. But the bus is occupied for the target's whole latency, and a slow target turns a shared bus into a queue behind it.
Combined (held) | Split | |
|---|---|---|
The bus is occupied | From request until the response | Only while a request or a response is actually moving |
During the target's latency | Nobody else can use the bus | Other requests and responses use it |
How the response finds its requester | Implicit: the bus is still connected | The response carries an ID or tag |
State needed | None beyond the bus | The target queues requests; the requester remembers what is outstanding |
A split transaction turns one long occupation of the bus into two short ones: a request phase and a response phase, with the target's latency between them spent off the bus. That is what lets a protocol keep many requests in flight (Part 6).
Master A reads from a slow target (4 cycles to answer) in cycle 0; master B reads from a fast target (1 cycle) in cycle 1. The testbench draws both buses side by side.
// Two masters, A and B, share a path to two targets: a slow one (4 cycles to answer)
// and a fast one (1 cycle). A asks the slow target in cycle 0; B asks the fast one in
// cycle 1. Both buses are simulated side by side.
// Combined: a transaction holds the bus from request until its data returns, so B
// waits for A. (c_a / c_b = bus held by A / B.)
// Split: a request takes one cycle on the request path and the bus is free again;
// the target answers later on the response path. (s_*_req / s_*_rsp.)
module tb;
logic clk = 0;
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
localparam int SLOW = 4, FAST = 1;
int cyc = -1;
always @(posedge clk) cyc <= cyc + 1;
// Combined bus: A holds it for 1 + SLOW cycles, then B for 1 + FAST
wire c_a = (cyc >= 0) && (cyc < 1 + SLOW);
wire c_b = (cyc >= 1 + SLOW) && (cyc < 2 + SLOW + FAST);
// Split bus: one-cycle requests; responses come back after each target's latency
wire s_a_req = (cyc == 0);
wire s_b_req = (cyc == 1);
wire s_b_rsp = (cyc == 1 + FAST + 1);
wire s_a_rsp = (cyc == SLOW);
int b_done_c = -1, b_done_s = -1, busy_c = 0, busy_s = 0;
always @(posedge clk) begin
if (c_b && !(cyc + 1 < 2 + SLOW + FAST) && b_done_c < 0) b_done_c <= cyc;
if (s_b_rsp) b_done_s <= cyc;
if (c_a || c_b) busy_c <= busy_c + 1;
if (s_a_req || s_b_req) busy_s <= busy_s + 1;
end
initial begin
#120;
if (b_done_c == 6 && b_done_s == 3 && busy_c == 7 && busy_s == 2)
$display("PASS: B finishes in cycle %0d split vs %0d combined; the request path is busy 2 cycles instead of 7", b_done_s, b_done_c);
else $display("FAIL: B done %0d / %0d, busy %0d / %0d", b_done_s, b_done_c, busy_s, busy_c);
$finish;
end
endmoduleThe model only draws who uses each path in each cycle; the point is when B is answered and how long the shared path is busy.
How to read it, slot by slot:
Combined, slots 0 to 4: c_a is high: A holds the bus for its request and the target's four cycles of latency, though data moves only at the end.
Combined, slots 5 and 6: only now can B use the bus (c_b); its answer arrives in slot 6.
Split, slots 0 and 1: A's request (s_a_req) and B's request (s_b_req) each take one cycle, back to back.
Split, slots 3 and 4: B's response comes back in slot 3, before A's (slot 4). B finished in slot 3 instead of 6, and the request path was busy for 2 cycles instead of 7.
Identifiers. With several requests outstanding, each response must say which request it answers (lesson 6.4).
Buffering. A target that accepts requests faster than it answers them must queue them; a requester must keep track of what it is waiting for, and have room for responses when they arrive.
Ordering questions. B's answer overtook A's. Whether that is allowed, and between which requests, is a protocol decision (Part 7).
Deadlock risk. Requests and responses now travel separately and can block each other if they share buffers (Part 9).
Protocol | Choice |
|---|---|
APB | Combined: one transfer at a time, stretched with wait states until PREADY. |
Original AHB (AMBA 2) | Combined, with escape hatches: SPLIT and RETRY responses let a slow target release the bus. AHB-Lite, a single-master version, dropped them. |
AXI | Split by construction: requests and responses travel on separate channels, matched by IDs. |
PCI Express | Split: a non-posted request is answered later by a completion carrying the requester's tag. |
A system (illustrative) put a slow configuration-flash controller on the same combined bus as the processors' fast peripherals. Every boot-time flash read froze every other bus access for hundreds of cycles, and an interrupt handler missed its deadline. Moving the flash behind a split interface (accept the request, answer later) freed the bus during flash latency; the alternative, a dedicated bus for the slow target, would have worked too.
About 21: the request cycle plus all 20 cycles of latency, during which almost no data moves.
On the split bus the paths are free during A's latency, so B's request and response pass through; on the combined bus B cannot even start until A is done.
A combined transaction holds the bus for the target's whole latency; a split one uses it only to move the request and the response.
Splitting lets other traffic use the bus during latency and allows many transactions in flight.
Splitting needs IDs, buffering, ordering rules and deadlock care.