搞嵌入式的基本上都躲不开SPI总线这四个字母。SPI(Serial Peripheral Interface)最早是Motorola在上世纪八十年代提出的一种同步串行总线,到今天,你手里的手机、车上的ECU、工控板上的Flash、屏幕、传感器,几乎还在用它。它的标准形态就四根线:SCK、MOSI、MISO、CS,没有地址帧,没有仲裁,没有应答,全双工,主机给时钟从机就跟着走,速率能拉到几十上百兆。我第一次用逻辑分析仪抓SPI波形的时候,看到时钟沿和数据位严丝合缝地对齐,那种"原来如此"的感觉到现在都还记得。这篇内容就是把我这些年用SPI踩过的坑、总结出来的配置方法、以及STM32平台上的实操代码整理一遍,从协议本质讲到DMA接收,再讲到车载、FPGA、机械臂这些具体场景,不管你是刚接触单片机的新手,还是已经在做量产项目的工程师,都能从里面翻到能直接抄作业的部分。
1. SPI总线到底是个什么东西:四根线背后的设计哲学
1.1 从"两个移位寄存器"理解SPI的本质
很多人学SPI的时候死记硬背"CPOL、CPHA、模式0到模式3",背完还是不知道自己在干什么。其实SPI的核心模型特别简单:主机和从机内部各有一个移位寄存器,主机把自己的数据从MOSI一位一位推出去,同时从机把自己的数据从MISO一位一位推回来,时钟每跳一个周期,两个寄存器就完成一次"环形交换"。八个时钟之后,主机寄存器里的内容跑到了从机,从机寄存器里的内容跑到了主机。这就是为什么SPI天然全双工,也是为什么主机接收数据时SCK照样在跑——时钟不是为发送服务的,时钟是为交换服务的。
理解了这个模型,很多事情就通了。比如为什么SPI没有应答机制?因为它压根不是"命令-响应"模型,而是"移位交换"模型,你想确认从机收到没有,只能靠软件层自己加校验。再比如为什么SPI从机不能主动发起通信?因为时钟握在主机手里,从机没有时钟就只能干等着。再比如为什么SPI读数据时你还得给它发一个字节(通常是0xFF或0x00)?因为你必须用发送动作把时钟"喂"出去,从机的数据才能被推回来。我见过有新手调SPI Flash读ID,卡了一整天,就是因为只调了接收函数没发时钟,从机的移位寄存器根本没被推动。
从硬件角度看,这个环形交换结构决定了SPI的引脚角色:SCK永远由主机输出,MOSI主机输出从机输入,MISO从机输出主机输入,CS由主机控制拉低以选中某个从机。注意MISO这根线在从机没被选中时应该是高阻态,所以一条总线上挂多个从机在理论上是可行的——只要每个从机有独立的CS,且没被选中的从机把MISO释放掉。但这在实际工程里有个隐患:如果某个从机的MISO驱动能力没做好,或者CS拉高的时机不对,总线就会打架。我后面在排查章节会专门讲这个问题。
1.2 SPI、UART、I2C到底差在哪,一张表说清
面试和实际选型里被问得最多的就是这几个总线的区别。我不喜欢背概念,直接上对比。
| 特性 | SPI | I2C | UART |
|---|---|---|---|
| 线数 | 4根(SCK/MOSI/MISO/CS) | 2根(SCL/SDA) | 2根(TX/RX) |
| 同步方式 | 同步,有时钟线 | 同步,有时钟线 | 异步,无时钟线 |
| 双工 | 全双工 | 半双工 | 全双工 |
| 寻址 | 片选线选从机 | 7位/10位地址 | 点对点,无地址 |
| 速率 | 几MHz到上百MHz | 100k/400k/1M/3.4M | 常用9600到几M |
| 多从机 | 每从机一根CS | 地址寻址,总线共享 | 一般点对点 |
| 硬件开销 | 引脚多,协议简单 | 引脚少,协议复杂 | 引脚少,协议中等 |
| 典型外设 | Flash、屏幕、ADC、传感器 | EEPROM、RTC、温感 | 模块、调试口、GPS |
这张表里最关键的一行是"寻址方式"。SPI用片选线选从机,代价是每个从机多一根IO,好处是选中逻辑简单到极致,不需要任何地址帧开销,时钟可以直接拉满。I2C用地址帧选从机,两根线能挂一百多个设备,代价是每次通信都要发地址、等应答,速率上不去,而且总线电容一超标波形就烂。UART是异步的,收发双方靠波特率对齐,没有时钟线意味着没法做高速长距离可靠传输,但胜在简单,两根线就能通。
选型的时候我的经验是:要速度、数据量大、实时性要求高,选SPI;要省引脚、挂一堆低速小外设,选I2C;要跟PC或其他设备做通用串口通信,选UART。有次做项目需要同时接一块TFT屏、一片Flash和一颗六轴传感器,我一开始想全走I2C省引脚,实测下来屏幕刷新根本扛不住,最后屏幕和Flash走SPI,传感器走I2C,各取所需。这种"混合布线"在实际板子上很常见,别被"一个项目只能用一种总线"的思维框住。
1.3 为什么高速外设偏爱SPI
SPI能跑到很高的速率,这一点是它跟I2C拉开差距的根本原因。I2C因为开漏输出加上拉电阻,上升沿是RC充电,速率被总线电容死死限制,400kHz以上就挑布线,1MHz以上更是要仔细设计。SPI是推挽输出,上升沿陡峭,时钟频率做上去很容易,STM32F103的SPI1挂在APB2上,理论可以到36MHz,实际手册推荐不超过18MHz;换成STM32H7这类高性能芯片,SPI能到100MHz以上,QSPI(四线SPI)甚至能到133MHz。这种速率差距直接决定了外设的选型:SPI Flash、SPI屏幕、高速ADC、摄像头的配置接口,几乎清一色走SPI。
还有一点容易被忽略:SPI的协议开销几乎为零。发一个字节就是八个时钟,没有起始位、停止位、地址位、应答位。I2C发一个字节实际要在总线上跑至少九个时钟(八位数据加一位应答),还要加上地址帧和起始停止条件。在大批量数据传输场景,比如从Flash读一兆字节的固件,SPI的吞吐效率能把I2C甩开一个数量级。这就是为什么带"硬件SPI"的单片机在做数据密集型任务时有明显优势。
不过"快"也是有代价的。SPI频率越高,对走线、阻抗、端接的要求就越高。低速的时候飞线都能跑,频率上到几十兆,走线长度、参考地平面、串扰就都成了问题。我在后面排查章节会讲几个因为走线太长导致高速SPI误码的真实案例,这是新手最容易忽视的地方。
2. 时序与模式:CPOL、CPHA和那些抓波形时才想起来的事
2.1 四种模式是怎么被定义出来的
CPOL(Clock Polarity)决定时钟空闲时是高电平还是低电平,CPHA(Clock Phase)决定数据在第一个边沿还是第二个边沿被采样。两个参数一组合,就有了模式0到模式3。
- 模式0:CPOL=0,CPHA=0。空闲低电平,第一个边沿(上升沿)采样。
- 模式1:CPOL=0,CPHA=1。空闲低电平,第二个边沿(下降沿)采样。
- 模式2:CPOL=1,CPHA=0。空闲高电平,第一个边沿(下降沿)采样。
- 模式3:CPOL=1,CPHA=1。空闲高电平,第二个边沿(上升沿)采样。
死记这四个组合没意义,我更喜欢用一个统一的理解方式:CPHA=0表示"第一个边沿采样,第二个边沿移位输出";CPHA=1表示"第一个边沿移位输出,第二个边沿采样"。你只要记住这条规律,再看CPOL决定空闲电平,四种模式自然就出来了。之所以要有这些模式,是因为不同的芯片设计者对"数据在哪个边沿稳定"的选择不一样,早期各家各自为政,后来就成了事实标准,谁也改不了。
我给新手的建议是:不要猜,去看数据手册。数据手册里一定会写"Data is sampled on the rising edge"或者画一张时序图。你要做的是把主机配置成和从机匹配的模式。模式配错了,波形抓出来会看到数据在错误的边沿跳变,读回来的值全是乱的,或者整体偏移一位。我调一颗压力传感器的时候,因为懒得翻手册,试了四个模式才蒙对,其实手册第一页就画了时序图。这种亏吃过一次就够了。
2.2 建立时间、保持时间和采样边沿的关系
光把模式配对还不够,还得看时序余量。从机数据手册里通常会标两个参数:tSU(数据建立时间)和tH(数据保持时间),意思是数据必须在采样边沿之前tSU时间就稳定,在采样边沿之后还要保持tH时间。主机SCK频率越高,一个时钟周期越短,留给建立和保持的时间就越少。当SCK周期的一半小于从机要求的tSU加驱动延迟时,采样就会出错。
举个具体例子。假设从机要求tSU=10ns,tH=5ns,主机SPI时钟是18MHz,周期约55.5ns,半周期约27.8ns。从机在SCK边沿之后翻转数据(CPHA=1的情况),那么从边沿翻转到下一个采样边沿之间只有半个周期约27.8ns,扣掉从机输出延迟比如15ns,还剩12.8ns,勉强大于tSU=10ns,能过。但如果把时钟提到36MHz,半周期只剩13.9ns,扣掉15ns的延迟,建立时间变成负数,直接崩。这就是为什么高速SPI不能只看芯片标称支持的最高频率,还得看时序余量。
实际调试时,如果发现偶尔读错、误码率随温度变化、或者换个批次的芯片就出问题,第一反应就应该是查时序余量。解决手段无非几种:降速、换模式、加驱动器、缩短走线。降速是最简单也最有效的,很多"玄学问题"把时钟从18MHz降到9MHz就消失了,其实就是时序余量不够。
2.3 硬件片选和软件片选,到底该用哪个
STM32的SPI外设自带NSS引脚,可以配置成硬件管理模式,也可以配置成软件管理模式。硬件模式下,NSS由SPI外设自动控制,通信开始时拉低,结束时拉高,看似省事。软件模式下,NSS被当成普通IO,由你自己写代码控制拉低拉高。
我强烈推荐在绝大多数项目里用软件片选。原因有三个。第一,硬件NSS在多从机场景下根本不够用,因为一个SPI外设只有一个NSS引脚,挂三个从机你就得想别的办法。第二,硬件NSS的拉高拉低时机和DMA传输结束的时序有时候对不上,尤其在高频下容易出现CS提前拉高、最后一个字节没发完的情况。第三,软件片选让你能精确控制CS和SCK之间的间隔,有些从机(比如某些Flash和ADC)要求CS拉低后要等一段时间才能开始发时钟,硬件模式给不了这个灵活性。
用软件片选的时候有几个细节要抠。第一,CS拉低到第一个SCK边沿之间要有足够的建立时间,一般从机手册会给tSLCH这类参数,通常几十纳秒,普通GPIO翻转加上函数调用开销基本够,但如果你在CS拉低后立刻调用SPI发送函数,稳妥起见可以插几个NOP。第二,CS拉高要在最后一个SCK边沿之后延迟一段,确保从机把最后一位数据锁存完,STM32上有个经典坑是SPI发送完成中断触发时BSY标志还没清零,这时候拉高CS会截断最后一位,正确做法是等BSY清零再拉高。第三,多从机时CS的初始状态一定要设成高,上电初始化顺序要保证GPIO先配置好再开SPI,否则可能出现上电瞬间多个从机同时被选中的情况。
3. 上手实操:STM32F103 配 CubeMX 的 SPI 与 DMA 接收
3.1 CubeMX里那几个关键参数怎么填
以STM32F103C8T6为例,用CubeMX配置SPI1做主机、DMA接收。打开SPI1,参数页里几个关键项:
- Mode:选Full-Duplex Master,全双工主机。如果你只接收不发送,理论上可以选半双工,但STM32的SPI接收需要时钟,半双工接收模式在F1上支持得不好,建议直接全双工。
- Hardware NSS Signal:选Disable,改用软件片选。NSS引脚会被释放成普通GPIO,你自己分配。
- Data Size:8 Bits。也有16位的选项,看你的外设,Flash和大多数传感器都是8位。
- First Bit:MSB First。绝大多数SPI设备是高位先出,除非手册明确写LSB First。
- Prescaler:分频系数,SPI1挂APB2,72MHz,分频4得到18MHz,分频8得到9MHz。初调建议先用分频8或16,跑通再提。
- CPOL/CPHA:按从机手册选,通常默认是模式0(CPOL Low,CPHA 1 Edge)。
- CRC Calculation:Disable,一般不用硬件CRC。
DMA页里,给SPI1_RX添加一个DMA通道,Mode选Normal或Circular。Normal是一次性传输,传完就停;Circular是循环模式,DMA自动重装,适合连续采集。Priority设Medium或High。注意F103的DMA通道是固定映射的,SPI1_RX固定用DMA1_Channel2,SPI1_TX固定用DMA1_Channel3,CubeMX会自动分配,你不要手动改。
中断页里,把SPI1全局中断使能,DMA1_Channel2中断也建议使能,这样传输完成或出错时能进回调函数处理。
3.2 时钟分频的计算与速率取舍
很多新手不知道分频系数和实际SCK频率的关系。公式很简单:
SCK频率 = SPI外设时钟 / 分频系数STM32F103的SPI1挂在APB2,主频72MHz,所以:
| 分频系数 | SCK频率 |
|---|---|
| 2 | 36 MHz |
| 4 | 18 MHz |
| 8 | 9 MHz |
| 16 | 4.5 MHz |
| 32 | 2.25 MHz |
| 64 | 1.125 MHz |
| 128 | 562.5 kHz |
| 256 | 281.25 kHz |
SPI2和SPI3挂在APB1,主频36MHz,同样分频系数下频率减半。注意F103手册里SPI的最高SCK是18MHz(PCLK2=72MHz时分频4),分频2得到的36MHz是超规格的,虽然有些人试过能跑,但不保证稳定,量产别这么干。
速率取舍的原则是:先低速跑通,再逐步提高,每提一档都做误码测试。我通常从1MHz左右开始,功能验证通过后翻倍往上试,直到出现误码再退一档。这样你能摸到这块板子的实际稳定上限,而不是盲信手册值。有些便宜的Flash标称支持80MHz,但你的板子走线差、电源不干净,可能到20MHz就开始出错,这是很正常的。
3.3 DMA接收的几种模式与代码骨架
SPI+DMA接收常见有三种用法,我分别说。
第一种:普通DMA接收,一次性读N字节。适合读Flash、读传感器寄存器这种"发命令-读数据"的场景。
uint8_t rxBuf[32]; void SPI_ReadBytes(uint8_t *txBuf, uint8_t *rxBuf, uint16_t len) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(&hspi1, txBuf, rxBuf, len); // 等待传输完成,实际项目里用回调或信号量,别死等 while (hspi1.State != HAL_SPI_STATE_READY); // 等BSY清零再拉高CS,这点很关键 while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_BSY)); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }这里用的是TransmitReceive而不是单独Receive,因为SPI是全双工,你读数据的同时必须发时钟,发送缓冲区里填0xFF占位就行。实测下来TransmitReceive最稳,不容易出问题。
第二种:DMA循环模式连续接收。适合数据流场景,比如SPI接口的ADC连续采样。CubeMX里DMA Mode选Circular,代码里只调一次启动:
HAL_SPI_Receive_DMA(&hspi1, adcBuf, BUF_SIZE);之后DMA会自动把每次接收的数据填进adcBuf,循环覆盖。你可以在半满和全满中断里处理数据,处理速度只要跟得上采样速度就不会丢。注意循环模式下SPI的CS要一直保持低电平,不能每帧拉高,否则从机状态机会乱。
第三种:DMA双缓冲。数据量大又要求实时处理时用。STM32F4/F7/H7的DMA支持双缓冲模式,一块填数据的同时另一块被CPU处理,交替进行,基本不会丢数据。F103的DMA不支持双缓冲,但可以用两个普通通道手动切换模拟,稍微麻烦点。
关于"SPI需要两个DMA吗"这个问题,答案是:全双工收发同时进行时需要TX和RX两个DMA通道;只接收或只发送时一个就够。做SPI Flash读写通常是"发几个字节命令 + 收几个字节数据",可以只用RX DMA配合普通发送,也可以TX/RX都用DMA,看你的数据量。
3.4 一个能直接用的 SPI Flash 读写例程
以W25Q64(或GD25Q128这类兼容芯片)为例,把常用操作串起来。先说几个核心命令:
| 命令 | 码值 | 作用 |
|---|---|---|
| Write Enable | 0x06 | 写操作前必须先发 |
| Read Status Reg1 | 0x05 | 查询BUSY位 |
| Read Data | 0x03 | 读数据 |
| Page Program | 0x02 | 页编程,一页256字节 |
| Sector Erase | 0x20 | 擦除4KB扇区 |
| JEDEC ID | 0x9F | 读厂商和设备ID |
读ID的代码:
uint32_t W25Q_ReadID(void) { uint8_t tx[4] = {0x9F, 0xFF, 0xFF, 0xFF}; uint8_t rx[4] = {0}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 4, 100); while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_BSY)); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return (rx[1] << 16) | (rx[2] << 8) | rx[3]; }正常应该返回0xEF4017之类(EF是Winbond,40是容量标识)。如果返回0x000000或0xFFFFFF,说明时序或接线有问题。
读数据的代码:
void W25Q_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] = 0x03; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive_DMA(&hspi1, buf, len); // 等DMA完成 + BSY清零再拉高CS while (hspi1.State != HAL_SPI_STATE_READY); while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_BSY)); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }写数据要复杂一些,因为Flash写入前必须发Write Enable(0x06),写入后要轮询Status Register的BUSY位(bit0),直到清零才算写完。擦除同理。这里有个新手常犯的错误:写完一个页之后立刻读,结果读出来是旧数据,因为Flash还在内部编程,你需要等BUSY位清零。轮询代码:
void W25Q_WaitBusy(void) { uint8_t tx[2] = {0x05, 0xFF}; uint8_t rx[2]; do { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } while (rx[1] & 0x01); }还有个细节,擦除操作的最小单位通常是4KB扇区,你不能只擦一个字节。要改一个字节,得先把整个扇区读到RAM里,改完再擦除再写回,这叫"读-改-写"。SPI Flash的寿命通常是十万次擦写,频繁读改写要加磨损均衡,否则很快坏块。
4. 常见问题排查实录:波形不对、数据错位、DMA收不全
4.1 高频故障速查表
我把这些年遇到的SPI问题整理成一张表,方便你对照。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读回全0或全FF | MISO没接、从机没供电、CS没拉低 | 万用表量电压、示波器看MISO |
| 数据整体偏移一位 | CPHA配错、采样边沿不对 | 换模式试、查手册时序图 |
| 数据随机错乱 | 时钟太快、走线太长、无地回流 | 降频、缩短线、加地线 |
| 最后一个字节丢失 | 发送完提前拉高CS、BSY没清零 | 等BSY清零再拉CS |
| 偶尔读到旧数据 | Flash还在编程就读取 | 加BUSY轮询 |
| DMA接收卡死 | DMA通道冲突、中断优先级没配 | 查DMA映射表、看NVIC |
| 多从机时互相干扰 | MISO没释放、CS时序重叠 | 每个从机独立CS、加缓冲 |
| 高速下误码率随温度变 | 时序余量不足 | 降频、换芯片批次 |
这张表看着简单,但每一个现象背后都是真金白银的调试时间换来的。比如"数据整体偏移一位"这条,我见过太多人卡在这里。SPI读回来的字节如果一直是0x7F、0xFE、0xFF这种"差一位"的值,八成是CPHA配错。因为CPHA决定了在哪个边沿采样,配错就会导致采样点落在数据跳变的位置,读出来的值就不稳定,有时候差一位,有时候整字节错。
4.2 上拉、电平匹配和PCB布线的坑
关于"SPI需要上拉吗"这个问题,得分情况。普通的SPI从机,比如Flash、屏幕,MISO是推挽输出,CS由主机推挽驱动,通常不需要上拉。但以下几种情况要上拉:
- SD卡/TF卡的SPI模式:SD卡规范建议MISO、MOSI、CS、SCK都加上拉,典型10k到50k。尤其是CS(DAT3)在SPI模式下有特殊用途,上拉保证初始状态是高电平不被误触发。
- 开漏输出的从机或长走线:如果从机MISO是开漏,必须上拉才能输出高电平。
- 多从机共享MISO:为每个从机的MISO加弱上拉,减少某个从机释放不及时导致的毛刺。
- 低速长线:上拉能改善上升沿,但高速时反而会加重负载,所以要权衡。
电平匹配是另一个大坑。3.3V的主机接1.8V的从机,直接连会烧从机;接5V的从机,虽然有些3.3V输出能被5V芯片识别为高电平,但不保证,最好用电平转换芯片或者分压电阻。我见过有人拿3.3V单片机直接连5V的SPI屏,能显示但偶尔花屏,最后查出是MISO回传的高电平不足以被3.3V主机可靠识别,加上拉解决。
布线方面,SPI频率上了10MHz就要当回事。几条经验:SCK和MISO尽量等长,减少偏斜;走线下面要有完整地平面,别跨分割;串接22到33欧的源端电阻能有效抑制过冲和振铃;线长超过10厘米就要考虑降速或者用差分方案。有次我做一块板子,SPI Flash用了飞线连接,9MHz都不稳,改成短排线加地线回绕,18MHz随便跑,走线的影响就是这么直接。
4.3 DMA和中断到底怎么选
"SPI接收用中断还是DMA"这个问题没有标准答案,看数据量和实时性。我的判断标准是:
- 单次通信小于8字节,且事件稀疏:用中断或阻塞发送都行,代码简单,不用折腾DMA。
- 单次通信几十到几百字节:用DMA,能解放CPU,避免中断风暴。
- 持续数据流,比如ADC采样或屏幕刷新:必须用DMA,最好循环模式或双缓冲。
中断接收的问题在于每收一个字节就进一次中断,收到几百字节就是几百次中断,CPU全耗在进出中断上了,还容易因为中断延迟丢数据。DMA接收是硬件把数据搬进内存,收完一整块才中断一次,效率高得多。我做过一个对比测试,同样接收256字节,中断方式CPU占用大概15%,DMA方式不到2%,差距很明显。
但DMA也不是没有坑。第一,DMA传输完成中断里如果处理时间太长,循环模式下可能来不及处理下一块,得用半满中断提前处理。第二,DMA和CPU同时访问同一块内存会有总线仲裁延迟,双缓冲就是为了规避这个。第三,DMA传输结束后SPI的BSY标志不一定立刻清零,直接拉CS会截断数据,务必在回调里等BSY。第四,HAL库的DMA状态机和SPI状态机是分开的,用的时候要同时检查两个State,别只检查一个。
5. SPI的延伸玩法:从Flash到车载、FPGA和机械臂
5.1 SPI Flash和存储类应用
SPI Flash是SPI总线最经典的应用。除了前面讲的W25Q系列,还有GD25、MX25这些兼容芯片,读写命令基本一致,换芯片通常不用改代码,只要确认容量和JEDEC ID。往上层走,你会接触到文件系统(FatFs)、固件在线升级(IAP)、参数存储这些应用。IAP的做法是把升级固件先写进SPI Flash的某个区域,然后Bootloader读出来搬到内部Flash,这个过程全靠SPI跑,速率直接影响升级时间。一个2MB的固件,SPI跑9MHz大概要十几秒,跑到18MHz能压到几秒,用户体验差别很大。
选Flash的时候除了容量,还要看几个参数:页大小(通常256字节)、扇区大小(通常4KB)、擦除时间(扇区擦除典型几十毫秒到几百毫秒)、擦写寿命(通常十万次)、数据保持年限(通常20年)。做频繁写入的应用,比如数据记录仪,一定要考虑磨损均衡,否则某个扇区先写坏,整个芯片都受影响。我见过一个项目因为每秒往同一个扇区写一次,不到一周就把那块区域写废了,最后改成环形缓冲加均衡算法才解决。
5.2 FPGA和嵌入式SoC上的SPI
SPI在FPGA上通常是作为IP核或者软核实现的。Xilinx、Intel这些厂商的FPGA都提供SPI Master/Slave的IP,配置好时钟分频、数据位宽、CPOL/CPHA就能用。FPGA做SPI主机的好处是时钟精度高、时序可控,而且可以并行挂多个SPI从机,用状态机轮询。做高速采集的时候,FPGA用SPI读ADC,数据通过FIFO进DDR,再由总线传给处理器,这是很典型的架构。
这里顺便说下总线的层次。一颗SoC里,处理器核通过AXI或AHB这类高速总线连到DDR和高速外设,低速外设比如SPI、I2C、UART挂在APB这样的低速总线上。APB总线是为低功耗、低带宽外设设计的,SPI通常就挂在APB下面。你在配置SPI时钟的时候,本质上是在改APB分频和SPI分频两级,理解了这个层次,时钟计算就不会晕。香橙派Zero3、树莓派这类开发板上的SPI接口,其实也是处理器内部SPI控制器引出来的,用的时候配置好设备树和时钟就行。
关于"FPGA是实现CAN总线"这个话题,SPI和CAN经常在同一个系统里配合:CAN负责设备间通信,SPI负责本地外设扩展。有些方案用一颗带SPI接口的CAN控制器,比如MCP2515,MCU通过SPI读写CAN控制器的寄存器来实现CAN收发。这种方式比自己用MCU内部CAN外设更灵活,尤其适合那些没内置CAN的单片机。MCP2515的SPI时序要求不高,几MHz就能跑,是入门CAN总线的经典方案。
5.3 总线舵机、车载传感器和其它有意思的场景
总线舵机和机械臂这几年挺火。传统舵机是PWM控制,一个舵机一根信号线,机械臂上有六个关节就是六根线,走线乱还容易受干扰。总线舵机把控制改成了串行总线,常见的是TTL半双工的UART,也有用SPI或CAN的版本。好处是一根线串起十几个舵机,每个舵机有独立ID,指令里带ID寻址。SPI版本的总线舵机理论上速率更高、延迟更低,适合需要高频刷新的机械臂控制。做机械臂的时候,关节角度控制要求实时性,如果走SPI总线,建议用DMA循环发送指令,避免CPU在发送上卡时间。
车载场景里SPI也很常见,但通常是短距离的板内通信。比如摄像头模块的配置接口、EEPROM参数存储、传感器数据读取,很多都走SPI。车载对可靠性的要求高,所以SPI信号往往要加ESD保护、滤波电容,PCB要走差分或者包地。车载CAN总线负责的是设备之间的通信,跟SPI分工明确:CAN管"网上通信",SPI管"板内配置"。车载总线工程师需要懂的东西很多,从物理层的线束、终端电阻,到数据链路层的帧格式、错误帧,再到应用层的诊断协议,但SPI这种板级总线也是基本功,因为很多模块内部都用它。
还有个场景是ESP8266/ESP32这类WiFi模块。这类模块本身通常用UART跟主控通信,但有些方案会用SPI接口的WiFi芯片,比如把主控和WiFi芯片做成SPI从机+主机的组合,主控通过SPI发AT命令或者数据帧。不过要注意,很多WiFi模组不支持直接挂在SPI上作为从机,因为它们内部已经是完整的SoC,SPI接口的用途有限。真要接,得看模组的规格书,别想当然。至于"ESP8266能不能连SPI接口芯片",答案是能——ESP8266本身有硬件SPI,可以作为主机去读SPI Flash、SPI传感器,做物联网节点很常见。
FPGA和MCU配合、总线舵机、车载传感器,这些场景看着五花八门,底层其实都是同一个SPI环形移位交换模型在起作用。你把SCK、MOSI、MISO、CS这四根线的时序吃透,把模式配对上,剩下的就是各家的命令格式和寄存器定义,查手册就能搞定。
最后分享一个我自己调试SPI的小习惯:遇到问题先抓波形,别先改代码。我见过太多人一读不到数据就开始瞎改代码,改了半天发现是线没插好。逻辑分析仪现在很便宜,几百块就能买一个能解SPI协议的,抓一次波形,时钟有没有、数据对不对、CS时序合不合规,一眼就看出来了。我个人的经验是,SPI问题里大概七成是硬件原因(接线、供电、走线、上拉),两成是模式或时序配置,只有一成是真的代码逻辑问题。先排除硬件,能省你很多时间。还有一点,初调一定要降速,别一上来就顶格跑,等低速跑通、波形漂亮了再往上提,稳扎稳打比什么都快。