文章目录
一、SPI协议
Serial Peripheral Interface,串行外围设备接口。SPI 是串行同步全双工通信协议。一般用于控制器和控制器之间的数据传输。
1.物理层

每个从机都有四条线和主机相连,不同从机的 MISO MOSI SCK 连接的是主机统一个端口,SS 线,连接不同端口
- SS :(Slave Select)从机选择。当主机想和某个从机通讯时,就把从机的 SS 条线的电平拉低。
- SCK :(Serial Clock)时钟信号线。由主机产生,f103的SPI挂载在不同的总线上,最大频率为 f p l k / 2 f_{plk}/2 fplk/2 ,因此时钟最高频率是不一样的。
- MOSI:(Master Output,Slave Input)
- MISO:(Master Input,Slave Output)这两条就是数据线,因此 SPI 是全双工的。
2.协议层
总体讲解
- 主机发出选择信号,拉低某条 SS 线。
- SCK 上升沿,数据可以变化。SCK 下降沿,数据采样,进行传输。
- 8个时钟后,一个字节的数据就发送出去了。
- 发送过程中高位先行和低位先行没有影响规定,一般选择高位先行。
- 图中 MSBOUT 中 MSB 意为 最高有效位(most significant bit)。把MSBOUT写在最前面意思就是高位先行。
具体讲解
但实际上,SPI 并没有硬性规定必须在下降沿采样。这里需要配置时钟极性(CPOL) 和 时钟相位 (CPHA) ,他们决定了空闲时,时钟电平,和采样时刻。
- 时钟极性(CPOL):SPI 空闲时,SCK 信号线的电平。 CPOL=1 高电平;CPOL=0 低电平。
- 时钟相位 (CPHA) :数据采样时刻。CPHA=1 在偶数边采样,CPHA=0 在奇数边采样。
- 如果CPOL=0,CPHA=1 即空闲状态为0,偶数边采样;这样就是上面图中上升沿变化,下降沿采集。
由此,CPOL,CPHA 的不同组合一共四种,对应了四种模式,其中最常用的是 模式0 和 模式3。(即:低电平,上升沿采集 和 高电平,上升沿采集)
模式 | CPOL | CPHA |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
2 | 1 | 0 |
3 | 1 | 1 |
二、STM32 SPI外设
STM32共有3个 SPI 外设。 其中SPI1挂载在 APB2 总线上,SPI2,SPI3挂载在 APB1 总线上。其中后面两个还支持 I2S。框图在 参考手册 中可以查到。

1.通讯引脚
通信引脚的对应的端口可以在 数据手册 中查到,现在总结如下:
要注意的是 SPI3 的引脚用到了下载引脚,复用功能为SPI,一般不会使用SPI3。一般 NSS 不用这里的硬件控制,而是直接使用 GPIO控制。
2.时钟控制逻辑
SCK 的时钟信号,其波特率是由 控制寄存器 CR1 的 BR位控制的,控制结果如下表:
BR | 频率 | BR | 频率 |
---|---|---|---|
000 | f p c k l / 2 f_{pckl}/2 fpckl/2 | 100 | f p c k l / 32 f_{pckl}/32 fpckl/32 |
001 | f p c k l / 4 f_{pckl}/4 fpckl/4 | 101 | f p c k l / 64 f_{pckl}/64 fpckl/64 |
010 | f p c k l / 8 f_{pckl}/8 fpckl/8 | 110 | f p c k l / 128 f_{pckl}/128 fpckl/128 |
011 | f p c k l / 16 f_{pckl}/16 fpckl/16 | 111 | f p c k l / 256 f_{pckl}/256 fpckl/256 |
f p c k l f_{pckl} fpckl 指 SPI 所在总线的时钟频率。
3.数据控制
接收数据时,数据从 MISO进入,串行存到移位寄存器,再并行移动到接收缓存区被读出。
发送数据,先写入发送缓冲区,经过移位寄存器由 MOSI 发送出去。这两个缓冲区对应的寄存器就是 DR。框图中 LSBFIRST 控制位可以控制高位先行。
事实上,发送缓冲区和接收缓冲区 都对应了同一个寄存器 DR。
SPI 是全双工通信,硬件端口部分确实有两根线,但这两条线共用了同一个移位寄存器,共用了同一个缓冲区,这难道不会发生冲突吗?
全双工的整体过程如下:
- 发送数据:数据写入 DR,并行传输到移位寄存器,移位寄存器通过 MOSI 串行发出数据。
- 此时如果有数据从 MISO 输入,则紧跟在移出寄存器的发送数据后面进入移位寄存器(也就是说移位寄存器一边接收数据,一边吐出数据)
- 当发送数据的每一位都移出移位寄存器时,接收的数据也正好填满了移位寄存器。此时,移位寄存器内储存的就是接收数据。
- 移位寄存器把数据并行传给 DR。读取DR 就可得到输入数据。
- 重复上述过程,就是全双工的工作模式。
4.整体控制逻辑
-
CR :控制寄存器:控制了 SPI模式,波特率,数据帧格式,LSB先行,主从模式,NSS软控制,使能,时钟等。
-
SR 状态寄存器,工作过程中会有置位和清零。
-
DR 数据寄存器,其对应了发送缓冲区和接收缓冲区。
三、通信过程
和I2C一样,通信过程中,状态寄存器会置位和清零,可以通过读取这些位来了解通信状态。
先介绍两个标志位,TXE(Transmit buffer empty),当发送缓冲区为空时,其置1;RXEN(Receive buffer not empty) ,当接收缓冲区位空时,其清零。
上面已经把全双工解释的很详细了,这里结合置位在说一下。
- 总体上 SCK 产生时钟信号,数据被发送和接收进来;因为是全双工,发送和接收时可以同时进行的。
- 先看发送部分:首先,当 DR 被写入数据时, 标志位 TXE 会硬件置0。
- 接着数据并行移动到移位寄存器,此时DR变空;TXE 硬件置1;这时可以传入第二个要发送的数据到 DR 中。
- 接收部分:当发送的数据完全从移位寄存器移出时,接收的数据正好填满移位寄存器,数据被并行移动到 DR。
- 此时 RXNE置1,标志有数据传入 DR,也表明已经完成发送数据。
- 最后,只有写入数据后,SCK 才会产生时钟。所以就算只要接收,也要先发送一帧数据以开启时钟。
四、固件库编程
1.结构体
typedef struct
{
uint16_t SPI_Direction; //配置SPI的单双向模式
uint16_t SPI_Mode; //配置主/从模式
uint16_t SPI_DataSize; //数据帧大小,可选8/16
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; //校验表达式
}SPI_InitTypeDef;
2.固件库函数
- SPI_Init :初始化函数
- SPI_Cmd :使能SPI
- SPI_I2S_SendData,SPI_I2S_ReceiveData :发送和接收数据
- SPI_I2S_GetFlagStatus :获取工作状态
五、FLASH介绍
板子上用的FLASH是 W25Q64 信号的Flash,是一种 NOR FLASH。其中“64” 表示 64M bit,即为 8M Byte即8MB 8兆字节的存储器(真小~)。
(注:M 表示
2
20
2^{20}
220)
1.芯片引脚图

通过读芯片手册,可以得知芯片引脚对应的功能。
- /WP : 为写保护,其中前面的斜杠表示低电平有效。
- /HOLD:暂停通信。
2.块和扇区
FLASH 把自己 8MB 的空间分为128个块(Block),这样每个块是64KB。再把每个块分为16个扇区(Sector),每个扇区是 4KB。
由于FLASH的存储特性,在写入数据之前必须先把原有数据擦除(恢复为全1状态);因此对存储单元的划分有利于快速读写。
擦除时,最小擦除最小单位为扇区。而读写时不同的FLASH写入最小单位不同。NOR FLASH 可以一个字节读写,而 NADN FLASH 只能以块或扇区读写。这也是为什么NOR FLASH可以执行程序。

3.控制FLASH的指令
不同于EEPROM,FLASH的控制较为复杂。对FLASH的读写和检测需要用预先规定好的指令,FLASH会相应这些指令,并做出操作。
这些命令在芯片手册和零死角中都有总结。
表中,第一列是指令名;第二列是发送代码;第三列即以后都是相关的代码传输,其中有括号的表示是从FLASH传入stm32中的。
举例说明:FLASH写入操作是较耗时的,因此在进行操作时必须得知当前FLASH的状态,而对FLASH状态的读取就是用指令完成的;stm32发出指令,FLASH返回当前自身状态。
这一个操作对应的指令名是 Read Status Register-1,首先stm32发送代码 05,FLASH返回自身状态(返回状态寄存器低8位的值S7~S0)。
下面解析一些操作的命令。
Sector Erase(4KB)
扇区擦除。前面提到,在对FLASH写入时要先对其进行擦除,而擦除的最小单元就是扇区(Sector);
这里指令为 20;后面还要连续传入三个字节,这三个字节就要擦除扇区的地址(FLASH的地址是32位的);
Page Program
写入数据。操作代码是 02。最多允许写入256个字节。发出写入地址( A23~A0 ),最后传入数据( D7~D0 )。(这里手册上写错了,最后一个字节仍是stm32发送,不用加括号)
Read Data
读取数据。操作指令 03 。发送读取地址,FLASH返回地址的存储值。
检测指令
Manufacturer/Device ID ,Read Unique ID,JEDEC ID。这些指令读取的是FLASH上特定的ID号。
其作用在于:开机检测时,用于检测FLASH是否工作正常,SPI线能否正常工作。
注:对发送地址的要求
上面向FLASH发送地址时,要发送最低位的地址。如 Sector Erase 擦除一个扇区的地址,传入这个扇区的最低地址。
如擦除 Block3 的 Sector2 ;那就传入 020000+002000 = 022000。
开始编程
1.硬件电路图

2.初始化GPIO
static void SPI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOC,ENABLE);
//CLK
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//DO/MISO
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//DIO/MOSI
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//CS/NSS
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC,&GPIO_InitStruct);
}
这里把 DO,DIO,CLK
对应的引脚设置成复用推挽,因为我们想要使用软件控制 NSS
,因此 NSS
引脚设置位推挽输出。使用或操作同时配置 GPIOA 和 GPIOC 的时钟。
3.初始化SPI
在初始化时,一定事先把 NSS
的电平拉高,让FLASH处于未被选中的状态。否则初始化不会成功。
static void SPI_FLASH_Config(void)
{
SPI_InitTypeDef SPI_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
FLASH_SPI_CS_HIGH();//非常重要!!!!!
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_High;
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_Init(SPI1,&SPI_InitStruct);
SPI_Cmd(SPI1,ENABLE);
}
把两者的代码封装以便调用。
void SPI_FLASH_Init(void)
{
SPI_GPIO_Config();
SPI_FLASH_Config();
}
4.发送数据
发送数据可以用库函数中的 SPI_I2S_SendData
,注意时序的检测。
首先检测 TXE位是否置位,当 TXE置位时表示 DR 为空,可以写入数据
发送完数据后,检测 RXNE是否置位,置位时表示 DR 有接收到数据,也就是发送已经完成。
最后把接收到的数据用 SPI_I2S_ReceiveData
获得。
uint8_t SPI_FLASH_SentByte(uint8_t byte)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET)
{
if( (SPITimeout--)==0 ) return SPI_TIMEOUT_UserCallback(0);
}
SPI_I2S_SendData(SPI1,byte);
SPITimeout = SPIT_FLAG_TIMEOUT;
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET)
{
if( (SPITimeout--)==0 ) return SPI_TIMEOUT_UserCallback(1);
}
return SPI_I2S_ReceiveData(SPI1);
}
函数中超时警告函数和,I2C中编写的一样,这里附上代码不再讲解。
#define SPI_DEBUG_ON 1
#define SPI_INFO(fmt,arg...) printf("<<-SPI-INFO->> "fmt"\n",##arg)
#define SPI_ERROR(fmt,arg...) printf("<<-SPI-ERROR->> "fmt"\n",##arg)
#define SPI_DEBUG(fmt,arg...) do{\
if(SPI_DEBUG_ON)\
printf("<<-SPI-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
}while(0)
static __IO uint32_t SPITimeout = SPIT_FLAG_TIMEOUT;
static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorcode)
{
SPI_ERROR("SPI 等待超时!errorcode=%d",errorcode);
return 0;
}
5.对FLASH的操作
把上面对FLASH的操作表格内容封装为宏,便于使用。
/************************FLASH Instruction***************************/
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define WIP_Flag 0x01
#define Dummy_Byte 0xFF
读取FLASH的ID
#define FLASH_SPI_CS_LOW GPIO_ResetBits(GPIOC,GPIO_Pin_0)
#define FLASH_SPI_CS_HIGH GPIO_SetBits(GPIOC,GPIO_Pin_0)
#define Dummy_Byte 0xFF
uint32_t SPI_FLASH_ReadID(void)
{
uint32_t Temp=0,Temp0=0,Temp1=0,Temp2=0;
FLASH_SPI_CS_LOW;
SPI_FLASH_SentByte(W25X_JedecDeviceID);
Temp0 = SPI_FLASH_SentByte(Dummy_Byte);
Temp1 = SPI_FLASH_SentByte(Dummy_Byte);
Temp2 = SPI_FLASH_SentByte(Dummy_Byte);
FLASH_SPI_CS_HIGH;
Temp = (Temp0<<16) | (Temp1<<8) | Temp2;
return Temp;
}
在发送数据时需要拉低选择线 NSS 的电平,我们使用软件控制,编写一个宏 FLASH_SPI_CS_LOW/HIGH
来控制。在发送数据时拉低电平。
发送完读取设别ID的指令后,FLASH会返回三个字节的数据,分别是 生产厂商,储存类型,容量。 直接使用 SPI_FLASH_SentByte
接收就好。
使能函数
在写入数据前要进行写使能。
void SPI_FLASH_WriteEnable(void)
{
FLASH_SPI_CS_LOW;
SPI_FLASH_SentByte(W25X_WriteEnable);
FLASH_SPI_CS_HIGH;
}
空闲检测
写入数据需要时间,要想知道FLASH是否已经写好数据,正在空闲可以写入下一个数据,需要检测FLASH的状态寄存器。
用到指令Read Status Register
,会返回状态寄存器的低8位数据,其中最后一位 BUSY
表明FLASH是否忙,1为忙。
#define WIP_Flag 0x01
void SPI_FLASH_WaiteForWriteEnd(void)
{
uint8_t FLAG = 0;
FLASH_SPI_CS_LOW;
SPI_FLASH_SentByte(W25X_ReadStatusReg);
do
{
FLAG = SPI_FLASH_SentByte(Dummy_Byte);
}
while((FLAG & WIP_Flag ) == 1);
FLASH_SPI_CS_HIGH;
}
发送过读取请求后,FLASH会一直返回当前状态的数据,我们就用一个while循环不断读取,直到FLASH不再忙。
擦除扇区
写入数据前要先擦除扇区,上面也介绍到最小擦除单元为扇区,使用指令 Sector Erase
,先发送指令,在发送24位的地址。在发送擦除指令前要使能写入。
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
SPI_FLASH_WriteEnable();
SPI_FLASH_WaiteForWriteEnd();
FLASH_SPI_CS_LOW();
SPI_FLASH_SentByte(W25X_SectorErase);
SPI_FLASH_SentByte((SectorAddr>>16)&0xff);
SPI_FLASH_SentByte((SectorAddr>>8)&0xff);
SPI_FLASH_SentByte(SectorAddr&0xff);
FLASH_SPI_CS_HIGH();
SPI_FLASH_WaiteForWriteEnd();
}
页写入
出于不明原因,SPI_FLASH_WaiteForWriteEnd();
必须在FLASH_SPI_CS_HIGH();
下面,数据才能传输有效。
void SPI_FLASH_PageWrite(uint8_t *pBuffer,uint32_t WriteAddr,uint16_t NumberByteToWrite)
{
SPI_FLASH_WriteEnable();
//SPI_FLASH_WaiteForWriteEnd();
FLASH_SPI_CS_LOW();
SPI_FLASH_SentByte(W25X_PageProgram);
SPI_FLASH_SentByte((WriteAddr>>16)&0xff);
SPI_FLASH_SentByte((WriteAddr>>8)&0xff);
SPI_FLASH_SentByte(WriteAddr&0xff);
if(NumberByteToWrite > 256)
{
NumberByteToWrite = 256;
SPI_ERROR("数据过多,已修改为256");
}
while(NumberByteToWrite--)
{
SPI_FLASH_SentByte(*pBuffer);
pBuffer++;
}
FLASH_SPI_CS_HIGH();
SPI_FLASH_WaiteForWriteEnd();
}
读取数据
void SPI_FLASH_BufferRead(uint8_t *pBuffer,uint32_t ReadAddr,uint16_t NumberByteToRead)
{
FLASH_SPI_CS_LOW();
SPI_FLASH_SentByte(W25X_ReadData);
SPI_FLASH_SentByte((ReadAddr>>16)&0xff);
SPI_FLASH_SentByte((ReadAddr>>8)&0xff);
SPI_FLASH_SentByte(ReadAddr&0xff);
while(NumberByteToRead--)
{
*pBuffer = SPI_FLASH_SentByte(Dummy_Byte);
pBuffer++;
}
FLASH_SPI_CS_HIGH();
}
main函数
uint8_t Tx_Buffer[]= "JOJO,ÎÒ²»×öÈËÁË£¡";
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define BufferSize (countof(Tx_Buffer)-1)
uint8_t Rx_Buffer[BufferSize];
int main(void)
{
uint32_t DeviceID;
uint8_t Manufactuer_ID,Memory_Tyep,Capacity;
USART_Config();
SPI_FLASH_Init();
DeviceID = SPI_FLASH_ReadID();
Manufactuer_ID = (DeviceID >> 16)&0XFF;
Memory_Tyep = (DeviceID >> 8)&0XFF;
Capacity = DeviceID & 0xff;
printf("Manufactuer_ID: 0x%x \n Memory_Tyep: 0x%x \n Capacity: 0x%x\n"
,Manufactuer_ID,Memory_Tyep,Capacity);
SPI_FLASH_SectorErase(0x00000);
SPI_FLASH_PageWrite(Tx_Buffer,0X00000,BufferSize);
SPI_FLASH_BufferRead(Rx_Buffer,0x00000,BufferSize);
printf("接收完成 %s \n",Rx_Buffer);
}