2

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

 2 years ago
source link: https://xie.infoq.cn/article/917a89222b6c2acf6f6a6df4f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 口连接。

e71c186b383e805413d2bbaae74facda.png

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

7deeb35086f3e1981b23ce8a8bd2af50.png

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

b906145fe237c713379f5f1f2bedb03a.png

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 设备端结构:

5d2fa81adb7edb85c81b98a6e4862a63.png

SPI0 的具体 GPIO 口位置:

e44fbe66302cbc427119ec19e9566f2e.png

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");

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK