STM32外挂FLASH模拟U盘(基于HAL库)
1、背景
1.1这篇文章能给你带来什么
这篇文章是我从0开始做这个的完整记录,我会将我做这个模拟U盘的整个过程记录下来,包括在做这个的过程中遇到的问题、怎么解决这些问题的、原因是什么、怎么去查找资料解决的以及我最后完成之后的源码包(不准吐槽我代码写的烂,谁吐槽我就骂谁哈哈哈)。方便新手和在做这个问题的过程中遇到问题的小伙伴能够找到解决问题的方式。这篇文章也能帮助你学习SPI读写操作W25QXX系列(如W25Q80、W25Q16、W25Q32、W25Q64、W25Q128、W25Q512等)FLASH。
1.2根据你要解决的问题,精确快速跳转到相应位置
如果你是新手啥也不懂,那你就老老实实从头到尾除了背景全部看完吧,不然我也帮不了你。
如果你是知道你要做啥,但是不知道咋做,也不知道具体流程啥的,你就直接跳转到制作模拟U盘的具体操作步骤看具体操作,然后根据我制作U盘的流程弄出来后发现有问题,再去遇到的问题及解决方案查看相应的处理方式,结果发现按照我的配置弄不出来或者是弄出来了还是不明白,那就去相关的知识储备查看相应的文章学习去吧。
如果你是在CSDN看了很多其他小伙伴写的制作模拟U盘的博文,但是还是没看懂,你可以针对性的跳转到相关的知识储备、制作模拟U盘的具体操作步骤或遇到的问题及解决方案查看相应的处理方式,以及还有啥不懂的,重新返回顶部目录看我都写了啥
如果你不想学习,你只是需要这个模块,然后尽快把这个东西弄出来,然后又想白嫖我,那你就去这个地方吧(写这个不易啊小哥哥,5毛钱给买个棒棒糖嘛)
1.3我在做完这个后还有不明白的地方,希望能有大触解答困惑
我看了很多这个关于STM32堆栈的说明的博客,居然我都看不懂哈哈哈。不过这里我不得不吐槽下,这地方就是有很多人就是把别人的东西搬过来搬过去,然后自己又没理解,很多关键的点没说清楚,很浪费别人的学(白)习(嫖)时间。
言归正传,1、我目前对这个STM32CUBEMX里的堆和栈的设置原理不清楚,看了一些文章说是什么中断异常向量啥的、数据变量啥的,但是,我没看懂/2、我对STM32模拟U盘的具体原理并不懂,现在只会配置把代码生成出来。有懂的大佬在评论区解释下可好,这个设置的依据是啥,模拟U盘的详细原理是什么。
2、相关的知识储备
2.1、FLASH芯片的相关知识
我目前测试使用的FLASH芯片是华邦的W25Qxx系列芯片(W25Q80、W25Q16、W25Q32、W25Q64、W25Q128),W25Q256和W25Q512的内存地址比其他的多1个字节。没有测试过的我不能保证用这种方式配置能用,但是这不影响FLASH的知识点说明。简单一句话总结就是,这些芯片的区别仅仅在于FLASH芯片内的块儿数量不同,也就是以下第一点不同,其他的性质都是一致的。W25Q256和W25Q512的内存地址要多一个字节,其他的地址是三个字节,这两个芯片地址是4个字节,需要注意。
Point1–FLASH的块儿、扇区、页、字节之间的关系
这几个概念打个比方比较好解释,以用的比较多的W25Q16为例就是这个FLASH芯片就像是一个学校(FLASH芯片W25Q16),这个学校里有小学到高中32年级(这个芯片有32块儿),每个年级又有16个班级(1个块有16个扇区),每个班级有16个学生(1个扇区有16页),每个学生有256本书(1页有256字节);有一天我要去借某本书,但是这本书仅仅只有学校的一个同学有,那我就只能按照这个学生的年级、班级、这个学生姓名这三项(这个flash芯片的内存地址)去找到这个学生,然后借用他的书籍(操作芯片指定这一页的其中一个数据),但是有一年开学的时候,有一个班级的语文书发错了,发成数学书了,然后我们就需要把这个班级的书都收回来(按照扇区为单位擦除),收回来之后发现这个年级的书都发错了,那我们要把整个年级的书都收回来(按照块儿为单位擦除),都收回来之后发现书是《论如何优雅的装逼》,不是我们这学期要用的语文书,这时候我们要把整个学校发的这本《论如何优雅的装逼》都收回来(全片擦除),都收回来后,问题得到解决。总结一下就是下面的描述(一下数据均来自相应的数据手册总结)
- 特性1
- W25Q80有16个块儿,共16*65536 = 1048576 Byte;1048576/1024/1024 = 1MB,寻址空间:0x000000~0x0FFFFF;
- W25Q16有32个块儿,共32*65536 = 2097152 Byte;2097152/1024/1024 = 2MB,寻址空间:0x000000~0x1FFFFF;
- W25Q32有64个块儿,共64*65536 = 4194304 Byte;4194304/1024/1024 = 4MB,寻址空间:0x000000~0x3FFFFF;
- W25Q64有128个块儿,共128*65536 = 8388608 Byte;8388608/1024/1024 = 8MB,寻址空间:0x000000~0x7FFFFF;
- W25Q128有256块儿,共256*65536 = 16777216 Byte;1677216/1024/1024 = 16MB,寻址空间:0x000000~0xFFFFFF;
- W25Q256有512块儿,共512*65536 = 33554432 Byte;33554432/1024/1024 = 32MB,寻址空间:0x00000000~0x01FFFFFF;
- W25Q512有1024块儿,共1024*65536 = 67108864 Byte;67108864/1024/1024 = 64MB,寻址空间:0x00000000~0x03FFFFFF;
- 数据出处:以W25Q16为例,参考数据手册“W25Q16JV”的Publication Release Date: May 09, 2017 Revision F版本,第8页,5. BLOCK DIAGRAM–>Figure 2. W25Q16JV Serial Flash Memory Block Diagram,其他芯片同理。
- 特性2
- 1块儿 = 16扇区
- 1块儿 = 1616256字节(Byte)= 65536Byte = 64KB(65536Byte/1024=64KB)
- 1扇区 = 16页
- 1扇区 = 16*256(Byte)= 4096Byte = 4KB
- 1页 = 256字节
- FLASH芯片只能按扇区、块为单位擦除,或者是全片擦除。写可以1~256字节写,一次最多写256字节
- 1块儿 = 16扇区
- 特性3
- W25Q80的芯片ID为:0XEF13
- W25Q16 的芯片ID为:0XEF14
- W25Q32 的芯片ID为:0XEF15
- W25Q64 的芯片ID为:0XEF16
- W25Q128的芯片ID为:0XEF17
- W25Q256的芯片ID为:0XEF18
- W25Q512的芯片ID为:0XEF19
- 数据出处:以W25Q16为例,参考数据手册“W25Q16JV”的Publication Release Date: May 09, 2017 Revision F版本,第19页,8.1 Device ID and Instruction Set Tables–>8.1.1Manufacturer and Device Identification的表格,其他芯片同理。
2.2、SPI通信
关于这部分的功能,这里引用这位博主的博文,个人感觉写的挺好的。他写了一篇【STM32】HAL库 STM32CubeMX教程十四—SPI他在这篇文章中把这个SPI通信原理这些基本讲清楚了,大家可以去拜读下,会有收获的。
2.3、STM32模拟U盘原理
我在做这个开始是看了一位博主的博文stm32USB之模拟U盘,虽然他实现了功能,但是我感觉他就是搬过来的,那时候自己理解的也不是很深。他是引用了这位博主的博文STM32HAL----USB串行FLASH模拟U盘,至于模拟U盘的这些具体原理啥的,我也还要去学习,我现在也不懂。
【挖坑】嗨呀,这部分我也还在学习,我就不来误人子弟了,自己学习去吧嘿嘿嘿,等我神功练成我再来把这里补充完整。
3、外挂FLASH有什么作用
其实我只是来学习,做着玩的,现在想到,后面可以用这个来存储一些配置文件信息的数据,或者回头弄个SD卡模拟U盘+IIS做一个MP3耍一耍。其他的什么功能相比各位在做这个之前就有自己的想法了。
4、制作模拟U盘的流程和难点分析
4.1制作模拟U盘的流程
-
步骤1–配置STM32CUBEMX基本工程
- 用STM32CUBEMX配置堆栈、配置烧录方式、配置系统时钟、配置SPI、配置USB_OTG_FS、配置USB_DEVICE,及其他选择配置的串口和IO口等;配置完成后生成代码。
-
步骤2–在Keil里写SPI通信方式驱动W25Qxx的驱动程序
- 新增W25Qxx.c和W25Qxx.h文件,加入到项目中去,我们便可以在主函数或其他位置调用操作FLASH的函数。
-
步骤3–修改“usbd_storage_if.c”函数
- 修改这个函数是电脑能否将STM32识别为U盘的关键,这里边的下面这两个值设置会影响识别后的U盘大小(以25Q16为例)
- #define STORAGE_BLK_NBR 512 //W25Q16有32*16=512个扇区
- #define STORAGE_BLK_SIZ 4096 //每个扇区有4096个字节
- 另外就是需要将W25Q16的读写操作函数放到这个函数下来,如下图所示
- 修改这个函数是电脑能否将STM32识别为U盘的关键,这里边的下面这两个值设置会影响识别后的U盘大小(以25Q16为例)
-
步骤4–选择性修改“startup_stm32f407xx.s”函数
-
这一步不是必须的,如果在STM32CUBEMX中配置好了堆栈,这里就不用修改了。不过要提醒一句,堆栈大小的设置,不合适的话会导致PC无法识别到模拟U盘。
-
在STM32CUBEMX中设置堆栈位置是下图所示
-
在程序“startup_stm32f407xx.s”函数代码中修改堆栈的方式如下图所示
-
以上两种修改堆栈的方式,任意选一种配置好了就行,但是在程序“startup_stm32f407xx.s”函数代码中修改堆栈的方式,如果在stm32cubemx中调整了其他配置重新生成了代码,在这里设置的堆栈大小就会被新的值覆盖掉。
-
- 步骤5–编译、烧录代码,插上PC测试效果
- 在第一次烧录代码后,会提示格式化U盘,格式化过后再插上这个就不会提示格式化了。看完效果我们又可以回到之前的位置了
- 步骤6–调试遇到问题,处理问题
- 这一步是必须的,也是非常重要的,我看了一些其他博主写的,直接来了就直接搞完,然后就通了,但是实际情况是我们总会遇到各种各样的问题,这里如果你也是遇到了问题,去参考遇到的问题及解决方案解决,我相信目前我列出来的这些问题,一般是能解决你遇到的问题的。
4.2难点1:全程懵逼,不知道如何下手
这中情况呢建议你就直接跳转到制作模拟U盘的具体操作步骤看具体操作,然后根据我制作U盘的流程弄出来后发现有问题,再去遇到的问题及解决方案查看相应的处理方式,结果发现按照我的配置弄不出来或者是弄出来了还是不明白,那就去相关的知识储备查看相应的文章学习去吧。
4.3难点2:不知道该如何写W25QXX驱动程序
这种情况一般是去CSDN搜SPI读写W25QXX,然后去白嫖一个程序来。这里我就不推荐了,自己去百度这上面一大堆,不过筛选起来有点麻烦,大多数是看了也懵逼的很,回头我自己来写一篇。
4.4难点3:不知道工程配置完后该怎么写代码,导致PC无法识别U盘
不知道工程配置完后该怎么写代码的话,就去制作模拟U盘的具体操作步骤看具体操作,原理我会在解决问题的地方说清楚。
5、制作模拟U盘的具体操作步骤
5.1确定开发环境
- 开发软件和硬件
- PC:Windows 10 专业版 21H1
- KEIL:KEIL:V5.36
- STM32CUBEMX:V6.1.2
- STM32F4固件包:STM32Cube_FW_F4_V1.25.2
- 单片机型号:STM32F407VET6(某宝热销的STM32F4核心板)
- 下载工具:JTAGV9
- FLASH芯片:以W25Q16为例(这个操作步骤适用W25Q80、W25Q16、W25Q32、W25Q64、W25Q128、W25Q256、W25Q512),W25Q256和W25Q512需要修改操作内存的地址,前面的芯片地址是3个字节,这两个芯片是4个字节
5.2配置STM32CUBEMX基本工程
5.2.1打开STM32CUBEMX,选择对应的芯片型号
5.2.2切换到“Project Manager”配置文件名/文件储存位置,开发的IDE和IDE版本以及堆栈的设置。
这里说明下,可以在这里设置堆栈,也可以在IDE里修改程序“startup_stm32f407xx.s”函数代码中修改堆栈,详细见这里,网上很多人说这里堆应该设置为0x1200,理论上这里设置为0x1000=4096(一个扇区就是4096字节)就好了,但是程序里还有一些其他的变量啥的,所以设置成0x1200是没毛病的,但是,如果你的程序较大,里边定义了较大的数组,这个对的值就需要酌情增大了,目前我测试0x1100是最小值,再小PC就不识别U盘了。另外就是这个堆的值,必须是100的倍数,具体原因我还不清楚。栈的值设置为0x400没问题,先这样用。
5.2.3切换到“PINout&configuration”配置SYS下载调试口
这里需要注意,我们用Jlink或是JTAG下载程序,这样配置用串口通信下载调试的都没问题的,但是必须要配置这里,很多人第一次能下载程序,下载完后第二次不能下载程序就是因为这里选择了disable导致Jlink和JTAG都没法用了。
5.2.4配置SPI、USB_OTG_FS和USB_DEVICE
这里因为USB设备对时钟的要求比较特殊,先配置时钟的话后面配置完USB_OTG_FS和USB_DEVICE后又需要再来配置一遍时钟,所以这里把配置时钟放到后面。这里SPI配置因为STM32F407VET6使用的引脚是PB3/PB4/PB5,所以这里需要注意,然后CS脚是PB0。
配置USB_OTG_FS,如果使用USB_OTG_HS,操作会快一些,后面再来研究。
配置USB_DEVICE,这里需要将MSC_MEDIA_PACKET由原来的512字节修改为4096字节,这里如果是没有修改为4096字节,那么在扇区操作的时候,可能会出现意料之外的问题。
5.2.5配置时钟
我们把前面的都配置好了之后再来配置时钟,就省去了一些麻烦事儿,个人感觉这还挺有用的。
因为我们是把USB_OTG_FS和USB_DEVICE配置好了再来配置的时钟,这里的48MHz就不会是其他值,如果是先配置的时钟,这里这个48MHz可能就是其他值,就需要我们自己修改到48MHz了。这一点要注意,这里必须是48MHz。
5.2.6(选配)配置的串口和IO口
这里我们一般不会一次性就可以使用,所以我配置了串口和IO口来协助调试,如果你觉得没必要可以不配置。串口配置基本是默认参数,波特率115200,数据位8位,无校验,停止位1位。
我还配置了板子上的两个LED灯来协助我调试,PA6和PA7是两个LED指示灯,PB0是SPI的CS引脚。
5.2.7生成代码
在上面的配置完成后,我们就可以生成代码了。
5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序
keil里写驱动程序主要就是针对W25QXX的驱动程序,下面的这两个程序都是直接可以使用的,只需要你在keil里新建好这两个文件,然后把代码复制过去就好了。不过需要注意的是,我使用了printf函数,所以需要在"usart.c"函数里包含下面这段代码,这段代码的含义是重定向串口输出,如果你的不是串口1,只需要修改这句话就可以了“HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);”;另外需要注意的是,下面这段代码中的这个#include “stdio.h"需要放在“usart.h”中,否则在其他地方使用printf函数的话会报警告,虽然是警告,但是我个人感觉不爽。
这里开始代码太长了翻来翻去不爽,我做了一个跳转,
点这里坐火车到“W25QXX.C”;
点这里做高铁到“W25QXX.H";
点这里坐电梯到“W25QXX.H"的结尾;
点这里直达 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序;
#include "stdio.h"
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
这里我们继续做好跳转按钮
点这里坐火车到“W25QXX.C";
点这里做高铁到“W25QXX.H";
点这里坐电梯到“W25QXX.H"的结尾;
点这里直达 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序;
W25QXX.C
#include "W25QXX.h"
uint32_t FLASH_SIZE=0; //FLASH有多少字节,根据不同的FLASH芯片来计算得到,单位Byte
uint8_t W25QXX_BUFFER[NumByteOfSector]; //定义一个扇区总共的字节数(4096字节)作为缓存数组
/*读取芯片ID
传入参数:无
返回参数:
0XEF13,表示芯片型号为W25Q80
0XEF14,表示芯片型号为W25Q16
0XEF15,表示芯片型号为W25Q32
0XEF16,表示芯片型号为W25Q64
0XEF17,表示芯片型号为W25Q128
0XEF18,表示芯片型号为W25Q256
*/
uint16_t W25QXX_ReadID(void)
{
uint16_t temp = 0;
uint8_t TX_cmd[6],RX_temp[6];
TX_cmd[0] = Manufacturer;
CS_Enable();
//发送指令和接收简单数据推荐使用查询模式
HAL_SPI_TransmitReceive(&hspi1,TX_cmd,RX_temp,6,10);//发送读取ID命令
temp=RX_temp[4]<<8 | RX_temp[5];
CS_Disable();
return temp; //返回FLASH的ID号
}
/*
初始化FLASH,包括读取FLASH 的ID
返回值:
*/
uint8_t W25QXX_Init(void)
{
uint16_t ReadFlah_Type; //定义实际读取的FLASH型号
uint8_t Flag = 0; //标记初始话flash状态;0--失败,1--成功
CS_Disable(); //SPI FLASH不选中
ReadFlah_Type=W25QXX_ReadID(); //读取FLASH的ID,确认FLASH型号
//printf("FLASH_ID:0x%x\r\n",ReadFlah_Type); //打印flash的芯片ID,测试用
switch(ReadFlah_Type)
{
case 0xEF13: //W25Q80
{
FLASH_SIZE = Num80BlockOfChip*NumByteOfBlock; //块*65536 =字节数
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q80\r\n",22,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num80BlockOfChip,Num80BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF14: //W25Q16
{
FLASH_SIZE = Num16BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q16\r\n",22,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num16BlockOfChip,Num16BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF15: //W25Q32
{
FLASH_SIZE = Num32BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q32\r\n",22,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num32BlockOfChip,Num32BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF16: //W25Q64
{
FLASH_SIZE = Num64BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q64\r\n",22,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num64BlockOfChip,Num64BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF17: //W25Q128
{
FLASH_SIZE = Num128BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q128\r\n",23,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num128BlockOfChip,Num128BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF18: //W25Q256
{
FLASH_SIZE = Num256BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q256\r\n",23,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num256BlockOfChip,Num256BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
case 0xEF19: //W25Q512
{
FLASH_SIZE = Num256BlockOfChip*NumByteOfBlock;
HAL_UART_Transmit(&huart1,(uint8_t *)&"Flash Mode is W25Q512\r\n",23,0xff);
//打印flash芯片信息
printf("FLASH_ID:0x%X Block:%d FLASH_SIZE:%dMB total:%d Bytes \r\n",ReadFlah_Type,Num512BlockOfChip,Num512BlockOfChip/16,FLASH_SIZE);
Flag = 1; //FLASH 初始化成功
break;
}
default:
{
HAL_UART_Transmit(&huart1,(uint8_t *)&"Other FLASH_ID,Flash init error\r\n",33,0xff);
Flag = 0; //FLASH 初始化失败
break;
}
}
return Flag;
}
//W25QXX写使能
//将WEL置位
void W25QXX_Write_Enable(void)
{
uint8_t cmd = Write_Enable;
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); //发送写使能
CS_Disable(); //取消片选
}
//W25QXX写禁止
//将WEL清零
void W25QXX_Write_Disable(void)
{
uint8_t cmd = Write_Disable;
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); //发送写禁止指令
CS_Disable(); //取消片选
}
//读取W25QXX的状态寄存器,W25QXX一共有3个状态寄存器
//状态寄存器1:
//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
//状态寄存器2:
//BIT7 6 5 4 3 2 1 0
//SUS CMP LB3 LB2 LB1 (R) QE SRP1
//状态寄存器3:
//BIT7 6 5 4 3 2 1 0
//HOLD/RST DRV1 DRV0 (R) (R) WPS ADP ADS
//regno:状态寄存器号,范:1~3
//返回值:状态寄存器值
uint8_t W25QXX_ReadSR(uint8_t regno)
{
uint8_t TX_cmd[2];
uint8_t RX_temp[2];
switch(regno)
{
case 1:
{
TX_cmd[0] = Read_Status_Register1; //读状态寄存器1指令
break;
}
case 2:
{
TX_cmd[0] = Read_Status_Register2; //读状态寄存器2指令
break;
}
case 3:
{
TX_cmd[0] = Read_Status_Register3; //读状态寄存器3指令
break;
}
default:
{
TX_cmd[0] = Read_Status_Register1;
break;
}
}
CS_Enable();
//Poll mode
//发送指令和接收简单数据推荐使用查询模式
HAL_SPI_TransmitReceive(&hspi1, TX_cmd, RX_temp, 2, 10);//发送读状态寄存器命令
CS_Disable(); //取消FLASH片选
return RX_temp[1];
}
//写W25QXX状态寄存器
void W25QXX_WriteSR(uint8_t regno,uint8_t sr)
{
uint8_t TX_cmd[2];
switch(regno)
{
case 1:
{
TX_cmd[0] = Write_Status_Register1; //写状态寄存器1指令
break;
}
case 2:
{
TX_cmd[0] = Write_Status_Register2; //写状态寄存器2指令
break;
}
case 3:
{
TX_cmd[0] = Write_Status_Register3; //写状态寄存器3指令
break;
}
default:
{
TX_cmd[0] = Write_Status_Register1;
break;
}
}
TX_cmd[1] = sr;
CS_Enable(); //使能器件
//Poll mode
//发送指令和接收简单数据推荐使用查询模式
HAL_SPI_Transmit(&hspi1, TX_cmd, 2, 10);//发送读状态寄存器命令
CS_Disable(); //取消片选
}
//等待空闲
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1)&0x01)==0x01); // 等待BUSY位清空
}
//进入掉电模式
void W25QXX_PowerDown(void)
{
uint8_t cmd = Power_down; //发送指令设置为掉电
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); //发送掉电命令
CS_Disable(); //取消片选
HAL_Delay(1); //等待TPD
}
//唤醒
void W25QXX_WAKEUP(void)
{
uint8_t cmd = Release_Power_down; //设置唤醒指令
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); //send W25X_PowerDown command 0xAB
CS_Disable(); //取消片选
HAL_Delay(1); //等待TRES1
}
//擦除一个扇区
//Dst_Addr:扇区地址 根据实际容量设置
//擦除一个扇区的最少时间:45ms - 400ms
void W25QXX_Erase_Sector(uint32_t Dst_Addr)
{
uint8_t cmd[5];
cmd[0] = Sector_Erase;
cmd[1] = (uint8_t)((Dst_Addr*NumByteOfSector)>>16);
cmd[2] = (uint8_t)((Dst_Addr*NumByteOfSector)>>8);
cmd[3] = (uint8_t)(Dst_Addr*NumByteOfSector);
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy(); //等待空闲
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, cmd, 4, 4000); //发送擦除扇区指令+地址
CS_Disable(); //取消片选
W25QXX_Wait_Busy(); //等待擦除完成
}
//擦除一个块
//Dst_Addr:块地址 根据实际容量设置
//擦除一个块的最少时间:150ms - 2000ms
void W25QXX_Erase_Block(uint32_t Dst_Addr)
{
uint8_t cmd[5];
cmd[0] = Block_Erase1;
cmd[1] = (uint8_t)((Dst_Addr*NumByteOfBlock)>>16);
cmd[2] = (uint8_t)((Dst_Addr*NumByteOfBlock)>>8);
cmd[3] = (uint8_t)(Dst_Addr*NumByteOfBlock);
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, cmd, 4, 20); //发送扇区擦除指令
CS_Disable(); //取消片选
W25QXX_Wait_Busy(); //等待擦除完成
}
void W25QXX_Erase_Chip(void)
{
uint8_t cmd = Chip_Erase;
W25QXX_Write_Enable(); //写使能
W25QXX_Wait_Busy(); //等待空闲
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000);//发送片擦除命令
CS_Disable(); //取消片选
//W25QXX_Wait_Busy(); //等待芯片擦除结束
}
//SPI方式读取FLASH
//在指定地址开始读取指定长度的数据
//pBuffer: 数据存储区
//ReadAddr: 开始读取的地址(24bit)
//NumByteToRead: 要读取的字节数(最大65535)
void W25QXX_Read(uint8_t* pBuffer, uint32_t ReadAddr,uint16_t NumByteToRead)
{
uint8_t cmd[5],cmd2[1];
cmd[0] = Read_Data;
cmd[1] = (uint8_t)((ReadAddr)>>16);
cmd[2] = (uint8_t)((ReadAddr)>>8);
cmd[3] = (uint8_t)(ReadAddr);
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1,cmd,4,20); //发送读取数据指令+读取指令的地址
HAL_SPI_TransmitReceive(&hspi1,cmd2,pBuffer,NumByteToRead,10*NumByteToRead); //发送读取数据指令,cmd2不够的数据会自动补0发送
CS_Disable();
}
//SPI方式写FLASH
//从指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer: 数据存储区
//WriteAddr: 开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint16_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t * W25QXX_BUF;
W25QXX_BUF = W25QXX_BUFFER;
secpos=WriteAddr/NumByteOfSector; //扇区地址
secoff=WriteAddr%NumByteOfSector; //在扇区内的偏移
secremain=NumByteOfSector-secoff; //扇区剩余空间大小
//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite<=secremain) //当需要写入的字节小于剩余空间数
{
secremain=NumByteToWrite; //设置剩余空间为需要写入的字节数
}
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*NumByteOfSector,NumByteOfSector); //读出整个扇区的内容,
for(i=0;i<secremain;i++) //校验数据
{
if(W25QXX_BUF[secoff+i]!=0XFF)
{
break; //需要擦除
}
}
if(i<secremain)//需要擦除
{
W25QXX_Erase_Sector(secpos); //擦除这个扇区
for(i=0;i<secremain;i++) //复制
{
W25QXX_BUF[i+secoff]=pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*NumByteOfSector,NumByteOfSector);//写入整个扇区
}
else
{
W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.
}
if(NumByteToWrite==secremain)
{
printf("Write Finished!!\r\n");
break;//写入结束了
}
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumByteToWrite-=secremain; //字节数递减
if(NumByteToWrite>NumByteOfSector)
{
secremain=NumByteOfSector; //下一个扇区还是写不完
}
else
{
secremain=NumByteToWrite; //下一个扇区可以写完了
}
}
}
}
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint8_t cmd[5];
cmd[0] = Page_Program;
cmd[1] = (uint8_t)((WriteAddr)>>16);
cmd[2] = (uint8_t)((WriteAddr)>>8);
cmd[3] = (uint8_t)(WriteAddr);
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
CS_Enable(); //使能器件
HAL_SPI_Transmit(&hspi1, cmd, 4, 20); //发送写页命令
HAL_SPI_Transmit(&hspi1, pBuffer, NumByteToWrite, 4000); //发送要写入的数据指令+地址
CS_Disable(); //取消片选
W25QXX_Wait_Busy(); //等待写入结束
}
//无检验写SPI FLASH
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint16_t pageremain;
uint16_t NumByteToWriteNow;
pageremain = NumByteOfPage - WriteAddr % NumByteOfPage; //单页剩余的字节数
NumByteToWriteNow = NumByteToWrite;
if(NumByteToWrite <= pageremain)
{
pageremain = NumByteToWriteNow;//不大于256个字节
}
while(1)
{
W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWriteNow==pageremain)
{
//printf("Write_No_CHECK Finished!!\r\n"); //测试用
break;//写入结束了
}
else //NumByteToWrite>pageremain
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWriteNow -= pageremain; //减去已经写入了的字节数
if(NumByteToWriteNow > NumByteOfPage)
{
pageremain=NumByteOfPage; //一次可以写入256个字节
}
else
{
pageremain=NumByteToWriteNow; //不够256个字节了
}
}
};
}
这里我们继续做好跳转按钮
点这里坐火车到“W25QXX.C";
点这里做高铁到“W25QXX.H";
点这里坐电梯到“W25QXX.H"的结尾;
点这里直达 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序;
W25QXX.H
/*
程序是我理解数据手册和学习了大佬写的程序后自己造的轮子,如果觉得好用可以多多移植使用
功能:用于SPI方式读写华邦的W25QXX(W25Q80、W25Q16、W25Q32、W25Q64、W25Q128...)系列FLASH的数据。
开发环境:
KEIL版本:V5.21a
CUBEMX版本:V6.2.1
CUBEMX针对STM32F407VET6的库版本:STM32Cube_FW_F4_V1.26.2
系统版本:Win10
FLASH型号:W25Q16
STM32的SPI口:SPI1(CLK--PB3,MISO--PB4,MOSI--PB5,CS--PB0),如果需要移植,注意这几个IO口。
W25QXX指令来源:华邦发布于2017年5月9日的数据手册“W25Q16JV”,Revision F版本
其他flash信息: 华邦发布于2015年10月2日的数据手册“W25Q80DV/DL”,Revision H版本
华邦发布于2017年5月9日的数据手册“W25Q16JV”,Revision F版本
华邦发布于2017年5月11日的数据手册“W25Q32JV”,Revision F版本
华邦发布于2017年5月11日的数据手册“W25Q64JV”,Revision H版本
华邦发布于2016年11月16日的数据手册“W25Q128JV”,Revision C版本
华邦发布于2017年8月3日的数据手册“W25Q256JV”,Revision G版本
华邦发布于2019年6月25日的数据手册“W25Q512JV”,Revision B版本
*/
#ifndef __W25QXX_H
#define __W25QXX_H
//说明:这里没有直接引入main.h头文件,是因为我的函数可能和你们的设置不一样,直接使用main.h会很省事,但是可移植性不好。如果你
//移植这个程序的话,可以把下面的几个头文件替换为你的main.h,方便省事。
#include "stdint.h" //包含这个函数是为了能够使用uint8_t、uint16_t等类似的数据类型定义,不包含自己定义也可以
#include "gpio.h" //包含这个是为了引入定义的GPIO
#include "spi.h" //因为是用SPI方式读写FLASH,发指令要用到SPI函数,所以包含上这个头文件
#include "usart.h" //调试时使用,我开的是串口1、波特率115200、数据位8、校验位无、停止位1、流控无
//如果要使用printf函数打印数据,需要在usart.c中添加下面这段代码,在uart.h中包含"stdio.h"头文件,在uart.c里包含这个头文件,在其他地方使用会报警告。
//#include "stdio.h"
//#ifdef __GNUC__
// /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
// set to 'Yes') calls __io_putchar() */
// #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
//#else
// #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
//#endif /* __GNUC__ */
///**
// * @brief Retargets the C library printf function to the USART.
// * @param None
// * @retval None
// */
//PUTCHAR_PROTOTYPE
//{
// /* Place your implementation of fputc here */
// /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
// HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
// return ch;
//}
/******************************************************W25QXX操作指令******************************************************/
#define Write_Enable 0x06
#define Volatile_SR_Write_Enable 0x50
#define Write_Disable 0x04
#define Release_Power_down 0xAB
#define Manufacturer 0x90
#define JEDEC_ID 0x9F
#define Read_Unique_ID 0x4B
#define Read_Data 0x03
#define Fast_Read 0x0B
#define Page_Program 0x02
#define Sector_Erase 0x20 //(4KB)
#define Block_Erase 0x52 //(32KB)
#define Block_Erase1 0xD8 //(64KB)
#define Chip_Erase 0xC7 //或0x60
#define Read_Status_Register1 0x05
#define Write_Status_Register1 0x01
#define Read_Status_Register2 0x35
#define Write_Status_Register2 0x31
#define Read_Status_Register3 0x15
#define Write_Status_Register3 0x11
#define Read_SFDP_Register 0x5A
#define Erase_Security_Register 0x44
#define Program_Security_Register 0x42
#define Read_Security_Register 0x48
#define Global_Block_Lock 0x7E
#define Global_Block_Unlock 0x98
#define Read_Block_Lock 0x3D
#define Individual_Block_Lock 0x36
#define Individual_Block_Unlock 0x39
#define Erase_Program_Suspend 0x75
#define Erase_Program_Resume 0x7A
#define Power_down 0xB9
#define Enable_Reset 0x66
#define Reset_Device 0x99
#define Fast_Read_Dual_Output 0x3B
#define Fast_Read_Dual_IO 0xBB
#define Device_ID_Dual_IO 0x92
#define Quad_Input_Page_Program 0x32
#define Fast_Read_Quad_Output 0x6B
#define Device_ID_Quad_IO 0x94
#define Fast_Read_Quad_IO 0xEB
#define Set_Burst_with_Wrap 0x77
/***************************************************************************************************************************/
/******************************************************W25QXX通用设置******************************************************/
/*
说明:W25QXX不管是哪一个型号,区别只是在于块儿的多少,其他的都是一致的。1MB=1024KB,1KB=1024Byte,1Byte=8bit
所有型号均满足(一下数据均来自相应的数据手册):
1块儿 = 16扇区
1块儿 = 16*16*256字节(Byte)= 65536Byte = 64KB(65536Byte/1024=64KB)
1扇区 = 16页
1扇区 = 16*256(Byte)= 4096Byte = 4KB
1页 = 256字节
只能按扇区、块为单位擦除,或者是全片擦除。写可以1~256字节写,一次最多写256字节
不同型号flash块儿的区别:
W25Q80:16块,共16*65536 = 1048576 Byte;1048576/1024/1024 = 1MB
W25Q16:32块,共32*65536 = 2097152 Byte;2097152/1024/1024 = 2MB
W25Q32: 64块,共64*65536 = 4194304 Byte;4194304/1024/1024 = 4MB
W25Q64: 128块,共128*65536 = 8388608 Byte;8388608/1024/1024 = 8MB
W25Q128:256块,共256*65536 = 16777216 Byte;1677216/1024/1024 = 16MB
W25Q256:512块,共512*65536 = 33554432 Byte;33554432/1024/1024 = 32MB
W25Q512:1024块,共1024*65536 = 67108864 Byte;67108864/1024/1024 = 64MB
*/
//定义块儿、扇区、页
#define NumByteOfPage 256 //一页有256字节
#define NumPageOfSector 16 //一个扇区有16页
#define NumSectorOfBlock 16 //一块有16个扇区
#define NumByteOfBlock 65536 //一块有65536字节
#define NumByteOfSector 4096 //一个扇区有4096字节
#define Num80BlockOfChip 16 //W25Q80有16块儿
#define Num16BlockOfChip 32 //W25Q16有32块儿
#define Num32BlockOfChip 64 //W25Q32有64块儿
#define Num64BlockOfChip 128 //W25Q64有128块儿
#define Num128BlockOfChip 256 //W25Q128有256块儿
#define Num256BlockOfChip 512 //W25Q256有512块儿
#define Num512BlockOfChip 1024//W25Q256有1024块儿
//定义FLASH芯片ID
#define W25Q80 0XEF13
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16
#define W25Q128 0XEF17
#define W25Q256 0XEF18
#define W25Q512 0XEF19
//设置CS片选脚;0--有效、1--无效;
#define CS_Enable() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);
#define CS_Disable() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);
extern uint8_t W25QXX_BUFFER[]; //定义一个扇区总共的字节数(4096字节)作为缓存数组
//定义功能函数
uint16_t W25QXX_ReadID(void); //读取FLASH芯片的ID
uint8_t W25QXX_Init(void); //FLASH初始化函数
void W25QXX_Write_Enable(void); //写使能
void W25QXX_Write_Disable(void); //写保护
uint8_t W25QXX_ReadSR(uint8_t regno); //读取状态寄存器
void W25QXX_WriteSR(uint8_t regno,uint8_t sr); //写状态寄存器
void W25QXX_Wait_Busy(void); //等待空闲
void W25QXX_PowerDown(void); //进入掉电模式
void W25QXX_WAKEUP(void); //唤醒
void W25QXX_Erase_Sector(uint32_t Dst_Addr); //扇区擦除
void W25QXX_Erase_Block(uint32_t Dst_Addr); //块擦除
void W25QXX_Erase_Chip(void); //整片擦除
void W25QXX_Read(uint8_t* pBuffer, uint32_t ReadAddr,uint16_t NumByteToRead); //读取flash数据
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite); //写入flash
void W25QXX_Write_Page(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite); //按页写入数据
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite); //无校验写入数据
#endif
这里我们继续做好跳转按钮
点这里坐火车到“W25QXX.C";
点这里做高铁到“W25QXX.H";
点这里坐电梯到“W25QXX.H"的结尾;
点这里直达 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序;
将这个W25Qxx一直过来后,我们操作FLASH的工具就有了,接下来我们就交给USB为所欲为了。不行,这里我必须打个比方
这里打个比方就是:我是地主,我家里有一块空地。我找了一个农民(USB)来帮我管理这个空地(flash),农民管理的方式包括收粮食(从flash里读取数据)、播种(向flash里写入数据)以及耕作土地(删除flash里的数据)。然后现在农民来了,但是我们还没工具(W25QXX的操作函数)给他,他就没法干活儿,所以我们现在就需要把这个工具先做好。农民有了工具之后,就用我们给他的锄头镰刀(SPI操作W25Qxx的各种函数)开始干活儿了。
所以我们 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序就是在做这个工具准备给农民使用,这里如果工具没做好,也就会导致后面我们会遇到的U盘可以识别,但是无法格式化的问题。
这里我们继续做好跳转按钮
点这里坐火车到“W25QXX.C";
点这里做高铁到“W25QXX.H";
点这里坐电梯到“W25QXX.H"的结尾;
点这里直达 5.3在Keil里写SPI通信方式驱动W25Qxx的驱动程序;
5.4选择性修改“startup_stm32f407xx.s”文件
在我们把SPI通信的方式读写W25QXX的函数移植过来后,我们就可以来修改“startup_stm32f407xx.s”文件了。
- 修改点有两个
- 1、扇区个数每个扇区的字节数,以w25Q16为例
- 2、在USB的读和写函数里加入对W25QXX的读写函数1和函数2
这里说明下,写入函数是自带扇区擦除的,所以不需要在单独写擦除函数。
以上两点修改完成后,其他的我们就不用修改了,接下来我们就可以愉快的玩耍了。
5.5编译、烧录代码,插上PC测试效果
万事具备,只欠东风,我们接下来就可以编译代码,然后烧录到单片机里边去开始测试了,如果不出意外的话,我们一般是要出意外了。不出意外的话,我们就可以看到这样的情景了
6、遇到的问题及解决方案
6.1单片机插上电脑后,PC无法识别到U盘
具体表现为插上单片机后,电脑可能会叮一声,但是资源管理器里看不到U盘图标,然后就没然后了。
原因分析:这样的情况一般是因为堆栈设置大小不对;
解决方式:可以参照这里进行设置,重新设置后再来试一下应该就没问题了。
6.2单片机识别到U盘,但是时钟无法进行格式化
具体表现为就是没发进行格式化;
原因分析:这样的情况一般是因为W25QXX函数写的不对,如果你是使用的我上面提供的这个函数,不会出现这样的问题,除非你用的是W25Q256或者W25Q512,因为这两个芯片的地址是4个字节,我上面提供这个程序要做修改才能兼容。
解决方式:更换FLASH芯片为W25Q80、W25Q16、W25Q32、W25Q64、W25Q128其中任意一种,或者自己修改W25QXX函数使其支持W25Q256和W25Q512;
6.3单片机识别U盘成功,但是识别的容量不对
具体表现为:你的FLASH芯片可能是2MB的,结果识别出来大于2MB,或者小于2MB了,但是又可以格式化和使用。
原因分析:这种情况一般是你扇区个数和扇区字节数没设置对,芯片对应的扇区和相关信息参考相关的知识储备
6.4其他问题
如果是遇到了这三种情况之外的问题,解决思路如下:
首先排除单片机问题,这个我就不展开说了,自行百度怎么确认单片机是否损坏。
接下来是排除FLASH芯片问题,这个就直接按照制作模拟U盘的具体操作步骤生成代码后不加入W25Qxx操作函数,直接编译烧录,看PC是否能识别U盘,如果可以,那就再加入W25QXX函数看是否能正常读写,如果还是不行,那就换一个FLASH芯片试试,如果换了FLASH芯片还是不行,那就看你移植W25QXX的代码是不是弄错了。
至此,我的轮子就造完啦啦啦,点这里回到开头位置