目录
STM32 中 SPI 特点 -- 同步 全双工 单端 串行
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.1
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;//数据位的宽度 根据从设备来确定 W25Q64手册中文9.1.1
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;//高位还是低位先发 根据从设备来确定 W25Q64手册中文9.1.1
SPI_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);
}
}
}
}