1、SPI介绍
SPI是一种通讯协议,目的是实现芯片与芯片之间、芯片和传感器之间的数据传输,数据传输可以是8位的也可以是16位的,具体情况具体分析。IIC可以理解成“汉语”,汉语的作用是会汉语的人之间交流的一种语言;SPI可以理解“英语”,英语的作用是会英语的人之间交流的一种语言。
学习SPI时,就是学习SPI的“语法规则”。
SPI通讯是全双工的,主机通过MOSI在发送1位数据时,主机的MISO也会接收1位数据。发送一个字节数据,相应的会接收到一个字节的数据。
2、SPI的总线接线图
- SPI通讯需要四个管脚:SCK(serial clock串行时钟,由主控产生脉冲方波时钟);MOSI(master output salve input,主机将数据一位一位的传输给从机的数据通道)/MISO(master input salve output,从机将数据一位一位的传输给主机的数据通道)/CS(chip select,低电平表示选择,高电平表示释放)。
- SPI总线可以挂载多个设备,如何区分多个设备呢?就是通过CS管脚来进行区分。每一个SPI从设备都对应有一个CS片选脚。主控和SPI从设备进行通信时,首先会将相应的从设备的片选管脚拉低。
3、SPI的时序
根据CPOL(时钟极性)和CPHA(CPHA)的状态的不同,CPOL的值和CPHA的值是通过寄存器来进行设置,每一款带硬件SPI的芯片可以设置CPOL和CPHA,SPI的时序分为四种:
如果 CPOL=0,串行同步时钟的空闲电平状态为低电平;如果CPOL=1,串行同步时钟的空闲电平状态高电平。
如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。
CPOL和CPHA组成成四种时序:
4、STM32F407ZGT6内部的硬件SPI控制器
4.1、SPI控制器的框图
数据发送过程
在发送缓冲区中写入字节时,即向SPIx->DR寄存器写入数据时,发送序列开始。在第一个位传输期间,数据字节并行加载到移位寄存器中,然后以串行方式移出到 MOSI 引脚。发送完成,则SPIx->SR寄存器的 TXE置 1,标志在数据从发送缓冲区传输到移位寄存器,标志着传送完成。
数据接收过程
在出现最后一个采样时钟边沿时, RXNE 位置 1, 移位寄存器中接收的数据字节被拷贝到接收缓冲区中。当读取 SPI_DR 寄存器时, SPI 外设将返回此缓冲值。
移位寄存器中的数据将传输到接收缓冲区,并且 RXNE 标志置 1,标志着接收完成。
4.2、SPI器件管理
STM32的SPI通信既可以作为主机,也可以作为从从机,对于主模式和从模式都可通过硬件或软件来进行管理:实现动态切换主/从操作。
硬件管理:通过NSS管脚的电平来决定是主机还是从机
软件管理:通过寄存器的SSI位的值来决定是主机还是从机。
4.3、硬件从器件管理
通过管脚的电平来决定SPI控制器是作为主机还是作为从机。
SSM清0,禁止软件从器件管理,NSS引脚的电平用于决定是主机模式还是 从机模式:NSS接高电平则是主机模式,NSS接低电平则是从机模式。如下图所示
4.4、软件从器件管理
SSM置1,使能软件从器件管理,SSI的值用于决定是主机模式还是从机模式:SSI置1则是主机模式,SSI清零则是从机模式。
本章节,我们是软件管理从器件,并且将SPI控制器配置为主机模式。
5、相关寄存器
5.1、SPI控制器1(SPI_CR1)
位 15 BIDIMODE: 双向通信数据模式使能
0:选择双线单向通信数据模式(全双工)
1:选择单线双向通信数据模式(半双工)
双线:MOSI/MISO
单向:MOSI只能用于数据输出,MISO只能用于数据输入
本章节,主要是学习SPI的读写是选择为“双线单向模式”,所以,该位需要清零。对应的代码:SPI1->CR1&=~(1<<15);
位 14 BIDIOE: 双向通信模式下的输出使能
此位结合 BIDIMODE 位,用于选择双向通信模式下的传输方向
0:禁止MOSI输出
1:使能MOSI输出
注意: 在主模式下,使用 MOSI 引脚
位 11 DFF: 数据帧格式
0:为发送/接收选择 8 位数据帧格式
1:为发送/接收选择 16 位数据帧格式
位 10 RXONLY: 只接收
0:全双工(发送和接收)
1:关闭输出(只接收模式)
需要清零,SPI1->CR1&=~(1<<10);
位 9 SSM: 软件从器件管理
当 SSM 位置 1 时, NSS 引脚输入替换为 SSI 位的值。
0:禁止软件从器件管理,使用硬件从器件管理。
1:使能软件从器件管理
该位需要置1,SPI1->CR1 |= (1<<9);
位 8 SSI: 内部从器件选择
当选择为软件从器件管理时
0:表示SPI控制器为从机
1:表示SPI控制器为主机
该位需要置1,SPI1->CR1 |= (1<<8);
位 7 LSBFIRST: 帧格式
0:先发送数据的最高位
1:先发送数据的最低位
本章节,主要是学习W25Q64的读写,W25Q64要求先传输最高位,该位需要清零SPI1->CR1&=~(1<<7);
位 6 SPE: SPI 使能
0:关闭外设
1:使能外设
该位需要置1
位 5:3 BR[2:0]: 波特率控制
000: fPCLK/2 100: fPCLK/32
001: fPCLK/4 101: fPCLK/64
010: fPCLK/8 110: fPCLK/128
011: fPCLK/16 111: fPCLK/256
fPCLK :是SPI的主时钟,主时钟经过分频后,则是SPI的串行时钟SCK的频率。
SPI1的主时钟是84MHZ
SPI2/SPI3的主时钟是42MHZ
提问:SPI1的SCK时钟配置2.625MHZ,那么这三位需要如何配置?
答:SPI1->CR1 |= (4<<3);
这里的配置需要考虑SPI从设备的波特率。
位 2 MSTR: 主模式选择
0:从配置
1:主配置
本章节,主要是学习W25Q64的从机,STM32作为主机
所以,该位需要置1,SPI1->CR1 |=(1<< 2);
位1 CPOL: 时钟极性
0:空闲状态时, SCK保持低电平
1:空闲状态时, SCK保持高电平
位 0 CPHA: 时钟相位
0:从第一个时钟边沿开始采样数据
1:从第二个时钟边沿开始采样数据
W25Q64支持时钟模式0:CPOL 为0,CPHA为0
W25Q64支持时钟模式3:CPOL 为1,CPHA为1
5.2、SPI状态寄存器
作用记录数据传输过程中的一些状态,我们主要关注的状态:①数据是否发送完成;②数据是否接收完成
位 1 TXE: 发送缓冲区为空 (Transmit buffer empty)
0:发送缓冲区非空
1:发送缓冲区为空,标志着发送已经完成
位 0 RXNE: 接收缓冲区非空 (Receive buffer not empty)
0:接收缓冲区为空
1:接收缓冲区非空,标志着接收到数据。
5.3、SPI数据寄存器
数据寄存器的有效位有16位,给数据寄存器赋值,则数据会通过移位寄存器一位一位发送出去。读取数据寄存器的值,可以接收数据。
5.4、SPI收发数据一体化函数
//data : 表示即将待发送的数据
//返回值:表示接收的数据
u8 SPI1_WriteRead(u8 data)
{
//位 1 TXE的值为1,则发送缓冲区为空,标志着发送已经完成
while(!(SPI1->SR &(1<<1)));
SPI1->DR = data;
//位 0 RXNE的值为1,则接收缓冲区非空,标志着接收到数据
while(!(SPI1->SR &(1<<0)));
return SPI1->DR;
}
6、W25Q64
W25Q64一个存储芯片。通讯接口是SPI接口。
6.1、原理图
6.2、W25Q64的芯片ID
W25Q64的ID号是0XEF16,0xEF表示华邦电子制造,0x16是设备ID。
读取W25Q64芯片ID的时序
对应的代码
u16 W25Q64_ReadDeviceID(void)
{
u16 deviceID;
FLASH_CS_L;
SPI1_WriteRead(0x90);
SPI1_WriteRead(0x00);
SPI1_WriteRead(0x00);
SPI1_WriteRead(0x00);
deviceID = SPI1_WriteRead(0xFF); //读取0xEF
deviceID = deviceID<<8;
deviceID |= SPI1_WriteRead(0xFF);//读取0x16,合并成0xEF16
FLASH_CS_H;
return deviceID;
}
6.3、W25Q64的内部存储结构
W25Q64的储存空间是:64Mbit,即8M Byte。
W25Q64的“页”:一页为256 字节。
W25Q64的“扇区”:4K个字节,即4096个字节为一个扇区。共有2048个扇区。
W25Q64的“块”:16个扇区为 1 块,64K个字节为一块,65535个字节,总共有128块。
W25Q64在写入之前必须先查出,使得每个字节空间的数据都为0xFF。
- W25Q64寻址
W25Q64每一个字节的空间都有对应的一个地址(编号)。类似于:超市存物柜。
- 块&扇区&页的结构图
- 整个W25Q64的存储结构图
6.4、W25Q64的状态寄存器
BUSY位是个只读位,位于状态寄存器中的S0。在器件中执行“页编程”“扇区擦除”“块区擦除”“芯片擦除”“写状态寄存器”指令时,该位自动置1.这时,除了“读状态寄存器”指令,其他指令都忽略,当编程、擦除和写状态寄存器指令执行完毕之后,该位自动变为0,表示芯片可以接受其它指令
WEL位是只读位,位于状态寄存器中的S1,执行完“写使能”指令后,该位置1,写使能指令将会使状态寄存器WEL位置位,在执行每个页编程,扇区擦除,块区擦除,芯片擦除和写状态寄存器命令之前,都要先置位WEL
- 读取状态寄存器的时序
- 对应代码
u8 W25Q64_ReadStatusRegister(void)
{
u8 status;
FLASH_CS_L;
SPI1_WriteRead(0x05);
status = SPI1_WriteRead(0xFF);
FLASH_CS_H;
return status;
}
6.5、写使能
写使能 指令将会使 状态寄存器 WEL位置位,在执行每个“页编程”“扇区擦除”“块区擦除”“芯片擦除”和“写状态寄存器”命令之前,都要先置位WEL
- 写使能时序
- 对应代码
void W25Q64_WriteEnable(void)
{
FLASH_CS_L;
SPI1_WriteRead(0x06);
FLASH_CS_H;
}
6.6、读数据
- 读数据时序
当片选CS/拉低之后,紧随其后是一个24位的地址(A23-A0)(24为的地址分3次发送,每次发送1个字节,先发高位)。W25Q64收到地址后,将要读的数据按字节传送给主机,读数据时,W25Q64的地址会自动增加,允许连续的读取多个字节的数据。数据读取完成之后,片选信号/CS拉高。
- 对应的代码
举例:从0x000000地址开始,读取50个字节
对应的代码:u8 dataBuf[50] ; //用于保存读取的数据
W25Q64_ReadDatas(0x000000,dataBuf,50);
void W25Q64_ReadDatas(u32 addr,u8 *pData,u32 count)
{
FLASH_CS_L;
SPI1_WriteRead(0x03);
SPI1_WriteRead((addr>>16)&0xFF); //发送地址的高8位
SPI1_WriteRead((addr>>8)&0xFF);//发送地址的中间8位
SPI1_WriteRead(addr&0xFF);//发送地址的低8位
while(count) //循环读取count个字节
{
*pData=SPI1_WriteRead(0xFF); //将读取到到一个字节数据保存起来
pData++; //指针偏移,用于保存下一个字节的数据
count--;
}
FLASH_CS_H;
}
6.7、扇区擦除
扇区擦除的作用是使得一个扇区的数据全部变为0xFF
- 对应时序
扇区擦除指令可以擦除指定一个扇区(4 k字节)内所有数据,使得被擦除的扇区的数据都变为0xFF。写入扇区擦除指令之前必须执行设备写使能(发送设备写使能指令0x06),并判断状态寄存器(状态寄存器位最低位必须等于0才能操作)。发送的扇区擦除指令前,先拉低/CS,接着发送扇区擦除指令码0X20 ,和24位地址(A23-A0),地址发送完毕后,拉高片选线CS/,并判断状态位,等待擦除结束。擦除一个扇区的最少需要150ms时间。
- 对应代码
void W25Q64_EraseSector(u32 addr)
{
u32 sectorPosition = addr/4096;
u32 sectorAddr = sectorPosition*4096;
FLASH_CS_L;
W25Q64_WriteEnable(); //写使能
while(0x01&W25Q64_ReadStatusRegister()); //提取状态寄存器的BUSY位
SPI1_WriteRead(0x20); //写命令
SPI1_WriteRead((sectorAddr>>16)&0xFF); //写地址
SPI1_WriteRead((sectorAddr>>8)&0xFF);
SPI1_WriteRead(sectorAddr&0xFF);
FLASH_CS_H;
while(0x01&W25Q64_ReadStatusRegister()); //提取状态寄存器的BUSY位
}
6.8、页编程(写数据)
- 时序
页编程指令允许写多个字节,但不能超过256个字节,页编程之前必须保证内存空间是0XFF,即先擦除再写入。在页编程之前,必须先发送写使能指令。写使能开启后,设备才能接收页编程指令。开启页编程,先拉底/CS,然后发送指令代码“02 h”,接着发送一个24位地址(A23-A0)(发送3次,每次8位)和至少一个字节的数据(数据字节不能超过256字节)。数据字节发送完毕,需要拉高片选线CS/,并判断状态位,等待写入结束。
- 对应代码
举例:将字符串"Hello world"写入页0
u8 string[ ] = "Hello world";
W25Q64_WritePage(0x000000,string,sizeof(string));
void W25Q64_WritePage(u8 addr,u8 *pData,u8 count)
{
W25Q64_WriteEnable(); //写使能
while(0x01&W25Q64_ReadStatus()); //提取状态寄存器的BUSY位
FLASH_CS_L;
Spi1_WriteRead(0x02);
Spi1_WriteRead((addr>>16)&0xFF);
Spi1_WriteRead((addr>>8)&0xFF);
Spi1_WriteRead(addr&0xFF);
while(count)
{
Spi1_WriteRead(*pData);
pData++;
count --;
}
FLASH_CS_H;
while(0x01&W25Q64_ReadStatus()); //提取状态寄存器的BUSY位
}
7、写FLASH函数封装
W25Q64一次最多是写一页,需要封装一个函数,实现写n个字节到Flash中(n>256)。
需要判断待写入的数据需要消耗多少“页”,在写的过程,需要动态调节写入的地址,以及写入的数据
必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
具有自动换页功能
在指定地址开始写入指定长度的数据,但是要确保地址不越界!
********************************************************************/
void W25QXX_WriteNoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 pageremain;
pageremain=256-WriteAddr%256; //单页剩余的字节数
if(NumByteToWrite<=pageremain)
pageremain=NumByteToWrite;//不大于256个字节
while(1)
{
W25q64_WritePage(WriteAddr,pBuffer,pageremain);
if(NumByteToWrite==pageremain)break;//写入结束了
else //NumByteToWrite>pageremain
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain; //减去已经写入了的字节数
if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
else pageremain=NumByteToWrite; //不够256个字节了
}
}
}
8、软件设计
模拟SPI读写W25Q64
原理图
寄存器版
#include "stm32f4xx.h"
#include "stdio.h"
#define FLSH_CS_H (GPIOB->ODR |= 1<<14)
#define FLSH_CS_L (GPIOB->ODR &=~(1<<14))
void Delay_us(u16 us)
{
SysTick->CTRL &=~(1<<2); //选择时钟源为21MHZ
SysTick->CTRL &=~(1<<1); //禁止滴答中断
SysTick->LOAD = 21*us; //设置重装载寄存器的值
SysTick->CTRL |= (1<<0); //使能滴答定时器
while(!(SysTick->CTRL&(1<<16)));//阻塞判断定时时间是否到达,判断SysTick->CTRL的位
}
/******************** 串口打印函数 ***************************************/
/* fputc是printf最底层的调用函数 */
int fputc(int data,FILE *file)
{
while( !(USART1->SR & (1 << 6)) );//等待发送完成
USART1->DR = data; //发送数据
return data;
}
void usart1_Init()
{
u16 integer ;
u16 fraction;
float USARTDIV;
u32 baudRate = 115200;
RCC->AHB1ENR |= 1<<0;//使能GPIOA的时钟 RCC_AHB1ENR
GPIOA->MODER &= ~(3<<18);//清零
GPIOA->MODER |= 2<<18;//设置PA9为复用功能模式MODER
GPIOA->MODER &= ~(3<<20);//清零
GPIOA->MODER |= 2<<20;//设置PA10为复用功能模式MODER
GPIOA->AFR[1]&= ~(0XF<<4);//清零
GPIOA->AFR[1]|= (7<<4);//设置PA9的复用功能为第7复用功能 RXD, AFRH
GPIOA->AFR[1]&= ~(0XF<<8);//清零
GPIOA->AFR[1]|= (7<<8);//设置PA10的复用功能为第7复用功能 TXD, AFRH
RCC->APB2ENR |= 1<<4;//使能串口1模块时钟 RCC_APB2ENR
USART1->CR1 |= 1<<15;//设置串口OVER8为1
USART1->CR1 &= ~(1<<12);//设置串口数据位长度 :1 起始位, 8 数据位
USART1->CR2 &=~(3<<12);//设置串口停止位长度 :1 个停止位
USART1->CR1 &= ~(1<<10);//无校验
USART1->CR1 |= 1<<3;//发送使能
USART1->CR1 |= 1<<2;//接收使能
USARTDIV = 84000000/8/baudRate;//串口波特率设置:USARTDIV = fCK/(8*(2-OVER8) /波特率
integer = (u16)USARTDIV;
fraction = ((u16)(USARTDIV-integer))<<4;
USART1->BRR |= integer<<4 | fraction;
USART1->CR1 |= 1<<13;//使能串口
}
//FLASH_CS →PB14
//SPI1_SCK →PB3
//SPI1_MISO →PB4
//SPI1_MOSI →PB5
void SPI1_Init(void)
{
RCC->AHB1ENR |= 1<< 1;//使能GPIOB时钟
GPIOB->MODER &=~(0x3F<<6); //清零
GPIOB->MODER |= 0x2A<<6; //PB3/4/5选择为复用功能模式
GPIOB->AFR[0] &=~(0xFFF<<12);//清零
GPIOB->AFR[0] |= 0x555<<12;//PB3/4/5选择复用功能5→AF5
//PB14配置为推挽输出
GPIOB->MODER &=~(3<<28); //清零
GPIOB->MODER |= 1<<28; //输出模式
GPIOB->OTYPER &=~(1<<14);//推挽
RCC->APB2ENR |= 1<<12;//使能SPI时钟
SPI1->CR1 &=~(1<<15);//选择双线单向通信数据模式 :全双工,MOSI发送数据,MISO接收数据
SPI1->CR1 &=~(1<<11);//配置数据帧格式是8位
SPI1->CR1 &=~(1<<10);//SPI1控制器全双工通信(发送和接收)
SPI1->CR1 |= 1<<9;//使能软件从器件管理,SSI的值决定是主机还是从机
SPI1->CR1 |= 1<<8;//SPI1控制器作为主机
SPI1->CR1 &=~(1<<7);//设置先送最高位
SPI1->CR1 |= (4<<3);//配置波特率2.625MHZ
SPI1->CR1 |= (1<<2);//主配置 : 配置为主机
//选择时钟模式0,CPOL和CPHA都要清零
SPI1->CR1 &=~(1<<1);
SPI1->CR1 &=~(1<<0);
SPI1->CR1 |= 1<<6;//使能SPI1
FLSH_CS_H; //片选拉高,表示“取消选择”
}
//data:待发送的数据
//返回值:是接收到的数据
u8 SPI1_SendReveiveData(u8 data)
{
u8 recvData = 0;
while(!(SPI1->SR&(1<<1))); //发送完成,跳出该循环
SPI1->DR = data; //SPI1发送一个字节数据,SPI1_DR寄存器执行写操作将 TXE 位清零。
while(!(SPI1->SR&(1<<0))); //接收完成,跳出该循环
recvData =SPI1->DR ; //读取接收到的一个字节数据,通过读取 SPI1_DR 寄存器将 RXNE 位清零。
return recvData;
}
//返回值:厂商号+ID号 0xEF16
u16 W25Q64_ReadID(void)
{
u8 mID = 0; //厂商号
u8 dID = 0; //设备号
FLSH_CS_L;//拉低片选:表示选择
SPI1_SendReveiveData(0x90);//发送指令代码“0x90”
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最高的字节
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送中间的字节
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最低的字节
mID = SPI1_SendReveiveData(0xFF);//读厂商号,0xEF
dID = SPI1_SendReveiveData(0xFF);//读取ID号,0x16
FLSH_CS_H;//拉高片选:表示取消选择
return mID<<8|dID;//返回厂商号+ID号 0xEF16
}
u8 W25Q64_ReadStatus(void)
{
u8 status = 0;
FLSH_CS_L;//片选拉低
SPI1_SendReveiveData(0x05);//STM32发送“读状态寄存器指令”→0x05给W25Q64
status = SPI1_SendReveiveData(0xFF);//STM32读取状态寄存器
FLSH_CS_H;//片选管脚拉高
return status;
}
void W25Q64_WriteEnable(void)
{
FLSH_CS_L;//片选拉低
SPI1_SendReveiveData(0x06);//STM32发送“写使能指令”→0x06给W25Q64
FLSH_CS_H;//片选管脚拉高
}
//addr:地址
//pBuf:读数据的存储区
//count :要读多少给字节
void W25Q64_ReadData(u32 addr,u8*pBuf,u32 count)
{
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x03);//发送“读数据指令”→0x03
//MOSI发送要读取的首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
//MISO可以读取到数据,可以连续读,W25Q64内部地址会自动递增。
while(count--)
{
*pBuf = SPI1_SendReveiveData(0xFF);
pBuf++;
}
FLSH_CS_H;//拉高片选
}
void W25Q64_EraseSector(u32 addr)
{
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x20);//发送“扇区擦除指令”→0x20
//MOSI发送要扇区的首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
}
void W25Q64_EraseChip(void)
{
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0xC7);//3)发送“芯片擦除指令”→0xC7
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
}
u8 W25Q64_WritePage(u32 addr,u8 *pData,u16 count)
{
if(count>256)
{
return 1;
}
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x02);//发送“页编程指令”→0x02
//MOSI发送要首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
//MOSI发送数据。W25Q64内部地址自动递增。发送多个字节的数据需要通过循环来完成。
while(count--)
{
SPI1_SendReveiveData(*pData);
pData++;
}
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“页编程动作”是否忙了。只有忙完“页编程”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
return 0;
}
int main(void)
{u8 writeBuf[ ] = {"欢度中秋"};
u8 readBuf[50] ={0};
u16 ID = 0x0000;
SPI1_Init();
usart1_Init();//初始化串口
ID = W25Q64_ReadID();
printf("W25Q64的ID号是0x%x\r\n",ID);
W25Q64_EraseSector(0x000000);
W25Q64_WritePage(0x000000,writeBuf,sizeof(writeBuf));
printf("写入到W25Q64的数据是 →%s\r\n",writeBuf);
W25Q64_ReadData(0x000000,readBuf,sizeof(writeBuf)) ;
printf("从W25Q64的读取到数据是 →%s\r\n",readBuf);
while(1)
{
}
}
库函数版
#include "stm32f4xx.h"
#include "stdio.h"
#define FLSH_CS_H (GPIOB->ODR |= 1<<14)
#define FLSH_CS_L (GPIOB->ODR &=~(1<<14))
void Delay_us(u16 us)
{
SysTick->CTRL &=~(1<<2); //选择时钟源为21MHZ
SysTick->CTRL &=~(1<<1); //禁止滴答中断
SysTick->LOAD = 21*us; //设置重装载寄存器的值
SysTick->CTRL |= (1<<0); //使能滴答定时器
while(!(SysTick->CTRL&(1<<16)));//阻塞判断定时时间是否到达,判断SysTick->CTRL的位
}
/******************** 串口打印函数 ***************************************/
/* fputc是printf最底层的调用函数 */
int fputc(int data,FILE *file)
{
while( !(USART1->SR & (1 << 6)) );//等待发送完成
USART1->DR = data; //发送数据
return data;
}
void usart1_Init()
{
u32 baudRate = 115200;
GPIO_InitTypeDef GPIO_InitStructe;
USART_InitTypeDef USART_InitStructe;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能 A 端口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //使能串口端口
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIO 端口映射到 USART
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIO 端口映射到 USART
// USART_DeInit(USART1); //复位
GPIO_InitStructe.GPIO_Mode =GPIO_Mode_AF; //配置为输出模式
GPIO_InitStructe.GPIO_Pin =GPIO_Pin_9|GPIO_Pin_10; //初始化 GPIOA9/10;
GPIO_Init(GPIOA,&GPIO_InitStructe);
USART_InitStructe.USART_BaudRate =baudRate; //波特率
USART_InitStructe.USART_HardwareFlowControl =USART_HardwareFlowControl_None;//无硬件流控制
USART_InitStructe.USART_Mode =USART_Mode_Tx|USART_Mode_Rx; //接收模式,发送模式使能
USART_InitStructe.USART_Parity =USART_Parity_No; //无奇偶校验
USART_InitStructe.USART_StopBits =USART_StopBits_1; //一位停止位
USART_InitStructe.USART_WordLength =USART_WordLength_8b;//8 位数据位
USART_Init(USART1,&USART_InitStructe);
USART_Cmd(USART1,ENABLE); //使能串口
}
//FLASH_CS →PB14
//SPI1_SCK →PB3
//SPI1_MISO →PB4
//SPI1_MOSI →PB5
void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef SPI_GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//使能 GPIOB 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);//使能 SPI1 时钟
//GPIOFB3,4,5 初始化设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;//PB3~5 复用功能输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3 复用为 SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB4 复用为 SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB5 复用为 SPI1
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置 SPI 单向或者双向的数据模式:SPI 设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置 SPI 工作模式:设置为主 SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置 SPI 的数据大小:SPI 发送接收8 位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由硬件(NSS 管脚)还是软件(使用SSI 位)管理:内部 NSS 信号有 SSI 位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; //定义波特率预分频的值:波特率预分频值为 32
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从 MSB 位还是 LSB 位开始:数据传输从 MSB 位开始
SPI_Init(SPI1, &SPI_InitStructure); //根据 SPI_InitStruct 中指定的参数初始化外设 SPIx 寄存器
SPI_Cmd(SPI1, ENABLE); //使能 SPI 外设
SPI_GPIO_InitStruct.GPIO_Mode=GPIO_Mode_OUT;
SPI_GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
SPI_GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14;
SPI_GPIO_InitStruct.GPIO_Speed=GPIO_Fast_Speed;
GPIO_Init(GPIOB,&SPI_GPIO_InitStruct);
GPIO_SetBits(GPIOB,GPIO_Pin_14);
}
//data:待发送的数据
//返回值:是接收到的数据
u8 SPI1_SendReveiveData(u8 data)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, data);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1);
}
//返回值:厂商号+ID号 0xEF16
u16 W25Q64_ReadID(void)
{
u8 mID = 0; //厂商号
u8 dID = 0; //设备号
FLSH_CS_L;//拉低片选:表示选择
SPI1_SendReveiveData(0x90);//发送指令代码“0x90”
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最高的字节
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送中间的字节
SPI1_SendReveiveData(0x00);//发送地址“0x00” ,发送最低的字节
mID = SPI1_SendReveiveData(0xFF);//读厂商号,0xEF
dID = SPI1_SendReveiveData(0xFF);//读取ID号,0x16
FLSH_CS_H;//拉高片选:表示取消选择
return mID<<8|dID;//返回厂商号+ID号 0xEF16
}
u8 W25Q64_ReadStatus(void)
{
u8 status = 0;
FLSH_CS_L;//片选拉低
SPI1_SendReveiveData(0x05);//STM32发送“读状态寄存器指令”→0x05给W25Q64
status = SPI1_SendReveiveData(0xFF);//STM32读取状态寄存器
FLSH_CS_H;//片选管脚拉高
return status;
}
void W25Q64_WriteEnable(void)
{
FLSH_CS_L;//片选拉低
SPI1_SendReveiveData(0x06);//STM32发送“写使能指令”→0x06给W25Q64
FLSH_CS_H;//片选管脚拉高
}
//addr:地址
//pBuf:读数据的存储区
//count :要读多少给字节
void W25Q64_ReadData(u32 addr,u8*pBuf,u32 count)
{
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x03);//发送“读数据指令”→0x03
//MOSI发送要读取的首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
//MISO可以读取到数据,可以连续读,W25Q64内部地址会自动递增。
while(count--)
{
*pBuf = SPI1_SendReveiveData(0xFF);
pBuf++;
}
FLSH_CS_H;//拉高片选
}
void W25Q64_EraseSector(u32 addr)
{
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x20);//发送“扇区擦除指令”→0x20
//MOSI发送要扇区的首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
}
void W25Q64_EraseChip(void)
{
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0xC7);//3)发送“芯片擦除指令”→0xC7
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“擦除动作”是否忙了。只有忙完“擦除”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
}
u8 W25Q64_WritePage(u32 addr,u8 *pData,u16 count)
{
if(count>256)
{
return 1;
}
W25Q64_WriteEnable();//执行写使能指令,
while(!((1<<1)&W25Q64_ReadStatus()));//读取状态寄存器,判断WEL位,判断是否已经进入“写使能状态”
FLSH_CS_L;//拉低片选
SPI1_SendReveiveData(0x02);//发送“页编程指令”→0x02
//MOSI发送要首地址,注意:高位先发送。
SPI1_SendReveiveData((addr>>16)&0xFF);
SPI1_SendReveiveData((addr>>8)&0xFF);
SPI1_SendReveiveData(addr&0xFF);
//MOSI发送数据。W25Q64内部地址自动递增。发送多个字节的数据需要通过循环来完成。
while(count--)
{
SPI1_SendReveiveData(*pData);
pData++;
}
FLSH_CS_H;//拉高片选
//读取状态寄存器,判断“页编程动作”是否忙了。只有忙完“页编程”之后,才可以去执行其他操作。
while(((1<<0)&W25Q64_ReadStatus()));
return 0;
}
int main(void)
{u8 writeBuf[ ] = {"欢度中秋"};
u8 readBuf[50] ={0};
u16 ID = 0x0000;
SPI1_Init();
usart1_Init();//初始化串口
ID = W25Q64_ReadID();
printf("W25Q64的ID号是0x%x\r\n",ID);
W25Q64_EraseSector(0x000000);
W25Q64_WritePage(0x000000,writeBuf,sizeof(writeBuf));
printf("写入到W25Q64的数据是 →%s\r\n",writeBuf);
W25Q64_ReadData(0x000000,readBuf,sizeof(writeBuf)) ;
printf("从W25Q64的读取到数据是 →%s\r\n",readBuf);
while(1)
{
}
}