| 第二十章 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 操作后的显示效果图 
 |