1 简介
W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为8M。该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块(block),每个块大小为64K字节,每个块又分为16个扇区(sector),每个扇区4K字节。W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。所以,这需要给W25Q64开辟一个至少4K的缓存区,这样必须要求芯片有4K以上的SRAM才能有很好的操作。W25Q64的擦写周期多达10W次,可将数据保存达20年之久,支持2.7~3.6V的电压,支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可达80MHz。
本文使用的硬件平台: Atmel SAMA5d4,在其平台上添加 W25Q64 芯片(SPI 设备),Linux 内核版本为 kernel-4.9.87,采用 DeviceTree 描述硬件连接信息。
2 硬件连接
Atmel SAMA5d4是基于ARM Cortex-A5架构的高性能处理器,它上边有3组SPI接口(SPI0、SPI1、SPI2)。具体可以查看处理器芯片手册。
处理器中的SPI引脚定义
W25Q64与处理器的引脚定义及连接关系:
序号 | W25Q64芯片引脚定义 | 处理器引脚定义 |
---|---|---|
1 | MOSI | PC1 |
2 | MISO | PC0 |
3 | SCLK | PC2 |
4 | CS | PD31 |
3 驱动调试分析
3.1 理论基础
先来看一些 flash的理论知识。
MTD层为NOR FLASH和NAND FLASH设备提供统一接口。MTD将文件系统与底层FLASH存储器进行了隔离。如图2.1所示,MTD设备通常可分为四层,从上到下依次是:设备节点、MTD设备层、MTD原始设备层、硬件驱动层。
Flash硬件驱动层:(相当于spi driver/i2c driver),Flash硬件驱动层负责对Flash硬件的读、写和擦除操作。MTD设备的Nand Flash芯片的驱动则在drivers/mtd/nand/子目录下,Nor Flash芯片驱动位于drivers/mtd/chips/子目录下。
MTD原始设备层:(相当于spi master/i2c client),用于描述MTD原始设备的数据结构是mtd_info,它定义了大量的关于MTD的数据和操作函数。其中mtdcore.c: MTD原始设备接口相关实现,mtdpart.c : MTD分区接口相关实现。
MTD设备层:基于MTD原始设备,linux系统可以定义出MTD的块设备(主设备号31)和字符设备(设备号90)。其中mtdchar.c : MTD字符设备接口相关实现,mtdblock.c : MTD块设备接口相关实现。
设备节点:通过mknod在/dev子目录下建立MTD块设备节点(主设备号为31)和MTD字符设备节点(主设备号为90)。通过访问此设备节点即可访问MTD字符设备和块设备。
3.2 设备树节点
根据实际的硬件连接情况修改设备树文件。
查看设备树文件(.dts 和 .dtsi)中spi0节点的属性;
.dtsi文件中:
引脚定义:
.dts文件中:
未修改前:
从设备树spi0节点的子节点m25p80可知,内核自带的驱动程序中支持了 SPI FLASH芯片m25p80类型的驱动,原compatible属性对应的是型号为“at25df321a”的SPI串行闪存,进入源码文件m25p80.c(路径:\linux-at91\drivers\mtd\devices\m25p80.c),可以搜索到“at25df321a”在 m25p_ids结构体中,同时该结构体也包含了我们使用的SPI芯片w25q64,说明该驱动也可直接用于w25q64。
重新配置设备树节点m25p80,我们这里使用的片选引脚为PD31,需要修改cs-gpios属性对应的值。另外还需修改m25p80节点中的compatible属性,修改为w25q64即可使能该flash。后续重新编译设备树文件生成.dtb,随内核一并烧录,就会成功加载w25q64驱动。
修改后:
3.3 spi flash驱动流程分析
这里要把SPI flash设备注册为MTD设备,MTD子系统实现了SPI flash芯片驱动程序,其驱动 Demo 为:
- drivers/mtd/devices/mtd_dataflash.c
(*Atmel AT45xxx DataFlash MTD driver for lightweight SPI framework) - drivers/mtd/devices/m25p80.c
(MTD SPI driver for ST M25Pxx (and similar) serial flash chips)
我们这里使用的是与m25p80相似的flash,所以套用源码文件m25p80.c,并没有对该文件进行修改。通读 m25p80.c 驱动代码,我们可以找出大概的脉络。首先是通过 module_spi_driver 函数注册 m25p80_driver 驱动,其中实现了 probe 和 remove 函数,分别是 m25p_probe 和 m25p_remove。并且填写了一张名为 m25p_ids 的兼容设备表,设备表中包含了"w25q64"。
下面对驱动的调用过程进行详细分析。
1) spi flash硬件驱动层部分
SPI 协议驱动有些类似平台设备驱动:
内核将调用 module_spi_driver() 这个宏来注册和卸载 spi 设备,这个 module_spi_driver 是专门针对于 spi 架构定义的。
module_spi_driver
继续追看 module_driver() 这个宏 ,它将 spi_register/unregister_driver() 与 module_init 和 module_exit 封装了起来。所以说实际上 module_spi_driver() 和 module_init/exit 几乎是没有区别的。之所以直接将其封装的原因是因为这个 SPI 设备本身是不可插拔的,也就不需要 init 和 exit 的过程,系统上电就直接注册了。
module_driver -> module_init
spi_register_driver
__spi_register_driver
driver_register * driver_register - register driver with bus
直接看 bus_add_driver
这里只截取一部分,最后调用的是driver_attach
在bus_for_each_dev执行了__driver_attach(dev, data),查看 __driver_attach
在static int__driver_attach(struct device *dev, void *data)中先调用了driver_match_device(drv,dev),用于匹配,成功才继续执行,否则直接返回了。driver_match_device(drv, dev)中:
如果match函数的指针不为空,则执行此bus的match函数,这也就是为什么老是说总线负责匹配设备和驱动了。这里也传递了参数struct device *dev。
匹配成功继续执行,driver_attach(dev, data)中有个driver_probe_device(drv,dev),继续跟踪:
有个really_probe(dev,drv),linux经常将一个函数传递给另一函数,后一个函数就是在前一个函数前加“do”、“really”、“__”,还经常宏定义或inline。
(截取部分代码)
这里如果有总线上的probe函数就调用总线的probe函数,如果没有则调用drv的probe函数。
m25p_probe
/*
* board specific setup should have ensured the SPI clock used here
* matches what the READ command supports, at least until this driver
* understands FAST_READ (for clocks over 25 MHz).
*/
static int m25p_probe(struct spi_device *spi)
{
struct flash_platform_data *data;
struct m25p *flash;
struct spi_nor *nor;
struct spi_nor_hwcaps hwcaps = {
.mask = (SNOR_HWCAPS_READ |
SNOR_HWCAPS_READ_FAST |
SNOR_HWCAPS_PP),
};
char *flash_name;
int ret;
data = dev_get_platdata(&spi->dev);
flash = devm_kzalloc(&spi->dev, sizeof(*flash), GFP_KERNEL);
if (!flash)
return -ENOMEM;
nor = &flash->spi_nor;
/* install the hooks */
nor->read = m25p80_read;
nor->write = m25p80_write;
nor->write_reg = m25p80_write_reg;
nor->read_reg = m25p80_read_reg;
nor->dev = &spi->dev;
spi_nor_set_flash_node(nor, spi->dev.of_node);
nor->priv = flash;
spi_set_drvdata(spi, flash);
flash->spi = spi;
if (spi->mode & SPI_RX_QUAD) {
hwcaps.mask |= SNOR_HWCAPS_READ_1_1_4;
if (spi->mode & SPI_TX_QUAD)
hwcaps.mask |= (SNOR_HWCAPS_READ_1_4_4 |
SNOR_HWCAPS_PP_1_1_4 |
SNOR_HWCAPS_PP_1_4_4);
} else if (spi->mode & SPI_RX_DUAL) {
hwcaps.mask |= SNOR_HWCAPS_READ_1_1_2;
if (spi->mode & SPI_TX_DUAL)
hwcaps.mask |= SNOR_HWCAPS_READ_1_2_2;
}
if (data && data->name)
nor->mtd.name = data->name;
/* For some (historical?) reason many platforms provide two different
* names in flash_platform_data: "name" and "type". Quite often name is
* set to "m25p80" and then "type" provides a real chip name.
* If that's the case, respect "type" and ignore a "name".
*/
if (data && data->type)
flash_name = data->type;
else if (!strcmp(spi->modalias, "spi-nor"))
flash_name = NULL; /* auto-detect */
else
flash_name = spi->modalias;
ret = spi_nor_scan(nor, flash_name, &hwcaps);
if (ret)
return ret;
return mtd_device_register(&nor->mtd, data ? data->parts : NULL,
data ? data->nr_parts : 0);
}
在 m25p_probe 函数中指定了 m25p80_read、m25p80_write 和m25p80_erase 等文件操作函数,当应用程序使用 read、write、ioctl 等接口操作时最终会调用到这里。那 open 和 close 函数呢? 我们把 W25Q64 注册成 MTD 设备了,所以另外一些操作函数在 drivers/mtd/mtdchar.c 中定义。实际上,它不仅有 mtdchar_open、mtdchar_close 等函数,还有 mtdchar_read 和 mtdchar_write 函数,而它们会调用 m25p80.c 中的 m25p80_read 和 m25p80_write 函数。
…
…
4 编译及开机验证
(1)重新编译 image 和 dtb,更新系统后重新启动,进入系统。
输入命令 dmesg | grep spi,看到如下内容则说明内核已经探测到 w25q64 设备,把设备和驱动程序匹配上了。
(2)查看设备文件:
可以看到/dev/mtd1 之类的设备节点,其中 /dev/mtd1 是字符设备,/dev/mtdblock1 是块设备,/dev/mtd1ro 是只读字符设备。
- cat /proc/mtd
- cat /proc/partitions
- mtd_debug info /dev/mtd7
- mtdinfo /dev/mtd7
从上面的命令返回结果可以看到,mtd7对应的设备即为Nor FLASH w25q64。
(3)挂载 MTD 设备
因为我们把 SPI Flash 注册成 MTD 设备了,因此,我们可以通过 MTD 子系统和文件系统对其进行操作。
首先对 Flash 进行格式化,然后挂载,接着就可以通过文件系统操作:
$ mkfs.vfat /dev/mtdblock7
$ mount -t vfat /dev/mtdblock7 /home/root/w25q64
$ cd /home/root/w25q64
$ echo “Hello W25Q64” > file.txt
$ sync
然后断电重启,看看文件及其内容是否还在,并且与断电前一致。
mount命令:
(4)“读写擦”测试
除了通过文件系统操作 W25Q64 设备外,也可以直接打开 /dev/mtd1 设备节点对其进行操作。
1)通过 mtd_debug 工具操作设备节点。
2)通过C语言编写测试程序进行 “读写擦”操作。
可参考该测试程序进行修改。
https://github.com/luhuadong/Linux-programming/blob/master/driver/mtd/test/mtd_go.c