资讯详情

SPI总线协议实战笔记:四线机制、四种模式与调试避坑

📅 2026/10/21 9:22:53 | 华诺云谱 👁 阅读
SPI总线协议实战笔记:四线机制、四种模式与调试避坑
学通信协议的第三天我给自己定的目标很明确把SPI总线协议彻底吃透。前两天刚把UART和I2C调通本以为SPI就是多几根线、速度快一点结果真正把一块SPI接口的彩屏和一颗SPI存储芯片接到主板上之后才发现这个“简单”的总线里藏着不少容易栽跟头的细节。这篇就当第三天的实战笔记讲清楚四根线怎么配合、四种工作模式怎么选、主机驱动怎么写以及我实际踩过的三个坑。适合正在学单片机和嵌入式通信的新手也适合一直用库函数却从没深究过时序的老手——很多问题其实就出在“以为懂了”的地方。1. 为什么第三天才轮到SPI它和UART、I2C的定位差异1.1 三条总线的“性格”完全不同前两天调UART和I2C的时候我一直在想一个问题既然已经有了两种串行总线SPI存在的意义到底是什么调下来的感受是这三种协议不是竞争关系而是各自解决了不同的痛点。UART最随性两根线就能跑异步通信不需要时钟线但速率做不高而且收发双方得提前约定好波特率。I2C省引脚两根线还能挂一堆设备每个设备有地址但通信过程带应答机制速率上限相对保守而且每传一个字节都要处理ACK读大量数据时协议开销很重。SPI则完全是另一个路子它把片选线单独拉出来一根CS就解决“跟谁说话”的问题不需要设备地址全双工四线同步传输时钟由主设备提供数据边沿完全确定。用一句话概括我第三天的体会UART适合两个设备之间简单透传I2C适合低速挂载一群小外设而SPI适合“把数据高速灌进屏幕、flash、ADC这种需要连续吞吐的芯片里”。下面这张表是我当时整理的对比拿来入门很够用对比项UARTI2CSPI数据线数量2线TX/RX2线SDA/SCL4线CS/SCLK/MOSI/MISO通信方式异步同步同步时钟来源双方各自约定波特率主设备提供SCL主设备提供SCLK设备寻址无需地址点对点7位或10位地址靠CS片选无需地址全双工支持半双工全双工协议开销低中有ACK/NACK低典型速率常见115200bps级别100k/400kHz级别几MHz到几十MHz最强场景日志、调试串口传感器挂载屏幕、Flash、高速外设1.2 SPI最常出现的面孔屏幕、Flash与高速采集我这次调试的外设是入门级彩屏和一颗SPI Nor Flash芯片。屏幕需要不断刷新像素点数据量大I2C那个速率刷新起来会肉眼可见地卡UART得额外处理帧格式也不适合。SPI就非常合适主设备只管把像素字节顺着MOSI送出去MISO甚至可以不用接因为屏幕大部分时候不回数据。Flash芯片则是另一个典型用法往里面写固件或者记录数据需要高速连续写入读出的时候主设备发一个读命令紧接着就能在MISO上拿到数据这个“发命令的同时读数据”的能力是SPI的灵魂。除了这两类我还见过用SPI接高精度ADC、DAC、SD卡模块、传感器集线器的场景。判断一个外设适不适合走SPI就看两点第一它是否需要持续大量传数据第二它能不能容忍主设备用片选线“独占”整条总线。如果两个条件都满足SPI基本就是最优解。2. 四根线怎么把一次读写跑通SPI的底层配合逻辑2.1 从CS拉低开始一次事务的生命周期我第一次看SPI时序图时最大的困惑是明明有SCLK时钟线为什么还要CS这个片选信号后来才想明白SCLK是全局时钟所有挂在总线上的从设备都能看到但到底哪个设备应该响应就必须靠CS来区分。所以一次完整的SPI事务是这样的主设备先把目标从设备对应的CS引脚拉低告诉对方“我要跟你通信了”然后SCLK开始产生连续的时钟脉冲MOSI上一位一位发送数据MISO上一位一位回收数据通信结束后主设备把CS拉高从设备知道自己这一轮的活儿干完了。这里有个关键习惯CS拉低到SCLK开始之间以及SCLK结束后到CS拉高之前都要预留一点时间。原因是从设备内部需要识别片选有效、准备好自己的状态机。有些从设备芯片手册会给出最小建立时间比如CS拉到低电平后至少要过几十纳秒才能给第一个时钟沿。我之前偷懒把CS和第一个SCLK边沿挤得太近设备偶尔工作偶尔不工作后来加上延时就好了。2.2 全双工的直觉读的时候到底发生了什么“全双工”这个词在SPI里有一层反直觉的理解主设备读数据的同时必须也“写”点什么。因为时钟是主设备产生的从设备自己没有时钟无法主动把数据推给主设备它只能借着主设备提供的SCLK信号在某个边沿把MISO上的数据推出去。也就是说想从Flash里读一个字节主设备不能干等着而是要在发送读命令后继续在MOSI上发送一个“哑字节”通常发0xFF或0x00。SCLK每跳一次从设备就往MISO上放一位数据主设备内心其实不关心这个哑字节的内容它关心的是从MISO上采到的这8个bit。我刚学的时候一直犯迷糊为什么读Flash要先发一个0xFF后来在逻辑分析仪上看到波形就明白了0xFF只是给从设备提供时钟节拍的工具。这个机制也解释了为什么SPI读速度可以做到很高写和读在同一个时钟周期内完成效率天然比I2C高。2.3 时钟边沿与数据变化先分清“变化”和“采样”要理解SPI模式绕不开两个动作数据变化和数据采样。它们分别发生在时钟的不同边沿。拿模式0来说SCLK空闲时是低电平。主设备在SCLK产生一个上升沿之前先把MOSI上的数据准备好然后在上升沿这个时刻从设备把MOSI上的电平抄走。而MISO上的数据变化发生在SCLK的下降沿也就是说从设备在下降沿之后才开始切换输出主设备等下一个上升沿再去采样。这就像发牌员和你之间的配合发牌员先把牌放到桌上然后喊一声“拿牌”你才拿起牌而你把牌放回去的动作得等他喊完“下一张”之后再做。如果两个人节奏错位牌就会放错地方、拿错时间。SPI四种模式本质上就是“空闲电平”和“第几个边沿采样”这两个维度的组合下一节展开讲。3. CPOL与CPHA四种模式不是四个选项而是两种判断3.1 用“空闲电平采样边沿”理解四个组合很多新手看到手册里的CPOL、CPHA就头皮发麻其实拆开看就两个问题CPOL决定SCLK空闲时的电平是低还是高。CPOL0空闲低CPOL1空闲高。CPHA决定数据是在SCLK的第一个边沿采样还是第二个边沿采样。CPHA0第一个边沿采样CPHA1第二个边沿采样。把两个维度组合起来正好是四种模式。以CPOL0、CPHA0的模式0为例时钟空闲为低第一个边沿是上升沿所以数据在上升沿被采样在下降沿发生变化。最常见的SPI Flash、屏幕控制器绝大多数默认就是模式0或模式3。我理解这个事有一个笨办法把SCLK的一个完整周期想象成“低-高-低”或者“高-低-高”的台阶。CPHA0意味着主设备在踩上第一个台阶的瞬间去拿数据CPHA1意味着主设备在踩上第二个台阶的瞬间才去拿数据。先想清楚台阶是谁、什么时候被踩相位问题就解决了一大半。3.2 四种模式速查表从从设备手册读到正确配置实际开发中最大的困惑不是记不住表而是不知道怎么从手册里反推出该用哪个模式。我的经验是不背默认值看手册里的时序图。模式CPOLCPHASCLK空闲电平采样边沿数据变化边沿模式000低上升沿下降沿模式101低下降沿上升沿模式210高下降沿上升沿模式311高上升沿下降沿看手册时找三个信号SCLK、MOSI主机输出、MISO从机输出。先看SCLK空闲状态是高还是低判断CPOL再看数据线的跳变和时钟边沿谁先谁后。如果MOSI上的数据在SCLK的某个边沿右侧稳定下来而这个边沿之后下一个边沿才是采样点那就要看第一个边沿是上升沿还是下降沿从而确定CPHA。举个实际例子某存储芯片手册上写着“在SCLK上升沿采样数据”并且时序图里时钟空闲为低那就是模式0如果写着“数据在SCLK下降沿之前有效”但空闲为高那多半是模式3。确认模式后直接到寄存器里设置CPOL、CPHA两个位即可。3.3 一个容易被忽略的细节模式选择与初始化顺序模式不是初始化时随便配一下就完事了它还牵涉初始化顺序。我踩过一个很隐蔽的问题先拉低了CS再配置时钟极性和相位结果从设备在CS刚拉低时就检测到了SCLK空闲电平异常直接进入错误状态。正确的顺序应该是先把GPIO引脚模式设好确保CS默认高、SCLK正确空闲电平、MOSI有确定的电平再配置SPI外设的CPOL、CPHA、分频系数最后使能SPI外设。全部准备好之后才去拉低CS发起事务。用库函数开发时不少人喜欢芯片复位后立刻拉低CS去读ID这样很容易踩到“模式还没配好片选已经有效”的坑。把CS看作整个通信的“门闩”门闩落下之前屋里所有东西都得摆好否则从设备看到的是一片混乱。4. 手写SPI主机从寄存器配置到波形验证的完整流程4.1 先选IO模拟还是硬件SPI我为什么建议先软后硬开发板的SPI外设很方便但初学阶段我更推荐先拿GPIO模拟一遍哪怕后面真正量产用的是硬件SPI。原因有三个第一IO模拟能逼着你理解时序。写代码的时候你不得不考虑“先放数据还是先拉时钟”“采样点在哪里”这些细节在硬件SPI里全被外设逻辑藏住了。第二IO模拟便于调试。写错了可以加打印、加延时问题定位到具体某一位硬件SPI出错时黑盒感很强只能反复看寄存器状态。第三低速下IO模拟的波形其实很干净很多传感器、屏幕对速率不敏感完全够用。等IO模拟版本的读写流程跑通之后再切换到硬件SPI把速率提上来加上DMA你会发现整个框架早就搭好了只是把“手动拉脚”换成了“写寄存器”。4.2 GPIO模拟SPI的C语言实现一个可直接抄的框架下面这段是我在测试板上调通的模式0主机收发框架。关键点在于数据在SCLK拉高之前先放到MOSI上并且MISO的采样放在SCLK高电平期间进行。#include chip.h #define CS_HIGH() gpio_write(CS_PIN, 1) #define CS_LOW() gpio_write(CS_PIN, 0) #define SCK_HIGH() gpio_write(SCK_PIN, 1) #define SCK_LOW() gpio_write(SCK_PIN, 0) #define MOSI_HIGH() gpio_write(MOSI_PIN, 1) #define MOSI_LOW() gpio_write(MOSI_PIN, 0) #define MISO_READ() gpio_read(MISO_PIN) void spi_gpio_init(void) { gpio_mode(CS_PIN, OUTPUT); gpio_mode(SCK_PIN, OUTPUT); gpio_mode(MOSI_PIN, OUTPUT); gpio_mode(MISO_PIN, INPUT_PULLUP); CS_HIGH(); SCK_LOW(); // 模式0空闲低电平 MOSI_LOW(); } static void delay_half_bit(void) { volatile int i; for (i 0; i 50; i) ; // 延时代替实际按主频调整 } uint8_t spi_exchange_byte(uint8_t data_out) { uint8_t data_in 0; int i; for (i 7; i 0; i--) { // 1. 在SCLK变化前把当前bit放到MOSI上MSB先发 if (data_out (1 i)) MOSI_HIGH(); else MOSI_LOW(); // 2. 拉高时钟产生上升沿 SCK_HIGH(); delay_half_bit(); // 3. 高电平期间从MISO采样 if (MISO_READ()) data_in | (1 i); // 4. 拉低时钟产生下降沿完成一个bit周期 SCK_LOW(); delay_half_bit(); } return data_in; } void spi_transfer(const uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len) { uint32_t i; CS_LOW(); // 拉低片选开始事务 for (i 0; i len; i) rx_buf[i] spi_exchange_byte(tx_buf ? tx_buf[i] : 0xFF); CS_HIGH(); // 拉高片选结束事务 }这段代码对应模式0空闲低、上升沿采样、下降沿换数据。如果你用的设备是模式3空闲高、上升沿采样只需把初始化里的SCK_LOW()改成SCK_HIGH()并且把收发循环里的时钟拉高拉低顺序反过来即可。其他模式以此类推核心思路永远是“先把输出数据准备好再产生采样边沿”。真正在项目里用的时候我还会加两个小细节delay_half_bit不要用太短的固定循环最好按主频和当前SCLK目标频率算好分频spi_transfer里发送缓冲区为空时统一替换成0xFF这样读Flash时就不用单独写一个“只读”函数了。4.3 硬件SPI初始化要点寄存器字段背后的含义IO模拟跑通后硬件SPI的寄存器配置就顺理成章了。不同芯片寄存器名字不一样但字段含义基本一致我把常见字段列一下主从模式Master/Slave设为主机模式由本机产生SCLK。波特率分频Baud Rate PrescalerSCLK频率 主机外设时钟 / 分频系数。初学先分频到1MHz以内稳定后再往上提。CPOL/CPHA对应四种工作模式和上面表格一致。数据帧格式通常是8位一帧MSB先行。软件/硬件片选管理大多数MCU的SPI外设可以自动控制CS但我个人更喜欢软件控制CS因为可以精确控制片选拉低/拉高的时间点而且多从设备总线切换更灵活。使能位初始化顺序里所有参数配好之后最后写使能位。只做一个方向的传输时可以关掉未使用的接收或发送功能省一点功耗但调试阶段建议全开。我记得有一次调Flash读操作死活读不到数据最后发现是接收功能没使能FIFO里的数据根本没进接收寄存器。细节决定成败在SPI上体现得非常彻底。5. 实测记录三次调试翻车的完整排查链路5.1 翻车一时钟极性和相位不匹配读回来的字节全是错位现象很典型初始化完代码读Flash的厂商ID返回的全是0xFF偶尔还会出现0x00和0x01这种“半错位”数据。屏幕则干脆全白像没通电一样。一开始我怀疑是接线问题用万用表量了一遍MOSI、MISO、SCLK、CS通断全部正常供电也正常。接着用逻辑分析仪抓MOSI波形发现发出去的命令字节其实是正确的说明主机发送链路没问题。于是把注意力转向MISO结果发现MISO上偶尔有波形但边沿位置看起来和SCLK的上升沿贴得很近。问题最后定位在CPHA上。设备手册的时序图画得比较含糊我开始按默认的模式0跑但实际上这颗芯片要求的是模式0的“采样点稍微靠后”。把CPHA从0改成1之后读回来的ID完全正确。这件事给我最大的教训是不要默认“大家都用模式0”尤其是用库函数开发时库封装的默认参数很可能是0但不一定匹配你的从机器件。每次接新芯片第一件事就是查它手册里明确描述的采样边沿。5.2 翻车二提前释放片选最后一个字节的神秘丢失第二坑出现在往Flash里写数据。写入一页数据回读校验的时候总是最后一个字节不对要么是旧值要么是0xFF。我一度以为是Flash品质问题换了一颗还是同样现象。排查思路是这样的因为只有最后一个字节出错说明前面的数据链路都正常问题大概率出在事务的“收尾动作”上。我在代码里找到了问题执行完最后一字节的发送后立刻拉高了CS。从Flash芯片的角度看它还在处理最后一个字节的数据写入此时CS突然变高芯片会认为主机中断了本次事务于是最后一笔数据被丢弃。解决方法是发送完最后一个字节的SCLK边沿与CS拉高之间增加一个极短的延时或者额外产生一个完整的时钟周期再拉高CS。这个延时不需要很长但必须存在。后来我把这个“CS释放时间”作为代码里的固定配置每次初始化和传输结束时都检查一遍再没出过类似问题。5.3 翻车三MOSI和MISO接线反了却“看起来”正常这是一个非常误导人的故障。某次调一块SPI转接板读设备寄存器发现所有值都是0x00但设备命令“看起来”发出去了逻辑分析仪上MOSI也确实有波形。奇怪的是即使我换了一台主机现象依旧。后来把逻辑分析仪的四个通道分别接到转接板的两侧才发现问题板子上丝印标注的MOSI和MISO其实和另一边是对调的我的线缆按直觉接到主机上等效于把主机的MOSI接到了从机的MOSI主机和从机都在往同一个方向上发数据谁也没法收。这类故障的特征是主设备发送正常但接收永远为0偶尔有些芯片会在MISO线上配置上拉电阻于是读回的全是1。排查方法很简单把CS拉低后主设备故意发一个0x00给从机观察逻辑分析仪上从机方向的数据线上有没有变化如果完全静止很可能是收发两根线接反了或者从机压根没被唤醒。我后来习惯在每个SPI外设的初始化代码里先做一次“环回自测”——把主机的MOSI短接到MISO发一个字节看能不能原样收回来能收回来至少说明主机侧没问题。5.4 排查顺序建议遇到SPI不工作我现在的第一反应经过三天的折腾我总结了一套排查顺序以后遇到SPI不工作不再手忙脚乱用万用表量四根线的通断排除接触不良和反接。这一步最便宜最省事却最容易被跳过。用逻辑分析仪抓一次完整事务确认CS确实拉低了、SCLK确实在走、MOSI有没有波形。确认从设备供电和复位引脚正常。很多SPI从设备有独立复位脚如果复位脚被误接低电平芯片根本不工作。核对CPOL和CPHA。不要凭空猜去看手册上的时序图对照采样边沿。降速重试。把SCLK降到几百kHz甚至几十kHz如果故障消失大概率是信号完整性问题比如线太长、沿太陡、电平不匹配。检查CS时序。看看CS拉低到首个SCLK边沿的时间、SCLK结束到CS拉高的时间是否满足手册要求。这套流程下来绝大多数SPI问题都能定位到具体环节而不是像无头苍蝇一样反复换芯片。6. 调试工具与波形判读没有逻辑分析仪也能定位大方向6.1 逻辑分析仪看四线关系一次错误的波形长什么样逻辑分析仪是调SPI最好用的工具。不用买太贵的采样率几十MHz的四通道就够了。接线时把四个通道分别接CS、SCLK、MOSI、MISO公共地必须和主板共地。看波形时我习惯先找CS的下降沿再从左往右数SCLK上升沿。在第一个SCLK边沿处看MOSI上第一位数据是否已经稳定再看MISO上有没有跟着变化。有一次抓Flash读ID发现MISO在第四个字节之后才出现波形我立刻意识到前面的发送时序里多了一个命令字节而芯片在等待更多数据。没有逻辑分析仪的时候这种“看不见摸不着”的问题只能靠猜有了波形图几分钟就能定位。6.2 示波器单次触发把信号完整性看明白逻辑分析仪能看时序关系但看不了信号完整性。比如SCLK信号存在明显振铃逻辑分析仪会把它当成多个边沿导致数据错乱。这时候需要示波器。我的做法是将示波器触发模式设为“单次触发”触发源选SCLK触发电平设为SCLK高电平的一半然后在代码里只执行一次短暂的数据传输让示波器抓到完整的一帧。观察MOSI线上数据电平是否在采样边沿处已经稳定稳定不少于半个时钟周期才算可靠。如果发现边沿过冲超过0.3V我会优先检查这几件事SCLK线上是不是串联了过长导线信号线之间是不是有平行走线带来的串扰SPI速率是不是设得太高末端有没有加匹配电阻。低速调通的系统在提高速率时经常在这里栽跟头。6.3 没有仪器时用串口回读和LED闪烁缩小范围不是所有环境都有逻辑分析仪和示波器第一天调UART时我也只能靠串口打印。调试SPI时如果手头只有串口我依然能缩小范围。先写一个最简传输函数固定发送一串递增字节比如0x00、0x01、0x02一直到0x0F然后把MISO上的返回值通过UART打印出来。如果返回值完全不等于发送值先别慌检查MISO引脚是否确实收到数据如果接收结果全是0xFF很可能是MISO没通或者从设备没上电。还可以配置一个不用的GPIO在CS拉低时点亮LED拉高时熄灭用肉眼看片选动作是否发生。这些土办法不如逻辑分析仪直观但在现场没有仪器的条件下能帮我确认最基础的问题信号有没有过去、设备有没有响应、数据线通不通。至少能避免“调了半天发现原来是从设备根本没工作”的尴尬。我在实际调试中的体会是SPI总线协议真正难的地方不在协议本身而在那些“看不见的时序细节”CS释放时间不够会导致最后一个字节丢失CPOL/CPHA不匹配会导致数据全乱MOSI和MISO接反了却不知道。如果你最近也在被SPI折磨建议按照我上面的排查顺序走一遍大部分问题都能在半小时内水落石出。第三天能把这些东西彻底搞明白后面再碰高速SPI、DMA传输、多从设备总线心里就有底了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑