3、SPI发送数据通路设计
好,咱们今天聊聊SPI发送数据通路。这块儿说白了,就是数据怎么从你写进去,到最终从MOSI脚上飞出去的全过程。我当年第一次做SPI控制器时,觉得这玩意儿简单,结果调试时发现数据老是对不上,后来才发现是发送FIFO和移位寄存器之间的握手出了问题。嗯,咱们一步步拆开看。
3.1 发送FIFO设计
发送FIFO的作用,说白了就是个缓冲池。CPU或者DMA把数据扔进来,然后发送逻辑按自己的节奏取走。为什么要加这个?你想想看,如果CPU每次发一个字节都要等移位寄存器空,那效率得多低。我习惯用同步FIFO,深度一般取8或16,具体看系统需求。
FIFO的核心信号就这几个:
- wr_en:写使能,CPU写数据时拉高
- rd_en:读使能,发送状态机取数据时拉高
- full:满标志,满了就不能再写
- empty:空标志,空了就不能再读
- data_in:写入的数据
- data_out:读出的数据
这里有个坑,我曾经遇到过:FIFO的满标志判断一定要用格雷码跨时钟域处理,否则在高速场景下容易误判。嗯,这个后面跨时钟域章节会细讲,先记住有这么回事。
设计要点:
- FIFO深度建议取2的幂次,方便地址指针回绕
- 空/满标志用组合逻辑产生,但要注意时序
- 读使能信号必须与时钟同步,避免亚稳态
3.2 移位寄存器设计
移位寄存器是发送通路的核心。它从FIFO取出并行数据,然后按位串行移出去。SPI有四种模式,移位时机取决于时钟极性和相位。我个人习惯把移位寄存器设计成可配置的,这样一套代码能兼容所有模式。
基本结构是这样的:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shift_reg <= 'd0;
bit_cnt <= 'd0;
end else if (shift_en) begin
// 根据CPOL和CPHA决定移位时机
if (cpha == 1'b0) begin
// 在时钟第一个边沿移位
shift_reg <= {shift_reg[WIDTH-2:0], 1'b0};
end else begin
// 在时钟第二个边沿移位
shift_reg <= {shift_reg[WIDTH-2:0], 1'b0};
end
bit_cnt <= bit_cnt + 1'b1;
end else if (load_en) begin
// 从FIFO加载新数据
shift_reg <= fifo_data;
bit_cnt <= 'd0;
end
end
注意看,这里有个load_en信号,它由发送状态机产生。当移位寄存器发完一个字节后,状态机就会拉高load_en,从FIFO取新数据。我曾经犯过一个错误:忘了在加载数据时清零bit_cnt,结果下一个字节的位数对不上,调试了一整天才发现。
小技巧:移位寄存器的宽度建议参数化,比如用parameter DATA_WIDTH = 8。这样以后想改成16位数据宽度,改一个参数就行,不用到处改代码。
3.3 波特率发生器设计
波特率发生器,说白了就是分频器。系统时钟太高,SPI从机跟不上,所以得降频。我一般用计数器实现,分频系数可配。
设计思路:
- 输入系统时钟
clk,输出SPI时钟sclk - 通过配置寄存器
baud_div控制分频比 - 计数器从0计到
baud_div-1,然后翻转sclk
举个例子,系统时钟50MHz,想要1MHz的SPI时钟,分频系数就是25。注意,这里分频系数是半周期计数,所以实际sclk频率 = clk / (2 * baud_div)。
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 'd0;
sclk <= 1'b0;
end else if (cnt == baud_div - 1) begin
cnt <= 'd0;
sclk <= ~sclk;
end else begin
cnt <= cnt + 1'b1;
end
end
嗯,这里要注意:分频系数不能为0,否则计数器会出问题。我一般在配置寄存器里加个保护逻辑,如果写0就默认用2分频。
3.4 发送状态机设计
发送状态机是整个发送通路的指挥官。它协调FIFO、移位寄存器和波特率发生器的工作。我习惯用三段式状态机,清晰又好维护。
状态定义:
| 状态 | 说明 |
|---|---|
| IDLE | 空闲状态,等待发送触发 |
| LOAD | 从FIFO加载数据到移位寄存器 |
| SHIFT | 移位发送,每个时钟边沿移出一位 |
| WAIT | 等待FIFO非空,用于连续发送 |
状态转移逻辑:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
end else begin
case (state)
IDLE: begin
if (tx_start) state <= LOAD;
end
LOAD: begin
state <= SHIFT;
end
SHIFT: begin
if (bit_cnt == DATA_WIDTH-1) begin
if (!fifo_empty) state <= LOAD; // 连续发送
else state <= IDLE;
end
end
default: state <= IDLE;
endcase
end
end
这里有个设计细节:在SHIFT状态结束时,如果FIFO还有数据,就直接进入LOAD状态继续发,不用回到IDLE。这样可以实现连续发送,提高吞吐量。我最初的设计是每发完一个字节就回IDLE,结果发现效率低了一半,后来才改成现在这样。
注意:状态机输出信号要用组合逻辑或寄存器打一拍,避免产生毛刺。特别是cs_n(片选)信号,必须在发送开始前拉低,结束后拉高,中间不能抖动。
整体数据流
说了这么多,咱们用一张图把整个发送通路串起来:
从图上可以看得很清楚:发送状态机是大脑,它从FIFO读数据,控制移位寄存器加载和移位,同时波特率发生器提供时钟节拍。数据从FIFO到移位寄存器,再一位一位从MOSI脚送出去。整个流程环环相扣,任何一个环节出问题,数据就发不出去。
好了,发送数据通路就讲这么多。记住一句话:FIFO管缓冲,移位寄存器管串行化,波特率发生器管节奏,状态机管协调。把这四个模块搞明白,SPI发送这块儿就稳了。
避坑总结:
- FIFO满空标志一定要可靠,否则数据会丢或重复
- 移位寄存器加载时要同步清零bit_cnt
- 波特率分频系数不能为0
- 状态机输出要防毛刺,特别是片选信号
可可精品资料库