FPGA--UART通信协议

引言:串行 vs. 并行

并行接口 (Parallel Interface): 在同一时刻,通过多条数据线同时传输多个数据位。优点是速度快(理论上),逻辑简单。缺点是需要大量引脚和导线,远距离传输时容易出现位间偏斜(Skew,即不同数据位到达时间不一致)和串扰,成本高,难以实现高频率。  

串行接口 (Serial Interface): 在一条或一对数据线上,将数据一位一位地按顺序传输

优点是引脚少,布线简单,成本低,易于解决远距离传输的偏斜问题,可以通过差分信号等技术轻松实现极高频率。如今,高速串行接口已成为主流。

1. UART

概述 通用异步收发传输器,是一种非常普遍的异步、点对点、全双工串行通信协议。常用于设备间的低速通信,如PC的COM口、微控制器与外设(GPS、蓝牙模块)的通信。    

物理层与信号线

2根线 `TXD` (Transmit Data): 发送数据线。

`RXD` (Receive Data): 接收数据线。

连接: A设备的TXD连接到B设备的RXD,A设备的RXD连接到B设备的TXD,并共地(GND)。

信号电平: 常用TTL/CMOS电平(如0V, 3.3V/5V)。长距离或恶劣环境下会用RS-232, RS-485等电平标准转换。

工作原理 “异步”是其核心。通信双方没有共享的时钟线。为了同步数据,双方必须事先约定好相同的通信速率(波特率,Baud Rate)。数据传输以数据帧 (Frame)的形式进行。

通信协议/传输步骤 一个UART数据帧包含以下部分,按时序依次传输:

1. 空闲状态 (Idle): 数据线保持高电平。

2. 起始位 (Start Bit): 发送方将数据线从高电平拉低,并保持1个比特时间。接收方检测到这个下降沿后,开始准备接收数据。

3. 数据位 (Data Bits): 紧跟起始位,传输有效数据。通常是5到8位,低位(LSB)在前。

4. 校验位 (Parity Bit) (可选): 用于简单的数据校验。可以是奇校验、偶校验或无校验

5. 停止位 (Stop Bit(s)): 数据传输结束后,发送方将数据线拉高,并保持1、1.5或2个比特时间。这标志着一帧的结束,并为下一帧的起始位提供了必要的空闲时间。

       使用verilog实现UART的收发,波特率为115200。实现FPGA与PC通信,命令格式:0x0A 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0A。

       第一部分:Verilog 代码

       1. `uart_rx.v` (UART接收模块)

`timescale 1ns / 1ps

module uart_rx #(
    parameter CLK_FREQ = 50_000_000,
    parameter BAUD_RATE = 115200
)(
    input  wire i_clk,
    input  wire i_rst_n,
    input  wire i_rx_serial,
    output reg  [7:0] o_rx_byte,
    output reg  o_rx_dv
);

    // 计算每个比特位需要多少个系统时钟周期
    localparam CLKS_PER_BIT = CLK_FREQ / BAUD_RATE;
   
    // FSM 状态定义
    localparam [1:0] IDLE     = 2'b00;
    localparam [1:0] START_BIT= 2'b01;
    localparam [1:0] DATA_BITS= 2'b10;
    localparam [1:0] STOP_BIT = 2'b11;

    reg [1:0] r_state = IDLE;
   
    // 计数器
    reg [$clog2(CLKS_PER_BIT)-1:0] r_clk_cnt = 0; // 以2为底的对数的向上取整
    reg [3:0] r_bit_cnt = 0; // 0-7 for data, 8 for stop
   
    // 寄存器
    reg [7:0] r_rx_byte = 8'b0;
    reg r_rx_dv = 1'b0;
   
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_state   <= IDLE;
            r_clk_cnt <= 0;
            r_bit_cnt <= 0;
            o_rx_byte <= 8'b0;
            o_rx_dv   <= 1'b0;
            r_rx_byte <= 8'b0;
            r_rx_dv   <= 1'b0;
        end else begin
            // 默认情况下,数据有效信号只持续一个周期
            r_rx_dv <= 1'b0;
            o_rx_dv <= r_rx_dv;

            case (r_state)
                IDLE: begin
                    if (i_rx_serial == 1'b0) begin
                        // 检测到下降沿,可能是起始位
                        r_clk_cnt <= 0;
                        r_state   <= START_BIT;
                    end
                end
               
                START_BIT: begin
                    if (r_clk_cnt == (CLKS_PER_BIT / 2) - 1) begin
                        // 到达比特位中间,再次确认
                        if (i_rx_serial == 1'b0) begin
                            // 确认是起始位,准备接收数据
                            r_clk_cnt <= 0;
                            r_bit_cnt <= 0;
                            r_state   <= DATA_BITS;
                        end else begin
                            // 是毛刺,回到IDLE
                            r_state <= IDLE;
                        end
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                end
               
                DATA_BITS: begin
                    if (r_clk_cnt == CLKS_PER_BIT - 1) begin
                        r_clk_cnt <= 0;
                        // 将接收到的位存入寄存器 (LSB first)
                        r_rx_byte <= {i_rx_serial, r_rx_byte[7:1]};
                       
                        if (r_bit_cnt == 7) begin
                            r_bit_cnt <= 0;
                            r_state   <= STOP_BIT;
                        end else begin
                            r_bit_cnt <= r_bit_cnt + 1;
                        end
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                end
               
                STOP_BIT: begin
                    if (r_clk_cnt == CLKS_PER_BIT - 1) begin
                        // 此处应检查停止位是否为高电平,可添加错误标志
                        o_rx_byte <= r_rx_byte;
                        r_rx_dv   <= 1'b1; // 数据准备好了!
                        o_rx_dv   <= 1'b1;
                        r_state   <= IDLE;
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                end
               
                default: r_state <= IDLE;
            endcase
        end
    end
endmodule

       2. `uart_tx.v` (UART发送模块)

`timescale 1ns / 1ps

module uart_tx #(
    parameter CLK_FREQ = 50_000_000,
    parameter BAUD_RATE = 115200
)(
    input  wire i_clk,
    input  wire i_rst_n,
    input  wire [7:0] i_tx_byte,
    input  wire i_tx_dv,
    output reg  o_tx_serial,
    output wire o_tx_busy
);
   
    localparam CLKS_PER_BIT = CLK_FREQ / BAUD_RATE;

    localparam [1:0] IDLE     = 2'b00;
    localparam [1:0] START_BIT= 2'b01;
    localparam [1:0] DATA_BITS= 2'b10;
    localparam [1:0] STOP_BIT = 2'b11;

    reg [1:0] r_state = IDLE;
   
    reg [$clog2(CLKS_PER_BIT)-1:0] r_clk_cnt = 0;
    reg [3:0] r_bit_cnt = 0;
    reg [8:0] r_tx_shift_reg = 9'b0; // 8 data bits + 1 stop bit
   
    assign o_tx_busy = (r_state != IDLE);
   
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_state     <= IDLE;
            o_tx_serial <= 1'b1; // 空闲时为高电平
            r_clk_cnt   <= 0;
            r_bit_cnt   <= 0;
        end else begin
            case (r_state)
                IDLE: begin
                    o_tx_serial <= 1'b1;
                    if (i_tx_dv) begin
                        // 准备发送,加载数据 (Stop bit + Data bits LSB first)
                        r_tx_shift_reg <= {1'b1, i_tx_byte};
                        r_clk_cnt      <= 0;
                        r_bit_cnt      <= 0;
                        o_tx_serial    <= 1'b0; // 发送起始位
                        r_state        <= START_BIT;
                    end
                end
               
                START_BIT: begin
                    if (r_clk_cnt == CLKS_PER_BIT - 1) begin
                        r_clk_cnt <= 0;
                        r_state   <= DATA_BITS;
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                    o_tx_serial <= 1'b0; // 保持起始位
                end
               
                DATA_BITS: begin
                    o_tx_serial <= r_tx_shift_reg[0]; // 发送当前位
                    if (r_clk_cnt == CLKS_PER_BIT - 1) begin
                        r_clk_cnt      <= 0;
                        r_tx_shift_reg <= r_tx_shift_reg >> 1; // 移位
                       
                        if (r_bit_cnt == 8) begin // 8 data bits + 1 stop bit sent
                            r_bit_cnt <= 0;
                            r_state   <= STOP_BIT;
                        end else begin
                            r_bit_cnt <= r_bit_cnt + 1;
                        end
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                end
               
                STOP_BIT: begin
                    o_tx_serial <= 1'b1; // 发送停止位
                    if (r_clk_cnt == CLKS_PER_BIT - 1) begin
                        r_clk_cnt <= 0;
                        r_state   <= IDLE;
                    end else begin
                        r_clk_cnt <= r_clk_cnt + 1;
                    end
                end
               
                default: r_state <= IDLE;
            endcase
        end
    end
endmodule

3. `uart_top.v` (顶层逻辑模块)`

`timescale 1ns / 1ps

module uart_top #(
    parameter CLK_FREQ = 50_000_000,
    parameter BAUD_RATE = 115200
)(
    input  wire i_clk,
    input  wire i_rst_n,
    input  wire i_uart_rx,
    output wire o_uart_tx
);

    // 内部连线 
    wire [7:0] w_rx_byte;
    wire       w_rx_dv;
   
    reg  [7:0] r_tx_byte;
    reg        r_tx_dv;
    wire       w_tx_busy;

    // 实例化
    uart_rx #(
        .CLK_FREQ(CLK_FREQ),
        .BAUD_RATE(BAUD_RATE)
    ) u_uart_rx (
        .i_clk(i_clk),
        .i_rst_n(i_rst_n),
        .i_rx_serial(i_uart_rx),
        .o_rx_byte(w_rx_byte),
        .o_rx_dv(w_rx_dv)
    );

    uart_tx #(
        .CLK_FREQ(CLK_FREQ),
        .BAUD_RATE(BAUD_RATE)
    ) u_uart_tx (
        .i_clk(i_clk),
        .i_rst_n(i_rst_n),
        .i_tx_byte(r_tx_byte),
        .i_tx_dv(r_tx_dv),
        .o_tx_serial(o_uart_tx),
        .o_tx_busy(w_tx_busy)
    );

    // --- 命令解析 FSM ---
    // 状态定义 
    localparam [3:0] S_IDLE       = 4'h0;
    localparam [3:0] S_HEADER     = 4'h1; // 注意:此状态现在也用于接收第一个数据字节
    localparam [3:0] S_DATA       = 4'h2;
    localparam [3:0] S_CS0        = 4'h3; // 接收Checksum MSB
    localparam [3:0] S_CS1        = 4'h4; // 接收Checksum LSB
    localparam [3:0] S_CMD_VALID  = 4'h5;
   
    reg [3:0] r_cmd_state = S_IDLE;
    reg [3:0] r_data_cnt = 0;
    reg [7:0] r_data_buf [0:7];

    reg [15:0] r_checksum_calc;

    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_cmd_state     <= S_IDLE;
            r_data_cnt      <= 0;
            r_checksum_calc <= 16'h0000;
        end else begin
            if (w_rx_dv) begin // 只有当接收到新字节时才处理
                case (r_cmd_state)
                    S_IDLE: begin
                        if (w_rx_byte == 8'h0A) begin
                            // 检测到包头,初始化累加和 ***
                            r_checksum_calc <= 16'h000A;
                            r_data_cnt      <= 0; // 清零数据计数器
                            r_cmd_state     <= S_DATA;
                        end
                    end
                   
                    S_DATA: begin
                        // 存储数据并累加 ***
                        r_data_buf[r_data_cnt] <= w_rx_byte;
                        r_checksum_calc        <= r_checksum_calc + w_rx_byte;
                       
                        if (r_data_cnt == 7) begin
                            r_data_cnt  <= 0; // 为下次做准备
                            r_cmd_state <= S_CS0; // 8个数据字节接收完毕,准备接收校验和
                        end else begin
                            r_data_cnt <= r_data_cnt + 1;
                        end
                    end
                   
                    S_CS0: begin
                        // 比较校验和高8位 (MSB) 
                        if (w_rx_byte == r_checksum_calc[15:8]) begin
                            r_cmd_state <= S_CS1; // 高位匹配,继续
                        end else begin
                            r_cmd_state <= S_IDLE; // 校验失败,复位状态机
                        end
                    end
                   
                    S_CS1: begin
                        // 比较校验和低8位 (LSB) 
                        if (w_rx_byte == r_checksum_calc[7:0]) begin
                            r_cmd_state <= S_CMD_VALID; // 校验成功!命令有效
                        end else begin
                            r_cmd_state <= S_IDLE; // 校验失败,复位状态机
                        end
                    end        
                   
                    default: r_cmd_state <= S_IDLE;
                endcase
            end else if (r_cmd_state == S_CMD_VALID) begin
                // 命令有效后的处理逻辑 
                if (!w_tx_busy) begin 
                    r_tx_byte   <= r_data_buf[0] + 1;
                    r_tx_dv     <= 1'b1;
                    r_cmd_state <= S_IDLE;
                end
            end
           
            // r_tx_dv 只持续一个周期
            if(r_tx_dv) begin
                r_tx_dv <= 1'b0;
            end
        end
    end
endmodule

       第二部分:Verilog Testbench (`tb_uart_top.v`) 代码

`timescale 1ns / 1ps

module tb_uart_top;

    // --- 参数定义 ---
    // 必须与DUT的参数保持一致
    localparam CLK_FREQ    = 50_000_000;
    localparam BAUD_RATE   = 115200;
   
    // 计算时钟周期和比特周期,方便后续使用
    localparam CLK_PERIOD  = 1_000_000_000 / CLK_FREQ; // in ns
    localparam BIT_PERIOD  = CLK_FREQ / BAUD_RATE;     // in clk cycles

    // --- 信号声明 ---
    reg  r_clk;
    reg  r_rst_n;
    reg  r_uart_rx; // Testbench的发送端,连接到DUT的接收端
    wire w_uart_tx; // Testbench的接收端,连接到DUT的发送端

    // --- 实例化DUT ---
    uart_top #(
        .CLK_FREQ(CLK_FREQ),
        .BAUD_RATE(BAUD_RATE)
    ) u_dut (
        .i_clk(r_clk),
        .i_rst_n(r_rst_n),
        .i_uart_rx(r_uart_rx),
        .o_uart_tx(w_uart_tx)
    );

    // --- 时钟生成 ---
    initial begin
        r_clk = 0;
        forever #(CLK_PERIOD / 2) r_clk = ~r_clk;
    end

    // --- 复位生成 ---
    initial begin
        r_rst_n = 0;
        r_uart_rx = 1; // UART空闲时为高电平
        #200;
        r_rst_n = 1;
    end

    // --- 任务:发送一个字节 ---
    task send_byte(input [7:0] data);
    begin
        // Start bit
        r_uart_rx = 0;
        #(BIT_PERIOD * CLK_PERIOD);

        // Data bits (LSB first)
        for (integer i = 0; i < 8; i = i + 1) begin
            r_uart_rx = data[i];
            #(BIT_PERIOD * CLK_PERIOD);
        end

        // Stop bit
        r_uart_rx = 1;
        #(BIT_PERIOD * CLK_PERIOD);
    end
    endtask

    // --- 任务:接收一个字节 ---
    task receive_byte(output reg [7:0] data);
    begin
        // Wait for start bit
        @(negedge w_uart_tx);

        // Wait half bit period to sample in the middle of start bit
        #( (BIT_PERIOD / 2) * CLK_PERIOD );

        // Sample data bits
        for (integer i = 0; i < 8; i = i + 1) begin
            #(BIT_PERIOD * CLK_PERIOD);
            data[i] = w_uart_tx;
        end

        // Wait for stop bit
        #(BIT_PERIOD * CLK_PERIOD);
    end
    endtask

    // --- 主要测试序列 ---
    initial begin
        reg [7:0] received_byte;
        integer   timeout;

        // 等待复位完成
        @(posedge r_rst_n);
        #1000;

        $display("--------------------------------------------------");
        $display("TB INFO: Starting UART Communication Test...");
        $display("--------------------------------------------------");

        // --- Test Case 1: Valid Command Frame ---
        $display("TB INFO: Test Case 1: Sending a valid command...");
        // Command: 0A 01 02 03 F0 00 00 00 00 01 00
        send_byte(8'h0A);
        send_byte(8'h01);
        send_byte(8'h02);
        send_byte(8'h03);
        send_byte(8'hF0);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h01); // Checksum MSB
        send_byte(8'h00); // Checksum LSB

        $display("TB INFO: Command sent. Waiting for response...");
        receive_byte(received_byte);

        if (received_byte == 8'h02) begin // Expected response is 0x01 + 1
            $display("TB PASS: Test Case 1 Passed. Received expected response: 0x%0h", received_byte);
        end else begin
            $display("TB FAIL: Test Case 1 Failed. Expected 0x02, but received 0x%0h", received_byte);
        end
        #2000; // Wait a bit before next test

        // --- Test Case 2: Invalid Checksum ---
        $display("--------------------------------------------------");
        $display("TB INFO: Test Case 2: Sending command with bad checksum...");
        // Command: 0A 0F 02 03 F0 00 00 00 00 01 0F (Correct checksum is 01 0E)
        send_byte(8'h0A);
        send_byte(8'h0F);
        send_byte(8'h02);
        send_byte(8'h03);
        send_byte(8'hF0);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h01); // Checksum MSB
        send_byte(8'h0F); // BAD Checksum LSB

        $display("TB INFO: Invalid command sent. Expecting NO response...");
       
        // Use a timeout to check for the absence of a response
        timeout = 0;
        fork
            begin
                receive_byte(received_byte);
                timeout = 1; // If receive_byte completes, set flag
            end
            begin
                #(BIT_PERIOD * 20 * CLK_PERIOD); // Wait for ~2 frames time
                timeout = 2; // If this completes first, it's a timeout
            end
        join_any
       
        if (timeout == 1) begin
            $display("TB FAIL: Test Case 2 Failed. DUT responded (0x%0h) when it should not have.", received_byte);
        end else if (timeout == 2) begin
            $display("TB PASS: Test Case 2 Passed. DUT correctly ignored the bad command.");
        end
        #2000;

        // --- Test Case 3: Invalid Header ---
        $display("--------------------------------------------------");
        $display("TB INFO: Test Case 3: Sending command with bad header...");
        // Command: 0B 01 02 ...
        send_byte(8'h0B); // BAD Header
        send_byte(8'h01);
        send_byte(8'h02);
        send_byte(8'h03);
        send_byte(8'hF0);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h00);
        send_byte(8'h01);
        send_byte(8'h01);

        $display("TB INFO: Invalid command sent. Expecting NO response...");

        timeout = 0;
        fork
            begin
                receive_byte(received_byte);
                timeout = 1;
            end
            begin
                #(BIT_PERIOD * 20 * CLK_PERIOD);
                timeout = 2;
            end
        join_any
       
        if (timeout == 1) begin
            $display("TB FAIL: Test Case 3 Failed. DUT responded (0x%0h) when it should not have.", received_byte);
        end else if (timeout == 2) begin
            $display("TB PASS: Test Case 3 Passed. DUT correctly ignored the bad command.");
        end
       
        $display("--------------------------------------------------");
        $display("TB INFO: All tests completed.");
        $finish; // End the simulation
    end

endmodule





原文链接:,转发请注明来源!