本文对于串口通信接收的讲解主要分为下面3个部分展开(具体见右侧目录):

  1. 串口接收逻辑设计要点分析
  2. 串口接收逻辑的Verilog设计与验证(代码部分)
  3. 串口接收设计优化

一、串口接收逻辑设计要点分析

设计一个UART串口接收逻辑控制器,要求:

  • 能够接收8位,无校验位,1位停止位的UART串口数据
  • 能够通过一定的方式修改波特率(parameter)
  • 每接收完一个数据,将接收到的数据结果显示到开发板的8位LED灯上
  • 每接收完一个数据,将开发板上的LED8状态翻转一次(注意是翻转,不是闪烁,效果同串口发送逻辑的一样)

要点1:
对于串口接收来说,我们应该怎么样从串行数据中准确的获取到每一位数据?

借助计数器,像上一节一样

要点2:
如何检测串口接收数据的起始位的下降沿?

检测下降沿可以参考下面电路,当且仅当上一次输出Q为1 且当前输出D为0时 下降沿检测电路输出为1,检测上升沿与此同理。 

 要点3:
如何设计波特率分频计数器使能逻辑?

我们可能会遇到两种情况

第一种在结束时不再接收到信号

第二种就是遇到毛刺,这种情况要排除

 要点4:
如何知道当前所读取信号位于UART信号的哪一位?

借助位计数器

要点5:
如何实现位接收逻辑?

借助case

要点6:
产生一个标志信号,能在每次接收完毕后,通知外界数据接收完成

假如我在采样停止位的时候,采样到了低电平怎么办:
恿知外界完成了一个数据的接收,不产生标志信号做法1:该数据直接舍弃,也就是不,
做法2:当成新的起始位来解
做法3:当成正确的数据通知外部
做法4:通知外界接收完成,但同时也输出一个错误标志信号,告知外界,此次接收的数据是错误的

二、串口接收逻辑的Verilog设计与验证

verilog代码

`timescale 1ns / 1ps  // 定义仿真时间单位/精度

module uart_byte_rx(
    input Clk,        // 系统时钟输入
    input Reset_n,    // 低电平有效复位信号
    input uart_rx,    // UART 串行接收数据线
    output reg Rx_Done, // 接收完成标志信号
    output reg [7:0] Rx_Data // 接收到的8位数据
    );
    
    // 参数定义
    parameter CLOCK_FREQ = 50000000;  // 系统时钟频率50MHz
    parameter BAUD = 9600;            // 波特率设置
    parameter MCNT_BAUD = CLOCK_FREQ / BAUD - 1; // 计算波特率分频计数值
    
    // 内部寄存器声明
    reg [29:0] baud_div_cnt;  // 波特率分频计数器
    reg en_baud_cnt;          // 波特率计数器使能信号
    reg [3:0] bit_cnt;        // 接收位计数器(0-9共10个状态)
    reg r_uart_rx;            // 经过同步后的UART输入信号
    wire w_Rx_Done;           // 接收完成中间信号
    wire nedge_uart_rx;       // UART输入下降沿检测信号
    
    // 同步寄存器用于消除亚稳态
    reg dffO_uart_rx;         // 第一级同步触发器
    reg dff1_uart_rx;         // 第二级同步触发器

    // 波特率计数器逻辑
    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        baud_div_cnt <= 0;    // 复位时计数器清零
    else if(en_baud_cnt) begin
        if(baud_div_cnt == MCNT_BAUD)
            baud_div_cnt <= 0; // 达到最大值时归零
        else
            baud_div_cnt <= baud_div_cnt + 1'd1; // 正常计数
    end
    else
        baud_div_cnt <= 0;    // 计数器未使能时保持清零
    
    // UART输入信号同步和边沿检测逻辑
    always@(posedge Clk)
        dffO_uart_rx <= uart_rx;  // 第一级同步
    always@(posedge Clk)
        dff1_uart_rx <= dffO_uart_rx; // 第二级同步
    always@(posedge Clk)
        r_uart_rx <= dff1_uart_rx;    // 第三级寄存用于边沿检测
    
    assign nedge_uart_rx = (dff1_uart_rx == 0) && (r_uart_rx == 1); // 检测下降沿
    
    // 波特率计数器使能控制逻辑
    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        en_baud_cnt <= 0;           // 复位时禁用计数器
    else if(nedge_uart_rx)          // 检测到起始位下降沿
        en_baud_cnt <= 1;           // 启动波特率计数器
    else if((baud_div_cnt == MCNT_BAUD/2) && (bit_cnt == 0) && (dff1_uart_rx == 1))
        en_baud_cnt <= 0;           // 起始位验证失败(非持续低电平)时停止
    else if((baud_div_cnt == MCNT_BAUD) && (bit_cnt == 9))
        en_baud_cnt <= 0;           // 接收完成(10位)后停止计数器
                                        
    // 接收位计数器逻辑
    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        bit_cnt <= 0;               // 复位时位计数器清零
    else if(baud_div_cnt == MCNT_BAUD) begin  // 每个波特率周期
        if(bit_cnt == 9)
            bit_cnt <= 0;          // 接收完10位后归零
        else
            bit_cnt <= bit_cnt + 1'd1; // 正常递增位计数器
    end
    
    // 数据位接收逻辑(在波特率周期中点采样)
    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        Rx_Data <= 8'd0;            // 复位时数据寄存器清零
    else if(baud_div_cnt == MCNT_BAUD/2) begin  // 在波特率周期中间采样
        case(bit_cnt)
            1: Rx_Data[0] <= dff1_uart_rx;  // 接收第1位数据(LSB)
            2: Rx_Data[1] <= dff1_uart_rx;  // 接收第2位数据
            3: Rx_Data[2] <= dff1_uart_rx;  // 接收第3位数据
            4: Rx_Data[3] <= dff1_uart_rx;  // 接收第4位数据
            5: Rx_Data[4] <= dff1_uart_rx;  // 接收第5位数据
            6: Rx_Data[5] <= dff1_uart_rx;  // 接收第6位数据
            7: Rx_Data[6] <= dff1_uart_rx;  // 接收第7位数据
            8: Rx_Data[7] <= dff1_uart_rx;  // 接收第8位数据(MSB)
            default: Rx_Data <= Rx_Data;  // 其他状态保持数据不变
        endcase
    end  
    
    // 接收完成标志生成逻辑
    assign w_Rx_Done = ((baud_div_cnt == MCNT_BAUD) && (bit_cnt == 9)); // 第10位(停止位)完成
    always@(posedge Clk)
    if(!Reset_n)
        Rx_Done <= 0;           // 复位时完成标志清零
    else
        Rx_Done <= w_Rx_Done;   // 同步输出完成信号
    
endmodule

再回顾一下UART接收模块的系统架构:

一、模块接口
输入:

  • 系统时钟(Clk) // 50MHz主时钟

  • 低电平复位信号(Reset_n) // 全局异步复位

  • 串行数据输入(uart_rx) // RS-232标准信号

输出:

  • 接收完成标志(Rx_Done) // 高电平有效脉冲信号

  • 8位并行数据(Rx_Data) // 有效数据输出

二、核心功能模块

  1. 信号同步与边沿检测单元

    • 三级同步触发器链:消除亚稳态

    • 下降沿检测电路:识别起始位(1→0跳变)

  2. 波特率生成系统

    • 可配置分频器:CLOCK_FREQ/BAUD-1

    • 16位计数器:产生精确的波特率时钟

  3. 接收状态控制器

    • 位计数器:0(起始位)~9(停止位)状态机

    • 使能控制逻辑:管理采样时序

  4. 数据采样单元

    • 中点采样机制:每位中心点采样

    • 数据移位寄存器:串并转换存储

  5. 校验与完成系统

    • 起始位验证:检测虚假起始信号

    • 停止位检测:确认帧结束

    • 完成信号生成:输出有效脉冲

三、工作流程

  1. 起始检测阶段

    • 持续监测uart_rx信号

    • 检测到下降沿后启动接收流程

  2. 波特率同步阶段

    • 启动波特率分频计数器

    • 在起始位中点验证持续低电平

  3. 数据接收阶段

    • 每波特率周期递增位计数器

    • 在数据位中点进行8次采样

    • 完成8位数据存储

  4. 帧结束处理

    • 检测停止位高电平

    • 生成接收完成信号

    • 复位内部状态机

四、关键设计特性

  1. 可靠性设计

    • 三级信号同步:消除亚稳态

    • 中点采样:抗信号抖动

    • 起始位验证:防误触发

  2. 灵活配置

    • 可调波特率:通过参数修改

    • 自动计算分频系数:CLOCK_FREQ/BAUD

  3. 错误处理机制

    • 起始位异常终止:检测到非持续低电平立即中止接收

    • 超时保护:内置状态机超时复位

五、时序特征

  1. 典型帧处理周期

    • 10个波特率周期(1起始+8数据+1停止)

    • 9600bps时约1.04ms/字节

  2. 信号延迟

    • 完成信号相对停止位中点延迟1个系统时钟周期

    • 数据输出与完成信号同步更新

六、性能参数

  1. 最大理论速率

    • 取决于系统时钟频率

    • 50MHz系统时钟支持最高3.125Mbps

  2. 资源占用

    • 约32个寄存器

    • 无乘法器/分频器特殊资源

仿真代码

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 2025/03/28 22:46:36
// Design Name: 
// Module Name: uart_byte_rx_tb
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


module uart_byte_rx_tb(
    );
    reg Clk;
    reg Reset_n;
    reg uart_rx;
    wire [7:0]Rx_Data; 
    wire Rx_Done;    

    uart_byte_rx uart_byte_rx(
        .Clk(Clk),
        .Reset_n(Reset_n),
        .uart_rx(uart_rx),
        .Rx_Data(Rx_Data),
        .Rx_Done(Rx_Done)  
    );   
    
    initial Clk = 1;
    always#10 Clk = ~Clk;
    
    initial begin
       Reset_n = 0;
       uart_rx = 1;
       #201;
       Reset_n = 1;
       #200; 
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       #(5208*20*10);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       
        #(5208*20*10);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=0;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       uart_rx=1;       #(5208*20);
       #(5208*20*10);
       $stop;
    end    
    
    
endmodule
仿真结果 

三、串口接收设计优化

可能存在的问题

通常来说,接收器接收一个数据所耗费的时间比发送器长100ns,就会出现发送方发送完第一个数据后,并立即开始发送第二个数据,而接收方还处于接收上一个数据的状态,忽略了新一个数据的起始位,导致第二个数据接收失败。 

想象你在餐厅传菜

  • 原版设计:服务员必须把菜完全放到桌上(波特率周期结束),才能回头接新菜

  • 问题:当厨房快速出菜时,服务员回头时新菜已经掉地上(错过起始位)

优化后的设计

  1. 服务员在菜盘刚进桌边(半周期时)就松手

  2. 利用菜滑向桌心的惯性时间(剩余半周期):

    • 确认菜已上桌 ✅

    • 提前转身准备接新菜 👐

为什么有效

步骤 原版耗时 优化版耗时 赢得的时间用途
完成当前任务 10秒 5秒 提前准备迎接新任务
准备新任务 0秒 5秒 能及时响应下一个请求

为了解决这个问题,可以为发送器提供0.5位的容错时间

原来的代码,也就是接收完成标志信号

((baud_div_cnt == MCNT_BAUD)&& (bit_cnt==9))

将条件修改为

((baud_div_cnt == MCNT_BAUD/2)&& (bit_cnt==9))

 同时还要修改位计数器逻辑

    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        bit_cnt<= 0;
    else if(baud_div_cnt==MCNT_BAUD)begin
        if(bit_cnt==9)
            bit_cnt<=0;
        else
            bit_cnt<=bit_cnt+1'd1;
    end

修改为

    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        bit_cnt<= 0;
    else if((bit_cnt==9)&&(baud_div_cnt==MCNT_BAUD/2))
        bit_cnt<= 0;
    else if(baud_div_cnt==MCNT_BAUD)
        bit_cnt<=bit_cnt+1'd1;

这样就能兼容更多的硬件了,短距离,非强干扰情况下适用 。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐