Verilog实现多字节串口接收

该文章已生成可运行项目,
AI助手已提取文章相关产品:

串口接收多字节 Verilog程序

在FPGA开发中,我们经常需要与PC、MCU或其他设备通过UART进行通信。虽然单字节接收看似简单,但在实际项目中,大多数协议都要求一次性接收多个字节——比如一条包含命令、地址和校验的完整数据帧。如果只用基础的UART接收模块,主控逻辑就得频繁响应中断或轮询,不仅效率低,还容易因响应不及时导致后续数据被覆盖。

有没有一种方式,能让FPGA自动把一整帧数据收完再通知CPU?答案是肯定的: 设计一个多字节串口接收器 。它不仅能提升通信可靠性,还能显著降低系统负担。

本文将带你从零实现一个参数化、高鲁棒性的多字节UART接收模块。我们将深入探讨采样机制、状态机设计、缓冲管理以及抗干扰策略,并提供可综合的Verilog代码,适用于Xilinx、Intel等主流FPGA平台。


异步串行通信的本质:时间同步的艺术

UART之所以被称为“异步”,是因为它没有专用时钟线。发送端和接收端仅靠事先约定的波特率来对齐每一位数据。这意味着,接收端必须精准地知道每个比特的起始和结束位置。

以115200 bps为例,每位持续时间为约8.68 μs。为了准确判断电平值,通常采用 16倍过采样 ——即每比特采样16次,在中间时刻(第7~9次)取多数表决结果作为该位最终值。这种做法能有效抵抗毛刺和噪声。

假设你的FPGA主频为50 MHz,那么分频系数就是:

50_000_000 / (115200 × 16) ≈ 27

也就是说,每27个系统时钟周期产生一次采样脉冲。这个数值越接近整数,波特率误差就越小。一般建议控制在±3%以内,否则长期累积可能导致错位。

更关键的是,我们必须识别出 起始位 (下降沿)才能开始同步。一旦错过,整个字节都会错乱。因此,很多设计会在进入接收前先检测rx信号是否为空闲高电平,避免误触发。


多字节接收的核心挑战

比起单字节接收,多字节处理面临几个新问题:

  • 如何确定帧边界?
    是固定长度?还是遇到特定结束符(如0x0D 0x0A)才停止?
  • 收到的数据存在哪?
    如果直接输出,主控来不及读取就会丢失;需要缓存。
  • 何时通知外部逻辑?
    每收到一字节就中断?还是整帧收完再发完成信号?

最合理的方案是: 使用内部缓冲区 + 完成标志机制 。当指定数量的字节全部接收完毕后,拉高 frame_done 信号,由主控逻辑主动读取所有数据并清除标志。这样既保证了数据完整性,又减少了中断频率。

此外,还要考虑错误处理:
- 停止位不是高电平 → 帧错误
- 新数据到来时缓冲区未清空 → 溢出
- 连续接收超时 → 可设置超时计数器防止死锁

这些细节决定了模块在真实环境中的稳定性。


状态机驱动的设计思路

整个接收过程可以用一个简洁的状态机来描述:

typedef enum logic [2:0] {
    IDLE,
    START,
    DATA,
    STOP
} state_t;
  • IDLE :等待起始位下降沿
  • START :确认起始位有效,准备接收数据
  • DATA :逐位采样并移入移位寄存器
  • STOP :验证停止位是否为高

每一个状态都在 采样时钟的驱动下推进 。这里的关键是“何时切换状态”——我们不能依赖绝对时间,而是根据 位计数器 分频器归零信号 来判断是否该进入下一位。

例如,在 DATA 状态下,每当 next_bit 信号到来(表示已过完一个比特周期),就把当前采样值写入 shift_reg[bit_counter] ,然后递增 bit_counter 。当计数达到7(8位数据),说明本字节已完成,下一步应进入 STOP 状态。

这种基于事件而非延时的设计,更具可移植性和精度保障。


抗干扰采样:不只是简单的边沿检测

你可能见过一些简化的UART接收代码,只在每位中间采样一次。这在理想环境下没问题,但现实中电磁干扰无处不在——尤其是工业现场或长线传输。

我们的解决方案是: 16次连续采样 + 多数表决

reg [15:0] sample_reg;
...
sample_reg <= {sample_reg[0], rx_in};

每次分频计数结束时,将当前rx电平移入 sample_reg 的最高位。经过16次移位后,这个寄存器就记录了该比特周期内的全部采样序列。

然后我们统计其中“1”的个数:

function reg get_sampled_bit;
    input reg [SAMPLES_PER_BIT-1:0] reg_in;
    integer i, sum;
    begin
        sum = 0;
        for (i = 0; i < SAMPLES_PER_BIT; i = i + 1)
            sum = sum + reg_in[i];
        get_sampled_bit = (sum > (SAMPLES_PER_BIT >> 1));
    end
endfunction

只要超过半数为高,就认为该位是“1”。这种方法能有效过滤短暂的尖峰脉冲,即使有几个采样点出错也不影响最终判断。

当然,也可以进一步优化,比如跳过首尾几个采样点(避开跳变沿不稳定区域),只取中间9~12次做表决,效果更好。


缓冲区管理:静态数组还是FIFO?

对于固定长度帧(如4字节指令包),使用寄存器数组是最轻量的选择:

reg [7:0] data_buffer [0:FRAME_LEN-1];
reg [$clog2(FRAME_LEN)-1:0] buf_ptr;

每收到一个有效字节,就将其存入 data_buffer[buf_ptr] ,然后 buf_ptr++ 。当 buf_ptr == FRAME_LEN - 1 时,说明最后一字节已存入,此时拉高 frame_done

优点是资源消耗极小,适合小型FPGA;缺点是长度固定,不够灵活。

如果你希望支持可变长度帧(比如直到收到0xFF为止),那就得引入 超时机制 特殊字符检测 。例如:

if (shift_reg == 8'hFF) begin
    frame_done <= 1;
    // 不再递增buf_ptr,直接结束
end

或者配合一个自由运行的计数器,若超过一定时间未收到新字节,则强制结束当前帧。

至于异步FIFO,则更适合跨时钟域场景。比如UART使用外部24MHz晶振,而系统主频为100MHz。这时需要用双端口RAM+异步指针同步技术来安全传递数据。


完整Verilog实现(精简可综合版本)

module uart_multi_rx #(
    parameter DATA_WIDTH = 8,
    parameter FRAME_LEN  = 4,
    parameter CLK_FREQ   = 50_000_000,
    parameter BAUD_RATE  = 115200
) (
    input               clk,
    input               rst_n,
    input               rx_in,

    output [DATA_WIDTH-1:0] data_out,
    output [3:0]            byte_cnt,
    output                  frame_done,
    output                  data_valid,
    output                  overflow
);

localparam SAMPLES_PER_BIT = 16;
localparam CLK_DIV = CLK_FREQ / (BAUD_RATE * SAMPLES_PER_BIT);

// 信号声明
reg [SAMPLES_PER_BIT-1:0] sample_reg;
reg [CLK_DIV-1:0]         clk_divider;
reg [2:0]                 sample_counter;
reg [3:0]                 bit_counter;
reg                       start_flag;
reg [DATA_WIDTH-1:0]      shift_reg;
reg [DATA_WIDTH-1:0]      data_buffer [0:FRAME_LEN-1];
reg [$clog2(FRAME_LEN):0] buf_ptr;
reg                       load_data;

wire next_bit = (sample_counter == 0 && clk_divider == CLK_DIV - 1);
wire start_condition = (rx_in == 1'b0) && !start_flag;

// 分频器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        clk_divider <= 0;
    else if (start_flag)
        clk_divider <= (clk_divider == CLK_DIV - 1) ? 0 : clk_divider + 1;
    else
        clk_divider <= 0;
end

// 采样控制
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        sample_counter <= 0;
        sample_reg <= 0;
    end else if (start_flag) begin
        if (clk_divider == CLK_DIV - 1) begin
            sample_reg <= {sample_reg[0], rx_in};
            sample_counter <= (sample_counter == SAMPLES_PER_BIT - 1) ? 0 : sample_counter + 1;
        end
    end else begin
        sample_counter <= 0;
        sample_reg <= 0;
    end
end

// 多数表决函数
function reg majority_vote(input reg [SAMPLES_PER_BIT-1:0] sreg);
    integer ones;
    ones = 0;
    for (integer i = 0; i < SAMPLES_PER_BIT; i++)
        ones = ones + sreg[i];
    return (ones >= (SAMPLES_PER_BIT >> 1));
endfunction

// 状态机定义
typedef enum logic [1:0] { IDLE, START, DATA, STOP } state_t;
state_t state;

// 主状态机
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else begin
        case (state)
        IDLE:
            if (start_condition) state <= START;
        START:
            if (next_bit && bit_counter == DATA_WIDTH - 1) state <= DATA;
        DATA:
            if (next_bit && bit_counter == DATA_WIDTH - 1) state <= STOP;
        STOP:
            if (next_bit) state <= IDLE;
        endcase
    end
end

// 控制逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bit_counter <= 0;
        shift_reg <= 0;
        start_flag <= 0;
        load_data <= 0;
        buf_ptr <= 0;
        frame_done <= 0;
        overflow <= 0;
    end else begin
        load_data <= 0;

        case (state)
        IDLE:
            if (start_condition) begin
                bit_counter <= 0;
                start_flag <= 1;
                frame_done <= 0;
            end

        START:
            if (next_bit) begin
                if (bit_counter < DATA_WIDTH - 1)
                    bit_counter <= bit_counter + 1;
                else
                    bit_counter <= 0;
            end

        DATA:
            if (next_bit) begin
                shift_reg[bit_counter] <= majority_vote(sample_reg);
                if (bit_counter < DATA_WIDTH - 1) begin
                    bit_counter <= bit_counter + 1;
                end else begin
                    data_buffer[buf_ptr] <= shift_reg;
                    load_data <= 1;
                    if (buf_ptr == FRAME_LEN - 1) begin
                        frame_done <= 1;
                        buf_ptr <= 0;
                    end else begin
                        buf_ptr <= buf_ptr + 1;
                    end
                end
            end

        STOP:
            if (next_bit) begin
                if (!majority_vote(sample_reg))
                    overflow <= 1;  // 停止位错误
                start_flag <= 0;
            end
        endcase
    end
end

// 输出映射
assign data_out = data_buffer[byte_cnt];
assign byte_cnt = buf_ptr;
assign data_valid = load_data;

endmodule

实际应用中的工程技巧

1. 输入信号预处理

原始 rx_in 可能带有抖动或亚稳态风险。建议加两级D触发器同步:

reg rx_sync1, rx_sync2;
always @(posedge clk) begin
    rx_sync1 <= rx_in;
    rx_sync2 <= rx_sync1;
end

后续逻辑使用 rx_sync2 作为输入。

2. 参数化配置提升复用性

FRAME_LEN BAUD_RATE 设为参数后,同一份代码可用于不同项目。甚至可以通过添加配置寄存器,实现运行时动态调整帧长。

3. 调试建议

在关键节点插入ILA(Xilinx Integrated Logic Analyzer)探针,特别是:
- rx_in 和同步后的信号
- state 状态变量
- frame_done buf_ptr

这样可以在Vivado中实时观察波形,快速定位起始位误判、采样偏移等问题。

4. 避免常见陷阱

  • 分频系数非整数 :会导致相位漂移,建议选择能整除的主频/波特率组合
  • 未清空缓冲区就启动新接收 :应检查 frame_done 是否已被软件清除
  • 忽略停止位验证 :某些干扰可能让停止位变低,此时应视为帧错误

结语

一个多字节UART接收器看似只是个小功能模块,但它背后涉及了时序控制、抗干扰设计、状态机建模等多个数字系统核心概念。通过合理运用16倍过采样、多数表决、缓冲管理和完成标志机制,我们可以构建出一个稳定可靠的通信前端。

这类模块特别适合用于工业控制、传感器采集、配置加载等场景。未来还可在此基础上扩展:
- 支持奇偶校验
- 添加CRC校验单元
- 封装为AXI-Lite从设备,便于集成到MicroBlaze/Zynq系统中
- 结合DMA实现大批量数据零干预传输

掌握这种底层通信设计能力,意味着你不仅能“让东西工作”,更能“让它可靠地工作”。这才是FPGA工程师真正的价值所在。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

本文章已经生成可运行项目

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值