NorFlash 学习 NorFlash 学习


title: NorFlash 学习
tags: ARM
date: 2018-10-19 18:31:59

NOR/NAND Flash的差别

NOR NAND
容量 1~32MB 16~512MB
XIP 可执行程序 Yes(因为是内存接口), 可以随机访问任意地址的数据 No
擦除 非常慢(5s) 快(3ms)
可靠性 比较高,位反转的比例小于NAND Flash 的10% 比较低:,位反转比较常见,必需有校验措施,比如“1-4 bit EDC/ECC”;必须有坏块管理措施
可擦除次数 10000~100000 100000~1000000
生命周期 低于NAND Flash的10% 是NOR Flash的10倍以上
接口 RAM接口相同 I/O接口
访问方法 随机访问 顺序访问
易用性 容易 复杂
主要用途 常用于保存代码和关键数据 用于保存数据
价格
  • Nor Flash的块大小范围为64kB、128kB:NAND Flash的块大小范围为8kB,64kB,擦/写一个Nor Flash块需4s,而擦/写一个NAND Flash块仅需2ms。
  • NAND Flash一般以512字节为单位进行读写。这使得Nor Flash适合于运行程序,而NAND Flash更适合于存储数据
  • 容量相同的情况下,NAND Flash的体积更小.Nor Flash的容量通常为IMB~4MB(也有32MB的Nor Flash),NAND Flash的容量为8MB~512MB。
  • 在Nor Flash上常用jffs2文 件系统,而在NAND Flash常用yaffs文件系统。在更底层,有MTD驱动程序实现对它们的读、写、擦除操仵,它也实现了EDC/ECC校验。
  • 这里用的nor里面有4个bank,每个bank里面有若干block(擦除大小),不同bank里面的block可能不一样的

引脚接口

更多信息参考 2440内存管理

这里NorFlash是16位的,所以地址线忽略A0

NorFlash 学习
NorFlash 学习

命令

NorFlash 学习
NorFlash 学习

所谓命令是什么意思呢?

  • Read MODE,就是先发送地址,然后就有数据得到,和正常的读内存是一样的

  • Reset Mode,往任意地址写0xF0都退出特殊模式

  • 读ID,NOR Flash 分为1字节和2字节模式,所以有两种情况的操作指令.板载是2字节的.操作如下:

    1. 发送地址555,发送数据AA, 0x555*2=0xaaa
    2. 发送地址2AA,发送数据55 0x2AA*2=0x554
    3. 发送地址555,发送数据90 0x555*2=0xaaa
    4. 发送地址00,收到数据C2

但是板载的MCU的A1接到Nor的A0,所以也就是Nor获得地址X,实际MCU需要发送地址X<<1

Nor Flash的操作 2440的操作 U-BOOT上的操作
往地址555H写入AAH(解锁) 往地址AAAH写入AAH(解锁) mw.w aaa aa
往地址2AAH写入55H(解锁) 往地址554H写入55H(解锁) mw.w 554 55
往地址555H写入90H(命令) 往地址AAAH写入90H(命令) mw.w aaa 90
读0地址得到厂家ID(C2H) 读0地址得到厂家ID(C2H) md.w 0 1 (1:表示读一次)
读1地址得到设备ID(22DAH或225BH) 读2地址得到设备ID 2249 md.w 2 1 (1:表示读一次)
退出读ID状态:给任意地址写F0H就可以了 退出读ID状态:给任意地址写F0H就可以了 mw.w 0 f0

注意,这个uboot应该在NOR上启动,因为NAND启动的时候,看不到NORFLASH

Device ID : MX29LV160DT: 22C4; MX29LV160DB: 2249.

OpenJTAG> mw.w 0 f0
OpenJTAG> mw.w aaa aa
OpenJTAG> mw.w 554 55
OpenJTAG> mw.w aaa 90
OpenJTAG> md.w 0 1
00000000: 00c2    ..
OpenJTAG> md.w 2 1
00000002: 2249    I"

参数识别(CFI)

CFI(common flash interface)探测,就是直接发各种命令来读取芯片的信息,比如 ID、容量等,芯片本身就包含了电压有多大,容量有有多少等信息.其实就是先用55地址写98,然后读参数.

一般情况下先去读取是否支持CFI,也就是

  1. 写98H到55H
  2. 查询0x10,0x11,0x12是否为QRY字符串
  3. 如果是,那么支持CFI接口
Nor Flash上操作cfi 2440上操作cfi U-BOOT上操作cfi
往55H地址写入98H 往AAH地址写入98H mw.w aa 98
读地址10H得到0051 读地址20H得到0051 md.w 20 1
读地址11H得到0052 读地址22H得到0052 md.w 22 1
读地址12H得到0059 读地址24H得到0059 md.w 24 1
读地址27H得到容量 读地址4EH得到容量 md.w 4e 1

下图中读取地址10,读取到0x0051='Q',比如读设备大小地址0x27,会读到0x15=2**15/1024/1024=2M,具体的一些参数参考标准文档CFI100

NorFlash 学习
NorFlash 学习

NorFlash 学习
NorFlash 学习

识别存储信息,其中具体的信息要搜索文档CFI100

可以看到有4个bank reggion,region里又若干sector.这个是擦除最小单位了.根据计算如下.其中2D为低位,2E为高位.block=[2E:2D]+1,

blocks sector size
bank1 1 256*0x0040/1024=16k
bank2 2 256*0x0020/1024=8k
bank3 1 256*0x0080/1024=32k
bank3 0x1E+1=31 256*0x0100/1024=64k
总计 35个block 空间大小=16+2*8+32+64*31=2048k=2M

NorFlash 学习
NorFlash 学习

CFI描述如下

NorFlash 学习
NorFlash 学习

NorFlash 学习
NorFlash 学习

备注手册里另一张图Table 1-1. MX29LV160DT SECTOR ARCHITECTURE好像是倒着来的扇区排序

写数据

NorFlash也是一种FLASH,需要先擦除才能写数据.对于已经擦除的单元,同样需要命令才能写入

Nor Flash上操作写操作 2440上操作写操作 U-BOOT上操作写操作
往地址555H写AAH(解锁) 往地址AAAH写AAH(解锁) mw.w aaa aa
往地址2AAH写55H(解锁) 往地址554H写55H(解锁) mw.w 554 55
往地址555H写A0H 往地址AAAH写A0H mw.w aaa a0
往地址PA写PD 往地址0x100000写1234h mw.w 100000 1234

命令参考,往1M的地方写,地址需要*2,也就是0x20,0000


OpenJTAG> mw.w aaa aa
OpenJTAG> mw.w 554 55
OpenJTAG> mw.w aaa 80
OpenJTAG> mw.w aaa aa
OpenJTAG> mw.w 554 55
OpenJTAG> mw.w 200000 30
OpenJTAG> mw.w 0 f0
OpenJTAG> md.w 200000 1
00200000: ffff    ..

//读取
OpenJTAG> mw.w aaa aa
OpenJTAG> mw.w 554 55
OpenJTAG> mw.w aaa a0
OpenJTAG> mw.w 200000 1234
OpenJTAG> md.w 200000 1
00200000: 1234    4.

//接下来不能直接写了 又需要重新来执行命令
OpenJTAG> mw.w 200004 5555
OpenJTAG> md.w 200004
00200004: ffff    ..

程序设计

注意 板载的是16位位宽,地址线是CPU的A1接到NOR的A0,所以MCU地址=NOR指令地址*2.

调试代码 的时候发现工具MobaXterm_Personal_10.4有时候无法接受串口输入,重启下软件串口

  • 时序设置: 参考内存管理.md

  • 基本的读写操作

     #define NOR_BASE_BANK   0
    
    /**
     * @brief 在基于Nor cmd_addr的位置写入 val
     * 比如在55H的地方写入x,也就是地址信号发出55,
     * 对应实际的地址线为55<<1 地址线 加上bank的基地址,
     * 所以cpu实际的地址是 base+(55<<1)
     * @param base 
     * @param addr 
     * @param val 
     */
    void NorWriteU16(uint16_t base,uint16_t addr, uint16_t val)
    {
        volatile uint16_t* p= (volatile uint16_t* )((addr<<1)+base);
        *p=val;  
    }
    /**
     * @brief 读取2字节
     * 
     * @param base 
     * @param addr 
     * @param val 
     */
    uint16_t NorReadU16(uint16_t base,uint16_t addr)
    {
        volatile uint16_t* p= (volatile uint16_t* )((addr<<1)+base);
        return *p;
    }
    
    /**
     * @brief  CMD写入16位数据
     * @param addr cmd地址
     * @param val  写入的值
     */
    void NorFlashCMDPut(uint16_t addr,uint16_t val )
    {
        NorWriteU16(NOR_BASE_BANK,addr,val);
    }
    /**
     * @brief CMD读取Nor的16位数据
     * 
     * @param addr 
     * @param val 
     * @return uint16_t 
     */
    uint16_t NorFlashCMDGet(uint16_t addr)
    {
        return NorReadU16(NOR_BASE_BANK,addr);
    }
    
    
  • 识别CFI

    void NorFlashInfo(void)
    {
        // 进入CFI模式,INTERFACE (CFI) MODE 
        NorFlashCMDPut(0x55,0x98);
        //开始读取数据
            uint8_t buf[10]={0,0,0,0,0,0,0,0,0};
            //尝试读取“QRY”
            buf[0]=(uint8_t)NorFlashCMDGet(0x10);
            buf[1]=(uint8_t)NorFlashCMDGet(0x11);
            buf[2]=(uint8_t)NorFlashCMDGet(0x12);
            buf[3]=(uint8_t)' ';
            printf("get 0x20,0x22,0x24=%s
    ",buf);
            printf("end
    ");
        NorFlashCMDPut(0, 0xf0);
    }
    
  • NOR的擦除与写

    注意: NOR的物理地址是需要CPU地址>>1的,所以操作地址的时候之前的底层函数已经使用了cpu地址>>1

    但是,Nor是16位位宽的,也就是如下排列的

    16位宽 8位宽
    00 00,01
    01 02,03
    02 04,05
    03 06,17

    我们需要将8位位宽的地址转换为16位位宽的地址,也就是说addr>>1.

    void do_write_nor_flash(void)
    {
    	unsigned int addr;
    	unsigned char str[100];
    	int i, j;
    		unsigned int val;
    
    	/* 获得地址 */
    	printf("Enter the address of sector to write: ");
    	addr = get_uint();
    
    	printf("Enter the string to write: ");
    	gets(str);
    
    	printf("writing ...
    
    ");
    
    	/* str[0],str[1]==>16bit 
    	 * str[2],str[3]==>16bit 
    	*/
    	i = 0;
    	j = 1;
    	while (str[i] && str[j])
    	{
    		val = str[i] + (str[j]<<8);
    	
    		/* 烧写 */
    		nor_cmd(0x555, 0xaa);	 /* 解锁 */
    		nor_cmd(0x2aa, 0x55); 
    		nor_cmd(0x555, 0xa0);	 /* program */
            
            //这个是重点-------------------------------------------------
    		nor_cmd(addr>>1, val);
            //这个是重点-------------------------------------------------
            
            
    		/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
    		wait_ready(addr);
    
    		i += 2;
    		j += 2;
    		addr += 2;
    	}
    
    	val = str[i];
    	/* 烧写 */
    	nor_cmd(0x555, 0xaa);	 /* 解锁 */
    	nor_cmd(0x2aa, 0x55); 
    	nor_cmd(0x555, 0xa0);	 /* program */
    	nor_cmd(addr>>1, val);
    	/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
    	wait_ready(addr);
    }
    

    FAQ:

    在老师的视频中,写NOR的具体地址时,nor_cmd(addr>>1, val); 这里地址右移1位,老师的解释是地址线A1->A0的缘故.但是我觉得不对.
    因为 nor_cmd函数已经将地址转换为nor视角的地址了,也就是说我想写nor中的x地址,我们应该直接操作地址x即可.

    但是测试之后发现代码确实跑的没问题?那么我哪里理解错了呢?
    后来想到 Nor是16位位宽的,也就是说Nor中的0,其实对应了存储单元[0,1],

    结论: 所以这里的地址>>1,并不是因为地址线偏移(其实也是因为地址线,因为是16位,所以偏移1位),但是从函数的角度来说,实际是因为nor的地址是16位宽的,我们需要转换为8位位宽的 addr>>1 变成16位位宽的地址

    后面的测试了下往 addr=1里面写12,实际是在0,1里面写,往addr=2里面写12,实际是往2里面写,往addr=3里面写,也是在往2里面写

    参考链接

  • 判断是否忙(DQ6)

    • DQ7:Data# Polling bit,DQ7在编程时的状态变化.
      在编程过程中从正在编程的地址中读出的数据的DQ7为要写进数据的补码.比如写进的数据为0x0000,及输进的DQ7为'0',则在编程中读出的数据为'1';当编程完成时读出的数据又变回输进的数据即'0'. 在擦除过程中DQ7输出为'0';擦除完成后输出为'1';留意读取的地址必须是擦除范围内的地址.
      RY/BY#:高电平表示'停当',低电平表示'忙'.

    • DQ6:轮转位1(Toggle Bit 1).
      在编程和擦除期间,读任意地址都会导致DQ6的轮转(0,1间相互变换)当操纵完成后,DQ6停止转换.
      DQ2:轮转位2(Toggle Bit 2).当某个扇区被选中擦除时,读有效地址(地址都在擦除的扇区范围内)会导致DQ2的轮转.
      留意:DQ2只能判定一个特定的扇区是否被选中擦除.但不能区分这个快是否正在擦除中或者正处于擦除暂停状态.相比之下,DQ6可以区分NorFLASH是否处于擦除中或者擦除状态,但不能区分哪个快被选中擦除.因此需要这2个位来确定扇区和模式状态信息.

    • DQ5: 超时位(Exceeded Timing Limits),当编程或擦除操纵超过了一个特定内部脉冲计数是DQ5=1;这表明操纵失败.当编程时把'0'改为'1'就会导致DQ5=1,由于只有擦除擦做才能把'0'改为'1'.当错误发生后需要执行复位命令(见图1-1)才能返回到读数据状态.

    • DQ3: (扇区擦除计时位)Sector Erase Timer,只在扇区擦除指令时起作用.当擦除指令真正开始工作是DQ3=1,此时输进的命令(除擦除暂停命令外)都被忽略.DQ3=0,是可以添加附加的扇区用于多扇区擦除.

      void wait_ready(unsigned int addr)
      {
      	unsigned int val;
      	unsigned int pre;
      
      	pre = nor_dat(addr>>1);
      	val = nor_dat(addr>>1);
      	while ((val & (1<<6)) != (pre & (1<<6)))
      	{
      		pre = val;
      		val = nor_dat(addr>>1);		
      	}
      }
      

程序优化

  1. 老师的写操作并没有去判断地址是否是高低地址,也就是写0地址和写1地址都是写在0地址2个字节,实际上如果写1地址应该先读取0地址,再写1地址,这里会涉及到擦除? 如果都先擦除就没有这个问题

其他设计点

  1. 读写NORFLASH的时候需要关闭中断

    因为我们使用NOR启动的时候,cpu执行中断的时候会跳回到0地址,也就是NOR中去读取指令,这个时候如果在操作NOR,很明显取址有问题了,会死机等.之前串口有时候没输出死机可能就是这个原因了.

  2. nor是16位宽的,所以在擦除和写nor的时候,实际地址需要>>1才是nor要接受的实际地址,具体分析见程序设计章节中的写操作

  3. Makefile中加入选项-march=armv4否则在操作下面函数时,16位变量被处理为2个8位变量.网上搜索是可能gcc 考虑地址对齐的问题

    	volatile unsigned short *p = (volatile unsigned short *)(base + (offset << 1));
    	*p = val;
    	
    	30000f3c:	e5c23000 	strb	r3, [r2]
    
    %.o : %.c
    	arm-linux-gcc -march=armv4 -c -o $@ $<
    
    %.o : %.S
    	arm-linux-gcc -march=armv4 -c -o $@ $<
    

    https://blog.csdn.net/klutz120215/article/details/7277130

  4. 程序应该烧录在NOR上,因为nand启动的时候,片内ram是0地址,mcu无法访问nor