提要:外部FLASH芯片据说通常用来放置字库,图形库,反正它的大内存让我可以避免在写程序时因为其中的数据过大而无法编译成功。
软件:KEIL C51、stc-isp-15xx-v6.87C
硬件:STC8A8K64S4A12、M25P80、0.96寸OLED
说明:本文章纯属个人学习笔记,当然也欢迎大家的指正,只不过一方面,外部FLASH的使用在CSDN上有不少文章,我这篇既不够深入,也可能讲错;再者我看大多数文章使用STM32和W25QXX系列为例进行讲解,而我由于硬件所限,开发板上是M25P80,我只好也用它,(不过这个影响倒是不大,原理都是一样的)。所以大家在学习外部FLASH的时候最好还是不要找这篇文章作为入门了。
最后,还是那样,若是有侵犯到相关权益,请联系我进行删改。
参考学习资料:
1,http://www.shendongmcu.com/texts/spi-bus-and-flash/spi-bus-and-flash.htm
2,https://blog.csdn.net/lalala098/article/details/81302579/
3,https://blog.csdn.net/zxh1592000/article/details/78759736
4,https://blog.csdn.net/qq_38405680/article/details/83048201
5,https://m.tb.cn/h.V8U6kox
目录:
1,M25P80的简单介绍
2,软件SPI和硬件SPI
3,M25P80的几个指令
4,读写M25P80的一般过程
1,M25P80的简单介绍
外部FLASH芯片在储存空间上一般有几个层级上的划分:整体(BULK)、块(BLOCK)、扇区(SECTOR)、页(PAGE)、字节(Byte)、位(bit)。而我们今天介绍的这款M25P80整体空间1Mbyte,也就是8Mbits(一字节有八位嘛),这1M字节没有进行块的划分,直接分成16个扇区(SECTOR),每个扇区256页(PAGE),每页256字节(Byte),然后你乘一乘,看看是不是1MByte。
在读写方面,FLASH很有意思,每次写入数据之前,应该要把相应的区间擦除,即全部变成“1”,因为写入数据的时候,只能写入“0”,而无法写入“1”;另外,擦除数据也是要一次擦除一大片,而这个“片”的量级示芯片而定,有的有块擦除、整体擦除、扇区擦除;有的只有扇区擦除、整体擦除,比如今天要介绍的这块M25P80。
在数据传输方面,M25P80采用SPI协议。
注:这种使用SPI协议的好像是串行FLASH,还有一种并行FLASH,引脚较多,请自行研究。
/S:也叫CS,片选信号,低电平有效,在上电之后,在执行一条新的指令之前,必须让/CS管脚先有一个下降沿。
Q(MISO):为串行数据输出引脚,(maser input slave output)
/W:写保护管脚,,有效电平为低电平。高电平可读可写,低电平仅仅可读。
VSS:地
VCC:电源(2.7----3.6v)
/HOLD:保持管脚,低电平有效。当CS为低电平,并且把HOLD拉低时,数据输出管脚将保持高阻态,并且会忽略数据输入管脚和时钟管脚上的信号。把HOLD管脚拉高,器件恢复正常工作。
C:也即CLK,时钟引脚,为输入输出提供时钟脉冲。
D(MOSI): 为串行数据输入引脚,数据、地址和命令从D引脚输入到芯片内部。(master output slave input)
2,软件SPI和硬件SPI
这两种方法各有利弊,软件SPI不受引脚功能限制,普通的I/O口就可以,甚至我觉着可能只要满足输入或输出单方面的功能就行,比如CLK引脚只要输出就可以了。但是软件模拟SPI的程序较复杂一点,而且速度会比较慢。相比之下,硬件SPI引脚受限,不过速度快,程序简单。
软硬件SPI都要配置的几个点是:空闲时的时钟电平、在什么时候读取数据,又在什么时候发送数据。这个要引入“时钟极性 CPOL”和“时钟相位CPHA”的概念。详细内容请自查资料。而对于M25P80而言,支持CPOL和CPHA为“0 0”以及“1 1”的这两种情况,具体说来,就是单片机在下降沿接收FLASH发来的数据,在上升沿给FLASH发送数据,而空闲的时候,时钟电平可高可低。
软件模拟SPI:
//正确的应该是每发送一位,就要收回一位数据。然后还有一个要注意的
//的是,发送数据发生在上升沿,接收数据发生在下降沿,空闲时时钟电
//平是高还是低这个无所谓,FLASH都可以。
unsigned char SPI_WRITE_BYTE(unsigned char d)
{
unsigned char val=0x80;
unsigned char back_data = 0;
unsigned char i = 0;
FLASH_CLK_Set();//时钟空闲设为高电平
while(val)
{
FLASH_CLK_Clr(); //时钟电平拉低,产生一个下降沿,主芯片接收到一位数据。
//要注意的是,在这个while前,有一个高电平,这样在发送
//数据的之前就接收了一位数据。这跟FLASH的读写特性有关。
//有说这个地方要再加一个延时。
if(d&val)
{
FLASH_MOSI_Set();
}
else
{
FLASH_MOSI_Clr();
}
FLASH_CLK_Set();//时钟电平拉高,这时候产生一个上升沿,发出去一位数据
val >>= 1;
back_data = back_data << 1;
back_data |= FLASH_MISO;
}
return back_data;
}
我之前写的函数是全部发送完之后再全部接收,现在看来显然不行。
硬件SPI:
//SPI初始化
//然后初始化SPI总线
void Init_SPI()
{
P_SW1=0X00; //选择用哪套SPI引脚,STC8A8K64S4A12一共有四组SPI可以使用。
SPDAT = 0;
SPSTAT=0xc0; //SPDAT.7和SPDAT.6写11,可以将中断标志清零。注意是写1才清零
SPCTL = 0xd0; // SSIG 1 开启主机模式 SPEN 1 SPI使能 DORD 0 先发最高位 MSTR 1 主机模式
// CPOL 0 SPICLK空闲时为低 CPHA 0 数据在SPICLK的前时钟沿驱动 时钟CPU_CLK/4
}
//SPI为全双工通讯 所以在发送的同时可以接收到数
unsigned char SPI_WRITE_BYTE(unsigned char SPI_SendData)
{
SPDAT= SPI_SendData; // 将数据 写入
while((SPSTAT&0x80)==0); //等待写入完成
SPSTAT = 0xc0; //清除中断标志,和写冲突标志,注意是对应位写1才能清零
return SPDAT; //返回得到的数据
}
//SPI时钟速率设置
void SPI_Speed(unsigned char speed)
{
switch(speed) //每一次降速 都要先清为最高 在进行降速
{
case 0: SPCTL&=0xFC;break; //SysClk/4,SPR1=0,SPR0=0
case 1: SPCTL&=0xFC; SPCTL|=0x01;break; //SysClk/16,SPR1=0,SPRO=1
case 2: SPCTL&=0xFC; SPCTL|=0x02;break; //SysClk/64,SPR1=1,SPR0=0
case 3: SPCTL&=0xFC; SPCTL|=0x03;break; //SysClk/128,SPR1=1,SPR0=1
}
}
这个说实话我也没有认真研究,详细内容需要查看芯片的技术手册,看看每个寄存器是干什么的就可以了。
3,M25P80的几个指令
M25P80的指令与常常提到的W25Q64几乎没什么区别,一定程度上还简化了,具体如下:
这里强烈建议看这篇推文(上面的参考学习资料里也有):https://blog.csdn.net/lalala098/article/details/81302579/
讲得十分详细了。
4,读写M25P80的一般过程
最后叙述一下使用FLASH的基本操作:就是简单的往里面写一点数据,再读出来,由于我还没有学习串口,因此还不懂得用电脑查看。暂时是将得到的数据显示到OLED上。
最开始一般把FLASH的指令宏定义一下,用起来简单直观,如下:
//M25P80常用指令表
#define WRITE_ENABLE 0x06
#define WRITE_DISABLE 0x04
#define READ_STATUS_REGISTER 0x05
#define WRITE_STATUS_REGISTER 0x01
#define READ_DATA_BYTES 0x03
#define READ_DATA_at_HIGHER_SPEED 0x0B
#define PAGE_PROGRAM 0x02
#define SECTOR_ERASE 0xD8
#define BULK_ERASE 0xC7
#define DEEP_POWER_DOWN 0xB9
#define RELEASE_from_DEEP_POWER_DOWN 0xAB
#define PAGE_SIZE 256
#define DUMMY_BYTE 0xFF //随便的
用到的几个基本函数
1,写使能函数
//写使能函数
void FLASH_WRITE_ENABLE(void)
{
FLASH_CS_Clr(); //CS拉低
SPI_WRITE_BYTE(WRITE_ENABLE);
FLASH_CS_Set(); //CS拉高
}
在“擦除”指令、“页编程”指令、“写状态寄存器“指令之前,都要先用一下这个“写使能函数”。
逻辑:拉低CS,送入“写使能”指令,拉高CS。
2,读取状态寄存器的值
//读取FLASH状态寄存器的值
unsigned char FLASH_READ_STATUS_REGISTER(void)
{
unsigned byte = 0;
FLASH_CS_Clr();
SPI_WRITE_BYTE(READ_STATUS_REGISTER);
byte = SPI_WRITE_BYTE(DUMMY_BYTE);
FLASH_CS_Set();
return byte;
}
逻辑:拉低CS,送入“读取状态寄存器”指令,接收数据,拉高CS。
要注意的是,送完指令后立马返还回来的并不是我们所需要的,只有再随便发一字节,接收回来的才是状态寄存器的值。这个可以参考上面的时序图。
3,等待FLASH内部工作完成。
//等待FLASH内部工作完成
void FLASH_WAIT_for_END(void)
{
while((FLASH_READ_STATUS_REGISTER() & 0x01) == 0x01);
}
这个“等待”函数是很有必要的,因为我们对FLASH芯片的每个操作都是要花时间的,如果上个指令还没有结束就送下个指令或是数据,怕是会出问题。而判断是否完成一般通过状态寄存器的第0位:“WIP”位(有的也叫BUSY)。其描述如下:
WIP bit. The Write In Progress (WIP) bit indicates whether the memory is busy with a Write Status Register, Program or Erase cycle. When set to 1, such a cycle is in progress, when reset to 0 no such cycle is in progress.
其中“1”表示正忙;“0”表示不忙。
4,擦除某一扇区
//擦除FLASH某一扇区
void FLASH_ERASE_SECTOR(unsigned long SECTOR_ADDRESS)
{
FLASH_WAIT_for_END();
FLASH_WRITE_ENABLE();
FLASH_WAIT_for_END();
FLASH_CS_Clr();
SPI_WRITE_BYTE(SECTOR_ERASE);
SPI_WRITE_BYTE((unsigned char)(SECTOR_ADDRESS >> 16));
SPI_WRITE_BYTE((unsigned char)(SECTOR_ADDRESS >> 8));
SPI_WRITE_BYTE((unsigned char)(SECTOR_ADDRESS));
FLASH_CS_Set();
FLASH_WAIT_for_END();
}
逻辑:等待前面工作完成,写使能,等待工作完成,拉低CS,送入“扇区擦除”指令,而后马上送入要擦除的扇区地址,拉高CS,等待工作完成。
关于这个地址,许多文章都说有24位,不过我觉着M25P8020位就够了,因为:1×16×256×256=1048576=2^20,他们提到的W25Q64可是有8MByte,多了一些。
5,擦除整体FLASH
//擦除整体FLASH
void FLASH_ERASE_BULK(void)
{
FLASH_WAIT_for_END();
FLASH_WRITE_ENABLE();
FLASH_WAIT_for_END();
FLASH_CS_Clr();
SPI_WRITE_BYTE(BULK_ERASE);
FLASH_CS_Set();
FLASH_WAIT_for_END();
}
这个要花费的时间比较长。
6,单页写入
//单页写入,使用本函数前要擦除扇区,一次写入不得超过256字节
//WRITE_DATA_LENGTH:要写入的字节数
void FLASH_PROGRAM_PAGE(unsigned long WRITE_ADDRESS,unsigned char* WRITE_DATA,unsigned int WRITE_DATA_LENGTH)
{
unsigned int i = 0;
FLASH_WAIT_for_END();
FLASH_WRITE_ENABLE();
FLASH_WAIT_for_END();
FLASH_CS_Clr();
SPI_WRITE_BYTE(PAGE_PROGRAM);
SPI_WRITE_BYTE((unsigned char)(WRITE_ADDRESS >> 16));
SPI_WRITE_BYTE((unsigned char)(WRITE_ADDRESS >> 8));
SPI_WRITE_BYTE((unsigned char)(WRITE_ADDRESS));
if(WRITE_DATA_LENGTH > PAGE_SIZE)
{
WRITE_DATA_LENGTH = PAGE_SIZE;
}
for(i = 0;i < WRITE_DATA_LENGTH;i ++)
{
SPI_WRITE_BYTE(WRITE_DATA[i]);
}
FLASH_CS_Set();
FLASH_WAIT_for_END();
}
逻辑:等待前面工作完成,写使能,等待完成,拉低CS,送入“页编程”指令,马上送入要写入数据的地址,再马上写入数据,拉高CS,等待工作完成。
7,任意页数写入
//上面那个函数是比较极端的情况,要求字节数小于你从开始地址到结束所包含的字
//节,因此还需一个普适的函数,可以在任意地址写入任意字节。(当然,也是要有限度的)。
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//WRITE_DATA:数据存储区
//WRITE_ADDRESS:开始写入的地址(24bit)
//WRITE_DATA_LENGTH:要写入的字节数(最大65535)
void FLASH_ANY_PROGRAM(unsigned long WRITE_ADDRESS,unsigned char* WRITE_DATA,unsigned int WRITE_DATA_LENGTH)
{
unsigned int pageremain;
pageremain = 256 - WRITE_ADDRESS%256; //单页剩余的字节数 页写入是把整个空间用256个字节平分的
//如果给出一个地址 在当前页之中 那么要算出当前页后面没有写的字节数 先写入这部分字节
if(WRITE_DATA_LENGTH <= pageremain)
{
pageremain = WRITE_DATA_LENGTH;//不大于256个字节
}
while(1)
{
FLASH_PROGRAM_PAGE(WRITE_ADDRESS,WRITE_DATA,pageremain);
if(WRITE_DATA_LENGTH == pageremain)
{
break;//写入结束了
}
else
{
WRITE_DATA += pageremain;
WRITE_ADDRESS += pageremain;
WRITE_DATA_LENGTH -= pageremain; //减去已经写入了的字节数
if(WRITE_DATA_LENGTH > 256)
{
pageremain=256; //一次可以写入256个字节
}
else
{
pageremain = WRITE_DATA_LENGTH; //不够256个字节了
}
}
}
}
8,读数据
//读数据函数
//READ_DATA_LENGTH:要读取的字节数
void FLASH_READ_DATA(unsigned long READ_ADDRESS,unsigned char* READ_DATA,unsigned int READ_DATA_LENGTH)
{
unsigned int i = 0;
FLASH_WAIT_for_END();
FLASH_CS_Clr();
SPI_WRITE_BYTE(READ_DATA_BYTES);
SPI_WRITE_BYTE((unsigned char)(READ_ADDRESS >> 16));
SPI_WRITE_BYTE((unsigned char)(READ_ADDRESS >> 8));
SPI_WRITE_BYTE((unsigned char)(READ_ADDRESS));
for(i=0;i<READ_DATA_LENGTH;i++)
{
READ_DATA[i] = SPI_WRITE_BYTE(DUMMY_BYTE); //循环读数
}
FLASH_CS_Set();
}
读数据也很有意思,一旦开始,只要CS电平为低,它就一直读下去,直至把整片FLASH芯片数据读完(不过读完会怎样我也没试过)。所以一般读到我们想要的数据之后拉高CS电平就好了。
9,main.c
int main(void)
{
unsigned char ccc[300];
unsigned char ddd[300] = 0;
unsigned char *p;
unsigned char *qq;
int t;
for(t = 300;t >= 0;t --)
{
ccc[300 - t] = t;
}
p = ccc;//或者是p = &ccc[0];
qq = ddd;
Init_SPI();
OLED_Init();
OLED_Clear();
//FLASH_ERASE_BULK();
FLASH_ERASE_SECTOR(0xF0000);
FLASH_ANY_PROGRAM(0xF0000,p,300);
FLASH_READ_DATA(0xF0000,qq,300);
while(1)
{
OLED_Clear();
OLED_ShowChar(2,2,*(qq + 299) + 45);
OLED_ShowChar(40,4,*(qq + 288) + 45);
OLED_ShowChar(80,6,*(qq + 289) + 45);
}
}
逻辑:准备两个数组:ccc和ddd,分别用来存放要发送的和要接收的数据。从得到的数据中挑几个显示在OLED上,结合OLED原有的字库,验证是否准确。