4. UART串口接收模块设计
任务目标
设计一个可综合的UART接收模块,以9600波特率(8-N-1帧格式)接收外部串行数据,并将接收到的字节并行输出。模块需具备接收完成标志,便于上层逻辑读取数据。
实现方案
采用状态机实现UART接收协议:检测起始位下降沿后,以16倍波特率时钟采样数据位中心,避免误采样。设计要点:
- 系统时钟50MHz,波特率9600,需生成波特率分频时钟(或采样使能脉冲)
- 状态机包含:IDLE、START、DATA、STOP四个状态
- 数据位采用中间采样,每位采样3次取多数表决(抗干扰)
- 输出8位并行数据及单周期接收完成脉冲
完整代码(Verilog)
module uart_rx #(
parameter CLK_FREQ = 50_000_000, // 系统时钟50MHz
parameter BAUD_RATE = 9600 // 波特率
)(
input wire clk, // 系统时钟
input wire rst_n, // 异步复位,低有效
input wire rx, // 串行输入
output reg [7:0] rx_data, // 并行数据输出
output reg rx_done // 接收完成脉冲(单周期高)
);
// 分频参数:每比特16个采样点
localparam DIV_CNT = CLK_FREQ / (BAUD_RATE * 16) - 1;
localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;
reg [15:0] clk_cnt; // 采样时钟计数器
reg sample_en; // 采样使能(16倍波特率脉冲)
reg [3:0] bit_cnt; // 数据位计数(0~7)
reg [2:0] sample_cnt; // 每位采样计数(0~7,取中间3次)
reg [1:0] state, next_state;
reg [7:0] shift_reg; // 移位寄存器
reg [1:0] rx_sync; // 输入同步器
// 异步复位与时钟同步
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_sync <= 2'b11;
end else begin
rx_sync <= {rx_sync[0], rx};
end
end
wire rx_clean = rx_sync[1];
// 采样时钟生成
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 16'd0;
sample_en <= 1'b0;
end else begin
if (clk_cnt == DIV_CNT) begin
clk_cnt <= 16'd0;
sample_en <= 1'b1;
end else begin
clk_cnt <= clk_cnt + 1'b1;
sample_en <= 1'b0;
end
end
end
// 状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
end else if (sample_en) begin
state <= next_state;
end
end
// 状态转移与数据处理
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
next_state <= IDLE;
bit_cnt <= 4'd0;
sample_cnt <= 3'd0;
shift_reg <= 8'd0;
rx_data <= 8'd0;
rx_done <= 1'b0;
end else if (sample_en) begin
rx_done <= 1'b0; // 默认拉低
case (state)
IDLE: begin
bit_cnt <= 4'd0;
sample_cnt <= 3'd0;
if (rx_clean == 1'b0) // 检测到起始位
next_state <= START;
else
next_state <= IDLE;
end
START: begin
// 起始位中点确认
if (sample_cnt == 3'd7) begin
if (rx_clean == 1'b0) begin
next_state <= DATA;
sample_cnt <= 3'd0;
end else begin
next_state <= IDLE; // 毛刺,重新等待
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
DATA: begin
// 每位采样16个点,取第7、8、9点多数表决
if (sample_cnt >= 3'd6 && sample_cnt <= 3'd8) begin
// 多数表决逻辑(简化:直接取中间点)
if (sample_cnt == 3'd7)
shift_reg <= {rx_clean, shift_reg[7:1]};
end
if (sample_cnt == 3'd15) begin
sample_cnt <= 3'd0;
if (bit_cnt == 4'd7) begin
next_state <= STOP;
bit_cnt <= 4'd0;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
STOP: begin
if (sample_cnt == 3'd15) begin
next_state <= IDLE;
rx_data <= shift_reg;
rx_done <= 1'b1; // 产生完成脉冲
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
default: next_state <= IDLE;
endcase
end
end
endmodule
仿真测试与验证
编写testbench验证接收功能:模拟发送字节0xA5(二进制10100101,LSB先发),检查rx_data与rx_done时序。
module tb_uart_rx;
reg clk = 0;
reg rst_n = 0;
reg rx = 1;
wire [7:0] rx_data;
wire rx_done;
// 生成50MHz时钟
always #10 clk = ~clk;
// 待测模块实例化
uart_rx #(.CLK_FREQ(50_000_000), .BAUD_RATE(9600)) uut (
.clk(clk), .rst_n(rst_n), .rx(rx),
.rx_data(rx_data), .rx_done(rx_done)
);
// 发送任务:模拟9600波特率发送1字节
task send_byte(input [7:0] data);
integer i;
begin
// 起始位(低电平)
rx = 0;
#104167; // 1/9600 ≈ 104.167us
// 8个数据位,LSB first
for (i = 0; i < 8; i = i + 1) begin
rx = data[i];
#104167;
end
// 停止位(高电平)
rx = 1;
#104167;
end
endtask
initial begin
// 复位
#100 rst_n = 1;
#200;
// 发送0xA5
send_byte(8'hA5);
#200;
// 检查结果
if (rx_data == 8'hA5 && rx_done == 1)
$display("TEST PASSED: rx_data = 0x%02X", rx_data);
else
$display("TEST FAILED: rx_data = 0x%02X", rx_data);
#500;
$finish;
end
endmodule
运行与判定标准
- 仿真运行:在Vivado/ModelSim中编译两个文件,运行仿真。观察波形中rx_done在停止位结束后产生单周期高脉冲。
- 通过条件:仿真控制台打印
TEST PASSED,且rx_data输出为0xA5。 - 上板验证:将rx连接至USB-UART的TX引脚,使用串口助手发送0xA5,观察FPGA接收数据与发送一致。
常见问题
- 采样点偏移:若接收数据错误,检查DIV_CNT计算是否正确(50MHz/9600/16-1=324)。
- 起始位误触发:输入同步器rx_sync可消除亚稳态,但若噪声过大需增加滤波逻辑。
- 连续接收:本模块在STOP结束后立即回到IDLE,可连续接收下一字节,无需额外处理。