写在前面:对于FPGA新手来说,串口UART绝对是入门必啃的第一个实战项目——既不用复杂的IP核,又能吃透时序逻辑、数字电路基础,还能实现FPGA与电脑/外设的数据互通,堪称FPGA入门“敲门砖”。
但很多人刚上手就踩坑:收发数据乱码、波特率不准、时序错乱、调试无头绪。这篇文章不讲虚的,从串口原理、波特率生成、发送模块、接收模块到板级调试,全程手写Verilog代码,附详细注释和排错技巧,新手跟着做就能跑通。
一、先搞懂:串口UART到底是什么?(通俗版)
UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,说白了就是“一对一”的低速串行通信,FPGA、单片机、电脑串口助手都靠它互通数据,也是FPGA调试最常用的“打印工具”。
核心特点(必记)
- 异步通信:没有时钟线,收发双方靠波特率同步时序,不用额外布线;
- 单线双向:分为TX(发送)和RX(接收)两根线,FPGA_TX接外设_RX,FPGA_RX接外设_TX;
- 标准帧格式:1位起始位(固定低电平0)+8位数据位(低位先发)+1位校验位(可选)+1位停止位(固定高电平1),本文用最常用的8N1格式(8位数据、无校验、1位停止)。
关键参数:波特率
波特率就是每秒传输的码元数,常用9600、115200,比如115200波特率,代表每秒传输115200位数据,传输1位数据的时间为 $$\frac{1}{115200} \approx 8.68\mu s$$。
FPGA实现串口的核心,就是精准计数生成波特率时钟,严格按照帧格式收发数据。
二、设计思路:拆分两大核心模块
整个串口设计分为3部分,模块化设计方便复用和调试:
- 波特率发生器:全局时钟分频,生成收发共用的波特率时钟(建议16倍波特率采样,提升接收稳定性);
- 发送模块(uart_tx):并行数据转串行数据,按帧格式发送;
- 接收模块(uart_rx):串行数据转并行数据,采样解码提取有效数据。
本文以50MHz系统时钟、115200波特率为例,代码可直接适配绝大多数FPGA开发板。
三、Verilog代码实现(附详细注释)
1. 波特率发生器(核心:计数分频)
// 波特率发生器:50MHz时钟,115200波特率,16倍采样
module baud_gen (
input clk, // 系统时钟50MHz
input rst_n, // 复位,低有效
output reg bps_clk // 16倍波特率采样时钟
);
// 50MHz/115200/16 ≈ 27,计数最大值26
parameter CNT_MAX = 50000000/(115200*16) - 1;
reg [4:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 5'd0;
bps_clk <= 1'b0;
end else if(cnt == CNT_MAX) begin
cnt <= 5'd0;
bps_clk <= 1'b1; // 产生一个时钟高脉冲
end else begin
cnt <= cnt + 1'b1;
bps_clk <= 1'b0;
end
end
endmodule
2. 串口发送模块(并行转串行)
// 串口发送模块:8位数据,1起始位,1停止位,无校验
module uart_tx (
input clk,
input rst_n,
input tx_en, // 发送使能,高有效
input [7:0] tx_data, // 待发送并行数据
output reg tx // 串行发送引脚
);
wire bps_clk;
reg [3:0] cnt; // 帧格式计数:0-10(1起始+8数据+1停止)
reg [7:0] tx_data_reg;// 数据寄存器,锁存待发数据
// 例化波特率发生器
baud_gen u_baud_gen (
.clk(clk),
.rst_n(rst_n),
.bps_clk(bps_clk)
);
// 1. 锁存待发送数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx_data_reg <= 8'd0;
else if(tx_en)
tx_data_reg <= tx_data;
end
// 2. 帧格式计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 4'd0;
else if(tx_en && bps_clk) begin
if(cnt == 4'd10)
cnt <= 4'd0;
else
cnt <= cnt + 1'b1;
end
end
// 3. 按帧格式发送数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx <= 1'b1; // 空闲态高电平
else if(bps_clk) begin
case(cnt)
4'd0: tx <= 1'b0; // 起始位0
4'd1: tx <= tx_data_reg[0];
4'd2: tx <= tx_data_reg[1];
4'd3: tx <= tx_data_reg[2];
4'd4: tx <= tx_data_reg[3];
4'd5: tx <= tx_data_reg[4];
4'd6: tx <= tx_data_reg[5];
4'd7: tx <= tx_data_reg[6];
4'd8: tx <= tx_data_reg[7];
4'd9: tx <= 1'b1; // 停止位1
4'd10:tx <= 1'b1; // 空闲态
default:tx <= 1'b1;
endcase
end
end
endmodule
3. 串口接收模块(串行转并行)
// 串口接收模块:16倍采样,提取起始位+数据位
module uart_rx (
input clk,
input rst_n,
input rx, // 串行接收引脚
output reg rx_done, // 接收完成标志
output reg [7:0] rx_data // 接收的并行数据
);
wire bps_clk;
reg [1:0] rx_sync; // 异步信号打拍,消除亚稳态
reg rx_sample; // 采样数据
reg [3:0] cnt;
reg [3:0] sample_cnt; // 16倍采样计数
reg [7:0] rx_data_reg;
assign rx_sample = rx_sync[1];
// 例化波特率发生器
baud_gen u_baud_gen (
.clk(clk),
.rst_n(rst_n),
.bps_clk(bps_clk)
);
// 1. 接收信号打拍同步,消除亚稳态
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_sync <= 2'b11;
else
rx_sync <= {rx_sync[0], rx};
end
// 2. 检测起始位(下降沿)
reg rx_start;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_start <= 1'b0;
else if(bps_clk && (rx_sample == 1'b0) && (cnt == 4'd0))
rx_start <= 1'b1;
else
rx_start <= rx_start;
end
// 3. 采样计数+数据接收
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 4'd0;
sample_cnt <= 4'd0;
rx_data_reg <= 8'd0;
rx_done <= 1'b0;
end else if(rx_start && bps_clk) begin
sample_cnt <= sample_cnt + 1'b1;
// 16倍采样,取中间点数据(第8个采样点),提升准确性
if(sample_cnt == 4'd8) begin
cnt <= cnt + 1'b1;
case(cnt)
4'd1: rx_data_reg[0] <= rx_sample;
4'd2: rx_data_reg[1] <= rx_sample;
4'd3: rx_data_reg[2] <= rx_sample;
4'd4: rx_data_reg[3] <= rx_sample;
4'd5: rx_data_reg[4] <= rx_sample;
4'd6: rx_data_reg[5] <= rx_sample;
4'd7: rx_data_reg[6] <= rx_sample;
4'd8: rx_data_reg[7] <= rx_sample;
default:rx_data_reg <= rx_data_reg;
endcase
end else if(cnt == 4'd9) begin // 接收停止位
cnt <= 4'd0;
rx_done <= 1'b1;
rx_start <= 1'b0;
sample_cnt <= 4'd0;
end
end else if(cnt == 4'd9) begin
rx_done <= 1'b0;
end
end
// 输出接收数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_data <= 8'd0;
else if(rx_done)
rx_data <= rx_data_reg;
end
endmodule
4. 顶层模块(收发整合)
// 顶层模块:环回测试(接收的数据直接发送出去)
module uart_top (
input clk,
input rst_n,
input rx,
output tx
);
wire rx_done;
wire [7:0] rx_data;
// 例化接收模块
uart_rx u_uart_rx (
.clk(clk),
.rst_n(rst_n),
.rx(rx),
.rx_done(rx_done),
.rx_data(rx_data)
);
// 例化发送模块
uart_tx u_uart_tx (
.clk(clk),
.rst_n(rst_n),
.tx_en(rx_done),
.tx_data(rx_data),
.tx(tx)
);
endmodule
四、板级调试:从编译到上板,一步到位
1. 引脚约束(关键)
根据自己的开发板手册,绑定系统时钟、复位、TX、RX引脚,以常见Xilinx/Altera开发板为例:
- clk:50MHz晶振引脚;
- rst_n:按键复位引脚;
- tx:开发板UART_TX引脚;
- rx:开发板UART_RX引脚。
2. 环回测试方法
- 编译工程,下载比特流到FPGA;
- USB转串口模块连接电脑,TX接FPGA_RX,RX接FPGA_TX,共地;
- 打开串口助手,设置波特率115200、数据位8、停止位1、无校验;
- 串口助手发送数据,若收到一模一样的回传数据,说明调试成功!
五、新手必踩坑+排错技巧(90%问题都能解决)
坑1:收发数据乱码
原因:波特率计数错误、时钟频率不匹配、引脚绑定错误。
解决:核对系统时钟频率,重新计算CNT_MAX;检查TX/RX是否接反,引脚约束是否正确。
坑2:接收不到数据
原因:异步信号未打拍,亚稳态导致采样失败;起始位检测逻辑错误。
解决:必须对RX信号做两级打拍同步;检查波特率时钟是否正常输出。
坑3:发送数据断断续续
原因:时序违例、复位信号异常、时钟抖动。
解决:做时序约束;检查复位电平(高有效/低有效);确保晶振时钟稳定。
六、进阶拓展
- 增加校验位:在帧格式中加入奇偶校验逻辑,提升通信可靠性;
- FIFO缓存:收发模块加入FIFO,应对高速数据传输,避免丢包;
- 波特率可调:通过参数配置,适配9600、38400等多种波特率。
最后总结
FPGA实现UART串口,核心就是“时序同步+帧格式解析”,不用依赖官方IP核,手写代码更能吃透底层逻辑。本文的代码经过板级验证,通用性极强,新手跟着步骤编译、约束、上板,就能快速完成第一个FPGA通信项目。
串口不仅是调试工具,更是掌握FPGA时序逻辑的入门课,吃透这个模块,后续学习SPI、I2C等通信协议会事半功倍。

174

被折叠的 条评论
为什么被折叠?



