1. 基本概念
NOR FLASH是很常见的一种存储芯片,数据掉电不会丢失。NOR FLASH支持Execute On Chip,即程序可以直接在FLASH片内执行(这意味着存储在NOR FLASH上的程序不需要复制到RAM就可以直接运行)。这点和NAND FLASH不一样。因此,在嵌入式系统中,NOR FLASH很适合作为启动程序的存储介质。NOR FLASH的读取和RAM很类似(只要能够提供数据的地址,数据总线就能够正确的给出数据),但不可以直接进行写操作。对NOR FLASH的写操作需要遵循特定的命令序列,最终由芯片内部的控制单元完成写操作。
FLASH一般都分为很多个SECTOR,每个SECTOR包括一定数量的存储单元。对有些大容量的FLASH,还分为不同的BANK,每个BANK包括一定数目的SECTOR。FLASH的擦除操作一般都是以SECTOR,BANK或是整片FLASH为单位的。在对FLASH进行写操作的时候,每个BIT可以通过编程由1变为0,但不可以有0修改为1。为了保证写操作的正确性,在执行写操作前,都要执行擦除操作。擦除操作会把FLASH的一个SECTOR,一个BANK或是整片FLASH的值全修改为0xFF。这样,写操作就可以正确完成了。
NOR FLASH在价格上比NAND FLASH贵,且容量很小,擦除和写数据都慢,好处在于接口简单,稳定,无位反转,坏块,常用于保存关键数据,而NAND FLASH常用于保存大容量数据。
2. 硬件分析
本人使用的是韦东山老师的JZ2440开发板,CPU是S3C2440A,上面所使用的NOR FLASH芯片为MX29LV160DB。连接原理图如下:
从原理图中我们能看到NOR FLASH有地址线,有数据线,这些地址线数据线同样还连接到了开发板上的其他芯片(如SDRAM、DM9000、NAND FLASH),当想要去读写NOR FLASH或者操作其他芯片时都在这些I/O引脚上发出信号,我们如何让他们之间不会互相干扰,正确控制自己想要控制的芯片呢?
S3C2440A有一个存储控制器,如下图:
接在地址线或数据线上的芯片可能有很多,不同的芯片都会有CE片选引脚,可以手动控制该引脚来选择操作的芯片。
S3C2440A帮我们省去了这一操作,S3C2440A通过将可寻址地址(物理地址)划分为8个内存块(每个内存块128MB,8*128MB=(2^27)B,所以S3C2440A地址线共有27根(LADDR0~26)),每个内存块都有一个片选信号(nGCS0~nGCS7),具体每一部分物理地址范围如上图。以第一个部分物理地址0x00000000-0x08000000为例,当CPU读写该范围内的物理地址时,CPU会自动将nGCS0引脚拉低,挂在该片选信号线上的芯片就会被选中,本节所要讲的NOR FLASH芯片MX29LV160DB的CE片选引脚就是连接到nGCS0上的,所以MX29LV160DB的起始物理地址为0。
2.1 地址线错位连接
为什么Nor Flash的地址线A0是接在2440的LADDR1上? 因为NOR FLASH的数据共有16位,也就是每个地址保存了2Byte数据,而我们的2440每个地址是保存的1Byte数据,比如:
1. 当2440访问0X00地址时,就会读取到Nor上0地址的2Byte数据,然后2440的内存控制器会根据0x00来找到低8位字节,并返回给CPU。
2. 当2440访问0x01地址时,由于2440的LADDR0线未接,所以还是访问Nor的0地址上的2Byte数据,然后内存控制器会根据0x01来找到高8位字节,并返回给CPU。
以读ID为例,MX29LV160DB芯片手册上读ID命令如下:
MX29LV160DB芯片是16位的,所以读写操作按照Word模式,设备ID为2249。即对于MX29LV160DB读ID操作步骤如下:
往地址555H写AAH
往地址2AAH写55H
往地址555H写90H
读0地址得到厂家ID: C2H
读1地址得到设备ID: 2249H
退出读ID状态: 给任意地址写F0H
因为我们2440的ADDR1接到NOR FLASH的A0(地址左移一位),所以对于CPU的角度来说,我们实际要操作的步骤如下:
往地址AAAH写AAH (mw.w aaa aa)
往地址554写55H (mw.w 554 55)
往地址AAAH写90H (mw.w aaa 90)
读0地址得到厂家ID: C2H (md.w 0 1)
读2地址得到设备ID: 2249H (md.w 2 1)
退出读ID状态 (mw.w 0 f0)
使用上面命令在uboot下测试:
可以看到正确读出来了厂家ID(0xc2)、设备ID(0x2249)。
2.2 nor flash硬件开关
在JZ2440中可以通过硬件开关来设置OM0为Nand启动(不使用NOR FLASH)还是Nor启动,如下图所示:
OM1在开发板上默认接地,当SW2开关接通时,OM0为1时,表示16位nor启动模式,OM0为0时,表示Nand Flash启动模式。
2.3 CFI接口
CFI(Common Flash Interface),是JEDEC(Joint Electron Device Engineering Council,电子器件工程联合委员会)制定的一个接口,用来帮助程序读取Flash的制造商ID和设备ID,确定Flash的大小,获得Flash的各个物理特性,比如block块的擦除时间等等。
在应用CFI之前,Flash器件的有关信息都储存在系统软件的表格中。当有新的器件发布时,一般必须修改软件来添加该器件的描述信息。CFI出现后,工程师们正在利用CFI来构建代码,它不仅能够运行在现在的Flash存储器上,而且随时准备着应用在下一代的低成本版本上。这使得原始设备制造商能够在低成本Flash存储器设备可用时使用它而不必重写代码。
CFI的好处:它可以使系统软件查询已安装的Flash Memory器件的各种参数,包括器件阵列结构参数、电气和时间参数以及器件支持的功能等。利用CFI可以不用修改系统软件就可以用新型的和改进的产品代替旧版本的产品。例如:如果新型的Flash Memory的擦除时间只有旧版本的一半,系统软件只要通过CFI读取新器件的擦除时间等参数,修改一下定时器的时间参数即可。
MX29LV160DB芯片手册第26页中有如下所示:
对于MX29LV160DB需要往地址55h写入98h,便可以进入CFI模式了,通过发送相应的地址就能得到存储在芯片内的信息,想要退出CFI模式可以发送重置命令(F0)即可返回进入CFI模式之前的模式。
MX29LV160DB芯片手册第26~28页列出了许多读地址数据的列表,以下两个列表为例:
MX29LV160DB芯片是16位的,所以读写操作按照Word模式。即对于MX29LV160DB操作步骤如下:
写98H到地址55H (进入CFI模式)
读地址10H
读地址11H
读地址12H
读地址27H (得到容量)
因为我们2440的ADDR1接到NOR FLASH的A0(地址左移一位),所以对于CPU的角度来说,我们实际要操作的步骤如下:
写98H到地址AAH (进入CFI模式:mw.w aa 98)
读地址20H (md.w 20 1)
读地址22H (md.w 22 1)
读地址24H (md.w 24 1)
读地址4EH (得到容量:md.w 4e 1)
使用上面命令在uboot下测试:
可以看到分别读MX29LV160DB的10H、11H、12H可以得到“0051”、“0052”、“0059”(16进制),分别对应于ASCII码“Q”、“R”、“Y”,读到的“0015”转换为10进制为21,即2^21=2MB,与芯片手册描述的完全相同,接下来看内核自带的驱动是如何操作NOR Flash的。
3. 分析内核
NOR Flash驱动也是放在内核的mtd设备中,mtd设备也知道对nor如何来读写擦除,只是不知道NOR Flash的位宽(数据线个数),基地址等,所以我们的NOR Flash驱动同样要实现硬件相关的操作,供给mtd设备调用。
内核(linux-2.6.22.6)自带的NOR Flash驱动位于drivers/mtd/maps/physmap.c,首先进入该驱动入口函数。
3.1 physmap_init()入口函数
physmap_init()入口函数代码如下:
/*
static struct platform_driver physmap_flash_driver = {
.probe = physmap_flash_probe,
.remove = physmap_flash_remove,
#ifdef CONFIG_PM
.suspend = physmap_flash_suspend,
.resume = physmap_flash_resume,
.shutdown = physmap_flash_shutdown,
#endif
.driver = {
.name = "physmap-flash",
},
};
*/
static int __init physmap_init(void)
{
int err;
err = platform_driver_register(&physmap_flash_driver);
#ifdef PHYSMAP_COMPAT
if (err == 0)
platform_device_register(&physmap_flash);
#endif
return err;
}
使用platform设备驱动模型(参考十一、Linux驱动之platform总线设备驱动)构造并注册platform_driver结构体,并且注册了platform_device结构体,定位到传入的platform_device结构体physmap_flash相关内容如下:
/*CONFIG_MTD_PHYSMAP_BANKWIDTH 表示位宽*/
static struct physmap_flash_data physmap_flash_data = {
.width = CONFIG_MTD_PHYSMAP_BANKWIDTH,
};
/*
*CONFIG_MTD_PHYSMAP_START 表示物理基地址
*CONFIG_MTD_PHYSMAP_LEN 表示长度
*/
static struct resource physmap_flash_resource = {
.start = CONFIG_MTD_PHYSMAP_START,
.end = CONFIG_MTD_PHYSMAP_START + CONFIG_MTD_PHYSMAP_LEN - 1,
.flags = IORESOURCE_MEM,
};
static struct platform_device physmap_flash = {
.name = "physmap-flash",
.id = 0,
.dev = {
.platform_data = &physmap_flash_data,
},
.num_resources = 1,
.resource = &physmap_flash_resource,
};
可以看出,在physmap_flash结构里传入了设备资源,例如NOR Flash芯片的物理基地址,位宽,长度。可以通过通过对内核menuconfig菜单配置,也可以直接赋值。配置内核体验一下,ubuntu进入内核目录,执行如下:
cd /work/system/linux-2.6.22.6
make munuconfig (配置内核步骤如下)
Device Drivers --->
Memory Technology Device (MTD) support --->
Mapping drivers for chip access --->
<M> CFI Flash device in physical memory map
(0x0) Physical start address of flash mapping (物理基地址)
(0x1000000) Physical length of flash mapping (长度,要>=norflash真实长度)
(2) Bank width in octets (NEW) (位宽,2字节)
make uImage
make modules
cp arch/arm/boot/uImage /work/nfs_root/first_fs (拷贝新内核到网络文件系统上)
cp drivers/mtd/maps/physmap.ko /work/nfs_root/first_fs (拷贝该驱动到网络文件系统上)
uboot命令行状态下载新内核启动:
nfs 0x30000000 192.168.1.13:/work/nfs_root/first_fs/uImage
bootm 30000000
装载驱动与查看norflash信息,进入该驱动文件目录,执行如下命令:
insmod physmap.ko (可以看到设置的参数被打印出来了)
ls dev/mtd*
可以看到创建了2个mtd0字符设备,一个mtd0块设备。
回到入口函数,当设备与驱动匹配成功便会调用platform_driver->probe成员函数,即physmap_flash_probe()函数。
3.2 physmap_flash_probe()函数
physmap_flash_probe()函数部分代码如下:
/*
static const char *rom_probe_types[] = { "cfi_probe", "jedec_probe", "map_rom", NULL }; //芯片名称
*/
static int physmap_flash_probe(struct platform_device *dev)
{
const char **probe_type;
... ...
/*分配结构体*/
info = kzalloc(sizeof(struct physmap_flash_info), GFP_KERNEL);
/*设置map_info 结构体*/
info->map.name = dev->dev.bus_id; //norflash的名字
info->map.phys = dev->resource->start; //物理基地址
info->map.size = dev->resource->end - dev->resource->start + 1; //容量长度
info->map.bankwidth = physmap_data->width; //字节位宽
info->map.virt = ioremap(info->map.phys, info->map.size); //虚拟地址
simple_map_init(&info->map); //简单初始化map_info的其它成员
probe_type = rom_probe_types;
/*设置mtd_info 结构体 */
/*通过probe_type指向的名称来识别芯片,当do_map_probe()函数返回NULL表示没找到*/
/*当找到对应的芯片mtd_info结构体,便返回给当前的info->mtd */
for (; info->mtd == NULL && *probe_type != NULL; probe_type++)
info->mtd = do_map_probe(*probe_type, &info->map); //通过do_map_probe ()来识别芯片
if (info->mtd == NULL) { //最终还是没找到芯片,便注销之前注册的东西并退出
dev_err(&dev->dev, "map_probe failed\n");
err = -ENXIO;
goto err_out;
}
info->mtd->owner = THIS_MODULE;
/*添加mtd设备*/
add_mtd_device(info->mtd);
return 0;
err_out:
physmap_flash_remove(dev); //该函数用来注销之前注册的东西
return err;
}
3.2.1 probe函数主要结构
可以看出类似上一节十七、Linux驱动之nand flash驱动,该驱动主要有以下几点:
1. 分配mtd_info结构体和map_info结构体
2. 设置map_info结构体
3. 设置mtd_info结构体
4. 使用add_mtd_partitions()或者add_mtd_device()来创建MTD字符/块设备
其中通过do_map_probe()函数来识别NOR Flash获取硬件信息(CFI模式与jedec模式)。
3.2.2 do_map_probe()函数
该函数通过使用CFI模式或jedec模式来识别NOR Flash(读取Flash的制造商ID和设备ID,确定Flash的大小,获得Flash的各个物理特性,比如block块的擦除时间等等)。
CFI模式识别过程如下:
do_map_probe("cfi_probe", s3c_nor_map);
drv = get_mtd_chip_driver(name) //得到对应名字的驱动函数
ret = drv->probe(map); // 调用到cfi_probe.c文件里的cfi_probe函数
cfi_probe
mtd_do_chip_probe
cfi = genprobe_ident_chips
genprobe_new_chip
cp->probe_chip
cfi_probe_chip
cfi_send_gen_cmd(0x98, 0x55, base, map, cfi, cfi->device_type, NULL); //进入CFI模式
qry_present(map,base,cfi) // 看是否能读出"QRY"
... ...
使用CFI模式读取Flash芯片上所有信息。
jedec模式识别过程如下:
do_map_probe("jedec_probe", s3c_nor_map);
drv = get_mtd_chip_driver(name) //得到对应名字的驱动函数
ret = drv->probe(map); // 调用到jedec_probe .c文件里的cfi_probe函数
jedec_probe
mtd_do_chip_probe(map, &jedec_chip_probe);
genprobe_ident_chips(map, cp);
genprobe_new_chip(map, cp, &cfi)
cp->probe_chip(map, 0, NULL, cfi)
jedec_probe_chip
/*进入读ID*/
cfi_send_gen_cmd(0xaa, ...)
cfi_send_gen_cmd(0x55, ...)
cfi_send_gen_cmd(0x90, ...)
/*得到厂家ID,设备ID*/
cfi->mfr = jedec_read_mfr(map, base, cfi);
cfi->id = jedec_read_id(map, base, cfi);
jedec_table //通过ID来匹配jedec_table[]数组
两者区别:CFI模式是读取Flash芯片上所有信息。jedec模式是读取Flash芯片上ID信息,如果与内核里保存各种NOR Flash信息的数组某一项的ID匹配,使用该数组里的信息。
4. 编写代码
驱动程序s3c_nor.c代码如下:
/*
* 参考 drivers\mtd\maps\physmap.c
*/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/map.h>
#include <linux/mtd/partitions.h>
#include <asm/io.h>
static struct map_info *s3c_nor_map;
static struct mtd_info *s3c_nor_mtd;
static struct mtd_partition s3c_nor_parts[] = {
[0] = {
.name = "bootloader_nor",
.size = 0x00040000,
.offset = 0,
},
[1] = {
.name = "root_nor",
.offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL,
}
};
static int s3c_nor_init(void)
{
/* 1. 分配map_info结构体 */
s3c_nor_map = kzalloc(sizeof(struct map_info), GFP_KERNEL);
/* 2. 设置: 物理基地址(phys), 大小(size), 位宽(bankwidth), 虚拟基地址(virt) */
s3c_nor_map->name = "s3c_nor";
s3c_nor_map->phys = 0;
s3c_nor_map->size = 0x1000000; /* >= NOR的真正大小 */
s3c_nor_map->bankwidth = 2;
s3c_nor_map->virt = ioremap(s3c_nor_map->phys, s3c_nor_map->size);
simple_map_init(s3c_nor_map);
/* 3. 使用: 调用NOR FLASH协议层提供的函数来识别 */
printk("use cfi_probe\n");
s3c_nor_mtd = do_map_probe("cfi_probe", s3c_nor_map); //cfi模式识别芯片
if (!s3c_nor_mtd)
{
printk("use jedec_probe\n");
s3c_nor_mtd = do_map_probe("jedec_probe", s3c_nor_map); //jedec模式识别芯片
}
if (!s3c_nor_mtd)
{
iounmap(s3c_nor_map->virt);
kfree(s3c_nor_map);
return -EIO;
}
/*4. 使用add_mtd_partitions()或者add_mtd_device()来创建MTD字符/块设备*/
add_mtd_partitions(s3c_nor_mtd, s3c_nor_parts, 2);
return 0;
}
static void s3c_nor_exit(void)
{
del_mtd_partitions(s3c_nor_mtd);
iounmap(s3c_nor_map->virt);
kfree(s3c_nor_map);
}
module_init(s3c_nor_init);
module_exit(s3c_nor_exit);
MODULE_LICENSE("GPL");
Makefile代码如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += s3c_nor.o
5. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
5.1 配置内核并开发板启动
使用3.1方式配置内核并下载到开发板上启动(一定要在nor启动下挂载才行,因为2440使用nand启动时,是访问不了nor的前4k地址)。
5.2 测试
1. 安装驱动。在开发板驱动文件目录下执行命令:
insmod s3c_nor.ko
ls /dev/mtd* -l (新出现的设备就是norflash设备)
2. 格式化。执行如下命令:
flash_eraseall -j dev/mtd1 (格式化:加上-j格式化为jffs2,用字符设备格式)
3. 挂载
mount -t jffs2 dev/mtdblock1 /mnt/ (挂接:用块设备格式将分区2挂接到/mnt下,卸载:umount /mnt,不能在/mnt目录下卸载)
这样就能直接在/mnt目录下读写文件,实际是操作到NOR Flash的mtdblock1块设备中!