很多人觉得SPI比I2C简单——没有地址、没有仲裁、没有时钟拉伸,四根线随便接。确实,SPI的协议本身比I2C简单,但在FPGA里实现SPI时,有些坑反而比I2C更隐蔽,因为SPI时钟更快、对时序更敏感,而且一个不起眼的配置错误就能让数据全错。
坑一:CPOL/CPHA搞错——数据全部错位,看起来像乱码
问题现象:
SPI通信"能跑",但接收到的数据全是错的。用示波器看波形,SCK、MOSI、MISO、CS信号都在,时序看着也正常,但读回来的寄存器值就是不对。
根本原因:
SPI有4种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定。这两个参数决定了数据在SCK的哪个边沿被采样。
| 模式 | CPOL | CPHA | 空闲SCK | 采样边沿 | 典型设备 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 | SD卡、大部分ADC |
| 1 | 0 | 1 | 低 | 下降沿 | 大部分传感器 |
| 2 | 1 | 0 | 高 | 下降沿 | — |
| 3 | 1 | 1 | 高 | 上升沿 | 部分DAC、OLED |
快速记忆法:看从设备datasheet的时序图——数据在时钟沿之前稳定,则CPHA=0;数据在时钟沿之后变化,则CPHA=1。CPOL看SCK空闲电平(高=1,低=0)。
关键点:主从设备的CPOL/CPHA必须一致。一个在上升沿发送、一个在下降沿采样,数据必然错位。
错误做法:
verilog
// 错误:硬编码Mode 0,不看从设备的datasheet
always @(posedge clk) begin
if (sck_edge) begin
rx_data[bit_idx] <= miso;
bit_idx <= bit_idx + 1;
end
end
正确做法:
verilog
parameter CPOL = 0, CPHA = 0;
// SCK输出
assign SCK = (CPOL == 1) ? ~sck_reg : sck_reg;
// 采样边沿与移位边沿(generate块,避免语法错误)
wire sample_edge, shift_edge;
generate
if (CPHA == 0) begin
assign sample_edge = (CPOL == 0) ? sck_reg : ~sck_reg; // 第一个边沿
assign shift_edge = ~sample_edge;
end else begin
assign shift_edge = (CPOL == 0) ? sck_reg : ~sck_reg; // 第一个边沿
assign sample_edge = ~shift_edge;
end
endgenerate
// 采样(电平判断,实际工程建议用边沿检测)
always @(posedge clk) begin
if (cs_active && sample_edge)
rx_data[bit_idx] <= miso;
end
自检方法:
打开从设备datasheet,搜索"SPI timing"或"SPI mode"
确认CPOL和CPHA的值
示波器抓MOSI和SCK,确认数据在正确的边沿稳定
坑二:CS拉高时机太早——最后一个字节丢失或损坏
问题现象:
SPI多字节传输时,前N-1个字节都正确,最后一个字节总是错。有时候读到0x00,有时候差一个bit。
根本原因:
CS不仅仅是“选中”信号,还定义了一次传输的边界。很多从设备在CS拉高时才锁存最后一位数据。如果在最后一个bit还没传输完就拉高CS,从设备会认为传输提前结束。
错误做法:
verilog
// 错误:最后一个bit采样完立即拉高CS
if (sample_edge && bit_cnt == 7)
cs_reg <= 1'b1; // 太早了
正确做法:
verilog
// 最后一个bit采样完成后,等待半个SCK周期再拉高CS
if (sample_edge && bit_cnt == 7) begin
state <= POST_WAIT;
post_cnt <= 0;
end
if (state == POST_WAIT) begin
post_cnt <= post_cnt + 1;
if (post_cnt == SCK_HALF_PERIOD) begin
cs_reg <= 1'b1;
state <= IDLE;
end
end
关键点:
最后一个bit采样完成后,至少等待半个SCK周期再拉高CS
保守一点不会出错
坑三:SPI时钟过快+信号完整性差——高频下概率性丢数据
问题现象:
SPI通信在低速(1MHz)时完全正常,提高到目标速率(20~50MHz)后开始概率性丢数据或读错。错误不固定,用逻辑分析仪不一定能抓到(采样率不足或缓存深度不够)。
根本原因:
当速率超过10MHz后,信号反射、时钟抖动、建立/保持时间不足等问题开始显现。
错误做法:
verilog
// 错误:计数器分频产生SCK,无约束
assign SCK = sck_reg;
正确做法:
verilog
// 使用ODDR输出SCK(提高边沿质量)
ODDR #(.DDR_CLK_EDGE("SAME"), .INIT(1'b0)) sck_oddr (
.Q(SCK), .C(clk_2x), // clk_2x需用PLL产生,频率为SCK的2倍
.D1(1'b0), .D2(1'b1)
);
// 添加时序约束(XDC)
set_max_delay -from [get_pins sck_reg/C] -to [get_ports SCK] 2.0
硬件改进:
SCK走线加串联电阻(22~33Ω)抑制振铃
高速SPI(>20MHz)建议用ODDR输出SCK
坑四:多从设备CS管理混乱——总线冲突和数据串台
问题现象:
系统有多个SPI从设备(如ADC + Flash + DAC),单独测试每个都正常,同时使用时偶尔出现数据串台。
根本原因:
CS管理不当会导致:
两个CS同时拉低→ 两个从设备同时驱动MISO → 总线冲突
CS切换间隔太短→ 前一个从设备没释放MISO,后一个就开始驱动
速率差异大的从设备共用SCK→ 慢设备拖累快设备,或信号质量恶化
错误做法:
verilog
// 错误:直接切换CS,无间隔
assign cs_adc = (sel == 0) ? 1'b0 : 1'b1;
assign cs_flash = (sel == 1) ? 1'b0 : 1'b1;
正确做法:
verilog
// CS切换必须经过全高中间态
localparam DESELECT_WAIT = 4; // 等待周期数(查从设备datasheet)
always @(posedge clk) begin
case (cs_state)
CS_ACTIVE: if (transfer_done) cs_state <= CS_DESELECT;
CS_DESELECT: begin
if (deselect_cnt >= DESELECT_WAIT)
cs_state <= CS_IDLE;
else
deselect_cnt <= deselect_cnt + 1;
end
endcase
end
// 输出CS:任何时候最多只有一个为低
assign cs_adc = (cs_state == CS_ACTIVE && sel == 0) ? 1'b0 : 1'b1;
assign cs_flash = (cs_state == CS_ACTIVE && sel == 1) ? 1'b0 : 1'b1;
额外提示:如果从设备速率差异很大(如50MHz ADC和1MHz EEPROM),建议使用独立SCK或临时降频,避免共用。
调试自检清单
| 检查项 | 通过条件 |
|---|---|
| CPOL/CPHA匹配 | 主从设备一致,datasheet确认 |
| CS时序正确 | 最后一个bit采样后再拉高CS |
| 高速信号完整 | 无振铃、MOSI/SCK延迟在约束内 |
| 多从设备CS无重叠 | 切换时经过全高中间态 |
| SCK时钟源合理 | 使用PLL/ODDR而非计数器分频 |
| 示波器验证 | 全速率下波形干净、边沿陡峭 |
FAQ
Q:SPI最大速率能跑多少?
A:板上10cm以内可达50~80MHz,更长走线建议降到20MHz以下。
Q:CPOL/CPHA搞混了会怎样?
A:大概率全部错,因为每个bit采样点都偏了半个周期。
Q:为什么要用ODDR输出SCK?
A:计数器分频产生的SCK边沿抖动大,ODDR利用专用寄存器,输出边沿质量更好。
Q:多从设备能否共用SCK?
A:可以,但要求所有设备支持相同的SPI模式和相近的速率。
⚠️ 注意事项
CPOL/CPHA是SPI调试的第一排查项。90%的SPI通信问题都是模式配置错误。
CS不是开关,是协议的一部分。CS的setup/hold时间同样重要。
高速SPI必须用示波器。逻辑分析仪采样率(100~200MHz)在50MHz以上不够用。
先用低速跑通,再逐步提速,同时用示波器监控信号质量。
关注我,本期接口协议避坑,FPGA定制开发、项目调试、IP定制,开发服务可私。