4、SPI接收数据通路设计:接收FIFO设计、数据采样逻辑设计、接收状态机设计、双缓冲机制设计

好,咱们今天聊聊SPI接收通路。说实话,这部分是SPI控制器里最容易出bug的地方。我当年第一次做SPI控制器时,就在接收数据采样上栽过跟头——数据老是少采一个bit,查了两天才发现是采样时钟沿搞反了。嗯,咱们今天就把这块彻底讲透。

4.1 接收FIFO设计

接收FIFO,说白了就是个数据暂存池。SPI接收数据是一bit一bit进来的,但CPU读取数据是一次读一个字节或一个字。这中间的速度不匹配,就得靠FIFO来缓冲。

我个人习惯用同步FIFO,因为SPI时钟域和系统时钟域通常需要做跨时钟域处理。但这里有个坑——如果你直接把SPI时钟域的接收数据写到系统时钟域的FIFO里,那就要做异步FIFO。我建议的做法是:

  • 深度选择:一般选8或16深度就够了。SPI速率通常不高,太深浪费资源
  • 宽度匹配:接收FIFO宽度等于数据位宽。SPI常用8位、16位或32位
  • 空满标志:必须要有空标志和满标志,这是控制逻辑的基础

核心要点:接收FIFO的写使能由SPI接收状态机控制,读使能由CPU或DMA控制。两者互不干扰,但要注意空满状态的处理。

我曾经遇到过一个案例:FIFO深度选了4,结果SPI连续传输时,CPU来不及读,FIFO就溢出了。后来改成8深度,问题解决。所以深度选择要结合SPI速率和CPU响应速度来定。

4.2 数据采样逻辑设计

数据采样是SPI接收的核心。SPI有四种模式(CPOL和CPHA的组合),采样边沿各不相同。咱们得设计一个通用的采样逻辑。

先看一个典型的采样电路结构:

// SPI数据采样逻辑
always @(posedge sclk or negedge rst_n) begin
    if (!rst_n) begin
        rx_shift_reg <= 'd0;
        rx_bit_cnt   <= 'd0;
    end else if (sample_en) begin
        rx_shift_reg <= {rx_shift_reg[WIDTH-2:0], sdi};
        rx_bit_cnt   <= rx_bit_cnt + 1'b1;
    end
end

这里有个关键点:sample_en信号怎么产生?它取决于SPI模式:

SPI模式 CPOL CPHA 采样边沿
模式0 0 0 SCLK上升沿
模式1 0 1 SCLK下降沿
模式2 1 0 SCLK下降沿
模式3 1 1 SCLK上升沿

你想想看,如果采样边沿选错了,数据就会偏移半个周期。我调试时遇到过这种情况——波形上看数据是对的,但读出来的值就是不对。后来发现是CPHA配置反了。

小技巧:采样逻辑最好用双沿采样。先用SCLK的上升沿和下降沿分别产生两个采样使能,再根据模式选择其中一个。这样代码复用性高。

4.3 接收状态机设计

接收状态机控制整个接收过程。我习惯用三段式状态机,清晰又好维护。状态机通常包含以下几个状态:

  • IDLE:空闲状态,等待片选信号有效
  • RECV_DATA:接收数据状态,逐bit采样
  • RECV_DONE:接收完成状态,将数据写入FIFO

状态转移图如下:

IDLE RECV_DATA RECV_DONE cs_n有效 bit_cnt == WIDTH 写入FIFO完成

状态机代码示例:

// 三段式状态机 - 接收部分
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        case (state)
            IDLE:       if (!cs_n) state <= RECV_DATA;
            RECV_DATA:  if (rx_bit_cnt == WIDTH) state <= RECV_DONE;
            RECV_DONE:  state <= IDLE;
            default:    state <= IDLE;
        endcase
end

注意:RECV_DONE状态只持续一个时钟周期。在这个周期内,要把移位寄存器里的数据写入FIFO,同时产生接收完成中断信号。如果漏掉这个周期,数据就丢了。

4.4 双缓冲机制设计

双缓冲机制,说白了就是两个寄存器交替工作。一个寄存器在接收数据,另一个寄存器给FIFO提供数据。这样能提高吞吐率。

为什么需要双缓冲?因为SPI接收是串行的,但FIFO写入是并行的。如果没有双缓冲,你必须在接收完一个字节后才能写入FIFO,这段时间SPI总线是空闲的。双缓冲可以做到「边接收边写入」。

我设计的双缓冲结构如下:

// 双缓冲寄存器
reg [WIDTH-1:0] rx_buf0, rx_buf1;
reg buf_sel;  // 0: 使用buf0, 1: 使用buf1

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_buf0 <= 'd0;
        rx_buf1 <= 'd0;
        buf_sel <= 1'b0;
    end else if (sample_en) begin
        if (!buf_sel)
            rx_buf0 <= {rx_buf0[WIDTH-2:0], sdi};
        else
            rx_buf1 <= {rx_buf1[WIDTH-2:0], sdi};
    end else if (rx_done) begin
        buf_sel <= ~buf_sel;  // 切换缓冲区
    end
end

这里有个设计细节:当rx_done信号到来时,当前正在使用的缓冲区已经存满了数据。我们立即切换缓冲区,让另一个缓冲区开始接收下一帧数据。同时,已满的缓冲区数据可以写入FIFO。

双缓冲的优势:

  • 接收不中断:SPI可以连续接收数据
  • 写入更灵活:FIFO写入可以在任意时刻进行,只要缓冲区数据有效
  • 降低FIFO深度要求:因为数据可以暂存在缓冲区里

我记得有一次做高速SPI设计,SPI时钟跑到50MHz。如果没有双缓冲,每接收一个字节就要停一个周期写FIFO,效率直接打八折。加上双缓冲后,吞吐率几乎翻倍。

嗯,接收通路的设计就这些。核心就是四个字:稳、准、快、省。采样要准,FIFO要稳,状态机要快,双缓冲要省资源。把这些搞明白了,SPI接收这块基本就稳了。

避坑指南:我曾经在双缓冲切换时忘记处理最后一个bit,导致数据错位。解决办法是在切换前确保当前bit已经采样完成,可以用一个延迟拍来处理。

可可精品资料库