SPI协议从入门到精通:时序、模式、多从机连接与实战调试
2026/8/27 4:56:15 网站建设 项目流程

1. 项目概述:从“未完”到“通透”的SPI协议之旅

“SPI协议理解(未完)”——这个标题我太熟悉了,它像极了我们每个嵌入式工程师、硬件开发者或FPGA爱好者在学习路上的一个路标。我们可能在某篇笔记、某个项目文件夹里,都留下过这样一个带着“未完”标记的文件。它意味着我们接触了SPI,知道了它的名字,甚至调通了几个例程,但总觉得隔着一层纱,知其然不知其所以然。今天,我就想和大家一起,把这个“未完”彻底画上句号。SPI,全称Serial Peripheral Interface,串行外设接口,是嵌入式世界里最经典、最常用的短距离通信协议之一。它不像UART那样需要事先约定波特率,也不像I2C那样需要复杂的起始停止信号和应答机制。SPI以其简单、高速、全双工的特性,牢牢占据着Flash存储、传感器、显示屏驱动等场景的核心地位。但它的“简单”往往是一种假象,四线制还是六线制?模式0和模式3到底差在哪?硬件片选和软件片选怎么选?这些问题不搞清楚,调程序时就免不了要抓耳挠腮。这篇文章,就是要把SPI从概念到波形,从配置到调试,掰开了、揉碎了讲清楚。无论你是正在用STM32、ESP32还是国产的HC32、国民技术芯片,无论你是玩标准库、HAL库还是准备用Verilog手撕一个SPI从机,这里的内容都能帮你构建一个清晰、扎实的SPI知识框架,让你下次见到SPI时,心里有的是底气,而不是一个“未完”的标签。

2. SPI协议核心思想与拓扑结构拆解

2.1 “主从分明”与“全双工高速”的设计哲学

要理解SPI,首先要忘掉UART的“对等”和I2C的“线与”。SPI从诞生起就带着鲜明的“主从”烙印,而且是一个绝对强势的主设备带着一个或多个从设备玩。主设备完全掌控通信的节奏,它提供时钟(SCLK),它决定何时发起通信(通过片选SS/CS),从设备只能被动地响应。这种设计带来的最大好处就是简单和高速。因为不需要像I2C那样在总线上进行仲裁和应答,时钟可以拉得很高,只要你的硬件布线能承受,几十MHz甚至上百MHz的通信速率在SPI上很常见。这就是为什么大容量的SPI Flash、高速的ADC芯片都喜欢用SPI接口。

另一个核心是“全双工”。SPI在物理上就有两条数据线:MOSI(Master Out Slave In)和MISO(Master In Slave Out)。这意味着主设备在向外发送数据的同时,也在从从设备那里接收数据。这个特性非常巧妙,你发一个命令字节(比如读Flash的指令0x03),从设备在接收到这个字节的同时,就可以开始准备返回数据,主设备在发送后续地址字节时,就能同步收到从设备返回的数据。这种“边发边收”的模式极大地提高了总线利用率。但这里有个关键点需要理解:全双工不代表你发什么和收什么一定有逻辑关联。很多时候,主设备只是为了提供时钟而发送数据(称为“哑元”,Dummy Byte),真正目的是为了把从设备的数据“挤”出来。理解这一点,对后续分析时序和编写驱动至关重要。

2.2 四线制基础与六线制(含Ready/Busy)扩展

我们最常说的是四线制SPI,也就是SCLK, MOSI, MISO, SS这四根线。这是SPI的基石。

  • SCLK(Serial Clock):时钟线,由主设备产生。一切数据的移入移出都以此为基准。
  • MOSI(Master Out Slave In):主设备数据输出,从设备数据输入。
  • MISO(Master In Slave Out):主设备数据输入,从设备数据输出。这里有个易错点:当总线上只有一个从设备时,MISO就是从设备的数据输出线;但当有多个从设备时,所有从设备的MISO引脚不能直接连在一起(除非是三态输出),通常需要主设备为每个从设备分配独立的MISO线,或者使用IO扩展芯片。这与I2C的开漏输出有本质区别。
  • SS/CS(Slave Select / Chip Select):片选线,低电平有效。这是主设备“点名”从设备的方式。主设备将某条SS线拉低,对应的从设备就被唤醒并准备通信;拉高后,从设备通常应忽略总线状态,进入高阻态。

那么“六线制SPI”或者“SPI Ready”又是什么?这常见于一些高速或需要流控制的设备,比如某些型号的SPI Flash在执行页编程(Page Program)或扇区擦除(Sector Erase)时,内部需要时间,这时它会通过一根额外的IO口来告知主设备“我忙完了”。这根线可能叫/HOLD/WP(在Quad SPI中复用)、或者专门的READY/BUSY引脚。

  • HOLD引脚:在一些标准的SPI Flash上,/HOLD引脚的作用是暂停当前传输。主设备拉低/HOLD,从设备会在完成当前字节传输后暂停,并保持其内部状态,直到/HOLD变高。这用于主设备需要处理更高优先级中断时。但注意,有些Flash的HOLD功能是默认禁用的,需要通过状态寄存器配置。
  • Ready/Busy引脚:这是一根专用于流控制的输出引脚。从设备在执行内部耗时操作时,将此引脚拉低(Busy),操作完成后拉高(Ready)。主设备可以通过查询此引脚状态,而不是不断发送读状态寄存器命令(节省了总线流量和功耗),来获知从设备是否准备就绪。这就是“六线制”的常见来源:SCLK, MOSI, MISO, CS, HOLD, WP。实际上,在Quad SPI(QSPI)中,IO0-IO3这四根数据线可能会被复用为这些控制功能,模式更为复杂。

实操心得:拿到一个新的SPI器件,第一件事就是看数据手册的引脚定义和时序图。一定要分清哪些是必需的(四根基础线),哪些是可选的或复用的(如HOLD, WP)。对于有Ready/Busy引脚的设备,强烈建议使用此方式进行轮询,这比通过SPI总线查询状态寄存器更可靠、更高效,尤其是在多任务系统中,可以避免不必要的总线占用。

2.3 多从设备连接方案:独立片选与菊花链

如何连接多个SPI从设备?主要有两种拓扑。

方案一:独立片选(最常用)这是最直观的方式。主设备的MOSI、MISO、SCLK并联到所有从设备的对应引脚。然后,主设备为每一个从设备提供一个独立的SS引脚。当主设备需要与某个从设备通信时,仅将该从设备的SS线拉低,其他保持高电平。这种方式的优点是逻辑简单,每个从设备完全独立,通信时序清晰。缺点是占用主设备IO口较多,每个从设备都需要一个独立的SS线。

方案二:菊花链这种方式将所有从设备的MISO和MOSI首尾相连,形成一个环。只有第一个从设备的MOSI连接主设备MOSI,最后一个从设备的MISO连接主设备MISO。所有从设备共享SCLK和一个公共的SS线。当主设备拉低SS并发出N个时钟周期时,数据会从一个设备“流”到下一个设备。例如,主设备发送三个字节[A, B, C],对于三个从设备的链,最终第一个从设备收到A,第二个收到B,第三个收到C;同时,主设备会依次收到第三个、第二个、第一个从设备发出的数据。这种方式节省IO口(仅需一个SS),但软件逻辑复杂,所有从设备必须同时参与传输,且对从设备的兼容性有要求(必须支持数据在内部移位寄存器中传递),实际应用较少,多见于特定的移位寄存器或DAC芯片组。

注意事项:在独立片选方案中,务必确保在任何时刻,只有一个从设备的MISO输出是有效的(即其SS为低)。其他SS为高的从设备,其MISO引脚必须处于高阻态。如果某个从设备不支持MISO高阻态,就需要在外面加三态缓冲器,否则会导致总线冲突,烧毁IO口。

3. SPI通信时序的魔鬼细节

3.1 时钟极性(CPOL)与时钟相位(CPHA)的四种模式

这是SPI入门路上最大的“拦路虎”,也是导致通信失败最常见的原因之一。CPOL和CPHA这两个参数,共同定义了数据在时钟的哪个边沿被采样(捕获),以及在哪个边沿被改变(输出)。

  • 时钟极性 CPOL:定义SCLK在空闲状态时的电平。
    • CPOL=0:SCLK空闲时为低电平。
    • CPOL=1:SCLK空闲时为高电平。
  • 时钟相位 CPHA:定义数据采样的边沿。
    • CPHA=0:数据在第一个时钟边沿(即SCLK从空闲状态第一次跳变时)被采样。对于CPOL=0,第一个跳变是上升沿;对于CPOL=1,第一个跳变是下降沿。
    • CPHA=1:数据在第二个时钟边沿(即SCLK从空闲状态第一次跳变后,再次跳变回来时)被采样。

两两组合,就形成了SPI的四种模式(Mode 0-3)。我强烈建议不要死记硬背,而是掌握一个万能判断法则,并学会看时序图。

万能法则

  1. 先看空闲电平(CPOL):确定SCLK在CS有效(拉低)前和无效(拉高)后的状态。
  2. 找到数据采样边沿(根据CPHA)
    • 如果CPHA=0,数据在第一个边沿被采样。那么数据必须在第一个边沿到来之前就已经稳定在数据线上。因此,数据输出(改变)发生在第一个边沿的相反边沿(或者更早)。
    • 如果CPHA=1,数据在第二个边沿被采样。那么数据在第一个边沿时改变,在第二个边沿时被采样。

模式详解与助记

  • Mode 0 (CPOL=0, CPHA=0):最常用的模式。空闲低电平,在第一个上升沿采样。所以数据在下降沿(或更早)改变,在上升沿被捕获。波形看起来“先有数据,后有上升沿”
  • Mode 3 (CPOL=1, CPHA=1):另一个常用模式。空闲高电平,在第一个下降沿采样。所以数据在上升沿改变,在下降沿被捕获。波形看起来“先有数据,后有下降沿”。 你会发现,Mode 0和Mode 3的波形在视觉上很相似,都是“数据线先变化,然后采样时钟边沿才到来”,只是电平高低相反。很多SPI Flash同时支持这两种模式。
  • Mode 1 (CPOL=0, CPHA=1)Mode 2 (CPOL=1, CPHA=0):相对少用。它们的特征是采样时钟边沿先到来,数据才改变。例如Mode 1,空闲低电平,在第二个边沿(下降沿)采样,数据在第一个边沿(上升沿)改变。波形看起来是“先有上升沿,数据才变化”。

避坑指南:如何为你的设备选择正确的模式?一切以从设备的数据手册为准!手册的时序图部分一定会标明。通常,SPI Flash常用Mode 0和Mode 3,一些传感器可能用Mode 1或2。在STM32的HAL库中,配置为SPI_MODE_0等即可。一个快速验证方法是:如果配置错了模式,你可能能收到数据,但全是0xFF或0x00,或者错位。用逻辑分析仪抓取CS、SCLK、MOSI、MISO四根线的波形,对照数据手册的时序图,是排查此类问题最直接的方法。

3.2 数据帧格式:MSB先行与LSB先行

SPI协议本身没有规定一个字节内,是先传输最高位(MSB First)还是最低位(LSB First)。这同样由设备决定。绝大多数设备默认是MSB First,因为这与我们的书写习惯(从左到右)一致。但有些设备,特别是某些老式或特定应用的芯片,可能会是LSB First。

在配置主设备控制器时(如STM32的SPI_CR1寄存器中的LSBFIRST位,或HAL库的FirstBit参数),必须与从设备保持一致。否则,你发送的0x01(二进制0000 0001,MSB First发送是0000 0001,LSB First发送就变成了1000 0000即0x80),从设备会完全误解。

3.3 片选信号的时序学问

片选CS的操控,分为硬件片选(Hardware NSS)和软件片选(Software NSS)。

  • 硬件片选:主设备的SPI外设有一个专用的NSS引脚。当使能硬件主模式时,这个引脚通常会自动管理。在通信开始时自动拉低,在通信结束后(例如发送完指定数据后)自动拉高。这种方式省心,但不够灵活,特别是对于多从机独立片选的情况,通常一个SPI外设只有一个硬件NSS输出。
  • 软件片选:更通用、更灵活的方式。我们将一个普通的GPIO引脚连接到从设备的CS引脚。在代码中,在发起SPI传输前,手动将该GPIO拉低;在传输完成后,手动拉高。这里面的时序细节至关重要
    1. CS建立时间(t_CS_SU):在发出第一个SCLK时钟边沿之前,CS必须已经有效(拉低)一段时间。具体时间看从设备手册。
    2. CS保持时间(t_CS_HD):在最后一个SCLK时钟边沿之后,CS必须继续保持有效一段时间,才能拉高。
    3. CS无效时间(t_CS_IDLE):两次传输之间,CS必须保持高电平(无效)的最小时间。

很多初学者写的软件片选驱动,只是简单地在传输函数前后拉低、拉高CS,忽略了建立和保持时间,在低速时可能侥幸工作,一旦速度提升,必然出错。正确的做法是,在拉低CS后,调用一个微秒级的延时(Delay_us(t_CS_SU));在传输结束、准备拉高CS前,也调用一个延时(Delay_us(t_CS_HD))。这个延时时间可以从纳秒到微秒不等,需要查手册。

实操心得:对于大多数应用,如果时序要求不严,软件片选在拉低和拉高操作之间插入几个空指令(__NOP())也能起到一定的延时作用。但最稳妥的方式,还是用定时器或阻塞延时实现手册要求的时间。使用硬件SPI的DMA传输时尤其要注意,DMA传输完成中断触发后,数据可能还没有完全从移位寄存器发出,此时不能立即拉高CS,需要等待SPI的TXE(发送缓冲区空)和BSY(忙)标志位都清除后再操作。

4. 主流平台SPI外设驱动实战解析

4.1 STM32标准库与HAL库下的SPI配置要点

无论是F1、F4还是H7系列,STM32的SPI外设功能都很强大。我们以最常见的STM32F103为例。

标准库配置流程:

  1. 时钟使能:开启SPI所在总线的时钟(RCC_APB2PeriphClockCmd)和GPIO端口的时钟。
  2. GPIO初始化:将SCK、MOSI、MISO引脚配置为复用推挽输出(AF_PP),速度设为50MHz。CS引脚配置为普通推挽输出。
  3. SPI参数初始化:填充SPI_InitTypeDef结构体。
    SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主模式 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // CPOL=0 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0,注意库中“1Edge”代表第一个边沿 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件片选 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; // 波特率分频 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB先行 SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式,不用则无关 SPI_Init(SPI1, &SPI_InitStructure);
    关键点SPI_CPHA_1Edge对应CPHA=0(第一个边沿采样),SPI_CPHA_2Edge对应CPHA=1。这个命名容易混淆,务必注意。
  4. 使能SPISPI_Cmd(SPI1, ENABLE);

HAL库配置流程(更简洁):使用CubeMX生成代码最为方便。手动配置核心如下:

hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); }

标准库与HAL库的发送接收函数对比:

操作标准库(阻塞式)HAL库(阻塞式)说明
单字节发送SPI_I2S_SendData(SPI1, data);HAL_SPI_Transmit(&hspi1, &data, 1, timeout);HAL库以数组指针形式传入,更通用。
单字节接收recv = SPI_I2S_ReceiveData(SPI1);HAL_SPI_Receive(&hspi1, &recv, 1, timeout);接收前通常需要先发送哑元数据。
全双工收发需先发送,再等待接收。HAL_SPI_TransmitReceive(&hspi1, pTxData, pRxData, Size, timeout);强烈推荐,这是最符合SPI全双工特性的API,效率高。

避坑指南:使用HAL库的HAL_SPI_TransmitReceive时,pTxDatapRxData必须是两个不同的缓冲区,即使你只想接收数据,也需要提供一个有效的发送缓冲区(通常填充0xFF或0x00)。另外,HAL库的阻塞式API的超时参数timeout要合理设置,太短容易因系统繁忙导致返回超时错误,太长则可能卡死。建议根据传输数据量估算时间,并留有余量。

4.2 ESP32-S3 SPI外设的灵活性与高级特性

ESP32-S3的SPI外设(SPI2, SPI3)功能非常灵活,除了支持基本的Master/Slave模式,还支持Quad SPI(QSPI)和Octal SPI,常用于连接外置PSRAM和Flash。这里我们聚焦在普通SPI主模式。

ESP-IDF框架下配置SPI主机:

// 1. 总线配置 spi_bus_config_t buscfg = { .mosi_io_num = GPIO_NUM_11, .miso_io_num = GPIO_NUM_13, .sclk_io_num = GPIO_NUM_12, .quadwp_io_num = -1, // 不使用WP .quadhd_io_num = -1, // 不使用HD .max_transfer_sz = 4092, // 最大传输大小 }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); // 2. 设备配置 spi_device_interface_config_t devcfg = { .command_bits = 0, // 无命令位 .address_bits = 0, // 无地址位 .dummy_bits = 0, // 无哑元位 .mode = 0, // SPI模式0 .clock_speed_hz = 10 * 1000 * 1000, // 10MHz .spics_io_num = GPIO_NUM_10, // CS引脚 .queue_size = 1, // 传输队列大小 }; spi_device_handle_t spi_handle; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle));

ESP32-S3 SPI的强大之处在于其“事务”(Transaction)模型。你创建一个spi_transaction_t结构体来描述一次传输,可以包含命令、地址、哑元和数据多个阶段,非常适合操作SPI Flash等需要复杂指令序列的设备。

// 向SPI Flash发送读命令(0x03)和24位地址,并读取数据 spi_transaction_t t = { .cmd = 0x03, // 读命令 .addr = 0x000000, // 24位地址 .length = 8 * 4, // 总数据长度(位),命令8位+地址24位+数据32位?这里需要分阶段 .rxlength = 8 * 4, // 接收数据长度(位) .tx_buffer = tx_buf, // 发送缓冲区(可包含命令、地址) .rx_buffer = rx_buf, // 接收缓冲区 }; // 更常见的做法是使用flags来分段 spi_transaction_ext_t t = { .base = { .flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY, .cmd = 0x03, .addr = 0x000000, .length = 8 * 4, // 仅数据长度 .rxlength = 8 * 4, .tx_buffer = NULL, .rx_buffer = rx_data, }, .command_bits = 8, .address_bits = 24, .dummy_bits = 0, }; ESP_ERROR_CHECK(spi_device_transmit(spi_handle, (spi_transaction_t*)&t));

这种事务机制将复杂的多阶段SPI通信封装得井井有条,是ESP-IDF SPI驱动的一大亮点。

4.3 国产芯片(如国民技术、HC32)及STC15模拟SPI要点

国产芯片(如国民技术N32、华大HC32): 这些芯片的SPI外设与STM32大同小异,寄存器架构和库函数设计思路都很相似。关键点在于:

  1. 仔细阅读参考手册的SPI章节,关注其特有的寄存器位或功能。
  2. 注意时钟源和分频器计算。有些芯片的SPI时钟来源于PCLK(APB总线时钟),分频配置寄存器可能和STM32不同,需要根据所需SCLK频率正确计算分频系数。
  3. 关注FIFO和中断。很多国产芯片的SPI也配备了深度FIFO,可以减轻CPU负担。配置中断时,清楚TXE(发送缓冲区空)、RXNE(接收缓冲区非空)、错误等中断标志的使能和清除方式。

STC15等51内核单片机模拟SPI: 在没有硬件SPI外设的MCU上,用GPIO口模拟SPI是必备技能。核心就是“位拆解”(bit-banging)。以Mode 0为例,模拟发送一个字节的函数:

void Soft_SPI_SendByte(uint8_t dat) { uint8_t i; SPI_CS = 0; // 拉低片选 Delay_us(1); // CS建立时间 for(i=0; i<8; i++) { SPI_SCK = 0; // 确保时钟起始于低电平(CPOL=0) if(dat & 0x80) // 判断最高位(MSB First) SPI_MOSI = 1; else SPI_MOSI = 0; Delay_us(1); // 数据建立时间 SPI_SCK = 1; // 产生上升沿(CPHA=0,在此上升沿从设备采样) dat <<= 1; // 左移,准备下一位 Delay_us(1); // 时钟高电平保持时间 // 如果CPHA=0,通常在此下降沿主设备可以采样MISO(如果需要接收) // SPI_SCK = 0; // 对于纯发送,可以不加这句,循环开头会拉低 } SPI_SCK = 0; // 循环结束后,时钟回到空闲低电平 Delay_us(1); // CS保持时间 SPI_CS = 1; // 拉高片选 }

模拟SPI的关键

  • 严格保证时序:尤其是数据建立时间(t_SU)和数据保持时间(t_HD)。需要用_nop_()或软件延时来满足从设备手册要求的最小时间。
  • 注意IO口方向:如果要做全双工模拟,在发送位的同时还要读取MISO线,就需要将MISO对应的GPIO配置为输入模式。
  • 速度瓶颈:软件模拟的速度受限于CPU执行指令的速度,通常很难超过1-2MHz。对于高速设备,必须使用硬件SPI。

5. SPI高级应用与深度调试技巧

5.1 SPI Flash读写与文件系统集成

SPI Flash(如W25Q64, GD25Q127等)是SPI协议最典型的应用。操作它远不止简单的收发字节,而是一套完整的命令集。

基本操作流程:

  1. 写使能(WREN, 0x06):在执行任何写操作(编程、擦除)前,必须先发送写使能命令。
  2. 读状态寄存器(RDSR, 0x05):用于查询Flash是否忙(BUSY位)。在写操作后,必须轮询此位直到为0,才能进行下一步操作。
  3. 页编程(PP, 0x02):向指定地址写入数据(通常最多256字节一页)。重要限制:只能将“1”写成“0”,不能将“0”写成“1”。因此写入前,目标扇区必须是已擦除状态(全FF)。
  4. 扇区擦除(SE, 0x20):擦除一个扇区(通常4KB),将其所有位变为“1”。擦除耗时较长(几十到几百毫秒)。
  5. 读数据(READ, 0x03):从指定地址开始读取数据。

与文件系统集成(如LittleFS, SPIFFS): 在RT-Thread、FreeRTOS等系统中,可以通过MTD(Memory Technology Device)层将SPI Flash驱动封装成块设备,然后在其上挂载LittleFS等文件系统。核心是实现read,write,erase,control这几个操作接口。这样做的好处是,你可以像操作普通文件一样(fopen,fwrite,fread)来管理Flash上的数据,文件系统会帮你处理磨损均衡、坏块管理、掉电保护等复杂问题。

实操心得:操作SPI Flash最容易出错的地方是擦除和编程的时序。一定要严格遵守“写使能->擦除/编程->等待BUSY位清除”的流程。在等待BUSY时,建议用READY/BUSY引脚查询(如果有),其次是用读状态寄存器命令轮询,避免用死等延时。另外,跨页写入时,如果数据长度超过一页的剩余空间,需要手动分两次写入,这是很多驱动库的陷阱。

5.2 使用逻辑分析仪与示波器进行SPI调试

当通信不正常时,万用表已经无能为力,逻辑分析仪是你的第一选择。

连接与设置: 将逻辑分析仪的通道分别连接到SCLK、MOSI、MISO、CS(如果需要,还可以接/HOLD/WP)。在逻辑分析仪软件中(如Saleae Logic, DSView),添加一个SPI解码器。

配置解码器

  • 时钟通道:选择SCLK所在的通道。
  • 数据输入通道:选择MOSI(主发)和MISO(主收)通道。
  • 片选通道:选择CS通道,并设置为低电平有效。
  • 位顺序:选择MSB First或LSB First。
  • 时钟模式(CPOL/CPHA):根据你的配置选择。如果不知道,可以尝试四种模式,看哪种模式下解码出的数据是合理的(比如命令字符合预期)。

分析波形

  1. 看CS:CS拉低的时间是否覆盖了整个数据传输过程?CS的建立和保持时间是否足够?
  2. 看SCLK:时钟频率是否符合预期?空闲电平是否正确(CPOL)?
  3. 看数据对齐:在软件设置的模式下,MOSI和MISO的数据变化边沿和采样边沿是否与理论一致?这是判断CPHA是否正确的最直观方法。
  4. 看数据内容:解码出的十六进制数据是否与你发送/预期的命令、地址、数据一致?

示波器的使用: 逻辑分析仪擅长看数字逻辑和协议,而示波器擅长看信号质量。如果通信不稳定(偶尔出错),需要用示波器检查:

  • 信号完整性:SCLK、MOSI等信号线上是否有过冲、振铃?边沿是否陡峭?
  • 噪声与干扰:信号线上是否有毛刺?地线是否干净?
  • 时序裕量:测量数据建立时间(Data Setup Time)和数据保持时间(Data Hold Time)是否满足从设备要求。这需要在SCLK的采样边沿附近,测量数据信号稳定区域的时间。

5.3 常见问题排查速查表

以下表格整理了SPI通信中最常见的问题现象、可能原因及排查思路:

问题现象可能原因排查思路与解决方案
完全收不到数据,或收到全0/全FF1. 片选CS信号错误(未拉低、时序不对)。
2. 时钟模式(CPOL/CPHA)不匹配。
3. 从设备未上电或损坏。
4. MISO线连接错误或从设备未输出。
1. 用逻辑分析仪确认CS波形。
2. 逐一尝试四种SPI模式。
3. 检查从设备电源、复位引脚。
4. 将MISO线暂时接高电平或低电平,看主设备能否收到固定值,以判断主设备接收通路是否正常。
收到数据错位(如0x01变成0x80)位顺序(MSB/LSB)设置错误。检查并修改主从设备的位顺序设置,确保一致。
高速时通信不稳定,低速正常1. 信号完整性差(过冲、振铃)。
2. 时序裕量不足(建立/保持时间)。
3. 软件延时或中断影响。
1. 用示波器检查信号质量,考虑缩短走线、加串阻。
2. 降低时钟频率,或调整软件片选延时。
3. 提高SPI中断优先级,或在传输关键段关闭全局中断。
多从设备时,只能与一个通信多个从设备的MISO线直接并联冲突。检查从设备MISO是否为三态输出,若不是,需改用独立MISO线或加三态缓冲器。确保未选中的从设备CS为高电平。
SPI Flash能读不能写1. 未发送写使能(WREN)命令。
2. 写保护(WP)引脚被拉低。
3. 正在执行内部编程/擦除操作(BUSY位为1)。
1. 确保每次写/擦除前发送0x06。
2. 检查硬件WP引脚电平,或通过状态寄存器检查软件写保护位。
3. 写/擦除后轮询状态寄存器BUSY位,直到为0。
使用DMA传输时数据错乱1. DMA缓冲区内存不对齐或越界。
2. SPI传输未完成就操作了数据缓冲区。
3. DMA和SPI的时钟域不同步。
1. 确保DMA缓冲区地址和长度符合要求(如32位对齐)。
2. 在DMA传输完成回调函数中操作数据,或等待SPIBSY标志清除。
3. 检查总线时钟配置,确保DMA和SPI时钟源稳定。

6. SPI、I2C、UART横向对比与选型指南

在项目初期,面对传感器、存储器等外设,我们常常需要在SPI、I2C、UART之间做选择。下表从多个维度进行了对比:

特性SPII2CUART
通信方式全双工,同步半双工,同步全双工,异步
线数3线或4线(+片选线)2线(SDA, SCL)2线(TX, RX,不含地线)
速度(通常10M+,可达100Mbps)中(标准模式100kbps,快速模式400kbps,高速模式3.4Mbps)低(常用115200bps,最高可达数Mbps但受硬件限制)
寻址方式硬件片选(线选)软件地址(7位/10位)无(点对点)
拓扑结构主从,一对一或一对多(独立片选)多主多从,总线式点对点
协议复杂度简单(无固定数据帧格式,无应答)中等(有起始、停止、应答位)简单(有起始、停止、校验位)
硬件开销主设备IO占用多(每个从设备需独立CS)最少(两根线挂多个设备)中等(一对一连接)
抗干扰能力较强(推挽输出,电压摆幅大)较弱(开漏输出,上升沿依赖上拉电阻)一般(取决于电平标准,如RS-485很强)
典型应用Flash, LCD屏, ADC, 高速传感器温度传感器, EEPROM, RTC, 低速IO扩展调试日志, GPS模块, 蓝牙模组, 与PC通信

选型建议:

  • 追求速度:首选SPI。例如读取大容量Flash,驱动高分辨率显示屏。
  • 节省IO口,设备多:首选I2C。例如在同一个板上连接多个传感器(温湿度、气压等)。
  • 远距离、简单可靠、点对点:首选UART(可转换为RS-485增强抗干扰)。例如工业现场与控制器通信。
  • 需要硬件流控制:考虑UART(带RTS/CTS)或SPI(带Ready/Busy引脚)。
  • 协议灵活性:SPI最灵活,数据帧长度、内容完全由主从设备软件定义。I2C和UART有固定的帧结构。

最后,关于SPI中的“ACK”角色,这通常是I2C的概念。在标准的SPI协议中,没有硬件层面的应答(ACK)机制。其可靠性依赖于稳定的硬件连接和正确的时序。应用层如果需要确认,必须在软件协议中自己定义,例如在发送一帧数据后,等待从设备返回一个特定的确认字节。这种“软件ACK”是构建在SPI物理层之上的应用层协议,不属于SPI标准本身。

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

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

立即咨询