STM32CubeMX教程20 SPI - W25Q128驱动

1、准备材料

开发板(正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(MDK-Arm

ST-LINK/V2驱动

XCOM V2.6串口助手

逻辑分析仪nanoDLA

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的SPI1与W25Q128芯片通信,以轮询方式读写W25Q128 FLASH芯片,并通过USART1输出相关信息,具体为使用开发板上的三个用户按键KEY0/1/2,分别实现对W25Q128芯片写数据/读数据/擦除数据的操作,操作过程中与用户的交互由USART1输出信息来实现

3、实验流程

3.0、前提知识

本实验重点是理解标准SPI通信协议,而STM32CubeMX的配置则相对简单,这里不会过于详细全面的介绍SPI通信协议,但是会对所有需要知道的知识做介绍

标准SPI通信协议由时钟信号线SCK、主设备输出从设备输入MOSI和主设备输入从设备输出MISO三根线组成,与I2C通信协议不同,挂载在SPI总线上的外围器件不需要有从设备地址,而是由片选CS/SS信号选择从机设备,当片选信号为低电平时,表示该从设备被选中,此时主设备通过SCK、MOSI与MISO三根线与该从设备之间进行通信和数据传输,如下所示为SPI总线连接图 (注释1)

本实验所使用的开发板上有一颗FLASH芯片W25Q128,STM32F407通过PB3(SPI1_SCK)、PB4(SPI1_MISO)和PB5(SPI1_MOSI)三个引脚利用标准SPI协议与其进行通信和数据传输,W25Q128的片选信号选择了MCU的PB14引脚,如下图所示为其硬件原理图

SPI通信协议的时序根据CPOL(时钟极性)和CPHA(时钟相位)两个寄存器位的不同一共有四种组合模式

时钟极性CPOL位用来控制SCK引脚在空闲状态时的电平,当该位为0时则表示空闲时刻SCK为低电平,反之为高电平

时钟相位CPHA位用来控制在SCK信号的第几个边沿处采集信号,当该位为0时表示在SCK型号的第一个边沿处采集信号,反之则表示在第二个边沿处采集信号

如下图所示为根据CPOL和CPHA位取不同值时SPI通信协议的四种时序图 (注释2)

使用逻辑分析仪对STM32F407 SPI1通信SCLK、MISO、MOSI和CS四个引脚进行逻辑电平监测,可以发现在执行读取W25Q128芯片ID操作的过程中,其四个引脚的时序与我们所介绍的一致

如下图所示为执行读取W25Q128芯片ID操作所使用的程序、CPOL=0 CPHA=0时SPI通信采集到的时序和CPOL=1 CPHA=1时SPI通信采集到的时序

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

此实验主要是利用SPI通信协议与W25Q128芯片进行通信和数据传输,并且需要串口将读取的数据输出给用户,同时还需要三个用户按键KEY0/1/2/,因此外设需要初始化KEY0/1/2、USART1和SPI1

按键初始化操作请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”实验

单击Pinout & Configuration页面左边Connectivity/USART1选项,然后按照“STM32CubeMX教程9 USART/UART 异步通信”实验中将USART1配置为异步通信模式,无需开启中断,如下图所示

单击Pinout & Configuration页面左边Connectivity/SPI1选项,Mode选择全双工主机模式,不需要硬件片选,时钟分频选择16分频,根据W25Q128的数据手册 (注释3),读数据指令支持的最高频率为33MHz,因此适当降低频率确保通信不会出现错误,其他参数配置默认即可,具体配置如下图所示

然后在右边芯片引脚预览Pinout view中找到W25Q128芯片的片选引脚PB14,左键单击并配置其功能为GPIO_Ouput,然后单击System Core/GPIO,配置PB14引脚默认输出电平高,推挽输出,无上下拉,IO速度非常高,具体配置如下图所示

3.1.3、外设中断配置

本实验无需启用中断,如果需要启用SPI1的中断,请单击System Core/NVIC,然后根据需求勾选SP1全局中断,并选择合适的中断优先级即可,具体配置如下图所示

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of ‘c/h’ files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节

3.2.1、外设初始化调用流程

在生成的工程代码主函数中新增了MX_SPI1_Init()函数,在该函数中实现了对SPI1的模式及参数配置

在MX_SPI1_Init()函数中调用了HAL_SPI_Init()函数使用配置的参数对SPI1进行了初始化

在HAL_SPI_Init()函数中又调用了HAL_SPI_MspInit()函数对SPI1引脚复用设置,SPI1时钟使能,如果开启了中断该函数中还会有中断相关设置及使能

具体的SPI1初始化函数调用流程如下图所示

3.2.2、外设中断调用流程

本实验无需中断,因此未启动任何SPI1的中断

3.2.3、添加其他必要代码

需要添加W25Q128的驱动文件,注意本实验只使用而不会介绍W25Q128具体驱动文件的原理,具体源代码如下图所示 (注释4)

w25flash.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
/* 文件: w25flash.c
* 功能描述: Flash 存储器W25Q128的驱动程序
* 作者:王维波
* 修改日期:2019-06-05
*/


#include "w25flash.h"

#define MAX_TIMEOUT 200 //SPI轮询操作时的最大等待时间,ms

//SPI接口发送一个字节,byteData是需要发送的数据
HAL_StatusTypeDef SPI_TransmitOneByte(uint8_t byteData)
{
return HAL_SPI_Transmit(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
}

//SPI接口发送多个字节, pBuffer是发送数据缓存区指针,byteCount是发送数据字节数,byteCount最大256
HAL_StatusTypeDef SPI_TransmitBytes(uint8_t* pBuffer, uint16_t byteCount)
{
return HAL_SPI_Transmit(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//SPI接口接收一个字节, 返回接收的一个字节数据
uint8_t SPI_ReceiveOneByte()
{
uint8_t byteData=0;
HAL_SPI_Receive(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
return byteData;
}

//SPI接口接收多个字节, pBuffer是接收数据缓存区指针,byteCount是需要接收数据的字节数
HAL_StatusTypeDef SPI_ReceiveBytes(uint8_t* pBuffer, uint16_t byteCount)
{
return HAL_SPI_Receive(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//Command=0x05: Read Status Register-1,返回寄存器SR1的值
uint8_t Flash_ReadSR1(void)
{
uint8_t byte=0;
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x05); //Command=0x05: Read Status Register-1
byte=SPI_ReceiveOneByte();
__Deselect_Flash(); //CS=1
return byte;
}

//Command=0x35: Read Status Register-2,返回寄存器SR2的值
uint8_t Flash_ReadSR2(void)
{
uint8_t byte=0;
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x35); //Command=0x35: Read Status Register-2
byte=SPI_ReceiveOneByte(); //读取一个字节
__Deselect_Flash(); //CS=1
return byte;
}


//Command=0x01: Write Status Register, 只写SR1的值
//耗时大约10-15ms
void Flash_WriteSR1(uint8_t SR1)
{
Flash_Write_Enable(); //必须使 WEL=1

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x01); //Command=0x01: Write Status Register, 只写SR1的值
SPI_TransmitOneByte(0x00); //SR1的值
// SPI_WriteOneByte(0x00); //SR2的值, 只发送SR1的值,而不发送SR2的值, QE和CMP将自动被清零
__Deselect_Flash(); //CS=1

Flash_Wait_Busy(); //耗时大约10-15ms
}

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void) //Command=0x50: Write Volatile Enable
{
__Select_Flash(); //CS=0
HAL_StatusTypeDef result=SPI_TransmitOneByte(0x50);
__Deselect_Flash(); //CS=1
return result;
}


//Command=0x06: Write Enable, 使WEL=1
HAL_StatusTypeDef Flash_Write_Enable(void)
{
__Select_Flash(); //CS=0
HAL_StatusTypeDef result=SPI_TransmitOneByte(0x06); //Command=0x06: Write Enable, 使WEL=1
__Deselect_Flash(); //CS=1
Flash_Wait_Busy(); //等待操作完成
return result;
}

//Command=0x04, Write Disable, 使WEL=0
HAL_StatusTypeDef Flash_Write_Disable(void)
{
__Select_Flash(); //CS=0
HAL_StatusTypeDef result=SPI_TransmitOneByte(0x04); //Command=0x04, Write Disable, 使WEL=0
__Deselect_Flash(); //CS=1
Flash_Wait_Busy(); //
return result;
}

//根据Block绝对编号获取地址, 共256个Block, BlockNo 取值范围0-255
//每个块64K字节,16位地址,块内地址范围0x0000-0xFFFF。
uint32_t Flash_Addr_byBlock(uint8_t BlockNo)
{
// uint32_t addr=BlockNo*0x10000;

uint32_t addr=BlockNo;
addr=addr<<16; //左移16位,等于乘以0x10000
return addr;
}

//根据Sector绝对编号获取地址, 共4096个Sector, SectorNo取值范围0-4095
//每个扇区4K字节,12位地址,扇区内地址范围0x000-0xFFF
uint32_t Flash_Addr_bySector(uint16_t SectorNo)
{
if (SectorNo>4095) //不能超过4095
SectorNo=0;
// uint32_t addr=SectorNo*0x1000;

uint32_t addr=SectorNo;
addr=addr<<12; //左移12位,等于乘以0x1000
return addr;
}

//根据Page绝对编号获取地址,共65536个Page, PageNo取值范围0-65535
//每个页256字节,8位地址,页内地址范围0x00—0xFF
uint32_t Flash_Addr_byPage(uint16_t PageNo)
{
// uint32_t addr=PageNo*0x100;

uint32_t addr=PageNo;
addr=addr<<8; //左移8位,等于乘以0x100
return addr;
}

//根据Block编号和内部Sector编号计算地址,一个Block有16个Sector
//BlockNo取值范围0-255, 内部SubSectorNo取值范围0-15
uint32_t Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo)
{
if (SubSectorNo>15) //不能超过15
SubSectorNo=0;

// uint32_t addr=BlockNo*0x10000; //先计算Block的起始地址
uint32_t addr=BlockNo;
addr=addr<<16; //先计算Block的起始地址

// uint32_t offset=SubSectorNo*0x1000; //计算Sector的偏移地址
uint32_t offset=SubSectorNo; //计算Sector的偏移地址
offset=offset<<12; //计算Sector的偏移地址

addr += offset;

return addr;
}

//根据Block编号,内部Sector编号,内部Page编号获取地址
//BlockNo取值范围0-255
//一个Block有16个Sector, 内部SubSectorNo取值范围0-15
//一个Sector有16个Page , 内部SubPageNo取值范围0-15
uint32_t Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo, uint8_t SubPageNo)
{
if (SubSectorNo>15) //不能超过15
SubSectorNo=0;

if (SubPageNo>15) //不能超过15
SubPageNo=0;

// uint32_t addr=BlockNo*0x10000; //先计算Block的起始地址
uint32_t addr=BlockNo;
addr=addr<<16; //先计算Block的起始地址

// uint32_t offset=SubSectorNo*0x1000; //计算Sector的偏移地址
uint32_t offset=SubSectorNo; //计算Sector的偏移地址
offset=offset<<12; //计算Sector的偏移地址
addr += offset;

// offset=SubPageNo*0x100; //计算Page的偏移地址
offset=SubPageNo;
offset=offset<<8; //计算Page的偏移地址

addr += offset; //Page的起始地址
return addr;
}

//将24位地址分解为3个字节
//globalAddr是全局24位地址, 返回 addrHigh高字节,addrMid中间字节,addrLow低字节
void Flash_SpliteAddr(uint32_t globalAddr, uint8_t* addrHigh, uint8_t* addrMid,uint8_t* addrLow)
{
*addrHigh= (globalAddr>>16); //addrHigh=高字节

globalAddr =globalAddr & 0x0000FFFF; //屏蔽高字节
*addrMid= (globalAddr>>8); //addrMid=中间字节

*addrLow =globalAddr & 0x000000FF; //屏蔽中间字节, 只剩低字节,addrLow=低字节
}


//读取芯片ID
//返回值如下:
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

//读取芯片的制造商和器件ID,高字节是Manufacturer ID,低字节是Device ID
uint16_t Flash_ReadID(void)
{
uint16_t Temp = 0;
__Select_Flash(); //CS=0

SPI_TransmitOneByte(0x90); //指令码,0x90=Manufacturer/Device ID
SPI_TransmitOneByte(0x00); //dummy
SPI_TransmitOneByte(0x00); //dummy
SPI_TransmitOneByte(0x00); //0x00
Temp =SPI_ReceiveOneByte()<<8; //Manufacturer ID
Temp|=SPI_ReceiveOneByte(); //Device ID, 与具体器件相关

__Deselect_Flash(); //CS=1

return Temp;
}

// 参数High32和Low32分别返回64位序列号的高32位和低32位的值
// 函数返回值为64位序列号的值
uint64_t Flash_ReadSerialNum(uint32_t* High32, uint32_t* Low32)//读取64位序列号,
{
uint8_t Temp = 0;
uint64_t SerialNum=0;
uint32_t High=0,Low=0;

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x4B); //发送指令码, 4B=read Unique ID
SPI_TransmitOneByte(0x00); //发送4个Dummy字节数据
SPI_TransmitOneByte(0x00);
SPI_TransmitOneByte(0x00);
SPI_TransmitOneByte(0x00);

for(uint8_t i=0; i<4; i++) //高32位
{
Temp =SPI_ReceiveOneByte();
High = (High<<8);
High = High | Temp; //按位或
}

for(uint8_t i=0; i<4; i++) //低32位
{
Temp =SPI_ReceiveOneByte();
Low = (Low<<8);
Low = Low | Temp; //按位或
}
__Deselect_Flash(); //CS=1

*High32 = High;
*Low32=Low;

SerialNum = High;
SerialNum = SerialNum<<32; //高32位
SerialNum=SerialNum | Low;

return SerialNum;
}


//在任意地址读取一个字节的数据,返回读取的字节数据
// globalAddr是24位全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr)
{
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x03); //Command=0x03, read data
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
byte2 = SPI_ReceiveOneByte(); //接收1个字节
__Deselect_Flash(); //CS=1

return byte2;
}


//从任何地址开始读取指定长度的数据
//globalAddr:开始读取的地址(24bit), pBuffer:数据存储区指针,byteCount:要读取的字节数
void Flash_ReadBytes(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x03); //Command=0x03, read data
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
SPI_ReceiveBytes(pBuffer, byteCount); //接收byteCount个字节数据
__Deselect_Flash(); //CS=1
}

//Command=0x0B, 高速连续读取flash多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{
// uint16_t i;
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

__Select_Flash(); //CS=0

SPI_TransmitOneByte(0x0B); //Command=0x0B, fast read data
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
SPI_TransmitOneByte(0x00); //Dummy字节

SPI_ReceiveBytes(pBuffer, byteCount); //接收byteCount个字节数据
__Deselect_Flash(); //CS=1

}

//Command=0xC7: Chip Erase, 擦除整个器件
// 擦除后,所有存储区内容为0xFF,耗时大约25秒
void Flash_EraseChip(void)
{
Flash_Write_Enable(); //使 WEL=1
Flash_Wait_Busy(); //等待空闲

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0xC7); // Command=0xC7: Chip Erase, 擦除整个器件
__Deselect_Flash(); //CS=1

Flash_Wait_Busy(); //等待芯片擦除结束,大约25秒
}

// Command=0x02: Page program, 对一个页(256字节)编程, 耗时大约3ms,
// globalAddr是写入初始地址,全局地址
// pBuffer是要写入数据缓冲区指针,byteCount是需要写入的数据字节数
// 写入的Page必须是前面已经擦除过的,如果写入地址超出了page的边界,就从Page的开头重新写
void Flash_WriteInPage(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

Flash_Write_Enable(); //SET WEL
Flash_Wait_Busy();

__Select_Flash(); //CS=0
SPI_TransmitOneByte(0x02); //Command=0x02: Page program 对一个扇区编程
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);
SPI_TransmitBytes(pBuffer, byteCount); //发送byteCount个字节的数据
// for(uint16_t i=0; i<byteCount; i++)
// {
// byte2=pBuffer[i];
// SPI_WriteOneByte(byte2); //要写入的数据
// }
__Deselect_Flash(); //CS=1

Flash_Wait_Busy(); //耗时大约3ms
}

//从某个Sector的起始位置开始写数据,数据可能跨越多个Page,甚至跨越Sector,不必提前擦除
// globalAddr是写入初始地址,全局地址,是扇区的起始地址,
// pBuffer是要写入数据缓冲区指针
// byteCount是需要写入的数据字节数,byteCount不能超过64K,也就是一个Block(16个扇区)的大小,但是可以超过一个Sector(4K字节)
// 如果数据超过一个Page,自动分成多个Page,调用EN25Q_WriteInPage分别写入
void Flash_WriteSector(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{
//需要先擦除扇区,可能是重复写文件
uint8_t secCount= (byteCount / FLASH_SECTOR_SIZE); //数据覆盖的扇区个数
if ((byteCount % FLASH_SECTOR_SIZE) >0)
secCount++;

uint32_t startAddr=globalAddr;
for (uint8_t k=0; k<secCount; k++)
{
Flash_EraseSector(startAddr); //擦除扇区
startAddr += FLASH_SECTOR_SIZE; //移到下一个扇区
}

//分成Page写入数据,写入数据的最小单位是Page
uint16_t leftBytes=byteCount % FLASH_PAGE_SIZE; //非整数个Page剩余的字节数,即最后一个Page写入的数据
uint16_t pgCount=byteCount/FLASH_PAGE_SIZE; //前面整数个Page
uint8_t* buff=pBuffer;
for(uint16_t i=0; i<pgCount; i++) //写入前面pgCount个Page的数据,
{
Flash_WriteInPage(globalAddr, buff, FLASH_PAGE_SIZE); //写一整个Page的数据
globalAddr += FLASH_PAGE_SIZE; //地址移动一个Page
buff += FLASH_PAGE_SIZE; //数据指针移动一个Page大小
}

if (leftBytes>0)
Flash_WriteInPage(globalAddr, buff, leftBytes); //最后一个Page,不是一整个Page的数据
}

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址
//清除后存储区内容全部为0xFF, 耗时大概150ms
void Flash_EraseBlock64K(uint32_t globalAddr)
{
Flash_Write_Enable(); //SET WEL
Flash_Wait_Busy();

uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

__Select_Flash(); //CS=0

SPI_TransmitOneByte(0xD8); //Command=0xD8, Block Erase(64KB)
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);

__Deselect_Flash(); //CS=1
Flash_Wait_Busy(); //耗时大概150ms
}


//擦除一个扇区(4KB字节),Command=0x20, Sector Erase(4KB)
//globalAddr: 扇区的绝对地址,24位地址0x00XXXXXX
//擦除后,扇区内全部内容为0xFF, 耗时大约30ms,
void Flash_EraseSector(uint32_t globalAddr)
{
Flash_Write_Enable(); //SET WEL
Flash_Wait_Busy();
uint8_t byte2, byte3, byte4;
Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4); //24位地址分解为3个字节

__Select_Flash(); //CS=0

SPI_TransmitOneByte(0x20); //Command=0x20, Sector Erase(4KB)
SPI_TransmitOneByte(byte2); //发送24位地址
SPI_TransmitOneByte(byte3);
SPI_TransmitOneByte(byte4);

__Deselect_Flash(); //CS=1
Flash_Wait_Busy(); //大约30ms
}

//检查寄存器SR1的BUSY位,直到BUSY位为0
uint32_t Flash_Wait_Busy(void)
{
uint8_t SR1=0;
uint32_t delay=0;
SR1=Flash_ReadSR1(); //读取状态寄存器SR1
while((SR1 & 0x01)==0x01)
{
HAL_Delay(1); //延时1ms
delay++;
SR1=Flash_ReadSR1(); //读取状态寄存器SR1
}
return delay;
}

//进入掉电模式
//Command=0xB9: Power Down
void Flash_PowerDown(void)
{
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0xB9); //Command=0xB9: Power Down
__Deselect_Flash(); //CS=1
HAL_Delay(1); //等待TPD
}

//唤醒
//Command=0xAB: Release Power Down
void Flash_WakeUp(void)
{
__Select_Flash(); //CS=0
SPI_TransmitOneByte(0xAB); //Command=0xAB: Release Power Down
__Deselect_Flash(); //CS=1
HAL_Delay(1); //等待TRES1
}

w25flash.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/* 文件: w25flash.h
* 功能描述: Flash 存储器W25Q128的驱动程序
* 作者:王维波
* 修改日期:2019-06-05
* W25Q128 芯片参数: 16M字节,24位地址线
* 分为256个Block,每个Block 64K字节
* 一个Block又分为16个Sector,共4096个Sector,每个Sector 4K字节
* 一个Sector又分为16个Page,共65536个Page,每个Page 256字节
* 写数据操作的基本单元是Page,一次连续写入操作不能超过一个Page的范围。写的Page必须是擦除过的。
*/

#ifndef _W25FLASH_H
#define _W25FLASH_H

#include "stm32f4xx_hal.h"
#include "spi.h" //使用其中的变量 hspi1,表示SPI1接口

/* W25Q128硬件接口相关的部分:CS引脚和SPI接口 ,若电路不同,更改这部分配置即可 */
// Flash_CS -->PB14, 片选信号CS操作的宏定义函数
#define CS_PORT GPIOB
#define CS_PIN GPIO_PIN_14
#define SPI_HANDLE hspi1 //SPI接口对象,使用spi.h中的变量 hspi1

#define __Select_Flash() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET) //CS=0
#define __Deselect_Flash() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET) //CS=1

//===========Flash存储芯片W25Q128的存储容量参数================
#define FLASH_PAGE_SIZE 256 //一个Page是256字节

#define FLASH_SECTOR_SIZE 4096 //一个Sector是4096字节

#define FLASH_SECTOR_COUNT 4096 //总共4096个 Sector

//=======1. SPI 基本发送和接收函数,阻塞式传输============
HAL_StatusTypeDef SPI_TransmitOneByte(uint8_t byteData); //SPI接口发送一个字节
HAL_StatusTypeDef SPI_TransmitBytes(uint8_t* pBuffer, uint16_t byteCount); //SPI接口发送多个字节

uint8_t SPI_ReceiveOneByte(void); //SPI接口接收一个字节
HAL_StatusTypeDef SPI_ReceiveBytes(uint8_t* pBuffer, uint16_t byteCount); //SPI接口接收多个字节

//=========2. W25Qxx 基本控制指令==========
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

uint16_t Flash_ReadID(void); // Command=0x90, Manufacturer/Device ID

uint64_t Flash_ReadSerialNum(uint32_t* High32, uint32_t* Low32); //Command=0x4B, Read Unique ID, 64-bit

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void); //Command=0x50: Write Volatile Enable

HAL_StatusTypeDef Flash_Write_Enable(void); //Command=0x06: Write Enable, 使WEL=1
HAL_StatusTypeDef Flash_Write_Disable(void); //Command=0x04, Write Disable, 使WEL=0

uint8_t Flash_ReadSR1(void); //Command=0x05: Read Status Register-1, 返回寄存器SR1的值
uint8_t Flash_ReadSR2(void); //Command=0x35: Read Status Register-2, 返回寄存器SR2的值

void Flash_WriteSR1(uint8_t SR1); //Command=0x01: Write Status Register, 只写SR1的值,禁止写状态寄存器

uint32_t Flash_Wait_Busy(void); //读状态寄存器SR1,等待BUSY变为0,返回值是等待时间
void Flash_PowerDown(void); //Command=0xB9: Power Down
void Flash_WakeUp(void); //Command=0xAB: Release Power Down

//========3. 计算地址的辅助功能函数========
//根据Block 绝对编号获取地址,共256个Block
uint32_t Flash_Addr_byBlock(uint8_t BlockNo);
//根据Sector 绝对编号获取地址,共4096个Sector
uint32_t Flash_Addr_bySector(uint16_t SectorNo);
//根据Page 绝对编号获取地址,共65536个Page
uint32_t Flash_Addr_byPage(uint16_t PageNo);

//根据Block编号,和内部Sector编号计算地址,一个Block有16个Sector,
uint32_t Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo);
//根据Block编号,内部Sector编号,内部Page编号计算地址
uint32_t Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo, uint8_t SubPageNo);
//将24位地址分解为3个字节
void Flash_SpliteAddr(uint32_t globalAddr, uint8_t* addrHigh, uint8_t* addrMid,uint8_t* addrLow);

//=======4. chip、Block,Sector擦除函数============
//Command=0xC7: Chip Erase, 擦除整个器件,大约25秒
void Flash_EraseChip(void);

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址,耗时大约150ms
void Flash_EraseBlock64K(uint32_t globalAddr);

//Command=0x20: Sector Erase(4KB) 扇区擦除, globalAddr是扇区的全局地址,耗时大约30ms
void Flash_EraseSector(uint32_t globalAddr);

//=========5. 数据读写函数=============
//Command=0x03, 读取一个字节,任意全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr);

//Command=0x03, 连续读取多个字节,任意全局地址
void Flash_ReadBytes(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount);

//Command=0x0B, 高速连续读取多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount);

//Command=0x02: Page program 对一个Page写入数据(最多256字节), globalAddr是初始位置的全局地址,耗时大约3ms
void Flash_WriteInPage(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount);

//从某个Sector的起始地址开始写数据,数据可能跨越多个Page,甚至跨越Sector,总字节数byteCount不能超过64K,也就是一个Block的大小
void Flash_WriteSector(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount);

#endif

向工程中添加.c/.h文件的步骤请阅读“STM32CubeMX教程19 I2C - MPU6050驱动”实验3.2.3小节

在主函数中添加操作提示信息和按键操作逻辑程序,具体如下图所示

源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*主函数主循环外代码*/
uint16_t ID = Flash_ReadID();
printf("W25Q128 ID:0x%x\r\n",ID);
printf("---------------------\r\n");
printf("KEY2: Flash_Write\r\n");
printf("KEY1: Flash_Read\r\n");
printf("KEY0: Flash_Erase\r\n");
printf("---------------------\r\n");

/*主函数主循环内代码*/
/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
Flash_TestWrite();
while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
}
}
/*按键KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
Flash_TestRead();
while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
}
}
/*按键KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
printf("---------------------\r\n");
printf("Erasing Block 0(256 pages)...\r\n");
uint32_t globalAddr=0;
Flash_EraseBlock64K(globalAddr);
printf("Block 0 is erased.\r\n");
printf("---------------------\r\n");
while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
}
}

在spi.c中实现W25Q128的写入/读取测试函数Flash_TestWrite()/Flash_TestRead(),具体源代码如下所示 (注释4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*spi.c中包含的头文件*/
#include "w25flash.h"
#include "string.h"
#include "stdio.h"

/*spi.c中的函数定义*/
//测试写入Page0和Page1
//注意:一个Page写入之前必须是被擦除过的,写入之后就不能再重复写
void Flash_TestWrite(void)
{
uint8_t blobkNo = 0;
uint16_t sectorNo = 0;
uint16_t pageNo = 0;
uint32_t memAddress = 0;

printf("---------------------\r\n");
//写入Page0两个字符串
memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo, pageNo); //Page0的地址
uint8_t bufStr1[] = "Hello from beginning";
uint16_t len = 1 + strlen("Hello from beginning"); //包括结束符'\0'
Flash_WriteInPage(memAddress, bufStr1, len); //在Page0的起始位置写入数据
printf("Write in Page0:0\r\n%s\r\n", bufStr1);

uint8_t bufStr2[]="Hello in page";
len = 1 + strlen("Hello in page"); //包括结束符'\0'
Flash_WriteInPage(memAddress+100, bufStr2, len); //Page0内偏移100
printf("Write in Page0:100\r\n%s\r\n", bufStr2);

//写入Page1中0-255数字
uint8_t bufPage[FLASH_PAGE_SIZE]; //EN25Q_PAGE_SIZE=256
for (uint16_t i=0;i<FLASH_PAGE_SIZE;i++)
bufPage[i] = i; //准备数据
pageNo = 1; //Page 1
memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo, pageNo); //page1的地址
Flash_WriteInPage(memAddress, bufPage, FLASH_PAGE_SIZE); //写一个Page
printf("Write 0-255 in Page1\r\n");
printf("---------------------\r\n");
}

//测试读取Page0 和 Page1的内容
void Flash_TestRead(void)
{
uint8_t blobkNo=0;
uint16_t sectorNo=0;
uint16_t pageNo=0;

printf("---------------------\r\n");
//读取Page0
uint8_t bufStr[50]; //Page0读出的数据
uint32_t memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);
Flash_ReadBytes(memAddress, bufStr, 50); //读取50个字符
printf("Read from Page0:0\r\n%s\r\n",bufStr);

Flash_ReadBytes(memAddress+100, bufStr, 50); //地址偏移100后的50个字字节
printf("Read from Page0:100\r\n%s\r\n",bufStr);

//读取Page1
uint8_t randData = 0;
pageNo = 1;
memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);

randData = Flash_ReadOneByte(memAddress+12); //读取1个字节数据,页内地址偏移12
printf("Page1[12] = %d\r\n",randData);

randData = Flash_ReadOneByte(memAddress+136); //页内地址偏移136
printf("Page1[136] = %d\r\n",randData);

randData = Flash_ReadOneByte(memAddress+210); //页内地址偏移210
printf("Page1[210] = %d\r\n",randData);
printf("---------------------\r\n");
}

/*spi.h中的函数声明*/
void Flash_TestWrite(void);
void Flash_TestRead(void);

4、常用函数

1
2
3
4
5
/*SPI发送数据函数*/
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

/*SPI接收数据函数*/
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

5、烧录验证

烧录程序,开发板上电后首先读取FLASH芯片的ID,并通过串口显示给用户,然后输出操作提示,按下KEY0按键会擦除块0内容,擦除后按下KEY1按键读取内容会发现全是FF,然后按下KEY2按键将数据写入,此时再按下KEY1按键读取内容会发现和我们写入的内容一致,如下图所示为整个过程串口详细输出信息

6、注释详解

注释1:图片来源多路SPI从设备连接方法–技术天地

注释2图片来源STM32Cube高效开发教程(基础篇)

注释3W25Q128FV Datasheet

注释4:驱动代码来源STM32Cube高效开发教程(基础篇)

参考资料

STM32Cube高效开发教程(基础篇)


STM32CubeMX教程20 SPI - W25Q128驱动
https://lc-guo.github.io/2024/01/26/STM32CubeMX教程20-SPI-W25Q128驱动/
作者
OSnotes
发布于
2024年1月26日
许可协议