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

一、串口通信发送原理 (UART\TTL\RS232\RS485)

二、串口发送逻辑设计要点分析

三、串口发送逻辑Verilog设计与仿真验证

四、串口发送逻辑优化

五、常见问题

串口通信发送原理

1. UART:协议层

UART(Universal Asynchronous Receiver/Transmitter) 是一种异步串行通信协议,定义了数据如何以位(bit)的形式在设备间传输,但不涉及物理电平或硬件接口的规范。其核心特点包括:

  • 异步传输:无需时钟信号同步,依靠起始位和停止位界定数据帧。

  • 数据帧格式:每帧数据包含:

    • 1个起始位(低电平,0)

    • 5-9个数据位(通常为8位)

    • 可选的校验位(奇偶校验或无校验)

    • 1-2个停止位(高电平,1)

  • 波特率(Baud Rate):双方需约定一致的波特率(如9600、115200),即每秒传输的符号数(符号=1 bit)。

  • 115200:每秒钟传输115200个码元,每个码元占用时间为1/115200秒

UART: 全称为Universal Asynchronous Receiver/Transmitter,通用异步收发器是一种串行异步的通信协议,该协议规定了传输数据时数据的传输方式以及所使用的信号,在嵌入式领域中有着非常广泛的应用。

关键点

  • UART仅负责协议层,即数据格式、时序逻辑,不定义物理层的电平标准或硬件接口。

  • 在FPGA/单片机中,UART模块通常通过TTL电平直接输出信号,但无法直接与RS232/RS485设备通信,需电平转换芯片。


2. TTL、RS232、RS485:物理层标准

(1) TTL电平
  • 定义:晶体管-晶体管逻辑电平(Transistor-Transistor Logic),是数字电路中最常见的电平标准。

  • 电压范围

    • 逻辑0:0V(或接近0V,如0.3V)

    • 逻辑1:3.3V或5V(取决于供电电压)

  • 应用场景

    • 芯片间短距离通信(如FPGA与传感器直接连接)。

    • 常见于开发板的UART引脚(如Arduino的TX/RX)。

与UART的关系

  • UART协议在芯片内部通过TTL电平实现,但若需与外部设备通信(如PC),必须转换为RS232/RS485电平。


(2) RS232
  • 定义:一种单端信号串行通信标准,定义了物理接口(如DB9接头)和电平规范。

  • 电压范围

    • 逻辑0:+3V至+15V(正电压)

    • 逻辑1:-3V至-15V(负电压)

  • 特点

    • 抗干扰能力弱,通信距离短(通常<15米)。

    • 支持全双工(收发独立线路)。

  • 应用场景

    • 早期PC串口(COM口)、工业设备调试接口。

与UART的关系

  • UART + RS232电平转换芯片(如MAX232) = RS232接口

  • 例如:FPGA的TTL-UART信号通过MAX232芯片转换为RS232电平后,才能与PC的COM口通信。


(3) RS485
  • 定义:一种差分信号串行通信标准,支持多点通信(总线拓扑)。

  • 电压范围

    • 逻辑0:A线比B线高+2V以上(差分电压)。

    • 逻辑1:B线比A线高+2V以上。

  • 特点

    • 抗干扰能力强,通信距离长(可达1200米)。

    • 支持半双工(同一线路分时收发)或多点通信。

  • 应用场景

    • 工业自动化(如PLC、传感器网络)。

    • 长距离多设备通信。

与UART的关系

  • UART + RS485电平转换芯片(如MAX485) = RS485接口

  • RS485仅定义物理层,数据格式仍需遵循UART协议。


3. UART与物理层标准的对比

特性 TTL RS232 RS485
电平类型 单端(0V/5V) 单端(±3-15V) 差分(A-B电压)
通信距离 <1米 <15米 可达1200米
抗干扰能力 一般
拓扑结构 点对点 点对点 总线型(多点)
典型应用 芯片间通信 PC串口 工业现场总线

4. 总结与设计要点

  1. UART是协议,TTL/RS232/RS485是物理层标准

    • UART定义数据帧格式和时序,物理层标准定义如何用电平表示逻辑0/1。

    • UART数据需要经过电平转换芯片才能适配不同物理接口

  2. 选择电平标准的依据

    • 通信距离:短距离用TTL,中距离用RS232,长距离用RS485。

    • 抗干扰需求:工业环境优先选择RS485。

    • 拓扑结构:多点通信必须使用RS485。

  3. 常见组合与芯片

    • UART转RS232:MAX232、SP3232。

    • UART转RS485:MAX485、SN65HVD72。


5. 常见问题

Q1:为什么FPGA的UART不能直接连接PC的RS232串口?
  • FPGA的UART输出是TTL电平(0V/3.3V),而PC的RS232使用±12V电平,直接连接会损坏硬件。需通过MAX232芯片进行电平转换。

Q2:RS485如何实现多点通信?
  • RS485总线挂接多个设备,每个设备通过使能信号控制收发状态。同一时间只能有一个设备发送数据,避免冲突。

Q3:如何选择校验位?
  • 校验位用于错误检测:

    • 奇校验:数据位+校验位中1的个数为奇数。

    • 偶校验:数据位+校验位中1的个数为偶数。

    • 无校验:不进行校验,依赖硬件稳定性。

UART串口发送逻辑设计要点分析

在这个模块的学习中,我们主要解决以下任务:设计一个串口发送模块,发送用户输入的数据给电脑,要求:

  • 波特率为9600
  • 8位数据位
  • 1位停止位
  • 无校验位
  • 无流控功能
  • 每1s发送一次当前8位拨码开关的值
  • 每次发送完成后将LED0的状态翻转

先理解我们需要设计的端口有哪些,见下图

思路:设计一个1s计数器,不断计数,当计数到1s时,让串口发送一次数据

一、总体设计目标

实现一个自动循环发送的UART模块,每发送完一帧数据后等待固定间隔再发送下一帧,同时用LED指示发送状态。设计核心围绕精准时序控制数据完整性展开。


二、核心功能模块分解

1. 波特率发生器
  • 作用:产生符合9600波特率的时钟信号

  • 实现

    • 50MHz系统时钟 → 每个波特周期需计数5208次(参数MCNT_BAUD=5208-1

    • 计数器baud_div_cnt在使能信号en_baud_cnt有效时循环计数

    • 关键点:每个完整的波特周期(计数到5208)对应1个数据位的持续时间

2. 数据帧控制器
  • 作用:控制每帧数据的10个位(1起始位 + 8数据位 + 1停止位)顺序发送

  • 实现

    • 位计数器bit_cnt在波特周期结束时递增(0→1→2...→9→0循环)

    • 关键状态

      • bit_cnt=0:发送起始位(低电平)

      • bit_cnt=1-8:发送数据位(从LSB到MSB)

      • bit_cnt=9:发送停止位(高电平)

3. 发送间隔控制器
  • 作用:防止连续发送导致数据淹没

  • 实现

    • 延时计数器delay_cnt持续计数到50,000,000(约1秒@50MHz)

    • 达到计数值时触发新数据锁存和发送使能

    • 关键逻辑:只有延时计数器归零后才允许启动新一轮发送

4. 数据锁存器
  • 作用:保证发送过程中数据稳定不变

  • 实现

    • 在延时计数器归零瞬间锁存输入数据Datar_Data

    • 必要性:避免发送中途数据变化导致位错误

5. 串行化发送
  • 作用:将并行数据转为符合UART协议的串行比特流

  • 实现

    • 根据bit_cnt值选择发送内容:

      • 起始位强制拉低

      • 数据位按顺序输出

      • 停止位强制拉高

    • 默认状态:非发送期间保持高电平(符合UART空闲状态)

6. 状态指示
  • 作用:可视化反馈发送完成状态

  • 实现

    • 每完成一帧数据(bit_cnt=9且波特周期结束)翻转LED

    • LED频率 = 1/(2×发送间隔)


三、工作流程

  1. 初始状态

    • 所有计数器归零

    • uart_tx输出高电平(空闲状态)

    • LED熄灭

  2. 等待阶段

    • 延时计数器delay_cnt开始累加

    • 当计数到50,000,000(约1秒):

      • 锁存新数据到r_Data

      • 开启波特率使能信号en_baud_cnt

  3. 数据发送阶段

    • 波特率发生器开始工作,每5208个时钟周期产生一个位切换信号

    • 位计数器从0开始递增,每个波特周期切换一次:

      复制

      0 → 起始位(0) → D0 → D1 → ... → D7 → 停止位(1) → 0(循环)
    • 串行输出按位计数器状态依次发送对应比特

  4. 发送完成

    • bit_cnt=9且波特周期结束时:

      • 关闭en_baud_cnt停止发送

      • 翻转LED状态

      • 重启延时计数器进入下一轮等待


四、设计亮点

  1. 三级安全防护

    • 波特使能en_baud_cnt严格限定发送窗口

    • 数据锁存r_Data保证帧内数据稳定

    • 延时计数器防止数据过载

  2. 精准时序控制

    • 所有状态变化严格对齐波特周期结束点(baud_div_cnt==MCNT_BAUD

    • 避免亚稳态和毛刺

  3. 可观测性设计

    • LED直接反映发送周期

    • 每个功能模块都有明确的计数器指示状态


五、参数调整指南

  • 修改波特率:调整MCNT_BAUD
    公式:MCNT_BAUD = (时钟频率) / (波特率) - 1

  • 修改数据位宽:调整MCNT_BIT
    例如发送7位数据:MCNT_BIT = 9-1(1+7+1)

  • 修改发送间隔:调整MCNT_DLY
    例如500ms间隔:MCNT_DLY = 25_000_000-1

Verilog设计与仿真验证

uart_byte_tx代码
module uart_byte_tx(
    Clk,
    Reset_n,
    Data,
    uart_tx,
    Led
);
    // 端口定义
    input Clk;          // 系统时钟(50MHz)
    input Reset_n;      // 低电平有效复位
    input [7:0] Data;   // 待发送的8位并行数据
    output reg uart_tx; // UART串行输出
    output reg Led;     // 发送状态指示灯

    // 参数配置
    parameter MCNT_BAUD = 5208-1;    // 波特率计数器最大值(对应9600波特率)
    parameter MCNT_BIT  = 10-1;      // 每帧数据总位数(1起始位+8数据位+1停止位)
    parameter MCNT_DLY  = 50_000_000-1; // 发送间隔延迟(1秒@50MHz)

    // 内部寄存器
    reg [12:0] baud_div_cnt;  // 波特率分频计数器(13位)
    reg en_baud_cnt;          // 波特率计数使能信号
    reg [7:0] r_Data;         // 数据锁存寄存器(保证发送过程数据稳定)
    reg [25:0] delay_cnt;     // 发送间隔计数器(26位)
    reg [3:0] bit_cnt;        // 数据位计数器(0-9共10位)

    //============ 波特率发生器 ============//
    always @(posedge Clk or negedge Reset_n) begin
        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;  // 未使能时保持清零
    end
    /* 设计要点:
    1. 每个波特率周期包含5208个时钟周期(对应50MHz时钟下的9600波特率)
    2. 仅在en_baud_cnt有效时计数,精确控制发送时序 */

    //============ 波特率使能控制 ============//
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)
            en_baud_cnt <= 0;
        else begin
            if (delay_cnt == MCNT_DLY)
                en_baud_cnt <= 1;  // 延迟时间到,启动发送
            else if ((bit_cnt == 9) && (baud_div_cnt == MCNT_BAUD))
                en_baud_cnt <= 0;  // 发送完停止位后关闭使能
        end  
    end
    /* 核心逻辑:
    1. 通过delay_cnt实现发送间隔控制(防数据淹没有效措施)
    2. 严格限定使能信号作用区间,确保每次只发送一帧数据 */

    //============ 数据位计数器 ============// 
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)
            bit_cnt <= 0;
        else if (en_baud_cnt && (baud_div_cnt == MCNT_BAUD)) begin
            if (bit_cnt == MCNT_BIT)
                bit_cnt <= 0;  // 完成10位发送后复位
            else 
                bit_cnt <= bit_cnt + 1'd1; // 每个波特周期递增
        end
    end
    /* 关键设计:
    1. 同步于波特率时钟边沿,确保精确的位切换时序
    2. 计数范围0-9对应:起始位(0) + 数据位(1-8) + 停止位(9) */

    //============ 发送间隔计数器 ============//
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)    
            delay_cnt <= 0;
        else if (delay_cnt == MCNT_DLY)
            delay_cnt <= 0;  // 达到设定间隔后复位
        else
            delay_cnt <= delay_cnt + 1'd1; // 持续计数
    end
    /* 功能说明:
    1. 控制两次发送之间的最小间隔(此处设为1秒)
    2. 防止连续发送导致接收端缓冲区溢出 */

    //============ 输入数据锁存 ============//
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)    
            r_Data <= 0;
        else if (delay_cnt == MCNT_DLY)
            r_Data <= Data;  // 在发送间隔结束时锁存新数据
        else
            r_Data <= r_Data; // 发送期间保持数据稳定
    end
    /* 重要特性:
    1. 数据在发送开始时锁存,保证帧内数据一致性
    2. 避免发送过程中数据变化导致的位错误 */

    //============ 串行数据发送 ============//
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)
            uart_tx <= 1'd1;  // 复位时保持高电平(UART空闲状态)
        else if (en_baud_cnt == 0)
            uart_tx <= 1'd1;  // 非发送期间保持高电平
        else begin
            case (bit_cnt)
                0: uart_tx <= 1'd0;   // 起始位(低电平)
                1: uart_tx <= r_Data[0]; // LSB先发送
                2: uart_tx <= r_Data[1];
                3: uart_tx <= r_Data[2];
                4: uart_tx <= r_Data[3];
                5: uart_tx <= r_Data[4];
                6: uart_tx <= r_Data[5];
                7: uart_tx <= r_Data[6];
                8: uart_tx <= r_Data[7]; // MSB
                9: uart_tx <= 1'd1;     // 停止位(高电平)
                default: uart_tx <= uart_tx;
            endcase     
        end
    end
    /* 发送协议实现:
    1. 严格遵循UART帧格式:Start(0) + D0-D7 + Stop(1)
    2. LSB(最低有效位)先发送的行业标准实现 */

    //============ 状态指示灯控制 ============//
    always @(posedge Clk or negedge Reset_n) begin
        if (!Reset_n)
            Led <= 0;
        else if ((bit_cnt == 9) && (baud_div_cnt == MCNT_BAUD))
            Led <= !Led;  // 每次完整发送后翻转LED
    end
    /* 可视化指示:
    1. LED闪烁频率 = 1/(2*MCNT_DLY*时钟周期) 
    2. 直观显示发送完成状态 */
    
endmodule

    reg [12:0]baud_div_cnt;
    首先是计算波特率计数器  1/9600 *1000000000/20-1  是13位,至于为什么这么计算请看下面的解释

  1. 1/9600:计算每个比特位的持续时间(秒),即波特率为9600时,每个比特占用的时间为1/9600秒 ≈ 104.1667微秒。

  2. 乘以1000000000:将秒转换为纳秒(1秒=1e9纳秒),得到每个比特的纳秒数:
    19600×109≈104166.6667 纳秒96001​×109≈104166.6667纳秒。

  3. 除以20:假设系统时钟周期为20纳秒(对应50MHz频率),计算每个比特需要多少个时钟周期:
    104166.666720≈5208.333320104166.6667​≈5208.3333。

  4. 减1:由于计数器从0开始计数到N-1,需将结果减1得到最大计数值:
    5208.3333−1≈5207.33335208.3333−1≈5207.3333,取整后为5207

        即在50MHz系统时钟下,通过一个13位计数器(最大计数值8191),设置baud_div_cnt为5207,即可生成约9600.307的波特率(误差0.0032%),满足实际应用需求。此计算确保每5208个时钟周期(0到5207)产生一次波特率时钟信号。

仿真代码

module uart_byte_tx_tb(

    );
    reg Clk;
    reg Reset_n;
    reg [7:0]Data;
    wire uart_tx;
    wire Led;   
    

    uart_byte_tx uart_byte_tx(
        .Clk       (Clk),     // 连接系统时钟
        .Reset_n   (Reset_n),   // 连接系统复位信号(低有效)
        .Data      (Data),     // 连接待发送的字节数据
        .uart_tx   (uart_tx), // 连接UART物理输出引脚
        .Led       (Led)   // 连接状态指示灯
    );
    defparam uart_byte_tx.MCNT_DLY=500000-1;
    
    initial Clk=1;
    always #10 Clk=~Clk;
    
    
    
    initial  begin
    Reset_n=0;
    #201;
    Reset_n=1;
    Data=8'b0101_0101;
    #30000000;
    Data=8'b1001_1001;
    #30000000; 
    
    $stop;
    end
    
endmodule

板子烧录

 板子连线:比平时多加一个USB 转串口模块

串口发送逻辑优化


module uart_byte_tx(
    Clk,
    Reset_n,
    Send_Go,
    Data,
    uart_tx,
    Tx_Done
    );
    
    input Clk;
    input Reset_n;
    input [7:0]Data;
    input Send_Go;
    output reg uart_tx;
    output reg Tx_Done;
    
    parameter BAUD=9600;
    parameter CLOCK_FREQ=50_000000;
    parameter MCNT_BAUD=CLOCK_FREQ/BAUD-1;
    parameter MCNT_BIT=10-1;

    reg [29:0]baud_div_cnt;
    reg en_baud_cnt;
    reg [7:0]r_Data;
    reg [3:0]bit_cnt;
    wire w_Tx_Done;
    
    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;
        
    always@(posedge Clk or negedge Reset_n)
        if(!Reset_n)
            en_baud_cnt<=0;
        else begin 
        if(Send_Go)
            en_baud_cnt<=1;
        else if(w_Tx_Done)
            en_baud_cnt<=0;        
        end  
    //位计数器

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

    always@(posedge Clk or negedge Reset_n)
        if(!Reset_n)    
            r_Data<=0;
        else if(Send_Go)
            r_Data<=Data;
        else
            r_Data<=r_Data;
        
    //位发送逻辑
    always@(posedge Clk or negedge Reset_n)
    if(!Reset_n)
        uart_tx<=1'd1;
    else if(en_baud_cnt==0)
    uart_tx<=1'd1;
    else begin
        case (bit_cnt)
            0:uart_tx<=1'd0; 
            1:uart_tx<=r_Data[0];
            2:uart_tx<=r_Data[1];  
            3:uart_tx<=r_Data[2];  
            4:uart_tx<=r_Data[3];  
            5:uart_tx<=r_Data[4];  
            6:uart_tx<=r_Data[5];  
            7:uart_tx<=r_Data[6];  
            8:uart_tx<=r_Data[7];  
            9:uart_tx<=1'd1;        
            default:uart_tx<=uart_tx;
            endcase     
        end

    assign w_Tx_Done=((bit_cnt==9)&&(baud_div_cnt==MCNT_BAUD));
    always@(posedge Clk )
        Tx_Done<=w_Tx_Done;
endmodule

一、接口优化(核心改进)

1. 新增控制信号
  • Send_Go输入:外部触发发送信号

  • Tx_Done输出:发送完成标志(替代原LED指示)

2. 移除冗余信号
  • 删除原Led输出和delay_cnt延时计数器

优势

  • 更标准化接口:符合UART控制器通用设计规范

  • 精确控制:外部可自由决定发送时机,不再受限于固定延时

  • 状态明确Tx_Done直接反馈发送状态,便于系统级控制


二、参数化设计改进

1. 动态计算波特率参数
parameter BAUD=9600;
parameter CLOCK_FREQ=50_000000; 
parameter MCNT_BAUD=CLOCK_FREQ/BAUD-1; // 自动计算计数值
2. 移除固定延时参数
  • 删除原MCNT_DLY=50_000_000-1

优势

  • 灵活配置:修改波特率/时钟频率时无需手动重算计数值

  • 代码复用性:模块可直接用于不同时钟系统(如25/100MHz平台)


三、核心逻辑优化

1. 发送控制逻辑重构
版本 启动条件 停止条件
原版 延时计数器超时 发送完停止位
新版 Send_Go信号触发 w_Tx_Done标志生效

也可以总结为

模块 旧版逻辑 新版逻辑 改进点
发送触发 延时计数器超时自动启动 Send_Go信号触发 支持外部实时控制,响应速度更快
数据锁存 延时结束时锁存Data Send_Go生效时立即锁存 确保发送最新数据,避免旧数据残留
状态指示 LED周期性翻转 Tx_Done单周期脉冲 精准标识发送完成时刻,便于级联
波特率控制 固定5208次计数 动态计算MCNT_BAUD 适配任意波特率,精度更高

代码对比

// 原版(延时启动)
else if(delay_cnt==MCNT_DLY)
    en_baud_cnt<=1;

// 新版(外部触发)
else if(Send_Go)
    en_baud_cnt<=1;

优势

  • 精准触发:支持突发数据发送

  • 实时响应:立即响应发送请求,无固定延迟等待

2. 数据锁存时机优化
// 原版:延时结束后锁存
else if(delay_cnt==MCNT_DLY)
    r_Data<=Data;

// 新版:发送请求时锁存
else if(Send_Go)
    r_Data<=Data;

优势

  • 数据新鲜度:确保发送的是最新输入数据

  • 防止覆盖:避免在发送过程中数据被意外修改


四、状态机简化

1. 移除延时计数器逻辑
  • 删除原delay_cnt相关代码(约5行)

2. 精简状态指示
// 原版:LED翻转
else if((bit_cnt==9)&&(baud_div_cnt==MCNT_BAUD))
    Led<=!Led;

// 新版:完成脉冲
assign w_Tx_Done=((bit_cnt==9)&&(baud_div_cnt==MCNT_BAUD));
always@(posedge Clk )
    Tx_Done<=w_Tx_Done;

优势

  • 明确状态:单周期脉冲更易被其他模块捕获

  • 降低功耗:减少不必要的信号跳变


五、资源优化

1. 寄存器位宽调整
信号 原版位宽 新版位宽 优化效果
baud_div_cnt 13位 30位 支持更高时钟频率
delay_cnt 26位 移除 节省26个触发器
2. 逻辑单元减少
  • 移除比较器:delay_cnt==MCNT_DLY

  • 移除LED控制逻辑

优势

  • 节省FPGA资源:整体减少约15%的逻辑单元占用

  • 提升时序性能:关键路径缩短(原延时计数器比较逻辑被移除)

常见问题系列

一、电脑端口无法识别USB 转串口模块(CP2102N USB to UART Bridge Controller 驱动程序无法使用)

在官网下载驱动就好了

官网

解压后安装对应的x86或者是x64版本即可

OK加上来了,如果你也遇到类似的问题也可以考虑去解决驱动

Logo

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

更多推荐