STM32CubeMX学习笔记(48)——USB接口使用(MSC基于外部Flash模拟U盘)
Posted Leung_ManWah
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32CubeMX学习笔记(48)——USB接口使用(MSC基于外部Flash模拟U盘)相关的知识,希望对你有一定的参考价值。
一、USB简介
USB(Universal Serial BUS)通用串行总线,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。USB 接口支持设备的即插即用和热插拔功能。USB 是在 1994 年底由英特尔、康柏、IBM、Microsoft 等多家公司联合提出的。
USB 发展到现在已经有 USB1.0/1.1/2.0/3.0 等多个版本。目前用的最多的就是 USB1.1 和 USB2.0,USB3.0 目前已经开始普及。
STM32F103 自带的 USB 符合 USB2.0 规范,不过 STM32F103 的 USB 都只能用来做设备,而不能用作主机。
标准 USB 共四根线组成,除 VCC/GND 外,另外为 D+,D-; 这两根数据线采用的是差分电压的方式进行数据传输的。在 USB 主机上,D-和 D+都是接了 15K 的电阻到低的,所以在没有设备接入的时候,D+、D-均是低电平。而在 USB 设备中,如果是高速设备,则会在 D+上接一个 1.5K 的电阻到 VCC,而如果是低速设备,则会在 D-上接一个 1.5K 的电阻到 VCC。这样当设备接入主机的时候,主机就可以判断是否有设备接入,并能判断设备是高速设备还是低速设备。
STM32F103 的 MCU 自带 USB 从控制器
,符合 USB 规范的通信连接;PC 主机和微控制器之间的数据传输是通过共享一专用的数据缓冲区来完成的,该数据缓冲区能被 USB 外设直接访问。这块专用数据缓冲区的大小由所使用的端点数目和每个端点最大的数据分组大小所决定,每个端点最大可使用 512 字节缓冲区(专用的 512 字节,和 CAN 共用),最多可用于 16 个单向或 8 个双向端点。USB 模块同 PC 主机通信,根据 USB 规范实现令牌分组的检测,数据发送/接收的处理,和握手分组的处理。整个传输的格式由硬件完成,其中包括 CRC 的生成和校验。
1.1 USB MSC简介
USB大容量存储设备类(The USB mass storage device class)是一种计算机和移动设备之间的传输协议,它允许一个通用串行总线(USB)设备来访问主机的计算设备,使两者之间进行文件传输。通过这个标准的计算机连接到的设备包括:移动硬盘、移动光驱、U盘、SD、TF等储存卡读卡器、数码相机、各种数字音频播放器和便携式媒体播放器、智能卡阅读器、掌上电脑和手机。
MSC的通用性和操作简单使他成为移动设备上最常见的文件系统,USB MSC并不需要任何特定的文件系统, 相反,它提供了一个简单的界面来读写接口用于访问任何硬盘驱动器。操作系统可以把MSC像本地硬盘一样格式化,并可以与他们喜欢的任何文件系统格式它,当然也可以创建多个分区。
1.2 外部Flash芯片
开发板中的 FLASH 芯片型号:W25Q64。W25Q 系列为台湾华邦公司推出的是一种使用 SPI 通讯协议的 NOR FLASH 存储器。芯片型号后两位表示芯片容量,例如 W25Q64 的 64 就是指 64Mbit 也就是 8M 的容量。它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SPI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚虽然是其片上 SPI 外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的 GPIO,使用软件的方式控制 NSS 信号,所以在 SPI 的硬件设计中,NSS 可以随便选择普通的 GPIO,不必纠结于选择硬件 NSS 信号。
FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
二、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
三、USB
3.1 参数配置
在 Connectivity
中选择 USB
设置,并勾选 Device(FS)
激活 USB 设备。
在 Parameter Settings
进行具体参数配置。
- Speed:
Full Speed 12MBit/s
(固定为全速) - Low Power: 默认
Disabled
(在任何不需要使用usb模块的时候,通过写控制寄存器总可以使usb模块置于低功耗模式(low power mode ,suspend模式)。在这种模式下,不产生任何静态电流消耗,同时usb时钟也会减慢或停止。通过对usb线上数据传输的检测,可以在低功耗模式下唤醒usb模块。也可以将一特定的中断输入源直接连接到唤醒引脚上,以使系统能立即恢复正常的时钟系统,并支持直接启动或停止时钟系统。)
3.2 引脚配置
USB 的 DP 引脚必须上拉 1.5K 欧的电阻
,电脑才能检测到 USB,否则检测不到。
查看野火指南者开发板原理图可知,需要将 PD6
配置为低电平
使能 USB。
在右边图中找到 PD6 引脚,选择 GPIO_Output
。
在GPIO output level
中选择 Low
输出低电平。
3.3 配置时钟
选择 Clock Configuration
,USB 时钟配置为 48MHz
,且来源最好是外部晶振分频得到。
3.4 USB Device
USB有主机(Host)和设备(Device)之分。一般电脑的USB接口为主机接口,而键盘、鼠标、U盘等则为设备。
部分型号的STM32芯片有1~2个USB接口。像STM32F103系列的有一个USB Device接口,STM32F407系列的有2个USB接口,既可以作为HOST,又可以作为Device,还可以作为OTG接口。
在 Middleware
中选择 USB_DEVICE
设置,在 Class For FS IP
设备类别选择 Mass Storage Class(HID)
大容量存储设备类。
参数配置保持默认(或根据存储介质的最小存储单元修改缓冲区大小)。
- MSC_MEDIA_PACKET (Media I/O buffer Size)(读写缓冲区大小):
4096
(默认为512,这个的大小对于USB读写速度会有一些影响,最好和存储介质的最小存储单元一致)
本实验板使用的W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb。该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块,每个块大小为64K字节,每个块又分为16个扇区,每个扇区4K个字节。
W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。
所以,这需要给W25Q64开辟一个至少4K的缓存区,这样必须要求芯片有4K以上的SRAM才能有很好的操作。
设备描述符保持默认。
四、SPI1
4.1 参数配置
在 Connectivity
中选择 SPI1
设置,并选择 Full-Duplex Master
全双工主模式,不开启 NSS
即不使用硬件片选信号
原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。
在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output
。纠正:野火STM32F103指南者开发板SPI1 NSS须配置为PC0
修改输出高电平 High
,标签为 W25Q64_CHIP_SELECT
。
SPI 为默认设置不作修改。只需注意一下,Prescaler
分频系数最低为 4
,波特率 (Baud Rate) 为 18.0 MBits/s
。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。
- Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
- Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。
根据 FLASH 芯片的说明,它支持 SPI模式0
及模式 3
,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。
所以这里配置 CPOL 为Low
,CPHA 为1 Edge
即 SPI模式0
。
五、生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的 ’.c/.h’
文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
六、编写W25Q64的驱动程序
驱动程序有问题后面Windows无法格式化硬盘
6.1 添加宏定义
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define W25Q80 0XEF13
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16
#define W25Q128 0XEF17
#define W25Q256 0XEF18
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg1 0x05
#define W25X_ReadStatusReg2 0x35
#define W25X_ReadStatusReg3 0x15
#define W25X_WriteStatusReg1 0x01
#define W25X_WriteStatusReg2 0x31
#define W25X_WriteStatusReg3 0x11
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define W25X_Enable4ByteAddr 0xB7
#define W25X_Exit4ByteAddr 0xE9
/* USER CODE END PD */
6.2 封装SPI Flash(W25Q64)的命令和底层函数
- 发送数据的同时读取数据的函数
//SPI 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
uint8_t SPI1_ReadWriteByte(uint8_t TxData)
uint8_t Rxdata;
HAL_SPI_TransmitReceive(&hspi1, &TxData, &Rxdata, 1, 1000);
return Rxdata; //返回收到的数据
6.3 读取 Manufacture ID 和 Device ID
读取 Flash 内部这两个 ID 有两个作用:
- 检测 SPI Flash 是否存在
- 可以根据 ID 判断 Flash 具体型号
uint16_t W25QXX_TYPE; //定义W25QXX芯片型号
uint16_t W25QXX_ReadID(void)
uint16_t Temp = 0;
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI1_ReadWriteByte(0x90);//发送读取ID命令
SPI1_ReadWriteByte(0x00);
SPI1_ReadWriteByte(0x00);
SPI1_ReadWriteByte(0x00);
Temp|=SPI1_ReadWriteByte(0xFF)<<8;
Temp|=SPI1_ReadWriteByte(0xFF);
W25QXX_TYPE=Temp;
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return Temp;
6.4 读取状态寄存器数据并判断Flash是否忙碌
SPI Flash 的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有 2-3 个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:
当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断 Flash 是否忙完。
//读取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 byte=0,command=0;
switch(regno)
case 1:
command=W25X_ReadStatusReg1; //读状态寄存器1指令
break;
case 2:
command=W25X_ReadStatusReg2; //读状态寄存器2指令
break;
case 3:
command=W25X_ReadStatusReg3; //读状态寄存器3指令
break;
default:
command=W25X_ReadStatusReg1;
break;
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI1_ReadWriteByte(command); //发送读取状态寄存器命令
byte=SPI1_ReadWriteByte(0Xff); //读取一个字节
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return byte;
然后编写阻塞判断 Flash 是否忙碌的函数:
//等待空闲
void W25QXX_Wait_Busy(void)
while((W25QXX_ReadSR(1)&0x01)==0x01); // 等待BUSY位清空
6.5 读取数据
SPI Flash 读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制。
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead)
uint16_t i;
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI1_ReadWriteByte(W25X_ReadData); //发送读取命令
if(W25QXX_TYPE==W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>24));
SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>16)); //发送24bit地址
SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>8));
SPI1_ReadWriteByte((uint8_t)ReadAddr);
for(i=0;i<NumByteToRead;i++)
pBuffer[i]=SPI1_ReadWriteByte(0XFF); //循环读数
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
6.6 写使能/禁止
Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能。
//W25QXX写使能
//将WEL置位
void W25QXX_Write_Enable(void)
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET); //使能器件
SPI1_ReadWriteByte(W25X_WriteEnable); //发送写使能
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET); //取消片选
//W25QXX写禁止
//将WEL清零
void W25QXX_Write_Disable(void)
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET); //使能器件
SPI1_ReadWriteByte(W25X_WriteDisable); //发送写禁止指令
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET); //取消片选
6.7 擦除扇区
SPI Flash有个特性:
数据位可以由1变为0,但是不能由0变为1。
所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF
(即全为1)。
//擦除一个扇区
//Dst_Addr:扇区地址 根据实际容量设置
//擦除一个扇区的最少时间:150ms
void W25QXX_Erase_Sector(uint32_t Dst_Addr)
//监视falsh擦除情况,测试用
//printf("fe:%x\\r\\n",Dst_Addr);
Dst_Addr*=4096;
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI1_ReadWriteByte(W25X_SectorErase); //发送扇区擦除指令
if(W25QXX_TYPE==W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>24));
SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>16)); //发送24bit地址
SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>8));
SPI1_ReadWriteByte((uint8_t)Dst_Addr);
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy(); //等待擦除完成
6.8 页写入操作
向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入。
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
uint16_t i;
W25QXX_Write_Enable(); //SET WEL
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI1_ReadWriteByte(W25X_PageProgram); //发送写页命令
if(W25QXX_TYPE==W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>24));
SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>16)); //发送24bit地址
SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>8));
SPI1_ReadWriteByte((uint8_t)WriteAddr);
for(i=0;i<NumByteToWrite;i++)SPI1_ReadWriteByte(pBuffer[i]);//循环写数
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy(); //等待写入结束
6.9 写入数据
//无检验写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;
pageremain=256-WriteAddr%256; //单页剩余的字节数
if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节
while(1)
W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWrite==pageremain)break;//写入结束了
else //NumByteToWrite>pageremain
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain; //减去已经写入了的字节数
if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
else pageremain=NumByteToWrite; //不够256个字节了
;
//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
uint8_t W25QXX_BUFFER[4096];
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t * W25QXX_BUF;
W25QXX_BUF=W25QXX_BUFFER;
secpos=WriteAddr/4096;//扇区地址
secoff=WriteAddr%4096;//在扇区内的偏移
secremain=4096-secoff;//扇区剩余空间大小
//printf("ad:%X,nb:%X\\r\\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
while(1)
W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
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*4096,4096);//写入整以上是关于STM32CubeMX学习笔记(48)——USB接口使用(MSC基于外部Flash模拟U盘)的主要内容,如果未能解决你的问题,请参考以下文章
STM32CubeMX学习笔记(45)——USB接口使用(HID鼠标)
STM32CubeMX学习笔记(44)——USB接口使用(HID按键)
STM32CubeMX学习笔记(44)——USB接口使用(HID按键)
STM32CubeMX学习笔记(45)——USB接口使用(HID鼠标)