1.SPI接口原理
SPI是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省了空间,提供方便,主要应用在EEPROM,FLASH,实时时钟,AD转换器还有数字信号处理直接。

SPI接口一般使用四根线:
- MISO,主设备数据输入,从设备数据输出(Master In Slave Out)
- MOSI,主设备数据输出,从设备数据输入
- SCLK,时钟信号,由主设备产生
- CS,从设备片选信号,由主设备控制(Chip Select)

2.SPI工作原理
- 硬件为四根线
- 主机和从机都有一个串行移位寄存器,主机通过SPI的串行移位寄存器写入一个字节发送一次传输。
- 串行移位寄存器通过MOSI信号线将字节发送给从机,从机将自己的串行移位寄存器的内容通过MISO返回给主机,这样两个移位寄存器的内容被交换。
- 外设的读写操作同步完成,只进行写操作,则忽略读操作;主机只进行从机的读操作,则主机须发送一个空字节给从机引发传输。
3.SPI特征

- STM32 的SPI接口可以配置为支持SPI协议或者支持IIS
- 音频协议,默认是SPI协议,也可以通过软件方式切换称IIS方式
4.从设备引脚管理(NSS)
Negated Slave Select
1.软件模式
可以设置SPI_CR1寄存器的SSM位来使能这种模式,在这种模式下NSS引脚可以用作它用,而内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动。
2.硬件模式
第一种情况:NSS输出使能,当STM32工作为SPI模式的时,NSS输出已经通过SPI_CR2寄存器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并配置为硬件NSS的SPI设备,将自动变成从的SPI设备。
第二种情况:NSS输出被关闭:允许操作于多主环境。
5.时钟信号的相位和极性
SPI_CR寄存器的CPOL(Clock Polarity)和CPHA(Clock Phase)位,能够组合成四种可能的时序关系,CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。
- 如果CPOL被清'0',SCK引脚在空闲状态下保持低电平;如果CPOL被置'1',SCK引脚在空闲状态保持高电平。
- 如果CPHA(时钟相位)位被置'1',SCK时钟的第二个边沿(0为下降沿,1为上升沿)进行数据位的采样,数据在第二个时钟边沿被锁存。
- 如果CPHA位被清0,SCK时钟的第一个边沿(0为下降沿,1为上升沿)进行数据位采集,数据在第一个时钟边沿被锁存。
CPOL时钟极性和CPHA时钟相位的组合选择数据捕捉的时钟边沿。


6.数据帧和状态
1.数据帧格式
- 根据SPI_CR1寄存器的LSBFIRST位,输出数据可以选择MSB(高位)先或者LSB(低位)先。
- 根据SPI_CR1寄存器的DFF位,可以选择数据帧是8位或者16位,对发送和接收都有效。
2.状态标志
通过三个标志可以完全监控SPI总线的状态
- 发送缓存器空闲标志(TXE)
- 此标志为1的时候,表示发送缓冲寄存器为空,可以写入下一个待发送数据进入缓冲器中,当写入SPI_DR(数据寄存器)时,TXE标志被清除。
- 接收缓冲器非空(RXNE)
- 此标志为1表明接收缓冲器中包含有效数据,读SPI数据寄存器可以清楚此标志
- 忙BUSY标志
- BSY标志由硬件设置与清除,此标志表明SPI通信层的状态
7.SPI中断

8.SPI引脚配置

9.结构体
typedef struct
{
uint16_t SPI_Direction; //方向
uint16_t SPI_Mode; //模式
uint16_t SPI_DataSize; //数据大小
uint16_t SPI_CPOL; //时钟极性
uint16_t SPI_CPHA; //时钟相位
uint16_t SPI_NSS; //NSS位
uint16_t SPI_BaudRatePrescaler; //波特率
uint16_t SPI_FirstBit; //位优先
uint16_t SPI_CRCPolynomial; //CRC校验位
}SPI_InitTypeDef;
10.SPI配置过程
①配置引脚,使能时钟
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
②初始化SPI,设置工作模式
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
③使能SPIx
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
④SPI传输数据
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
⑤查看SPI传输状态
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
11.代码
cs
#include "stm32f10x.h"
#include "spi.h"
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2,&SPI_InitStructure);
}
u8 SPI2_ReadWriteByte(u8 data)
{
u8 i;
// 等待发送缓冲区为空(表示可以发送新数据)
i = 0;
while(SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_TXE) == RESET)
{
i++;
if(i >= 200)
{
return 0;
}
}
SPI_I2S_SendData(SPI2, data);
// 等待接收缓冲区非空(表示有数据可读)
i = 0;
while(SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_RXNE) == RESET)
{
i++;
if(i >= 200)
{
return 0;
}
}
return SPI_I2S_ReceiveData(SPI2);
}
void SPI2_SetSpeed(uint16_t SPI_BaudRatePrescaler)
{
//检查传入的分频值是否合法
assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
//清除旧的分频设置
SPI2->CR1 &= 0Xffc7;
//设置新的分频值
SPI2->CR1 |= SPI_BaudRatePrescaler;
//重新使能SPI
SPI_Cmd(SPI2,ENABLE);
}
注意:
1.SPI通信的本质
SPI是全双工同步串行通信协议,这意味着:
- 主从设备同时收发数据 - 发送和接收同时进行
- 时钟由主机控制 - 主机提供时钟信号SCK
- 数据传输由时钟边沿触发
关键原理:时钟驱动数据传输
在SPI中,数据在时钟边沿被采样。要实现这个采样过程,必须要有:
-
时钟信号(SCK) - 主机产生
-
数据传输 - 有数据在MOSI/MISO线上传输
当主机想要读取从机数据时:
-
主机必须产生时钟脉冲
-
要产生时钟脉冲,主机必须发送数据(即使是无效数据)
-
每个时钟脉冲同时完成:
-
主机发送1位数据到从机(通过MOSI)
-
从机发送1位数据到主机(通过MISO)
-
注:
-
发送命令和地址时确实产生了时钟
-
但从机在这些时钟周期内发送的是无效数据
-
必须继续产生时钟,才能让从机输出有效数据
-
每个SPI数据交换操作(一个字节)同时完成发送和接收
cs
例:
// 读取SPI Flash的完整函数
void SPI_Flash_Read(uint32_t addr, uint8_t *buffer, uint32_t len)
{
CS_LOW();
// 1. 发送命令和地址(这些读取返回值是垃圾数据,直接丢弃)
SPI2_ReadWriteByte(FLASH_CMD_READ); // 命令字节
SPI2_ReadWriteByte((addr >> 16) & 0xFF); // 地址字节1
SPI2_ReadWriteByte((addr >> 8) & 0xFF); // 地址字节2
SPI2_ReadWriteByte(addr & 0xFF); // 地址字节3
// 2. 现在开始读取实际数据
// 每次发送0xFF来产生时钟,同时读取一个字节数据
for(uint32_t i = 0; i < len; i++)
{
buffer[i] = SPI2_ReadWriteByte(0xFF); // 发送伪数据,读取真实数据
}
CS_HIGH();
}
2.SPI2 的引脚
| SPI模块 | 默认引脚(不复用/重映射前) | 所在章节 |
|---|---|---|
| SPI1 | PA4 (NSS), PA5 (SCK), PA6 (MISO), PA7 (MOSI) | 8.3.10 |
| SPI2 | PB12 (NSS), PB13 (SCK), PB14 (MISO), PB15 (MOSI) | 无重映射,固定 |
| SPI3 | PA15 (NSS), PB3 (SCK), PB4 (MISO), PB5 (MOSI) | 8.3.11 |
3.SPI2_ReadWriteByte(u8 dat)
作用: 通过SPI2发送一个字节数据,同时接收从机返回的一个字节数据
cs
示例1:读取传感器数据
// 发送读取命令,同时接收数据
u8 cmd = 0xA5; // 读取命令
u8 response = SPI2_ReadWriteByte(cmd);
// response = 从传感器返回的数据
示例2:写入配置寄存器
// 发送配置值,可能返回状态
u8 config_value = 0x3C;
u8 status = SPI2_ReadWriteByte(config_value);
// status = 从机返回的操作状态
示例3:连续读写操作
// 通常需要先拉低片选
SPI2_NSS_LOW(); // PB12 = 0
// 发送多个字节
SPI2_ReadWriteByte(0x01); // 发送命令
SPI2_ReadWriteByte(0x02); // 发送地址高字节
SPI2_ReadWriteByte(0x03); // 发送地址低字节
u8 data = SPI2_ReadWriteByte(0xFF); // 发送dummy数据,接收实际数据
SPI2_NSS_HIGH(); // PB12 = 1
4.void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
作用:修改SPI2的时钟分频系数,从而改变通信速度
参数:预分频值,对应不同的分频系数(2, 4, 8, 16, 32, 64, 128, 256)
5.bps /Bps的基本定义
bps
- 全称:Bits Per Second(比特/秒)
- bit:二进制的一位(0 或 1)
- per second:每秒钟
- 合起来:每秒钟传输的二进制位数
Bps
- Bytes per second
- 字节/秒
- 1 Bps = 8 bps