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.

B Drives Before A Has Finishedoe_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.
A drivesboth driveB drives 01234567891011121314151617181920212223oe_aA outoe_bB outline
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.
  1. Cycle 0

    The same frame from A.

  2. 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.

  3. Cycle 6

    A drives its stop bit, 1; B drives its start bit, 0. The line is x: two drivers fight.

  4. 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, Half Duplex and Full DuplexArrows show which way data can go on each wire.
Simplex, Half Duplex and Full DuplexArrows show which way data can go on each wire. component A only sends component B only receives component A sends or receives component B sends or receives component A sends and receives component B sends and receives one wire, one wayone wire, taking turnsA to BB to A
  1. Simplex

    Data goes one way only. Nothing can come back on that wire, not even an acknowledge.

  2. 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.

  3. 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.

A Turnaround on One Wireoe_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.
A drivesnobody drivesB drives 01234567891011121314151617181920212223oe_aA outoe_bB outline
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.
  1. Cycle 0

    A drives its last data bits; B only listens.

  2. Cycle 6

    A's stop bit. B's receiver samples it in the middle and delivers the byte.

  3. Cycle 8

    A has let go. Nobody drives; the pull-up holds the line at 1.

  4. 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.

ExchangeWiresMeasured
8 bytes each way at once280.0 bit-times
8 bytes, then 8 back1160.5 bit-times
8 exchanges of one byte each way1167.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 arrives8 exchangesGap per turnaround
0.5 bit160.0 bit-times0.0 bit
1.0 bit167.5 bit-times0.5 bit
2.0 bit182.5 bit-times1.5 bit
4.0 bit212.5 bit-times3.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

Multi-Drop and Daisy ChainTop: every device sits on the same wires. Bottom: data passes through each device to the next.
Multi-Drop and Daisy ChainTop: every device sits on the same wires. Bottom: data passes through each device to the next. component Controller starts everytransfer queue Shared wires one set for all component Target 1 answers its address component Target 2 or its select line component Controller shifts a long word component Device 1 passes bits on component Device 2 passes bits on component Device 3 back to thecontroller data out
  1. Point-to-point

    Two devices, nothing to share: UART, and SPI with one peripheral. The duplex figure above is point-to-point.

  2. 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).

  3. 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 upDuplex and shapeLesson
UART TX and RXFull duplex, point-to-pointP6.1
SPI MOSI and MISOFull duplex; many peripherals by select lines or a chainP7.1, P7.4
QSPI data pinsHalf duplex: four pins turn around togetherP7.7
I2C SDAHalf duplex, multi-drop; the receiver drives the ACKP8.1, P8.3
I2C Ultra Fast-modeSimplex: the controller only sendsP8.12
I3C SDAHalf duplex, multi-drop, with push-pull handoffsP9.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.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 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 errors
Read 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

Predict the outputFour round trips on the shared wire. Each side sends one 8N1 frame, ten bit-times with no idle bit after it, then lets go; the other side starts exactly 2 bit-times later. The program prints the time from A's first start edge to the end of B's last stop bit. What does it print?
`timescale 1ns/1ps
module tb;
  reg clk = 0, rst_n = 0;
  always #0.5 clk = ~clk;
  // Full duplex: A to B on one wire, B to A on the other.
  wire fa_out, fb_out;
  uart_tx fa(fa_out), fb(fb_out);
  p53_rx fa_rx(clk, rst_n, fb_out), fb_rx(clk, rst_n, fa_out);
  // Half duplex: one shared wire.
  tri1 line;
  wire ha_out, hb_out;
  reg oe_a = 0, oe_b = 0;
  uart_tx ha(ha_out), hb(hb_out);
  assign line = oe_a ? ha_out : 1'bz;
  assign line = oe_b ? hb_out : 1'bz;
  wire a_hears = oe_a ? 1'b1 : line, b_hears = oe_b ? 1'b1 : line;  // a side does not listen while it drives
  p53_rx ha_rx(clk, rst_n, a_hears), hb_rx(clk, rst_n, b_hears), m_rx(clk, rst_n, line);
  realtime x_since = 0, x_time = 0;
  always @(line) begin
    if (line === 1'bx) x_since = $realtime;
    else if (x_since > 0) begin x_time += $realtime - x_since; x_since = 0; end
  end
  real t0, guard = 1.0;
  function automatic string bt(input realtime t); return $sformatf("%0.1f", t / 16.0); endfunction
  // The turnaround: a side drives `guard` bit-times after its receiver delivers the other side's byte,
  // which happens at that byte's stop-bit sample. (B's rule is the line the lesson's twin changes.)
  bit early = 0;
  task automatic b_turn(); if (early) wait (hb_rx.u.state == 3); else begin @(hb_rx.done); #(16.0 * guard); end endtask
  task automatic a_turn(); @(ha_rx.done); #(16.0 * guard); endtask
  initial begin #2000000; $fatal(1, "watchdog"); end
  // Both sides send n bytes at once, one wire each way.
  task automatic full_bulk(input int n);
    t0 = $realtime;
    fork
      for (int i = 0; i < n; i++) fa.send_byte(8'h40 + i);
      for (int i = 0; i < n; i++) fb.send_byte(8'h80 + i);
    join
  endtask
  // A sends n bytes, lets go; B turns around and sends n bytes.
  task automatic half_bulk(input int n);
    t0 = $realtime;
    fork
      begin oe_a = 1; for (int i = 0; i < n; i++) ha.send_byte(8'h31 + i); oe_a = 0; end
      begin repeat (n - 1) @(hb_rx.done); b_turn(); oe_b = 1; for (int i = 0; i < n; i++) hb.send_byte(8'h61 + i); oe_b = 0; end
    join
  endtask
  // n exchanges of one byte each way; the two sides run as two processes, like two nodes.
  task automatic half_pingpong(input int n);
    t0 = $realtime;
    fork
      for (int i = 0; i < n; i++) begin
        if (i > 0) a_turn();
        oe_a = 1; ha.send_byte(8'h31 + i); oe_a = 0;
      end
      for (int i = 0; i < n; i++) begin
        b_turn();
        oe_b = 1; hb.send_byte(8'h61 + i); oe_b = 0;
      end
    join
  endtask
  initial begin
    fa.no_idle_between = 1; fb.no_idle_between = 1; ha.no_idle_between = 1; hb.no_idle_between = 1;
    #4 rst_n = 1; #4;
    t0 = $realtime;
    for (int i = 0; i < 4; i++) begin
      if (i > 0) #(16 * 2);                       // A starts 2 bit-times after B lets go
      oe_a = 1; ha.send_byte(8'h55); oe_a = 0;     // one 8N1 frame, no idle bit after it
      #(16 * 2);                                   // B starts 2 bit-times after A lets go
      oe_b = 1; hb.send_byte(8'hAA); oe_b = 0;
    end
    $display("%s bit-times", bt($realtime - t0));
    $finish;
  end
endmodule
Read 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
Spot the bugFour exchanges on the shared wire. Each option is B's rule for when it may switch its driver on after A's byte; A waits one bit after B's byte. The bench checks that a listener gets 8 clean frames and that the line is never x. Which rule is wrong?
Read 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
ConceptEight bytes each way took about twice as long on one wire as on two. What does the half-duplex link buy for that time?
ConceptOn I2C the target drives the acknowledge bit on SDA right after the controller's eighth bit, with no guard time. Why is there no fight like the twin's?

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.