第二十章 NOR Flash实验
本章将介绍如何使用Kendryte K210的SPI功能,并实现对外部Flash的读写并把结果显示在TFTLCD模块上。通过学习本章内容,读者将掌握利用SDK编程技术实现外部Flash的读写方法。 本章分为如下几个小节: 20.1SPI和NOR Flash芯片简介 20.2 硬件设计 20.3 程序设计 20.4 运行验证
20.1 SPI和NOR Flash简介 20.1.1 SPI简介有关Kendryte K210的SPI模块介绍,请见第16.1.1小节《SPI简介》。 20.1.2 NOR Flash简介20.1.2.1 Flash简介Flash是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的Flash主要有NOR Flash和Nand Flash两种类型,它们的特性如表20.1.2.1.1所示。NOR和NAND是两种数字门电路,可以简单地认为Flash内部存储单元使用哪种门作存储单元就是哪类型的Flash。U盘,SSD,eMMC等为NAND型,而NOR Flash则根据设计需要灵活应用于各类PCB上,如BIOS,手机等。 表20.1.2.1.1 NOR Flash和NAND Flash特性对比 NOR与NAND在数据写入前都需要有擦除操作,但实际上NOR Flash的一个bit可以从1变成0,而要从0变1就要擦除后再写入,NAND Flash这两种情况都需要擦除。擦除操作的最小单位为“扇区/块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前的数据都可能会被擦除。 NOR的地址线和数据线分开,它可以按“字节”读写数据,符合CPU的指令译码执行要求,所以假如NOR上存储了代码指令,CPU给NOR一个地址,NOR就能向CPU返回一个数据让CPU执行,中间不需要额外的处理操作,这体现于表20.1.2.1.1中的支持XIP特性(eXecute In Place)。因此可以用NOR Flash直接作为嵌入式MCU的程序存储空间。 NAND的数据和地址线共用,只能按“块”来读写数据,假如NAND上存储了代码指令,CPU给NAND地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。 若代码存储在NAND上,可以把它先加载到RAM存储器上,再由CPU执行。所以在功能上可以认为NOR是一种断电后数据不丢失的RAM,但它的擦除单位与RAM有区别,且读写速度比RAM要慢得多。 Flash也有对应的缺点,我们在使用过程中需要尽量去规避这些问题:一是Flash的使用寿命,另一个是可能的位反转。 使用寿命体现在:读写上是Flash的擦除次数都是有限的(NOR Flash普遍是10万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。由于NAND通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用NAND Flash最好通过算法扫描介质找出坏块并标记为不可用,因为坏块上的数据是不准确的。 位反转是数据位写入时为1,但经过一定时间的环境变化后可能实际变为0的情况,反之亦然。位反转的原因很多,可能是器件特性也可能与环境、干扰有关,由于位反转的问题可能存在,所以FLASH存储器需要“探测/错误更正(EDC/ECC)”算法来确保数据的正确性。 FLASH芯片有很多种芯片型号,比如有:W25Q128、BY25Q128、NM25Q128,它们是来自不同的厂商的同种规格的NOR Flash芯片,内存空间都是128M字,即16M字节。它们的很多参数、操作都是一样的。 下面我们以W25Q128为例,认识一下具体的NOR Flash的特性。 W25Q128是一款大容量SPI Flash产品,其容量为16M。它将16M字节的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每一个扇区16页,每页256个字节,即每个扇区4K个字节。W25Q128的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Q128开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。 W25Q128的擦写周期多达10W次,具有20年的数据保存期限,支持电压为2.7~3.6V,W25Q128支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可以到104Mhz(双输出时相当于208Mhz,四输出时相当于280M)。 下面我们看一下W25Q128芯片的管脚图,如图20.1.2.1.1所示。 图 20.1.2.1.1 W25Q128芯片引脚图 芯片引脚连接如下:CS即片选信号输入,低电平有效;DO是MISO引脚,在CLK管脚的下降沿输出数据;WP是写保护管脚,高电平可读可写,低电平仅仅可读;DI是MOSI引脚,主机发送的数据、地址和命令从SI引脚输入到芯片内部,在CLK管脚的上升沿捕获数据;CLK是串行时钟引脚,为输入输出提供时钟脉冲;HOLD是保持管脚,低电平有效。 Kendryte K210通过SPI总线连接到W25Q128对应的引脚即可启动数据传输。 20.1.2.2 NOR Flash工作时序前面对于W25Q128的介绍中也提及其存储的体系,W25Q128有写入、读取还有擦除的功能,下面就对这三种操作的时序进行分析,在后面通过代码的形式驱动它。 下面先让我们看一下读操作时序,如图20.1.2.2.1所示: 图20.1.2.2.1 W25Q128读操作时序图 从上图可知读数据指令是03H,可以读出一个字节或者多个字节。发起读操作时,先把CS片选管脚拉低,然后通过MOSI引脚把03H发送芯片,之后再发送要读取的24位地址,这些数据在CLK上升沿时采样。芯片接收完24位地址之后,就会把相对应地址的数据在CLK引脚下降沿从MISO引脚发送出去。从图中可以看出只要CLK一直在工作,那么通过一条读指令就可以把整个芯片存储区的数据读出来。当主机把CS引脚拉高,数据传输停止。 接着我们看一下写时序,这里我们先看页写时序,如图20.1.2.2.2所示: 图20.1.2.2.2 W25Q128页写时序 在发送页写指令之前,需要先发送“写使能”指令。然后主机拉低CS引脚,然后通过MOSI引脚把02H发送到芯片,接着发送24位地址,最后你就可以发送你需要写的字节数据到芯片。完成数据写入之后,需要拉高CS引脚,停止数据传输。 下面介绍一下扇区擦除时序,如图20.1.2.2.3所示: 图20.1.2.2.3 扇区擦除时序图 扇区擦除指的是将一个扇区擦除,通过前面的介绍也知道,W25Q128的扇区大小是4K字节。擦除扇区后,扇区的位全置1,即扇区字节为FFh。同样的,在执行扇区擦除之前,需要先执行写使能指令。这里需要注意的是当前SPI总线的状态,假如总线状态是BUSY,那么这个扇区擦除是无效的,所以在拉低CS引脚准备发送数据前,需要先要确定SPI总线的状态,这就需要执行读状态寄存器指令,读取状态寄存器的BUSY位,需要等待BUSY位为0,才可以执行擦除工作。 接着按时序图分析,主机先拉低CS引脚,然后通过MOSI引脚发送指令代码20h到芯片,然后接着把24位扇区地址发送到芯片,然后需要拉高CS引脚,通过读取寄存器状态等待扇区擦除操作完成。 此外还有对整个芯片进行擦除的操作,时序比扇区擦除更加简单,不用发送24bit地址,只需要发送指令代码C7h到芯片即可实现芯片的擦除。 在W25Q128手册中还有许多种方式的读/写/擦除操作,我们这里只分析本实验用到的,其他大家可以参考W25Q128手册,存放路径A盘à硬件资料à芯片资料。 20.2 硬件设计 20.2.1 例程功能 1. 通过KEY1按键来控制NOR Flash的写入,通过按键KEY0来控制NOR Flash的读取。并在LCD模块上显示相关信息。 20.2.2 硬件资源 1. 独立按键 KEY0按键 - IO18 KEY1按键 - IO19 KEY2按键 - IO16 2. NOR Flash CS - F_CS SO - F_D1 WP - F_D2 SI - F_D0 CLK - F_CLK HOLP - F_D3 3. LCD LCD_RD - IO34 LCD_BL - IO35 LCD_CS - IO36 LCD_RST - IO37 LCD_RS - IO38 LCD_WR - IO39 LCD_D0~LCD_D7 - SPI0_D0~SPI0_D7 20.2.3 原理图 本章实验内容,需要使用到板载的W25Q128,正点原子DNK210开发板上的NOR Flash连接原理图,如下图所示: 图20.2.3.1 ATK-MC2640 NOR Flash接口原理图 20.3 程序设计 20.3.1 NOR Flash驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NOR Flash驱动源码包括四个文件:w25qxx.c、w25qxx.h、norflash.c和norflash.h。这四个文件可以做个简单的区分,w25qxx.c和w25qxx.h文件来源于官方的SDK裸机编程DEMO,用于NOR Flash的底层驱动,norflash.c和norflash.h文件是由正点原子团队为了符合我们的代码规范而编写,提供函数接口方便外部FLASH的读写和擦除等。我们这里主要介绍norflash.c和norflash.h文件的函数。 下面介绍norflash.c文件几个重要的函数,首先是NOR Flash初始化函数,其定义如下: /** * @param 无 * @retval 0:成功 1:失败 */ void norflash_init(void) { uint8_t manuf_id, device_id; uint8_t spi_index = 3, spi_ss = 0; w25qxx_init(spi_index, spi_ss); w25qxx_enable_quad_mode(); /* flash 四倍模式开启*/ /* 读取flash的ID */ norflash_read_id(&manuf_id, &device_id); printf("manuf_id:0x%02x, device_id:0x%02x\r\n", manuf_id, device_id); if ((manuf_id != 0xEF && manuf_id != 0xC8) || (device_id != 0x17 && device_id != 0x16)) { /* flash初始化失败 */ printf("w25qxx_read_id error\n"); printf("manuf_id:0x%02x, device_id:0x%02x\r\n", manuf_id, device_id); return 0; } else { return 1; } } 在初始化函数中,首先定义变量manuf_id和device_id,分别用于存放NOR Flash的厂家ID和芯片ID,然后选择SPI总线和设备号初始化W25Q128,使能SPI四线模式,最后调用norflash_read_id函数读取厂家ID和设备ID判断W25Q128是否初始化成功,W25Q128的厂家ID是0XEF,设备ID是0X17,读取成功函数返回0。 下面介绍一下FLASH读取函数,其定义如下: /** * @brief 读取SPI FLASH * @param pbuf : 数据存储区 * @param addr : 开始读取的地址 * @param datalen : 要读取的字节数 * @retval 无 */ void norflash_read(uint8_t *pbuf, uint32_t addr, uint32_t datalen) { /* norflash读取数据 */ w25qxx_read_data(addr, pbuf, datalen, W25QXX_QUAD_FAST); } 该函数直接调用w25qxx_read_data函数实现,大家可以访问到w25qxx.c文件的_w25qxx_read_data函数,这里可以根据前面的时序图对照理解。 有读函数,那肯定就有写函数,接下来我们介绍一下NOR FLASH写函数,其定义如下: /** * @brief 写SPI FLASH * @note 在指定地址开始写入指定长度的数据 , 该函数带擦除操作! * SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block * 擦除的最小单位为Sector. * * @param pbuf : 数据存储区 * @param addr : 开始写入的地址 * @param datalen : 要写入的字节数 * @retval 无 */ void norflash_write(uint8_t *pbuf, uint32_t addr, uint32_t datalen) { w25qxx_write_data(addr, pbuf, datalen); } 该函数直接调用w25qxx_write_data函数实现,我们在w25qxx.c文件找到w25qxx_write_data函数,其代码如下所示。 w25qxx_status_t w25qxx_write_data(uint32_t addr, uint8_t *data_buf, uint32_t length) { uint32_t sector_addr = 0; uint32_t sector_offset = 0; uint32_t sector_remain = 0; uint32_t write_len = 0; uint32_t index = 0; uint8_t *pread = NULL; uint8_t *pwrite = NULL; uint8_t swap_buf[w25qxx_FLASH_SECTOR_SIZE] = {0}; while (length) { sector_addr = addr & (~(w25qxx_FLASH_SECTOR_SIZE - 1)); sector_offset = addr & (w25qxx_FLASH_SECTOR_SIZE - 1); sector_remain = w25qxx_FLASH_SECTOR_SIZE - sector_offset; write_len = ((length < sector_remain) ? length : sector_remain); w25qxx_read_data(sector_addr, swap_buf, w25qxx_FLASH_SECTOR_SIZE); pread = swap_buf + sector_offset; pwrite = data_buf; for (index = 0; index < write_len; index++) { if ((*pwrite) != ((*pwrite) & (*pread))) { w25qxx_sector_erase(sector_addr); while (w25qxx_is_busy() == W25QXX_BUSY) ; break; } pwrite++; pread++; } if (write_len == w25qxx_FLASH_SECTOR_SIZE) { w25qxx_sector_program(sector_addr, data_buf); } else { pread = swap_buf + sector_offset; pwrite = data_buf; for (index = 0; index < write_len; index++) *pread++ = *pwrite++; w25qxx_sector_program(sector_addr, swap_buf); } length -= write_len; addr += write_len; data_buf += write_len; } return W25QXX_OK; } 该函数可以在NOR Flash的任意地址开始写入任意长度(必须不超过NOR Flash的容量)的数据。我们这里简单介绍一下思路:先获得首地址(addr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个swap_buf的数组,用于擦除时缓存扇区内的数据。 下面简单介绍一下擦除函数,前面工作时序中也有对此描述,现在就来看一下代码: /** * @brief 擦除整个芯片 * @note 等待时间超长... * @param 无 * @retval 无 */ void norflash_erase_chip(void) { w25qxx_chip_erase(); } /** * @brief 擦除一个扇区 * @note 注意,这里是扇区地址,不是字节地址!! * * @param saddr : 扇区地址 根据实际容量设置 * @retval 无 */ void norflash_erase_sector(uint32_t saddr) { //printf("fe:%x\r\n", saddr); /* 监视falsh擦除情况,测试用 */ w25qxx_sector_erase(saddr); } 该代码也是老套路,直接调用对应的函数实现擦除功能,norflash_erase_chip函数是整片擦除,所需时间较长,norflash_erase_sector函数是按扇区擦除。需要注意的是假如调用了norflash_erase_chip函数将会对整个NOR Flash进行擦除,一般情况不建议对整个NOR Flash进行擦除,因为Kendryte K210的程序存储在NOR Flash上,整片擦除会导致程序全部丢失。 20.3.2 main.c代码 main.c中的代码如下所示: /* 要写入到FLASH的字符串数组 */ const uint8_t g_text_buf[] = {"NORFLASH TEST"}; #define LCD_SPI_CLK_RATE 15000000 #define TEXT_SIZE sizeof(g_text_buf) + 4 /* TEXT字符串长度 */ #define DATA_ADDRESS 0xB00000 /* 读写地址 */ int main(void) { uint8_t key; uint8_t i = 0; uint8_t datatemp[TEXT_SIZE]; sysctl_pll_set_freq(SYSCTL_PLL0, 800000000); sysctl_pll_set_freq(SYSCTL_PLL1, 400000000); sysctl_pll_set_freq(SYSCTL_PLL2, 45158400); sysctl_set_power_mode(SYSCTL_POWER_BANK6, SYSCTL_POWER_V18); sysctl_set_power_mode(SYSCTL_POWER_BANK7, SYSCTL_POWER_V18); sysctl_set_spi0_dvp_data(1); lcd_init(); /* 初始化LCD */ lcd_set_direction(DIR_YX_LRUD); key_init(); /* 初始化按键 */ norflash_init(); /* 初始化NORFLASH */ lcd_draw_string(10, 10, "KEY1:Write KEY0:Read", RED); /* 显示提示信息 */ lcd_draw_string(10, 30, "SPI FLASH Ready!", BLUE); while (1) { key = key_scan(0); if (key == KEY1_PRES) /* KEY1按下,写入 */ { lcd_draw_fill_rectangle(0, 50, 319, 90, WHITE); /* 清除显示 */ lcd_draw_string(10, 50, "Start Write FLASH.", BLUE); sprintf((char *)datatemp, "%s%d", (char *)g_text_buf, i); norflash_write((uint8_t *)datatemp, DATA_ADDRESS, TEXT_SIZE); lcd_draw_fill_rectangle(10, 50, 319, 70, WHITE); /* 清除显示 */ lcd_draw_string(10, 50, "FLASH Write Finished!", BLUE); } else if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */ { lcd_draw_fill_rectangle(10, 50, 319, 90, WHITE); /* 清除显示 */ lcd_draw_string(10, 50, "Start Read FLASH.", BLUE); norflash_read(datatemp, DATA_ADDRESS, TEXT_SIZE); lcd_draw_fill_rectangle(10, 50, 319, 70, WHITE); /* 清除显示 */ lcd_draw_string(10, 50, "The Data Readed Is: ", BLUE); lcd_draw_string(10, 70, (char *)datatemp, BLUE); } i++; if (i > 200) { i = 0; } msleep(10); } } 在main函数前面,我们定义了g_text_buf数组,用于存放要写入到FLASH的字符串。main函数代码具体流程大致是:首先完成系统级和按键、LCD、NOR Flash初始化工作,然后提醒交互信息,最后通过KEY0去读取地址DATA_ADDRESS处开始的数据并把数据显示在LCD上;另外还可以通过KEY1在地址DATA_ADDRESS处写入g_text_buf数据并在LCD界面中显示传输中,完成后并显示“FLASH Write Finished!”。 20.4 运行验证 将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,可以看到LCD显示信息,LCD显示的内容如图20.4.1所示: 图20.4.1 SPI实验程序运行效果图 通过先按下KEY1写入数据,然后再按KEY0读取数据,得到如图20.4.2所示: 图20.4.2 操作后的显示效果图
|