SPI
- 什么是SPI?
SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线,由摩托罗拉公司开发,广泛用于微控制器和外围设备(如Flash存储器、传感器、ADC等)之间的短距离通信,具体特点如下图:

- SPI外设
所有SPI设备的SCK、MOSI、MISO分别连在一起,主机另外引出多条SS控制线,分别接到各从机的SS引脚。
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
那么,为什么输出引脚和输入引脚会这样配置???
答:这需要结合SPI的设计原则:
①谁控制,谁用推挽输出主设备控制的信号需要强驱动能力,而推挽输出的优势就是强驱动能力(能够快速充放电线路电容)、明确的高电平(不会出现不确定的中间电平)和高速切换(适合SPI的高速通信->可达数十MHz)。
如果使用开漏输出:上升沿靠上拉电阻,速度慢,在高频SPI下会导致时序混乱,信号边沿不陡峭,可能采样错误。②谁被读取,谁决定输入特性
从设备的数据线要防止冲突,MISO线在从设备未选中时可能处于高阻态,而上拉电阻确保此时为明确的高电平,避免噪声。上拉输入提供了确定的默认状态,只有当从设备主动驱动时为低电平。
- SPI运行原理
如图:

SPI的主从数据交换遵循高位先行的原则
- SPI基本时序单元
CPOL(Clock Polarity):时钟极性,表示SCK在空闲时的电平。
CPOL=0:空闲时SCK为低电平。
CPOL=1:空闲时SCK为高电平。
CPHA(Clock Phase):时钟相位,表示数据采样的时刻。
CPHA=0:在时钟的第一个边沿(即SCK从空闲状态跳变到相反状态的边沿)采样数据。
CPHA=1:在时钟的第二个边沿(即SCK从非空闲状态跳变回空闲状态的边沿)采样数据。
起止条件:

交换一个字节-模式0:

交换一个字节-模式1:

交换一个字节-模式2:

交换一个字节-模式3:

四种模式对比图:

- SPI和I2C对比如图:




W25Q64
-
什么是W25Q64?
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
存储介质:Nor Flash(闪存)
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
存储容量(24位地址):W25Q64:64Mbit / 8MByte -
W25Q64的内部存储层次:
W25Q64的存储结构采用分层设计,从大到小依次为:
芯片 (Chip, 8MB)–>块 (Block, 64KB) -->扇区 (Sector, 4KB)–>页 (Page, 256B)
①页 (Page) - 编程操作的最小单位
大小:256字节
特性:
1.最小的可编程单元
2.一次页编程操作可以写入1-256字节
3.编程操作只能将位从"1"改为"0"
②扇区 (Sector) - 擦除操作的最小单位
大小:4KB(16页)
特性:
1.最小的可擦除单元
2.擦除操作将整个扇区置为全"1"状态
3.必须先擦除才能进行编程
③块 (Block) - 大容量擦除单位
大小:64KB(16个扇区)
特性:
1.支持块擦除命令(0xD8)
2.擦除时间比逐个扇区擦除更快
3.适用于大范围数据更新
容量关系表:

存储框图:

3. 对W25Q64写入读取时的注意事项:
①写入操作时:
1.写入操作前,必须先进行写使能
2.每个数据位只能由1改写为0,不能由0改写为1
3.写入数据前必须先擦除,擦除后,所有数据位变为1
4.擦除必须按最小擦除单元进行
5.连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
6.写入操作结束后,芯片进入忙状态,不响应新的读写操作
②读取操作时:
直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
软件SPI读写W25Q64
- 接线图如下:

- 在Hardware文件夹下新建MySPI.c和MySPI.h文件
MySPI.c代码:
#include "stm32f10x.h" // Device header
//片选
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,BitValue);
}
//时钟
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,BitValue);
}
//主机输出
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,BitValue);
}
//主机输入
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void MySPI_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//设置初始电平
MySPI_W_SS(1);
MySPI_W_SCK(0);
}
//开始信号
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
//结束信号
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
//模式0输出一个字节
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i,ByteReceive = 0x00;
for(i=0;i<8;i++)
{
MySPI_W_MOSI(ByteSend & (0x80>>i));
MySPI_W_SCK(1);
if(MySPI_R_MISO() == 1){ByteReceive |= (0x80>>i);}
MySPI_W_SCK(0);
}
return ByteReceive;
}
////模式0输出一个字节(移位寄存器模型)
//uint8_t MySPI_SwapByte(uint8_t ByteSend)
//{
// uint8_t i;
// for(i=0;i<8;i++)
// {
// MySPI_W_MOSI(ByteSend & 0x80);
// ByteSend <<= 1;
// MySPI_W_SCK(1);
// if(MySPI_R_MISO() == 1){ByteSend |= 0x01;}
// MySPI_W_SCK(0);
// }
//
// return ByteSend;
//}
值得注意的是:
①在函数MySPI_SwapByte();中,佛如循环内的MySPI_W_MOSI(ByteSend & (0x80>>i));作用就是让ByteSend一位一位的发送,例如ByteSend的数据为1010 0101,在第一次循环中,1010 0101 & 1000 0000,其结果是1000 0000,及发送数据的最高位为1;第二次循环中,1010 0101 & 0100 0000,结果是0000 0000,发送数据的第二位为0,在语句if(MySPI_R_MISO() == 1){ByteReceive |= (0x80>>i);},这条语句的作用是先判断
MySPI_R_MISO()是否为1,意思是判断W25Q64发来的位是不是1,也就是判断相应的引脚是否被置为高电平,当发送的数据为1,就更改ByteReceive的相应位,判断不为1就跳出if执行后面的语句,由于ByteReceive的初始值为0000 0000,当if条件不成立时,数据为0,此时正好对应W25Q64发送的数据为0,由于if条件的不成立,正好跳过if条件而不修改ByteReceive,也就保持了原本位置的0.
MySPI.h代码:
#ifndef __MYSPI_H
#define __MYSPI_H
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
#endif
- 在Hardware文件夹下新建W25Q64.c和W25Q64.h文件
W25Q64.c代码:
#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();
//指令
MySPI_SwapByte(W25Q64_JEDEC_ID);
//交换厂商ID *MID
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
//交换设备ID *DID
*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();
}
//等待寄存器1忙状态结束
void W25Q64_WaitBusy(void)
{
uint32_t Timeout = 10000;
MySPI_Start();
//给指令
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
//状态寄存器的第0位专门是BUSY(忙)位,为0时表示空闲
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout --;
if(Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
//页编程 从给定的起始地址写入数据
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
W25Q64_WriteEnable();
uint16_t i;
MySPI_Start();
//给指令
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
//给定24位页地址,高位冲突自动舍弃
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
//开始写入数据
for(i = 0;i < Count;i ++)
{
MySPI_SwapByte(DataArray[i]);
}
MySPI_Stop();
W25Q64_WaitBusy();
}
//扇区擦除(一次4KB)
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();
MySPI_Start();
//给指令
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
//给定24位页地址,高位冲突自动舍弃
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();
}
//从给定的起始地址读出数据
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{
uint32_t i;
MySPI_Start();
//给指令
MySPI_SwapByte(W25Q64_READ_DATA);
//给定24位页地址,高位冲突自动舍弃
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for(i = 0;i < Count;i ++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
值得注意的是:
①无论是什么命令的发出,要严格经过这几个流程:
开始信号–>发指令–>发地址(24位地址码)–>读或发数据(若有数据要求)–>停止信号
②但在写操作时要先写使能,为什么要这样?
答:1.安全保护:防止意外写入导致数据丢失,避免电源波动时的数据损坏,增加操作的 intentionality(必须明确意图才能修改).
2.操作流程控制:确保每个修改操作都是经过深思熟虑的,提供明确的操作顺序:使能→操作→等待完成.
③在写操作性质的函数末尾或者开始要添加一个等待寄存器忙状态的函数,为什么这样做?开始和末尾位置的等待寄存器忙的函数有什么区别吗?
答:WaitBusy的必要性:
1.硬件要求:Flash物理操作需要时间
2.数据安全:确保操作完全生效后再继续
3.协议合规:遵循芯片制造商的操作规范
4.错误预防:避免在忙状态下发送命令导致的异常
开始和末尾调用等待寄存器忙的函数的区别如图:
末尾调用WaitBusy:提供"操作完成保证",适合简单应用
开始调用WaitBusy:提供"操作安全启动",适合复杂环境
W25Q64.h代码:
#ifndef __W25Q64_H
#define __W25Q64_H
void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count);
#endif
- 为了方便指令码被更好的看出来和调用,在Hardware文件夹下建立一个W25Q64_ins.h文件
W25Q64_ins.h代码:
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE 0xFF
#endif

2150

被折叠的 条评论
为什么被折叠?



