SPI通信-(STM32)

一、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 头文件 | 寄存器宏定义、共用体等 |

相关推荐
亿道电子Emdoor2 小时前
【ARM】Keil恢复默认设置
arm开发·stm32·单片机
电子小子洋酱2 小时前
ESP32移植Openharmony外设篇(7)土壤湿度传感器YL-69
单片机·物联网·华为·harmonyos·鸿蒙
时光の尘5 小时前
嵌入式Linux(二)·配置VMware使用USB网卡连接STM32MP157实现Windows、Ubuntu以及开发板之间的通信
linux·服务器·c语言·windows·stm32·ubuntu
沐欣工作室_lvyiyi6 小时前
基于单片机的家庭智能垃圾桶(论文+源码)
人工智能·stm32·单片机·嵌入式硬件·单片机毕业设计·垃圾桶
小禾苗_6 小时前
51单片机——共阴数码管实验
单片机·嵌入式硬件·51单片机
i只喝怡宝8 小时前
基于辉芒51单片机的5档调光灯
c语言·单片机·嵌入式硬件·51单片机
云山工作室8 小时前
基于单片机的人体健康指标采集系统设计
单片机·嵌入式硬件·毕业设计·毕设
Asa3199 小时前
Personal APP
嵌入式硬件·个人开发·极限编程
佳心饼干-12 小时前
单片机-静动态数码管实验
单片机·嵌入式硬件
1101 110113 小时前
STM32-笔记35-DMA(直接存储器访问)
笔记·stm32·嵌入式硬件