一、SPI
1 SPI概念理解
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI 接口主要应用在 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器、屏幕,还有数字信号处理器和数字信号解码器之间。
SPI信号线
SCK:时钟线(由主机产生,用于同步数据,一个脉冲传输/接收一位数据),另名:C SCL SCLK
CS:使能控制端(主机控制从机),选定哪个从机可以工作
MOSI:Master Output Slave Input,主机输出,从机输入
MISO:Master Input Slave Output, 主机输入,从机输出
2 SPI总线四种工作方式
SPI 模块为了和外设模块进行数据交换,根据外设模块工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。
从机选择什么工作方式,则主机要配置成对应工作方式。
SPI时序的工作方式由时钟极性及时钟相位共同决定。
时序详解:
CPOL:时钟极性选择,为0时SPI总线空闲为低电平(时钟线由低电平开始),为1时SPI总线空闲为高电平(时钟线由高电平开始)
CPHA:时钟相位选择,为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样
采样就是数据此时发送或者可接收回来
时钟极性选择决定了时钟线先由高电平开始还是低电平开始。CPOL = 0,时钟线由低电平开始;CPOL = 1,时钟线由高电平开始;
CPOL = 0 CPHA = 1表示时钟脉冲低开始,在每一个脉冲下降沿进行采样(收发数据)
工作方式0:
当CPHA=0、CPOL=0时SPI总线工作在方式0。MISO引脚上的数据在第一个SPSCK沿跳变之前已经上线了(数据已经放在引脚上),而为了保证正确传输,MOSI引脚的MSB位必须与SPSCK的第一个边沿同步,在SPI传输过程中,首先将数据上线,然后在同步时钟信号的上升沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(下降沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。
工作方式1:
当CPHA=1、CPOL=0时SPI总线工作在方式2。MISO引脚和MOSI引脚上的数据的MSB位必须与SPSCK的第一个边沿同步,在SPI传输过程中,在同步时钟信号周期开始时(上升沿)数据上线,然后在同步时钟信号的下降沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(上升沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。
工作方式2:
当CPHA=0、CPOL=1时SPI总线工作在方式1。与前者唯一不同之处只是在同步时钟信号的下降沿时捕捉位信号,上升沿时下一位数据上线。
工作方式3:
当CPHA=1、CPOL=1时SPI总线工作在方式3。与前者唯一不同之处只是在同步时钟信号的上升沿时捕捉位信号,下降沿时下一位数据上线。
SPI总线最常用的工作方式是工作方式0
具体选择哪一个工作方式,一般是取决于从机,如果从机是工作方式0,那么主机需要设置为工作方式0。通常SPI工作在方式0
3 SPI配置步骤流程图:
(1)理解电路原理图
使用SPI1
SCK连接PB3
MISO连接PB4
MOSI连接PB5
CS连接PB14(普通引脚输出)
(2)使能SPIx和IO口时钟
RCC_AHB1PeriphClockCmd() / RCC_APBxPeriphClockCmd();
(3)初始化IO口为复用功能
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
(4)设置引脚复用映射:
GPIO_PinAFConfig();
(5)初始化SPIx,设置SPIx工作模式
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
(6)使能SPIx
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
(7)SPI传输数据
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ;
(8)查看SPI传输状态
//查看接收标志位
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE);
二、W25Q128
1 W25Q128概念
Flash存储器属,内存器件的一种,是一种非易失性( Non-Volatile )内存。
flash闪存是非易失存储器,可以对称为块的存储器单元块进行擦写和再编程。任何flash器件的写入操作只能在空或已擦除的单元内进行,所以大多数情况下,在进行写入操作之前必须先执行擦除。
2 W25Q128引脚与内部
W25Q128引脚说明
3、内部结构
W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。 W25Q128 的最少擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。操作需要给 W25Q128 开辟一个至少 4K 的缓存区,对 SRAM (运行内存)要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。
块>扇区>页:16M->256块(64K)->每块16个扇(4K)->每扇16页(256B)
W25Q128是容量为128Mbit的SPI接口NORFlash的器件,内部是按照Page、Sector、Block的结构来划分的,一个Page为256个Byte,一个Sector为16个Page也就是4KB,一个Block为16个Sector也就是64KB。相较于EEPROM而言,SPI Flash的存储空间更大,存取速度更快,广泛应用于嵌入式系统中数据、代码的固化。
内部
0x3BA990(第59块第10扇区第9页)
建议数据不要操作0块,因为系统部分信息存储在这个扇区
对SPI Flash的操作主要有:写使能、擦除、数据写入、数据读出、读取ID、检测Flash忙状态等几种操作。对SPI Flash进行各种操作,需要先发送对应的控制命令。
1.写使能操作
在对Flash进行擦除和写操作之前,必须先使能写操作,这是通过将Flash状态寄存器。
2.擦除操作
Flash存储单元中是无法写入位1的,只能写入位0,
所以要写入数据的话要先将原来的数据都擦出成0xFF然后写入数据的时候遇到bit 1时不做处理,
遇到bit 0时写入0即可。因此, Flash进行写入之前需要先将目标区块擦除。
3.数据写操作
数据写操作最多一次不能超过256个字节(一个页)。写入的时候可以不从页的开始地址写入,
如果一次写入字节数溢出了一个页的空间,那么多出来的会从循环到页的开始地址处
覆盖原来的数据(与EEPROM类似),页写的命令字为0x02,尾随24位的地址。
4.数据读操作
读操作没有类似于写操作的显示,可以进行任意读。
5.读取ID操作
为了识别芯片型号,需要读取芯片的ID号,W25Qxx系列Flash有多个ID号,
Device ID(0xAB)、JEDEC ID(0x9F)、Unique ID(0x4B)、Manufacturer ID(0x90)等。
W25Q128读取到的Manufacturer ID为0xEF,Device ID为0x17,JEDEC ID为0xEF4017。
6.检测Flash忙状态
读取Flash状态寄存器的内容,并判断其中的WIP(Write In Progress)位状态(0:空闲,1:忙),
检测Flash是否处于忙状态。读取Flash状态寄存器命令是0x05。
7.唤醒和掉电
通过发送0xAB(区别于读取ID操作,在发送命令后不发送空闲字节)命令可以唤醒Flash。
通过发送0xB9命令可以使得Flash进入掉电模式
4 命令操作W25Q128
4.1 读生产商与设备ID
0x90--读生产商与设备ID
u16 W25q128_id(void)
{
u16 id = 0x00;
//使能芯片
F_CS = 0;
//发送读生产商与设备ID的命令
Spi1_Send_Recv_Byte(0x90);
//地址拆分发送,先发高位地址
//下面地址合起来0x000000
Spi1_Send_Recv_Byte(0x00);
Spi1_Send_Recv_Byte(0x00);
Spi1_Send_Recv_Byte(0x00);
//发送任意字符(0xBB),获取生产商ID
id |= Spi1_Send_Recv_Byte(0xBB)<<8; //将生产ID移动15位~8位
id |= Spi1_Send_Recv_Byte(0xBB); //设置ID放置在7位~0位
//不使能芯片
F_CS = 1;
return id;
}
4.2 写使能
void Write_Enable(void)
{
F_CS = 0;
//发送写使能命令
Spi1_Send_Recv_Byte(0x06);
F_CS = 1;
}
4.3 读状态寄存器
//SPI发一个字节,可以收一个字节
u8 Spi1_Send_Recv_Byte(u8 txdata)
{
u8 rxdata = 0x00;
//等待发送的标志位
while( SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == 0);
//发送数据的缓冲区为空,才能再次发送数据
SPI_I2S_SendData(SPI1, txdata);
//等待接受的标志位
while( SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == 0);
//缓冲区非空,才去接受数据
rxdata = SPI_I2S_ReceiveData(SPI1);
return rxdata;
}
4.4 擦除扇区
//u32 addr:擦除起始地址
void Erase_Sector(u32 addr)
{
u8 status;
//写使能
Write_Enable();
F_CS = 0;
//发送擦除命令
Spi1_Send_Recv_Byte(0x20);
//发送擦除起始地址 0x11A025
Spi1_Send_Recv_Byte((addr>>16)&0xFF ); //发23~16位地址(addr>>16) ==> 0x0000 11
Spi1_Send_Recv_Byte((addr>>8)&0xFF ); //发15~8位地址 (addr>>8) ==> 0x0011 A0
Spi1_Send_Recv_Byte(addr&0xFF ); //发7~0位地址 addr ==> 0x11A0 25
F_CS = 1;
//等待擦除完成
F_CS = 0;
//发送读状态寄存器1命令
Spi1_Send_Recv_Byte(0x05);
//等待擦除完成
while(1)
{
//判断BUSY位是否为0
if((Spi1_Send_Recv_Byte(0x05) & 0x01) == 0)
break;
}
F_CS = 1;
}
4.5 写数据
void Page_Write(u32 addr, u8 *write_buff, u32 len)
{
//写使能
Write_Enable();
//使能芯片
F_CS = 0;
//发送写数据命令
Spi1_Send_Recv_Byte(0x02);
Spi1_Send_Recv_Byte((addr>>16)&0xFF); //先发23~16位地址
Spi1_Send_Recv_Byte((addr>>8)&0xFF); //发15~8位地址
Spi1_Send_Recv_Byte(addr&0xFF); //发7~0位地址
//写数据
while(len--)
{
Spi1_Send_Recv_Byte(*write_buff);
write_buff++;
}
//不使能芯片
F_CS = 1;
F_CS = 0;
//发送读状态寄存器1命令
Spi1_Send_Recv_Byte(0x05);
while(1)
{
//判断最低位是否为1
if( (Spi1_Send_Recv_Byte(0xFF) & 0x01) == 0)
break;
}
F_CS = 1;
}
4.6 读数据
//读数据
void Read_Data(u32 addr, u8 *read_buff, u32 len)
{
//使能芯片
F_CS = 0;
//发送读数据命令
Spi1_Send_Recv_Byte(0x03);
//发送24位地址 先发高位
Spi1_Send_Recv_Byte(addr>>16); //先发23~16位地址
Spi1_Send_Recv_Byte(addr>>8); //发15~8位地址
Spi1_Send_Recv_Byte(addr); //发7~0位地址
while(len--)
{
//0x77:任意字符
*read_buff = Spi1_Send_Recv_Byte(0x77);
read_buff++;
}
F_CS = 1;
}
4.7 W25Q128数据写入与读取效果
1、例子源码:https://download.csdn.net/download/m0_63622771/90861744
2、效果: