SPI总线调试实战:从AD7606到QSPI升级的踩坑复盘

📅 发布时间:2026/10/8 5:14:40
SPI总线调试实战:从AD7606到QSPI升级的踩坑复盘
入行第二年那会儿我拿到一块八通道同步采样板主控是GD32F303ADC是安富莱板子上常见的AD7606两者之间就靠SPI连接。当时我的心态和大多数人一样四根线而已调起来能有多难结果一上电读回来的数据没有一个通道是对的。我花了一整周把CPOL和CPHA的四种组合全试一遍最后蹲下来用示波器去量MISO发现那个引脚上连一个稳定的高电平都等不到——少了一颗上拉电阻。那次经历之后我把SPI/QSPI/OctoSPI/MICROWIRE这一族串行同步通信总线从头到尾理了一遍。这篇就当是当年的踩坑复盘顺便把TF卡SPI电路、SPI DMA、软件SPI与硬件SPI的区别、QSPI在线升级这些真实项目里躲不开的问题一起讲清楚。适合正在调试SPI从设备、准备做Bootloader或者被片选信号折腾到头秃的嵌入式工程师。1. 为什么一根总线能活三十年从SPI到QSPI和OctoSPI1.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读取安富莱AD76063.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微秒内搬完128bitSPI时钟至少要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实现串口升级QSPIBootloader侧的协议切换6.1 从UART到QSPI Flash的整体数据流很多用FPGA做主控的设备固件存放在外部QSPI Nor Flash里平时不希望把JTAG口引到外壳外于是串口升级成了刚需。整体链路是PC通过UART把固件分包发过来FPGA里的UART接收模块收包、校验、缓存到RAM再由QSPI控制器把数据按页写入Flash设备下一次上电BootROM直接从QSPI Flash读取新固件。这条链路里UART侧要处理帧格式、长度、CRCQSPI侧要处理擦除、写使能、页编程、状态轮询。最容易想当然的地方是很多人以为往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 3d0; localparam WREN 3d1; localparam PAGE_PROG 3d2; localparam WAIT_WIP 3d3; localparam DONE 3d4;状态机的每拍都要严格管理CSCS拉低后发命令字节和地址发完一个完整指令段才能拉高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都只是把这种约定不断拉宽、提速。工程里少一点花活多一点对物理层的敬畏很多所谓玄学问题其实都只是没把最基础的那几根线伺候好。