SPI简介
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)同步, 全双工支持总线挂载多设备(一主多从) SPI较IIC传输更快,最大传输速度取决于芯片厂商的设计需求,但硬件开销较IIC大,通信线个数较多
硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起 主机另外引出多条SS控制线,分别接到各从机的SS引脚(即片选) 输出引脚配置为推挽输出;
输入引脚配置为浮空或上拉输入
从机未被选中时,MISO需切换为高阻态
移位示意图
主机移位寄存器中的数据通过MOSI,随着波特率发生器输出的时钟频率一位一位的移入从机的移位寄存器,从主机的【高位】移动至从机【低位】;
同时从机同理,数据通过MISO移入主机移位寄存器,二者同步发生;
基本时序单元
起始条件:
SS从高电平切换到低电平
终止条件: SS从低电平切换到高电平
模式选择
SPI根据CPOL和CPHA为1或0,排列组合出四种模式:00、01、10、11
1、模式0:CPOL=0 CPHA=0 (常用
CPOL=0:空闲状态时,SCK为低电平 CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据 2、模式1:CPOL=0 CPHA=1
CPOL=0:空闲状态时,SCK为低电平 CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据 3、模式2:CPOL=1 CPHA=0
CPOL=1:空闲状态时,SCK为高电平 CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据 4、模式4:CPOL=1 CPHA=1
CPOL=1:空闲状态时,SCK为高电平 CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
W25Qxx存储芯片简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
存储介质:Nor Flash(闪存) 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI) 存储容量(24位地址): W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte
W25Q128: 128Mbit / 16MByte
W25Q256: 256Mbit / 32MByte
硬件电路
串行闪存框图
使用W25Q128存储数据时,若不进行区块划分,则容量过大不利于管理;故按照【基本单元】进行划分和操作。
在W25Q128中,以64KB作为一个基本单元划分为256个【块】,地址为xx0000->xxFFFF,每一个块以4KB作为单元划分为16个【扇区】,地址为xxx000->xxxFFF,在写入数据时,以256字节划分为16个【页】,地址为xxxx00->xxxxFF;
左下角的“页地址锁存/计数器”和“字节地址锁存/计数器”用于指定地址,在SPI发送的三字节地址信息中,前两个字节会进入页地址锁存/计数器,后一个字节进入字节地址锁存/计数器;页地址通过“写保护”和“行解码”来选择操作的【页】,字节地址通过“列解码”“256字节页缓存”,来进行指定字节的读写操作;
右下角的“256字节页缓存区”用于读写数据,写入时先写入该区域,时序结束后,芯片再将缓存区的数据复制到对应的页中,故连续写入的数据量不能超过256字节,也即一页;
Flash操作注意事项
写入操作时:
写入操作前,必须先进行写使能 每个数据位只能由1改写为0,不能由0改写为1 写入数据前必须先擦除,擦除后,所有数据位变为1,故FF通常代表空白 擦除必须按最小擦除单元进行,可按块擦除,最小为一个扇区 连续写入多字节时,最多写入一页(256字节)的数据,超过页尾位置的数据,会回到页首覆盖写入,且页缓存区是与页对应的,从页起始位置才能最大写入256字节,且不可跨越页的边沿,否则会引起地址错乱 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时:
直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取 状态寄存器的重要位 BUSY位 BUSY是状态寄存器(S00)中的只读位,当设备执行页程序、扇区擦除、块擦除、芯片擦除或写状态寄存器指令时,它被设置为1状态。在此期间,设备将忽略除读状态寄存器和擦除暂停指令之外的其他指令。当程序、擦除或写状态寄存器指令完成时,BUSY位将被清除为0状态,表明设备已准备好接受进一步的指令。
WEL位 Write Enable Latch (WEL)写使能锁存,是状态寄存器(S1)中的一个只读位,在执行写使能指令后被设置为1。当设备被禁止写时,WEL状态位被清除为O。写禁用状态发生在上电或以下任何指令之后:写禁用、页程序、扇区擦除、块擦除、芯片擦除和写状态寄存器。
SPI指令集
常用为:
写使能:Write Enable 06
写失能:Write Disable 04
读状态寄存器1:Read Status Register-1 05
页编程:Page Program 02
扇区擦除:Sector Erase (4KB) 20
表格中A开头的表示读写地址(Address),D开头的表示写入的数据(Data)
软件模拟SPI读写W25Q128代码
MySPI.c
#include "stm32f10x.h" // Device header
void MySPI_W_CS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_13, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_15, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14);
}
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
MySPI_W_CS(1); //初始化时不选中
MySPI_W_SCK(0); //使用模式0,默认低电平
}
void MySPI_Start(void) //起始信号
{
MySPI_W_CS(0);
}
void MySPI_Stop(void) //中止信号
{
MySPI_W_CS(1);
}
uint8_t MySPI_SwapByte(uint8_t ByteSend)//交换一字节
{
uint8_t ByteReceive=0x00;
uint8_t i;
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;
}
W25Q128.c
#include "stm32f10x.h" // Device header
#include "My_SPI.h"
#include "W25Q128_Ins.h"
void W25Q128_Init(void)
{
MySPI_Init();
}
void W25Q128_ReadID(uint8_t *MID, uint16_t *DID)//读取W25Q128的厂商ID和设备ID
{
MySPI_Start();
MySPI_SwapByte(W25Q128_JEDEC_ID); //根据指令集,9H为读ID号的指令
*MID = MySPI_SwapByte(W25Q128_DUMMY_BYTE); //交换数据,用FF交换厂商ID
*DID = MySPI_SwapByte(W25Q128_DUMMY_BYTE); //交换数据,用FF交换设备ID高8位
*DID <<= 8;
*DID |= MySPI_SwapByte(W25Q128_DUMMY_BYTE); //交换数据,用FF交换设备ID低8位
MySPI_Stop();
}//由于寄存器只读,故FF不会产生影响,且地址指针跟随时钟信号递增
void W25Q128_WriteEnable(void)
{
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q128_WRITE_ENABLE); //交换发送写使能的指令
MySPI_Stop(); //SPI终止
}
void W25Q128_WaitBusy(void) //等待Busy为0
{
uint32_t Timeout;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q128_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令
Timeout = 100000; //给定超时计数时间
while ((MySPI_SwapByte(W25Q128_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位。 //& 0x01,用掩码取出最低位;==0x01就是BUSY为1。BUSY为1,进入While死循环,进行等待,在次读出一次状态寄存器。BUSY为0,跳出循环。
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
MySPI_Stop(); //SPI终止
}
void W25Q128_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i; //定义变量
W25Q128_WriteEnable(); //写使能,写使能仅对之后跟随一条时序有效
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q128_PAGE_PROGRAM); //交换发送页编程的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据
}
MySPI_Stop(); //SPI终止
W25Q128_WaitBusy(); //事后等待忙,就是写入后,立刻等待,不忙了再退出
}
void W25Q128_SectorErase(uint32_t Address)
{
W25Q128_WriteEnable(); //写使能,写使能仅对之后跟随一条时序有效
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q128_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
MySPI_Stop(); //SPI终止
W25Q128_WaitBusy(); //事后等待忙,就是写入后,立刻等待,不忙了再退出
}
void W25Q128_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
uint32_t i;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q128_READ_DATA); //交换发送读取数据的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
DataArray[i] = MySPI_SwapByte(W25Q128_DUMMY_BYTE); //依次在起始地址后读取数据
}
MySPI_Stop(); //SPI终止
}
main.c
#include "stm32f10x.h" // Device header
#include "delay.h"
#include "LED.h"
#include "KEY.h"
#include "OLED.h"
#include "W25Q128.h"
int main(void)
{
delay_init();
OLED_Init();
W25Q128_Init();
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组
/*显示静态字符串*/
OLED_ShowString(1, 1, "MID: DID:");
OLED_ShowString(2, 1, "W:");
OLED_ShowString(3, 1, "R:");
/*显示ID号*/
W25Q128_ReadID(&MID, &DID); //获取W25Q128的ID号
OLED_ShowHexNum(1, 5, MID, 2);
OLED_ShowHexNum(1, 12, DID, 4);
/*W25Q64功能函数测试*/
W25Q128_SectorErase(0x000000); //指定扇区起始地址来扇区擦除
W25Q128_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q128中。起始地址:0x000000。写入数组:把ArrayWrite传进去。写入数量:4个字节。
//这里00是最后2个16进制数的页内地址,前面4位是页地址
W25Q128_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中。读取数组:把ArrayRead传进去。读取数量:4个字节
/*显示数据*/
OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //显示写入数据的测试数组
OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //显示读取数据的测试数组
OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
while (1)
{
}
}