☰
SPI总线调试实战:从AD7606到QSPI升级的踩坑复盘
2026/10/8 5:14:36 网站建设 项目流程

入行第二年那会儿,我拿到一块八通道同步采样板,主控是GD32F303,ADC是安富莱板子上常见的AD7606,两者之间就靠SPI连接。当时我的心态和大多数人一样:四根线而已,调起来能有多难?结果一上电,读回来的数据没有一个通道是对的。我花了一整周把CPOL和CPHA的四种组合全试一遍,最后蹲下来用示波器去量MISO,发现那个引脚上连一个稳定的高电平都等不到——少了一颗上拉电阻。

那次经历之后,我把SPI/QSPI/OctoSPI/MICROWIRE这一族串行同步通信总线从头到尾理了一遍。这篇就当是当年的踩坑复盘,顺便把TF卡SPI电路、SPI DMA、软件SPI与硬件SPI的区别、QSPI在线升级这些真实项目里躲不开的问题一起讲清楚。适合正在调试SPI从设备、准备做Bootloader或者被片选信号折腾到头秃的嵌入式工程师。

1. 为什么一根总线能活三十年:从SPI到QSPI和OctoSPI

1.1 四根线打天下:SPI的基本盘

SPI全称Serial Peripheral Interface,上世纪八十年代由Motorola提出,初衷是解决板级芯片之间的低成本高速通信。它和UART最大的区别是:UART靠双方约定波特率,对时钟误差很敏感;SPI直接由主设备提供时钟,从设备跟着节拍走,不存在"两边速度不一样导致乱码"的问题。用生活里的例子来说,SPI像两个人在同一只节拍器下轮流念词:主设备控着节拍器,从设备只管在自己该张嘴的节拍上说话就行。

标准SPI有四根信号线:SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。CS低电平有效,主设备把CS拉低,表示"我现在点名的是你",从设备看到自己的CS被拉低就开始工作。同一根总线上可以挂多个从设备,一种方式是每个从设备各占一个CS引脚,另一种方式是菊花链共用一根CS。刚入门的老实选第一种,菊花链的时序延迟问题能把人绕晕。

1.2 CPOL/CPHA四模式:为什么两个芯片总是对不上话

SPI没有像I2C那样的握手应答,全靠主设备约定什么时刻采样。这个约定拆成两个参数:CPOL决定SCLK空闲时的电平,CPHA决定数据在第一个还是第二个跳变沿被采样。两者组合成Mode 0到Mode 3:

模式CPOLCPHASCLK空闲电平数据采样沿
Mode 000低上升沿
Mode 101低下降沿
Mode 210高下降沿
Mode 311高上升沿

我调试AD7606时犯的第二个错就在这里:MCU默认配成Mode 0,但从设备手册时序图里明确写着数据在SCLK下降沿锁存,这其实是Mode 1。你光看引脚都通了、CS也拉了,数据就是不对,因为你采样沿选错了。判断方法只有一个:打开器件手册的串行时序图,看它在哪个边沿把数据送出、在哪个边沿要求接收方采样,而不是凭感觉猜。

1.3 QSPI/OctoSPI:把串行做成"小并行"

标准SPI只有一根数据输出线和一根数据输入线,带宽上限摆在那里。于是QSPI把数据线扩成四根:IO0到IO3,读写时四根线同时传数据,吞吐率直接翻四倍。比如一颗主流QSPI Nor Flash跑133MHz,四线读模式下可以到532Mbps左右,这个速度足够支撑不少MCU的XIP片上执行。

OctoSPI再往上翻一倍,八根数据线,主要用在高端Nor Flash、超高速RAM以及大屏显示接口。注意这类总线不是简单地把SPI引脚加多,它额外引入了命令阶段、地址阶段、空周期以及数据阶段的概念。典型地,QSPI读操作要先发命令字节,地址阶段只有部分数据线参与,之后再切回全部数据线读数据,中间还可能有dummy周期,这些细节在后面的FPGA升级章节再展开。

2. TF卡SPI电路设计清单:最容易翻车的电气细节

2.1 上拉电阻:MOSI/MISO/CS各自的归宿

SD卡在SPI模式下有四根线:SCLK、DI(对应MOSI)、DO(对应MISO)、CS。很多开发板把这四根线直接拉到MCU就能用,省掉上拉电阻,短距离、低速下问题不大,但一旦线长了、干扰大了,莫名其妙读错扇区、初始化失败就都来了。

先说MISO,也就是卡的DO引脚。SD卡在SPI模式下这个引脚往往是开漏输出,不能自己输出可靠的高电平,必须靠外部上拉电阻把电平拉起来。我那次AD7606乱码,本质就是同一类问题:MISO外设输出能力不足,线上又没有上拉,导致边沿被寄生电容拖慢,高速采样时直接读错。上拉阻值范围通常在10k到47k,我习惯用10k,再并联一个100nF的去耦电容到地。

CS也必须上拉。如果CS悬空,卡的片选信号会在高低电平之间抖,卡可能把垃圾命令当成有效命令处理,甚至退出SPI模式。DI和SCLK通常不用上拉,主设备推挽驱动足够,反倒建议各串一颗33Ω左右的小电阻,用于抑制过冲和振铃。电源引脚附近放一组1μF加100nF去耦电容,TF卡在写数据时瞬态电流能到100mA量级,供电不稳最容易复位。

2.2 SD卡初始化的74个SCLK:这步错了什么都怪不了

SPI模式下的SD卡初始化有个隐藏门槛:上电后必须先给至少74个SCLK脉冲,然后在CS拉低的状态下发送CMD0,卡才会进入SPI模式并回复0x01。很多人拿着逻辑分析仪一看,MCU上电就开始发CMD0,完全没有额外时钟,于是卡一直不响应,还以为是电路问题。

实际做法是:初始化阶段把速率压到100kHz到400kHz,先让SCLK空跑几十个周期,再拉低CS发CMD0,之后继续读卡返回的R1响应,直到收到0x01为止。初始化完成后再把速率提到1MHz以上,SD卡SPI模式跑到25MHz附近基本到头,再往上就不是SPI该干的事了,得换SDIO。

2.3 硬件片选与软件片选:别一上来就分配GPIO

片选是SPI里最容易被人忽视的一个环节。软件片选就是用GPIO拉低拉高CS,优点是完全可控,什么时候拉、什么时候放都由代码说了算,多从机扩展时想切谁就切谁。硬件片选利用MCU外设的NSS引脚让硬件自动管理CS,好处是一帧数据的CS窗口和SCLK严格同步,CPU不用干预;但很多芯片的NSS会在字节间隙自动跳变,或者和软件NSS配置冲突,反而制造更隐蔽的时序问题。

我的经验是:默认用软件片选,只在极简单的一主一从、且确认硬件NSS行为完全符合预期时才用硬件片选。尤其在TF卡这种对CS时序敏感的从设备上,GPIO软件片选是最稳的。另一个细节是,片选切换时确保SCLK处于确定的空闲电平,不要在CS刚拉低时让SCLK产生毛刺,否则从设备会把多余的边沿当成有效时钟。

3. 全双工模式不神秘:GD32F303读取安富莱AD7606

3.1 AD7606为什么值得用SPI读

AD7606是8通道16位同步采样ADC,所有通道在同一个采样时刻被锁存,最大吞吐率200kSPS。安富莱的板卡上最流行的读取方式之一就是SPI串行输出:MCU通过SPI时钟逐位把16位转换结果移出来。这里有个新手必踩的坑:SPI是全双工,主设备要产生SCLK,唯一的办法是往发送寄存器写一个字节。也就是说,哪怕你只是想读数据,也得先"假装"发点什么,用这个假数据产生时钟,同时从MISO上收回从设备送出的结果。

3.2 全双工模式设置与寄存器

GD32F303的SPI外设配置并不复杂,关键是把传输模式设置成全双工。下面这段基于GD32标准外设库的初始化代码,基本就是我项目里的实际配置:

spi_parameter_struct spi_init_para; spi_init_para.trans_mode = SPI_TRANSMODE_FULLDUPLEX; spi_init_para.device_mode = SPI_MASTER; spi_init_para.frame_size = SPI_FRAMESIZE_16BIT; spi_init_para.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2ND; spi_init_para.nss = SPI_NSS_SOFT; spi_init_para.prescale = SPI_PSC_4; spi_init_para.endian = SPI_ENDIAN_MSB; spi_init(SPI1, &spi_init_para); spi_enable(SPI1);

帧大小选16位,不是随手写的:AD7606每个通道结果正好16位,一次SPI传输完成一个通道的读取,软件里不需要再做字节拼接和对齐。时钟极性这里配的是Mode 3,具体到你的板子还是要以AD7606手册为准。

全双工模式的核心约束是:主设备发送的每一位都在同一时刻从MISO接收一位。你发送一个0x0000,实际上就是给从设备提供16个时钟脉冲,顺带取回从设备在这16拍里推上MISO的数据。读AD7606时,CS和RD拉低之后,每个SCLK移出一位结果,16个SCLK读完一个通道。

3.3 用DMA把吞吐率拉满

如果只读一个通道,轮询写法也凑合。但AD7606是8通道16位,一次转换要读128bit,如果MCU在中断里逐字轮询,时间全耗在等待和状态检查上了。这时必须上DMA。GD32F303的DMA配置大致长这样:

dma_parameter_struct dma_init_para; dma_deinit(DMA0, DMA_CH3); dma_init_para.direction = DMA_PERIPHERAL_TO_MEMORY; dma_init_para.periph_addr = (uint32_t)&SPI_DATA(SPI1); dma_init_para.memory_addr = (uint32_t)adc_buf; dma_init_para.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_init_para.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_init_para.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; dma_init_para.memory_width = DMA_MEMORY_WIDTH_16BIT; dma_init_para.number = 8; dma_init_para.periph_to_memory_priority = DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH3, &dma_init_para); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3);

流程并不复杂:先配置好SPI全双工,再让DMA把外设数据寄存器里的16位数据连续搬到内存数组,搬运长度设为8,代表8个通道。实际项目里我会用BUSY引脚下降沿触发一个外部中断,在中断里启动一次DMA传输,读取这一轮8个通道的结果。开DMA循环模式后,整个读数据过程CPU基本不参与,只等在中断里取数组。

3.4 轮询与DMA效率对比:别只改了个寂寞

轮询转DMA常见误区是:软件结构改了,但SPI时钟还是4MHz,性能提升有限。算一下:AD7606满吞吐率200kSPS时,每5微秒要完成一次8通道读取,也就是5微秒内搬完128bit,SPI时钟至少要25.6MHz。如果SCLK只有10MHz,读完8个通道需要12.8微秒,实际吞吐率最多78kSPS,再优化DMA也突破不了物理上限。

我在安富莱例程基础上把SPI时钟提到20MHz以上,再配合DMA,才真正接近200kSPS的标称值。所以,遇到采样率上不去的项目,先算SPI时钟够不够,再谈DMA优化,顺序不能反。

4. 软件SPI和硬件SPI的本质区别

4.1 硬件SPI的"硬"在时序

硬件SPI的SCLK由外设内部的时钟分频电路直接产生,一旦配置完成,波形完全由硬件逻辑维系,不受中断、线程调度、编译器优化级别影响。这意味着SCLK的周期抖动很小,可以稳定跑在较高频率。GPIO模拟则不同,每一位的翻转靠CPU逐条执行指令,中间来一个中断,SCLK高电平就可能拉长几百纳秒,整套时序就花了。对慢速器件还能忍,对高速Flash、高速ADC就是灾难。

硬件SPI另一个常被低估的好处是收发的原子性:移位寄存器一边把MOSI的数据推出去,一边把MISO的数据收进来,完全同步。软件SPI很难做到同一句代码里同时精确操作四个引脚,哪怕用寄存器直接写GPIO,也要注意读MISO的时刻和SCLK边沿的先后关系,稍不小心就差一个节拍。

4.2 软件SPI的适用范围

软件SPI没有想象中那么不堪,它在三个场景里非常好用:一是GPIO引脚不够,硬件SPI引脚被复用占用时,用任意两个GPIO临时顶上;二是调试阶段,怀疑硬件SPI配置问题,先用软件SPI把最小通信打通,确认为例程有效;三是和极慢速老设备通信,比如93C46这类EEPROM,时钟几百kHz就够,软件模拟完全没压力。

但它的缺点也很明显:占CPU、时序抖动、频率上限低。我在48MHz主频的M0内核上实测,GPIO翻转加循环判断,软件SPI撑死跑到3到5MHz,而且这个频率下CPU已经被占死,什么别的任务都干不了。持续大流量传输、实时性要求高的场景,不要用软件SPI。

4.3 选型对照表

维度硬件SPI软件SPI
最高速率可达几十MHz通常3~5MHz封顶
CPU占用极低,可配DMA接近100%
时序稳定性硬件保障,抖动小受中断影响大
引脚灵活性固定引脚任意GPIO
实现复杂度寄存器配置较多逻辑直观
适用场景高速ADC、Flash、TF卡、显示慢速初始化、调试、兼容老芯片

补充一个个人习惯:即使是慢速从设备,只要硬件SPI引脚没被占死,我还是优先用硬件SPI。理由很简单,软件SPI这套代码一旦放进工程,之后每个人都要维护、都要理解,成本远高于几行外设初始化。

5. 一条SPI总线怎么测才算测过

5.1 回环自测:最简单的功能验证

SPI接口测试第一步永远是回环:把MOSI和MISO短接,MCU发出已知数据,再从接收寄存器读回来对比。这里有个细节,全双工下你每次发送一个字节的同时就会收到一个字节,所以发送和接收要放在同一个循环里做。我常用的测试序列是0xA5、0x5A、0xF0、0x0F,既能验证字节内比特顺序,又能验证收发缓冲。

回环通过只能证明MCU侧的SPI收发通路没问题,证明不了CS时序、从设备侧电气特性。所以回环之后必须接真正从设备,或者至少接一颗标准SPI Flash做读写测试。只做MCU自发自收就宣称"SPI调好了",是很多项目的坑。

5.2 逻辑分析仪看四根线的真实波形

回环通过之后,别急着高兴,拿逻辑分析仪把CS、SCLK、MOSI、MISO四根线都抓下来。重点看三处:CS下降沿是否先于第一个有效SCLK并保持足够建立时间;数据位在采样沿到来时是否早已稳定;整个帧结束后CS是否正常拉高,有没有残留的SCLK毛刺。

我碰到过一种典型情况:从设备返回的数据只有前几个字节对,后面全错。蹲在波形前才发现,逻辑分析仪的探头夹在MISO上引入了十几pF电容,把信号边沿拖缓了,等于给高速信号加了个低通滤波器。测量工具改变了被测对象,这种事在高速SPI上经常发生。所以逻辑分析仪尽量用短的接地弹簧针,别用一大把长杜邦线。

5.3 误码、抖动与边界测试:不要只看"能通"

"能通"和"稳定"是两回事。SPI总线在常温短线下能通,不意味着高低温、长线、多从机共存的极端条件下也稳。我会做一组简单但有效的压力测试:先发0x00和0xFF交替序列,再发伪随机序列,各跑100KB以上,统计误码率;然后把SPI时钟逐步调高一档,找到开始出现错误的临界频率,最后把工作频率定格在临界频率的三分之二左右留余量。

如果跨板连接超过10cm,信号完整性问题开始变得明显。这时候别盲目提频率,先看波形上升沿是否变缓、是否出现过冲振铃。加22到33Ω串联电阻往往比调寄存器更有效。很多人的第一反应是改驱动削draft,其实先用示波器看一眼SCLK和MISO的边沿,问题根源通常很快就暴露了。

6. FPGA实现串口升级QSPI:Bootloader侧的协议切换

6.1 从UART到QSPI Flash的整体数据流

很多用FPGA做主控的设备,固件存放在外部QSPI Nor Flash里,平时不希望把JTAG口引到外壳外,于是串口升级成了刚需。整体链路是:PC通过UART把固件分包发过来,FPGA里的UART接收模块收包、校验、缓存到RAM,再由QSPI控制器把数据按页写入Flash;设备下一次上电,BootROM直接从QSPI Flash读取新固件。

这条链路里,UART侧要处理帧格式、长度、CRC,QSPI侧要处理擦除、写使能、页编程、状态轮询。最容易想当然的地方是:很多人以为"往Flash写数据"就是发数据命令,实际上Nor Flash的写流程相当啰嗦。

6.2 QSPI核心命令与擦写状态机

以最常见的W25Q系列或GD25Q系列为例,写一个扇区前必须先发写使能命令0x06,否则内部锁存器不允许任何写操作;写完或擦完还要循环读状态寄存器0x05,等待WIP位清零。常用的命令包括:

功能命令码说明
写使能0x06每次写/擦前必须发送
读状态寄存器0x05查询WIP位是否忙
页编程0x02单线写256B一页
四线页编程0x32QSPI四线数据
扇区擦除0x20擦除4KB扇区
读数据0x03单线读
四线快读0x6B命令后四线输出

FPGA侧写状态机时,一个典型的QSPI页写入流程是:IDLE状态收到写请求后进入WREN发送0x06,再发写地址和页数据,最后进入WAIT_WIP循环读状态寄存器,直到WIP位为0才拉高CS收尾。简化版的状态机如下:

localparam IDLE = 3'd0; localparam WREN = 3'd1; localparam PAGE_PROG = 3'd2; localparam WAIT_WIP = 3'd3; localparam DONE = 3'd4;

状态机的每拍都要严格管理CS:CS拉低后发命令字节和地址,发完一个完整指令段才能拉高CS。QSPI Flash对手册里"CS高电平时间tSH"这类参数很敏感,拉高时间不够,下一次命令可能压根不识别。

6.3 在线升级的两个工程坑

第一个坑是擦除边界。Nor Flash按扇区擦除,起始地址必须落在4KB扇区边界上,如果你从扇区中间开始擦,会把这个扇区里的其他数据一起抹掉。而页编程又按256字节一页,写入数据不能跨页。稳妥做法是在FPGA里做一个地址对齐模块,先把起始地址向下取整到扇区边界,再按页拆分写入。

第二个坑是掉电保护。固件写到一半断电,Flash里可能残留半截固件,下次上电未必能启动。工业上常用A/B双镜像:保留一个固定偏移的启动标志区,新固件先写在B分区,全部写完后把启动标志改成指向B,下一次启动再去校验B分区完整性。如果校验失败,回滚到A分区。这套方案比单纯"写快点、断电概率低"可靠得多。

顺带回应一个网上常问的问题:不拆SPI Flash芯片能不能刷BIOS?答案是看平台的BootROM是否提供在线回写机制,很多路由器和主板都有类似的原厂恢复工具,但具体命令和流程因平台差异很大,不是SPI协议本身能决定的。如果没有这样的机制,用烧录器离线烧录仍然是最后兜底方案,这跟协议无关,跟平台固件设计有关。

7. MICROWIRE:三根线写下的历史课

7.1 半双工的前辈

在SPI被Motorola定义之前,National半导体在COP400系列单片机里已经用起了一个三线串行接口,名字叫MICROWIRE。它只有SK时钟、DI输入、DO输出三根线,工作在半双工模式。一次传输通常以一个16位的"协议字"为单位,内容包括起始位、几位操作码、地址位和数据位,具体位数由器件手册决定。

MICROWIRE年代最经典的器件就是93C系列EEPROM,比如93C46,直到今天还在生产。读它的时候,片选拉高后,主机在SK引脚送出一个起始位表明命令开始,随后按位送出操作码和地址,从设备在后面的时钟节拍里把数据一位一位推回DO。整个过程没有任何应答,主设备数清楚拍数就行,这就是同步串行总线最朴素的形态。

7.2 用现代MCU兼容老设备

如果你手头有块93C46,又不想专门找老编程器,用一个MCU的GPIO模拟就能读。93C46的读指令格式大概是起始位1、操作码10、六位地址,之后从设备在DO上输出16位数据。为什么不用硬件SPI直接发?因为MICROWIRE的命令字短,而且半双工切换时机完全由自己控制,用GPIO一位位模拟反而更简单:

// 伪代码:GPIO模拟MICROWIRE读 cs_high(); send_bit(1); // 起始位 send_bit(1); // 操作码 MSB send_bit(0); // 操作码 LSB for (addr_bit = 5; addr_bit >= 0; addr_bit--) send_bit((addr >> addr_bit) & 1); uint16_t data = 0; for (i = 15; i >= 0; i--) { sclk_high(); data |= gpio_read(DO_PIN) << i; sclk_low(); } cs_low();

如果非要拿现代MCU的硬件SPI去兼容MICROWIRE,也不是不能,但发完命令之后必须继续发一个dummy字节来产生时钟,同时把方向从输出切到输入。这就回到了SPI全双工那句老话:想读就得先发。本质上,MICROWIRE和SPI是一家人,只是当时的硬件设计倾向省引脚,选择了半双工。

调完AD7606那个晚上,我突然想明白SPI为什么能活三十年:它的强大不在快,而在简单。主设备和从设备之间只要定好片选、时钟和采样沿,剩下的全是数据。后来再看QSPI、OctoSPI甚至老掉牙的MICROWIRE,都只是把这种约定不断拉宽、提速。工程里少一点花活,多一点对物理层的敬畏,很多所谓玄学问题,其实都只是没把最基础的那几根线伺候好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询