第13章 DS18B20温度传感器和Flash存储器(13.3 13.4)
13.3 Flash存储器
Flash存储器又名闪存,也是一种掉电后可以存储数据的存储器,按主要类型分为NOR Flash和NAND Flash。Flash存储器在现代电子设备中扮演者重大角色,广泛应用于手机、平板。数码相机等消费电子产品,以及汽车、工业控制、航空航天等领域,主流单片机的程序存储空间也是Flash。
为了更好的理解Flash,将Flash和EEPROM的主要特点进行对比。
1、相较于EEPROM,Flash存储器能够提供较高的存储密度,容量更大,适合需要大容量存储的应用场景。列如Kingst51开发板上的Flash型号为W25Q32,是一个32Mbit大小的Flash存储器。而24C02的仅有256个字节,也就是2Kbit存储空间,存储空间上是16000倍,价格上仅2到3倍。
2、相较于EEPROM,Flash存储器的读写速度更快,尤其在读取速度方面,NOR Flash的读取速度超级快,适应于需要频繁读取操作的应用。
3、相较于Flash,EEPROM允许按照字节进行写操作,可以灵活地修改单个字节的数据。而Flash要修改某个数据前,一般需要对整个扇区(4096字节)进行擦除后(整个扇区初始化为1)才能重新写入。因此EEPROM更适合需要频繁更新数据的应用场景,而Flash更适合读写大量数据而不需要频繁改变数据的场景。
4、Flash的读写比EEPROM的读写略微,用户每次操作之前需要通知Flash具体操作指令,列如是“读”还是“写”,写的话是“写寄存器”还是“写数据”。写之前要先写使能等等操作。
当然这些指令在手册里有列表说明,每一条指令也都有详细解释,如表13-1所示。

表13-1 Flash指令集
读Flash流程:
1、检测是否“忙”。
2、使能引脚,写入“读数据”指令。
3、发送Flash读数据起始地址,需要注意的是W25Q32这颗Flash的存储空间是32Mbit,即4M字节,因此他的地址是24位地址。
4、根据当前地址读取相应数量的数据。
写Flash流程:
1、检测是否“忙”。
2、使能引脚,写入“写使能”指令。
3、发送“页写”指令。
4、发送Flash写数据起始地址。
5、连续发送要写入的数据,写入到Flash中去。
值得注意的是,如果写Flash的数据牵扯到跨页写入,需要对跨页进行操作处理。
写一个简单的程序,从某一地址读出连续的2个数据,第一个字节加1,第二个字节加2,分别重新写回到flash中去。
/*****************************main.c文件程序源代码******************************/
#include <reg52.h>
extern void FlashRead(unsigned char *buf, unsigned long addr, unsigned int len);
extern void FlashSectorErase(unsigned long addr);
extern unsigned int FlashPageWrite(unsigned char *buf, unsigned long addr, unsigned int len);
void main()
{
unsigned char buf[2]; //数据读写缓冲区
unsigned long addr = 0; //数据读写地址
FlashRead(buf, addr, 2); //将数据读入缓冲区
buf[0] += 1; //第一个字节+1
buf[1] += 2; //第二个字节+2
FlashSectorErase(addr); //擦除读写地址所在扇区
FlashPageWrite(buf, addr, 2); //写入缓冲区中的数据
while (1);
}
/*****************************flash.c文件程序源代码******************************/
#include <reg52.h>
#define FLASH_READ_REG1 0x05 //读寄存器1命令
#define FLASH_READ_DATA 0x03 //读数据命令
#define FLASH_WRITE_ENABLE 0x06 //写使能命令
#define FLASH_SECTOR_ERASE 0x20 //扇区擦除命令
#define FLASH_PAGE_WRITE 0x02 //页写入命令
#define FLASH_PAGE_SIZE 256 //每页字节数
#define FLASH_SECTOR_SIZE 4096 //每扇区字节数
sbit SPI_SCK = P3^5;
sbit SPI_MISO = P3^4;
sbit SPI_MOSI = P1^5;
sbit SPI_SSEL = P1^7;
/* SPI总线写操作:dat-待写入字节 */
void SPIWrite(unsigned char dat)
{
unsigned char mask; //用于探测字节内某一位值的掩码变量
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
SPI_SCK = 0; //拉低SCK
if ((mask&dat) == 0) //该位的值输出到MOSI上
SPI_MOSI = 0;
else
SPI_MOSI = 1;
SPI_SCK = 1; //再拉高SCK
}
}
/* SPI总线读操作:返回值-读到的字节 */
unsigned char SPIRead()
{
unsigned char mask;
unsigned char dat;
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
SPI_SCK = 0; //拉低SCK
if (SPI_MISO == 0) //读取MISO的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
SPI_SCK = 1; //再拉高SCK
}
return dat;
}
/* Flash忙等待函数,循环查询busy标志位,至芯片不再忙时函数返回 */
void FlashBusyWait()
{
unsigned char dat;
do {
SPI_SSEL = 0;
SPIWrite(FLASH_READ_REG1); //发送读寄存器1命令
dat = SPIRead(); //读取寄存器1的值
SPI_SSEL = 1;
} while (dat & 0x01); //判断读回的寄存器最低位,即busy标志位
}
/* Flash读取函数:buf-数据接收指针,addr-起始地址,len-读取长度 */
void FlashRead(unsigned char *buf, unsigned long addr, unsigned int len)
{
FlashBusyWait();
//发送读数据命令与地址
SPI_SSEL = 0;
SPIWrite(FLASH_READ_DATA);
SPIWrite(addr>>16);
SPIWrite(addr>>8);
SPIWrite(addr);
//连续读取数据
while (len–)
{
*buf++ = SPIRead();
}
SPI_SSEL = 1;
}
/* Flash扇区擦除函数:addr-擦除地址 */
void FlashSectorErase(unsigned long addr)
{
FlashBusyWait();
//发送写使能命令
SPI_SSEL = 0;
SPIWrite(FLASH_WRITE_ENABLE);
SPI_SSEL = 1;
//发送擦除命令与地址
SPI_SSEL = 0;
SPIWrite(FLASH_SECTOR_ERASE);
SPIWrite(addr>>16);
SPIWrite(addr>>8);
SPIWrite(addr);
SPI_SSEL = 1;
}
/* Flash页写入函数:
buf-源数据指针,addr-起始地址,
len-待写入长度,返回值-实际写入长度;
本函数不保证将全部数据都写入Flash中,当地址跨页时即停止写入,
如写入数据需跨页时,请在在调用时自行处理跨页及可能的擦除操作 */
unsigned int FlashPageWrite(unsigned char *buf, unsigned long addr, unsigned int len)
{
unsigned int n;
//计算起始地址至下一个页边界的字节数,即可写入的最大字节数
n = FLASH_PAGE_SIZE – (addr % FLASH_PAGE_SIZE);
//待写入长度超过可写入最大字节数时,将其重置为最大字节数
if (len > n)
{
len = n;
}
FlashBusyWait();
//发送写使能命令
SPI_SSEL = 0;
SPIWrite(FLASH_WRITE_ENABLE);
SPI_SSEL = 1;
//发送页写命令及地址
SPI_SSEL = 0;
SPIWrite(FLASH_PAGE_WRITE);
SPIWrite(addr>>16);
SPIWrite(addr>>8);
SPIWrite(addr);
//连续发送待写入数据
for (n=0; n<len; n++)
{
SPIWrite(*buf++);
}
SPI_SSEL = 1;
return len; //返回实际写入的数据长度
}
flash.c程序的最开始把特殊指令和页大小、扇区大小进行宏定义。
SPIWrite:将单字节按照SPI时序发送。
SPIRead:按照SPI协议读取一个字节数据。
FlashBusyWait:检测Flash是否“忙”状态。Flash在跨页写入、扇区擦除、块擦除等操作中都需要必定的时间,这段时间flash都处于“忙”状态,读写指令均不响应,因此读写之前要使用这个函数进行“忙”检测。
FlashRead:Flash连续读数据指令。
FlashSectorErase:Flash扇区擦除。Kingst51开发板所采用的W25Q32这个Flash最小擦除单位就是扇区擦除。因此除非擦除后没有写入数据,否则要改变已经写入的数据,必须进行擦除动作,最小擦除单元为扇区。
FlashPageWrite:由于连续写入数据有可能遇到跨页的情况,本函数不提供跨页判断,一旦发现无法写入后,将返回写入的字节长度,跨页判断可以在上层应用函数中实现。
将使用SPI协议读写Flash的时序用逻辑分析仪抓出来,并且分别用SPI协议进行解析,如图13-10和13-11所示。

13-10 SPI配置信息

13-11 SPI通信时序图
软件的SPI的配置信息,必须和实际通信一致,才能正确解析数据。除了使用SPI进行解析外,由于这是个flash器件,还可以使用特有的QSPI-flash解析器直接将指令解析出来,如图13-12和图13-13所示。

13-12 QSPI-flash配置信息

13-13 QSPI-flash解析结果
由于程序中存在扇区擦除操作,扇区擦除的时候flash会一直处于“忙”状态无法响应再次写入,因此程序中间段存在一些忙检测波形。将解析结果导出到excel表格中,截取一部分进行观察,如图13-14所示,一共403个字节的数据,图片左侧为前16个字节,图片右侧为后17个字节,中间字节全部为“忙检测”。

图13-14 QSPI-flash数据导出
从解析的数据可以看出,第一个字节是读状态寄存器,了解Flash是否在忙,第二个字节读到了0x00,说明Flash处于不忙的状态。第三个字节0x03为读数据命令,第四个字节是要读取0x000000这个地址的连续数据,读到的2个字节数据是0x05和0x0B。而后再次进行忙检测,由于刚刚进行的是读操作,所以Flash还是处于不忙的状态。然后使用0x06这个写使能命令,写入了0x20指令进行了从0x00地址开始的单个扇区擦除(4096字节)。擦除命令结束后程序进入循环检测flash的“忙”信号,此时在一段时间内,发送0x05,回复的都是0x03,说明flash都处于忙状态,一直到解析后数据的第398行再次检测到0x00的不忙信号,发送0x06这个写使能指令,发送0x02的页写入指令,从地址0x000000开始,写入的2个字节分别为0x06和0x0D。
W25Q32这个Flash的页写入、扇区擦除、块擦除等各种操作所需要的大致时间在手册里都有标注,如图13-15所示。

13-5 W25Q32时序要求
13.4 练习题
1、掌握 DS18B20的时序过程,能够理解1-wrie总线读写的时序。
2、理解SPI的通信原理,SPI通信过程的四种模式配置。
3、理解flash存储器的基本原理和操作方式,能独立完成flash的读写操作。