STC89C52是经典的C51单片机,该芯片不自带硬件SPI接口,正好有手上一块W25Q32的存储模块(某宝上买的2.2元),试着使用89C52模拟SPI接口驱动W25Q32,在驱动的过程中遇到了几个问题,首先的问题是电平不匹配,其次是对芯片datasheet资料的解读,关于W25Qx的资料基本上是全英文的资料,笔者英文水平有限解读起来有一定的困难,只能一点点去解读;其次网络上关于使用C51驱动W25Qx的相关资料有限,很大部分都是使用stm32芯片驱动的案例,结合stm32案例实现W25Qx的驱动,以下内容为个人学习过程小结,由于笔者水平有限,难免有错误,敬请谅解。
一、电路搭建(解决芯片IO电平匹配):
依据W25Qx芯片资料,该芯片支持电平范围为2.7V-3.6V,过电平可能造成损坏芯片,而89C52的IO电平为5V。解决办法是加一块3.3V转5V的电平转换模块(TXS-0108E),电路的连接如下图:
另外网络上也有采用电阻限流的方案,由于元件较多,焊接有点麻烦,笔者未验证,提供下图,仅供学习参考。
二、编写软SPI驱动:
SPI接口一般使用 4 条线通信:
DO 主设备数据输入,从设备数据输出。
DI 主设备数据输出,从设备数据输入。
SCK 时钟信号,由主设备产生。
CS 从设备片选信号,由主设备控制。
SPI 主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。
以下是SPI传送时序,SPI传送时序有4种方式,方式0-方式3。
方式0(0,0),方式1(0,1),方式2(1,0),方式3(1,1)
SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;SPI主模块和与之通信的外设时钟相位和极性应该一致。CPOL是用来决定SCK时钟信号空闲时的电平,CPOL=0,空闲电平为低电平,CPOL=1时,空闲电平为高电平。CPHA是用来决定采样时刻的,CPHA=0,在每个周期的第一个时钟沿采样,CPHA=1,在每个周期的第二个时钟沿采样。
小结下上述内容:CPOL=0时起始电平为低电平,CPOL=1时起始电平为高电平。CPHA=0时,CLK的第一个上升沿为采样(写W25Qx), CLK的第一个下降沿为输出(读W25Qx),CPHA=1时,CLK的第一个下降沿为采样(写W25Qx), CLK的第一个上升沿为输出(读W25Qx)。
W25Qx的SPI传输支持方式0和方式3,即方式0(CPOL=0,CPHA=0),方式3(CPOL=1,CPHA=1)。
为了方便起见,笔者以方式0为基础,编写一个SPI的传输函数,该函数发送以MSB(高位优先传输)。
//SPI传输函数,发送与接收一个字节
unsignedchar SPI_Byte(unsigned char dat)
{
unsigned charread=0,i=0;
for(i=0;i<8;i++)
{
W25_DI=dat&0x80;
W25_CLK=1;
dat<<=1;
read<<=1;
read|=W25_DO;
W25_CLK=0;
}
return read;
}
三、编写函数测试获取芯片的Flash ID号(JEDEC ID):
当电路搭建完成后,SPI通信基础函数编写完成后,下一步需要获取芯片返回一个值,测试电路与基础函数成功与否。
依据资料文件,获取ID之前有两点需要注意,第一点是MSB高位优先传输,第二点是获取操作时CS必须拉低。
当获取成功后W25Q32对应的16位的码值为0x4016,下面依据芯片资料提供的时序。
依据上图小结如下信息。
1、获取时CS必须为低电平,高位优先传输。
2、获取JEDEC ID的指令为0x90。
3、接收到信息三个字节,第一个字节为ManufacturerID(制造商ID)固定值为0xEF,第二个字节为MemoryType(内存类型),第三个字节为Capacity(容量)。
编写代码如下:
unsignedlong W25x_read_ID()
{
unsigned long re,t1,t2,t3;
W25_CS=0;
W25_CLK=0;
SPI_Byte(0x9F);
t3=SPI_Byte(0xff);
t2=SPI_Byte(0xff);
t1=SPI_Byte(0xff);
W25_CS=1;
re=(t3<<16)|(t2<<8)|t1;
return re;
}
测试结果:
四、芯片写使能与忙状态等待。
1、写使能:
写使能的操作与读ID的操作类似,其操作指令为0x06,写使能成功后Status Register1的S1位WSL置1,即允许写操作与擦除操作。
//写使能
voidW25x_write_Enable()
{
W25_CS=0;
W25_CLK=0;
SPI_Byte(0x06);
W25_CS=1;
}
2、忙状态等待函数:
当芯片正在执行擦除、写操作时,不允许其它操作,此时必须进入等待状态,直到执行完成后Status Register1的S0位BUSY重新置0后才允许后续操作,函数必须不断的读取状态寄存器,从中提取S0的数据,从而确定相应操作的状态。Status Register1读取指令为0x05,读取方式为MSB那么S0位对应与操作位为0x01,操作方式与读ID的操作类似,依据下图资料的时序,编写函数如下。
//等待擦除或写入操作,直到操作完成结束。
voidW25x_wait_BusyEnd()
{
unsigned char i=0,temp;
W25_CS=0;
W25_CLK=0;
SPI_Byte(0x05);
temp=SPI_Byte(0xff);
while((temp&0x01)==1)
{
temp=SPI_Byte(0xff);
}
W25_CS=1;
}
五、编写擦除指令:
W25Q32为非易失性存储器,写数据之前必须先执行擦除操作,这里我们先编写一个芯片擦除函数。
小结上图芯片资料信息:
1、所谓的芯片擦除是往存储单元字节中填充0xFF即为擦除。
2、芯片擦除操作前必须开启Write Enable(写使能)之前有介绍。
3、芯片擦除操作的指令0x07或0x60,当然CS也必须低电平。
4、芯片擦除完成后需要等待一段时间(读忙状态),之前有介绍。
5、其它与读取ID操作类似。
//芯片擦除操作函数
voidW25x_chipErase()
{
W25x_write_Enable();//开使能
W25_CS=0;
W25_CLK=0;
SPI_Byte(0xC7);
W25_CS=1;
W25x_wait_BusyEnd();//等待擦除操作完成结束
}
六、页写入操作:
W25Qx最大写入单位为页(Page),每页最多写入256B,超过256B必须另写一页,每16页组成一个扇区(Sector),每16个扇区组成一个块(Block)。W25x32为4MB的容量,共有64个块,1024个扇区,16384个页。地址范围为(0x000000-0x2FFFFF),理论上地址每一位对应一个字节,实际在连续写入大数据的过程中还需考虑页、扇区、块的容量问题。注意写之前必须确保是擦除操作过的,最小的擦除单位不是页而是扇区。下面的例子仅考虑页写,理想的起始地址为页的起始位置,内容小于256B。
1块=16扇区,1扇区=16页,1页=256字节
小结上图芯片资料如下:
1、页写操作与擦除操作类似,需要CS低电平、写使能、忙等待。
2、页写操作指令为0x02,接下来是24位地址(页的每页的起始地址为:0xXXXX00,页结束地址为0xXXXXFF其中X为16进制任意数)。
3、写入地址完成后,再次写入不大于256B的数据内容
页写代码如下:
//页写操作,*buf为内容,addr为地址,PageSize为写入字节数必//须小于256个
voidW25x_write_Page(unsigned char *buf,unsigned long addr,unsigned int Pagesize)
{
W25x_write_Enable();
W25_CS=0;
W25_CLK=0;
SPI_Byte(0x02);
SPI_Byte(addr>>16&0xff);
SPI_Byte(addr>>8&0xff);
SPI_Byte(addr&0xff);
if(Pagesize>256)
{
Pagesize=256;
}
while(Pagesize--)
{
SPI_Byte(*buf++);
}
W25_CS=1;
W25x_wait_BusyEnd();
}
七、读操作:
读操作相对于写操作比较简单,不用考虑页、扇区、块,仅使用24位地址即可读。
小结上图芯片资料如下:
1、读操作与读ID操作类似,需要CS低电平。
2、读操作指令为0x03,接下来是24位地址。
3、写入地址完成后直接读出数据内容。
//读操作,*buf为内容,addr为地址,PageSize为读出的字节数
voidW25x_read_Page(unsigned char *buf,unsigned long addr,unsigned int Pagesize)
{
W25_CS=0;
W25_CLK=0;
SPI_Byte(0x03);
SPI_Byte(addr>>16&0xff);
SPI_Byte(addr>>8&0xff);
SPI_Byte(addr&0xff);
while(Pagesize--)
{
*buf=SPI_Byte(0xff);
buf++;
}
W25_CS=1;
}
八、读写测试
数组a为写入内容,数据b为读出数据,i为芯片ID信息,所有信息通过串口发送至电脑。
voidmain()
{
unsigned long i;
unsigned chara[]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x10,0x11,0x12,0x13};//写入内容
unsigned char b[15];//读出内容
char j;
uart_init(); //串口初始化
i=W25x_read_ID();//读ID
uart_sendbyte(i/256/256);
uart_sendbyte(i/256);
uart_sendbyte(i%256);
W25x_chipErase();//芯片擦除
W25x_write_Page(a,0x000002,15);//从02位置开始写入
W25x_read_Page(b,0x000000,15);//从00位置开始读信息
for(j=0;j<15;j++)
{
uart_sendbyte(b[j]);//输出所读内容
}
while(1);
}
输出结果:
实物展示: