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
状态转移图如下:
状态机代码示例:
// 三段式状态机 - 接收部分
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已经采样完成,可以用一个延迟拍来处理。
可可精品资料库