3. UART串口发送模块设计
任务目标
设计一个可综合的UART发送模块,支持1个起始位、8个数据位、1个停止位(8N1格式),通过FPGA引脚实现数据的串行输出。模块接收并行字节数据后,按设定的波特率逐位发送,并输出发送完成标志。
实现方案
采用状态机方式实现UART发送协议。系统时钟50MHz,目标波特率9600bps,因此需要分频计数器产生位周期。模块接口包含时钟、复位、发送使能、并行数据输入、串行输出及忙状态指示。核心逻辑分为波特率生成、状态转移和移位输出三部分。
完整代码
module uart_tx (
input clk, // 50MHz系统时钟
input rst_n, // 异步复位,低有效
input tx_start, // 发送启动脉冲(高有效一个时钟周期)
input [7:0] tx_data, // 待发送的并行数据
output reg txd, // 串行数据输出
output reg tx_busy // 忙标志:1=正在发送,0=空闲
);
// 波特率参数:50MHz / 9600 = 5208
localparam BAUD_CNT_MAX = 5208 - 1;
localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;
reg [1:0] state;
reg [12:0] baud_cnt; // 波特率分频计数器
reg [2:0] bit_cnt; // 数据位计数(0~7)
reg [7:0] shift_reg; // 发送数据移位寄存器
// 波特率时钟生成
wire baud_tick = (baud_cnt == BAUD_CNT_MAX);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
baud_cnt <= 13'd0;
end else if (state == IDLE) begin
baud_cnt <= 13'd0;
end else if (baud_tick) begin
baud_cnt <= 13'd0;
end else begin
baud_cnt <= baud_cnt + 1'b1;
end
end
// 主状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
txd <= 1'b1; // 空闲时输出高电平
tx_busy <= 1'b0;
shift_reg <= 8'd0;
bit_cnt <= 3'd0;
end else begin
case (state)
IDLE: begin
txd <= 1'b1;
tx_busy <= 1'b0;
if (tx_start) begin
state <= START;
shift_reg <= tx_data;
tx_busy <= 1'b1;
end
end
START: begin
txd <= 1'b0; // 起始位
if (baud_tick) begin
state <= DATA;
bit_cnt <= 3'd0;
end
end
DATA: begin
txd <= shift_reg[0]; // LSB先发送
if (baud_tick) begin
if (bit_cnt == 3'd7) begin
state <= STOP;
end else begin
shift_reg <= shift_reg >> 1;
bit_cnt <= bit_cnt + 1'b1;
end
end
end
STOP: begin
txd <= 1'b1; // 停止位
if (baud_tick) begin
state <= IDLE;
tx_busy <= 1'b0;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
仿真测试代码
module uart_tx_tb;
reg clk = 0, rst_n = 0, tx_start = 0;
reg [7:0] tx_data = 8'h00;
wire txd, tx_busy;
uart_tx dut (
.clk(clk), .rst_n(rst_n), .tx_start(tx_start),
.tx_data(tx_data), .txd(txd), .tx_busy(tx_busy)
);
always #10 clk = ~clk; // 50MHz
initial begin
#20 rst_n = 1;
#20 tx_data = 8'hA5; // 1010_0101
@(posedge clk) tx_start = 1;
@(posedge clk) tx_start = 0;
// 等待发送完成
wait (!tx_busy);
#100 $finish;
end
// 监控串行输出
initial begin
$monitor("time=%0t txd=%b tx_busy=%b", $time, txd, tx_busy);
end
endmodule
运行与验证
仿真验证:在Vivado或ModelSim中运行上述测试平台,观察波形。发送数据0xA5时,预期输出序列为:空闲高电平→起始位0→数据位LSB优先(1,0,1,0,0,1,0,1)→停止位1。每个位持续5208个时钟周期(约104.16μs @50MHz)。
通过条件:
- tx_busy在tx_start有效后拉高,发送完成后自动拉低;
- txd在起始位输出0,停止位输出1,空闲保持高电平;
- 数据位顺序与tx_data的LSB-first一致,且每位宽度正确。
板级验证:将txd连接至FPGA的UART_TX引脚,使用USB转串口工具连接PC,发送单字节数据(如0xA5),PC端串口助手应收到对应十六进制数据。注意PC端需配置为9600-8-N-1。
常见问题
- 接收数据错误:检查波特率分频值是否精确(50MHz/9600=5208.33,取5208引入约0.006%误差,可接受);确认PC端波特率设置一致。
- tx_start毛刺:发送启动信号需为单时钟周期脉冲,若外部信号持续拉高,模块会在发送完成后立即重新发送,需在外部逻辑中处理。
- 时序约束:若系统时钟非50MHz,需重新计算BAUD_CNT_MAX值。