Duplex and Topology
Tell simplex, half and full duplex apart, measure what each turnaround on a shared wire costs, and name the three ways devices are wired together.
- Lesson
- P5.3
- Time
- 25 min
- Runs on
- Your browser
- Check
- 4 questions
A meter network ran request and reply on one shared pair. A logger on the same pair reported a framing error on the last byte of almost every request. Each meter switched its driver on as soon as its receiver had eight data bits, while the requester was still sending its stop bit.
oe_a and oe_b enable each side's driver; A out and B out are what each would drive; line is the shared wire, pulled up when nobody drives.Read phases and sample points (half bit)
- A drives: columns 0 through 4.
- both drive: columns 5 through 7.
- B drives: columns 8 through 23.
- Cycle 0
The same frame from A.
- Cycle 5
B's receiver samples the eighth data bit in the middle of that bit, and the twin enables B's driver at once. A is still sending.
- Cycle 6
A drives its stop bit, 1; B drives its start bit, 0. The line is
x: two drivers fight. - Cycle 8
A lets go, and the line follows B. Any other node read A's stop bit from a fight.
Measured: the twin, the same exchange. Where both drive and disagree, the line is x.
Which Way the Bits Go
- Simplex
Data goes one way only. Nothing can come back on that wire, not even an acknowledge.
- Half duplex
One wire, both directions, one at a time. Every change of direction is a turnaround: one driver lets go before the other starts.
- Full duplex
A wire each way. Both sides send at once and never wait for the line.
Read the diagram as text
- A: only sends
- B: only receives
- A: sends or receives
- B: sends or receives
- A: sends and receives
- B: sends and receives
- A → B: one wire, one way
- A → B: one wire, taking turns
- A → B: A to B
- B → A: B to A
UART and SPI are full duplex; I2C and I3C data is half duplex; I2C Ultra Fast-mode is simplex.
Turnaround: The Price of One Wire
The bench runs the library UART both ways: two wires, or one wire with a driver enable on each side. Frames are 8N1, back to back, ten bit-times each. On the shared wire a side may drive one bit after its receiver delivers the other side's byte.
oe_a and oe_b enable each side's driver; A out and B out are what each would drive; line is the shared wire, pulled up when nobody drives.Read phases and sample points (half bit)
- A drives: columns 0 through 7.
- nobody drives: columns 8 through 8.
- B drives: columns 9 through 23.
- Cycle 0
A drives its last data bits; B only listens.
- Cycle 6
A's stop bit. B's receiver samples it in the middle and delivers the byte.
- Cycle 8
A has let go. Nobody drives; the pull-up holds the line at 1.
- Cycle 9
B enables its driver and sends its start bit. The line belongs to B now.
Measured: the first exchange of the example, from bit 5 of A's frame. B waits one bit after its receiver delivers A's byte, which it does at the middle of the stop bit.
| Exchange | Wires | Measured |
|---|---|---|
| 8 bytes each way at once | 2 | 80.0 bit-times |
| 8 bytes, then 8 back | 1 | 160.5 bit-times |
| 8 exchanges of one byte each way | 1 | 167.5 bit-times |
Measured. A listener on the shared wire logged 32 frames, 0 framing errors.
The shared wire halves the throughput, because only one side sends at a time. Each turnaround then adds a gap. Bulk transfers turn around once; a request and reply turn around on every byte.
How Long to Wait
| Guard after the byte arrives | 8 exchanges | Gap per turnaround |
|---|---|---|
| 0.5 bit | 160.0 bit-times | 0.0 bit |
| 1.0 bit | 167.5 bit-times | 0.5 bit |
| 2.0 bit | 182.5 bit-times | 1.5 bit |
| 4.0 bit | 212.5 bit-times | 3.5 bit |
Measured. 160 bit-times carry data; the rest is 15 turnarounds. The receiver delivers a byte in the middle of its stop bit, so a half-bit guard leaves no gap at all.
A half-bit guard has no margin: a real driver takes time to switch on and off. Too long a guard wastes the line. The twin in the opening figure went the other way and drove before the stop bit ended.
With that twin, the listener logged 15 frames, 7 framing errors where 16 clean frames were sent, and the line spent 94.0 ns in all at x. Lesson P5.4 shows what x is on a real wire.
Topology: Who Is Wired to Whom
- Point-to-point
Two devices, nothing to share: UART, and SPI with one peripheral. The duplex figure above is point-to-point.
- Multi-drop
Many devices on one set of wires. Each needs a way to know it is addressed, and only one may drive at a time (lesson P5.6).
- Daisy chain
Each device passes bits to the next, so one transfer reaches all of them. The data is late by one device per hop.
Read the diagram as text
- Controller: starts every transfer
- Shared wires: one set for all
- Target 1: answers its address
- Target 2: or its select line
- Controller: shifts a long word
- Device 1: passes bits on
- Device 2: passes bits on
- Device 3: back to the controller
- Controller → Shared wires
- Shared wires → Target 1
- Shared wires → Target 2
- Controller → Device 1
- Device 1 → Device 2
- Device 2 → Device 3
- Device 3 → Controller: data out
A multi-drop bus shares the line and must take turns; a chain never shares a wire but makes every transfer longer.
| Where it shows up | Duplex and shape | Lesson |
|---|---|---|
| UART TX and RX | Full duplex, point-to-point | P6.1 |
| SPI MOSI and MISO | Full duplex; many peripherals by select lines or a chain | P7.1, P7.4 |
| QSPI data pins | Half duplex: four pins turn around together | P7.7 |
| I2C SDA | Half duplex, multi-drop; the receiver drives the ACK | P8.1, P8.3 |
| I2C Ultra Fast-mode | Simplex: the controller only sends | P8.12 |
| I3C SDA | Half duplex, multi-drop, with push-pull handoffs | P9.3 |
Taking turns at the transaction level, valid and ready, is lesson P1.3; this lesson is the wire underneath.
Try It
The first three lines time each exchange from the first start edge to the end of the last stop bit. The last line is a third node that only listens to the shared wire: it saw both directions, 32 frames, with no framing error.
Set guard = 0.5; or guard = 3.0; before the first exchange and compare the two half-duplex lines. Then set early = 1; and read the listener's line again.
full duplex, 8 bytes each way at once: 80.0 bit-times half duplex, 8 bytes, then 8 back: 160.5 bit-times half duplex, 8 exchanges of one byte each way: 167.5 bit-times listener on the shared wire: 32 frames, 0 framing errorsRead included file: uart_tx.sv
`timescale 1ns/1ps
module uart_tx(output reg tx=1);
real error=0.0; int bits=8, parity=0; real stop_bits=1.0;
bit lsb_first=1, parity_includes_start=0, parity_includes_stop=0, parity_wrong_sense=0, short_stop=0, no_idle_between=0;
task automatic bit_time(input bit v); tx=v; #(16.0*(1.0+error)); endtask
task automatic send_byte(input byte unsigned value);
bit p; p=(parity==2);
bit_time(0);
for(int b=0;b<bits;b++) begin bit_time(value[lsb_first?b:bits-1-b]); p=p^value[b]; end
if(parity!=0) bit_time(p ^ parity_includes_stop ^ parity_wrong_sense);
tx=1; #(16.0*(1.0+error)*(short_stop?0.25:stop_bits));
// no_idle_between is a legal back-to-back frame control, not a fault.
if(!no_idle_between) #(16.0*(1.0+error));
endtask
task automatic send_break(); tx=0; #(200*16); tx=1; #32; endtask
task automatic set_error(input real e); error=e; endtask
endmodule
Read included file: uart_rx.sv
`timescale 1ns/1ps
module uart_rx(input wire clk, rst_n, rx, take,
output reg [7:0] data=0, output reg valid=0, frame_err=0, parity_err=0, overrun=0, break_seen=0, frame_done=0);
int bits=8, parity=0; real stop_bits=1.0;
bit trace=0;
bit parity_includes_start=0; // XOR of the zero-valued START is an equivalent control.
bit sample_at_edge=0, qualify_1_tick=0, no_vote=0, no_resync=0, rearm_on_low=0, stop_as_data=0, no_overrun_flag=0, parity_includes_stop=0;
int state=0, low_run=0, left_ticks=0, index=0, votes=0, frames=0, ticks=0, next_data_tick=0;
bit armed=1, sampled=0, p=0, pe=0, pending_zero_fe=0; reg [7:0] shift=0;
always @(posedge clk) begin
frame_done=0;
if(!rst_n) begin state=0;low_run=0;left_ticks=0;index=0;votes=0;valid=0;data=0;frame_err=0;parity_err=0;overrun=0;break_seen=0;armed=1;frames=0;ticks=0;next_data_tick=0;pending_zero_fe=0; end
else begin
ticks++;
if(take)valid=0;
low_run=rx?0:low_run+1;
if(rx)break_seen=0;
if(low_run>=176&&!rearm_on_low)begin break_seen=1;state=4;pending_zero_fe=0;end
if(state==4)begin if(rx)begin
if(pending_zero_fe)begin
if(valid&&!no_overrun_flag)overrun=1;
data=0;valid=1;frame_err=1;parity_err=pe;frames++;frame_done=1;pending_zero_fe=0;
end
state=0;armed=1;
end end
else if(state==0)begin
if(rx)begin armed=1;end
else if(armed&&low_run>=(qualify_1_tick?1:8))begin
state=1;left_ticks=sample_at_edge?8:16;index=0;shift=0;p=(parity==2)^parity_includes_stop;votes=0;pe=0;
// Fault: later frames keep the first frame's free-running schedule.
// At nominal back-to-back pacing it can work; a new START must normally
// reset this timing so baud error cannot accumulate across frames.
if(no_resync&&frames>0)begin
while(next_data_tick<=ticks)next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
left_ticks=next_data_tick-ticks;
end else next_data_tick=ticks+left_ticks;
next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
end
end else begin
if(left_ticks<=3)votes=votes+(rx?1:0);
left_ticks=left_ticks-1;
if(left_ticks==0)begin
sampled=no_vote?rx:(votes>=2);votes=0;left_ticks=16;
if(trace)$display("UART SAMPLE t=%0t state=%0d bit=%0d value=%0d",$time,state,index,sampled);
if(state==1)begin shift[index]=sampled;p=p^sampled;index=index+1;if(index==bits)state=parity==0?3:2;end
else if(state==2)begin pe=(sampled!=p);state=3;end
else begin
if(shift==0&&!sampled&&!rearm_on_low)pending_zero_fe=1;
else begin
if(valid&&!no_overrun_flag)overrun=1;
data=stop_as_data?{sampled,shift[7:1]}:shift;
frame_err=!sampled;parity_err=pe;valid=1;frames=frames+1;frame_done=1;
end
state=(sampled||rearm_on_low)?0:4;armed=1;
end
end
end
end
end
endmodule
Read included file: uart_flow.sv
`timescale 1ns/1ps
module uart_flow(input wire clk,rst_n,push,pop,cts_n,input wire [7:0] din,
output wire rts_n,output wire send,output wire [7:0] dout,output reg overrun=0);
reg [7:0] fifo[0:15]; int head=0,count=0,trigger=12;
bit cts_ignored=0,trigger_off_by_one=0;
assign rts_n=count>=(trigger+(trigger_off_by_one?1:0));
assign send=count>0&&(!cts_n||cts_ignored);
assign dout=count>0?fifo[head]:8'h00;
always @(posedge clk)begin
if(!rst_n)begin head=0;count=0;overrun=0;end
else begin
if(pop&&send)begin head=(head+1)%16;count=count-1;end
if(push)begin if(count==16)overrun=1;else begin fifo[(head+count)%16]=din;count=count+1;end end
end
end
endmodule
Read included file: uart_checker.sv
`timescale 1ns/1ps
module uart_checker(input wire clk,rx,valid,frame_err,parity_err,input wire [7:0] data,input wire frame_done);
int broken=0,received=0;
task automatic expect_byte(input byte unsigned wanted,input bit fe=0,pe=0);
if(!valid||data!==wanted||frame_err!==fe||parity_err!==pe)begin broken++;$display("UART CHECK got=%02h valid=%b FE=%b PE=%b expected=%02h FE=%b PE=%b",data,valid,frame_err,parity_err,wanted,fe,pe);end
endtask
always @(negedge clk)if(frame_done)received++;
endmodule
Read included file: uart_mon.sv
`timescale 1ns/1ps
// Independent wire decoder: completion is reported after the stop sample, never at START.
module uart_mon(input wire rx);
int bits=8,parity=0; real bit_period=16.0; int frames=0; bit trace=0;
reg [7:0] value; bit p,pe,fe;realtime began;
initial forever begin
@(negedge rx);began=$realtime; #(bit_period/2); if(rx==0)begin
value=0;p=(parity==2);pe=0;
for(int i=0;i<bits;i++)begin #(bit_period);value[i]=rx;p=p^rx;end
if(parity!=0)begin #(bit_period);pe=(rx!=p);end
#(bit_period);fe=!rx;
if(value==0&&fe)begin
while(rx==0&&$realtime-began<11*bit_period)#(bit_period/16);
if(rx==0)begin wait(rx==1);if(trace)$display("UART -- BREAK");end
else begin frames++;if(trace)$display("UART 00 FE");end
end
else begin frames++;if(trace)$display("UART %02h %s",value,fe?"FE":pe?"PE":"ok");end
end
end
endmodule
Read included file: uart_tasks.svh
task automatic send_byte(input byte unsigned b); tx0.send_byte(b); endtask
task automatic send_break(); tx0.send_break(); endtask
task automatic set_error(input real e); tx0.set_error(e); endtask
task automatic consume(); @(negedge clk);take=1;@(negedge clk);take=0;endtask
Read included file: p53_rx.sv
`timescale 1ns/1ps
module p53_rx(input wire clk, rst_n, rx);
wire valid, frame_err, parity_err, overrun, break_seen, frame_done;
wire [7:0] data;
uart_rx u(clk, rst_n, rx, 1'b1, data, valid, frame_err, parity_err, overrun, break_seen, frame_done);
int got = 0, bad = 0; reg [7:0] last = 0; event done;
always @(negedge clk) if (frame_done) begin got++; last = data; if (frame_err) bad++; -> done; end
endmodule
Check Yourself
Remember
- Simplex goes one way, half duplex takes turns on one wire, full duplex has a wire each way.
- A shared wire halves throughput and adds a gap at every turnaround, so short request and reply exchanges pay the most.
- A side may only drive after the other side has finished its last bit; driving early makes two drivers fight.
- Multi-drop shares one set of wires and must take turns; a daisy chain passes data through every device.