作为一名FPGA开发者,UART通信模块的设计与验证是每个初学者必须跨越的一道坎。这次我要分享的是一个真实的项目开发案例——多路UART控制器的设计与回环验证过程。这个看似简单的任务,却让我经历了从晚上8点到凌晨2点的漫长Debug过程,也让我深刻理解了硬件开发中"理想"与"现实"的差距。
我的项目目标是实现一个基于FPGA的多路UART控制器,核心功能包括:
最初,我以为回环测试不过是把uart_rx和uart_tx在顶层连接起来,看着串口助手里的数据完美回显就大功告成了。但现实给了我一记响亮的耳光——板子下载程序后疯狂发送数据、串口助手被0xFF和0x00之类的垃圾信息刷屏、LED灯不受控地乱跳...这些现象让我意识到,FPGA开发远不是写对代码那么简单。
UART接收模块的核心在于准确检测和解析异步串行数据。我采用了经典的16倍过采样方案,其工作原理如下:
对于115200波特率:
多数表决逻辑:
verilog复制6,7,8,9,10,11: START_BIT <= START_BIT + uart_rx_sync2;
// ... 数据位采样 ...
150,151,152,153,154,155: STOP_BIT <= STOP_BIT + uart_rx_sync2;
这种设计能有效过滤线路噪声,提高接收稳定性。
接收模块采用三段式状态机:
关键的双拍同步与下降沿检测代码:
verilog复制always@(posedge clk or negedge reset_n)
if(!reset_n) begin
uart_rx_sync1 <= 1'b0;
uart_rx_sync2 <= 1'b0;
end
else begin
uart_rx_sync1 <= uart_rx;
uart_rx_sync2 <= uart_rx_sync1;
end
assign uart_rx_nedge = ~uart_rx_reg1 & uart_rx_reg2;
注意事项:同步寄存器初始化为0可能导致上电瞬间RX引脚为高时被误判为上升沿。虽然不直接导致乱发,但会在边界条件下引入亚稳态问题。
最初的理想化设计如下:
verilog复制module uart_loopback(
input wire clk,
input wire reset_n,
input wire uart_rx,
output wire uart_tx,
output wire led0, led1, led2, led3, led4, led5, led6, led7
);
// 接收模块
jieshou u_receiver(
.clk(clk),
.reset_n(reset_n),
.uart_rx(uart_rx),
.baud_set(3'd4), // 115200
.data_byte(rx_data),
.rx_done(rx_done)
);
// 发送模块(回环)
uart_tx u_transmitter(
.clk(clk),
.reset_n(reset_n),
.send_en(rx_done && !tx_busy),
.data_byte(rx_data),
.baud_set(3'd4),
.uart_tx(uart_tx),
.tx_done(),
.uart_state(tx_busy)
);
// LED显示接收数据
assign led0 = rx_data[0];
// ... led1-7 ...
endmodule
问题现象:编译报错Cannot find pin led0
根本原因:
output wire [7:0] led,但约束文件分开定义led0到led7修正后的约束文件:
tcl复制set_pin_assignment { uart_rx } { LOCATION = E4; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { uart_tx } { LOCATION = E3; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; }
经验教训:约束文件引脚定义必须对照原理图逐字核对,简单的引脚混淆可能导致整个系统无法正常工作。
修正引脚后,乱发问题依然存在。我尝试了多种软件过滤方案:
关键发现:按住复位键时,乱发停止;松开后立即恢复。这说明问题不在代码逻辑,而在CH9102F芯片的上电自检特性——它会输出格式完全正确的"假数据"。
verilog复制module uart_loopback(
input wire clk,
input wire reset_n,
input wire uart_rx,
output wire uart_tx,
output wire led0, led1, led2, led3, led4, led5, led6, led7
);
// 接收模块(仅用于LED显示)
jieshou u_jieshou(
.clk(clk),
.reset_n(reset_n),
.uart_rx(uart_rx),
.baud_set(baud_set),
.data_byte(rx_data),
.rx_done(rx_done)
);
// 硬件直连:物理层回环
assign uart_tx = uart_rx;
// LED显示
assign led0 = rx_data[0];
// ... led1-7 ...
endmodule
这个方案的优点:
发送模块关键设计点:
verilog复制case(baud_set)
0: bps_DR <= 16'd5207; // 9600
1: bps_DR <= 16'd2603; // 19200
2: bps_DR <= 16'd1301; // 38400
3: bps_DR <= 16'd867; // 57600
4: bps_DR <= 16'd433; // 115200
default: bps_DR <= 16'd433;
endcase
verilog复制always@(posedge clk or negedge reset_n)
if(!reset_n)
uart_state <= 1'b0;
else if(send_en && !tx_busy) // 防重复触发
uart_state <= 1'b1;
else if(bps_cnt == 16'd11)
uart_state <= 1'b0;
接收模块的创新点:
verilog复制else if((bps_cnt == 8'd159) || ((bps_cnt == 8'd12) && (START_BIT > 2)))
uart_state <= 1'b0;
在bps_cnt=12时检查起始位,如果START_BIT>2(多数表决不通过),则提前终止接收。
verilog复制data_byte[0] <= (data_byte_pre0 >= 4) ? 1'b1 : 1'b0;
// ...其他数据位...
采用>=4的阈值判断(6次采样中至少4次为1才判为1),提高抗干扰能力。
虽然当前项目没有实现FIFO,但成熟的UART设计应考虑:
选择依据:
这个项目让我深刻体会到:真正的工程思维,是在资源约束下找到最优解而非完美解。六小时的调试看似浪费,实则是宝贵的经验积累——它教会我在未来的项目中更高效地定位问题、更务实地选择解决方案。