1. FPGA串口通信概述
串口通信作为嵌入式系统中最基础的外设接口之一,在FPGA开发中有着广泛的应用场景。与MCU不同,FPGA实现串口通信需要从底层开始设计时序逻辑和状态机,这既是对数字电路设计能力的考验,也是理解通信协议的绝佳实践。
我在工业控制项目中多次使用Xilinx和Altera(现Intel)FPGA实现自定义串口协议,发现几个关键点:首先,波特率误差必须控制在2%以内(工业标准要求);其次,收发状态机的设计直接影响抗干扰能力;最后,FIFO缓冲深度需要根据具体应用场景计算。下面分享一套经过量产验证的Verilog实现方案。
2. 硬件架构设计
2.1 接口信号定义
典型FPGA串口模块包含以下信号:
verilog复制module uart_top(
input clk, // 系统时钟(如50MHz)
input rst_n, // 低电平复位
input uart_rxd, // 串口接收线
output uart_txd, // 串口发送线
output [7:0] rx_data, // 接收数据输出
output rx_valid, // 接收数据有效标志
input [7:0] tx_data, // 发送数据输入
input tx_start, // 发送启动信号
output tx_busy // 发送忙标志
);
注意:实际项目中建议添加奇偶校验位和硬件流控信号(CTS/RTS),特别是传输距离超过1米时。
2.2 时钟分频计算
以常见的115200波特率为例,假设系统时钟为50MHz:
code复制分频系数 = 系统时钟频率 / (波特率 × 过采样率)
= 50,000,000 / (115200 × 16)
≈ 27.13 → 取整27
实际波特率 = 50,000,000 / (27 × 16) ≈ 115740.7
误差率 = (115740.7 - 115200)/115200 × 100% ≈ 0.47% (符合要求)
Verilog实现:
verilog复制reg [15:0] baud_counter;
always @(posedge clk) begin
if(baud_counter == 16'd26) begin
baud_tick <= 1'b1;
baud_counter <= 16'd0;
end else begin
baud_tick <= 1'b0;
baud_counter <= baud_counter + 1'b1;
end
end
3. 接收模块实现
3.1 状态机设计
接收状态机包含四个状态:
- IDLE:检测起始位下降沿
- START:确认起始位有效性
- DATA:采样8位数据
- STOP:检测停止位
verilog复制localparam
RX_IDLE = 2'b00,
RX_START = 2'b01,
RX_DATA = 2'b10,
RX_STOP = 2'b11;
reg [1:0] rx_state;
reg [3:0] sample_count;
reg [2:0] bit_count;
reg [7:0] rx_shift_reg;
3.2 中点采样策略
为提高抗干扰能力,采用16倍过采样,取中间3个样本投票决定电平值:
verilog复制always @(posedge clk) begin
case(sample_count)
4'd7: sample[0] <= uart_rxd;
4'd8: sample[1] <= uart_rxd;
4'd9: sample[2] <= uart_rxd;
endcase
if(sample_count == 4'd15) begin
rx_bit <= (sample[0]+sample[1]+sample[2] >= 2) ? 1'b1 : 1'b0;
end
end
经验:工业环境建议增加起始位有效性检查,连续3个周期低电平才确认起始位,可有效避免毛刺误触发。
4. 发送模块实现
4.1 并串转换时序
发送时序需严格满足协议要求:
code复制起始位(0) → 数据位(LSB first) → 停止位(1)
典型实现:
verilog复制reg [3:0] tx_state;
reg [7:0] tx_shift_reg;
always @(posedge clk) begin
case(tx_state)
4'd0: if(tx_start) begin // 等待发送请求
tx_shift_reg <= tx_data;
tx_state <= 4'd1;
tx_busy <= 1'b1;
end
4'd1: begin // 发送起始位
uart_txd <= 1'b0;
if(baud_tick) tx_state <= tx_state + 1;
end
4'd2,4'd3,4'd4,4'd5,4'd6,4'd7,4'd8,4'd9: begin // 发送数据位
uart_txd <= tx_shift_reg[tx_state-2];
if(baud_tick) tx_state <= tx_state + 1;
end
4'd10: begin // 发送停止位
uart_txd <= 1'b1;
if(baud_tick) begin
tx_state <= 4'd0;
tx_busy <= 1'b0;
end
end
endcase
end
4.2 预加重技术
长距离传输时,可在发送端第一个比特周期提高驱动电流:
verilog复制// 在Xilinx FPGA中的实现示例
OBUFDS #(
.DRIVE(12), // 默认驱动强度
.IOSTANDARD("LVCMOS33")
) OBUF_start (
.I(1'b0),
.O(uart_txd),
.OB()
);
OBUFDS #(
.DRIVE(16), // 增强驱动
.IOSTANDARD("LVCMOS33")
) OBUF_first_bit (
.I(tx_shift_reg[0]),
.O(uart_txd),
.OB()
);
5. 调试与优化技巧
5.1 眼图测试方法
使用SignalTap或ChipScope捕获波形时:
- 设置触发条件为起始位下降沿
- 时间窗口覆盖2-3个字节周期
- 检查:
- 起始位宽度是否准确
- 数据位跳变沿是否对齐采样点
- 停止位电平是否稳定
5.2 常见故障排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 接收数据错位 | 波特率误差过大 | 重新计算分频系数 |
| 偶发丢帧 | 未做时钟域同步 | 添加双级触发器同步跨时钟域信号 |
| 长距离通信失败 | 未终端匹配 | 在接收端添加100Ω终端电阻 |
| 电磁干扰严重 | 未差分传输 | 改用RS485电平标准 |
5.3 性能优化建议
-
FIFO深度计算:
- 接收FIFO深度 ≥ (最大中断延迟 × 波特率) / (10 × 8)
- 例如:Linux系统典型中断延迟1ms,115200波特率下:
(0.001 × 115200) / 10 ≈ 12字节 → 建议16字节FIFO
-
自适应波特率:
verilog复制// 通过测量起始位宽度自动校准 always @(negedge uart_rxd) begin baud_counter <= 0; end always @(posedge clk) begin if(!uart_rxd) baud_counter <= baud_counter + 1; end assign measured_baud = system_clk / (baud_counter × 16); -
RTL仿真技巧:
在Testbench中注入以下异常场景:- 随机插入1-2个周期的毛刺
- 动态改变波特率(±5%)
- 模拟电缆反射(添加随机抖动)
这套实现方案在多个工业现场稳定运行超过3年,最远实现过300米RS485传输。关键点在于严格把控时序余量——所有关键路径必须满足建立/保持时间要求,特别是跨时钟域信号必须经过同步处理。实际部署时建议预留20%以上的时序裕量,以应对环境温度变化带来的时序漂移。
