当前位置: 首页 > news >正文

别再只会调库了!手把手教你用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. 低功耗优化:对于电池供电设备,可以:

    • 在不通信时关闭波特率时钟
    • 使用更深的睡眠模式,通过起始位唤醒
    • 动态调整波特率以降低功耗
http://www.jsqmd.com/news/782144/

相关文章:

  • 从CTF实战案例反推:安全归约思想如何帮你快速定位加密题漏洞?
  • AD7606并行驱动避坑指南:实测200KHz采样率下,为什么你的数据会窜通道?
  • 游戏化学习新范式:CLI驱动的任务式编程学习系统设计与实践
  • 量化研究入门:基于开源框架的策略开发与回测实战
  • 手把手教你用Veeam Backup 12.2搞定ESXi 7.0虚拟机备份(附离线激活与代理配置)
  • AI协作五要素精简指南
  • Windows右键菜单终极清理指南:ContextMenuManager让你的桌面操作效率翻倍
  • 通过 curl 命令快速测试 Taotoken 各模型接口是否通畅
  • 专业直播录制工具:DouyinLiveRecorder完全实战指南
  • SpringBoot + WebSocket实战:从零搭建一个能实时聊天、加好友的Web应用(附完整源码)
  • 量子化学模拟新突破:CIM-QS(H)CI算法解析
  • 鸣潮自动化助手:解放双手的智能游戏伴侣,轻松告别重复劳动
  • 2026 年智能割草机厂家推荐 国内源头生产厂家实力详细盘点 - 奔跑123
  • 别再纠结Mealy还是Moore了!手把手教你用Verilog三段式状态机搞定序列检测(附仿真对比)
  • 实测华南4家头部知识产权代理机构,广东德硕凭实力成为企业信赖之选 - 速递信息
  • 终极Blender与虚幻引擎桥梁:5分钟掌握PSK/PSA格式导入导出
  • 手把手教你读懂BetaFlight统一配置文件:从AOCODARC H7DUAL板子看硬件定义
  • 2026 国内有实力的十大铸铝门厂家精选推荐(制造业 / ToB 高客单专属・实战案例 + 数据佐证) - 呼呼拉呼
  • 3分钟快速指南:MiGPT让小爱音箱秒变AI语音助手完整教程
  • 多智能体系统编排框架:从原理到实践,构建AI协作工作流
  • 通过OpenClaw配置Taotoken快速搭建AI智能体工作流
  • 告别卡顿!用一张SD卡和Rufus,5分钟搞定友善R2S固件刷写(保姆级教程)
  • 2026 智能割草机厂家哪家强?果园 / 林地 / 荒地作业 - 奔跑123
  • 抖音视频下载终极指南:轻松获取无水印高清内容的完整解决方案
  • 5分钟掌握终极免费图表工具:用代码思维重塑你的文档创作体验
  • 别再用默认停用词表了!手把手教你用Python清洗哈工大停用词表,适配你的NLP项目
  • EcoVadis携手谷歌云AI:你的文件还能“蒙混过关”吗? - 奋飞咨询ecovadis
  • 2026年有哪些优质的四川白酒加盟品牌可以推荐?四川五粮人家项目合作/四川白酒项目合作/四川白酒品牌代理 - 品牌推荐官方
  • 镭达晶元的讯可安入选工信部目录、获国家医保局一等奖:一份值得被看见的“国家级信任状” - 热敏感科技蜂
  • 命令行提示符定制指南:用prompt-line打造高效终端仪表盘