Four states cover idle, start, data, and stop. STOP returns to START at once when another byte waits and CTS permits acceptance, so frames can run with no idle gap.
Decision | Why |
|---|---|
| Glitch-free on a pin. It changes only right after a clock edge, and from state-derived values (a Moore-style, registered output). |
Byte captured into | The producer may change |
| Accepts the next byte in the last clock of the stop bit, so the next start bit follows with no gap (full line rate). |
One shared | Each bit lasts CLKS_PER_BIT clocks. START and STOP last one bit-time each, DATA lasts eight, and IDLE is unbounded. |
| A 3-bit counter; the FSM does not need extra states for each data bit. |
| RTL fallback only. All four 2-bit codes are legal; a known bit flip can select another legal phase. Synthesis recoding changes the physical fault space; see fault models and recovery. |
CLKS_PER_BIT must be >= 1. One clock per bit is legal: BW is guarded because $clog2(1) is zero. A time-zero simulation diagnostic rejects nonpositive divisors. The sweep below uses 1, 2, 3 and 434. CTS is a synchronized, active-high permission input; tie it high when unused.
Driving tx low as part of the accept edge makes the start bit begin exactly when baud_cnt is reset to 0, so every bit is exactly CLKS_PER_BIT clocks. Delaying it by a cycle would shorten the start bit or require extra state.
The transmitter plus a testbench that checks every clock of every frame.
module uart_tx #(parameter int CLKS_PER_BIT = 16) (
input logic clk, rst_n,
input logic tx_valid,
input logic cts_allow, // synchronized permission; blocks acceptance, never cancels a frame
output logic tx_ready,
input logic [7:0] tx_data,
output logic tx // serial line, idles high
);
typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t;
state_t state;
initial if (CLKS_PER_BIT < 1) $fatal(1, "uart_tx: CLKS_PER_BIT must be at least 1");
// CLKS_PER_BIT = 1 is legal (one clock per bit): guard the width as $clog2(1) is 0.
localparam int BW = (CLKS_PER_BIT > 1) ? $clog2(CLKS_PER_BIT) : 1;
logic [BW-1:0] baud_cnt; // counts clocks within one bit
logic [2:0] bit_idx; // which data bit is on the line
logic [7:0] shreg; // remaining data bits, LSB first
logic tick, accept;
assign tick = (baud_cnt == CLKS_PER_BIT - 1); // last clock of the current bit
assign tx_ready = cts_allow && ((state == IDLE) || (state == STOP && tick));
assign accept = tx_valid && tx_ready;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
state <= IDLE; tx <= 1'b1; baud_cnt <= '0; bit_idx <= '0; shreg <= '0;
end else if (accept) begin
shreg <= tx_data; // capture the byte
tx <= 1'b0; // start bit begins now
baud_cnt <= '0;
state <= START;
end else begin
baud_cnt <= tick ? '0 : baud_cnt + 1'b1; // default: count within the bit
unique case (state)
IDLE: begin tx <= 1'b1; baud_cnt <= '0; end
START: if (tick) begin // start bit done: put data bit 0 on the line
tx <= shreg[0]; shreg <= {1'b0, shreg[7:1]}; bit_idx <= '0; state <= DATA;
end
DATA: if (tick) begin
if (bit_idx == 3'd7) begin tx <= 1'b1; state <= STOP; end // all 8 bits sent: stop bit
else begin tx <= shreg[0]; shreg <= {1'b0, shreg[7:1]}; bit_idx <= bit_idx + 1'b1; end
end
STOP: if (tick) state <= IDLE; // stop bit done (no new byte waiting)
default: state <= IDLE;
endcase
end
endmodule
module uart_env #(parameter int CPB = 4) (input logic clk, output int errors, output bit finished);
timeunit 1ns; timeprecision 1ps;
logic rst_n = 1, tx_valid = 0, tx_ready, tx, cts_allow = 1;
logic [7:0] tx_data = 0;
uart_tx #(CPB) dut (.*);
logic [7:0] sent [$];
int frames_checked = 0, accepted = 0, cancelled = 0;
bit back_to_back = 0, stop_driver = 1;
int frames_before = 0, frames_in_burst = 0, boundary_guard;
bit manual = 1;
// acceptance is recorded AT the rising edge (it samples the values the DUT saw); the driver only moves at the falling edge
logic accepted_q = 0;
always @(posedge clk) accepted_q <= rst_n && tx_valid && tx_ready;
// driver: offers a byte (probability depends on mode) and holds valid and data unchanged until the edge that accepts it
always @(negedge clk) begin
if (!rst_n) tx_valid = 0;
else if (manual) begin end
else if (tx_valid && !accepted_q) begin
// offered but not accepted at the last edge: hold (also while draining after stop_driver)
end
else if (stop_driver || !rst_n) tx_valid = 0;
else if (back_to_back || $urandom_range(0, 9) < 5) begin tx_valid = 1; tx_data = $urandom_range(0, 255); end
else tx_valid = 0;
end
// monitor: once a byte is offered and not accepted, valid and data must not change until it is
logic pending = 0;
logic [7:0] pend_data;
always @(posedge clk) if (!rst_n) pending <= 0;
else begin
if (pending && (tx_valid !== 1'b1 || tx_data !== pend_data)) begin
errors++;
if (errors < 5) $display("FAIL: offer changed before it was accepted (valid=%b data=%h, was %h)", tx_valid, tx_data, pend_data);
end
pending <= tx_valid && !tx_ready;
pend_data <= tx_data;
end
// Sample acceptance pre-edge, then line post-NBA: one independent frame model.
logic [7:0] exp_byte;
logic [9:0] frame;
int edge_no = 0, sample_no = -1, last_accept = -1, burst_pairs = 0;
bit last_was_burst = 0;
always @(posedge clk) begin
edge_no++;
if (!rst_n) begin
if (sample_no >= 0) cancelled++;
sample_no = -1; sent.delete(); last_accept = -1; last_was_burst = 0;
#1;
if (tx !== 1'b1) errors++;
end else begin
if (tx_valid && tx_ready) begin
if (sample_no >= 0) errors++; // previous stop must already have lasted CPB samples
if (back_to_back && last_was_burst) begin
burst_pairs++;
if (edge_no - last_accept != 10 * CPB) begin
errors++; $display("FAIL CPB=%0d: frame spacing %0d", CPB, edge_no - last_accept);
end
end
last_accept = edge_no; last_was_burst = back_to_back;
sent.push_back(tx_data); accepted++;
exp_byte = sent.pop_front();
frame = {1'b1, exp_byte, 1'b0};
sample_no = 0;
end
#1;
if (sample_no >= 0) begin
if (tx !== frame[sample_no / CPB]) begin
errors++; if (errors < 5) $display("FAIL CPB=%0d byte=%h sample=%0d tx=%b", CPB, exp_byte, sample_no, tx);
end
if (sample_no == 10 * CPB - 1) begin frames_checked++; sample_no = -1; end
else sample_no++;
end else if (tx !== 1'b1) errors++; // catches an unsolicited start while idle
end
end
task automatic offer(input logic [7:0] value);
int guard;
@(negedge clk); #1; tx_valid = 1; tx_data = value;
guard = 0;
do begin @(posedge clk); guard++; end while (!tx_ready && guard < 12 * CPB);
if (!tx_ready) $fatal(1, "uart_env: offer timed out CPB=%0d", CPB);
@(negedge clk); #1; tx_valid = 0;
endtask
initial begin
errors = 0; finished = 0;
#1 rst_n = 0; @(posedge clk); #1 rst_n = 1;
// Directed bytes: 00/FF repeat one bit; A5 reads the same MSB- or LSB-first; 96 does not, so it catches bit-order bugs.
offer(8'h00); offer(8'hff); offer(8'ha5); offer(8'h96);
repeat (11 * CPB) @(negedge clk);
if (tx !== 1'b1) errors++;
// Idle reset, then cancel an active frame. A receiver must discard the truncated frame.
#1 rst_n = 0;
@(posedge clk); #2;
if (tx !== 1'b1) errors++;
@(negedge clk); #1 rst_n = 1;
offer(8'h00);
repeat (2 * CPB) @(negedge clk);
#1 rst_n = 0; tx_valid = 0;
@(posedge clk); #2;
if (tx !== 1'b1) errors++;
@(negedge clk); #1 rst_n = 1;
offer(8'ha5);
repeat (11 * CPB) @(negedge clk);
// Withdraw CTS with a stable next offer stalled behind an accepted frame.
offer(8'h00);
@(negedge clk); #1; tx_valid = 1; tx_data = 8'h96;
@(posedge clk);
if (tx_ready !== 1'b0 || cts_allow !== 1'b1) errors++;
@(negedge clk); #1 cts_allow = 0; // withdraw permission from an already stalled offer
repeat (11 * CPB) begin
@(posedge clk);
if (tx_ready !== 1'b0) errors++;
end
@(negedge clk); #1;
if (tx_valid !== 1'b1 || tx_data !== 8'h96 || tx !== 1'b1) errors++;
cts_allow = 1;
@(posedge clk); // pending byte accepts now, not during the stall
@(negedge clk); #1 tx_valid = 0;
repeat (11 * CPB) @(negedge clk);
// CTS may also fall exactly before the STOP acceptance edge.
offer(8'hff);
@(negedge clk); #1; tx_valid = 1; tx_data = 8'h3c;
// Wait for the last stop-bit slot; drive CTS before its rising edge.
boundary_guard = 0;
while (!(dut.state == dut.STOP && dut.tick) && boundary_guard < 11 * CPB) begin
@(negedge clk); #1; boundary_guard++;
end
if (boundary_guard == 11 * CPB) $fatal(1, "UART STOP boundary not reached");
cts_allow = 0;
@(posedge clk);
if (tx_ready !== 1'b0) errors++;
@(negedge clk); #1;
if (tx !== 1'b1 || tx_valid !== 1'b1 || tx_data !== 8'h3c) errors++;
cts_allow = 1;
@(posedge clk);
@(negedge clk); #1 tx_valid = 0;
repeat (11 * CPB) @(negedge clk);
manual = 0; stop_driver = 0;
repeat (200 * 10 * CPB) @(posedge clk); // random gaps
#2; back_to_back = 1; frames_before = frames_checked;
repeat (200 * 10 * CPB) @(posedge clk); // continuous stream
#2;
frames_in_burst = frames_checked - frames_before;
stop_driver = 1;
repeat (30 * CPB) begin @(posedge clk); #2; end
if (frames_checked + cancelled != accepted || cancelled != 1) errors++;
if (sample_no >= 0 || tx_valid || sent.size() != 0 || tx !== 1'b1) errors++;
if (frames_in_burst < 199 || burst_pairs < 198) errors++;
finished = 1;
end
endmodule
module tb_uart;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
int e1, e2, e3, e434;
bit f1, f2, f3, f434;
uart_env #(1) u1(clk, e1, f1);
uart_env #(2) u2(clk, e2, f2);
uart_env #(3) u3(clk, e3, f3);
uart_env #(434) u434(clk, e434, f434);
initial begin
wait (f1 && f2 && f3 && f434);
if (e1 + e2 + e3 + e434 != 0) $fatal(1, "UART sweep FAIL: %0d %0d %0d %0d", e1, e2, e3, e434);
$display("PASS: CPB=1,2,3,434; full frames, exact burst spacing, CTS and reset cancellation");
$finish;
end
initial begin
repeat (5000 * 10 * 434) @(posedge clk);
$fatal(1, "UART sweep watchdog");
end
endmodule