软件模拟SPI时序实现25Q64读写操作
单片机采用SPI/IIC通讯协议访问外围电子模块如:显示屏、EEPROM、FLASH、各种电子传感器等等越来越多,掌握SPI/IIC通讯协议访问外设非常必要。
Aarduino是我比较喜欢的平台,虽然它有硬件SPI接口,有封装的函数,但为了更深入了解SPI协议和读写25Q64,还是选择软件模拟比较好。
第一部分SPI简介
SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片机和 EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。
SPI通信原理比I2C要简单的多,通常只有一个主机和一个从机或多个从机之间的通讯,标准的SPI采用4根线,MOSI、MISO、SCLK、SS。
1、MOSI 主机输出从机输入,连接从机的输入端DO(MISO)。
2、MISO 主机输入从机输出,连接从机的输出端DI(MOSI)。
3、SCLK(CLK) 通讯时钟,连接从机的SCLK(CLK)。
4、SS(CS) 片选,连接从机的SS(CS),有时标注的是CS。

从机由于SS低电平被主机选中进行通讯。
主机MOSI上的数据按位bit(1/0)在SCLK的时钟作用下,由高到低(或低到高)输出在MOSI线上,与此同时,从机也在SCLK的同步下,接收了主机发出的在MOSI线上的数据。相应的,在SCLK的时钟作用下,由低到高(或高到低)从机输出在MISO线上的数据被主机同步接收。
SPI通讯有4种模式,SCLK的电位极性和相位的不同组合产生了4种不同的通信方式,一般通信方式由从机所确定,主机和从机必须同一种通信方式才可以正常通信。采取0模式通讯方式比较常见。

SPI通信0模式,即CPOL=0、CPHA=0,脉冲传输前和完成后都保持在低电平状态(低电平数据有效),在第一个边沿(上升沿)采样数据,第二个边沿(下降沿)输出数据。其他通讯方式用到时再去了解。
可以简要的说,发送数据时,主机SS低电平有效,主机MOSI上的数据在SCLK的上升沿(低电平到高电平)作用下,逐位向从机输出数据。接收数据时,主机SS低电平有效,SCLK的下降沿(高电平到低电平)作用下,逐位向MISO线上输出数据,从而主机读得数据。每8个时钟周期发送或接收1个字节数据(1个时钟周期上沿写数据、下沿读数据这种情况除外)。
第二部分 25Q64使用方法
这个帖子对我帮助很大http://www.51hei.com/bbs/dpj-41029-1.html,http://blog.sina.com.cn/s/blog_49677f890102w531.html,瑞嵌的w25x16\w25x32\w25x64数据手册中文翻译版。
25Q64简介,25Q64是华邦公司推出的大容量“SPI FLASH” 产品。容量为64Mbit,存储结构:1页256Byte(字节),1扇区=16页=256*16=4096B=4KB,1块=16扇区=64KB。故:25Q64 容量为8M,128块,2048扇区。
25Q64采用标准的SPI四线制通讯:
1、串行时钟引脚CLK
2、片选引脚 CS
3、串行数据输出引脚DO
4、串行数据输入引脚DIO
电路接线图:

模块图,擦除指令要用到这张表,1块=16扇区:

25Q64指令表:

这张表格很非常重要,必须读懂它。BYTE1代表指令、BYTE2-BYTE6和指令配合使用,不同的指令有不同的含义。 25Q64常用指令15条,现以90h,06h,05h,02h,03h,20h介绍使用方法:
一、“读制造/器件号”指令(90h)
首先使用90h的指令目的是检验器件连接是否正确、25Q64是否正常以及代码编写是否正确。
先把/CS引脚拉低,然后把指令90h通过引脚DIO送到芯片,然后接着把24位地址000000h送到芯片,然后芯片会先后把“生产ID”和“器件ID”通过DO引脚在CLK的下降沿逐位发送出去,从而主机同步读得数据。

代码实现:(开发环境arduino 328p)
#define CLK_H() (digitalWrite(9,HIGH))
#define CLK_L() (digitalWrite(9,LOW))
#define MOSI_H() (digitalWrite(10,HIGH))
#define MOSI_L() (digitalWrite(10,LOW))
#define CS_H() (digitalWrite(11,HIGH))
#define CS_L() (digitalWrite(11,LOW))
#define MISO_RD() (digitalRead(8))
#define Pin_MOSI 10 //MOSI端口
#define Pin_MISO 8 //MISO端口
#define CLK_SET() (pinMode(9,OUTPUT))
#define MOSI_SET() (pinMode(10,OUTPUT))
#define CS_SET() (pinMode(11,OUTPUT))
#define MISO_RD_SET() (pinMode(8,INPUT))
#define WriteEn 0x06 //写使能
#define WriteDis 0x04 //写禁止
#define WriteStatus 0x01 //写状态
#define ReadStatus 0x05 //读状态
#define WritePage 0x02 //页写
#define ReadData 0x03 //读数据
#define SectorEra 0x20 //删去擦除
#define ReadDevid 0x90 //读设备ID
#define ReadJedecid 0x9f //读设备JEDECID
#define PageSize 256 //一页256字节(8bit)
void INIT()
{CLK_SET();MOSI_SET();CS_SET();MISO_RD_SET();CS_H();CLK_L();
}
//-------*向25Qxx 写入一个8位的数据----------/
void Write_Byte(uint8_t x)
{for(uint8_t i=0;i<8;i++){ uint8_t d;d=x&(0x80>>i);digitalWrite(Pin_MOSI,d);CLK_L();CLK_H(); }
}
//---------从25Qxx读得一个8位数据------------/
uint8_t Read_Byte()
{ uint8_t i=0,data=0;for(i=0;i<8;i++){ CLK_H();CLK_L();if(MISO_RD()) data|=0x80>>i;}return data;
}void setup() {
Serial.begin(9600);INIT();
CS_L();Write_Byte(ReadDevid);Write_Byte(00);Write_Byte(00);Write_Byte(00);data=Read_Byte();Serial.println(data,HEX);//delay(1);data=Read_Byte();Serial.println(data,HEX);CS_H();
}
显示:EF 16
arduino向25Q64发送90h+000000h后,随后读得2个8位字节的数据,发送和接收的频率由CLK控制。
理解了90h指令的使用,对后面的其他指令理解就轻松多了。
二、写使能指令(06h)
“写使能”指令会使“状态寄存器”WEL位置位。在执行每一个“页编程”“扇区删除”“块删除”“芯片删除”和“写状态寄存器”命令之前先置位WEL。对照25Q64指令表,/CS引脚先拉低之后,“写使能”指令代码06h从DI引脚输入,在CLK上升沿采集,8个时钟周期过后再拉高/CS引脚。

void Write_En()
{CS_L();
Write_Byte(WriteEn); //0x06CS_H();
}
三、“读状态寄存器”指令(05h)
当/CS拉低之后,开始把05h从DI引脚送到芯片,在CLK的上升沿数据被芯片采集,当芯片认出采集到的数据是05h时,芯片就会把“状态寄存器”的值从DO 引脚在CLK的下降沿输出,高位在先。
“读状态寄存器”指令在任何时候都可以用,甚至在编程、删除和写状态寄存器的过程中也可以用,这样,就可以从状态寄存器的BUSY位判断编程、删除和写状态寄存器周期有没有结束,从而让我们知道芯片是否可以接收下一条指令了,如果/CS不被拉高,状态寄存器的值一直从DO引脚输出,/CS拉高之后,读指令结束。

uint8_t Read_Status()
{uint8_t s=0; CS_L();Write_Byte(ReadStatus); // 0x05读取状态的命令字s=Read_Byte(); // 读取状态字节 CS_H(); // 关闭片选return s;
}
四、“读数据”指令(03h)
“读数据”指令允许读出一个字节或一个以上的字节。先把/CS引脚拉低,然后把03h通过DIO引脚送到芯片,只有再送入24位地址,这些数据在CLK的上升沿被芯片采集,芯片接收完24位地址之后,就会把相应地址的数据在接下来的CLK的下降沿作用下,从DO引脚送出去,高位在前。当读完这个地址的数据之后,地址自动增加,然后DO引脚把下一个地址的数据送出去,形成以个数据流。也就是说,只要时钟在工作,通过一条读指令,就可以把整个芯片存储区的数据读出来。把/CS引脚拉高,“读数据”指令结束。当芯片在执行编程、擦除和读状态寄存器指令的周期内,“读数据”指令不起作用。

//-----读取 数据---------------
//pbuf 存放读取的数据地址
//Addr 数据起始地址
//len 读取数据长度
void Read_Data(uint8_t * pbuf, uint32_t Addr24, uint16_t Len)
{while(Read_Status()&0x01) Serial.println("Status 0.0=0 ,25Q busy of Read_data"); //判断是否忙CS_L();Write_Byte(ReadData);Write_Byte((uint8_t)(Addr24>>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr24>>8)); Write_Byte((uint8_t) Addr24); for(int i=0;i
五、“页编程”指令(02h)
执行“页编程”指令前,先执行“写使能”指令,而且要求待写入的区域位都为1,也就是需要先把待写入的区域擦除。先把/CS引脚拉低,然后把代码02h通过DIO引脚送到芯片,然后在把24位地址送到芯片,然后接着送要写的字节到芯片。在写完数据之后,把/CS引脚拉高。
24位地址可以是该页(每页256字节,模块图所示)的起始地址也可以是中间的任何地址,当写到该页最后地址后,也就是写满这个页后,如果继续写,数据将覆盖该页的起始地址,如果想未写忙的数据继续写入下个页地址,要重新进行“页编程”操作,当然24位地址也要指向下页的起始地址。

//---连续写入不大于256个字节数据-----------------------------
//pbuf:数据存储区
//Addr:开始写入的地址(24bit)
//Len:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void Write_Data(uint8_t * pbuf,uint32_t Addr, uint16_t Len)
{uint16_t i; while(Read_Status()&0x01) Serial.println("Status 0.0=0 ,25Q busy of write begin"); //判断是否忙Write_En(); //SET WEL CS_L(); //使能器件 Write_Byte(WritePage); //发送写页命令 0x02Write_Byte((uint8_t)(Addr>>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr>>8)); Write_Byte((uint8_t) Addr); for(i=0;i
六、“扇区擦除”指令(20h)
“扇区擦除”指令将24位地址所在的整个扇区(4K字节)擦除,擦除后扇区位都为1,扇区字节都为FFh。在执行“扇区擦除”指令前,先执行“写使能”指令。在指令执行期间,BUSY位位1,可以通过“读状态寄存器”指令观察。当指令执行完毕,BUSY位位0,WEL位也为0。如果需要擦除的地址处于只读状态,指令将不起作用。
24位所在的地址可以理解为,0x003xx和0x00300一样效果,也可以理解写入一个24位地址,芯片自动将低8位置0。
先把/CS拉低,然后把指令代码20h通过DIO引脚送到芯片,然后接着把24位扇区地址送到芯片,然后拉高/CS引脚。

void Sector_Erase(uint32_t Addr24)
{while(Read_Status()&0x01); // 判断是否忙 Write_En(); // 写允许 06CS_L();Write_Byte(SectorEra); // 整扇擦除命令 Write_Byte((uint8_t)(Addr24>>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr24>>8)); Write_Byte((uint8_t) Addr24); CS_H();while(Read_Status()&0x01); // 等待擦除完成
}
总结:
对SPI协议来说,1、数据是在CLK的上升沿的作用下逐位从主机的MOSI引脚输出,于此同时,从机同步采集到MOSI线上的数据。2、从机在CLK的下降沿的作用下逐位从从机的DO引脚输出,于此同时,主机同步读得DO线上的数据。
对25Q64来说,1、准备一份数据手册很重要。2、看懂指令表。3、/CS低电平后,先发送指令,随后根据指令的要求发送数据或者读取数据,结束后/CS拉高。4、重点弄明白:读制造/器件号指令(90h)、擦除指令(20h)、读数据指令(03h)和页编程指令(02h),其他指令用到时再阅读数据手册。
完整代码:
#define CLK_H() (digitalWrite(9,HIGH))
#define CLK_L() (digitalWrite(9,LOW))
#define MOSI_H() (digitalWrite(10,HIGH))
#define MOSI_L() (digitalWrite(10,LOW))
#define CS_H() (digitalWrite(11,HIGH))
#define CS_L() (digitalWrite(11,LOW))
#define MISO_RD() (digitalRead(8))#define Pin_MOSI 10 //MOSI端口
#define Pin_MISO 8 //MISO端口#define CLK_SET() (pinMode(9,OUTPUT))
#define MOSI_SET() (pinMode(10,OUTPUT))
#define CS_SET() (pinMode(11,OUTPUT))
#define MISO_RD_SET() (pinMode(8,INPUT))#define WriteEn 0x06 //写使能
#define WriteDis 0x04 //写禁止
#define WriteStatus 0x01 //写状态
#define ReadStatus 0x05 //读状态
#define WritePage 0x02 //页写
#define ReadData 0x03 //读数据
#define SectorEra 0x20 //删去擦除#define ReadDevid 0x90 //读设备ID
#define ReadJedecid 0x9f //读设备JEDECID#define PageSize 256 //一页256字节(8bit)void INIT()
{CLK_SET();MOSI_SET();CS_SET();MISO_RD_SET();CS_H();CLK_L();delay(1);
}
//---使能-----------
void Write_En()
{CS_L();Write_Byte(WriteEn);CS_H();
}
//------禁能---------
void Write_Dis()
{CS_L();Write_Byte(WriteDis);CS_H();
}uint8_t Read_Status()
{uint8_t s=0; CS_L();Write_Byte(ReadStatus); // 0x05读取状态的命令字s=Read_Byte(); // 读取状态字节 CS_H(); // 关闭片选return s;
}//------------------跨页写入数据-----------------------------
//如果空闲空间够Len个数据写入,写完数据就停止,否则,先写满空闲空间,不够写时再翻页写入数据
//pbuf:数据存储区,注意字节长度
//Addr:开始写入的地址(24bit),注意字节长度
//Len:要写入的字节数(可大256,也可以小于),注意字节长度void Write_TurnPage(uint8_t * pbuf,uint32_t Addr, uint32_t Len)
{ uint16_t FreeSpace, Head,Page, Remain; FreeSpace=PageSize-Addr%PageSize; //25Q64 有32768编程页,x8000H页if (Len <= FreeSpace) {Head=Len;Page=0; //剩余空间够Len 写入,不需要写下一页,和下下页Remain=0; }if (Len > FreeSpace) {Head=FreeSpace;Page=(Len-FreeSpace)/PageSize; Remain=(Len-FreeSpace)%PageSize;}if(Head !=0){Serial.print("----Headt:");Serial.println(Head);Write_Data(pbuf, Addr, Head);pbuf=pbuf+Head;Addr=Addr+Head;}if(Page !=0) {Serial.print("----Page:");Serial.println(Page);for (uint16_t i=0; i>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr>>8)); Write_Byte((uint8_t) Addr); for(i=0;i>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr24>>8)); Write_Byte((uint8_t) Addr24); CS_H();while(Read_Status()&0x01) Serial.println("Status 0.0=0 ,25Q busy of SectorErase end"); // 等待擦除完成
} //-----读取 数据-------------------
//pbuf 存放读取的数据地址
//Addr 数据起始地址
//len 读取数据长度
void Read_Data(uint8_t * pbuf, uint32_t Addr24, uint16_t Len)
{while(Read_Status()&0x01) Serial.println("Status 0.0=0 ,25Q busy of Read_data"); //判断是否忙CS_L();Write_Byte(ReadData);Write_Byte((uint8_t)(Addr24>>16)); //发送24bit地址 Write_Byte((uint8_t)(Addr24>>8)); Write_Byte((uint8_t) Addr24); for(int i=0;i>i);digitalWrite(Pin_MOSI,d);CLK_L();CLK_H(); }
}
//---------从25Qxx读得数据------------/
uint8_t Read_Byte()
{ uint8_t i=0,data=0;for(i=0;i<8;i++){ CLK_H();CLK_L();if(MISO_RD()) data|=0x80>>i;}return data;
}void setup() {// put your setup code here, to run once:Serial.begin(9600);uint8_t data=0;int g=0;uint8_t str[1280];memset(str,0,sizeof(str));unsigned int j=0; //2个8字节无符号for (int k=0; k<5;k++){for (int i=0; i<256;i++){str[j]=i;j++;}}
//for (int i=0; i<513;i++) Serial.println(str[i],HEX);INIT();Write_En(); //status wel=1data=Read_Status();Serial.println(data,BIN);//读设备ID-----------------CS_L();Write_Byte(ReadDevid);Write_Byte(00);Write_Byte(00);Write_Byte(00);data=Read_Byte();Serial.println(data,HEX);//delay(1);data=Read_Byte();Serial.println(data,HEX);CS_H();//-------------------------/*Serial.println("---开始 Erase ---"); Sector_Erase(0x0);//---0x10起写入20个字节--------Write_Data(str,0x10,20);memset(str,0,sizeof(str));//-----读出数据---------------Read_Data(str,0x00,512);for(int k=0;k<512;k++){if(g==16){Serial.println("|");if(k%256 ==0) Serial.println("---------------");g=1;} else g++;Serial.print(str[k],HEX);Serial.print(".");}*///-------跨页写入更多的数据------Serial.println("");Serial.println("-----跨页写入数据-------");Sector_Erase(0x0);Write_TurnPage(str,0xf5,520);memset(str,0,sizeof(str));Read_Data(str,0x00,1024);Serial.println("str:");for(int k=0;k<1024;k++){if(g==16){Serial.println("|");if(k%256 ==0) Serial.println("---------------");g=1;} else g++;Serial.print(str[k],HEX);Serial.print(".");}/*
Read_Data(tmp,0x1000,256);
Serial.println("tmp:");j=0;for(int k=0;k<256;k++){if(j==16){Serial.println("|");j=1;} else j++;Serial.print(tmp[k],HEX);Serial.print(".");}*/
}void loop() {// put your main code here, to run repeatedly:}