嵌入式软件:SPI(专栏长期持续更新)
SPI是一种同步串行通信协议,由摩托罗拉公司提出,主打高速全双工数据传输,广泛应用于嵌入式系统中短距离外设通信(如Flash、OLED屏、ADC、传感器等)。其核心特点是通过时钟同步数据,支持主从架构,可扩展多从机,无需地址帧(通过片选信号区分从机),协议简单、传输速率高(通常可达MHz级)。参数定义CPOL(时钟极性)空闲状态时SCLK的电平:- CPOL=0:空闲时SCLK为低电平;- CPO
·
SPI协议实战宝典:原理、时序、软硬实现与故障排查(STM32全解析)
聚焦嵌入式串行通信协议落地与问题解决
一、协议核心认知:SPI是什么?适合做什么?
SPI是摩托罗拉推出的同步串行通信协议,核心定位「短距离、高速全双工」外设通信,因协议简单、速率高、硬件实现成本低,成为嵌入式高速通信首选。
核心特征与选型逻辑
| 核心优势 | 典型应用场景 | 局限性 |
|---|---|---|
| 高速全双工(MHz级) | SPI Flash、OLED、高速ADC/DAC | 短距离(<10cm,高速下<5cm) |
| 主从架构、协议轻量化 | 传感器、FPGA通信 | 无地址/仲裁,多从机需额外SS线 |
| 硬件实现简单、时序可控 | 电机驱动、音频Codec、SD卡(SPI模式) | 4线制,不适合远距离组网 |
选型结论:短距离+高速+全双工选SPI;长距离+少布线+多设备组网选IIC。
二、硬件层:总线组成与连接规范(物理层避坑核心)
1. 核心信号线定义(4线制标准)
| 信号线 | 作用说明 | 主从方向 | 硬件要求 |
|---|---|---|---|
| SCLK | 主机生成同步时钟,控制传输节奏 | 主机→从机 | 推挽输出,高速需100~150Ω终端电阻 |
| MOSI | 主机发→从机收数据 | 主机→从机 | 推挽输出,与SCLK等长布线 |
| MISO | 从机发→主机收数据 | 从机→主机 | 上拉输入,避免浮空 |
| SS(CS) | 低电平选中从机,每个从机独占1根IO | 主机→从机 | 默认高电平,禁止多从机同时拉低 |
2. 3线制简化配置
- 适用:单向传输(仅写OLED/仅读ADC);
- 连接:MOSI/MISO复用为SDIO,时序区分读写;
- 缺点:失去全双工,速率略降。
3. 硬件连接黄金规范(90%物理层问题根源)
- 同名引脚直连(MOSI/MISO接反是高频错误);
- 每个从机独占SS线,未选中时SS必须高电平;
- 布线:≤10cm(高速≤5cm),SCLK/MOSI/MISO等长,双绞屏蔽线;
- 高速传输(≥5Mbps):总线末端(靠近从机侧)加120Ω终端电阻;
- 电平匹配(3.3V/5V),外设电源端并0.1μF去耦电容。
三、时序层:CPOL/CPHA与4种工作模式(通信兼容核心)
主从设备CPOL/CPHA必须完全匹配,否则必乱码。
1. 核心时序参数
| 参数 | 含义 |
|---|---|
| CPOL | 空闲电平:0=低,1=高 |
| CPHA | 采样边沿:0=第一个跳变沿,1=第二个跳变沿 |
| 传输顺序 | 99%外设默认高位优先(MSB First) |
| 波特率 | 主机配置,需≤外设最大速率(如W25Q64支持80MHz,OLED≤5MHz) |
2. 4种工作模式(实战速查)
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 | 典型外设 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | SCLK上升沿 | OLED、多数ADC/DAC |
| 1 | 0 | 1 | 低 | SCLK下降沿 | 少数专用传感器 |
| 2 | 1 | 0 | 高 | SCLK下降沿 | 工业级Flash |
| 3 | 1 | 1 | 高 | SCLK上升沿 | W25Q系列Flash、SD卡 |
实战技巧:外设手册查“SPI Mode”/“CPOL/CPHA”,主机严格匹配即可。
四、传输逻辑:完整通信流程(全双工本质)
SPI全双工:主机发1字节必收1字节(无有效数据则收dummy)。
1. 单字节传输(模式0为例)
- 主机拉低SS选中从机;
- 主机生成SCLK,下降沿更新MOSI数据,上升沿从机采样MOSI、主机采样MISO;
- 8位传输完成后,主机拉高SS释放从机。
2. 读写操作实现
| 操作类型 | 实现逻辑 |
|---|---|
| 写操作 | 主机发有效数据,忽略MISO返回值 |
| 读操作 | 主机发0x00(dummy)触发时钟,从机MISO返回有效数据 |
| 读写混合 | 先发送命令/地址(写),再发dummy触发时钟,同时读取MISO数据(读) |
五、实现方案:软件SPI vs 硬件SPI(STM32F103实战代码)
1. 选型对比
| 维度 | 软件SPI(GPIO模拟) | 硬件SPI(MCU内置) |
|---|---|---|
| 速率 | <1Mbps(受CPU延时限制) | MHz级(F103 SPI1最高9MHz) |
| CPU占用 | 高(循环控GPIO) | 低(支持DMA) |
| 灵活性 | 高(适配非标时序) | 一般(仅支持标准模式) |
| 适用场景 | 低速、非标外设、无硬件SPI的MCU | 高速、批量传输(Flash/OLED) |
2. 软件SPI实现(模式0,高移植性)
#include "stm32f10x.h"
#include "delay.h" // SysTick实现us级延时
// 硬件抽象层(仅改此处适配不同MCU)
#define SPI_SCLK_PIN GPIO_Pin_5
#define SPI_MOSI_PIN GPIO_Pin_7
#define SPI_MISO_PIN GPIO_Pin_6
#define SPI_SS_PIN GPIO_Pin_4
#define SPI_GPIO_PORT GPIOA
#define SPI_GPIO_CLK RCC_APB2Periph_GPIOA
// 引脚操作宏
#define SPI_SCLK_HIGH() GPIO_SetBits(SPI_GPIO_PORT, SPI_SCLK_PIN)
#define SPI_SCLK_LOW() GPIO_ResetBits(SPI_GPIO_PORT, SPI_SCLK_PIN)
#define SPI_MOSI_HIGH() GPIO_SetBits(SPI_GPIO_PORT, SPI_MOSI_PIN)
#define SPI_MOSI_LOW() GPIO_ResetBits(SPI_GPIO_PORT, SPI_MOSI_PIN)
#define SPI_MISO_READ() GPIO_ReadInputDataBit(SPI_GPIO_PORT, SPI_MISO_PIN)
#define SPI_SS_HIGH() GPIO_SetBits(SPI_GPIO_PORT, SPI_SS_PIN)
#define SPI_SS_LOW() GPIO_ResetBits(SPI_GPIO_PORT, SPI_SS_PIN)
// 软件SPI初始化
void SoftSPI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(SPI_GPIO_CLK, ENABLE);
// SCLK/MOSI/SS:推挽输出
GPIO_InitStruct.GPIO_Pin = SPI_SCLK_PIN | SPI_MOSI_PIN | SPI_SS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// MISO:上拉输入
GPIO_InitStruct.GPIO_Pin = SPI_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// 初始状态:空闲低SCLK,SS高
SPI_SCLK_LOW();
SPI_SS_HIGH();
}
// 全双工收发1字节(高位优先)
u8 SoftSPI_ReadWriteByte(u8 tx_data)
{
u8 i, rx_data = 0;
for(i=0; i<8; i++)
{
// 发送1位
(tx_data & 0x80) ? SPI_MOSI_HIGH() : SPI_MOSI_LOW();
tx_data <<= 1;
// 上升沿采样
SPI_SCLK_HIGH();
delay_us(1); // 1us≈500Kbps,调整延时改速率
// 读取1位
rx_data <<= 1;
if(SPI_MISO_READ()) rx_data |= 0x01;
// 下降沿更新数据
SPI_SCLK_LOW();
delay_us(1);
}
return rx_data;
}
// 写命令+读n字节(适配Flash/ADC)
void SoftSPI_WriteCmdReadData(u8 cmd, u8 *rx_buf, u32 len)
{
u32 i;
SPI_SS_LOW();
delay_us(10); // 从机稳定时间
SoftSPI_ReadWriteByte(cmd);
for(i=0; i<len; i++) rx_buf[i] = SoftSPI_ReadWriteByte(0x00);
SPI_SS_HIGH();
}
3. 硬件SPI实现(模式0,高速传输)
#include "stm32f10x.h"
#include "delay.h"
#define SPIx SPI1
#define SPIx_CLK RCC_APB2Periph_SPI1
#define SPIx_GPIO_CLK RCC_APB2Periph_GPIOA
#define SPIx_SCLK_PIN GPIO_Pin_5
#define SPIx_MISO_PIN GPIO_Pin_6
#define SPIx_MOSI_PIN GPIO_Pin_7
#define SPIx_SS_PIN GPIO_Pin_4
#define SPIx_GPIO_PORT GPIOA
// 硬件SPI初始化(9MHz波特率,模式0)
void HardSPI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 使能时钟
RCC_APB2PeriphClockCmd(SPIx_GPIO_CLK | SPIx_CLK, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA时钟
// GPIO配置
GPIO_InitStruct.GPIO_Pin = SPIx_SCLK_PIN | SPIx_MOSI_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPIx_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPIx_SS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
// SPI配置
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // CPOL=0
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控SS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 72/8=9MHz
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_Init(SPIx, &SPI_InitStruct);
// 使能SPI
SPI_Cmd(SPIx, ENABLE);
GPIO_SetBits(SPIx_GPIO_PORT, SPIx_SS_PIN);
}
// 硬件SPI收发1字节
u8 HardSPI_ReadWriteByte(u8 tx_data)
{
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPIx, tx_data);
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPIx);
}
// DMA配置(SPI1_RX→DMA1_Channel2,批量读1KB)
void SPI_DMA_Config(u8 *rx_buf, u32 len)
{
DMA_InitTypeDef DMA_InitStruct;
DMA_DeInit(DMA1_Channel2);
DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)&SPIx->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (u32)rx_buf;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = len;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel2, &DMA_InitStruct);
DMA_Cmd(DMA1_Channel2, ENABLE);
SPI_I2S_DMACmd(SPIx, SPI_I2S_DMAReq_Rx, ENABLE);
}
六、进阶应用:SPI多从机通信(冲突规避)
1. 硬件连接
所有从机SCLK/MOSI/MISO并联,每个从机SS接主机不同IO。
2. 软件控制逻辑(核心:同一时刻仅选1个从机)
// 从机SS定义
#define SS_FLASH GPIO_Pin_4 // PA4
#define SS_OLED GPIO_Pin_0 // PB0
#define SS_ADC GPIO_Pin_1 // PB1
#define PORT_FLASH GPIOA
#define PORT_OTHER GPIOB
// 从机选择宏(先释放所有,再选目标)
#define SELECT_FLASH() {GPIO_SetBits(PORT_OTHER, SS_OLED|SS_ADC); GPIO_ResetBits(PORT_FLASH, SS_FLASH); delay_us(1);}
#define SELECT_OLED() {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_ADC); GPIO_ResetBits(PORT_OTHER, SS_OLED); delay_us(1);}
#define SELECT_ADC() {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_OLED); GPIO_ResetBits(PORT_OTHER, SS_ADC); delay_us(1);}
#define RELEASE_ALL() {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_OLED|SS_ADC);}
// 多从机通信示例
void SPI_MultiSlave_Example(void)
{
u8 flash_id[3], adc_data;
// 读Flash ID
SELECT_FLASH();
SoftSPI_WriteCmdReadData(0x90, flash_id, 3);
RELEASE_ALL();
// 写OLED清屏指令
SELECT_OLED();
SoftSPI_ReadWriteByte(0x01);
RELEASE_ALL();
// 读ADC数据
SELECT_ADC();
adc_data = SoftSPI_ReadWriteByte(0x00);
RELEASE_ALL();
}
3. 冲突规避要点
- 切换从机时,先拉高当前SS,延时1~2us再拉低目标SS;
- 未使用的SS默认高电平;
- 高速传输时,SS切换避开SCLK周期。
七、排障手册:SPI常见问题+根因+排查步骤
1. 通信失败/数据乱码
| 根因 | 排查步骤 |
|---|---|
| CPOL/CPHA不匹配 | 1. 核对外设手册模式;2. 逻辑分析仪抓波形验证采样边沿;3. 切换主机模式测试 |
| 波特率过高 | 1. 查外设最大速率;2. 降速测试(如9MHz→1MHz) |
| 引脚接反/SS未拉低 | 1. 核对MOSI/MISO;2. 测量SS电平(传输时是否为低) |
| 软件SPI延时不足 | 增加delay_us值(如1us→2us) |
2. 多从机冲突
| 根因 | 排查步骤 |
|---|---|
| 双从机同时选中 | 测量SS电平,确认同一时刻仅1根为低 |
| SS切换时序干扰 | 切换时增加1~2us延时,逻辑分析仪查SS与SCLK时序 |
3. 高速传输数据丢失
| 根因 | 排查步骤 |
|---|---|
| 总线过长/信号反射 | 缩短布线至≤5cm,加120Ω终端电阻 |
| 电源纹波 | 外设电源并0.1μF电容,测量纹波≤100mV |
核心总结
- SPI核心是「同步全双工+高速」,物理层连接和时序匹配是通信成功关键;
- 软件SPI灵活、硬件SPI高速低耗,按需选择;
- 多从机通信核心是SS线独立控制,避免同时选中;
- 调试优先级:物理层(连接/布线)→ 时序层(CPOL/CPHA/波特率)→ 软件逻辑。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)