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字节可编程页
当前测试使用的开发板采用友善之臂的Tiny4412开发板,芯片是三星的EXYNOS-4412,最高主频1.5GHZ。
开发板引出了SPI的IO口,这里使用的W25Q64是外置的模块,使用杜邦线与开发板的IO口连接。
开发板上引出的IO口都是5V和1.8V,为了方便供电,采用了一个USB转TTL模块提供电源,测试驱动。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cCsfyWk9-1647826115961)(https://gitee.com/dsxiaolong/blog-drawing-bed/raw/master/img/image-20220106103338509.png)]
W25Q64模块接在开发板的SPI0接口上面的。
Linux内核自带有SPI子系统的设备端示例代码:
Linux 内核自带的 SPI 驱动注册示例代码: \drivers\spi\spidev.c
Linux 内核自带的 SPI APP 注册示例代码: \Documentation\spi
如果要使用内核自带SPI驱动,可以在内核编译时配置一下。
root# make menuconfig
Device Drivers --->
[*] SPI support --->
<*> Samsung S3C64XX series type SPI
[*] Samsung S3C64XX Channel 0 Support.
Tiny4412自带内核里的SPI设备端结构:
SPI0的具体GPIO口位置:
下面是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和制造商ID
W25Q64: EF16
W25QQ128: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");
下面代码使用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和制造商ID
W25Q64: EF16
W25QQ128: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");