stm32-SPI通信协议、软件SPI

简介

SCK 就是时钟线,MOSI 是主机输出从机输入,MISO 是主机输入从机输出,SS是从机选择。有时候也不叫这些名字。

相比于I2C使用最少的线实现最多功能的复杂设计,SDA既收发数据,还要寻址和收发应答,SPI就是使用更多的线追求简单快速 实现功能,但这么多线也会造成资源浪费并且成本高。MOSI只用于主机向从机发送数据,MISO只用于主机接收从机的数据,SPI也不像I2C一样要进行寻址,它直接给每个从机都分配一根SS线,有多少从机就有多少SS线。

SPI是同步、全双工 。只支持一主多从,不支持多主机模式。

硬件电路

对于主机,SCK、SS和MOSI只会输出,MISO只会输入,所以不会存在冲突问题。主机和从机之间也需要共地,GND连接在一起,从机没有独立供电时主机也要给从机供电。

输出引脚都配置为推挽输出 ,相比于I2C开漏输出的弱上拉,推挽输出对高低电平都有强驱动能力,电平变化迅速,所以SPI数据传输速度 比I2C快得多,可以达到几十兆赫兹。输入引脚配置为浮空输入或者上拉输入

主机要通信时就拉低对应设备的SS线,其他设备的SS都是高电平,为了避免多台从机发送数据造成冲突,SPI规定只有SS低电平即被选中的设备才能发送数据,MISO引脚配置为推挽输出;SS高电平即没被选择的设备MISO引脚必须为高阻态,相当于没有电平,不能发送数据。

移位示意图

收发数据靠的是主机和从机交换数据 ,主机内部的波特率发生器也就是时钟既连接主机又连接从机。和I2C一样是先高位后低位

两者内部的移位寄存器里放入要交换的数据,比如在时钟的上升沿时两个数据向左移位,如图就是主机的1和从机的0分别移到了MOSI和MISO线上,所以MOSI高电平,MISO低电平,两个寄存器右边的最低位空了出来;随后在时钟的下降沿时数据位1移入了从机寄存器的空位,数据位0移入主机寄存器空位。

下一个时钟上升沿数据继续左移,主机的0和从机的1被移到线上,所以MOSI低电平,MISO高电平,又分别产生了一个空位;再下降沿,0进入从机的空位,1进入主机的空位。后面的6位数据都如此循环,最后数据交换完成,同时进行了数据发送和接收。也就相当于上升沿改变电平,下降沿采集电平了。SPI不需要应答。

如果主机想只发送不接收,依然需要和从机交换数据,只不过从机发来的数据主机不处理就行了,一般从机会设置发送0x00或者0xff;主机只接收不发送也一样,和从机交换数据,只是主机发送的数据是随意的,从机也不会去处理,一般主机也是发送0x00或者0xff。

软件时序
基本单元

SS默认高电平,拉低表示对应从机被选中,在接下来数据交换过程一直低电平,最后要结束交换就置高电平。

模式0

CPOL 是时钟极性,CPHA 是时钟相位。每个都有两种状态,组合起来一共四种模式,模式0最常用,因为上升沿采样是同步数字电路(如D触发器)更高效的操作,电路设计更简单。

CPOL=0即空闲时,时钟线SCK为低电平。CPHA=0即SCK第一个边沿移入数据,第二个边沿移出数据,但是要移入数据肯定要先移出数据产生空位,所以模式0中在SCK第一个边沿之前就已经移出数据了,并且几乎是在SS下降沿的时候移出的。之后进行数据交换,交换一个字节之后如果要结束就拉高SS,从机的MISO就回到高阻态,主机的MISO浮空状态或者高电平;

如果要交换多个字节,SS继续保持低电平,其余三根线重复交换字节的过程。

模式1

模式1下SCK第一个边沿移出数据,第二个边沿移入数据。

模式2

模式2下SCK空闲高电平。

模式3
指令

SPI使用的是发送指令+数据的方式,主机第一个字节向从机发送指令,从机内部也有一个指令集,指令内容由从机厂商规定。以存储芯片W25Q64为例,指令0x06表示写使能,从机收到数据就会控制内部使能。写使能就是单独的一个字节,后面不需要跟别的数据。这里主机不需要接收,从机就发的0xff。

又比如写指令 ,后面要跟上写的地址和内容,所以主机发送指令0x02之后要继续发送地址和内容。W25Q64芯片容量是8M字节,需要23个二进制位才能分配地址,多留一位备用就是24位,所以W25Q64一个地址由三个字节组成。

发送写指令后跟着发送三个字节的地址,比如地址0x123456,就先发0x12,然后发0x34,最后0x56。最后再发生写入的数据。这里主机也不需要接收数据,从机就发送的0xff。

SPI和I2C一样有地址指针,在对应地址读/写一次之后就会指向下一个地址操作了。

又比如读指令,主机发送读指令0X03后要跟着发送地址,然后从机发送数据。主机发送读指令和地址的时候不需要接收数据,从机就发送的0xff,之后主机需要接收数据了从机才开始发送指定地址下的数据,主机这时候就不需要发送数据了,主机就发送的0xff。

W25Q64操作Flash

Flash因为要实现掉电不丢失 ,所以在读写操作上和RAM有所不同,比如数据0不能改写为数据1,对于这个限制,需要先在写入之前进行擦除。但擦除也有限制,无法单独擦除某一个字节,一次最少要擦除一片扇区,即4096个字节。

连续写入多个字节时也有限制,写入的数据会先存入页缓存 中,因为它跟得上外部写入数据的速度,如果直接写入闪存的话速度跟不上。页缓存容量只有256字节,所以一次写入最多只能写入256字节,多了会覆盖。

等写入完成之后,芯片才会将数据转入Flash,这个过程属于忙状态 ,状态寄存器标志位BUSY 会置1,结束前不会响应新读写操作。除了写入数据 ,还有擦除 ,发送擦除指令后会进行擦除,这期间也会置BUSY,不会响应,还有写状态寄存器指令 。只会响应读状态寄存器和擦除挂起指令。忙状态结束后BUSY会置0。

状态寄存器中还有标志位WEL ,写使能锁存。发送写使能指令后WEL会置1,可以写入;发送写失能指令以及写入数据和擦除后WEL会置0,即不能写入了。这表明在每一次写入操作之前都要发送一次写使能指令,一次写使能指令只能保证后面一条写指令可以执行。

软件SPI读写W25Q64代码

MYSPI.c文件
cs 复制代码
#include "stm32f10x.h"                  // Device header
void MYSPI_W_SS(uint8_t bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)bitvalue);//改变SS
}

void MYSPI_W_MOSI(uint8_t bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)bitvalue);//改变MOSI
}

void MYSPI_W_SCK(uint8_t bitvalue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)bitvalue);//改变SCK
}

uint8_t MYSPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);//读MISO
}

void MYSPI_init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	GPIO_InitTypeDef GPIO_InitSturcture;
	GPIO_InitSturcture.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitSturcture.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;//SS,SCK,MOSI输出引脚为推挽输出
	GPIO_InitSturcture.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitSturcture);
	
	GPIO_InitSturcture.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitSturcture.GPIO_Pin=GPIO_Pin_6;//MISO输入引脚是上拉输入
	GPIO_InitSturcture.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitSturcture);
	
	MYSPI_W_SS(1);//默认不选中从机
	MYSPI_W_SCK(0);//模式0
}

void MYSPI_start(void)//开始
{
	MYSPI_W_SS(0);
}

void MYSPI_stop(void)//结束
{
	MYSPI_W_SS(1);
}

uint8_t MYSPI_byte(uint8_t byte)//交换一个字节
{
	uint8_t i,recebyte=0x00;
	for(i=0;i<8;i++)
	{
		MYSPI_W_MOSI(byte&(0x80>>i));//各自移出一位
		MYSPI_W_SCK(1);//准备移入数据
		if(MYSPI_R_MISO()==1){recebyte|=(0x80>>i);}//接收的数据放入recebyte
		MYSPI_W_SCK(0);//再准备移出数据
	}
	return recebyte;
}
W25Q64.c文件
cs 复制代码
#include "stm32f10x.h" 
#include "MYSPI.h"
#include "W25Q64_Ins.h"
void W25Q64_init(void)
{
	MYSPI_init();
}

void W25Q64_READID(uint8_t *MID,uint16_t *DID)//读取ID
{
	MYSPI_start();//开始
	MYSPI_byte(W25Q64_JEDEC_ID);//发送读取ID指令,此时不需要接收数据,不处理返回值
	*MID=MYSPI_byte(W25Q64_DUMMY_BYTE);//接收数据,从机会发三个字节,第一个字节是8位的MID,此时不需要发送数据,发一个0xff
	*DID=MYSPI_byte(W25Q64_DUMMY_BYTE);//第二个字节是16位的DID的高八位
	*DID<<=8;//左移八位,低位补零
	*DID|=MYSPI_byte(W25Q64_DUMMY_BYTE);//接收DID低八位,注意是与等于
	MYSPI_stop();//停止
}

void W25Q64_writeENABLE(void)//写使能指令
{
	MYSPI_start();
	MYSPI_byte(W25Q64_WRITE_ENABLE);
	MYSPI_stop();
}

void W25Q64_WaitBusy(void)//读取状态寄存器1最低位BUSY位,看是否处于忙状态
{
	uint32_t timeout=100000;
	MYSPI_start();
	MYSPI_byte(W25Q64_READ_STATUS_REGISTER_1);//发送读状态寄存器1指令
	while((MYSPI_byte(0xff)&0x01)==0x01)//读取从机发送的状态数据,取出最低位BUSY位,是1就还在忙
	{
		timeout--;
		if(timeout==0)
		{
			break;//设置超时退出
		}
	}
	MYSPI_stop();
}

void W25Q64_Pageprogram(uint32_t address,uint8_t *dataArray,uint16_t num)//页编程,即指定地址写
{
	uint16_t i;
	W25Q64_writeENABLE();//写使能
	MYSPI_start();
	MYSPI_byte(W25Q64_PAGE_PROGRAM);//发送页编程指令
	MYSPI_byte(address>>16);//接下来发送地址,24位地址要发三个字节
	MYSPI_byte(address>>8);//只会取低八位
	MYSPI_byte(address);
	for(i=0;i<num;i++)
	{
		MYSPI_byte(dataArray[i]);//发送写入内容,直接定义数组,发送几个num填几个
	}
	MYSPI_stop();
	W25Q64_WaitBusy();//等待退出忙状态
}

void W25Q64_SectorErase(uint32_t address)//扇区擦除
{
	W25Q64_writeENABLE();//写使能
	MYSPI_start();
	MYSPI_byte(W25Q64_SECTOR_ERASE_4KB);//发送扇区擦除指令
	MYSPI_byte(address>>16);//发送扇区地址,24位地址要发三个字节
	MYSPI_byte(address>>8);
	MYSPI_byte(address);
	MYSPI_stop();
	W25Q64_WaitBusy();//等待退出忙状态
}

void W25Q64_readdata(uint32_t address,uint8_t *dataArray,uint32_t num)//读取数据
{
	uint16_t i;
	MYSPI_start();
	MYSPI_byte(W25Q64_READ_DATA);//发送读取数据指令
	MYSPI_byte(address>>16);//发送读取地址,24位地址要发三个字节
	MYSPI_byte(address>>8);
	MYSPI_byte(address);
	for(i=0;i<num;i++)
	{
		dataArray[i]=MYSPI_byte(W25Q64_DUMMY_BYTE);//读取数据,数据放入数组,读取几个num填几个
	}
	MYSPI_stop();
}
main.c文件
cs 复制代码
#include "stm32f10x.h"// Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
uint8_t MID;
uint16_t DID;
uint8_t ArraySend[]={0x01,0x02,0x03,0x08};
uint8_t ArrayReceive[4];

int main()
{
	OLED_Init();
	W25Q64_init();
	W25Q64_READID(&MID,&DID);
	OLED_ShowHexNum(1,1,MID,2);
	OLED_ShowHexNum(1,8,DID,4);
	OLED_ShowString(2,1,"send:");
	OLED_ShowString(3,1,"rece:");
	W25Q64_SectorErase(0x000000);//写入之前先擦除扇区
	 W25Q64_Pageprogram(0x000000,ArraySend,4);
	 W25Q64_readdata(0x000000,ArrayReceive,4);
	OLED_ShowHexNum(2,6,ArraySend[0],2);
	OLED_ShowHexNum(2,9,ArraySend[1],2);
	OLED_ShowHexNum(2,12,ArraySend[2],2);
	OLED_ShowHexNum(2,15,ArraySend[3],2);
	
	OLED_ShowHexNum(3,6,ArrayReceive[0],2);
	OLED_ShowHexNum(3,9,ArrayReceive[1],2);
	OLED_ShowHexNum(3,12,ArrayReceive[2],2);
	OLED_ShowHexNum(3,15,ArrayReceive[3],2);
 while(1)
 {
	
 }
}

指令集.h文件

cs 复制代码
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H				//存放芯片指令集

#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1				0x05
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3

#define W25Q64_DUMMY_BYTE							0xFF

#endif
相关推荐
振南的单片机世界1 小时前
EXTI“一线多挂”陷阱:PA0和PB0同时触发,中断函数懵了
arm开发·stm32·单片机·嵌入式硬件
coward911 小时前
全志T507 (sun50iw9p1)sunxi pinctrl 子系统实现分析
linux·嵌入式硬件
ACP广源盛139246256731 小时前
Qwen3.8‑2.4T 开源落地@ACP#国产 MoE 大模型私有化部署中 MST 多屏转换芯片 GSV2231 硬件价值与应用场景
大数据·数据库·人工智能·嵌入式硬件
oshan20122 小时前
零基础STM32入门教程--04-工程文件分析
stm32·单片机·嵌入式硬件
lhbweilai3 小时前
FreeRTOS(事件组)
stm32·单片机·嵌入式硬件
szarron3 小时前
VNA6 便携式矢量网络分析仪:从入门到精通
数据库·嵌入式硬件·测试工具·5g·射频工程
恒锐丰科技韩生3 小时前
率能 SS8812T|8.2‑36V 双通道 H 桥电机驱动 ETSSOP28 带散热焊盘 打印机安防机器人专用
嵌入式硬件·硬件工程
210Brian3 小时前
STM32学习笔记(七)TIM定时中断(上)
笔记·stm32·学习
LCG元3 小时前
STM32 HAL 库串口 DMA + 空闲中断接收不定长数据:从原理到量产级稳定方案
stm32·单片机·嵌入式硬件