好像有⼏张图⽚被强制缩⼩了?看到这篇博客的⼈先对你们说声抱歉,我不知道怎么设置⽂字就可以很长(⽂章宽度的全部),图⽚就只有⽂章宽度的2/3宽度开新分页应该就是原始尺⼨了,这点还是和⼤家说抱歉。。。
⽂章⾥⾯提到的页编程,就是写数据了,因为这是英⽂直译的结果(PageProgram)
为了测试这个外挂Flash存储器,我在淘宝买了⼀个⼩板,3元不到其实也可以直接买芯⽚回来⾃⼰接,反正没⼏个元件这个芯⽚是⽤SPI通讯的
我找不到没⽔印的图⽚,暂时先⽤W25Q128的
不过他俩板⼦长得⼀模⼀样,元件也⼀样。除了芯⽚型号板⼦上的LED和电阻串联,上电后LED就亮,没别的意思电容是滤波⽤的,它紧靠芯⽚的VCC引脚
另外附上两个链接,这是我之前写的博客,是关于『STM8开发环境』和『STM8 - SPI通讯』,这篇博客的测试基础,是建⽴在STM8上的关于如何接线,SPI通讯这篇博客有提到,如果有需要可以观看
SPI相关知识有了,就可以开始了开始之前,还是先介绍⼀下⼤纲
【W25Q16芯⽚介绍】:芯⽚命名规则、芯⽚引脚图、引脚功能介绍【W25Q16指令】:官⽅定义的指令,还有时序图介绍
【W25Q16初步测试】:执⾏其中⼀个指令(读取芯⽚ID),看看执⾏的效果,以此确认步骤是否正确,如果这⼀步都不正确,就不⽤谈最主要的读和写吧?
【W25Q16状态寄存器】:寄存器的⼀些状态,例如芯⽚是不是在忙、是不是处于保护状态、保护的区域、是否可写状态。。。等等【W25Q16读、写、擦除】:读、写、擦除相关代码
【W25Q16芯⽚介绍】
应该很好理解,像W25Q02系列,就是2G的Flash,下⽅的红字也提醒了,这是2G bit,像我们下载的电影、⾳乐,这些都是byte为单位的,设计的时候要考虑⼀下
提取码:iq4j
W25Q16的引脚如下
统⼀说明:前⽅有斜线的/,例如/CS,这个斜线代表低电平使能
【/CS】:⽚选引脚,低电平呢芯⽚⼯作,⾼电平芯⽚就罢⼯,当然,别想着⼀劳永逸这种事,直接把它接GND,我就吃到苦头了,这引脚请务必接GPIO
【DO】:数据输出
【/WP】:写保护,低电平呢只能读,⾼电平就随你读写【GND】:接地
【DI】:数据输⼊(接收外来的指令)【CLK】:时钟
【/HOLD】:数据暂停控制,低电平代表暂停,⾼电平⼯作,通常⽤于多个设备共享⼀个SPI,如果只有⼀主⼀从,可以把这引脚接VCC【VCC】:2.7~3.6V
另外,这个芯⽚可以⽀持『双输出』和『四输出』,可以提升读数据的速度具体的⽅法是把其他引脚的功能都改为输出(IO1、2、3、4)
就好⽐⼤家的车速都⼀样,道路有两条的情况下,⼀定⽐只有⼀条道路,处理车流量来的快
在引脚图的上⽅,有芯⽚的介绍,其中会看到104MHz、208MHz、416MHz分别是SPI单输出、双输出和四输出
遗憾的是STM8的SPI,最快也只有10MHz左右,想要处理双输出和四输出,是不可能的不过对于我的项⽬来说,这已经⾜够了
【W25Q16指令】
下⾯介绍写使能的时序图,但是在『W25Q16初步测试』的环节中,会读取JEDEC ID(指令发送0x9F),最终看看W25Q16有没有反馈『⽣产商ID』和『芯⽚ID』给我
给下降沿的原因,在介绍引脚图时,⽚选引脚/CS已经说明了,下达每个指令之前,必须给下降沿DI,也就是W25Q16接收的数据,0x06,⽂章往上拉找到指令的图⽚,找对应的位置,0x06就是写使能DO,因为这个指令不需要反馈数据给主机,所以是⾼阻态
【W25Q16初步测试】
我是透过Uart来打印数据的,图⽚左上有⽰意图
⽤⽰意图上的1234来表⽰流程,就是『1 234 234 234 234 234 234 234 234 234 234』234出现了⼗次,因为在『SPI接收中断』⾥⾯,判断count < 10
除了第⼀个『2』是指令(0x9F)以外,后⾯所有的『2』全部都是伪字节(0xFE),这是为了制造时钟给从机,在我另⼀篇博客有提到下⾯贴上完整代码,另外附上链接,需要代码的朋友也可以下载提取码:4pbw
#include\"iostm8s103F3.h\"#include \"W25Qxx.h\"
typedef unsigned char u8;
typedef unsigned short int u16;typedef unsigned int u32;
void UART1_sendchar(unsigned char c);void SPI_sendchar(unsigned char c);u8 count = 0;
/* ====================================== *//* ============ 【Uart】init ============ */
/* ====================================== */void Init_UART1(void){
UART1_CR1 = 0x00; UART1_CR2 = 0x00; UART1_CR3 = 0x00;
// 设置波特率,必须注意以下⼏点: // (1) 必须先写BRR2
// (2) BRR1存放的是分频系数的第11位到第4位,
// (3) BRR2存放的是分频系数的第15位到第12位,和第3位 // 到第0位
// 例如对于波特率位9600时,分频系数=2000000/9600=208 // 对应的⼗六进制数为00D0,BBR1=0D,BBR2=00 UART1_BRR2 = 0x00; UART1_BRR1 = 0x0d;
UART1_CR2 = 0x2c; // 允许接收,发送,开接收中断}
/* ====================================== *//* =========== 【Uart】发送函数 ========= */
/* ====================================== */void UART1_sendchar(unsigned char c){
while((UART1_SR & 0x80) == 0x00); // 等待发送缓冲区为空 UART1_DR = c;}
/* ====================================== *//* =========== 【Uart】接收中断 ========= */
/* ====================================== */#pragma vector= UART1_R_OR_vector//0x19__interrupt void UART1_R_OR_IRQHandler(void){
PC_ODR_ODR4 = 0; // 串⼝收到数据后进⼊中断,先给W25Qxx下降沿,等等透过SPI发送指令
SPI_sendchar(UART1_DR); // 发送SPI数据(UART接收到什么就发什么),然后等待SPI中断,实现⾃发⾃收}
/* ====================================== *//* ============ 【SPI】init ============= */
/* ====================================== */void Init_SPI(void){
CLK_PCKENR1 |= 0x02; //打开SPI时钟 /*PC6、PC5设置为输出,最⼤10MHz*/
//PC_DDR = 0x60; // ⽤下⽅⽐较详细的写法 //PC_CR1 = 0xe0; // ⽤下⽅⽐较详细的写法 //PC_CR2 = 0x60; // ⽤下⽅⽐较详细的写法
PC_DDR_DDR4 = 1; // 配置PC4(/CS)端⼝为输出模式 PC_CR1_C14 = 1; // 配置PC4(/CS)端⼝为推挽输出模式 PC_CR2_C24 = 1; // 配置PC4(/CS)端⼝为⾼速率输出
PC_DDR_DDR5 = 1; // 配置PC5(SCK)端⼝为输出模式 PC_CR1_C15 = 1; // 配置PC5(SCK)端⼝为推挽输出模式 PC_CR2_C25 = 1; // 配置PC5(SCK)端⼝为⾼速率输出
PC_DDR_DDR6 = 1; // 配置PC6(MOSI)端⼝为输出模式 PC_CR1_C16 = 1; // 配置PC6(MOSI)端⼝为推挽输出模式 PC_CR2_C26 = 1; // 配置PC6(MOSI)端⼝为⾼速率输出
PC_DDR_DDR7 = 0; // 配置PC7(MISO)端⼝为输⼊模式
PC_CR1_C17 = 1; // 配置PC7(MISO)端⼝为弱上拉输⼊模式 PC_CR2_C27 = 0; // 禁⽌PC7(MISO)端⼝外部中断
SPI_ICR_RXIE = 1; // 开启SPI中断接收
// [7]先发MSB // [6]禁⽌SPI
// [5][4][3]f_Master / 2 // [2]主设备
// [1]空闲时SCK保持低电平
// [0]数据采样从第⼀个时钟沿开始
SPI_CR1 = 0x04; /*MSB、1MHz、主设备、CPOL空闲为低、CPHA第⼀个时钟开始*/
// [7]双线单向模式
// [6]输⼊使能(只接收模式) // [5]CRC计算禁⽌
// [4]下个发送数据来⾃Tx缓冲 // [3]保留
// [2]全双⼯(同时收发)
// [1]使能软件从设备管理(不需要判断硬件CS位,节省⼀个引脚) // [0]主模式
SPI_CR2 = 0x03; /*双线单向视距传输、CRC计算禁⽌、软件NSS、主模式*/
SPI_CR1_SPE = 1; // 打开SPI}
/* ====================================== *//* =========== 【SPI】发送函数 ========== */
/* ====================================== */void SPI_sendchar(unsigned char c){
while(!(SPI_SR & 0x02)); // 等待发送缓冲区为空
SPI_DR = c; // 将发送的数据写到数据寄存器
//while(!(SPI_SR & 0x01)); // 等待接收缓冲区⾮空,这是轮询的⽅式,但是我想在中断来处理 //UART1_sendchar(SPI_DR);}
/* ====================================== *//* =========== 【SPI】接收中断 ========== */
/* ====================================== */#pragma vector=SPI_RXNE_vector
__interrupt void SPI_RXNE_IRQHandler(void){
//RxBuf[cnt++]=SPI_DR; while(!(SPI_SR & 0x01));
UART1_sendchar(SPI_DR); // 把SPI接收到的数据,透过UART,传回给USB转TTL⼩板 count++;
if(count < 10) SPI_sendchar(0xfe); // 发送伪字节 else {
count = 0;
PC_ODR_ODR4 = 1; // 重新置为⾼电平,等待下⼀次的指令 }}
/* ====================================== *//* ============== 【Main】 ============== *//* ====================================== */main(){
Init_UART1(); Init_SPI();
PC_ODR_ODR4 = 1; // 初始上电给⾼电平,后续W25Qxx在执⾏指令前,再给下降沿
asm(\"rim\"); // 开中断,sim为关中断 while (1);}
【W25Q16状态寄存器】⽂章有点长,再说明⼀个寄存器就好了先上⼀张图,这是状态寄存器⾥的内容
下⾯是寄存器内各个『位』的说明,另外『R』代表『只可读』,『W』代表『只可写』,『RW』代表『可读可写』
【BUSY】(R):芯⽚在忙的时候,状态=1,不忙时=0,什么时候在忙呢?执⾏『页编程』『任何⼀种擦除』『写状态』都是,芯⽚忙完这些事会⾃动清0
【WEL】(R):『写保护』位,执⾏写使能后,由芯⽚⾃动置1,芯⽚处于『写保护』时该位=0,写禁⽤状态发⽣在『通电时』『写禁⽌』『页编程』『任何⼀种擦除』和『写状态寄存器』
【BP0、1、2】(RW):这三位决定了需要保护的区域,例如⼀些固件,你不想后续被修改的东西,都可以保护。默认为0,另外,它和TB、SEC位有关。这⾥不做过多介绍,我的项⽬没有⽤到,还没研究,未来有时间再看看。
【TB】(RW):默认为0,可以决定是『顶部』或是『底部』需要保护,例如有100个保险柜,你要保护前10个,或是保护最后20个,具体位置请参考上⾯的图⽚。这⾥不做过多介绍,我的项⽬没有⽤到,还没研究,未来有时间再看看。【SEC】:⾮易失性扇区保护位。这⾥不做过多介绍,我的项⽬没有⽤到,还没研究,未来有时间再看看。【SRP0、1】(RW):状态寄存器保护位,默认为0。 ❶ SPR=0:不能控制状态寄存器的『禁⽌写』
❷ SPR=1、引脚/WP=低电平:『写状态寄存器』的指令失效 ❸ SPR=1、引脚/WP=⾼电平:可以执⾏『写状态寄存器』的指令
【SUS】(R):挂起状态位是状态寄存器,在执⾏擦除挂起(75h)指令后设置为1。SUS状态位通过擦除恢复(7ah)指令以及断电、通电循环清除为0。
【QE】(RW):四输出使能位是状态寄存器。当qebit设置为0状态(出⼚默认值)时,/wp pinand/hold被启⽤。当qebit设置为1时,将启⽤四个io2和io3引脚,并禁⽤/wp和/hold功能。
Warning:如果在标准SPI或双SPI操作期间/wp或/hold引脚直接连接到电源或接地,则QE位不应设置为1。
看到这⾥的朋友,先和你们说声抱歉,读取状态寄存器我真的没有试出来,每次读取都是0x00 0x00 0x00 0x00。。。我尝试执⾏『写使能』,然后读取状态寄存器,还是0x00 0x00 0x00 0x00。。。我再尝试执⾏『写禁⽌』,然后读取状态寄存器,还是0x00 0x00 0x00 0x00。。。
照理说,『写使能』和『写禁⽌』应该会改变『WLE』这⼀位,结果没有,真是百思不得其解(读JEDEC ID都正常,所以不是我接线,或是SPI通讯的问题)(JEDEC上⾯说过了,是⽣产商ID)唉。。。
【W25Q16读、写、擦除】
在说明读和写之前,先说明⼀下Flash的物理特性:Flash只能写0,不能写1上⼀张图,来解释这个特性
有⼈说,写的时候不⽤擦除,那是因为特殊情况
第⼀天,Flash的值是0xFF(1111 1111),我写⼊0xF0(1111 0000)【⾼4位都是1➜1,没有影响】【低4位是1➜0,由于是写0的动作,所以⽆需擦除】
第⼆天,Flash的值是0xF0(1111 0000),我写⼊0x00(0000 0000)【⾼4位是1➜0,由于是写0的动作,所以⽆需擦除】【低4位是0➜0,这也是写0动作,⽆需擦除】
这些情况还不需要擦除,除⾮到某⼀天,你想写⼀个数据,不管这8位的哪⼀位要变成1,那么就必须擦除了
理解这个特性,能有效的增加Flash的寿命,在这篇博客,引脚图的上⽅,有芯⽚介绍,⾥⾯有⼀段英⽂『More than 100,000 erase/writecycles』
芯⽚能让你擦除10万次
具体要不要让你的程序复杂些,但是能让芯⽚寿命增长,就要⾃⾏斟酌了
讲了这么多,下⾯终于可以开始重头戏了
这个读写的代码,基本上和上⾯的读ID代码类似,只增加了两个变量,和修改两个中断【1】定义两个变量,testAddress、command【2】串⼝接收中断【3】SPI接收中断
u32 testAddress = 0x000000;
u8 command = 0; // 【0:写使能、写禁⽌、芯⽚擦除】【1:写】【2:读】
/* ====================================== *//* =========== 【Uart】接收中断 ========= */
/* ====================================== */#pragma vector= UART1_R_OR_vector//0x19__interrupt void UART1_R_OR_IRQHandler(void){
PC_ODR_ODR4 = 0; // 串⼝收到数据后进⼊中断,先给W25Qxx下降沿,等等透过SPI发送指令
if (UART1_DR == 0x01) // 页编程 {
command = 1;
SPI_sendchar(PageProgram); }
else if (UART1_DR == 0x02) // 读数据 {
command = 2;
SPI_sendchar(ReadData); }
else if (UART1_DR == 0x03) // 写使能 {
command = 0;
SPI_sendchar(WriteEnable); }
else if (UART1_DR == 0x04) // 写禁⽌ {
command = 0;
SPI_sendchar(WriteDisable); }
else if (UART1_DR == 0x05) // 芯⽚擦除 {
command = 0;
SPI_sendchar(EraseChip); }}
/* ====================================== *//* =========== 【SPI】接收中断 ========== */
/* ====================================== */#pragma vector=SPI_RXNE_vector
__interrupt void SPI_RXNE_IRQHandler(void){
//RxBuf[cnt++]=SPI_DR; while(!(SPI_SR & 0x01)); UART1_sendchar(SPI_DR);
if(count < 7 && command != 0) /* 地址+伪字节 < 7 并且 不是写使能、写禁⽌、芯⽚擦除进来的 */ {
if (command == 1) /* 执⾏页编程剩下的动作,先写24bit地址,然后给数据 */ {
if (count == 0) SPI_sendchar((testAddress & 0xFF0000) >> 16); // 【写】⾼位地址 else if(count == 1) SPI_sendchar((testAddress & 0xFF00) >> 8); // 【写】中间地址 else if(count == 2) SPI_sendchar(testAddress); // 【写】低位地址 else SPI_sendchar(0xaa); // 存⼊的数据 }
else if (command == 2) /* 执⾏读数据剩下的动作,先写24bit地址,然后给数据 */ {
if(count == 0) SPI_sendchar((testAddress & 0xFF0000) >> 16); // 【写】⾼位地址 else if(count == 1) SPI_sendchar((testAddress & 0xF000) >> 8); // 【写】中间地址 else if(count == 2) SPI_sendchar(testAddress); // 【写】低位地址
else SPI_sendchar(0xff); // 发送伪字节,制造时钟以便获得从机的数据 } count++; }
else /* count结束,或是写使能、写禁⽌、芯⽚擦除的复位 */ {
count = 0;
PC_ODR_ODR4 = 1; // 【/CS给⾼电平,等待下次命令给下降沿】 } }
循环7次,地址占3个字节,7 - 3 = 4,数据就占4个字节这⾥我写⼊数据0xAA,四个数据就都是⼀样的了
这些数据保存在地址0x000000,这些东西都是写死的,要⽤时再根据⾃⼰的项⽬做修改就可以了另外,⼀个地址可以理解为⼀个页⾯(page)
⼀样,在引脚图的上⽅介绍⾥,有⼀段英⽂『256-bytes per programmable page』⼀个地址可以写256个字节,但我只⽤了4个哈
哦对了,上⾯的代码有⽤到⼏个定义的东西,我把它放在头⽂件了,这些也就是W25Q16相关的命令罢了
#define WriteEnable 0x06 // 写使能#define WriteDisable 0x04 // 禁⽌写
#define WriteStatusRegister 0x01 // 写状态寄存器#define ReadStatusRegister_1 0x05 // 读状态寄存器#define ReadStatusRegister_2 0x35 // 读状态寄存器2#define PageProgram 0x02 // 页编程#define QuadPageProgram 0x32
#define EraseBlock64K 0xd8 // 块擦除(64KB)#define EraseBlock32K 0x52 // 块擦除(32KB)#define EraseSector4K 0x20 // 扇区擦除(4KB)#define EraseChip 0xc7 // 芯⽚擦除
#define EraseChip2 0x60 // 已经有了⼀个,为什么还要另⼀个芯⽚擦除指令?#define EraseSuspend 0x75#define EraseResume 0x7a
#define PowerDown 0xb9 // 掉电(可唤醒)#define HighPerformanceMode 0xa3#define ModeBitReset 0xff
#define ReleasePowerDownOrHPM 0xab // 掉电后可以释放掉电,然后器件会返回⼀个Device ID#define Manufacturer 0x90 // 制造,芯⽚会返回器件ID#define ReadUniqueID 0x4b#define JEDEC_ID 0x9f
#define ReadData 0x03
#define FastRead 0x0b // 快速读取
#define FaseReadDualOutput 0x3b // 快速读取(双输出)#define FastReadDualIO 0xbb#define FastReadQuadOutput 0x6b#define FastReadQuadIO 0xeb
提取码:kdsz
因篇幅问题不能全部显示,请点此查看更多更全内容