引言:串行 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