4. SPI发送路径设计:发送FIFO设计、移位寄存器实现、数据串行化过程、MSB/LSB优先配置
好,咱们今天聊聊SPI发送路径。这部分说白了,就是数据怎么从你的核心逻辑,一步一步变成串行比特流,从MOSI管脚飞出去。我做了这么多年SPI控制器,发送路径其实是整个模块里最容易出bug的地方,尤其是时序和位序问题。
4.1 发送FIFO设计:为什么要加这个缓冲区?
你想想看,CPU或者DMA往SPI控制器里丢数据,速度是不固定的。而SPI的SCK时钟,频率可能很低,也可能很高。如果没有一个缓冲区,CPU就得掐着点等SPI发完一个字节才能写下一个,效率太低了。
发送FIFO的作用,就是解耦这两个时钟域或速率域。CPU只管往FIFO里写,SPI移位寄存器只管从FIFO里读,各干各的。
- 深度:我一般用8或16。太深浪费面积,太浅容易溢出。如果系统DMA响应快,8深度足够。
- 宽度:跟数据寄存器宽度一致,通常是8位。但有些控制器支持16位或32位,那就按最大位宽来。
- 空/满标志:必须要有。空标志告诉移位寄存器“没数据了,别拉了”;满标志告诉CPU“你慢点写”。
我个人习惯用双端口RAM + 读写指针来实现FIFO。格雷码跨时钟域处理?嗯,如果读写时钟不同源,确实需要。但很多场景下SPI控制器和系统总线是同步的,直接用二进制指针加同步器就行。
4.2 移位寄存器实现:数据串行化的核心
移位寄存器,说白了就是一个可以左右移位的寄存器。SPI发送时,每个SCK时钟沿,把寄存器里的数据推出去一位。
我常用的结构是这样的:
// 发送移位寄存器,支持MSB/LSB优先
reg [DATA_WIDTH-1:0] shift_reg;
reg [CNT_WIDTH-1:0] bit_cnt; // 位计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shift_reg <= 'b0;
bit_cnt <= 'b0;
end else if (tx_start) begin
// 从FIFO读取数据,加载到移位寄存器
shift_reg <= fifo_rdata;
bit_cnt <= DATA_WIDTH - 1;
end else if (tx_shift_en) begin
// 每个SCK时钟沿,移出一位
if (msb_first) begin
shift_reg <= {shift_reg[DATA_WIDTH-2:0], 1'b0}; // MSB优先
end else begin
shift_reg <= {1'b0, shift_reg[DATA_WIDTH-1:1]}; // LSB优先
end
bit_cnt <= bit_cnt - 1;
end
end
// MOSI输出:取最高位或最低位
assign mosi = msb_first ? shift_reg[DATA_WIDTH-1] : shift_reg[0];
这里有个细节:tx_shift_en信号什么时候产生?它应该跟SCK的边沿对齐。如果是CPOL=0, CPHA=0模式,在SCK上升沿采样,那就在SCK下降沿移位。这样数据在SCK上升沿时已经稳定了。
4.3 数据串行化过程:从并行到串行的完整流程
咱们把整个过程串起来看看:
- FIFO非空:移位寄存器空闲时,从FIFO读取一个字节。
- 加载数据:在SCK空闲期间,把数据打入移位寄存器。
- 逐位移出:每个SCK时钟沿,移出一位到MOSI。
- 传输完成:位计数器归零,产生tx_done信号,通知FIFO可以读下一个数据。
这个过程,我用一张图来展示:
这张图把整个发送路径串起来了。你注意看,控制逻辑是大脑,它决定什么时候加载、什么时候移位、什么时候通知FIFO。
4.4 MSB/LSB优先配置:一个寄存器搞定
为什么需要这个配置?因为不同的从设备,数据格式不一样。有些传感器先发高位,有些LCD屏先发低位。如果控制器不支持配置,你就得在软件里手动翻转字节,麻烦不说,还容易出错。
实现方式其实很简单:
- MSB优先:移位方向向左,每次取最高位输出。比如数据0xA5 (1010_0101),先发1,再发0,以此类推。
- LSB优先:移位方向向右,每次取最低位输出。同样0xA5,先发1,再发0,再发1... 嗯,顺序完全反过来了。
代码里,我通常用一个寄存器msb_first来控制:
// 位序选择
assign mosi = msb_first ? shift_reg[DATA_WIDTH-1] : shift_reg[0];
// 移位方向
always @(posedge clk) begin
if (tx_shift_en) begin
if (msb_first)
shift_reg <= {shift_reg[DATA_WIDTH-2:0], 1'b0}; // 左移
else
shift_reg <= {1'b0, shift_reg[DATA_WIDTH-1:1]}; // 右移
end
end
4.5 避坑总结
最后,我把自己踩过的坑列一下,你写代码时注意:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| FIFO深度不足 | 数据丢失,通信错乱 | 根据最大突发长度计算深度,加“几乎满”标志 |
| 移位寄存器加载与移位冲突 | 第一个bit错误 | 加载和移位错开一个SCK周期 |
| MSB/LSB配置没同步到移位方向 | 数据位序颠倒 | 确保配置同时影响输出选择和移位方向 |
| 位计数器溢出 | 多移或少移一位 | 计数器到0后立即停止移位,等待下次加载 |
嗯,发送路径就这些内容。说白了,FIFO是缓冲,移位寄存器是核心,MSB/LSB配置是灵活性。把这三点搞明白,发送路径基本不会出大问题。