UART串口通信是FPGA开发者必须掌握的基础技能。作为一名在工业自动化领域使用FPGA多年的工程师,我可以肯定地说,90%的FPGA项目都需要通过UART与其他设备通信。无论是与上位机交换数据,还是与单片机协同工作,UART都是最经济实用的选择。
记得我第一次在FPGA上实现UART时,花了整整一周时间调试波特率同步问题。现在回头看,其实只要掌握了几个关键点,任何开发者都能在半天内实现稳定的UART通信。本文将分享我多年来总结的UART实现经验,包含可直接用于生产的Verilog代码,以及那些教科书上不会告诉你的实战技巧。
UART采用异步通信机制,这意味着收发双方没有共享时钟信号。这种设计带来了接线简单的优势(只需TX、RX和GND三根线),但也引入了同步挑战。在实际项目中,我见过太多因为理解不透彻而导致的通信故障。
通信帧结构是UART工作的基础。一个完整的UART帧包含:
关键经验:工业环境中,我强烈建议使用1位停止位而非2位。虽然理论上2位停止位能提高可靠性,但现代串口芯片对1位停止位的支持更好,实际使用中几乎观察不到差异。
波特率是UART通信中最容易出问题的参数。很多新手认为只要两端设置相同波特率就能通信,实际上还需要考虑以下因素:
时钟精度:FPGA的50MHz晶振通常有±50ppm的误差,意味着实际频率可能在49.9975MHz到50.0025MHz之间。对于115200波特率,这会导致约±5.8bps的偏差,仍在可接受范围内。
采样点优化:我的经验是在每个bit周期的中间位置采样。对于50MHz时钟和9600波特率:
抗干扰设计:在工业现场,我通常会:
经过多个项目的迭代,我总结出一个稳健的UART实现架构:
code复制 +---------------+
| |
| UART顶层模块 |
| |
+-------┬-------+
|
+---------------+---------------+
| |
+-------v-------+ +-------v-------+
| | | |
| UART发送模块 | | UART接收模块 |
| | | |
+-------+-------+ +-------+-------+
| |
v v
TX引脚 RX引脚
这种架构的优势在于:
发送模块的核心是一个状态机,这是我优化过的版本:
verilog复制// 状态定义
localparam IDLE = 1'b0;
localparam SENDING = 1'b1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_state <= IDLE;
tx_pin <= 1'b1; // 空闲时保持高电平
// 其他寄存器复位...
end else begin
case(tx_state)
IDLE: begin
if(tx_en) begin
tx_state <= SENDING;
tx_data_reg <= tx_data; // 数据锁存
bit_cnt <= 0;
baud_cnt <= 0;
end
end
SENDING: begin
baud_cnt <= baud_cnt + 1;
if(baud_cnt == BAUD_DIV - 1) begin
baud_cnt <= 0;
bit_cnt <= bit_cnt + 1;
case(bit_cnt)
0: tx_pin <= 1'b0; // 起始位
1: tx_pin <= tx_data_reg[0];
// ...其他数据位
9: begin
tx_pin <= 1'b1; // 停止位
tx_state <= IDLE;
tx_done <= 1'b1;
end
endcase
end
end
endcase
end
end
避坑指南:一定要在状态切换时锁存待发送数据。我曾在项目中遇到因数据变化导致的通信错误,后来发现是发送过程中源数据被修改导致的。
接收模块有几个关键优化点:
verilog复制always @(posedge clk) begin
rx_pin_reg1 <= rx_pin; // 第一级同步
rx_pin_reg2 <= rx_pin_reg1; // 第二级同步
end
verilog复制// 检测起始位的下降沿
wire start_detected = (rx_pin_sync==0) && (rx_pin_prev==1);
verilog复制localparam SAMPLE_WINDOW_START = BAUD_MID - BAUD_DIV/20;
localparam SAMPLE_WINDOW_END = BAUD_MID + BAUD_DIV/20;
if(baud_cnt >= SAMPLE_WINDOW_START &&
baud_cnt <= SAMPLE_WINDOW_END) begin
sample_now = 1'b1;
end
顶层模块需要处理好以下接口:
verilog复制module uart_top #(
parameter CLK_FREQ = 50_000_000,
parameter BAUD_RATE = 115200 // 推荐使用115200
)(
input clk,
input rst_n,
// UART接口
input rx_pin,
output tx_pin,
// 用户接口
input [7:0] tx_data,
input tx_en,
output tx_done,
output [7:0] rx_data,
output rx_done
);
// 模块例化...
endmodule
建立完善的测试平台非常重要,我通常包含以下测试用例:
verilog复制// 发送测试
tx_data_in = 8'h55;
tx_en_in = 1'b1;
#10 tx_en_in = 1'b0;
// 检查tx_pin输出波形
verilog复制// 模拟线路干扰
force rx_pin = 1'bx;
#100 release rx_pin;
硬件调试时,这些工具能极大提高效率:
逻辑分析仪:配置触发条件为UART起始位,可以捕获实际通信波形
串口调试助手:推荐使用支持二进制显示的版本,如Tera Term
LED指示灯:简单但有效
verilog复制assign led = rx_done ? rx_data[7:0] : 8'hFF;
常见硬件问题排查:
无通信:
数据错误:
在高速通信或大数据量传输时,建议添加硬件流控(RTS/CTS):
verilog复制module uart_with_flowcontrol(
input rts_n, // 输入流控
output cts_n, // 输出流控
// ...其他接口
);
assign cts_n = (rx_fifo_count > RX_FIFO_HIGH_WATER) ? 1'b0 : 1'b1;
always @(posedge clk) begin
if(tx_en && rts_n) begin
// 对方准备好才发送
end
end
添加FIFO可以显著提高系统性能:
verilog复制// 接收FIFO例化
fifo #(
.DATA_WIDTH(8),
.DEPTH(64)
) rx_fifo (
.clk(clk),
.rst(rst),
.wr_en(rx_done),
.wr_data(rx_data),
.rd_en(rx_read),
.rd_data(rx_fifo_out),
.full(rx_full),
.empty(rx_empty)
);
通过自动检测起始位宽度实现波特率检测:
verilog复制// 测量起始位持续时间
always @(negedge rx_pin_sync) begin
start_time <= $time;
end
always @(posedge rx_pin_sync) begin
if(bit_cnt == 0) begin
measured_baud <= 1/($time - start_time);
end
end
经过多个项目的验证,我总结出以下最佳实践:
verilog复制// 对异步信号使用双寄存器同步
always @(posedge clk) begin
ext_signal_sync1 <= ext_signal;
ext_signal_sync2 <= ext_signal_sync1;
end
makefile复制uart_%.v: uart_template.v
sed "s/BAUD_RATE/$*/g" $< > $@
这个Makefile规则可以自动生成不同波特率的版本,方便项目复用。
对于需要更高可靠性的应用,可以考虑以下增强措施:
在最近的一个工业控制器项目中,我们使用这套UART实现方案实现了超过1,000小时的稳定运行,误码率低于10^-9,充分验证了其可靠性。