别再只会调库了!手把手教你用Verilog从零实现一个可配置的UART收发器(附完整代码)
从零构建可配置UART收发器的Verilog实战指南在数字电路设计中UART通用异步收发器作为最基础的串行通信协议之一其重要性不言而喻。许多工程师虽然能够熟练调用现成的UART IP核但对底层实现原理却知之甚少。本文将带领读者从零开始用Verilog实现一个完全可配置的UART收发器支持5-8位数据宽度、可调波特率和可选的奇偶校验功能。1. UART核心模块架构设计一个完整的UART系统主要由三个关键模块组成波特率发生器、发送器和接收器。我们先来看整体架构module uart_top #( parameter DATA_WIDTH 8, parameter STOP_BITS 1, parameter PARITY_EN 1, parameter CLK_FREQ 100_000_000, parameter BAUD_RATE 115200 )( input clk, input rst_n, input tx_start, input [DATA_WIDTH-1:0] tx_data, output tx_done, output tx_busy, input rx_in, output [DATA_WIDTH-1:0] rx_data, output rx_valid, output rx_error ); // 内部信号声明 wire baud_tick; wire [15:0] baud_div; // 实例化子模块 baud_generator #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE) ) u_baud_gen ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .baud_div(baud_div) ); uart_tx #( .DATA_WIDTH(DATA_WIDTH), .STOP_BITS(STOP_BITS), .PARITY_EN(PARITY_EN) ) u_tx ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .tx_start(tx_start), .tx_data(tx_data), .tx_out(tx_out), .tx_done(tx_done), .tx_busy(tx_busy) ); uart_rx #( .DATA_WIDTH(DATA_WIDTH), .STOP_BITS(STOP_BITS), .PARITY_EN(PARITY_EN) ) u_rx ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .rx_in(rx_in), .rx_data(rx_data), .rx_valid(rx_valid), .rx_error(rx_error) ); endmodule这个顶层模块通过参数化设计实现了高度可配置性DATA_WIDTH: 数据位宽度(5-8位)STOP_BITS: 停止位数量(1或2位)PARITY_EN: 奇偶校验使能CLK_FREQ: 系统时钟频率BAUD_RATE: 波特率设置2. 波特率生成器的精确实现波特率生成器的核心是一个分频计数器它将系统时钟分频到所需的波特率时钟。这里的关键是避免累积误差module baud_generator #( parameter CLK_FREQ 100_000_000, parameter BAUD_RATE 115200 )( input clk, input rst_n, output reg baud_tick, output [15:0] baud_div ); // 计算分频系数 localparam DIVIDER CLK_FREQ / BAUD_RATE; assign baud_div DIVIDER; reg [15:0] counter; always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 0; baud_tick 0; end else begin if (counter DIVIDER - 1) begin counter 0; baud_tick 1; end else begin counter counter 1; baud_tick 0; end end end endmodule注意实际设计中需要考虑系统时钟频率与波特率的整数倍关系。当不能整除时可以采用累加器实现小数分频。3. UART发送器的状态机设计发送器采用有限状态机(FSM)实现状态转换图如下IDLE → START → DATA0 → DATA1 → ... → DATAn → PARITY → STOP1 → STOP2(可选) → IDLE对应的Verilog实现module uart_tx #( parameter DATA_WIDTH 8, parameter STOP_BITS 1, parameter PARITY_EN 1 )( input clk, input rst_n, input baud_tick, input tx_start, input [DATA_WIDTH-1:0] tx_data, output reg tx_out, output reg tx_done, output reg tx_busy ); // 状态定义 typedef enum { IDLE, START, DATA, PARITY, STOP } state_t; state_t current_state, next_state; reg [3:0] bit_counter; reg [DATA_WIDTH-1:0] data_reg; reg parity_bit; // 状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else if (baud_tick) current_state next_state; end // 下一状态逻辑 always (*) begin case (current_state) IDLE: next_state tx_start ? START : IDLE; START: next_state DATA; DATA: begin if (bit_counter DATA_WIDTH - 1) next_state PARITY_EN ? PARITY : STOP; else next_state DATA; end PARITY: next_state STOP; STOP: next_state IDLE; default: next_state IDLE; endcase end // 输出逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_out 1b1; tx_done 1b0; tx_busy 1b0; bit_counter 0; data_reg 0; parity_bit 0; end else if (baud_tick) begin case (next_state) IDLE: begin tx_out 1b1; tx_done 1b0; tx_busy 1b0; if (tx_start) begin data_reg tx_data; tx_busy 1b1; // 计算奇偶校验位 if (PARITY_EN) parity_bit ^tx_data; end end START: begin tx_out 1b0; // 起始位 bit_counter 0; end DATA: begin tx_out data_reg[bit_counter]; bit_counter bit_counter 1; end PARITY: tx_out parity_bit; STOP: begin tx_out 1b1; if (current_state STOP (STOP_BITS 1 || bit_counter 1)) tx_done 1b1; end endcase end end endmodule4. UART接收器的抗干扰设计接收器设计的关键在于起始位检测和数据的抗干扰采样。我们采用16倍过采样和多数表决机制来提高可靠性module uart_rx #( parameter DATA_WIDTH 8, parameter STOP_BITS 1, parameter PARITY_EN 1 )( input clk, input rst_n, input baud_tick, input rx_in, output reg [DATA_WIDTH-1:0] rx_data, output reg rx_valid, output reg rx_error ); // 状态定义 typedef enum { IDLE, START, DATA, PARITY, STOP } state_t; state_t current_state, next_state; reg [3:0] bit_counter; reg [DATA_WIDTH-1:0] data_reg; reg [3:0] sample_counter; reg [7:0] samples; reg parity_bit, parity_error; // 多数表决函数 function majority_vote; input [7:0] samples; begin // 统计1的个数 integer i, count; count 0; for (i 0; i 8; i i 1) if (samples[i]) count count 1; majority_vote (count 4) ? 1b1 : 1b0; end endfunction // 状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else if (baud_tick) current_state next_state; end // 下一状态逻辑 always (*) begin case (current_state) IDLE: next_state (majority_vote(samples) 1b0) ? START : IDLE; START: next_state DATA; DATA: begin if (bit_counter DATA_WIDTH - 1) next_state PARITY_EN ? PARITY : STOP; else next_state DATA; end PARITY: next_state STOP; STOP: next_state IDLE; default: next_state IDLE; endcase end // 采样逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin samples 8hFF; sample_counter 0; end else begin if (sample_counter 15) sample_counter 0; else sample_counter sample_counter 1; // 在采样点附近采集多个样本 if (sample_counter 4 sample_counter 11) samples[sample_counter-4] rx_in; end end // 输出逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_data 0; rx_valid 1b0; rx_error 1b0; bit_counter 0; data_reg 0; parity_bit 0; parity_error 0; end else if (baud_tick sample_counter 15) begin case (next_state) IDLE: begin rx_valid 1b0; rx_error 1b0; end START: begin bit_counter 0; parity_bit 0; end DATA: begin data_reg[bit_counter] majority_vote(samples); bit_counter bit_counter 1; // 更新奇偶校验 if (PARITY_EN) parity_bit parity_bit ^ majority_vote(samples); end PARITY: begin parity_error (majority_vote(samples) ! parity_bit); end STOP: begin if (majority_vote(samples) 1b1) begin rx_data data_reg; rx_valid 1b1; rx_error parity_error; end else begin rx_error 1b1; // 停止位错误 end end endcase end end endmodule5. 验证与调试技巧完成RTL设计后我们需要进行充分的验证。以下是一些实用的验证方法自检测试设计一个回环测试将发送端直接连接到接收端module uart_loopback_test; reg clk 0; reg rst_n 0; reg tx_start 0; reg [7:0] tx_data 0; wire tx_done; // 实例化UART uart_top u_uart ( .clk(clk), .rst_n(rst_n), .tx_start(tx_start), .tx_data(tx_data), .tx_done(tx_done), .rx_in(u_uart.tx_out), // 回环连接 .rx_data(), .rx_valid(), .rx_error() ); // 时钟生成 always #5 clk ~clk; initial begin #100 rst_n 1; #200 tx_data 8hA5; tx_start 1; #10 tx_start 0; (posedge tx_done); #1000 $finish; end endmodule波形分析要点检查波特率时钟是否准确验证起始位、数据位、停止位的时序检查奇偶校验是否正确生成和检测常见问题排查表问题现象可能原因解决方案接收不到数据波特率不匹配检查波特率分频系数计算数据错位起始位检测不准确增加过采样倍数优化多数表决逻辑偶发错误抗干扰不足增加采样点数量优化滤波算法奇偶校验错误校验位计算错误检查发送端和接收端的校验算法是否一致6. 性能优化与扩展思路基础功能实现后可以考虑以下优化和扩展方向FIFO缓冲添加FIFO可以解决数据突发问题典型实现module uart_fifo #( parameter WIDTH 8, parameter DEPTH 16 )( input clk, input rst_n, input wr_en, input [WIDTH-1:0] din, input rd_en, output [WIDTH-1:0] dout, output full, output empty ); reg [WIDTH-1:0] mem [0:DEPTH-1]; reg [4:0] wr_ptr, rd_ptr; reg [4:0] count; assign full (count DEPTH); assign empty (count 0); assign dout mem[rd_ptr[3:0]]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr 0; rd_ptr 0; count 0; end else begin case ({wr_en, rd_en}) 2b01: if (!empty) begin rd_ptr rd_ptr 1; count count - 1; end 2b10: if (!full) begin mem[wr_ptr[3:0]] din; wr_ptr wr_ptr 1; count count 1; end 2b11: begin mem[wr_ptr[3:0]] din; wr_ptr wr_ptr 1; rd_ptr rd_ptr 1; end default: ; endcase end end endmodule自动波特率检测通过测量起始位宽度自动识别波特率多UART通道集成通过时分复用实现多通道支持DMA接口添加DMA支持以减少CPU开销7. 实际应用中的经验分享在真实的项目开发中UART模块往往会遇到各种意料之外的问题。以下是几个实际案例中的经验总结时钟域交叉问题当系统时钟与波特率时钟不同源时需要特别注意跨时钟域同步。一个简单的解决方案是在接收端使用两级触发器同步外部信号reg rx_sync1, rx_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 1b1; rx_sync2 1b1; end else begin rx_sync1 rx_in; rx_sync2 rx_sync1; end end电磁干扰问题在工业环境中长距离UART通信容易受到干扰。除了硬件滤波外可以在软件层面增加以下处理增加起始位验证连续多个采样点都为低才确认起始位实现简单的CRC校验而不仅是奇偶校验添加超时重传机制低功耗优化对于电池供电设备可以在不通信时关闭波特率时钟使用更深的睡眠模式通过起始位唤醒动态调整波特率以降低功耗