1. UART通信基础与Verilog实现价值
搞过嵌入式开发的朋友对UART绝对不陌生,作为最古老的串行通信协议之一,从80年代沿用至今仍然活跃在各种设备中。最近在做一个FPGA与STM32的通信项目时,我放弃了现成的IP核,选择用Verilog从零实现UART协议栈。这个决定让我踩了无数坑,也收获了底层时序控制的宝贵经验。
UART的核心优势在于其简洁性——不需要时钟线,仅用TX/RX两根信号线就能实现全双工通信。但正是这种"简单"背后藏着魔鬼细节:起始位检测的亚稳态问题、波特率时钟的精确分频、停止位的容错处理...用Verilog实现时,每个环节都需要精心设计状态机。本文将分享我实现115200波特率UART通信的完整方案,包含可直接移植的代码片段和示波器实测波形分析。
2. 协议层设计与关键参数计算
2.1 UART帧结构解析
标准UART帧包含:
- 1位起始位(低电平)
- 5-9位数据位(通常8位)
- 可选校验位(奇/偶/无)
- 1-2位停止位(高电平)
以最常见的8N1配置(8数据位、无校验、1停止位)为例,每个字节实际传输10位。115200波特率下,每位持续时间约8.68μs(1/115200)。Verilog实现时需要精确生成这个时序。
2.2 时钟分频计算
假设FPGA主时钟为50MHz,分频系数计算:
code复制分频系数 = 主时钟频率 / (16 × 波特率)
= 50,000,000 / (16 × 115200)
≈ 27.13
取整后分频系数为27,实际波特率:
code复制实际波特率 = 50,000,000 / (16 × 27)
≈ 115740.7 bps
误差率0.47%,在RS-232标准允许的±3%范围内。代码中需要定义:
verilog复制parameter CLK_DIVISOR = 27;
3. Verilog发送模块实现
3.1 发送状态机设计
发送模块采用三段式状态机:
- IDLE:等待发送使能信号
- START:拉低起始位
- DATA:移位发送数据位
- STOP:拉高停止位
关键代码片段:
verilog复制always @(posedge clk) begin
case(state)
IDLE: if(tx_en) begin
tx_reg <= {1'b1, data_in, 1'b0}; // 停止位+数据+起始位
bit_cnt <= 0;
clk_cnt <= 0;
state <= START;
end
START: begin
tx <= 1'b0; // 起始位
if(clk_cnt == CLK_DIVISOR-1) begin
clk_cnt <= 0;
state <= DATA;
end else begin
clk_cnt <= clk_cnt + 1;
end
end
// 其他状态省略...
endcase
end
3.2 波特率时钟生成
采用16倍过采样时钟提高稳定性:
verilog复制always @(posedge clk) begin
if(clk_div_cnt == CLK_DIVISOR-1) begin
clk_div_cnt <= 0;
baud_clk <= ~baud_clk;
end else begin
clk_div_cnt <= clk_div_cnt + 1;
end
end
4. Verilog接收模块实现
4.1 亚稳态处理技巧
接收端起始位检测需要特别处理亚稳态:
verilog复制// 双寄存器同步
always @(posedge clk) begin
rx_sync <= {rx_sync[0], rx};
rx_clean <= rx_sync[1];
end
// 起始位检测
assign start_detect = (state == IDLE) && (rx_clean == 1'b0);
4.2 中点采样策略
在每位持续时间的中点采样数据:
verilog复制always @(posedge clk) begin
if(state == DATA && clk_cnt == CLK_DIVISOR/2) begin
rx_shift <= {rx_clean, rx_shift[7:1]};
end
end
5. 实测问题与解决方案
5.1 常见故障现象
- 数据错位:示波器显示起始位过短
- 误码率高:采样点偏移导致
- 死锁:状态机未正确复位
5.2 调试技巧
- 用SignalTap II捕获TX/RX信号
- 测量实际波特率(示波器测10位周期应为86.8μs)
- 添加超时复位逻辑:
verilog复制always @(posedge clk) begin
if(timeout_cnt == 32'd50000000) begin // 1秒超时
state <= IDLE;
end else begin
timeout_cnt <= timeout_cnt + 1;
end
end
6. 完整工程优化建议
6.1 FIFO缓冲区实现
添加16字节深度的FIFO提升吞吐量:
verilog复制module uart_fifo (
input wire clk,
input wire [7:0] data_in,
output wire [7:0] data_out,
// 其他控制信号...
);
reg [7:0] mem [0:15];
reg [3:0] w_ptr, r_ptr;
// 读写指针逻辑...
endmodule
6.2 自动波特率检测
通过测量起始位宽度动态调整分频系数:
verilog复制always @(negedge rx) begin // 检测起始沿
start_time <= $time;
end
always @(posedge rx) begin // 检测结束沿
bit_time <= ($time - start_time) / 16;
clk_divisor <= bit_time * clk_freq;
end
经过两周的调试优化,最终实现的UART模块在115200波特率下连续传输1MB数据零误码。关键收获是:时序逻辑必须严格同步,任何异步信号都需要双重缓冲处理。建议在Xilinx Artix-7开发板上实测时,将约束文件中的时钟不确定性(set_clock_uncertainty)设置为至少1ns以提高时序裕量。