11.SPI和W25Q64
目录
SPI 通信
常见的数字通信接口和协议
数字通信的共性名词
所谓的发送和接收
一主多从和点对点
SPI 简介
SPI 物理层和总线结构
SPI 如何传输数据
SPI 通信模式
如何选择使用哪种模式
SPI 相关的问题
实现 SPI 的两种方法
硬件 SPI
软件 SPI
SPI 的参数选择
SPI 的核心
STM32 中的 SPI 讲解
STM32 中 SPI 特点 -- 同步 全双工 单端 串行
STM32 中 SPI 的框图
STM32 的 SPI 主模式发送和接收
STM32 总共有几个硬件 SPI
使用 STM32 的 SPI 的 GPIO 的配置
FLASH 存储—W25Q64
常见的存储器
EEPROM
FLASH
存储器的作用
Flash 存储器
W25Q64
W25Q64 的容量布局
W25Q64 引脚和接口
选择W25Q64 通信模式
指令操作
时序图的读取
W25Q64 通信需要注意的细节
代码
SPI 通信
常见的数字通信接口和协议
常见数字通信接口和协议:UART 单总线(DHT11) IIC SPI CAN
数字通信的共性名词
所谓的发送和接收
发送(输出):发送方控制数据线的高低电平
接收(输入):接收方读取对方控制数据线的高低电平
一主多从和点对点
一主多从:1 个主机可以同时和多个从机通信
点对点:通信只存在与两个设备之间
SPI 简介
一主多从(主从结构)
CS :片选,选择和谁通信
SCK :时钟线 有时钟线同步通信,没有时钟线异步通信。
MOSI : 主机输出,从机输入 这根线主机控制,控制这根线的高低电平
MISO : 主机输入,从机输出 这根线从机控制,控制这根线的高低电平
M:master S:slave O:out I:in
MOSI :(主机)控制这根线的高低电平 (从机)读取这跟线的高低电平
如果 STM32 作为主机 MOSI 要配置成(输出)模式
MISO :(从机)控制这根线的高低电平,(主机)读取这根线的高低电平
如果 STM32 作为主机 MISO 要配置成(输入)模式
SCK : 一般是主机控制时钟线
如果 STM32 作为主机 SCK 要配置成(输出)模式
CS : 由主机控制
如果 STM32 作为主机 CS 要配置成(输出)模式
SPI 物理层和总线结构
SPI 如何传输数据
SPI 通信模式
SPI 四种工作模式
时钟极性 CPOL(0/1)时钟相位 CPHA(0/1)
时钟极性 CPOL:空闲时候,时钟线的电平 0 空闲低电平 1 空闲高电平
时钟相位 CPHA:
CPHA=0,在串行同步时钟的第一个(奇数)跳变沿(上升或下降)数据被采样(接收)
CPHA=1,在串行同步时钟的第二个(偶数)跳变沿(上升或下降)数据被采样(接收)
如何选择使用哪种模式
看 SPI 接口从设备的手册,确认它支持什么模式
SPI 相关的问题
实现 SPI 的两种方法
实现 SPI:两种方法,硬件 SPI 和软件 SPI
硬件 SPI
硬件 SPI:使用单片机自带的硬件 SPI 控制器
需要输出引脚配置成复用功能,需要配置 SPI 的结构体
设备必须接在有 SPI 功能的引脚上
软件 SPI
软件(模拟)SPI:使用单片机的 GPIO 口拉高拉低模拟出来 SPI 的时序
输出引脚配置成通用的输出,不需要配置 SPI 的结构体
软件 SPI 只要使用普通的 GPIO 口就行
SPI 的参数选择
确定模式:根据从设备确定选择 SPI0—SPI3
确定高位在前还是低位在前:根据从设备确定
数据位宽度:根据从设备确定
确定速率:根据从设备确定
SPI 的核心
封装出来一个单字节读写函数
STM32 中的 SPI 讲解
STM32 中 SPI 特点 -- 同步 全双工 单端 串行
STM32 中 SPI 的框图
NSS:
如果 STM32 作为从机,STM32 的片选引脚必须是 NSS
如果 STM32 作为主机,NSS 没有用
STM32 的 SPI 主模式发送和接收
STM32 总共有几个硬件 SPI
有三个硬件 SPI,如果使用硬件 SPI,必须将设备连接在有 SPI 功能的引脚上
如果使用的模拟(软件)SPI,只需要接在有 GPIO 功能,能拉高拉低电平就可以
使用 STM32 的 SPI 的 GPIO 的配置
NSS 这根线,如果 STM32 作为从机,必须配置成硬件主/从模式
如果 STM32 作为主机,这根线可以不接,但是这跟线也可以配置成普通的 GPIO 口,用来控制片选,选择从机
FLASH 存储—W25Q64
常见的存储器
EEPROM
特点:掉电不丢失,写入之前不用擦除,存储空间一般比较小
典型型号:AT24C02
器件接口:IIC
关系:AT24C02 是 EEPROM 的一种
FLASH
特点:掉电不丢失,写入之前必须擦除,存储空间一般比较大
典型型号:W25Q64
器件接口:SPI
关系:W25Q64 是 FLASH 的一种
存储器的作用
存 wifi 名称和密码 存设备编号(阿里云三要素) 存服务器地址等实现掉电不丢失
Flash 存储器
FLASH:掉电不丢失的存储 8G+256G 中的 256 就是 FLASH
芯片内部 FLASH:STM32F103ZET6 64K+512K 其中 512K 就是 FLASH
芯片外部 FALSH:单片机外部外接了 1 个芯片 -- 今天实现的
W25Q64:是 FLASH 的一种,不同厂家命名方法不一样
SPI:是一种重要的通信接口,和很多 SPI 接口设备通信。今天的 W25Q64 的接口就是 SPI
W25Q64
W25Q64 的容量布局
W25Q64 引脚和接口
选择W25Q64 通信模式
指令操作
需要用到的指令:写使能 读状态寄存器 页编程 扇区擦除 读数据
时序图的读取
以 0x90 为例
从时序图中获取的信息,选择模式 3
1. 主机把片选信号拉低
2. 主机发送 0x90 的命令,调用单字节发送函数,0x90 数据体现在 MOSI 这根线上,MOSI 接的 W25Q64 的DI,数据是确定的,所以 DI 的波形也是确定,因为是全双工,W25Q64 也会通过 DO(MISO)这根线给单片机发送数据,但是单片机知道这个数据没用,所以波形没有变化,单片机也可以不接收
3. 主机发送 24 位的地址,调用 3 次单字节发送函数,地址可能不确定,所以波形是胶囊的形状,胶囊证明此时数据,可能是 0/1
4. 从机接收到指定的命令或者数据之后,然后回复指定的内容,回复两个字节内容,数据体现在 DO(M ISO)这根线上,因为是全双工,主机也会通过 DI(MOSE)这根线给从机发送数据,主机发送的数据是没有意义的数据,所以 DI 的波形杂乱的。
5. 如果主机发送的地址是 0x000000,从机回复的数据是 0xEF 0x16
如果主机发送的地址是 0x000001,从机回复的数据是 0x16 0xEF
W25Q64 通信需要注意的细节
1. FLASH 使用的时候,必须先擦除,再写。擦除之后,里面放的数据全部都是 0xFF, FLASH 只能由 1 变 0, 不能由 0 变 1。
2. 最小擦除指令就是扇区(4K)擦除--扇区擦除
3. 写之前必须要写使能--写使能
4. 不能跨页写,超过 1 页(256 字节)会从该页的起始位置覆盖--页写
5. 指令执行完,检测状态寄存器是否操作完成--读状态
移植别人的代码的问题
1. 单字节读写函数不一致
可以通过宏定义改,或者把你的函数名字改了,或者改它的名字
2. 延时函数不一致
替换成你的延时函数
3. 官方代码示例代码给的是硬件 SPI1,结合板子修改SPI
代码
SPI.c
#include "SPI.h"/*
GPIO结合硬件,W25Q64接在SPI2上PB12 CS 通用推挽输出PB13 SCK 复用推挽输出PB14 MISO 浮空输入PB15 MOSI 复用推挽输出
*/
void SPI2_Config(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitTypeDef GPIO_InitStruct={0};GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_15;//待配置的引脚GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;//引脚速率GPIO_Init(GPIOB,&GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;//待配置的引脚GPIO_Init(GPIOB,&GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;//待配置的引脚GPIO_Init(GPIOB,&GPIO_InitStruct); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);SPI_InitTypeDef SPI_InitStruct={0};SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;//速率,波特率 根据从设备来确定 W25Q64手册中文 1 一般说明/*SPI2挂载在APB1总线(36M),如果2分频,就变成18M,W25Q64手册描述,支持80M,SPI速率<80M就可以*/SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;//时钟相位 根据从设备来确定 W25Q64手册中文9.1.1 SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;//时钟极性 根据从设备来确定 W25Q64手册中文9.1.1SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;//数据位的宽度 根据从设备来确定 W25Q64手册中文9.1.1SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;//高位还是低位先发 根据从设备来确定 W25Q64手册中文9.1.1SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//传输的方向SPI_InitStruct.SPI_CRCPolynomial = 0;//CRC校验的多项式 未使用 随便填SPI_InitStruct.SPI_Mode = SPI_Mode_Master; //主机模式SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; //软件模式/*如果STM32作为从机,必须配置成硬件模式,如果STM32作为主机,NSS没有用,配置成软件模式这样SPI2_NSS引脚就可以作为普通IO口使用,我们的硬件正好让NSS作为片选引脚了,所以配置NSS引脚软件模式,并且NSS引脚配置成通用推挽输出*///8.调用XXX_init函数将参数写入到寄存器中SPI_Init(SPI2,&SPI_InitStruct);//9.调用XXX_Cmd函数,将外设使能
//void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState); stm32f10x_spi.h 451行SPI_Cmd(SPI2,ENABLE);//10.释放从机GPIO_SetBits(GPIOB,GPIO_Pin_12); //中文固件库翻译手册 10.2.10
}//单字节发送和接收,一个字节8位
uint8_t SPI2_Send_Rec_Byte(uint8_t Byte)
{
//FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG); stm32f10x_spi.h 465行
//void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG); stm32f10x_spi.h 466行//1.先检测一下上次是否发完while(SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_TXE)==RESET);//2.上一次发送完成之后,发送新的数据
//void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data); stm32f10x_spi.h 455行SPI_I2S_SendData(SPI2,Byte);//3.检测是否接收到数据while(SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_RXNE)==RESET); //4.接收数据并返回
//uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx); stm32f10x_spi.h 456行return SPI_I2S_ReceiveData(SPI2);
}
W25Q64.c
#include "W25Q64.h"
#include "SPI.h"
#include "stdio.h"//通过0x90,获取芯片ID
//按照W25QW64
//验证通信是否成功
void W25Q64_Read_ID_0x90(void)
{uint8_t Buff[2]={0};//1.片选信号拉低GPIO_ResetBits(GPIOB,GPIO_Pin_12); //2.发送0x90的命令SPI2_Send_Rec_Byte(0x90);//3.发送24位的地址SPI2_Send_Rec_Byte(0x00); SPI2_Send_Rec_Byte(0x00); SPI2_Send_Rec_Byte(0x00); //4.连续接收两个字节数据Buff[0]=SPI2_Send_Rec_Byte(0xFF); //0xFF假数据,只要不和命令冲突,任意数据都可以 Buff[1]=SPI2_Send_Rec_Byte(0xFF); //5.把片选信号拉高GPIO_SetBits(GPIOB,GPIO_Pin_12); printf("0x90命令返回的结果:%x\r\n",(Buff[0]<<8)+Buff[1]);
}void W25Q64_Read_ID_0x9F(void)
{uint8_t Buff[3]={0};//1.片选信号拉低GPIO_ResetBits(GPIOB,GPIO_Pin_12); //2.发送0x90的命令SPI2_Send_Rec_Byte(0x9F);//4.连续接收三个字节数据Buff[0]=SPI2_Send_Rec_Byte(0xFF); //0xFF假数据,只要不和命令冲突,任意数据都可以 Buff[1]=SPI2_Send_Rec_Byte(0xFF);Buff[2]=SPI2_Send_Rec_Byte(0xFF);//5.把片选信号拉高GPIO_SetBits(GPIOB,GPIO_Pin_12);printf("0x90命令返回的结果:%x\r\n",(Buff[0]<<16)+(Buff[1]<<8)+Buff[2]);
}//写使能 参考W25Q64英文的11.2.4编程
void sFLASH_WriteEnable(void)
{// (CS) 引脚设置为低电平sFLASH_CS_LOW();//发送一个字节函数,定义了宏sFLASH_SendByte(sFLASH_CMD_WREN);// (CS) 引脚设置为低电平sFLASH_CS_HIGH();
}//读状态寄存器
//参考W25Q64中文10.2.6
void sFLASH_WaitForWriteEnd(void)
{uint8_t flashstatus = 0;/*!< 选择 Flash 存储器:将 Chip Select 置为低电平 */sFLASH_CS_LOW();/*!< 发送 "读取状态寄存器" 指令 */sFLASH_SendByte(sFLASH_CMD_RDSR);/*!< 循环,直到 Flash 存储器完成写入操作 */do{/*!< 发送一个空字节,以生成 Flash 所需的时钟,并将状态寄存器的值存入 flashstatus 变量 */flashstatus = sFLASH_SendByte(sFLASH_DUMMY_BYTE);}while ((flashstatus & sFLASH_WIP_FLAG) == SET);/* 写入进行中 *//*!< 取消选择 Flash 存储器:将 Chip Select 置为高电平 */sFLASH_CS_HIGH();
}//页编程 不支持跨页
//参考W25Q64中文10.2.14
/*
参数1 待写入数据的首地址
参数2 写入到W25Q64中的地址
参数3 待写入的长度
*/
void sFLASH_WritePage(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{/*!< 启用对 Flash 存储器的写访问权限 */sFLASH_WriteEnable();/*!< 选择 Flash 存储器:将 Chip Select 置为低电平 */sFLASH_CS_LOW();/*!< 发送 "写入内存" 指令 */sFLASH_SendByte(sFLASH_CMD_WRITE);/*!< Send WriteAddr high nibble address byte to write to *//*!< 发送写入地址的高字节 */sFLASH_SendByte((WriteAddr & 0xFF0000) >> 16);/*!< Send WriteAddr medium nibble address byte to write to *//*!< 发送写入地址的中间字节 */sFLASH_SendByte((WriteAddr & 0xFF00) >> 8);/*!< Send WriteAddr low nibble address byte to write to *//*!< 发送写入地址的低字节 */sFLASH_SendByte(WriteAddr & 0xFF);/*!< while there is data to be written on the FLASH *//*!< 当还有数据需要写入 Flash 时 */while (NumByteToWrite--){/*!< Send the current byte *//*!< 发送当前字节的数据 */sFLASH_SendByte(*pBuffer);/*!< Point on the next byte to be written *//*!< 移动到下一个字节 */pBuffer++;}/*!< Deselect the FLASH: Chip Select high *//*!< 取消选择 Flash 存储器:将 Chip Select 置为高电平 */sFLASH_CS_HIGH();/*!< Wait the end of Flash writing *//*!< 等待 Flash 完成写入操作 */sFLASH_WaitForWriteEnd();
}//扇区擦除
//参考W25Q64中文10.2.16
//参数 扇区的首地址
void sFLASH_EraseSector(uint32_t SectorAddr)
{/*!< 发送写使能指令 */sFLASH_WriteEnable();/*!< 扇区擦除 *//*!< 选择 Flash 存储器:将 Chip Select 置为低电平 */sFLASH_CS_LOW();/*!< 发送扇区擦除指令 */sFLASH_SendByte(sFLASH_CMD_SE);/*!< 发送扇区地址的高字节 */sFLASH_SendByte((SectorAddr & 0xFF0000) >> 16);/*!< 发送扇区地址的中间字节 */sFLASH_SendByte((SectorAddr & 0xFF00) >> 8);/*!< 发送扇区地址的低字节 */sFLASH_SendByte(SectorAddr & 0xFF);/*!< 取消选择 Flash 存储器:将 Chip Select 置为高电平 */sFLASH_CS_HIGH();/*!< 等待 Flash 完成写入操作 */sFLASH_WaitForWriteEnd();
}//读数据
//参考W25Q64中文10.2.8
/*
参数1 读取的数据存放的地址
参数2 读取W25Q64的地址
参数3 读取的长度
*/
void sFLASH_ReadBuffer(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{sFLASH_CS_LOW();sFLASH_SendByte(sFLASH_CMD_READ);sFLASH_SendByte((ReadAddr & 0xFF0000) >> 16);sFLASH_SendByte((ReadAddr& 0xFF00) >> 8);sFLASH_SendByte(ReadAddr & 0xFF);/*!< 当还有数据需要读取时 */while (NumByteToRead--) /*!< while there is data to be read */{/*!< Read a byte from the FLASH *//*!< 从 Flash 读取一个字节 */*pBuffer = sFLASH_SendByte(sFLASH_DUMMY_BYTE);/*!< Point to the next location where the byte read will be saved *//*!< 指向下一个存储读取字节的位置 */pBuffer++;}sFLASH_CS_HIGH();
}//跨页写
/*
参数1 待写入数据首地址
参数2 写到w25q64中的地址
参数3 待写入的长度
*/
void sFLASH_WriteBuffer(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;// 计算写入地址的偏移量Addr = WriteAddr % sFLASH_SPI_PAGESIZE;// 计算该页剩余空间count = sFLASH_SPI_PAGESIZE - Addr;// 计算需要写入的完整页面数NumOfPage = NumByteToWrite / sFLASH_SPI_PAGESIZE;// 计算剩余不足一页的字节数NumOfSingle = NumByteToWrite % sFLASH_SPI_PAGESIZE;if (Addr == 0) /*!< WriteAddr 已对齐到 sFLASH_PAGESIZE */{if (NumOfPage == 0) /*!< NumByteToWrite 小于 sFLASH_PAGESIZE */{// 如果写入数据少于一页,直接写入sFLASH_WritePage(pBuffer, WriteAddr, NumByteToWrite);}else /*!< NumByteToWrite 大于 sFLASH_PAGESIZE */{while (NumOfPage--)// 写入多页数据{sFLASH_WritePage(pBuffer, WriteAddr, sFLASH_SPI_PAGESIZE);WriteAddr += sFLASH_SPI_PAGESIZE;pBuffer += sFLASH_SPI_PAGESIZE;}// 写入最后不足一页的数据sFLASH_WritePage(pBuffer, WriteAddr, NumOfSingle);}}else /*!< WriteAddr 未对齐到 sFLASH_PAGESIZE */{if (NumOfPage == 0) /*!< NumByteToWrite 小于 sFLASH_PAGESIZE */{if (NumOfSingle > count) /*!< (NumByteToWrite + WriteAddr) 大于一页 */{temp = NumOfSingle - count;// 写入当前页剩余的数据sFLASH_WritePage(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;// 写入下一页的数据sFLASH_WritePage(pBuffer, WriteAddr, temp);}else{// 如果写入的数据不超过一页,直接写入sFLASH_WritePage(pBuffer, WriteAddr, NumByteToWrite);}}else /*!< NumByteToWrite 大于 sFLASH_PAGESIZE */{// 计算去除当前页后剩余的数据NumByteToWrite -= count;NumOfPage = NumByteToWrite / sFLASH_SPI_PAGESIZE;NumOfSingle = NumByteToWrite % sFLASH_SPI_PAGESIZE;// 写入当前页剩余的数据sFLASH_WritePage(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;// 写入剩余的完整页面数据while (NumOfPage--){sFLASH_WritePage(pBuffer, WriteAddr, sFLASH_SPI_PAGESIZE);WriteAddr += sFLASH_SPI_PAGESIZE;pBuffer += sFLASH_SPI_PAGESIZE;}// 写入最后不足一页的数据if (NumOfSingle != 0){sFLASH_WritePage(pBuffer, WriteAddr, NumOfSingle);}}}
}