SPI通信

简介

SPI Serial Peripheral Interface )是由 Motorola 公司开发的一种通用数据总线
四根通信线: SCK Serial Clock )、 MOSI Master Output Slave Input )、 MISO Master Input Slave Output )、 SS Slave Select
同步,全双工
支持总线挂载多设备(一主多从)
没有应答机制

相对I2C的优点缺点:1,传输速度快,可达80MHz;2,设计简单,学习容易;3,硬件资源消耗多,容易造成资源浪费

SPI通信_第1张图片SPI通信_第2张图片SPI通信_第3张图片

硬件电路 

所有 SPI 设备的 SCK MOSI MISO 分别连在一起
主机另外引出多条 SS 控制线,分别接到各从机的 SS 引脚
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入(得益于其推挽输出,高电平和低电平都有很强的驱动能力,传输数据就快,I2C不使用推挽输出是因为其一根通讯线实现收发数据(半双工)所以需要不断切换输入和输出,且I2C又要实现多主机的时钟同步和总线仲裁,所以I2C放弃了高性能换取了多功能)
·多设备需要共地
·从机的SS线低电平有效

SPI通信_第4张图片

 移位寄存器

SPI通信_第5张图片

SPI时序基本单元 

起始条件: SS 从高电平切换到低电平
终止条件: SS 从低电平切换到高电平

 SPI通信_第6张图片

交换一个字节(模式 0
CPOL=0 :空闲状态时, SCK 为低电平
CPHA=0 SCK 第一个边沿移入数据,第二个边沿移出数据

 SPI通信_第7张图片

交换一个字节(模式 1
CPOL=0 :空闲状态时, SCK 为低电平
CPHA=1 SCK 第一个边沿移出数据,第二个边沿移入数据

SPI通信_第8张图片

CPHA决定了是那个SCK开始采样

模式2和模式3类似与这两个模式,只不过SCK的高低电平翻转了而已

W25Q64简介

SPI通信_第9张图片

Dual SPI: 双重SPI模式,在一个SCK电平变化下,一次性交换两个数据

Quad SPI:四重SPI模式,同双重SPI模式

 硬件电路

SPI通信_第10张图片

 对于引脚旁括号的内容:即双重SPI模式或者四重SPI模式下的多位数据传输的通道,当为双重SPI模式时,DO(I01)和DI(IO0)就是一次性传输的两位数据,IO2和IO3同理。

W25Q64框图

SPI通信_第11张图片

一整个存储空间,首先先被划分为若干块,对应每一块又被划分为若干扇区,对于整个空间,又会被划分为很多页,每页256个字节

Page地址锁存器:用于指定我们要操作的页

Byte地址锁存器:用于指定操作我们指定的页中指定的字节

我们发送的24位地址(3字节地址)前两位是Page地址,会发送到Page地址锁存器中,后一位是字节地址,会发送到Byte地址锁存器中。

以上两个寄存器都有计数器,所以他们的地址指针是可以在读写之后可以自动加1的,实现从指定地址开始,连续读写多个字节的目的了。

写入的数据会先在RAM缓存区(Column Decode)中存储,在时序结束后,芯片再把缓存区中的数据复制到对应的Flash中,进行永久保存

因为SPI的写入频率非常高,而数据是要放进Flash中永久存储的,这个过程比较慢,所以写入的数据先放在页缓存区中存着,缓存区是RAM,所以速度非常快,可以跟上SPI总线的速度,但是这个缓存区也是有内存限制的(256Byte),所以写入的一个时序,连续写入的数据量不能超过256字节。

当我们发送好数据后,芯片才慢慢地把页缓存区的数据传入到Flash中,这会占用一定的时间,写入时序结束后,芯片就会进入“Buzy”的时间内,这时就会置标志位到status寄存器中,在这段时间内,芯片将不会响应新的读写时序。

Flash注意事项

写入操作时:

写入操作前,必须先进行写使能
每个数据位只能由 1 改写为 0 ,不能由 0 改写为 1
写入数据前必须先擦除,擦除后,所有数据位变为 1
擦除必须按最小擦除单元进行(最小单位:扇区)
连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

状态 寄存器

SPI通信_第12张图片

 Buzy如之前所说;

写使能:在每写入一个数据后,状态寄存器会自动配置为写失能,代表我们每写入一个字节之气那都需要配置为写使能,每一个写使能只能保证后续的一条写指令

指令集

ID

SPI通信_第13张图片 写使能

 页编程(前三个字节指定地址,后面一个写入数据,如果继续写入数据,指定的地址会自动加1,但是要注意范围为页)

擦除指令(标注的是最小的扇区擦除) 

 读取ID

读取数据

代码实操 

1,开启时钟,配置GPIO口(软件SPI任选引脚)

void MySPI_Init(void)
{
	//引脚初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	MySPI_W_SS(1);
	MySPI_W_SCK(0);
}

2,与软件I2C类似,使用函数封装一下置每个引脚高低电平的操作(模拟SPI)

//用于模拟每个端口的操作
void MySPI_W_SS(uint8_t Value)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)Value);
}

void MySPI_W_SCK(uint8_t Value)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)Value);
}

void MySPI_W_MOSI(uint8_t Value)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)Value);
}

uint8_t MySPI_R_MISO(void)
{
	GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

同时需要在初始化函数中添加

    MySPI_W_SS(1);
	MySPI_W_SCK(0);

3,根据时序来编写各个操作的函数

SPI通信_第14张图片例如开始——只需要把SS置0

void MySPI_Start(void){
	MySPI_W_SS(0);
}

void MySPI_Stop(void){
	MySPI_W_SS(1);
}
//掩码方式(优点,可以保持ByteSend不变)
uint8_t MySPI_WriteReadByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive=0x00;
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_SCK(1);
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	return ByteReceive;
}

//移位版(实现思路与SPI传输数据相同)
uint8_t MySPI_WriteReadByte1(uint8_t ByteSend)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_SCK(1);
		MySPI_W_MOSI(ByteSend & 0x80);
		ByteSend <<= 1;
		if (MySPI_R_MISO() == 1){ ByteSend |= 0x01;}
		MySPI_W_SCK(0);
	}
	return ByteSend;
}

4,编写W25Q64的相关函数

void W25Q64_Init(void)
{
	MySPI_Init();
}
void W25Q64_GetID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	//指令集中获取设备ID号的指令
	MySPI_WriteReadByte(W25Q64_JEDEC_ID);
	//第一个字节为厂商ID
	*MID = MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
	//第二个字节为设备ID高8位,第三个字节位设备ID低8位
	*DID = MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
	*DID <<= 8;
	*DID |= MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
	MySPI_Stop();
}

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_ins.h"

void W25Q64_Init(void)
{
	MySPI_Init();
}

void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	//指令集中获取设备ID号的指令
	MySPI_SwapByte(W25Q64_JEDEC_ID);
	//第一个字节为厂商ID
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	//第二个字节为设备ID高8位,第三个字节位设备ID低8位
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	MySPI_Stop();
}

void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

void W25Q64_WaitBusy(void)
{
	MySPI_Start();
	//读取状态寄存器比较特殊,写入以下值后,是连续读出状态寄存器的值
	uint32_t Timeout=100000;
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01 )== 0x01)
	{
		Timeout--;//防止卡死
		if (Timeout == 0)
		{
			break;
		}
	}
	MySPI_Stop();
}

//注意一次性只能发送一页(256个字节)的数据,Count最大值也只能是256
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	//事前等待(更高效,但是在每个操作函数的开头都需要加)
	W25Q64_WaitBusy();
	
	//写使能
	W25Q64_WriteEnable();
	
	MySPI_Start();
	//指令集中的PageProgram
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	//传输地址
	//因为地址是24位的,所以需要把地址分成8位一份发给从机
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	//发送数据
	for (uint16_t i = 0; i < Count; i ++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
	
}

//擦除一个扇形区
void W25Q64_SectorErase(uint32_t Address)
{
	//事前等待(更高效,但是在每个操作函数的开头都需要加)
	W25Q64_WaitBusy();
	//写使能
	W25Q64_WriteEnable();
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	MySPI_Stop();
	
}

void W25Q64_ReadData(uint32_t Address, uint8_t *Array, uint32_t Count)
{
	//事前等待(更高效,但是在每个操作函数的开头都需要加)
	W25Q64_WaitBusy();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for (uint32_t i = 0; i < Count; i ++)
	{
		Array[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}

主函数

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t Array_W[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t Array_R[4];

int main(void)
{
	OLED_Init();
	W25Q64_Init();
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	W25Q64_ReadID(&MID, &DID);
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	
	W25Q64_SectorErase(0x000000);
	W25Q64_PageProgram(0x000000, Array_W, 4);
	W25Q64_ReadData(0x000000, Array_R, 4);
	
	OLED_ShowHexNum(2,3,Array_W[0],2);
	OLED_ShowHexNum(2,6,Array_W[1],2);
	OLED_ShowHexNum(2,9,Array_W[2],2);
	OLED_ShowHexNum(2,12,Array_W[3],2);
	
	OLED_ShowHexNum(3,3,Array_R[0],2);
	OLED_ShowHexNum(3,6,Array_R[1],2);
	OLED_ShowHexNum(3,9,Array_R[2],2);
	OLED_ShowHexNum(3,12,Array_W[3],2);
	while(1)
	{
		
	}
}

SPI硬件

STM32 内部集成了硬件 SPI 收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻 CPU 的负担
可配置 8 /16 位数据帧、高位先行 / 低位先行
时钟频率: f PCLK / (2, 4, 8, 16, 32, 64, 128, 256)
支持多主机模型、主或从操作
可精简为半双工 / 单工通信
支持 DMA
兼容 I2S 协议
STM32F103C8T6 硬件 SPI 资源: SPI1 SPI2

 SPI框图

SPI通信_第15张图片

SPI基本结构 

SPI通信_第16张图片

 主模式全双工连续传输SPI通信_第17张图片非连续传输

SPI通信_第18张图片

 非连续传输的等待空隙在频率比较高时没太大影响,但是一旦频率很低,这个影响就不能被忽略了,所以我们在传输比较高频率的信号时不能使用非连续传输,可以使用连续输出或者进一步采用DMA自动转运。

其他内容真得看手册吧啊!

代码实操

SPI通信_第19张图片

函数介绍 

不用介绍

void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
//发送和接收数据
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

 标志位哥们

FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

 初始化流程

1,开启时钟,开启SPI和GPIO的时钟

2,初始化GPIO口,SCK和MOSI是硬件控制的输出引脚,配置为复用推挽输出,MISO是硬件外设的输入引脚,配置为上拉输入,SS引脚可由GPIO来模拟,配置位通用推挽输出

    //引脚初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

3,配置SPI外设

    SPI_InitTypeDef SPI_InitStruct;
	//主从
	SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
	/*裁剪SPI引脚的,即选择:
							单线半双工的接收模式Rx
							单线半双工的发送模式Tx
							双线全双工Full
							双线只接收模式RxOnly*/
	SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	//数据帧8位还是16位
	SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
	//低位先行or高位先行
	SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
	//SCK时钟的频率(即选择分频系数
	SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
	//SPI模式
	//时钟极性-这里选择了默认为低电平
	SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
	//1Edge即为0,2Enge即为1
	SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
	//软件实现NSS
	SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
	//不用了解
	SPI_InitStruct.SPI_CRCPolynomial = 7;
	SPI_Init(SPI1,&SPI_InitStruct);

4,开关控制

    SPI_Cmd(SPI1, ENABLE);
	MySPI_W_SS(1);

交换数据的函数

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	//必须发送同时接收,两个过程是绑定进行的
	while(!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE));
	
	SPI_I2S_SendData(SPI1, ByteSend);
	
	//在发送的同时MISO还会移位进行接收,发送和接收是同步的
	//接收移位完成了也就代表发送移位完成了
	//接收完成时会置标志位RXNE,我们可以借此来判断是否发送完数据
	while(!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE));
	//标志位不需手动清除
	//读取数据
	return SPI_I2S_ReceiveData(SPI1);
}

你可能感兴趣的:(单片机,嵌入式硬件,1024程序员节)