news 2026/5/9 9:32:35

别再只会调库了!手把手教你用Verilog从零实现一个可配置的UART收发器(附完整代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再只会调库了!手把手教你用Verilog从零实现一个可配置的UART收发器(附完整代码)

从零构建可配置UART收发器的Verilog实战指南

在数字电路设计中,UART(通用异步收发器)作为最基础的串行通信协议之一,其重要性不言而喻。许多工程师虽然能够熟练调用现成的UART IP核,但对底层实现原理却知之甚少。本文将带领读者从零开始,用Verilog实现一个完全可配置的UART收发器,支持5-8位数据宽度、可调波特率和可选的奇偶校验功能。

1. UART核心模块架构设计

一个完整的UART系统主要由三个关键模块组成:波特率发生器、发送器和接收器。我们先来看整体架构:

module uart_top #( parameter DATA_WIDTH = 8, parameter STOP_BITS = 1, parameter PARITY_EN = 1, parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input clk, input rst_n, input tx_start, input [DATA_WIDTH-1:0] tx_data, output tx_done, output tx_busy, input rx_in, output [DATA_WIDTH-1:0] rx_data, output rx_valid, output rx_error ); // 内部信号声明 wire baud_tick; wire [15:0] baud_div; // 实例化子模块 baud_generator #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE) ) u_baud_gen ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .baud_div(baud_div) ); uart_tx #( .DATA_WIDTH(DATA_WIDTH), .STOP_BITS(STOP_BITS), .PARITY_EN(PARITY_EN) ) u_tx ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .tx_start(tx_start), .tx_data(tx_data), .tx_out(tx_out), .tx_done(tx_done), .tx_busy(tx_busy) ); uart_rx #( .DATA_WIDTH(DATA_WIDTH), .STOP_BITS(STOP_BITS), .PARITY_EN(PARITY_EN) ) u_rx ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .rx_in(rx_in), .rx_data(rx_data), .rx_valid(rx_valid), .rx_error(rx_error) ); endmodule

这个顶层模块通过参数化设计实现了高度可配置性:

  • DATA_WIDTH: 数据位宽度(5-8位)
  • STOP_BITS: 停止位数量(1或2位)
  • PARITY_EN: 奇偶校验使能
  • CLK_FREQ: 系统时钟频率
  • BAUD_RATE: 波特率设置

2. 波特率生成器的精确实现

波特率生成器的核心是一个分频计数器,它将系统时钟分频到所需的波特率时钟。这里的关键是避免累积误差:

module baud_generator #( parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input clk, input rst_n, output reg baud_tick, output [15:0] baud_div ); // 计算分频系数 localparam DIVIDER = CLK_FREQ / BAUD_RATE; assign baud_div = DIVIDER; reg [15:0] counter; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 0; baud_tick <= 0; end else begin if (counter == DIVIDER - 1) begin counter <= 0; baud_tick <= 1; end else begin counter <= counter + 1; baud_tick <= 0; end end end endmodule

注意:实际设计中需要考虑系统时钟频率与波特率的整数倍关系。当不能整除时,可以采用累加器实现小数分频。

3. UART发送器的状态机设计

发送器采用有限状态机(FSM)实现,状态转换图如下:

IDLE → START → DATA0 → DATA1 → ... → DATAn → PARITY → STOP1 → STOP2(可选) → IDLE

对应的Verilog实现:

module uart_tx #( parameter DATA_WIDTH = 8, parameter STOP_BITS = 1, parameter PARITY_EN = 1 )( input clk, input rst_n, input baud_tick, input tx_start, input [DATA_WIDTH-1:0] tx_data, output reg tx_out, output reg tx_done, output reg tx_busy ); // 状态定义 typedef enum { IDLE, START, DATA, PARITY, STOP } state_t; state_t current_state, next_state; reg [3:0] bit_counter; reg [DATA_WIDTH-1:0] data_reg; reg parity_bit; // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else if (baud_tick) current_state <= next_state; end // 下一状态逻辑 always @(*) begin case (current_state) IDLE: next_state = tx_start ? START : IDLE; START: next_state = DATA; DATA: begin if (bit_counter == DATA_WIDTH - 1) next_state = PARITY_EN ? PARITY : STOP; else next_state = DATA; end PARITY: next_state = STOP; STOP: next_state = IDLE; default: next_state = IDLE; endcase end // 输出逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_out <= 1'b1; tx_done <= 1'b0; tx_busy <= 1'b0; bit_counter <= 0; data_reg <= 0; parity_bit <= 0; end else if (baud_tick) begin case (next_state) IDLE: begin tx_out <= 1'b1; tx_done <= 1'b0; tx_busy <= 1'b0; if (tx_start) begin data_reg <= tx_data; tx_busy <= 1'b1; // 计算奇偶校验位 if (PARITY_EN) parity_bit <= ^tx_data; end end START: begin tx_out <= 1'b0; // 起始位 bit_counter <= 0; end DATA: begin tx_out <= data_reg[bit_counter]; bit_counter <= bit_counter + 1; end PARITY: tx_out <= parity_bit; STOP: begin tx_out <= 1'b1; if (current_state == STOP && (STOP_BITS == 1 || bit_counter == 1)) tx_done <= 1'b1; end endcase end end endmodule

4. UART接收器的抗干扰设计

接收器设计的关键在于起始位检测和数据的抗干扰采样。我们采用16倍过采样和多数表决机制来提高可靠性:

module uart_rx #( parameter DATA_WIDTH = 8, parameter STOP_BITS = 1, parameter PARITY_EN = 1 )( input clk, input rst_n, input baud_tick, input rx_in, output reg [DATA_WIDTH-1:0] rx_data, output reg rx_valid, output reg rx_error ); // 状态定义 typedef enum { IDLE, START, DATA, PARITY, STOP } state_t; state_t current_state, next_state; reg [3:0] bit_counter; reg [DATA_WIDTH-1:0] data_reg; reg [3:0] sample_counter; reg [7:0] samples; reg parity_bit, parity_error; // 多数表决函数 function majority_vote; input [7:0] samples; begin // 统计1的个数 integer i, count; count = 0; for (i = 0; i < 8; i = i + 1) if (samples[i]) count = count + 1; majority_vote = (count >= 4) ? 1'b1 : 1'b0; end endfunction // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else if (baud_tick) current_state <= next_state; end // 下一状态逻辑 always @(*) begin case (current_state) IDLE: next_state = (majority_vote(samples) == 1'b0) ? START : IDLE; START: next_state = DATA; DATA: begin if (bit_counter == DATA_WIDTH - 1) next_state = PARITY_EN ? PARITY : STOP; else next_state = DATA; end PARITY: next_state = STOP; STOP: next_state = IDLE; default: next_state = IDLE; endcase end // 采样逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin samples <= 8'hFF; sample_counter <= 0; end else begin if (sample_counter == 15) sample_counter <= 0; else sample_counter <= sample_counter + 1; // 在采样点附近采集多个样本 if (sample_counter >= 4 && sample_counter <= 11) samples[sample_counter-4] <= rx_in; end end // 输出逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_data <= 0; rx_valid <= 1'b0; rx_error <= 1'b0; bit_counter <= 0; data_reg <= 0; parity_bit <= 0; parity_error <= 0; end else if (baud_tick && sample_counter == 15) begin case (next_state) IDLE: begin rx_valid <= 1'b0; rx_error <= 1'b0; end START: begin bit_counter <= 0; parity_bit <= 0; end DATA: begin data_reg[bit_counter] <= majority_vote(samples); bit_counter <= bit_counter + 1; // 更新奇偶校验 if (PARITY_EN) parity_bit <= parity_bit ^ majority_vote(samples); end PARITY: begin parity_error <= (majority_vote(samples) != parity_bit); end STOP: begin if (majority_vote(samples) == 1'b1) begin rx_data <= data_reg; rx_valid <= 1'b1; rx_error <= parity_error; end else begin rx_error <= 1'b1; // 停止位错误 end end endcase end end endmodule

5. 验证与调试技巧

完成RTL设计后,我们需要进行充分的验证。以下是一些实用的验证方法:

  1. 自检测试:设计一个回环测试,将发送端直接连接到接收端:
module uart_loopback_test; reg clk = 0; reg rst_n = 0; reg tx_start = 0; reg [7:0] tx_data = 0; wire tx_done; // 实例化UART uart_top u_uart ( .clk(clk), .rst_n(rst_n), .tx_start(tx_start), .tx_data(tx_data), .tx_done(tx_done), .rx_in(u_uart.tx_out), // 回环连接 .rx_data(), .rx_valid(), .rx_error() ); // 时钟生成 always #5 clk = ~clk; initial begin #100 rst_n = 1; #200 tx_data = 8'hA5; tx_start = 1; #10 tx_start = 0; @(posedge tx_done); #1000 $finish; end endmodule
  1. 波形分析要点

    • 检查波特率时钟是否准确
    • 验证起始位、数据位、停止位的时序
    • 检查奇偶校验是否正确生成和检测
  2. 常见问题排查表

问题现象可能原因解决方案
接收不到数据波特率不匹配检查波特率分频系数计算
数据错位起始位检测不准确增加过采样倍数,优化多数表决逻辑
偶发错误抗干扰不足增加采样点数量,优化滤波算法
奇偶校验错误校验位计算错误检查发送端和接收端的校验算法是否一致

6. 性能优化与扩展思路

基础功能实现后,可以考虑以下优化和扩展方向:

  1. FIFO缓冲:添加FIFO可以解决数据突发问题,典型实现:
module uart_fifo #( parameter WIDTH = 8, parameter DEPTH = 16 )( input clk, input rst_n, input wr_en, input [WIDTH-1:0] din, input rd_en, output [WIDTH-1:0] dout, output full, output empty ); reg [WIDTH-1:0] mem [0:DEPTH-1]; reg [4:0] wr_ptr, rd_ptr; reg [4:0] count; assign full = (count == DEPTH); assign empty = (count == 0); assign dout = mem[rd_ptr[3:0]]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 0; rd_ptr <= 0; count <= 0; end else begin case ({wr_en, rd_en}) 2'b01: if (!empty) begin rd_ptr <= rd_ptr + 1; count <= count - 1; end 2'b10: if (!full) begin mem[wr_ptr[3:0]] <= din; wr_ptr <= wr_ptr + 1; count <= count + 1; end 2'b11: begin mem[wr_ptr[3:0]] <= din; wr_ptr <= wr_ptr + 1; rd_ptr <= rd_ptr + 1; end default: ; endcase end end endmodule
  1. 自动波特率检测:通过测量起始位宽度自动识别波特率

  2. 多UART通道集成:通过时分复用实现多通道支持

  3. DMA接口:添加DMA支持以减少CPU开销

7. 实际应用中的经验分享

在真实的项目开发中,UART模块往往会遇到各种意料之外的问题。以下是几个实际案例中的经验总结:

  1. 时钟域交叉问题:当系统时钟与波特率时钟不同源时,需要特别注意跨时钟域同步。一个简单的解决方案是在接收端使用两级触发器同步外部信号:
reg rx_sync1, rx_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 <= 1'b1; rx_sync2 <= 1'b1; end else begin rx_sync1 <= rx_in; rx_sync2 <= rx_sync1; end end
  1. 电磁干扰问题:在工业环境中,长距离UART通信容易受到干扰。除了硬件滤波外,可以在软件层面增加以下处理:

    • 增加起始位验证(连续多个采样点都为低才确认起始位)
    • 实现简单的CRC校验而不仅是奇偶校验
    • 添加超时重传机制
  2. 低功耗优化:对于电池供电设备,可以:

    • 在不通信时关闭波特率时钟
    • 使用更深的睡眠模式,通过起始位唤醒
    • 动态调整波特率以降低功耗
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 9:26:43

终极鼠标性能测试指南:3步完成专业评测

终极鼠标性能测试指南&#xff1a;3步完成专业评测 【免费下载链接】MouseTester 项目地址: https://gitcode.com/gh_mirrors/mo/MouseTester MouseTester是一款专业的鼠标性能测试工具&#xff0c;能够深入分析鼠标响应速度、移动轨迹精度和点击延迟等关键指标。无论你…

作者头像 李华
网站建设 2026/5/9 9:24:43

3分钟上手TMSpeech:完全离线的Windows实时语音识别神器

3分钟上手TMSpeech&#xff1a;完全离线的Windows实时语音识别神器 【免费下载链接】TMSpeech 腾讯会议摸鱼工具 项目地址: https://gitcode.com/gh_mirrors/tm/TMSpeech 还在为会议记录手忙脚乱&#xff1f;担心在线语音识别泄露隐私&#xff1f;TMSpeech让你彻底告别这…

作者头像 李华
网站建设 2026/5/9 9:24:43

Claude Code 深度使用指南:45个技巧打造AI编程副驾驶

1. 项目概述&#xff1a;Claude Code 深度使用指南如果你是一名开发者&#xff0c;并且已经尝试过 Claude Code&#xff0c;你可能会觉得它就是个带代码编辑功能的聊天机器人。但我想告诉你&#xff0c;你只看到了冰山一角。我花了大量时间深度使用和“折腾”这个工具&#xff…

作者头像 李华