FPGA实现串口UART:从零手写Verilog,一文吃透收发模块+调试避坑

写在前面:对于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部分,模块化设计方便复用和调试:

  1. 波特率发生器:全局时钟分频,生成收发共用的波特率时钟(建议16倍波特率采样,提升接收稳定性);
  2. 发送模块(uart_tx):并行数据转串行数据,按帧格式发送;
  3. 接收模块(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. 环回测试方法

  1. 编译工程,下载比特流到FPGA;
  2. USB转串口模块连接电脑,TX接FPGA_RX,RX接FPGA_TX,共地;
  3. 打开串口助手,设置波特率115200、数据位8、停止位1、无校验;
  4. 串口助手发送数据,若收到一模一样的回传数据,说明调试成功!


五、新手必踩坑+排错技巧(90%问题都能解决)
坑1:收发数据乱码
原因:波特率计数错误、时钟频率不匹配、引脚绑定错误。
解决:核对系统时钟频率,重新计算CNT_MAX;检查TX/RX是否接反,引脚约束是否正确。
坑2:接收不到数据
原因:异步信号未打拍,亚稳态导致采样失败;起始位检测逻辑错误。
解决:必须对RX信号做两级打拍同步;检查波特率时钟是否正常输出。
坑3:发送数据断断续续
原因:时序违例、复位信号异常、时钟抖动。
解决:做时序约束;检查复位电平(高有效/低有效);确保晶振时钟稳定。



六、进阶拓展

  • 增加校验位:在帧格式中加入奇偶校验逻辑,提升通信可靠性;
  • FIFO缓存:收发模块加入FIFO,应对高速数据传输,避免丢包;
  • 波特率可调:通过参数配置,适配9600、38400等多种波特率。


最后总结
FPGA实现UART串口,核心就是“时序同步+帧格式解析”,不用依赖官方IP核,手写代码更能吃透底层逻辑。本文的代码经过板级验证,通用性极强,新手跟着步骤编译、约束、上板,就能快速完成第一个FPGA通信项目。
串口不仅是调试工具,更是掌握FPGA时序逻辑的入门课,吃透这个模块,后续学习SPI、I2C等通信协议会事半功倍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值