写点什么

Linux 驱动开发 - 编写 W25Q64(Flash) 驱动

作者:DS小龙哥
  • 2022 年 4 月 14 日
  • 本文字数:8978 字

    阅读完需:约 29 分钟

1. W25QXX 介绍

W25Q64 是一颗 SPI 接口的 Flash 存储芯片,是华邦 W25QXX 系列里的一个具体型号,这个系列里包含了 W25Q16,W25Q32,W25Q64,W5Q128 等等。编程代码逻辑都差不多,主要是容量的区别。


本篇文章就介绍如何在 Linux 系统下编写 W25Q64 芯片的驱动,完成数据存储,W25Q64 支持标准 SPI 总线,当前驱动程序底层的代码写了两种方式,一种是采用内核提供的 SPI 子系统框架,一种直接采用软件模拟 SPI 时序的方式驱动,具体代码在第 3 章贴出来了。


下面是来至 W25Qxx 中文手册的介绍


W25Q64 (64M-bit), W25Q16(16M-bit)和 W25Q32(32M-bit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行 Flash 存储器。 25Q 系列比普通的串行 Flash 存储器更灵活,性能更优越。基于双倍/四倍的 SPI,它们能够可以立即完成提供数据给 RAM, 包括存储声音、文本和数据。芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于 5mA,掉电时低于 1uA。所有芯片提供标准的封装。

W25Q64/16/32 由每页 256 字节组成。 每页的 256 字节用一次页编程指令即可完成。 每次可以擦除 16 页(1 个扇区)、 128 页(32KB 块)、 256 页(64KB 块)和全片擦除。W25Q64 的内存空间结构: 一页 256 字节, 4K(4096 字节)为一个扇区, 16 个扇区为 1 块, 容量为 8M 字节,共有 128 个块,2048 个扇区。W25Q64/16/32 支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、 I/O1(DO)、 I/O2(WP)和 I/O3(HOLD)。 SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率 160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上 8 位和 16 位的并行 Flash 存储器。HOLD 引脚和写保护引脚可编程写保护。此外,芯片支持 JEDEC 标准,具有唯一的 64 位识别序列号。

●SPI 串行存储器系列

-W25Q64:64M 位/8M 字节

-W25Q16:16M 位/2M 字节-W25Q32:32M 位/4M 字节-每 256 字节可编程页

2. 硬件环境

当前测试使用的开发板采用友善之臂的 Tiny4412 开发板,芯片是三星的 EXYNOS-4412,最高主频 1.5GHZ。


开发板引出了 SPI 的 IO 口,这里使用的 W25Q64 是外置的模块,使用杜邦线与开发板的 IO 口连接。



开发板上引出的 IO 口都是 5V 和 1.8V,为了方便供电,采用了一个 USB 转 TTL 模块提供电源,测试驱动。



W25Q64 模块接在开发板的 SPI0 接口上面的。



Linux 内核自带有 SPI 子系统的设备端示例代码:


Linux 内核自带的 SPI 驱动注册示例代码: \drivers\spi\spidev.cLinux 内核自带的 SPI APP 注册示例代码: \Documentation\spi
复制代码


如果要使用内核自带 SPI 驱动,可以在内核编译时配置一下。


root# make menuconfigDevice Drivers  --->  [*] SPI support  --->  <*>   Samsung S3C64XX series type SPI                                              [*]     Samsung S3C64XX Channel 0 Support.
复制代码


Tiny4412 自带内核里的 SPI 设备端结构:




SPI0 的具体 GPIO 口位置:


3. 案例代码

3.1 模拟 SPI 时序-编写驱动

下面是 W25Q64 的驱动测试代码,没有注册字符设备框架,只是在驱动的入口里测试时序是否 OK,打印了 ID,读写了数据进行测试。


#include <linux/init.h>#include <linux/module.h>#include <linux/ioctl.h>#include <linux/fs.h>#include <linux/device.h>#include <linux/err.h>#include <linux/list.h>#include <linux/errno.h>#include <linux/mutex.h>#include <linux/slab.h>#include <linux/compat.h>#include <linux/spi/spi.h>#include <linux/spi/spidev.h>#include <asm/uaccess.h>#include <asm/io.h>#include <linux/delay.h>
/*--------------------------------W25Q64相关操作代码---------------------------------------------*/

/*定义指针,用于接收虚拟地址*/volatile unsigned int *W25Q64_GPBCON;volatile unsigned int *W25Q64_GPBDAT;

/*函数功能:W25Q64初始化Tiny4412硬件连接: DO--MISO :GPB_2 //输入模式 DI--MOSI :GPB_3 //输出模式 CLK-SCLK :GPB_0 //时钟 CS--CS :GPB_1 //片选*/void W25Q64_Init(void){ /*1. 初始化GPIO*/ /*映射物理地址*/ W25Q64_GPBCON=ioremap(0x11400040,4); W25Q64_GPBDAT=ioremap(0x11400044,4); *W25Q64_GPBCON &= ~(0xf << 0 * 4);*W25Q64_GPBCON |= (0x1 << 0 * 4); *W25Q64_GPBCON &= ~(0xf << 1 * 4);*W25Q64_GPBCON |= (0x1 << 1 * 4); *W25Q64_GPBCON &= ~(0xf << 2 * 4); *W25Q64_GPBCON &= ~(0xf << 3 * 4);*W25Q64_GPBCON |= (0x1 << 3 * 4); /*2. 上拉GPIO口*/ //*W25Q64_GPBDAT &= ~(1 << 4);//输出0 *W25Q64_GPBDAT |= (1 << 0); //输出1 *W25Q64_GPBDAT |= (1 << 1); //输出1 *W25Q64_GPBDAT |= (1 << 3); //输出1}

/*函数功能:SPI时序读写一个字节说 明:SPI底层时序,程序的移植接口*/u8 W25Q64_SPI_ReadWriteOneByte(u8 data_tx){ u8 data_rx=0; u8 i; for(i=0;i<8;i++) { *W25Q64_GPBDAT &= ~(1 << 0);//输出0 if(data_tx&0x80)*W25Q64_GPBDAT |= (1 << 3); //输出1 else *W25Q64_GPBDAT &= ~(1 << 3);//输出0 data_tx<<=1; //继续发送下一个数据
*W25Q64_GPBDAT |= (1 << 0); //输出1 data_rx<<=1; if((*W25Q64_GPBDAT & (1 << 2)))data_rx|=0x01; } return data_rx;}
/*函数功能:写使能*/void W25Q64_WriteEnabled(void){ *W25Q64_GPBDAT &= ~(1 << 1); //选中W25Q64 W25Q64_SPI_ReadWriteOneByte(0x06); *W25Q64_GPBDAT |= (1 << 1); //取消选中W25Q64}

/*函数功能:读状态*/void W25Q64_GetBusyStat(void){ unsigned char stat=1; while(stat&0x01) //判断状态最低位 { *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x05); stat=W25Q64_SPI_ReadWriteOneByte(0xFF); //读取状态寄存器的值 *W25Q64_GPBDAT |= (1 << 1); }}

/*函数功能:读取设备ID和制造商IDW25Q64: EF16W25QQ128:EF17*/unsigned short W25Q64_ReadDeviceID(void){ unsigned short ID; *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x90); W25Q64_SPI_ReadWriteOneByte(0x0); W25Q64_SPI_ReadWriteOneByte(0x0); W25Q64_SPI_ReadWriteOneByte(0x0);
ID=W25Q64_SPI_ReadWriteOneByte(0xFF)<<8; //制造商ID ID|=W25Q64_SPI_ReadWriteOneByte(0xFF); //设备ID *W25Q64_GPBDAT |= (1 << 1); return ID;}

/*函数功能:全片擦除*/void W25Q64_ClearAll(void){ W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0xC7); *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器}

/*函数功能:页编程参 数: unsigned int addr:写入的地址 void *p:将要写入的数据 unsigned int len:写入的长度说 明:每次最多只能写入256字节*/void W25Q64_PageWrite(unsigned int addr,void*p,unsigned int len){ unsigned short i; unsigned char *buff=p; W25Q64_WriteEnabled(); //写使能 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x02); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); for(i=0;i<len;i++) { W25Q64_SPI_ReadWriteOneByte(buff[i]); } *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器}

/*函数功能:扇区擦除参 数: unsigned int addr:扇区的地址说 明:一个扇区是4096字节,擦除一个扇区时间至少150ms*/void W25Q64_ClearSector(unsigned int addr){ W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x20); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); *W25Q64_GPBDAT |= (1 << 1); W25Q64_GetBusyStat(); //检测状态寄存器}

/*函数功能:数据读取参 数:*/void W25Q64_ReadData(unsigned int addr,void *p,unsigned int len){ unsigned int i=0; unsigned char *buff=p; *W25Q64_GPBDAT &= ~(1 << 1); W25Q64_SPI_ReadWriteOneByte(0x03); W25Q64_SPI_ReadWriteOneByte(addr>>16); W25Q64_SPI_ReadWriteOneByte(addr>>8); W25Q64_SPI_ReadWriteOneByte((unsigned char)addr); for(i=0;i<len;i++) { buff[i]=W25Q64_SPI_ReadWriteOneByte(0xFF); } *W25Q64_GPBDAT |= (1 << 1);}

/*函数功能:在任意地址写入任意数据,不进行校验参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度*/void W25Q64_WriteDataONCheck(unsigned int addr,void *p,unsigned int len){ unsigned char *buff=p; unsigned short page_remain=256-addr%256; //当前地址开始一页剩下的空间 unsigned short remain_len; //剩余未写入的长度 if(len<page_remain) //当前这一页剩下的空间足够可以写入 { page_remain=len; }
while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; addr+=page_remain; //地址向后移动 buff+=page_remain; //地址向后移动 len-=page_remain; //长度递减
if(len>256)page_remain=256; else page_remain=len; } }

/*函数功能:在任意地址写入任意数据,对扇区进行校验参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度说明:一个扇区的空间4096字节*/unsigned char W25Q64_BUFF[1024*4]; //用来检验一个扇区的数据是否需要擦除
void W25Q64_WriteData(unsigned int addr,void *p,unsigned int len){ unsigned int sector_len=4096-addr%4096; //剩余空间大小 unsigned char *buff=p; unsigned int i=0; if(len<sector_len) //剩下的空间足够写 { sector_len=len; } while(1) { W25Q64_ReadData(addr,W25Q64_BUFF,sector_len); for(i=0;i<sector_len;i++) { if(W25Q64_BUFF[i]!=0xFF) { W25Q64_ClearSector(addr); //擦除扇区 break; } } W25Q64_WriteDataONCheck(addr,buff,sector_len); if(sector_len==len)break; //数据写完
buff+=sector_len; addr+=sector_len; len-=sector_len;
if(len>4096) { sector_len=4096; } else { sector_len=len; } }}

static int __init w25q64_init(void){ /*1. 初始化GPIO口*/ W25Q64_Init(); /*2. 打印厂商芯片ID*/ unsigned short id=W25Q64_ReadDeviceID(); printk("id=0x%X\n",id); /*3. 写入数据*/ char buff[]="W25Q64-test-123456789ABCDEFG"; W25Q64_WriteData(100,buff,strlen(buff)); printk("write-data:%s\n",buff); /*4. 读出数据*/ char buff_rx[100]; W25Q64_ReadData(100,buff_rx,strlen(buff)); printk("read-data:%s\n",buff_rx); return 0;}

static void __exit w25q64_exit(void){ /*释放虚拟地址*/ iounmap(W25Q64_GPBCON); iounmap(W25Q64_GPBDAT); printk("w25q64 driver exit ok!\n");}
module_exit(w25q64_exit);module_init(w25q64_init);MODULE_LICENSE("GPL");
复制代码

3.2 采用 SPI 子系统框架-编写驱动

下面代码使用 SPI 子系统框架编写的驱动测试代码,注册了字符设备框架,但是只是做了简单的测试,目的只是测试 W25Q64 是否可以正常驱动,能读写存储。


#include <linux/init.h>#include <linux/module.h>#include <linux/ioctl.h>#include <linux/fs.h>#include <linux/device.h>#include <linux/err.h>#include <linux/list.h>#include <linux/errno.h>#include <linux/mutex.h>#include <linux/slab.h>#include <linux/compat.h>#include <linux/spi/spi.h>#include <linux/spi/spidev.h>#include <asm/uaccess.h>#include <linux/gpio.h>#include <mach/gpio.h>#include <plat/gpio-cfg.h>#include <linux/delay.h>#include <linux/io.h>#include <linux/miscdevice.h>   /*杂项字符设备头文件*/#include <linux/fs.h>           /*文件操作集合*/#include <linux/slab.h>
/*--------------------------------W25Q64相关操作代码---------------------------------------------*/struct spi_device *w25q64_spi_Device;

/*函数功能:W25Q64初始化Tiny4412硬件连接: DO--MISO :GPB_2 //输入模式 DI--MOSI :GPB_3 //输出模式 CLK-SCLK :GPB_0 //时钟 CS--CS :GPB_1 //片选*/

/*函数功能:读取设备ID和制造商IDW25Q64: EF16W25QQ128:EF17
参数:0x90表示读取ID号的指令*/unsigned short W25Q64_ReadDeviceID(void){ /*使用硬件SPI同步读写时序*/ char tx_buf[6]={0x90,0x0,0x0,0x0,0xFF,0xFF}; char rx_buf[6];
struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=6, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); return rx_buf[4]<<8|rx_buf[5]; /*得到ID值*/}

/*函数功能:指定位置读取指定长度的数据参 数:0x03 表示读取数据的指令。*/void W25Q64_ReadData(unsigned int addr,void *p,unsigned int len){ /*使用硬件SPI同步读写时序*/ char tx_buf[4]; tx_buf[0]=0x03; //读指令 tx_buf[1]=addr>>16; //以下是地址指令 tx_buf[2]=addr>>8; tx_buf[3]=addr; spi_write(w25q64_spi_Device,tx_buf,4); spi_read(w25q64_spi_Device,p,len);}

/*函数功能:写使能*/void W25Q64_WriteEnabled(void){ /*使用硬件SPI同步读写时序*/ char tx_buf[1]={0x06}; struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .len=1, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m);}

/*函数功能:读状态*/void W25Q64_GetBusyStat(void){ unsigned char stat=1; /*使用硬件SPI同步读写时序*/ char tx_buf[2]={0x05,0xFF}; char rx_buf[2]; while(stat&0x01) //判断状态最低位 { struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=2, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); stat=rx_buf[1]; //得到状态寄存器 }}

/*函数功能:扇区擦除参 数: unsigned int addr:扇区的地址说 明:一个扇区是4096字节,擦除一个扇区时间至少150ms*/void W25Q64_ClearSector(unsigned int addr){ W25Q64_WriteEnabled(); //写使能 W25Q64_GetBusyStat(); //检测状态寄存器 /*使用硬件SPI同步读写时序*/ unsigned char tx_buf[4]; tx_buf[0]=0x20; tx_buf[1]=addr>>16; tx_buf[2]=addr>>8; tx_buf[3]=addr; char rx_buf[4]; struct spi_message m; struct spi_transfer t= { .tx_buf=tx_buf, .rx_buf=rx_buf, .len=4, .delay_usecs=0, .speed_hz=1000000, .bits_per_word=8 }; spi_message_init(&m); spi_message_add_tail(&t,&m); spi_sync(w25q64_spi_Device,&m); W25Q64_GetBusyStat(); //检测状态寄存器}
/*函数功能:页编程参 数: unsigned int addr:写入的地址 void *p:将要写入的数据 unsigned int len:写入的长度说 明:每次最多只能写入256字节*/void W25Q64_PageWrite(unsigned int addr,void*p,unsigned int len){ unsigned short i; unsigned char *buff=p; W25Q64_WriteEnabled(); //写使能 /*使用硬件SPI同步读写时序*/ unsigned char tx_buf[4]; tx_buf[0]=0x02; //页写指令 tx_buf[1]=(addr>>16)&0xFF; //以下是地址指令 tx_buf[2]=(addr>>8)&0xFF; tx_buf[3]=(addr&0xFF); //写数据 spi_write(w25q64_spi_Device,tx_buf,4); //写数据 spi_write(w25q64_spi_Device,p,len); W25Q64_GetBusyStat(); //检测状态寄存器}

/*函数功能:在任意地址写入任意数据,不进行校验参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度*/void W25Q64_WriteDataONCheck(unsigned int addr,void *p,unsigned int len){ unsigned char *buff=p; unsigned short page_remain=256-addr%256; //当前地址开始一页剩下的空间 unsigned short remain_len; //剩余未写入的长度 if(len<page_remain) //当前这一页剩下的空间足够可以写入 { page_remain=len; }
while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; addr+=page_remain; //地址向后移动 buff+=page_remain; //地址向后移动 len-=page_remain; //长度递减
if(len>256)page_remain=256; else page_remain=len; } }
/*函数功能:在任意地址写入任意数据,对扇区进行校验参 数: unsigned int addr:写入数据的地址 void *p :写入的数据 unsigned int len :写入数据的长度说明:一个扇区的空间4096字节*/static unsigned char W25Q64_BUFF[1024*4]; //用来检验一个扇区的数据是否需要擦除void W25Q64_WriteData(unsigned int addr,void *p,unsigned int len){ unsigned int sector_len=4096-addr%4096; //剩余空间大小 unsigned char *buff=p; unsigned int i=0; if(len<sector_len) //剩下的空间足够写 { sector_len=len; } while(1) { W25Q64_ReadData(addr,W25Q64_BUFF,sector_len); for(i=0;i<sector_len;i++) { if(W25Q64_BUFF[i]!=0xFF) { W25Q64_ClearSector(addr); //擦除扇区 break; } } W25Q64_WriteDataONCheck(addr,buff,sector_len); if(sector_len==len)break; //数据写完
buff+=sector_len; addr+=sector_len; len-=sector_len;
if(len>4096) { sector_len=4096; } else { sector_len=len; } }}

/*杂项字符设备注册示例----->LED*/static int tiny4412_open(struct inode *my_inode, struct file *my_file){ return 0;}
static int tiny4412_release(struct inode *my_inode, struct file *my_file){ return 0;}
static ssize_t tiny4412_read(struct file *my_file, char __user *buf, size_t len, loff_t *loff){ /*2. 打印厂商芯片ID*/ unsigned short id=W25Q64_ReadDeviceID(); printk("-ID=0x%X\n",id); /*3. 写入数据*/ char buff[100]="打印厂商芯片ID打印厂商芯片ID"; W25Q64_WriteData(0,buff,100); /*4. 读出数据*/ char buff_rx[100]; W25Q64_ReadData(0,buff_rx,100); printk("Read=%s\n",buff_rx); return 0;}

static ssize_t tiny4412_write(struct file *my_file, const char __user *buf, size_t len, loff_t *loff){ return 0;}
/*文件操作集合*/static struct file_operations tiny4412_fops={ .open=tiny4412_open, .read=tiny4412_read, .write=tiny4412_write, .release=tiny4412_release};

/*核心结构体*/static struct miscdevice tiny4412_misc={ .minor=MISC_DYNAMIC_MINOR, /*自动分配次设备号*/ .name="tiny4412_W25q64", /*设备文件,指定/dev/生成的文件名称*/ .fops=&tiny4412_fops};

static int __devinit w25q64_probe(struct spi_device *spi){ /*配置SPI模式*/ spi->bits_per_word = 8; spi->mode = SPI_MODE_0; spi->max_speed_hz=1*1000000; //1Mhz if(spi_setup(spi)<0)//配置 { printk("SPI配置失败!\n"); } /*保存指针指向*/ w25q64_spi_Device=spi; printk("w25q64 probe ok!\n"); printk("SpiNum=%d\n",spi->dev.id); /*杂项设备注册*/ misc_register(&tiny4412_misc); return 0;}

static int __devexit w25q64_remove(struct spi_device *spi){ /*杂项设备注销*/ misc_deregister(&tiny4412_misc); return 0;}

static struct spi_driver w25q64_spi_driver = { .driver = { .name = "spidev", .owner =THIS_MODULE, }, .probe =w25q64_probe, .remove = __devexit_p(w25q64_remove),};

/*-------------------------------------------------------------------------*/static int __init w25q64_init(void){ spi_register_driver(&w25q64_spi_driver); printk("w25q64 driver install ok!\n"); return 0;}

static void __exit w25q64_exit(void){ spi_unregister_driver(&w25q64_spi_driver); printk("w25q64 driver exit ok!\n");}
module_exit(w25q64_exit);module_init(w25q64_init);MODULE_LICENSE("GPL");
复制代码


发布于: 2022 年 04 月 14 日阅读数: 15
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
Linux驱动开发-编写W25Q64(Flash)驱动_4月月更_DS小龙哥_InfoQ写作平台