本次我们学习一下STM32F103关于SPI对存储芯片的读写,介绍W25QXX芯片和对芯片内部讲解和代码解读,学习W25QXX芯片的各种读写指令,操作芯片读写,认识底层驱动,本篇内容主要目的是教会大家看手册写代码,读代码。
本篇博客大部分是自己收集和整理,如有侵权请联系我删除。
本次实验板子使用的是正点原子精英版,芯片是STM32F103ZET6,需要资料可以@我拿取。
本博客内容原创,创作不易,转载请注明
没有初步认识SPI协议的,可以先看看我之前的博客:SPI协议讲解
应用:需要存储较多数据的时候,比如字库,图片,各种模块数据等等
总述:W25Q16,W25Q32 ,W25Q64 系列的FLASH存储器可以为用户提供存储解决方案。
存储芯片大小:这是其中几个,其他都差不多,具体要查一下
| W25Q16 |
|
| W25Q32 | 32M比特 (bit) / 4M字节(byte) |
| W25Q64 | 64M比特 (bit) / 8M字节(byte) |
| W25Q128 | 128M比特 (bit) / 16M字节(byte) |
本篇文章我们用W25Q16来介绍,内存容量大小为:2*1024*1024->内存地址
W25Q16,W25Q32 ,W25Q64 系列的FLASH存储器分别有8192,16384,32768可编程页,每页256个字节。芯片内部分为了块,扇区,页的编程指令和擦除指令,下面来看看他们之间的关系。
内部区分是从 块 -> 扇区 -> 页
| 块 | 1 块 = 16个扇区 = 64 K |
| 扇区 | 1 扇区= 16页 = 4096 字节 = 4 K |
| 页 | 1 页 = 256 个字节 |
| 字节 | 1 字节 = 8 位 |
1.用“页编程指令”每次就可以编程256个字节,意思就是每次都能一次性最多写入256个字节,因为芯片规定了不能跨页来写入数据,所以需要我们自己修改程序代码。
2.用 扇区擦除指令 每次可以擦除16页 ,不能自己删除指定的页区,每次使用这个指令都是直接擦除16页
3.用 块擦除指令 每次可以擦除256页 , 用 整片擦除指令 可擦除整个芯片。
标准的SPI接口:

地址和区域分布(在英文手册有描述):




通过手册我们可以看到SPI的配置模式选择:

SPI 配置我们就不多说了,不懂去看看我博客的SPI协议讲解,现在直接上代码:
#include "spi.h"
//SPI口初始化
void spi1_init(void)
{
RCC->APB2ENR|=1<<2; //PORTA时钟使能
RCC->APB2ENR|=1<<12; //SPI1时钟使能
GPIOA->CRL&=0X000FFFFF;
GPIOA->CRL|=0XB8B00000; //PA5 PA7复用推挽输出
GPIOA->ODR|=0X07<<5; //PA6 上拉输入
SPI1->CR1|=0<<11; //8bit数据格式
SPI1->CR1|=0<<10; //全双工模式
SPI1->CR1|=1<<9; //软件nss管理
SPI1->CR1|=1<<8;
SPI1->CR1|=0<<7; //MSB first
SPI1->CR1|=3<<3; //Fsck=Fpclk1/16,对SPI1属于APB2的外设.时钟频率最大为72M.
SPI1->CR1|=1<<2; //SPI主机
SPI1->CR1|=1<<1; //空闲模式下SCK为1 CPOL=1
SPI1->CR1|=1<<0; //数据采样从第二个时间边沿开始,CPHA=1
SPI1->CR1|=1<<6; //SPI设备使能
}
//spi1发送数据
void spi1_send_byte(u8 data)
{
//等待前面的发送完成
while((SPI1->SR&(1<<1))==0); //等待发送区空
//给数据DR寄存器
SPI1->DR = data; //这一步执行完成后并不会马上传输完成
//spi是一个数据交换协议,发送一个字节,必然可以收到一个字节
while((SPI1->SR&(1<<0))==0); //等待接收完成(无用的数据)
data = SPI1->DR; //读取接收到无用的数据
}
/********************************************************************
* Function: spi2_receive_byte
* Description: spi2接收数据
* Return : 接收到的数据
*********************************************************************/
u8 spi1_receive_byte(void)
{
u8 data;
//等待前面的发送完成
while(!(SPI1->SR&(1<<1)));
//给数据DR寄存器
SPI1->DR = 0xaa; // 读的时候传的值,这个值不重要,什么值都可以(0x01等)给这个值的目的主要是维持有时钟(时序图可以看出)
//等待接收完成
while(!(SPI1->SR&(1<<0)));
data = SPI1->DR; //读取数据
return data; //返回的是我们需要的数据。
}
//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 spi1_read_write_byte(u8 data)
{
//写功能
while((SPI1->SR&1<<1)==0); //等待发送区空 --等待发送缓冲为空
//这一步执行完成后并不会马上传输完成
SPI1->DR=data; //发送一个byte ,写进去之后会自动的发出去
//到这里其实发送函数已经发送完成了
//SPI是一个数据交换协议,发送一个字节,必然可以接收到一个字节
while((SPI1->SR&1<<0)==0) ; //等待接收完一个byte
return SPI1->DR; //返回收到的数据
}
芯片本身自带有一个控制和状态寄存器,要控制他读写,我们就需要控制操作寄存器来编写代码。



这个第0位 和 第1位,都是只读位,在执行操作的时候才会置1,所以平常默认为 0;

这两个为是可读可写位,根据自己想要执行的操作,给上对应的位,就可以通过SPI写入读出。

通过以上的寄存器了解,我们就可以读出状态数据知道芯片的存储器阵列是否可写或者不可写,会是否处于写保护状态。
在学习这个指令之前,我们要先了解清楚SPI的工作模式,以及使用到SPI的读写函数,这个不清楚可以看我往期博客,这里就不细说了;
函数的基本执行流程:一般来说就是拉低片选,然后发送指令,从机会随机返回一个数据,这个数据可以由你自己决定,执行相应操作后,拉高片选,这个函数就实现了对应的指令功能了,



1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把0X05的指令发送过去
3.接着从机返回一个随机数据,这里我们读取一个0xff
4.发送完成后,拉高片选,这样我们就完成了读状态寄存器的指令函数的编写了
因为这次是读取从机的数据,所以需要返回值
//读取W25QXX的状态寄存器
//BIT7 6 5 4 3 2 1 0
//SPR RV TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
//W25QXX_ReadStatusReg 0
u8 W25QXX_ReadSR(void)
{
u8 byte=0;
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ReadStatusReg); //发送读取状态寄存器命令
byte=spi1_read_write_byte(0Xff); //读取一个字节
W25QXX_CS=1; //取消片选
return byte;
}


1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把 0X01 的指令发送过去
3.然后写好我们需要发送的字节
4.发送完成后,拉高片选,这样我们就完成了写状态寄存器的指令函数的编写了
//写W25QXX状态寄存器
//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写!!!
// W25X_WriteStatusReg 0x01
void W25QXX_Write_SR(u8 sr)
{
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25X_WriteStatusReg);//发送写取状态寄存器命令
spi1_read_write_byte(sr); //写入一个字节
W25QXX_CS=1; //取消片选
}


1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把 0X06 的指令发送过去
3.发送完成后,拉高片选,这样我们就完成了写状态寄存器的指令函数的编写了
//W25QXX写使能
//将WEL置位
//W25QXX_WriteEnable 0x06
void W25QXX_Write_Enable(void)
{
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_WriteEnable); //发送写使能
W25QXX_CS=1; //取消片选
}
我们要通过写入数据进去flash,就需要用到这个页编程指令,在页内写入我们的数据,写这个函数都要用到上面的读写函数,所以我们要先写好其他的函数,才能执行其他的指令。


为什么要发送24位地址,这个24位地址又是什么东西?
1.首先我们要先知道,SPI每次只能发送8位地址,所以这个24位地址我们需要分三次发
2.这个24位的地址,本质上就是数据存放的位置,为什么是24位呢,因为这个地址就是24位的,我们看手册就能知道。

3.比如我们要发送0X123456怎么发呢,SPI是高位先发的,所以我们要用上点位运算就可以了

4.发完24位地址之后,我们就可以发送我们的字节了,最大只能发送256个字节,超过就不行了
下面直接上代码讲解一下实现过程:
//SPI在一页(0~32768)内写入少于256个字节的数据,按照你的芯片规格计算大小
//在指定地址开始写入最大256字节的数据 (不能跨扇区写)
//调用这个函数必须确保写入的区域已经擦除了
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
//W25QXX_PageProgram 0
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 i;
W25QXX_Write_Enable(); //SET WEL 发送写使能,WEL位自动置1
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_PageProgram); //发送写页命令
spi1_read_write_byte((u8)((WriteAddr)>>16)); //发送24bit地址
spi1_read_write_byte((u8)((WriteAddr)>>8));
spi1_read_write_byte((u8)WriteAddr);
for(i=0;i<NumByteToWrite;i++)
spi1_read_write_byte(pBuffer[i]); //循环写数
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待写入结束
}
1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,保证区域内没有数据,如果不确定就需要先擦除
3.发送页编程的指令0x02
4.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
5.使用一个for循环,写入数组或者指针内的数据,可以连续写入,最多可以写256个
6.拉高片选,写入数据结束
7.根据手册,最后还要一个等待写入结束,这时候我们写一个函数判断一下就行,后面也会用到这个函数的
u8 texttowrite[]="12345ABCDE"; //要发送的数据
#define SIZE sizeof(texttowrite) .
W25QXX_Write(texttowrite,88,SIZE);
//等待空闲
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR()&0x01)==0x01); // 等待BUSY位清空
}


1.根据手册描述,首先我们先拉低片选
2.发送页编程的指令0x03
4.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
5.使用一个for循环,读取数组或者指针内的数据,同时返回随机数据
6.拉高片选,读取数据结束
本质上和页编程差不多,对着时序和手册都能简单的写出来了
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大32768)
//W25QXX_ReadData 0
void W25QXX_Read(u8* buffer,u32 read_address,u16 NumByteToRead)
{
u16 i;
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ReadData); //发送读取命令
spi1_read_write_byte((u8)((read_address)>>16)); //发送24bit地址
spi1_read_write_byte((u8)((read_address)>>8));
spi1_read_write_byte((u8)read_address);
for(i=0;i<NumByteToRead;i++)
{
buffer[i]=spi1_read_write_byte(0XFF); //循环读数 ,发送空字节
}
W25QXX_CS=1;
}

1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,发送扇区擦除的指令0x20,等待空闲
3.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
4.拉高片选,扇区擦除完成,等待空闲
扇区擦除每次擦除4K=4096;在程序里面我们可自己设置擦除哪一个扇区的地址,也可以不设置,直接从写入地址开始擦除,下面看程序:
//擦除一个扇区
//Dst_Addr: 扇区编号,是第几个扇区,不是绝对地址
//擦除一个扇区的最少时间:150ms
// W25QXX_SectorErase 0x20
void W25QXX_Erase_Sector(u32 Dst_Addr) //1
{
Dst_Addr*=4096;
W25QXX_Write_Enable(); //SET WEL 写使能
W25QXX_Wait_Busy(); //等待空闲
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_SectorErase); //发送扇区擦除指令
spi1_read_write_byte((u8)((Dst_Addr)>>16)); //发送24bit地址 ,先写高位
spi1_read_write_byte((u8)((Dst_Addr)>>8));
spi1_read_write_byte((u8)Dst_Addr);
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待擦除完成
}


1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,发送扇区擦除的指令0xc7,等待空闲
3.拉高片选,扇区擦除完成,等待空闲
//擦除整个芯片
//等待时间超长...
//W25QXX_ChipErase 0xc7
void W25QXX_Erase_Chip(void)
{
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ChipErase); //发送片擦除命令
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待芯片擦除结束
}
这个指令实在适合电池供电的情况下使用,我们根据手册要求使用就可以

//进入掉电模式
//W25X_PowerDown 0XB9
void W25QXX_PowerDown(void)
{
W25QXX_CS=0; //使能器件
SPI2_ReadWriteByte(W25X_PowerDown); //发送掉电命令
W25QXX_CS=1; //取消片选
delay_us(3); //等待TPD
}
因为我们这个W25QXX,这个程序大部分都适用,所以当我们使用不同大小的flsah芯片的时候,我们就可以通过读取ID,选到自己对应的芯片了。

//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q32
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
u16 W25QXX_ReadID(void)
{
u16 Temp = 0;
W25QXX_CS=0;
SPI2_ReadWriteByte(0x90);//发送读取ID命令
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp|=SPI2_ReadWriteByte(0xFF)<<8;
Temp|=SPI2_ReadWriteByte(0xFF);
W25QXX_CS=1;
return Temp;
}
以上就是这个flash芯片的大部分指令函数以及使用了,相信很多人对这个擦除的方式不太了解,为什么每次都整片擦除,那之前写的数据怎么办,又要重新写入吗?
针对这个问题,那这种方法肯定是我们自己通过程序来实现,随时随地擦除和写入,由于本次内容已经很多了,所以这个问题留到下一个博客,下一个博客讲解一下如何实现随意的擦除和写入,还有对字库以及图片写入数据的讲解,
以上就是这篇博客的全部内容了,内容比较多,感觉大家耐心看完,代码其实都是大同小异,大家可以学会使用看手册和时序来编写自己想要实现的函数。
大家如果对我的博客有疑问或者错误,可以@我修改,大家相互交流。
点赞收藏关注博主,不定期分享单片机知识,互相学习交流。
我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re
我正在尝试用ruby中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc