FPGA入门实战:用Verilog实现UART串口通信的完整逻辑设计
UART串口是FPGA入门最经典的实战项目之一——它协议简单、调试直观、实用性强。用Verilog实现一个完整的UART收发器,能帮你深刻理解FPGA的时序逻辑、状态机和波特率分频三大核心概念。本文以115200波特率、8N1格式(8数据位、无校验、1停止位)为例,从波特率发生器到收发模块,给出可直接仿真验证的代码。
UART帧格式
UART的一帧数据包含:1位起始位(低电平)、8位数据位(LSB先发)、1位停止位(高电平)。空闲时总线为高电平。发送和接收双方约定相同的波特率,通过定时采样恢复数据。
波特率发生器
FPGA内部时钟通常远高于波特率(如50MHz),需要通过计数器分频产生采样时钟。对于115200波特率,每位宽度约为8.68μs。若系统时钟50MHz(周期20ns),则每位需要的时钟周期数为:
50MHz ÷ 115200 ≈ 434
因此,计数器从0计数到433即完成一位的定时。接收端通常在每位的中点采样(即计数到217时),以获得最稳定的数据。
module baud_gen #(parameter CLK_FREQ = 50_000_000, BAUD = 115200) (
input clk, rst_n,
output reg tick
);
localparam CNT_MAX = CLK_FREQ / BAUD - 1;
localparam CNT_WIDTH = $clog2(CNT_MAX + 1);
reg [CNT_WIDTH-1:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
tick <= 0;
end else if (cnt == CNT_MAX) begin
cnt <= 0;
tick <= 1;
end else begin
cnt <= cnt + 1;
tick <= 0;
end
end
endmodule
UART发送模块
发送模块的核心是一个状态机:空闲态等待发送使能,然后依次发送起始位、8个数据位和停止位。
module uart_tx (
input clk, rst_n,
input [7:0] data_in,
input tx_en,
output reg txd,
output reg tx_busy
);
localparam IDLE = 0, START = 1, DATA = 2, STOP = 3;
reg [1:0] state;
reg [3:0] bit_cnt; // 0-7数据位
wire tick;
baud_gen #(.CLK_FREQ(50_000_000), .BAUD(115200)) u_baud (.clk(clk), .rst_n(rst_n), .tick(tick));
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
txd <= 1;
tx_busy <= 0;
bit_cnt <= 0;
end else if (tick) begin
case (state)
IDLE: begin
txd <= 1;
tx_busy <= 0;
if (tx_en) begin
state <= START;
tx_busy <= 1;
end
end
START: begin
txd <= 0; // 起始位
state <= DATA;
bit_cnt <= 0;
end
DATA: begin
txd <= data_in[bit_cnt]; // LSB first
if (bit_cnt == 7) state <= STOP;
else bit_cnt <= bit_cnt + 1;
end
STOP: begin
txd <= 1; // 停止位
state <= IDLE;
end
endcase
end
end
endmodule
UART接收模块
接收的关键在于在每位的中点采样。用计数器记录当前处于位周期的哪个位置,在中间位置读取rxd引脚的电平。
module uart_rx (
input clk, rst_n,
input rxd,
output reg [7:0] data_out,
output reg rx_done
);
localparam IDLE = 0, START = 1, DATA = 2, STOP = 3;
reg [1:0] state;
reg [3:0] bit_cnt;
reg [8:0] sample_cnt; // 每位内的采样计数器
wire tick;
baud_gen u_baud (.clk(clk), .rst_n(rst_n), .tick(tick));
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
data_out <= 0;
rx_done <= 0;
sample_cnt <= 0;
end else if (tick) begin
case (state)
IDLE: begin
rx_done <= 0;
if (!rxd) begin // 检测到起始位
state <= START;
sample_cnt <= 0;
end
end
START: begin
if (sample_cnt == 7) begin // 中点采样
if (!rxd) begin // 确认起始位有效
state <= DATA;
bit_cnt <= 0;
sample_cnt <= 0;
end else state <= IDLE;
end else sample_cnt <= sample_cnt + 1;
end
DATA: begin
if (sample_cnt == 15) begin // 每位的中点
data_out[bit_cnt] <= rxd;
if (bit_cnt == 7) state <= STOP;
else bit_cnt <= bit_cnt + 1;
sample_cnt <= 0;
end else sample_cnt <= sample_cnt + 1;
end
STOP: begin
if (sample_cnt == 15) begin
rx_done <= 1;
state <= IDLE;
end else sample_cnt <= sample_cnt + 1;
end
endcase
end
end
endmodule
注意:接收模块的采样计数器范围是0-15,即在每位内采样16次,在第8次(中点)读取数据。这是因为波特率发生器产生的tick信号每周期只持续一个时钟周期,接收模块需要在每位内做更细粒度的采样。
顶层模块与测试
将收发模块例化到顶层,形成一个简单的UART环路测试:
module uart_top (
input clk, rst_n,
input rxd,
output txd,
input [7:0] sw_data,
input btn_send
);
wire [7:0] rx_data;
wire rx_done;
uart_rx u_rx (.clk(clk), .rst_n(rst_n), .rxd(rxd),
.data_out(rx_data), .rx_done(rx_done));
uart_tx u_tx (.clk(clk), .rst_n(rst_n), .data_in(rx_data),
.tx_en(rx_done), .txd(txd), .tx_busy());
endmodule
编写testbench,模拟发送0x55(01010101),观察txd波形是否符合UART时序。使用ModelSim或Vivado Simulator运行仿真,检查起始位、8个数据位和停止位的电平变化是否正确。
写在最后
UART收发器是FPGA入门的里程碑项目。通过这个实战,你亲手实现了波特率分频器、状态机、移位寄存器和边沿检测——这些模块是更复杂设计(SPI、I2C、以太网)的基础。当你在示波器上看到自己写的FPGA发出的UART波形与PC串口助手完美通信时,那种成就感是理论学习无法替代的。从UART出发,FPGA的世界才刚刚打开。





