1. UART串口通信基础与FPGA实现价值
UART(Universal Asynchronous Receiver/Transmitter)作为最古老的串行通信协议之一,至今仍在嵌入式系统和FPGA开发中占据重要地位。在FPGA上实现UART核心模块,本质上是通过硬件描述语言精确模拟串口通信的时序逻辑。与单片机内置的硬件UART不同,FPGA方案需要从底层构建起始位、数据位、校验位和停止位的完整时序控制链。
我曾在多个工业控制项目中采用FPGA实现自定义波特率的UART通信,其核心优势在于:
- 时钟精度可达0.1%以内(基于FPGA的PLL调节)
- 可灵活支持非标准波特率(如187500bps)
- 能实现多通道并行收发(资源允许情况下)
- 便于集成CRC校验等扩展功能
典型的UART发送模块包含三大状态:
- 空闲状态(持续输出高电平)
- 起始位(拉低1个时钟周期)
- 数据位传输(LSB或MSB优先)
以常见的115200bps为例,每个bit周期为1/115200≈8.68μs。在100MHz系统时钟下,需要精确计数868个时钟周期完成单个bit的持续时间。
2. 硬件架构设计与关键参数计算
2.1 系统时钟与波特率生成
FPGA实现UART的核心挑战是时钟同步。假设:
- 系统时钟:100MHz(周期10ns)
- 目标波特率:115200bps
- 过采样率:16x(行业通用标准)
则每个UART时钟周期需要的计数器值为:
code复制(100,000,000 Hz) / (115200 Hz * 16) ≈ 54.25
取整后使用54作为分频系数,实际波特率误差:
code复制理论周期:1/(115200*16) ≈ 542.53ns
实际周期:54*10ns = 540ns
误差率:(542.53-540)/542.53 ≈ 0.47%
这个误差率完全满足RS-232标准要求的<3%容限。
Verilog实现示例:
verilog复制reg [15:0] baud_counter;
always @(posedge clk) begin
if (baud_counter >= 16'd53) begin
baud_counter <= 0;
baud_tick <= 1;
end else begin
baud_counter <= baud_counter + 1;
baud_tick <= 0;
end
end
2.2 发送状态机设计
标准UART发送状态机应包含以下状态(以Verilog枚举表示):
verilog复制typedef enum {
IDLE,
START_BIT,
DATA_BITS,
PARITY_BIT,
STOP_BIT
} uart_state_t;
关键时序约束:
- 起始位:强制拉低1个完整波特率周期
- 数据位:按LSB-first顺序依次输出
- 停止位:至少保持1个周期高电平
建议添加2个时钟周期的"保护间隔"在状态切换时,避免亚稳态问题。
3. Verilog实现细节与优化技巧
3.1 核心发送模块代码解析
verilog复制module uart_tx (
input wire clk,
input wire rst,
input wire [7:0] data_in,
input wire tx_start,
output reg tx_out,
output wire tx_busy
);
reg [2:0] bit_index;
reg [3:0] state;
reg [7:0] tx_data;
always @(posedge clk or posedge rst) begin
if (rst) begin
state <= IDLE;
tx_out <= 1'b1;
end else begin
case (state)
IDLE: begin
tx_out <= 1'b1;
if (tx_start) begin
tx_data <= data_in;
state <= START_BIT;
end
end
START_BIT: begin
tx_out <= 1'b0;
if (baud_tick) state <= DATA_BITS;
end
DATA_BITS: begin
tx_out <= tx_data[bit_index];
if (baud_tick) begin
if (bit_index == 3'd7) state <= STOP_BIT;
else bit_index <= bit_index + 1;
end
end
STOP_BIT: begin
tx_out <= 1'b1;
if (baud_tick) state <= IDLE;
end
endcase
end
end
assign tx_busy = (state != IDLE);
endmodule
3.2 关键优化技术
- 双缓冲设计:添加发送缓冲区寄存器,允许在发送过程中准备下一帧数据
verilog复制reg [7:0] tx_buffer;
always @(posedge clk) begin
if (!tx_busy && new_data_ready)
tx_buffer <= next_data;
end
- 动态波特率调整:通过参数化设计支持运行时波特率切换
verilog复制parameter CLK_DIVIDER = 54;
reg [15:0] baud_divider = CLK_DIVIDER;
- 错误检测增强:添加奇偶校验位生成逻辑
verilog复制wire parity_bit = ^data_in; // 奇校验
4. 实测问题排查与解决方案
4.1 典型问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 接收端数据错位 | 波特率误差超标 | 检查时钟分频计算,建议使用PLL生成精确时钟 |
| 仅能接收部分字节 | 停止位持续时间不足 | 确保停止位保持至少1.5个波特率周期 |
| 数据位顺序错误 | LSB/MSB配置错误 | 检查bit_index的遍历方向 |
| 随机数据错误 | 未处理亚稳态 | 在状态机切换时添加2周期延迟 |
4.2 示波器调试技巧
- 触发设置:使用下降沿触发捕捉起始位
- 时间测量:验证各bit周期是否为8.68μs(115200bps时)
- 电平检查:确认空闲时为高电平,起始位为持续低电平
- 数据对齐:在bit周期中点采样(过采样率的第8个周期)
重要提示:实际PCB布局时,UART线路建议串联33Ω电阻并预留TVS二极管位置,可有效抑制ESD干扰。
5. 进阶应用与扩展方向
5.1 多通道UART控制器
利用FPGA的并行特性,可实现8通道独立UART发送:
verilog复制genvar i;
generate
for (i=0; i<8; i=i+1) begin
uart_tx utx (
.clk(clk),
.rst(rst),
.data_in(ch_data[i]),
.tx_start(ch_start[i]),
.tx_out(tx_pin[i]),
.tx_busy(ch_busy[i])
);
end
endgenerate
5.2 自定义协议封装
在UART基础上增加协议层:
text复制[HEADER][LENGTH][DATA][CRC]
其中:
- HEADER:0xAA(帧起始标志)
- LENGTH:数据字节数(1字节)
- DATA:有效载荷(N字节)
- CRC:CRC8校验值
实现示例:
verilog复制// CRC8计算模块
function [7:0] crc8;
input [7:0] data;
input [7:0] crc;
begin
crc8 = crc;
for (int i=0; i<8; i++) begin
if ((data[i] ^ crc8[7]) == 1'b1)
crc8 = {crc8[6:0],1'b0} ^ 8'h07;
else
crc8 = {crc8[6:0],1'b0};
end
end
endfunction
5.3 流量控制实现
添加RTS/CTS硬件流控信号:
verilog复制input wire cts_n; // 低电平允许发送
output reg rts_n; // 低电平表示准备好接收
always @(posedge clk) begin
if (fifo_almost_full) rts_n <= 1'b0;
else if (fifo_almost_empty) rts_n <= 1'b1;
end
// 在发送状态机中增加CTS检查
always @(*) begin
if (state == IDLE && !cts_n && tx_start)
next_state = START_BIT;
else
next_state = state;
end
6. 工程实践建议
- 时序约束:必须添加适当的时序约束
tcl复制create_clock -period 10 [get_ports clk]
set_output_delay -clock [get_clocks clk] -max 2 [get_ports tx_out]
- 资源优化:当需要实现多个UART通道时,可考虑:
- 共享波特率生成模块
- 使用时分割复用技术
- 采用LUT移位寄存器实现
- 测试方案:
verilog复制// 自测试模块示例
initial begin
#100;
data_in = 8'h55; // 发送01010101便于观察波形
tx_start = 1;
#10;
tx_start = 0;
wait(!tx_busy);
#1000;
$finish;
end
- 跨时钟域处理:当应用时钟与UART时钟不同源时:
verilog复制// 双级同步器处理启动信号
reg [1:0] tx_start_sync;
always @(posedge clk_uart) begin
tx_start_sync <= {tx_start_sync[0], tx_start};
end
wire tx_start_uart = tx_start_sync[1];