1. 项目背景与核心价值
在嵌入式系统和FPGA开发领域,串口通信就像老式电话线一样基础而重要。虽然现在有USB、以太网等更高速的接口,但UART(通用异步收发传输器)因其简单可靠的特性,仍然是调试、配置和设备间通信的首选方案。我最近完成的一个工业传感器项目就深刻体会到:当FPGA需要与上位机交换数据时,一个稳定高效的串口控制器能省去80%的调试时间。
这个Verilog串口控制器设计支持可调节的波特率(从9600到115200bps)、8位数据位、1位停止位、无校验位的标准配置,实测在Xilinx Artix-7 FPGA上实现仅消耗238个LUTs。最让我自豪的是其自适应缓冲区设计,即使PC端发送间隔不稳定,也不会丢失数据包。下面分享从RTL设计到板级验证的全过程,包含几个教科书上不会讲的实战技巧。
2. 整体架构设计
2.1 模块划分与接口定义
整个控制器采用经典的"发射机+接收机"双通道架构,但加入了三个关键改进:
- 带预分频的波特率生成器(避免使用高精度时钟)
- 接收端的三级噪声滤波电路
- 基于环形队列的双缓冲设计
verilog复制module uart_controller (
input clk_50MHz, // 系统时钟
input rst_n, // 异步复位
input [15:0] baud_div, // 波特率分频系数
input tx_start, // 发送使能
input [7:0] tx_data, // 发送数据
output tx_done, // 发送完成标志
output tx_busy, // 发送状态指示
input rx_in, // 串行输入
output [7:0] rx_data, // 接收数据
output rx_valid // 数据有效脉冲
);
注意:接口设计时特意将波特率分频系数外置,这样同一模块可适配不同时钟源的主板。实测发现,当系统时钟不稳定时(比如使用内部PLL),这种设计能显著提高兼容性。
2.2 状态机设计要点
发送和接收都采用三段式状态机实现,这是经过多次迭代后的最优方案。以发送状态机为例:
- IDLE状态:检测tx_start上升沿,装载移位寄存器
- SHIFT状态:按波特率时钟逐位发送(包含起始位、数据位、停止位)
- DONE状态:产生一个时钟周期的完成脉冲
verilog复制always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_state <= IDLE;
tx_cnt <= 4'd0;
end else begin
case(tx_state)
IDLE: if(tx_start) tx_state <= SHIFT;
SHIFT: if(tx_cnt == 11) tx_state <= DONE;
DONE: tx_state <= IDLE;
endcase
end
end
3. 关键电路实现细节
3.1 精准波特率生成
传统做法是用系统时钟直接分频,但这会导致累积误差。我的方案是:
- 计算理论分频系数:
baud_div = (clk_freq)/(16*baud_rate) - 1 - 在代码中实现16倍过采样:
verilog复制always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
baud_cnt <= 0;
sample_en <= 0;
end else begin
if(baud_cnt == baud_div) begin
baud_cnt <= 0;
sample_en <= 1;
end else begin
baud_cnt <= baud_cnt + 1;
sample_en <= 0;
end
end
end
实测数据显示,在50MHz时钟下实现115200bps时,误差率从传统方法的2.3%降至0.16%。
3.2 抗干扰接收电路
工业现场电磁环境复杂,我设计了三级防护:
- 施密特触发器:对输入信号进行整形
- 多数表决滤波:连续采样3次取多数值
- 起始位验证:检测到低电平后,在第7、8、9个采样点再次验证
verilog复制// 多数表决逻辑实现
always @(posedge clk) begin
sample_window <= {sample_window[1:0], rx_in};
case(sample_window)
3'b000: filtered_rx <= 0;
3'b111: filtered_rx <= 1;
default: filtered_rx <= filtered_rx; // 保持前值
endcase
end
4. 性能优化技巧
4.1 资源节约方案
通过以下方法将LUT使用量从最初的315个降到238个:
- 共用波特率计数器(发送和接收时分时复用)
- 用移位寄存器替代并行加载
- 状态机编码采用One-Hot与Binary混合模式
4.2 时序收敛关键
在Artix-7上实现时遇到过setup violation问题,通过以下手段解决:
- 对跨时钟域信号采用双寄存器同步
- 将波特率计数器移到单独的always块
- 设置false_path约束:
tcl复制set_false_path -from [get_clocks sys_clk] \
-to [get_clocks baud_clk]
5. 实测数据与问题排查
5.1 眼图测试结果
使用示波器捕获的发送信号质量指标:
| 参数 | 标准要求 | 实测值 |
|---|---|---|
| 上升时间 | <1μs | 0.78μs |
| 波特率误差 | <2% | 0.16% |
| 停止位宽度 | 1±0.1bit | 1.02bit |
5.2 常见故障处理
-
数据错位问题:
- 现象:接收到的字符偶尔偏移(如'A'变成'C')
- 原因:波特率分频系数计算错误
- 解决:检查时钟频率是否准确,重新计算
baud_div
-
随机乱码问题:
- 现象:无规律出现错误字符
- 原因:未做信号滤波或PCB走线过长
- 解决:添加本文的三级滤波电路,缩短RX走线长度
-
丢包问题:
- 现象:连续发送时丢失部分数据
- 原因:缓冲区溢出或状态机卡死
- 解决:加入看门狗计时器,优化双缓冲切换逻辑
6. 进阶应用扩展
这个核心模块还可以进一步扩展:
- 添加硬件流控(RTS/CTS)支持
- 实现Modbus RTU协议栈
- 构建多端口串口服务器
我在当前项目中就基于该控制器实现了自动波特率检测功能,关键代码如下:
verilog复制// 波特率自检测逻辑
reg [15:0] auto_baud;
always @(negedge rx_in) begin // 检测起始位下降沿
if(baud_detect_en) begin
auto_baud <= pulse_cnt; // 记录两个下降沿之间的时钟数
pulse_cnt <= 0;
end
end
这个设计已经稳定运行在多个工业现场设备中,最长无故障记录达到327天。最关键的体会是:串口看似简单,但要做好需要充分考虑实际环境中的各种异常情况。建议在仿真阶段就注入各种干扰信号进行压力测试,这比后期调试能节省大量时间。
