一、SPI通信协议原理

图片:1、W25Q64flash存储芯片 2、SPI OLED显示屏 3、2.4G无线通信模块NRF24L01 4、Micro ID卡
SPI通信最大速率取决于芯片厂商的设计需求。需要四根通信线,全双工。一主多从(通过ss片选进行从机选择),无应答机制设计。
硬件电路

作为单端信号,所有的设备都需要共地。主从机还需要供电。推挽输出保证了SPI下降沿和上升沿都非常快,保证了通信速率。
防止从机的推挽输出MISO引脚断路,SPI规定当从机SS未被选中,从机的MISO一定要为高阻态。
通信移位示意图:

可以看到主从机皆发送高位先行,接收低位先接收到然后移位。通信根据时钟(波特率发生器进行时钟通信同步)。
上升沿所有数据向左移位一次,移除出的1bit在引脚电平上。下降沿设备从线上读取电平到移位寄存器的最低位。
第一个上升沿

第一个下降沿

若需要发送同时接收,直接交换数据到位移寄存器,交换结束后读取位移寄存器数据即可。
若只需要发送,则直接交换数据到位移寄存器,然后不读取交换后的数据即可。
若只想接收,则随便发送一个数据,将置换过来的数据读取就行。
基本通信单元:低电平片选

时序基本单元:根据CPOL和CPHA(时钟相位)配置SPI模式,不同的模式规定了空闲状态的SCK,SCK第一个边沿进行移入/移除数据。

第一个边沿移入数据的话,那么SS选中低电平时进行第一个数据的移出。SCK高电平位移寄存器进行数据获取。

若交换一字节后继续操作主机不需要进行取消片选。
模式中最常用的为模式0;
通信时序示例:

主机 发送0x06,从机返回0xFF,可以看到高位先行。模式0下,SCK默认低电平,SCK第一个边沿进行移入(上升沿),第二个边沿(下降沿)进行移出。(ss起始为数据移出)。若要进行持续写数据,在SCK下降沿写入数据,SCK上升沿读取数据写入即可(没有应答机制,直接持续时序即可)。
SPI通信和I2C不一样,一般第发送的第一个字节为指令码(表示为什么操作),后面为具体操作。详细应该根据芯片通信协议手册。
指定地址写(高位先行):

示例为指定地址写,W25Q64为24bit地址为3byte,所以发送控制指令后需要发送3byte地址(同样高位先行),
指定地址读(高位先行):

示例为指定地址读,W25Q64为24bit地址为3byte,所以发送控制指令后需要发送3byte地址(同样高位先行),读数据和从机交换的数据任意,此处为0xFF。
总的来说,指定地址写和指定地址都都是根据W25Q64协议来的,实际上SPI通信模式0时序没变。
二、模块介绍
W25Q64(AT24C02使用I2C):

非易失性存储器一般为Flash(Nor Flash、Nand Flash)、EEPROM。
易失性存储器一般为SRAM、DRAM。
固件程序存储为XIP(eXecute in Place):直接把程序文件下载到外挂芯片中,需要执行程序时,直接读取外挂文件的程序芯片进行执行。就地执行,比如电脑里的BIOS固件。
双重SPI和四重SPI,利用总线线束进行多根一起收发(MISO、MOSI、WP写保护、HOLD),提高通信速率。
寻址为24bit, = 1677216/1024/1024 = 16MB,所以最大寻址空间为16MB。W25Q64为8MB,可进行完全寻址(含义为:寻址24bit表示,说明flash的可使用容量大小肯定小于
,这样才能完全覆盖内存地址)。对W25Q256有3byte寻址模式和4byte寻址模式,3byte寻址模式为24bit,只能操作前16MB内存。
电路原理图

hold:在通信时,通过HOLD低电平可以保持芯片当前引脚状态。这个时间主机可以释放CS做其他事情,后面可以继续(接着之前的时序)操作该芯片(注意片选要恢复低电平)。
W25Q64内存框图(存储空间(8MB,寻址24Bit)-Block(64KB,8*1024/64=128块)-Sector(4KB,64/4=16扇区)-Page(256B,4*1024 /256= 16页)):

图Block可知,起始到结束000000h-00FFFFh(B=65536B/1024=64KB),其中起始地址从000000h-7F0000h,共有128Block。
图Sector可知,起始到结束0000h-0FFFh(B=4096B/1024=4KB),其中起始地址从0000h-F000h,1Block共有16Sector。
图Sector可知,第一个Sector,起始到结束0000h-00FFh(B=256B),其中起始地址从0000h-0F00h,1Sector共有16Page。
图中为通过24bit寻址地址区分Block、Sector、Page。每个地址为1B。
High Voltage是高电压生成器,在掉电时保持芯片电路的状态。例如对点亮的和烧坏的LED表示1,熄灭的表示0,所以对应掉电不丢失的存储器需要一个高压源。
通信时24bit,其中的前16bit存入Byte Address Latch/Counter,低8bit存入Page Address Latch/Counter。因为存储器逻辑是通过page为行,所以通过行解码和列解码分别进行指定,然后定位地址位置,进行读写。读写后可以通过指针自动+1.
Column Decode And 256-Byte Page Buffer存有256B的页缓冲区,是RAM。通过这里缓冲进行寄存器读写。类似于cache,所以一次时序写入的字节不能超过256B。所以每次写入(擦除也同样)芯片会有一段时间Busy,会置Busy位,不会响应新的时序。
flash操作注意事项:

因写入需要擦除,所以Flash中0xFF为空白。最小为Sector单位擦除。最小为Page写入(受缓冲影响)。
手册:
状态寄存器

Busy位,会在写入(页编程)、擦除(扇区、块、整块)操作置位,不响应其他操作。会自动清0。
Write Enable Latch 写使能锁,当写使能=1,可以进行写入,当写失能=0,不能写入。设备会在上电、读取到写失能指令、写入(页编程)、擦除(扇区、块、整块)。在页编程后会自动进行写失能。
指令集和id

分为芯片id和厂商id,在测试通信时会首先进行芯片id的读取。




三、STM32外设SPI介绍

SPI和I2C一般都为高位先行。串口为低位先行。
STM32的APB2的PCLK为72MHz,APB1的PCLK=36MHz。
I2S是音频传输协议

SPI框图

收发流程:
1、默认空闲状态,数据从数据总线传输到发送寄存器
2、此时移位寄存器为空,发送寄存器数据会进入移位寄存器,当发送数据寄存器为空,TXE=1,表示后面的数据可以继续传入发送寄存器。
3、移位寄存器内的数据根据时钟自动通过MOSI进行发送,同时MISO的数据对应根据时钟交换转入进入移位寄存器。
4、当交换结束,位移寄存器内为接收到的数据,会自动存入接收寄存器,会置RXNE=1表示接收非空,此时可自行读取接收寄存器数据(尽快读出,否则会被下个数据覆盖)。
5、如此通过写入发送寄存器、位移寄存器交换数据、读取接收寄存器、如此紧密配合可以进行完整的数据连续收发。
USART是全双工、异步通信、所以移位寄存器和发送接收寄存器都是单独的。
I2C是半双工(发送和接收不会同时进行)所以发送接收寄存器和移位寄存器共用。
SPI是全双工(虽然只有单根线,但是为交换数据)、所以发送接收寄存器分开,但是移位寄存器共用。

可以指定高位先行还是低位先行
SPI简化结构图:

传输时序图(使用硬件通信,需要看硬件时序图,根据时序进行配置):
连续传输
主模式,全双工连续传输MODE=3示意图,MISO/MOSI(输入)上面为输出波形。
首先SS低电平,没画但是必须有。
1、SCK下降沿MOSI数据移出,SCK上升沿MISO数据移入。可以看到MISO和MOSI跟随SCK出现(低位先行,常用高位先行)。
2、Busy为忙标志位,当有数据传输时,Busy置1,可以看到在数据读入后,开始置1,一直到结束后的数据移入结束。
3、发送缓冲器中,当波形开始时TXE=1,软件直接桨F1数据写入发送缓冲器,在下一移出SCK边沿出现时,TEX会置0,此时软件检测到后,直接软件写入F2数据到发送缓冲器。然后在F1的数据传输过程中TEX都会=0。当F1传输完成后,会自动在下一移出SCK边沿,F2进入移位寄存器。TEX=1,在下一移出SCK边沿出现,软件直接写入F3即可。如此连续传输数据。在最后一个数据传输时,TXE=1后不在软件写入数据到发送缓冲器,直到SPI忙状态结束传输完毕。
4、接收缓冲器同发送缓冲器,在每次接收到1byte数据的最后一个移出边沿。会置RXNE=1,在下一数据覆盖接收寄存器前,接受寄存器内存储刚接收到的数据,需要及时读出。
非连续传输(发送)

1、数据根据SCK下降沿输出,上升沿输入。MOSI根据SCK进行逐渐发出
2、先将数据写入发送缓冲器,然后开始时序进行数据发送。可以看到TXE获取到一次数据后一直置1。直到BSY清零为数据发送结束。在数据发送接收后即可等待RXEN=1,进行数据获取。
3、在每次开始通信前进行数据写入即可。
不同SCLK非连续发送间隙情况:
72MHz/256 = 280K

64分频=1MHz

2分频 = 36MHz(超过了示波器的采样频率,所以波形看不完整,但是还是可以看出中间的间隔时间很长,拖延了通信速率)
软硬件SPI对比
可以看到硬件反应快,边沿几乎紧贴。

其他:

软件写入DR,会自动清除TXE。
四、程序实例

1、软件模拟写读W25Q64,显示到OLED
|------|--------|----------------------------------------|-------------------|-----------------------------------------------------------------|----------------------|-----------------------|
| 文件名 | main.c | Software_ SPI.c | Software_ SPI.h | Software_ W25Q64.c | Software_ W25Q64.h | Software_ W25Q64Reg.h |
| 文件内容 | 主函数 | 软件模拟SPI,时序基本单元函数(起始、结束、接收1bit、交换1Byte) | Software_SPI.c头文件 | W25Q64使用软件模拟函数实现读写,包括擦除扇区、等待芯片忙状态结束、写使能、写入指定地址和长度字节、读取指定地址和长度字节 | Software_W25Q64.c头文件 | W25Q64寄存器和结构体共用体宏定义 |
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Software_W25Q64.h"
uint8_t ID[3];
uint8_t W25Q64_DataW[]={0x01,0x02,0x03,0x04};
uint8_t W25Q64_DataR[4];
int main(void){
OLED_Init();
Software_W25Q64_Init();
OLED_ShowString(1,1,"ID:");
Software_W25Q64_GetID(&ID[0],&ID[1],&ID[2]);
OLED_ShowHexNum(1,4,ID[0],2);
OLED_ShowHexNum(1,7,ID[1],2);
OLED_ShowHexNum(1,10,ID[2],2);
while(1){
Software_W25Q64_Write(1,&W25Q64_DataW[0],4);
Software_W25Q64_Read(1,&W25Q64_DataR[0],4);
for(int i=0;i<4;i++){
OLED_ShowHexNum(2,1+3*i,W25Q64_DataW[i],2);
OLED_ShowHexNum(3,1+3*i,W25Q64_DataR[i],2);
W25Q64_DataW[i]++;
}
Delay_ms(200);
}
return 0;
}
Software_SPI.c
#include "stm32f10x.h" // Device header
#define SPI_SCK(x) GPIO_WriteBit(GPIOA,GPIO_Pin_5, (BitAction)(x))
#define SPI_NSS(x) GPIO_WriteBit(GPIOA,GPIO_Pin_4, (BitAction)(x))
#define SPI_MOSI_SEND(x) GPIO_WriteBit(GPIOA,GPIO_Pin_7, (BitAction)(x))
/**
* @brief 软件模拟SPI初始化,使用STM32-SPI1的引脚,可以做到软硬件直接切换
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Software_SPIInit(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_4 | GPIO_Pin_5;//MOSI-NSS-SCK
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//MISO
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
SPI_NSS(1);
SPI_SCK(0);
}
/**
* @brief 软件SPI开始信号,片选选择
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Software_SPI_Start(void){
SPI_NSS(0);
}
/**
* @brief 软件SPI通信结束,片选停止
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Software_SPI_Stop(void){
SPI_NSS(1);
}
/**
* @brief 软件I2C获取MISO从机输出数据1bit,本文件自己使用,所以.h文件不进行
* @param
* @arg
* @param
* @arg
* @retval None
*/
uint8_t SPI_MISO_GET(void){
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
/**
* @brief 软件SPI数据交换1Byte
* @param SendByte:发送的字节数据
* @arg
* @param
* @arg
* @retval 返回交换的Byte
*/
uint8_t Software_SPI_SwapByte(uint8_t SendByte){
uint8_t GetByte=0;
for(int i=0;i<8;i++){
SPI_MOSI_SEND((BitAction)(SendByte&(0x80>>i)));
SPI_SCK(1);
if(SPI_MISO_GET()){
GetByte |= 0x80>>i;
}
SPI_SCK(0);
}
return GetByte;
}
Software_SPI.h
#ifndef __SOFTWARE_SPI_H
#define __SOFTWARE_SPI_H
#include "stm32f10x.h" // Device header
void Software_SPIInit(void);
void Software_SPI_Start(void);
void Software_SPI_Stop(void);
uint8_t Software_SPI_SwapByte(uint8_t SendByte);
#endif
Software_W25Q64.c
#include "stm32f10x.h" // Device header
#include "Software_SPI.h"
#include "Software_W25Q64.h"
#include "Software_W25Q64Reg.h"
/**
* @brief 初始化
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Software_W25Q64_Init(void){
Software_SPIInit();
}
/**
* @brief 软件SPI清除W25Q64扇区
* @param SectorAddress:清除地址所在扇区的地址,一般用扇区起始地址,可查看手册计算,
* @arg W25Q64为24bit寻址,应输入24bit地址,此处取低地址。擦除默认为0xFF,需要先进行写使能
* @param
* @arg
* @retval None
*/
void Software_W25Q64_ClearSector(uint32_t SectorAddress){
uint32_t SendAddress = 0x00FFFFFF & SectorAddress;
Software_W25Q64_WriteEnable();
Software_SPI_Start();
Software_SPI_SwapByte(Sector_Erase_4KB);
Software_SPI_SwapByte(SendAddress>>16);//程序只发送低位,无需处理
Software_SPI_SwapByte(SendAddress>>8);
Software_SPI_SwapByte(SendAddress);
Software_SPI_Stop();
Wait_W25Q64_BusyEnd();
}
/**
* @brief 等待W25Q64忙状态结束,超时计时20000约400ms(计时可根据实际情况更改),根据手册擦除50-100k最大400ms
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Wait_W25Q64_BusyEnd(void){
uint32_t TimeOut = 0;
uint8_t Status_Register1 = 0xFF;
Software_SPI_Start();
Software_SPI_SwapByte(Read_Status_Register_1);
do{
Status_Register1 = Software_SPI_SwapByte(0xFF);
TimeOut++;
if(TimeOut>=100000){
Software_SPI_Stop();
break;
}
}while(Status_Register1&0x01);
Software_SPI_Stop();
}
/**
* @brief 软件SPI写入W25Q64,最大为1Page,256B,此函数写入时会清除地址所在的Sector(4KB)
* @param PageAddress:写入的页地址,W25Q64为24bit地址
* @arg 建议为页起始地址,若超出页范围会从页起始写入,覆盖原数据
* @param WriteArray:写入的数组起始指针,先写低下标数据
* @arg
* @param ArrayLength:写入的数组长度
* @arg
* @retval None
*/
void Software_W25Q64_Write(uint32_t PageAddress,uint8_t *WriteArray,uint16_t ArrayLength){
uint32_t SendAddress = 0x00FFFFFF & PageAddress;
//写入操作前对当前地址数据进行清除,Flash只能写入0,清除后为0xFF
Software_W25Q64_ClearSector(SendAddress);
//开启写使能
Software_W25Q64_WriteEnable();
//起始
Software_SPI_Start();
//页编程指令+地址
Software_SPI_SwapByte(Page_Program);
Software_SPI_SwapByte(SendAddress>>16);
Software_SPI_SwapByte(SendAddress>>8);
Software_SPI_SwapByte(SendAddress);
//数据写入
for(int i=0;i<ArrayLength;i++){
Software_SPI_SwapByte(WriteArray[i]);
}
//停止
Software_SPI_Stop();
//等待忙状态结束
Wait_W25Q64_BusyEnd();
}
/**
* @brief 软件SPI读入W25Q64,最大为1Page,256B
* @param PageAddress:写入的页地址,W25Q64为24bit地址
* @arg 建议为页起始地址,若超出页范围会从页起始写入,覆盖原数据
* @param WriteArray:写入的数组起始指针,先写低下标数据
* @arg
* @param ArrayLength:写入的数组长度
* @arg
* @retval 函数不能返回局部变量的地址,局部变量会在函数结束时释放,所以未使用返回地址的方法
*/
void Software_W25Q64_Read(uint32_t PageAddress,uint8_t *ReadArray,uint32_t ArrayLength){
uint32_t SendAddress = 0x00FFFFFF & PageAddress;
//起始
Software_SPI_Start();
//读数据指令+地址
Software_SPI_SwapByte(Read_Data);
Software_SPI_SwapByte(SendAddress>>16);
Software_SPI_SwapByte(SendAddress>>8);
Software_SPI_SwapByte(SendAddress);
//数据读取
for(uint32_t i=0;i<ArrayLength;i++){
ReadArray[i] = Software_SPI_SwapByte(0xFF);
}
//停止
Software_SPI_Stop();
}
/**
* @brief 软件SPI获取供应商id和芯片驱动id
* @param MANUFACTURER_ID 获取的供应商id变量指针
* @arg
* @param Device_ID_H 获取的芯片驱动id高8bit变量指针
* @arg
* @param Device_ID_L 获取的芯片驱动id低8bit变量指针
* @arg
* @retval None
*/
void Software_W25Q64_GetID(uint8_t *MANUFACTURER_ID,uint8_t *Device_ID_H,uint8_t *Device_ID_L){
Software_SPI_Start();
Software_SPI_SwapByte(JEDEC_ID);
MANUFACTURER_ID[0] = Software_SPI_SwapByte(0xFF);
Device_ID_H[0] = Software_SPI_SwapByte(0xFF);
Device_ID_L[0] = Software_SPI_SwapByte(0xFF);
Software_SPI_Stop();
}
/**
* @brief 写使能
* @param
* @arg
* @param
* @arg
* @retval None
*/
void Software_W25Q64_WriteEnable(void){
Software_SPI_Start();
Software_SPI_SwapByte(Write_Enable);
Software_SPI_Stop();
}
Software_W25Q64.h
#ifndef __SOFTWARE_W25Q64_H
#define __SOFTWARE_W25Q64_H
#include "stm32f10x.h" // Device header
void Software_W25Q64_ClearSector(uint32_t SectorAddress);
void Software_W25Q64_Init(void);
void Wait_W25Q64_BusyEnd(void);
void Software_W25Q64_WriteEnable(void);
void Software_W25Q64_Write(uint32_t PageAddress,uint8_t *WriteArray,uint16_t ArrayLength);
void Software_W25Q64_Read(uint32_t PageAddress,uint8_t *WriteArray,uint32_t ArrayLength);
void Software_W25Q64_GetID(uint8_t *MANUFACTURER_ID,uint8_t *Device_ID_H,uint8_t *Device_ID_L);
#endif
Software_W25Q64Reg.h
#ifndef __SOFTWARE_W25Q64REG_H
#define __SOFTWARE_W25Q64REG_H
#include "stm32f10x.h" // Device header
#define Write_Enable 0x06
#define Read_Status_Register_1 0x05
#define Page_Program 0x02
#define Sector_Erase_4KB 0x20
#define Block_Erase_32KB 0x52
#define JEDEC_ID 0x9F
#define Read_Data 0x03
#endif
2、硬件模拟
NSS继续使用软件模拟实现。硬件SPI根据手册有连续读写方法和非连续读写方法,非连续方法在通信速率高时会造成时间浪费,下面程序中有连续、非连续SPI函数,读写W25Q64使用连续方法。
|------|--------|----------------------------------------------------------------------------------------------------|------------------------|------------------------|
| 文件名 | main.c | Hardware_ W25Q64.c | Hardware_ W25Q64.h | Hardware_ W25Q64Reg .h |
| 文件内容 | 主函数 | 硬件SPI初始化,硬件通信单元函数(起始、停止、交换1Byte、交换指定字节、等待状态寄存器置位函数),W25Q64读写函数(扇擦除、忙状态等待、W25Q64写、W25Q64读、ID获取、写使能) | Hardware_ W25Q64.c 头文件 | 寄存器宏定义、共用体等 |