SPI通信协议深度解析:从四种模式到DMA驱动的嵌入式开发实战

发布时间:2026/7/28 9:07:09
SPI通信协议深度解析:从四种模式到DMA驱动的嵌入式开发实战 1. 项目概述从“通信”到“控制”的桥梁如果你刚开始接触嵌入式开发面对UART、I2C、SPI这些名词可能会感到一头雾水。今天我们就来深入聊聊SPI这个在单片机世界里扮演着“高速数据搬运工”角色的片上外设。与UART这种“打电话”式的异步串行通信不同SPI更像是一个“同步对讲”系统主设备一声令下从设备立刻响应数据在时钟的精准节拍下双向流动。它广泛用于连接Flash存储器、LCD屏幕、各类传感器如陀螺仪、加速度计以及无线模块如Wi-Fi、蓝牙芯片的固件加载。对于新手而言理解SPI不仅仅是看懂那四根线SCLK, MOSI, MISO, CS更是理解一种高效、可靠的控制与数据交换思想。掌握了它你就能让手中的单片机与周围丰富的电子世界对话从显示一幅图片到记录一串数据很多功能的大门就此打开。2. SPI协议核心原理与四种模式深度解析SPI协议的精髓在于其简洁性和同步性。它省去了UART复杂的起始位、停止位和波特率协商也规避了I2C的地址寻址和应答机制换来的是简单粗暴的高速度。其通信完全由主设备发起并控制核心信号线包括SCLK (Serial Clock) 由主设备产生的时钟信号是所有数据收发的节拍器。每一个时钟周期就传输一位数据。MOSI (Master Out Slave In) 主设备输出从设备输入的数据线。MISO (Master In Slave Out) 主设备输入从设备输出的数据线。CS/SS (Chip Select / Slave Select) 片选信号通常低电平有效。主设备通过拉低对应从设备的CS线来“选中”它通知其准备通信。这是SPI支持一主多从的关键。通信时主设备先拉低目标从设备的CS线然后产生SCLK时钟。数据在SCLK的每个边沿从MOSI线移出同时在另一个边沿从MISO线采样进来。这里就引出了SPI最核心也最让新手困惑的概念时钟极性(CPOL)与时钟相位(CPHA)它们共同定义了四种工作模式。2.1 时钟极性(CPOL)与相位(CPHA)详解CPOL (Clock Polarity) 定义SCLK时钟线在空闲状态即CS有效但数据传输未开始或已结束时的电平。CPOL0 SCLK空闲时为低电平。CPOL1 SCLK空闲时为高电平。CPHA (Clock Phase) 定义数据在SCLK的哪个边沿被采样捕获。CPHA0 数据在SCLK的第一个边沿即从空闲状态跳变到第一个有效状态的边沿被采样在下一个边沿切换。CPHA1 数据在SCLK的第二个边沿被采样在第一个边沿切换。这两者的组合构成了SPI的四种模式Mode 0, 1, 2, 3。理解它们的关键是看采样边沿。我习惯这样记忆先看CPHA。CPHA0就在第一个边沿采样CPHA1就在第二个边沿采样。然后再根据CPOL确定第一个边沿是上升沿还是下降沿。注意 这是最容易出错的地方不同厂商的数据手册对“模式”的定义是绝对的Mode 0,1,2,3但描述时序时可能用“上升沿采样”或“下降沿采样”。务必以从设备如传感器、Flash芯片数据手册的时序图为准对照确定主设备你的单片机应该配置的CPOL和CPHA。配置错误会导致数据全为0xFF或0x00通信完全失败。2.2 四种模式波形分析与应用场景为了更直观我们结合一个具体的8位数据发送过程例如主设备发送0xAA二进制10101010来分析波形。假设数据在MSB最高位先出。模式0 (CPOL0, CPHA0)空闲状态 SCLK为低电平。第一个边沿 SCLK的第一个边沿是上升沿。因为CPHA0所以在上升沿采样数据。这意味着数据必须在上升沿到来之前就已经在数据线上稳定建立好了。数据变化 数据位在SCLK的下降沿发生变化为下一个上升沿采样做准备。应用 这是最常用的模式很多常见的SPI Flash如W25Q系列、SD卡SPI模式都使用此模式。模式1 (CPOL0, CPHA1)空闲状态 SCLK为低电平。第一个边沿 SCLK的第一个边沿是上升沿。因为CPHA1所以这个上升沿用于切换数据。采样边沿 数据在SCLK的下降沿被采样。应用 一些特定的传感器或老式器件会使用此模式。模式2 (CPOL1, CPHA0)空闲状态 SCLK为高电平。第一个边沿 SCLK的第一个边沿是下降沿。因为CPHA0所以在下降沿采样数据。数据变化 数据位在SCLK的上升沿发生变化。应用 相对少用但一些特定接口的LCD控制器可能采用。模式3 (CPOL1, CPHA1)空闲状态 SCLK为高电平。第一个边沿 SCLK的第一个边沿是下降沿。因为CPHA1所以这个下降沿用于切换数据。采样边沿 数据在SCLK的上升沿被采样。应用 一些RFID读卡器芯片、部分音频编解码器使用此模式。实操心得 当你拿到一个新的SPI设备第一件事就是翻到数据手册的“电气特性”或“接口时序”章节找到类似“SPI Timing Diagram”的图。图上会明确标出数据线SDI/SDO相对于SCLK的建立时间Setup Time和保持时间Hold Time要求而采样边沿就隐含在这些时间要求与SCLK的关系中。对照你的单片机SPI外设支持的模式选择匹配的那个。如果实在不确定可以先用Mode 0尝试因为它是概率上最常用的。3. 硬件连接、片选策略与初始化配置理解了协议接下来就是动手连接。SPI的硬件连接看似简单但细节决定成败。3.1 标准四线制与三线制连接标准四线制全双工 这是最完整的连接方式支持同时收发。主设备SCLK —— 连接所有从设备的SCLK。主设备MOSI —— 连接所有从设备的MOSI或SDI。主设备MISO —— 连接所有从设备的MISO或SDO。主设备GPIO作为CS—— 每个从设备独占一根CS线。切记CS线必须独立不能像SCLK那样共用除非你的从设备有特殊的级联或广播地址功能。三线制半双工/双向 为了节省引脚有些器件支持将MOSI和MISO合并为一条数据线SIO。通信时通过方向控制在同一根线上分时进行发送和接收。这需要主设备的SPI外设支持“单线双向”模式或者用软件模拟方向切换。新手初期建议先用标准的四线制更稳定直观。接线注意事项上拉电阻 对于开漏输出的MISO线有些从设备是开漏通常需要在主设备端加上拉电阻如4.7kΩ-10kΩ到VCC以确保空闲时为高电平。但很多现代MCU的IO口内部已有可配置的上拉电阻可以优先使用。电平匹配 确保主从设备的工作电压一致如都是3.3V。如果不同如主设备3.3V从设备5V则需要使用电平转换芯片如TXB0104或电阻分压网络否则可能损坏器件或通信不稳定。走线长度 在高速SPI如几十MHz下信号线应尽量短并避免靠近高频噪声源以减少信号完整性问题。3.2 硬件片选与软件片选的抉择片选CS的控制是SPI通信稳定性的关键一环。硬件片选NSS 使用单片机SPI外设自带的NSSNegated Slave Select硬件引脚。其优势在于硬件会自动在数据传输开始前拉低NSS结束后拉高时序精准不占用CPU资源。但它通常只能管理一个从设备或者需要复杂的多主模式支持灵活性较差。软件片选 使用普通的GPIO引脚来模拟CS信号。这是最推荐新手使用的方式也是实际项目中最常见的。优势 极其灵活你可以用任意GPIO控制任意数量的从设备。控制权完全在程序手中可以在数据传输间隙插入延时、执行其他操作。操作要点初始化时将CS引脚配置为推挽输出模式并先将其置为高电平无效状态。在发起一次完整的SPI传输序列前手动拉低CS引脚。调用SPI发送/接收函数。在这次传输序列完全结束后确保最后一位数据已经送出再手动拉高CS引脚。关键细节 务必确保CS信号在SCLK空闲期间变化。例如在Mode 0下SCLK空闲为低那么你拉低或拉高CS时必须确保SCLK是处于空闲的低电平状态。违反这个规则是导致通信出错的常见原因。3.3 单片机SPI外设初始化详解以常见的STM32的HAL库为例初始化SPI涉及一系列参数配置。这些参数必须与从设备严格匹配。SPI_HandleTypeDef hspi1; void SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; // 主模式 hspi1.Init.Direction SPI_DIRECTION_2LINES; // 双线全双工 hspi1.Init.DataSize SPI_DATASIZE_8BIT; // 数据位宽也可以是16位 hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0 对应Mode 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选管理强烈推荐 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_64; // 波特率预分频决定SCLK频率 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(); } }参数解读与避坑指南BaudRatePrescaler 这是设置SPI时钟速度的关键。SCLK频率 系统时钟PCLK / 预分频值。例如PCLK为72MHz预分频64则SCLK约为1.125MHz。速度并非越快越好必须小于等于从设备支持的最大SCLK频率见其数据手册。过高的速度会导致数据采样错误。FirstBit 绝大多数器件是MSB First但极少数如某些音频芯片可能是LSB First。务必核对数据手册。DataSize 8位是最常见的但有些16位精度的ADC如你提到的ADS1256或特定显示器控制器可能需要16位传输格式。此时发送和接收的数据缓冲区类型应为uint16_t。NSS 设为SPI_NSS_SOFT后硬件NSS引脚就可以解放为普通GPIO或他用片选完全由我们手动控制GPIO实现。4. 基础轮询与中断驱动编程实战初始化完成后就可以进行数据传输了。SPI的驱动方式主要有三种轮询、中断和DMA。我们从最简单的轮询开始。4.1 轮询方式发送与接收轮询就是CPU“死等”SPI传输完成代码简单但效率低在传输过程中CPU无法处理其他任务。单次发送接收全双工uint8_t tx_data 0xA5; uint8_t rx_data 0; HAL_StatusTypeDef status; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 拉低CS选中器件 status HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 1, 100); // 超时100ms HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高CS释放器件 if (status ! HAL_OK) { // 处理错误 }在这条语句中tx_data被发送出去的同时rx_data也接收到了从设备返回的数据。这对于需要发送命令并读取返回值的器件如读传感器寄存器是标准操作。仅发送或仅接收 有些操作可能只需要发送命令如写寄存器或只需要读取数据流。可以使用HAL_SPI_Transmit或HAL_SPI_Receive。但要注意SPI是全双工硬件即使你只想接收也必须提供一个发送缓冲区内容通常为哑元0xFF或0x00因为时钟是由主设备发送数据的行为产生的。// 只接收3个字节 uint8_t tx_dummy[3] {0xFF, 0xFF, 0xFF}; // 发送哑元数据以产生时钟 uint8_t rx_buffer[3]; HAL_SPI_TransmitReceive(hspi1, tx_dummy, rx_buffer, 3, 100);4.2 中断方式驱动中断方式允许CPU在发起传输后就去处理其他任务等SPI传输完成产生中断时再回来处理数据。这提高了系统效率。步骤在初始化后使能SPI全局中断__HAL_SPI_ENABLE_IT(hspi1, SPI_IT_TXE | SPI_IT_RXNE);(对于HAL库通常在HAL_SPI_Transmit_IT等函数内部处理)。实现SPI中断服务函数对于STM32 CubeMX生成的代码你需要在stm32fxx_it.c中找到SPIx_IRQHandler并调用HAL_SPI_IRQHandler(hspi1);。使用中断式传输函数并编写传输完成回调函数。// 启动中断接收 uint8_t rx_buf[10]; if (HAL_SPI_Receive_IT(hspi1, rx_buf, 10) ! HAL_OK) { // 启动失败处理 } // ... CPU可以在这里执行其他代码 ... // 传输完成回调函数需要用户重写 void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // SPI1传输完成处理rx_buf中的数据 // 注意此时CS可能还未拉高回调函数里拉高CS是安全的常见做法 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } }中断方式的优缺点优点 CPU利用率高于轮询。缺点 每个字节或每个数据帧传输完成都会产生中断在高速连续传输大量数据时如刷新LCD中断频率会非常高导致大量CPU时间消耗在中断进出上反而可能成为瓶颈。此时就需要DMA。5. 高级应用DMA驱动与性能优化DMA直接存储器访问是解放CPU的终极武器。它可以在不占用CPU的情况下在外设如SPI和内存之间搬运数据。对于像驱动SPI LCD如ILI9341、ILI9488或高速读取ADC如ADS1256这类需要连续传输大量数据的场景DMA几乎是必选项。5.1 SPI DMA传输配置与启动配置DMA相对复杂但利用STM32 CubeMX工具可以图形化完成大部分配置。核心是配置DMA通道的源地址内存或外设数据寄存器地址、目标地址、数据宽度、传输模式是否循环等。CubeMX配置要点在SPI配置页将Mode设置为“Transmit and Receive”如果全双工。在DMA Settings选项卡点击Add添加DMA请求。对于SPI_TX方向设为Memory To Peripheral对于SPI_RX方向设为Peripheral To Memory。模式Mode通常选择Normal传输指定数量后停止如果是连续刷新屏幕可能需要Circular循环模式。数据宽度Data Width要与SPI的DataSize匹配Byte或Half Word。代码启动DMA传输uint8_t lcd_buffer[320]; // 准备一行LCD像素数据 // 先拉低CS HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); // 启动SPI通过DMA发送数据 if (HAL_SPI_Transmit_DMA(hspi1, lcd_buffer, 320) ! HAL_OK) { // 错误处理 } // 此时CPU立即被释放可以去做其他事情比如准备下一帧数据 // DMA传输完成回调函数 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 一行数据发送完毕拉高CS或准备发送下一行 HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); // 通知主程序可以进行后续操作 } }5.2 DMA使用中的典型问题与解决方案你提到的“stm32h750 dma 驱动 spi lcd 问题”非常典型DMA使用不当会导致各种奇怪现象。问题1数据显示错乱、花屏可能原因1数据缓冲区被覆盖。DMA在传输过程中CPU不能修改正在被DMA使用的内存缓冲区。如果你在DMA传输未完成时就更新了lcd_buffer的内容屏幕上就会显示新旧数据混合的错乱图像。解决方案 使用双缓冲区Ping-Pong Buffer。准备两个缓冲区A和B。当DMA正在传输缓冲区A的数据时CPU填充缓冲区B。A传输完成后在回调函数里立即切换让DMA传输B同时CPU去填充A。如此循环。可能原因2SPI时钟速度与DMA/内存速度不匹配。当SPI波特率设置过高而DMA从内存取数据的速度跟不上SPI发送的速度会导致SPI underrun错误发送寄存器空了从而插入错误数据。解决方案 降低SPI的波特率预分频系数。或者检查系统时钟配置确保DMA和SPI所在的总线如APB2时钟足够高。对于STM32H750还可以尝试启用Cache并配置为Write-through或Write-back模式以加速内存访问。问题2DMA传输无法启动或只传输一部分可能原因DMA或SPI外设未正确初始化或使能顺序有误。解决方案 遵循正确的初始化顺序先初始化GPIO再初始化DMA最后初始化SPI。确保在启动传输前SPI和DMA都处于就绪状态。检查CubeMX生成的DMA初始化代码是否被正确调用。问题3SPI DMA发送完成后CS信号时机不对现象 CS在最后一个数据位还没稳定送出时就拉高了导致从设备丢失最后一位数据。解决方案 不要在HAL_SPI_Transmit_DMA函数返回后立刻拉高CS。应该在DMA传输完成回调函数HAL_SPI_TxCpltCallback中拉高CS。此时硬件保证所有数据包括最后一位都已从移位寄存器中发出。6. 常见器件驱动要点与调试技巧掌握了通用方法我们来看看如何驱动一些具体的热门器件并分享调试中的“杀手锏”。6.1 驱动SPI Flash如W25Q128SPI Flash是学习SPI的经典外设。其操作有固定的命令集。读数据 发送命令0x03Read Data接着发送24位地址然后就可以连续读取数据。时钟不能停直到CS拉高。写使能与写状态寄存器 写入前必须先发0x06Write Enable命令。写入后需要轮询状态寄存器Read Status Register0x05的BUSY位等待内部编程完成。页编程与扇区擦除 写入有页边界限制通常256字节写入前必须确保目标区域已被擦除Erase Sector0x20。擦除以扇区通常4KB为单位。实操心得 初始化SPI Flash后可以尝试读取其制造商和设备ID命令0x9F这是一个快速验证SPI底层通信是否正常的有效方法。如果ID读不对后面的所有操作都无从谈起。6.2 驱动SPI LCD如ILI9341, ILI9488驱动LCD是综合应用涉及GPIO控制RESET、DC等、SPI发送大量像素数据。关键引脚 除了SPI线通常还有DC数据/命令选择、RESET复位、BL背光。DC线至关重要发送命令时拉低发送像素数据时拉高。初始化序列 LCD上电后需要一段复杂的初始化命令序列来配置驱动芯片。这些序列通常由厂商提供直接复制到代码中通过SPI发送即可。注意命令和参数都要在DC0时发送。刷屏优化 这是性能关键。设置好窗口地址Set Column/Page Address命令后就可以连续写入像素数据。务必使用DMA。将一帧或多行像素数据放入数组然后用SPI DMA发送。同时可以将设置窗口地址的命令和像素数据组合在一次CS低电平周期内发送减少CS切换开销。6.3 调试技巧逻辑分析仪是“第二双眼”当通信失败时仅靠打印调试信息往往力不从心。一个几十元的USB逻辑分析仪配合Sigrok/PulseView软件是嵌入式开发的必备神器。连接 将探针连接到SCLK、MOSI、MISO、CS线上。捕获 设置合适的采样率至少为SPI时钟频率的4倍以上触发条件设为CS下降沿。分析 软件可以自动解码SPI协议。你可以清晰地看到CS、SCLK的时序是否符合模式要求。MOSI上发送的数据是否正确命令、地址、数据。MISO上返回的数据是什么。数据位是在哪个时钟边沿采样和变化的。 通过对比抓取到的波形和数据手册的时序图绝大部分通信问题都能一目了然地定位是硬件连接错误、模式配置错误还是数据内容错误。7. 常见问题排查速查表下表汇总了SPI开发中最常遇到的问题和排查思路问题现象可能原因排查步骤通信完全无反应读回数据全是0xFF或0x001. 硬件连接错误线接反、虚焊2. 片选(CS)信号错误未拉低、时序不对3. SPI工作模式(CPOL/CPHA)配置错误4. 从设备未上电或损坏1. 用万用表检查通断和电压。2. 用逻辑分析仪看CS和SCLK波形。3. 核对主从设备模式。4. 测量从设备VCC电压。能读到数据但数据错误/不稳定1. SPI时钟速度过快超过从设备极限2. 电源噪声或地线干扰大3. 信号线过长边沿退化4. 未正确等待从设备忙状态1. 逐步降低SPI波特率测试。2. 检查电源滤波确保地线连接良好。3. 缩短走线或串联小电阻22-100Ω阻尼反射。4. 发送操作命令后增加延时或轮询状态。DMA传输数据错位或丢失1. 内存缓冲区在传输中被篡改2. DMA/SPI数据位宽不匹配8位 vs 16位3. DMA传输未完成就操作了CS4. 系统Cache导致数据一致性問題高级MCU1. 使用双缓冲机制。2. 检查CubeMX中DMA的Data Width设置。3. 在DMA完成回调中操作CS。4. 对DMA缓冲区进行Cache清洗或无效化操作。多从设备互相干扰1. 多个从设备MISO线直接并联未隔离2. 切换从设备时CS切换间隙SCKL有毛刺1. 确保同一时刻只有一个从设备的MISO被使能通常CS无效时MISO高阻。2. 在切换CS前确保SPI总线处于空闲状态完成上次传输。初始化成功但后续操作失败1. 从设备进入睡眠或保护模式2. 未正确处理从设备的写保护位3. 时序要求未满足如命令间需延时1. 查阅手册发送唤醒命令。2. 发送写使能命令如Flash。3. 在关键命令后增加HAL_Delay()或检查状态位。从理解四根线背后的同步哲学到搞定四种时序模式从软件轮询的简单直接到驾驭DMA的高效澎湃最后用逻辑分析仪这把利刃斩断所有调试荆棘——这条路我走过坑我也踩过。SPI就像一把精准的螺丝刀当你熟悉了它的握法和力道就能在嵌入式世界里拧紧一个又一个功能的螺丝。别怕手册上那些复杂的时序图把它们当成地图按照我们今天梳理的路径一步步走点亮第一个SPI设备屏幕的那一刻你会感受到最直接的成就感。剩下的就是大胆地去连接更广阔的世界了。