Asynchronous Serial Communication and Configurable Verilog UART Design

RS232 and RS485 Standards

Serial communication serves as a foundational protocol for hardware interfaces. RS232 utilizes dedicated transmission (TX) and reception (RX) lines for full-duplex operation. Data transfer speed is defined by the baud rate (bits per second), with standard frequencies including 9600, 38400, and 115200 bps.

RS485 offers distinct advantages over RS232:

  • Electrical Characteristics: RS232 relies on single-ended signaling with negative voltage for logic '1' and positive for logic '0'. RS485 employs differential signaling across A and B lines, providing superior noise immunity and supporting multi-drop configurations.
  • Distance and Speed: RS232 is limited to short distances (tens of meters) and speeds up to 115.2 kbps. RS485 can span thousands of meters and reach speeds up to 10 Mbps.
  • Topology: RS232 is strictly point-to-point, whereas RS485 supports bus topologies with multiple nodes.
  • Duplex Mode: RS232 operates in full-duplex. RS485 can function in either half-duplex or full-duplex modes depending on the wiring.

UART Frame Timing

The serial data frame follows a strict sequence:

  1. Idle State: The bus remains high.
  2. Start Bit: A single low bit indicates the beginning of a frame.
  3. Data Bits: Typically 8 bits, transmitted LSB first.
  4. Parity Bit: Optional error detection. Even parity ensures the total count of '1's is even; odd parity ensures it is odd. Often omitted in favor of protocol-level checksums.
  5. Stop Bit: One or two high bits signal the end of the frame.

A standard 8-bit frame without parity requires 10 clock cycles (1 start + 8 data + 1 stop). Including parity increases this to 11 cycles.

Verilog Implementation: Configurable UART Core

The following module implements a highly configurable UART transmitter and receiver, supporting variable baud rates, data widths, parity generation/checking, and stop bit lengths.

module uart_core #(
    parameter CLK_FREQ = 100_000_000,
    parameter BAUD_RATE = 115200
)(
    input  wire        clk_i,
    input  wire        rst_n_i,

    // Configuration
    input  wire [1:0]  cfg_data_len_i, // 00:5, 01:6, 10:7, 11:8
    input  wire [1:0]  cfg_parity_i,   // 00:None, 01:Odd, 10:Even
    input  wire        cfg_stop_len_i, // 0:1-bit, 1:2-bit

    // TX Interface
    input  wire [7:0]  tx_data_i,
    input  wire        tx_valid_i,
    output wire        tx_ready_o,
    output reg         tx_pin_o,

    // RX Interface
    output reg  [7:0]  rx_data_o,
    output reg         rx_valid_o,
    input  wire        rx_pin_i
);

    localparam DIV_FACTOR = CLK_FREQ / BAUD_RATE;

    // --- TX Logic ---
    localparam TX_IDLE = 3'd0, TX_START = 3'd1, TX_DATA = 3'd2, 
                 TX_PAR  = 3'd3, TX_STOP1 = 3'd4, TX_STOP2 = 3'd5;

    reg [2:0]  tx_state;
    reg [31:0] tx_clk_cnt;
    reg [3:0]  tx_bit_idx;
    reg [7:0]  tx_shift_reg;
    reg        tx_parity_bit;

    assign tx_ready_o = (tx_state == TX_IDLE);

    always @(posedge clk_i or negedge rst_n_i) begin
        if (!rst_n_i) begin
            tx_state    <= TX_IDLE;
            tx_pin_o    <= 1'b1;
            tx_clk_cnt  <= 0;
            tx_bit_idx  <= 0;
            tx_shift_reg <= 0;
        end else begin
            case (tx_state)
                TX_IDLE: begin
                    tx_pin_o <= 1'b1;
                    tx_clk_cnt <= 0;
                    if (tx_valid_i && tx_ready_o) begin
                        tx_shift_reg <= tx_data_i;
                        tx_state <= TX_START;
                    end
                end
                TX_START: begin
                    if (tx_clk_cnt == DIV_FACTOR - 1) begin
                        tx_clk_cnt <= 0;
                        tx_pin_o <= 1'b0; // Start bit
                        tx_state <= TX_DATA;
                        tx_bit_idx <= 0;
                    end else tx_clk_cnt <= tx_clk_cnt + 1;
                end
                TX_DATA: begin
                    if (tx_clk_cnt == DIV_FACTOR - 1) begin
                        tx_clk_cnt <= 0;
                        tx_pin_o <= tx_shift_reg[0];
                        tx_shift_reg <= {1'b0, tx_shift_reg[7:1]};
                        tx_bit_idx <= tx_bit_idx + 1;
                        case (cfg_data_len_i)
                            2'b00: if (tx_bit_idx == 4) tx_state <= TX_PAR;
                            2'b01: if (tx_bit_idx == 5) tx_state <= TX_PAR;
                            2'b10: if (tx_bit_idx == 6) tx_state <= TX_PAR;
                            2'b11: if (tx_bit_idx == 7) tx_state <= TX_PAR;
                            default: tx_state <= TX_PAR;
                        endcase
                    end else tx_clk_cnt <= tx_clk_cnt + 1;
                end
                TX_PAR: begin
                    if (tx_clk_cnt == DIV_FACTOR - 1) begin
                        tx_clk_cnt <= 0;
                        case (cfg_data_len_i)
                            2'b00: tx_parity_bit = ^tx_data_i[4:0];
                            2'b01: tx_parity_bit = ^tx_data_i[5:0];
                            2'b10: tx_parity_bit = ^tx_data_i[6:0];
                            2'b11: tx_parity_bit = ^tx_data_i[7:0];
                        endcase
                        case (cfg_parity_i)
                            2'b00: tx_pin_o <= 1'b1; // None
                            2'b01: tx_pin_o <= ~tx_parity_bit; // Odd
                            2'b10: tx_pin_o <= tx_parity_bit;  // Even
                            default: tx_pin_o <= 1'b1;
                        endcase
                        tx_state <= TX_STOP1;
                    end else tx_clk_cnt <= tx_clk_cnt + 1;
                end
                TX_STOP1: begin
                    if (tx_clk_cnt == DIV_FACTOR - 1) begin
                        tx_clk_cnt <= 0;
                        tx_pin_o <= 1'b1;
                        if (!cfg_stop_len_i) tx_state <= TX_IDLE;
                        else tx_state <= TX_STOP2;
                    end else tx_clk_cnt <= tx_clk_cnt + 1;
                end
                TX_STOP2: begin
                    if (tx_clk_cnt == DIV_FACTOR - 1) begin
                        tx_clk_cnt <= 0;
                        tx_state <= TX_IDLE;
                    end else tx_clk_cnt <= tx_clk_cnt + 1;
                end
            endcase
        end
    end

    // --- RX Logic ---
    localparam RX_IDLE = 3'd0, RX_START = 3'd1, RX_DATA = 3'd2,
                 RX_PAR  = 3'd3, RX_STOP1 = 3'd4, RX_STOP2 = 3'd5;

    reg [2:0]  rx_state;
    reg [31:0] rx_clk_cnt;
    reg [3:0]  rx_bit_idx;
    reg [7:0]  rx_shift_reg;

    // Synchronizer
    reg rx_sync_0, rx_sync_1;
    always @(posedge clk_i or negedge rst_n_i) begin
        if (!rst_n_i) begin
            rx_sync_0 <= 1'b1;
            rx_sync_1 <= 1'b1;
        end else begin
            rx_sync_0 <= rx_pin_i;
            rx_sync_1 <= rx_sync_0;
        end
    end

    always @(posedge clk_i or negedge rst_n_i) begin
        if (!rst_n_i) begin
            rx_state   <= RX_IDLE;
            rx_clk_cnt <= 0;
            rx_bit_idx <= 0;
            rx_shift_reg <= 0;
            rx_data_o  <= 0;
            rx_valid_o <= 0;
        end else begin
            rx_valid_o <= 0;
            case (rx_state)
                RX_IDLE: begin
                    rx_clk_cnt <= 0;
                    rx_bit_idx <= 0;
                    if (rx_sync_1 == 1'b0) begin
                        rx_state <= RX_START;
                    end
                end
                RX_START: begin
                    if (rx_clk_cnt == (DIV_FACTOR/2) - 1) begin
                        rx_clk_cnt <= 0;
                        if (rx_sync_1 == 1'b0) rx_state <= RX_DATA;
                        else rx_state <= RX_IDLE;
                    end else rx_clk_cnt <= rx_clk_cnt + 1;
                end
                RX_DATA: begin
                    if (rx_clk_cnt == DIV_FACTOR - 1) begin
                        rx_clk_cnt <= 0;
                        rx_shift_reg <= {rx_sync_1, rx_shift_reg[7:1]};
                        rx_bit_idx <= rx_bit_idx + 1;
                        case (cfg_data_len_i)
                            2'b00: if (rx_bit_idx == 4) rx_state <= (cfg_parity_i == 2'b00) ? RX_STOP1 : RX_PAR;
                            2'b01: if (rx_bit_idx == 5) rx_state <= (cfg_parity_i == 2'b00) ? RX_STOP1 : RX_PAR;
                            2'b10: if (rx_bit_idx == 6) rx_state <= (cfg_parity_i == 2'b00) ? RX_STOP1 : RX_PAR;
                            2'b11: if (rx_bit_idx == 7) rx_state <= (cfg_parity_i == 2'b00) ? RX_STOP1 : RX_PAR;
                            default: rx_state <= RX_PAR;
                        endcase
                    end else rx_clk_cnt <= rx_clk_cnt + 1;
                end
                RX_PAR: begin
                    if (rx_clk_cnt == DIV_FACTOR - 1) begin
                        rx_clk_cnt <= 0;
                        rx_state <= RX_STOP1;
                    end else rx_clk_cnt <= rx_clk_cnt + 1;
                end
                RX_STOP1: begin
                    if (rx_clk_cnt == DIV_FACTOR - 1) begin
                        rx_clk_cnt <= 0;
                        if (!cfg_stop_len_i) begin
                            rx_data_o <= rx_shift_reg;
                            rx_valid_o <= 1'b1;
                            rx_state <= RX_IDLE;
                        end else rx_state <= RX_STOP2;
                    end else rx_clk_cnt <= rx_clk_cnt + 1;
                end
                RX_STOP2: begin
                    if (rx_clk_cnt == DIV_FACTOR - 1) begin
                        rx_clk_cnt <= 0;
                        rx_data_o <= rx_shift_reg;
                        rx_valid_o <= 1'b1;
                        rx_state <= RX_IDLE;
                    end else rx_clk_cnt <= rx_clk_cnt + 1;
                end
            endcase
        end
    end

endmodule

Simulation Waveform Analysis

During FPGA simulation, the transmission sequence is observed as follows:

  • tx_valid_i pulses high for one clock cycle, loading the data payload (e.g., 0x12) into the core.
  • tx_pin_o drives low for the start bit, followed by the LSB-first data bits (0b01001000), and finally drives high for the stop bit.
  • On the receiver side, rx_valid_o asserts high once the full frame has been sampled and decoded.

Tags: UART RS232 RS485 FPGA Verilog

Posted on Tue, 22 Sep 2026 16:43:42 +0000 by Stevis2002