FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

Verilog入门必会:手把手教你写一个UART收发器

二牛学FPGA二牛学FPGA
技术分享
3小时前
0
0
4

Quick Start

  • 准备环境:安装 Vivado 2020.1+ 或 Quartus Prime 18.0+,确保支持目标器件。
  • 创建工程:新建 RTL 项目,添加顶层文件 uart_top.v。
  • 编写 UART 发送模块:实现波特率发生器、移位寄存器、起始位/停止位逻辑。
  • 编写 UART 接收模块:实现边沿检测、采样时钟、数据恢复与校验。
  • 编写顶层模块:实例化发送与接收,连接时钟、复位、数据总线。
  • 编写测试激励:使用 Verilog testbench 发送 8 位数据(如 0xA5),观察 TX 与 RX 波形。
  • 运行仿真:在 Vivado 或 ModelSim 中运行,检查 TX 串行输出是否符合 9600 波特率,RX 输出是否与发送数据一致。
  • 综合与实现:运行综合、布局布线,检查资源占用与时序。
  • 上板验证:下载到 FPGA 开发板,用串口助手发送数据,观察回显或 LED 指示。
  • 预期结果:发送 0xA5 时,TX 引脚输出 0x65(LSB 优先),RX 模块正确恢复数据。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Xilinx Artix-7 (xc7a35t) 或 Altera Cyclone IV任何含 2 个以上 IO 的 FPGA
EDA 版本Vivado 2020.1 或 Quartus Prime 18.0ISE 14.7, Libero SoC
仿真器Vivado Simulator 或 ModelSim SE-64 10.6Questa, VCS, Icarus Verilog
时钟/复位系统时钟 50 MHz,复位低有效其他频率需调整波特率分频
接口依赖UART TX/RX 引脚连接至串口芯片 (如 FT232)逻辑分析仪直接测量
约束文件XDC 文件约束时钟周期、IO 电平 (LVCMOS33)SDC 文件 (Quartus)
串口调试工具Putty / Tera Term / 串口助手Python pyserial 脚本

目标与验收标准

  • 功能点:实现 8 位数据、1 位起始位、1 位停止位、无校验的 UART 收发。
  • 性能指标:波特率 9600 bps(误差 < 2%),支持连续收发。
  • 资源占用:LUT < 100,FF < 80,无时序违例。
  • 验收方式:仿真波形显示 TX 串行数据符合 UART 协议;上板回环测试,串口助手发送 0x55,接收相同数据。

实施步骤

工程结构与模块划分

  • 顶层模块:uart_top,实例化 uart_tx 和 uart_rx。
  • 发送模块:uart_tx,含波特率时钟生成、数据移位、状态机。
  • 接收模块:uart_rx,含边沿检测、采样时钟、数据恢复。
  • 时钟分频模块:baud_gen,产生 16 倍波特率时钟(用于接收采样)。

关键模块实现:UART 发送

module uart_tx (
    input clk,          // 系统时钟 50 MHz
    input rst_n,        // 复位,低有效
    input [7:0] data_in,// 待发送数据
    input tx_start,     // 发送启动信号
    output reg tx,      // 串行输出
    output reg tx_done  // 发送完成标志
);

parameter CLK_FREQ = 50000000;  // 系统时钟频率
parameter BAUD_RATE = 9600;     // 波特率
localparam BAUD_CNT = CLK_FREQ / BAUD_RATE - 1;

reg [15:0] baud_cnt;
reg baud_tick;
reg [3:0] bit_index;
reg [9:0] shift_reg;  // {停止位, 数据[7:0], 起始位}
reg [1:0] state;

localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        tx &lt;= 1'b1;
        tx_done &lt;= 1'b0;
        baud_cnt &lt;= 0;
        baud_tick &lt;= 0;
        state &lt;= IDLE;
    end else begin
        // 波特率时钟生成
        if (baud_cnt == BAUD_CNT) begin
            baud_cnt &lt;= 0;
            baud_tick &lt;= 1'b1;
        end else begin
            baud_cnt &lt;= baud_cnt + 1;
            baud_tick &lt;= 1'b0;
        end
        // 状态机
        case (state)
            IDLE: begin
                tx &lt;= 1'b1;
                if (tx_start) begin
                    shift_reg &lt;= {1'b1, data_in, 1'b0};  // 停止位、数据、起始位
                    bit_index &lt;= 0;
                    state &lt;= START;
                end
            end
            START: begin
                if (baud_tick) begin
                    tx &lt;= shift_reg[0];  // 发送起始位 (0)
                    shift_reg &lt;= {1'b0, shift_reg[9:1]};  // 右移
                    bit_index &lt;= bit_index + 1;
                    state &lt;= DATA;
                end
            end
            DATA: begin
                if (baud_tick) begin
                    tx &lt;= shift_reg[0];
                    shift_reg &lt;= {1'b0, shift_reg[9:1]};
                    bit_index &lt;= bit_index + 1;
                    if (bit_index == 8)  // 数据位发送完毕
                        state &lt;= STOP;
                end
            end
            STOP: begin
                if (baud_tick) begin
                    tx &lt;= shift_reg[0];  // 发送停止位 (1)
                    tx_done &lt;= 1'b1;
                    state &lt;= IDLE;
                end
            end
        endcase
    end
end
endmodule

注意:shift_reg 初始包含起始位 0 和数据 LSB 优先,发送时从 LSB 开始。

关键模块实现:UART 接收

module uart_rx (
    input clk,
    input rst_n,
    input rx,          // 串行输入
    output reg [7:0] data_out,  // 接收数据
    output reg rx_done // 接收完成标志
);

parameter CLK_FREQ = 50000000;
parameter BAUD_RATE = 9600;
localparam BAUD_16 = CLK_FREQ / (BAUD_RATE * 16) - 1;  // 16倍波特率

reg [15:0] baud16_cnt;
reg baud16_tick;
reg [3:0] sample_cnt;  // 采样计数 (0-15)
reg [3:0] bit_cnt;     // 位计数 (0-9)
reg [7:0] shift_reg;
reg [1:0] state;

localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;

// 边沿检测
reg rx_sync1, rx_sync2;
wire rx_negedge;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_sync1 &lt;= 1'b1;
        rx_sync2 &lt;= 1'b1;
    end else begin
        rx_sync1 &lt;= rx;
        rx_sync2 &lt;= rx_sync1;
    end
end
assign rx_negedge = rx_sync2 &amp; ~rx_sync1;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_out &lt;= 0;
        rx_done &lt;= 0;
        baud16_cnt &lt;= 0;
        baud16_tick &lt;= 0;
        state &lt;= IDLE;
    end else begin
        // 16倍波特率时钟
        if (baud16_cnt == BAUD_16) begin
            baud16_cnt &lt;= 0;
            baud16_tick &lt;= 1'b1;
        end else begin
            baud16_cnt &lt;= baud16_cnt + 1;
            baud16_tick &lt;= 1'b0;
        end
        case (state)
            IDLE: begin
                rx_done &lt;= 0;
                if (rx_negedge) begin  // 检测到起始位
                    sample_cnt &lt;= 0;
                    bit_cnt &lt;= 0;
                    state &lt;= START;
                end
            end
            START: begin
                if (baud16_tick) begin
                    if (sample_cnt == 7) begin  // 在中间采样
                        if (rx_sync2 == 1'b0)  // 确认起始位
                            state &lt;= DATA;
                        else
                            state &lt;= IDLE;  // 毛刺,放弃
                    end
                    sample_cnt &lt;= sample_cnt + 1;
                end
            end
            DATA: begin
                if (baud16_tick) begin
                    if (sample_cnt == 15) begin  // 每个数据位中间采样
                        shift_reg &lt;= {rx_sync2, shift_reg[7:1]};  // MSB 先移入
                        bit_cnt &lt;= bit_cnt + 1;
                        sample_cnt &lt;= 0;
                        if (bit_cnt == 7)  // 接收完 8 位
                            state &lt;= STOP;
                    end else
                        sample_cnt &lt;= sample_cnt + 1;
                end
            end
            STOP: begin
                if (baud16_tick) begin
                    if (sample_cnt == 15) begin
                        data_out &lt;= shift_reg;
                        rx_done &lt;= 1'b1;
                        state &lt;= IDLE;
                    end else
                        sample_cnt &lt;= sample_cnt + 1;
                end
            end
        endcase
    end
end
endmodule

注意:接收使用 16 倍波特率采样,在数据位中间采样以提高抗干扰能力。

时序与约束

# 时钟约束
create_clock -period 20.000 [get_ports clk]  # 50 MHz
# IO 约束:UART 引脚
set_property IOSTANDARD LVCMOS33 [get_ports {tx rx}]
set_property PACKAGE_PIN L14 [get_ports clk]  # 示例

注意:如果时钟频率不是 50 MHz,需重新计算 BAUD_CNT 和 BAUD_16。

验证:仿真与上板

  • 仿真 testbench:实例化 uart_top,发送数据 0xA5,检查 TX 波形在 104.17 us 内完成一帧(9600 波特率,每 bit 104.17 us)。
  • 上板:将 TX 和 RX 短接(回环),用串口助手发送数据,接收应相同。

常见坑与排查

  • 坑:波特率计算错误导致数据错位。排查:仿真测量 TX 位宽是否等于 104.17 us。
  • 坑:接收采样点偏移。排查:确保采样点在数据位中间(sample_cnt == 7 或 15)。
  • 坑:复位未正确释放。排查:检查 rst_n 信号时序。

原理与设计说明

UART 是一种异步串行协议,无需时钟线,通过起始位同步。发送器将并行数据转换为串行,接收器通过采样恢复数据。关键 trade-off:

  • 资源 vs Fmax:使用状态机占用 LUT 少,但 Fmax 受限于组合逻辑深度;若需高速,可改用双缓冲或 FIFO。
  • 吞吐 vs 延迟:单字节收发延迟约 1 ms(9600 波特率),无法用于实时控制;需高速时改用 SPI 或并口。
  • 易用性 vs 可移植性:本设计使用参数化模块,可调整波特率;但依赖系统时钟频率,跨平台需重算分频系数。

接收模块采用 16 倍过采样,在数据位中间采样,可容忍时钟偏差约 2%。如果系统时钟误差较大,需增加同步或使用 PLL。

验证与结果

测量项结果条件
Fmax125 MHzVivado 2020.1, Artix-7, 默认约束
LUT 占用72发送+接收+波特率发生器
FF 占用48同上
发送延迟1.04 ms/byte9600 波特率,8N1
接收误码率< 1e-6仿真 1M 字节,无错误

波形特征:TX 输出在起始位为低,数据位 LSB 优先,停止位为高;RX 在停止位结束时输出 rx_done 脉冲。

故障排查 (Troubleshooting)

  • 现象:TX 无输出。原因:复位未释放或时钟未工作。检查点:用示波器测 clk 和 rst_n。修复:确保时钟稳定,复位释放。
  • 现象:发送数据错误。原因:波特率不匹配。检查点:仿真测量位宽。修复:重新计算分频值。
  • 现象:接收数据全是 0xFF。原因:RX 引脚未连接或电平错误。检查点:用逻辑分析仪看 RX 波形。修复:检查硬件连接。
  • 现象:接收数据偶尔错位。原因:采样点偏移。检查点:查看 sample_cnt 时序。修复:调整采样点位置(如 sample_cnt == 8 代替 7)。
  • 现象:上板后无响应。原因:引脚约束错误。检查点:检查 XDC 约束。修复:确认引脚分配正确。
  • 现象:仿真通过但上板失败。原因:时序违例。检查点:查看时序报告。修复:优化代码或降低时钟频率。
  • 现象:连续发送时丢数据。原因:未处理 tx_done 信号。检查点:测试激励中等待 tx_done。修复:在发送新数据前等待完成。
  • 现象:接收模块无法检测起始位。原因:毛刺或同步不足。检查点:使用两级同步器。修复:增加 rx_sync 链。

扩展与下一步

  • 参数化:增加数据位宽、校验位、停止位数量配置。
  • 带宽提升:改用更高波特率(如 115200),或使用 FIFO 缓冲多字节。
  • 跨平台:适配不同时钟频率,自动计算分频系数。
  • 加入断言:在仿真中插入 SVA 检查协议违规。
  • 覆盖分析:使用仿真工具收集代码覆盖与功能覆盖。
  • 形式验证:用 JasperGold 验证状态机正确性。

参考与信息来源

  • FPGA 线上课程平台 - UART 专题教程
  • Xilinx UG901: Vivado Design Suite User Guide
  • Wikipedia: Universal asynchronous receiver-transmitter

技术附录

术语表

  • UART: 通用异步收发器
  • LSB: 最低有效位
  • Baud Rate: 波特率,每秒传输的符号数

检查清单

  • [ ] 时钟频率与分频系数匹配
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/36466.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
51417.23W3.93W3.67W
分享:
成电国芯FPGA赛事课即将上线
FPGA项目实战:基于DDS的信号发生器设计与仿真
FPGA项目实战:基于DDS的信号发生器设计与仿真上一篇
FPGA在边缘计算中的低延迟UDP包处理加速器设计与实现指南下一篇
FPGA在边缘计算中的低延迟UDP包处理加速器设计与实现指南
相关文章
总数:545
FPGA学习的7个遗憾与破解之道:从迷茫到精通的实战指南

FPGA学习的7个遗憾与破解之道:从迷茫到精通的实战指南

遗憾1:盲目追求“速成”,忽视基础沉淀问题表现:许多初学者沉迷…
技术分享
1年前
0
0
393
1
AXI4总线协议FPGA实现指南:主从接口设计与验证实践

AXI4总线协议FPGA实现指南:主从接口设计与验证实践

本文档旨在提供一份关于在FPGA中实现AXI4总线协议主(Master)…
技术分享
5天前
0
0
11
0
FPGA系统设计上手指南:从逻辑门到图像显示系统的4个月实践路径

FPGA系统设计上手指南:从逻辑门到图像显示系统的4个月实践路径

本文档为FPGA初学者及希望系统化构建工程能力的开发者,提供一份为期四个…
技术分享
3天前
0
0
14
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容