发送通路设计:从FIFO到状态机的完整链路

大家好,我是你们的SPI设计课讲师。今天我们来聊聊发送通路——说白了,就是数据怎么从你的逻辑核心,一路送到SPI的MOSI引脚上。这条路径上有四个关键模块:发送FIFO、移位寄存器、波特率发生器,还有那个控制一切的发送状态机。

我个人习惯把发送通路比作一条流水线。FIFO是仓库,移位寄存器是包装工,波特率发生器是节拍器,状态机就是那个喊口号的工头。嗯,咱们一个一个来看。

核心链路: 软件写数据 → FIFO缓存 → 状态机调度 → 移位寄存器串行化 → 波特率控制输出 → MOSI引脚

1. 发送FIFO设计:别让CPU等你

FIFO的作用说白了就是解耦。CPU写数据的速度和SPI发送的速度往往不匹配。你想想看,CPU可能一次突发写8个字节,但SPI要一个一个bit往外送。没有FIFO,CPU就得干等着。

设计要点:

  • 深度选择: 我一般选8或16深度。太浅了容易溢出,太深了浪费面积。我在一个项目中遇到过深度选4的情况,结果CPU一次中断处理16字节,FIFO直接爆了。
  • 宽度: 跟SPI数据宽度一致,通常是8位。但有些控制器支持可配置宽度(4/8/16位),这时候FIFO宽度要取最大值。
  • 空/满标志: 这是必须的。空标志给状态机用,满标志给中断控制器用。

我的经验: 异步FIFO的格雷码指针同步,跨时钟域处理一定要小心。我曾经因为格雷码没处理好,导致空标志误判,数据还没准备好状态机就开始移位了——结果发出去的全是垃圾。

// 发送FIFO接口示例
module tx_fifo (
    input  wire        clk,
    input  wire        rst_n,
    // 写侧(CPU/总线侧)
    input  wire        wr_en,
    input  wire [7:0]  wr_data,
    output wire        full,
    // 读侧(状态机侧)
    input  wire        rd_en,
    output wire [7:0]  rd_data,
    output wire        empty
);
    // 这里用同步FIFO实现,深度8
    reg [7:0] mem [0:7];
    reg [2:0] wr_ptr, rd_ptr;
    reg [3:0] count;  // 计数法判断空满
    
    assign full  = (count == 8);
    assign empty = (count == 0);
    
    // ... 读写指针更新逻辑
endmodule

2. 移位寄存器实现:串行化的艺术

移位寄存器,说白了就是把并行数据变成串行bit流。SPI协议里,数据是在时钟边沿逐位移出的。

实现方式:

  • 左移还是右移? 取决于MSB first还是LSB first。SPI标准是MSB first,但有些器件支持LSB first。我建议设计成可配置的。
  • 移位方向: 用case语句或者条件赋值。我个人喜欢用移位操作符 << 或 >>,代码更简洁。
  • 加载时机: 状态机在LOAD状态把FIFO数据加载到移位寄存器。

注意: 移位寄存器的位宽必须和FIFO数据位宽一致。如果SPI支持4/8/16位可配置,移位寄存器要设计成最大位宽,然后用计数器控制实际移位的位数。

// 移位寄存器核心逻辑
reg [7:0] shift_reg;
reg [2:0] bit_cnt;  // 位计数器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        shift_reg <= 8'h00;
        bit_cnt   <= 3'd0;
    end else if (load_en) begin
        shift_reg <= fifo_data;  // 从FIFO加载
        bit_cnt   <= 3'd7;       // 8位数据,从高位开始
    end else if (shift_en) begin
        shift_reg <= {shift_reg[6:0], 1'b0};  // 左移,MSB先出
        bit_cnt   <= bit_cnt - 1'b1;
    end
end

assign mosi = shift_reg[7];  // 最高位输出到MOSI

3. 波特率发生器:节奏的把控者

波特率发生器,说白了就是一个分频器。系统时钟比如50MHz,SPI时钟可能只需要1MHz,那就分频50倍。

设计考量:

  • 分频系数: 通常用寄存器配置,比如baud_div[15:0]。分频值 = 系统时钟 / (2 * 目标SPI时钟)。
  • 占空比: SPI时钟要求50%占空比吗?不一定。CPOL=0时,时钟空闲为低,第一个边沿是上升沿。CPOL=1时相反。但占空比50%是最通用的。
  • 使能控制: 状态机控制波特率发生器的启动和停止。发送期间才产生时钟,空闲时时钟保持。

避坑指南: 我曾经在波特率发生器里用了组合逻辑分频,结果时钟毛刺一大堆。后来老老实实用计数器+触发器分频,问题解决。记住:SPI时钟一定要用寄存器输出,别用组合逻辑。

// 波特率发生器
reg [15:0] clk_cnt;
reg        sclk;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        clk_cnt <= 16'd0;
        sclk    <= 1'b0;
    end else if (tx_active) begin  // 状态机控制使能
        if (clk_cnt == baud_div) begin
            clk_cnt <= 16'd0;
            sclk    <= ~sclk;  // 翻转产生时钟
        end else begin
            clk_cnt <= clk_cnt + 1'b1;
        end
    end else begin
        clk_cnt <= 16'd0;
        sclk    <= (cpol) ? 1'b1 : 1'b0;  // 空闲电平
    end
end

4. 发送状态机:IDLE → LOAD → SHIFT → DONE

这是发送通路的大脑。四个状态,循环往复。

发送状态机状态转移图 IDLE 等待FIFO非空 LOAD 加载数据到移位寄存器 SHIFT 逐位移出,bit_cnt递减 DONE 发送完成,产生中断 FIFO非空 加载完成 bit_cnt==0 发送完成,返回IDLE 每个sclk边沿

状态机行为描述:

状态 行为 转移条件
IDLE 等待FIFO非空,sclk保持空闲电平 FIFO非空 → LOAD
LOAD 从FIFO读取数据,加载到移位寄存器,复位bit_cnt 加载完成(1个时钟)→ SHIFT
SHIFT 每个sclk边沿移出一位,bit_cnt递减 bit_cnt==0 → DONE
DONE 产生发送完成标志(可触发中断),等待确认 确认后 → IDLE
// 发送状态机实现
localparam IDLE  = 2'b00;
localparam LOAD  = 2'b01;
localparam SHIFT = 2'b10;
localparam DONE  = 2'b11;

reg [1:0] state, next_state;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) state <= IDLE;
    else       state <= next_state;
end

always @(*) begin
    next_state = state;
    case (state)
        IDLE:  if (!fifo_empty) next_state = LOAD;
        LOAD:  next_state = SHIFT;  // 加载只需1拍
        SHIFT: if (bit_cnt == 0) next_state = DONE;
        DONE:  next_state = IDLE;   // 确认后返回
    endcase
end

// 输出控制
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        load_en   <= 1'b0;
        shift_en  <= 1'b0;
        tx_done   <= 1'b0;
        fifo_rd   <= 1'b0;
    end else begin
        load_en  <= (state == LOAD);
        shift_en <= (state == SHIFT) && sclk_posedge;  // 时钟上升沿移位
        tx_done  <= (state == DONE);
        fifo_rd  <= (state == LOAD);  // 在LOAD状态读取FIFO
    end
end

关键点总结:

  • FIFO深度选8或16,别太小也别太大
  • 移位寄存器支持MSB/LSB可配置,用参数化设计
  • 波特率发生器用计数器分频,输出用寄存器
  • 状态机四个状态,注意DONE状态要产生中断信号

我的建议: 写RTL的时候,先把状态机画出来(就像上面的SVG图),再写代码。这样思路清晰,不容易漏状态。我在项目中吃过亏,一开始直接写代码,结果状态转移条件漏了一个,仿真跑了两天才发现。

好了,发送通路的四个模块就讲到这里。记住:FIFO是缓冲,移位寄存器是串行化的核心,波特率发生器控制节奏,状态机调度一切。把这四个模块拼起来,你的SPI发送通路就基本成型了。

可可精品资料库