1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
本章,我们将向大家介绍如何利用USB OTG FS在STM32F1开发板实现一个USB 读卡器。本章分为如下几个部分:
60.1 USB 读卡器简介
60.2 硬件设计
60.3 软件设计
60.4 下载验证
60.1 USB简介
USB,即通用串行总线(Universal Serial Bus),包括USB协议和USB硬件两个方面,支持热插拔功能。现在日常生活的很多方面都离不开USB的应用,如充电和数据传输等场景。
USB经过多次修改,1996年确定了初始规范版本USB1.0,目前由非盈利组织USB-IF(https://www.usb.org)管理。STM32自带的USB符合USB2.0规范,故2.0版本仍是本文的重点介绍对象。
60.1.1 USB简介
USB本身的知识体系非常复杂,本节只能作一点知识点的引入。本书篇幅有限,不可能在这里详细介绍,想更系统地学习USB的知识可以参考《圈圈教你玩 USB》、塞普拉斯提供的《USB101:通用串行总线2.0简介》等文献,下面我们一起来看USB的简单特性:
USB的硬件接口
USB协议有漫长的发展历程,为的不同的场合和硬件功能而发展出不同的接口:Type-A、Type-B、Type-C,Type-C规范碰巧是跟着USB3.1的规范一起发布的。常见的接口类型列出如图60.1.1所示。
图60.1.1 常见的USB连接器的形状
USB发展到现在已经有USB1.0/1.1/2.0/3.x/4等多个版本。目前用的最多的就是版本 USB1.1和USB2.0,USB3.x/USB4目前也在加速推广。从图中可以发现不同的版本的USB接口内的引脚数量是有差异的。USB3.0以后为了提高速度,采用了更多数量的通讯线,比如同样的是Type A接口,USB2.0版本内部只有四根线,采用半双工式广播式通讯,USB3.0版本则将通讯线提高到了9根,并可以支持全双工非广播式的总线,允许两个单向数据管道分别处理一个单向通信。
USB2.0常使用四根线:VCC(5V)、GND、D+(3.3V)和D-(3.3V) (注:五线模式多了一个DI脚用于支持OTG模式,OTG为USB主机+USB设备双重角色),其中数据线采用差分电压的方式进行数据传输。在USB主机上,D-和D+都是接了15K的电阻到地的,所以在没有设备接入的时候,D+、D-均是低电平。而在USB设备中,如果是高速设备,则会在D+上接一个1.5K的电阻到3.3V,而如果是低速设备,则会在D-上接一个1.5K的电阻到3.3V。这样当设备接入主机的时候,主机就可以判断是否有设备接入,并能判断设备是高速设备还是低速设备。
关于USB硬件还有更多具体的细节规定,硬件设计时需要严格按照USB的器件的使用描述和USB标准所规定的参数来设计。
USB速度
USB规范已经为USB系统定义了以下四种速度模式:低速(Low-Speed)、全速(Full-Speed)、高速(Hi-Speed)和超高速(SuperSpeedUSB)。接口的速度上限与设备支持的USB协议标准和导线长度、阻抗有关,不同协议版本对硬件的传输线数量、阻抗等要求各不相同,各个版本的能达到的理论速度上限对应如图60.1.2。
图60.1.2 USB协议发展与版本对应的速度
USB端口和连接器有时会标上颜色,以指示USB规格及其支持的功能。这些颜色不是USB规范所要求的,并且在设备制造商之间不一致。例如,常见的支持USB3.0的U盘和电脑等设备使用蓝色指示,英特尔使用橙色指示充电端口等。
USB系统
USB系统主要包括三个部分:控制器(Host Controller)、集线器 (Hub) 和USB设备。
控制器(Host Controller),主机一般可以有一个或多个控制器,主要负责执行由控制器驱动程序发出的命令。控制器驱动程序(Host Controller Driver)在控制器与USB设备之间建立通信信道。
集线器(Hub)连接到USB主机的根集线器,可用于拓展主机可访问的USB设备的数量。
USB设备(USB Device)则是我们常用的如U盘,USB鼠标这类受主机控制的设备。
USB通讯
USB针对主机、集线器和设备制定了严格的协议。概括来讲,通过检测、令牌、传输控制、数据传输等多种方式,定义了主机和从机在系统中的不同职能。USB系统通过“管道”进行通讯,有“控制管道”和“数据管道”两种, “控制管道”是双向的,而每个“数据管道”则是单向的,这种关系如图60.1.3所示。
图60.1.3 USB管道模型
USB通讯中的检测和断开总是由主机发起。USB主机与设备首次进行连接时会交换信息,这一过程叫“USB枚举”。枚举是设备和主机间进行的信息交换过程,包含用于识别设备的信息。此外,枚举过程主机需要分配设备地址、读取描述符(作为提供有关设备信息的数据结构),并分配和加载设备驱动程序,而从机需要提供相应的描述符使主机知悉如何操作此设备。整个过程需要数秒时间。完成该过程后设备才可以向主机传输数据。数据传输也有规定的三种类型,分别是:IN/读取/上行数据传输、OUT/写入/下行数据传输、控制数据传输。
USB通过设备端点寻址,在主机和设备间实现信息交流。枚举发生前有一套专用的端点用于与设备进行通信。这些专用的端点统称为控制端点或端点0,有端点0 IN和端点0 OUT两个不同的端点,但对开发者来说,它们的构建和运行方式是一样的。每一个USB设备都需要支持端点0。因此,端点0不需要使用独立的描述符。除了端点0外,特定设备所支持的端点数量将由各自的设计要求决定。简单的设计(如鼠标)可能仅要一个IN端点。复杂的设计可能需要多个数据端点。
USB规定的数据4种数据传输方式也是通过管道进行,分别是控制传输(Control Transfer)、中断传输(Interrupt Transfer)、批量传输或叫块传输(Bulk Transfer)、实时传输或叫同步传输(Isochronous Transfer ),每种模式规定了各自通讯时使用的管道类型。
关于USB还有很多更详细的时序和要求,像USB描述符、VID/PID的规定、USB类设备和调试等,因为USB2.0和之后的版本有差异,这里就不再为大家列举了,ST对USB2.0也有专门的培训资料,这部分我们也放到“光盘资料A盘8,STM32参考资料2,STM32 USB 学习资料”中了,感兴趣的朋友自行去查阅更的USB的相关扩展知识,我们对USB的简介就到这里。
60.1.2 STM32F1的USB特性
本节结合《STM32F10xxx参考手册_V10(中文版).pdf》的内容,对STM32F1的USB外设作简介。STM32F1 系列芯片自带了USB FS(FS,即全速,12Mbps),支持从机(Slave/Device),但其USB与CAN不能同时使用,因为硬件共享同一个SRAM。
STM32F1的USB外设实现了USB2.0的接口和APB1总线间的接口。它有以下特性:
符合USB2.0全速设备的技术规范
可配置1到8个USB端点
CRC(循环冗余校验)生成/校验,反向不归零(NRZI)编码/解码和位填充
支持同步传输
支持批量/同步端点的双缓冲区机制
支持USB挂起/恢复操作
帧锁定时钟脉冲生成
STM32F1的USB外设使用标准的48Mhz时钟,允许每个端点有独立的缓冲区,每个端点最大为512字节缓冲,最大16个单向或8个双向端点。USB的传输格式由硬件完成,状态可以由寄存器标记,可以很大程度上简化我们的程序设计。USB模块启动时间tSTARTUP最大为1us,这个需要在编程时注意。图60.1.2.1引用了STM32F1的USB设备框图,方便我们理解USB这个外设。
图60.1.2.1 USB设备框图
到这里我们已经对USB的硬件有了一定的了解,但由于USB协议的庞杂性,便得直接编写USB驱动的上手难度很大。故在本书编写USB驱动的思路是教大家学会移植ST官方的USB例程并学会使用USB驱动库。
ST官方Cube库中提供的官方USB协议栈,主要是包含了USBD内核与USB各种类。USBD内核一般是固定的,用户一般不需要修改,但USBD类,如果用户需要修改或者扩展,比如复合设备或者用户自定义设备,则需要用户自行修改。
USB协议栈将所有USB类都抽象成一个数据结构:USBD_ClassTypeDef,USBD内核与USBD类之间的纽带就是USBD_ClassType这个结构体。这个结构体是一个抽象类,定义了一些虚拟函数,比如初始化,反初始化,类请求指令处理函数,端点0发送完成,端点0接收处理,数据发送完成,数据接收处理,SOF中断处理,同步传输发送未完成,同步传输接收未完成处理等等;用户在实现自己具体的USB类的时候需要将它实例化,USBD_ClassTypeDef结构体是USBD内核提供给外部定义一个USB设备类的窗口,而USB类文件实际就是实现这个结构体具体实例化的过程。最后将这个具体实例化的对象注册到USBD内核的同时,USBD内核与USBD类也进行了关联。
介绍USB时我们说过USB有很多的设备类,这一节的USB读卡器实际上是一个大容量存储设备(MSC: Mass Storage Class),本实验实现过程我们需要对MSC类实例化,即定义这个类可以操作的功能,上层应用通过USBD_RegisterClass函数,将此对象注册到usbd内核,它主要在usbd_msc.c源文件中实现它的各个成员函数。
本实验中,STM32作为设备连接到主机,我们需要使能USB外设,以便主机识别到USB设备进行扫描,同时我们需要在软件上设计好USB枚举所需要的一些设备描述符和注册信息,配置对应的端点以用于USB通讯。
这个过程比较复杂,好在ST已经提供了实现了类似的例程:通过USB来读写SD卡(SDIO方式)和NAND FALSH,支持2个逻辑单元。我们在官方例程的基础上,只需要修改SD驱动部分代码,并将对NAND FLASH的操作修改为对SPI FLASH的操作。只要这两步完成了,剩下的就比较简单了,对底层磁盘的读写,都是在usbd-storage.c文件实现的,所以我们只需要修改文件中的对应接口使之与我们的SD卡和SPI FLASH对应起来即可。
60.2硬件设计
图60.2.1 USB SLAVE接口
除了USB接口外,其它外设原理图我们在之前的章节已经介绍过了,这里就不重复介绍了,不清楚的话可以查看本文之前章节的描述或对光盘资料提供的开发板原理图。
60.3 程序设计
由于USB驱动的复杂性,如果我们要从零开始编写USB 驱动,那将是一件相当困难的事情,尤其对于从没了解过USB的人来说,周期会更长。不过,ST提供了STM32F1的USB 驱动库,通过这个库,我们可以很方便的实现我们所要的功能,而不需要详细了解 USB 的整个驱动,大大缩短了我们的开发时间和精力。当能正常驱动起USB了,我们再去关联和研究USB底层的知识更容易达到事半功倍的效果。
USB库和相关参考例程在en.stm32cubef1.zip里面可以找到,该文件可以在 http://www.st.com网站搜索:cubef1找到。不过,我们已经帮大家下载到开发板光盘:8,STM32 参考资料→1,STM32F1xx固件库→stm32cube_fw_f1_v183.zip。解压可以得到STM32F1的固件支持包:STM32Cube_FW_F1_V1.8.0,该文件包含了我们常用的STM32F1的嵌入式软件源码,如果已经安装过CubeMX和F1对应的固件,则这个文件夹在CubeMX的资源仓库路径下,其中就有我们将要使用的USB库,它的位置如图60.3.1所示:
①.USB设备驱动库,从机使用
②.USB Host驱动库,主机使用
③.与我们使用的芯片型号近似的ST开发板的USB例程;
图60.3.1 ST以HAL库提供的USB组件
我们将通过图60.3.1序号的例程,移植并实现我们自己的USB Device设备。读卡器属于USB大存储设备,所以本章要移植的是官方的MSC_Standalone例程。我们先打开该例程的MDK工程(MDK-ARM文件夹下),查看一下其工程结构:
图60.3.2 ST官方例程的结构
从工程结构不难找出我们需要的USB功能代码,并且这些文件是加了只读属性的,我们移植后需要把只读属性去掉才能进行我们需要的修改。因为要用到SD卡和SPI Flash,为了减少步骤,我们复制之前的SD卡实验工程文件夹,重命名为“USB读卡器实验”,一方面是我们要用到SD卡,另一方面是USB的端点需要用到动态分配的内存。因为我们并不是所有例程都使用USB库驱动,故我们把USB作为一个第三方组件放到我们的“Middlewares”文件夹下,我们在该文件夹下新建一个USB文件目录,把USB相关的全部放到USB文件夹下使这部分驱动完全独立,这样可以方便我们以后事移植到其它项目中。
首先是usbd_core.c、usbd_ioreq.c、usbd_req.c这三个文件,我们查看它们所在的位置发现它们都位于“STM32_USB_Device_Library”文件夹下,所以我们可以直接把该文件夹复制到我们的USB文件夹下,后面再考虑精简工程。
接着同样的方法,找到usbd_msc_bot.c、usbd_msc.c、 usbd_msc_scsi.c、 usbd_msc_data.c这四个文件,发现他们同样位于“STM32_USB_Device_Library”文件夹,上一步我们已经把整个文件夹复制到我到我们的工程目录了,所以这步不需要再操作。
接下来的USB应用程序usb_desc.c、usbd_storage.c、usbd_conf.c三个文件,源文件和头文件分别位于图61.3.1所示USB_Device\MSC_Standalone\Src和USB_Device\MSC_Standalone\Inc下,我们在USB文件夹下新建一个“USB_APP”文件夹,把它们连同头文件都放到该文件夹的根目录下。
图60.3.3 USB APP文件夹下的文件
我们需要添加的文件已经准备好了,接下来我们添加到我们的工程中来。我们按原来的定义,在MDK中新建Middlewares/USB_CORE、Middlewares/USB_CLASS、Middlewares/USB_APP三个分组,把上面的文件的只读属性去掉后添加到我们的工程中,结果如图,然后把相关的HAL库的驱动加到Drivers/STM32F1xx_HAL_Driver目录下,如图60.3.4所示。
图60.3.4 在MDK中添加需要的代码
为了保持USB驱动部分更少的改动,我们添加原有USB库的头文件的引用路径,结果如图60.3.3所示。
图60.3.3 在MDK中添加USB引用的头文件的路径
这时我们直接编译会报错,因为我们没有引用ST开发板的BSP文件,这时我们还需要修改相关源码以匹配我们的底层的驱动,这部分与我们的开发板相关,我们在程序设计的时候再对应修改。
60.3.1 程序流程图
图60.3.2.1 USB读卡器程序流程图
我们按流程图编写的初始化顺序,在STM32注册USB内核,最后通过USB的中断和回调函数得到USB的操作状态和操作结果,主程序通过查询设定的标记变量的状态值后,在LCD上显示对应的USB操作状态。
60.3.2 usbd_stroage驱动
usbd_storage.c/.h需要适配我们的硬件信息。这个函数需要使用到我们的硬件底层驱动,我们需要把我们要对SD卡和SPI Flash的信息识别和读写操作在这里实现。
#include "./MALLOC/malloc.h"
#include "./FATFS/source/diskio.h"
#include "./BSP/SDIO/sdio_sdcard.h"
#include "./BSP/NORFLASH/norflash.h"
按照60.1的描述,接下来我们来对这几个接口进行补充实现。本章,我们用FATFS管理了2个磁盘:SD卡和SPI FLASH,我们设置SD_CARD为0,EX_FLASH位为1,对应到disk_read/disk_write函数里面。SD卡比较好说,但是SPI FLASH,因为其扇区是4K字节大小,我们为了方便设计,强制将其扇区定义为512字节,这样带来的好处就是设计使用相对简单,坏处就是擦除次数大增,所以不要随便往SPI FLASH里面写数据,非必要最好别写,如果频繁写的话,很容易将SPI FLASH写坏。
#define SD_CARD 0 /* SD卡,卷标为0 */
#define EX_FLASH 1 /* 外部qspi flash,卷标为1 */
/**
* 对于25Q128 FLASH芯片, 我们规定前 12M 给FATFS使用, 12M以后
* 紧跟字库, 3个字库 + UNIGBK.BIN, 总大小3.09M, 共占用15.09M
* 15.09M以后的存储空间大家可以随便使用.
*/
#define SPI_FLASH_SECTOR_SIZE 512
#define SPI_FLASH_SECTOR_COUNT 12 * 1024 * 2 /*25Q128,前12M字节给FATFS占用*/
#define SPI_FLASH_BLOCK_SIZE 8 /* 每个BLOCK有8个扇区 */
#define SPI_FLASH_FATFS_BASE 0 /* FATFS 在外部FLASH的起始地址从0开始 */
另外,diskio.c里面的函数,直接决定了磁盘编号(盘符/卷标)所对应的具体设备,比如,以上代码中,我们就通过switch来判断,到底要操作SD卡,还是SPI FLASH,然后,分别执行对应设备的相关操作。以此实现磁盘编号和磁盘的关联。
USBD_StorageTypeDef USBD_DISK_fops =
{
STORAGE_Init, /* 外设初始化 */
STORAGE_GetCapacity, /* 获取容量 */
STORAGE_IsReady, /* 检查设备就绪状态 */
STORAGE_IsWriteProtected, /* 查询设备读保护状态 */
STORAGE_Read, /* 读操作 */
STORAGE_Write, /* 写操作 */
STORAGE_GetMaxLun, /* 获取磁盘数 */
(int8_t *)STORAGE_Inquirydata, /* 设备信息标识 */
};
其中STORAGE_Inquirydata表示设备的基本描述信息,它会在USB设备注册时被SCSI_Inquiry()函数调用,大小与宏STANDARD_INQUIRY_DATA_LEN表示的数值相同,为36个字节,大容量存储设备对这个设备信息进行的格式有了标准的规定,具体如下面的表60.3.2.1所示,其中的设备类型也有作了相应的规定,由于我们使用的是存储设备,所以前面的信息保持一致即可:
表60.3.2.1 大容量存储设备信息
USB大容量存储设备的每个函数操作时都会对应一个卷标号,需要支持多个设备时,保证每个设备的描述信息都为完整合法的36个字节即可。我们模仿ST的例程,修改为支持两个设备,则有如下的设备信息代码:
/* USB Mass storage 标准查询数据(每个lun占36字节) */
const int8_t STORAGE_Inquirydata[] =
{
/* LUN 0 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 4),
0x00,
0x00,
0x00,
/* Vendor Identification */
'A', 'L', 'I', 'E', 'N', 'T', 'E', 'K', ' ',/* 9字节 */
/* Product Identification */
'S', 'P', 'I', ' ', 'F', 'l', 'a', 's', 'h',/* 15字节 */
' ', 'D', 'i', 's', 'k', ' ',
/* Product Revision Level */
'1', '.', '0', ' ', /* 4字节 */
/* LUN 1 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 4),
0x00,
0x00,
0x00,
/* Vendor Identification */
'A', 'L', 'I', 'E', 'N', 'T', 'E', 'K', ' ', /* 9字节 */
/* Product Identification */
'S', 'D', ' ', 'F', 'l', 'a', 's', 'h', ' ', /* 15字节 */
'D', 'i', 's', 'k', ' ', ' ',
/* Product Revision Level */
'1', '.', '0', ' ', /* 4字节 */
};
/**
* @brief 初始化存储设备
* @param lun : 逻辑单元编号
* @arg 0, SD卡
* @arg 1, SPI FLASH
* @retval 操作结果
* @arg 0 , 成功
* @arg 其他 , 错误代码
*/
int8_t STORAGE_Init (uint8_t lun)
{
uint8_t res = 0;
switch (lun)
{
case 0: /* SPI FLASH */
norflash_init();
break;
case 1: /* SD卡 */
res = sd_init();
break;
}
return res;
}
函数返回值:
返回硬件初始化结果:0 : 成功, 其它 : 错误或失败。
3. STORAGE_GetCapacity函数
STORAGE_GetCapacity从名字就可以知道这个接口需要返回存储器设备的存储容量,并将参数返回给对应的指针:
int8_t STORAGE_GetCapacity (uint8_t lun, uint32_t *block_num,
uint16_t *block_size)
函数描述:
获取指定标号的存储设备的容量信息。
函数形参:
形参1 lun是存储设备的卷标,从0开始,有多个设备时应该与STORAGE_Inquirydata中定义的设备顺序一致。
代码实现如下:
int8_t STORAGE_GetCapacity (uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
switch (lun)
{
case 0: /* SPI FLASH */
*block_size = 512;
*block_num = (1024 * 1024 * 12)/512;/*SPI FLASH的12MB字节,文件系统用 */
break;
case 1: /* SD卡 */
*block_size = 512;
*block_num = ((long long)g_sdcard_handler.SdCard.BlockNbr *
g_sdcard_handler.SdCard.BlockSize)/512;
break;
}
return 0;
}
函数返回值:
返回硬件初始化结果:0 : 成功, 其它 : 错误或失败。
4. STORAGE_IsReady函数
STORAGE_IsReady用于查询设备的就绪状态,我们的代码比较简单,只为了演示功能部分,所以直接返回就绪状态即可。
int8_t STORAGE_IsReady (uint8_t lun)
为了标识USB的操作状态,我们加入一个全局的标记来指示USB的操作状态,也方便我们在其它位置查询到当前的USB执行结果:
/* 自己定义的一个标记USB状态的寄存器, 方便判断USB状态
/**
* @brief 查看存储设备是否就绪
* @param lun : 逻辑单元编号
* @arg 0, SD卡
* @arg 1, SPI FLASH
* @retval 就绪状态
* @arg 0 , 就绪
* @arg 其他 , 未就绪
*/
int8_t STORAGE_IsReady (uint8_t lun)
{
g_usb_state_reg |= 0X10; /* 标记轮询 */
return 0;
}
函数返回值:
返回0表示设备就绪,返回其它则未就绪。
5. STORAGE_Read函数
STORAGE_Read实现USB对物理设备的读操作,其声明如下:
int8_t STORAGE_Read (uint8_t lun, uint8_t *buf, uint32_t blk_addr,
uint16_t blk_len)
函数描述:
初始化指定编号的磁盘,磁盘所指定的存储区。
函数形参:
形参1 lun是存储设备的卷标,从0开始,有多个设备时应该与STORAGE_Inquirydata中定义的设备顺序一致。
形参2 buf为要写入的数据的缓冲区指针,为字节类型。
形参3 blk_addr为要读数据的起始地址,对应为SPI Flash和SD卡的扇区地址。
形参4 blk_len表示的是要读取到buf的字节数。
代码实现如下:
int8_t STORAGE_Read (uint8_t lun, uint8_t *buf, uint32_t blk_addr,
uint16_t blk_len)
{
int8_t res = 0;
g_usb_state_reg |= 0X02; /* 标记正在读数据 */
switch (lun)
{
case 0: /* SPI FLASH */
norflash_read(buf, USB_STORAGE_FLASH_BASE + blk_addr * 512,
blk_len * 512);
break;
case 1: /* SD卡 */
res = sd_read_disk(buf, blk_addr, blk_len);
break;
}
if (res)
{
printf("rerr:%d,%d\r\n", lun, res);
g_usb_state_reg |= 0X08; /* 读错误! */
}
return res;
}
函数返回值:
返回0表示设备就绪,返回其它则未就绪。
usbd_storage的代码就讲到这里,其它的几个函数的实现方法类似,大家参考光盘的代码即可,代码里也有相应的注释,大家根据自己的硬件情况实现即可。
60.3.3 usbd_conf驱动
usbd_conf.c/.h主要实现USB的硬件初始化和中断操作,当USB状态机处理完不同事务的时候,会调用这些回调函数,我们通过这些回调函数,就可以知道USB当前状态,比如:是否枚举成功了?是否连接上了?是否断开了?根据这些状态,用户应用程序可以执行不同操作,完成特定功能。
USBD_Init()接口会调用ST芯片的USBD_LL_Init()初始化函数,HAL_PCD_MspInit()函数在这个时候被调用,所以我们需要实现USBD_LL_Init()和HAL_PCD_MspInit函数,使之与我们开发板上的USB接口对应。
USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev)
{
/* 设置LL驱动相关参数 */
hpcd.Instance = USB; /* 使用USB */
hpcd.Init.dev_endpoints = 8; /* 端点数为8 */
hpcd.Init.phy_itface = PCD_PHY_EMBEDDED; /* 使用内部PHY */
hpcd.Init.speed = PCD_SPEED_FULL; /* USB全速(12Mbps) */
hpcd.Init.low_power_enable = 0; /* 不使能低功耗模式 */
hpcd.pData = pdev; /* hpcd的pData指向pdev */
pdev->pData = &hpcd; /* pdev的pData指向hpcd */
HAL_PCD_Init((PCD_HandleTypeDef *) pdev->pData); /* 初始化LL驱动 */
HAL_PCDEx_PMAConfig(pdev->pData, 0x00, PCD_SNG_BUF, 0x18);
HAL_PCDEx_PMAConfig(pdev->pData, 0x80, PCD_SNG_BUF, 0x58);
HAL_PCDEx_PMAConfig(pdev->pData, MSC_EPIN_ADDR, PCD_SNG_BUF, 0x98);
HAL_PCDEx_PMAConfig(pdev->pData, MSC_EPOUT_ADDR, PCD_SNG_BUF, 0xD8);
return USBD_OK;
}
2. HAL_PCD_MspInit函数
HAL_PCD_MspInit中我们需要开启USB的引脚的复用功能,我们使用的是PA11/PA12,并开启USB中断:
/**
* @brief 初始化PCD MSP
* @note 这是一个回调函数, 在stm32f1xx_hal_pcd.c里面调用
* @param hpcd : PCD结构体指针
* @retval 无
*/
void HAL_PCD_MspInit(PCD_HandleTypeDef *hpcd)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能PORTA时钟 */
__HAL_RCC_USB_CLK_ENABLE(); /* 使能USB时钟 */
/* PA11/PA12,复用为(USB DM/DP)功 */
GPIO_Initure.Pin = (GPIO_PIN_11 | GPIO_PIN_12);
GPIO_Initure.Mode = GPIO_MODE_AF_INPUT;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 0, 3); /* 抢占0,子优先3,组2 */
HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn); /*开启USB中断*/
}
由于开启了中断,我们还需要定义USB的中断处理函数,类似我们之前实验的中断处理函数定义,直接调用HAL库的USB中断处理接口,然后再实现对应的回调函数
/**
* @brief USB OTG 中断服务函数
* @note 处理所有USB中断
* @param 无
* @retval 无
*/
void USB_LP_CAN1_RX0_IRQHandler(void)
{
HAL_PCD_IRQHandler(&hpcd);
}
HAL库为我们提供了回调事件的接口。可以监测USB在通讯中的各个状态,为了方便,我们加入了一个全局变量g_device_state标记USB的运行状态,以方便程序访问,具体可以查看我们添加的回调函数的代码。
3. USB内存管理函数
ST的例程提供了静态和动态两种内存分配方式,注意到USB默认使用堆来分配USB设备的工作内存,我不使用默认的堆,不去修改startup_stm32f103xe.s中的堆大小,我们对应地要把默认的内存管理函数变成我们自己的内存管理方式,在usbd_conf.h中把以下宏重定义:
/* Memory management macros */
#define USBD_malloc(x) mymalloc(SRAMIN,x)
#define USBD_free(x) myfree(SRAMIN,x)
到此就基本移植完成了,下面我们开始编写我们的main函数来测试移植效果。
60.3.4. main.c代码
在main.c就比较简单了,按照我们的流程图的思路编写即可,我们初始化探按键,LCD和LED、SRAM等外设辅助程序显示。
最后,我们编写的main函数如下:
USBD_HandleTypeDef USBD_Device; /* USB Device处理结构体 */
extern volatile uint8_t g_usb_state_reg; /* USB状态 */
extern volatile uint8_t g_device_state; /* USB连接 情况 */
int main(void)
{
uint8_t offline_cnt = 0;
uint8_t tct = 0;
uint8_t usb_sta;
uint8_t device_sta;
uint16_t id;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
sram_init(); /* SRAM初始化 */
norflash_init(); /* 初始化NOR FLASH */
my_mem_init(SRAMIN); /* 初始化内部SRAM内存池 */
my_mem_init(SRAMEX); /* 初始化外部SRAM内存池 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "USB Card Reader TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
if (sd_init()) /* 初始化SD卡 */
{ /* 检测SD卡错误 */
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
}
else /* SD 卡正常 */
{ /* 显示SD卡容量 */
lcd_show_string(30, 110, 200, 16, 16, "SD Card Size: MB", RED);
lcd_show_num(134, 110, SD_TOTAL_SIZE_MB(&g_sdcard_handler), 5, 16, RED);
}
id = norflash_read_id();
if ((id == 0) || (id == 0XFFFF))
{ /* 检测W25Q128/NM25Q错误 */
lcd_show_string(30, 110, 200, 16, 16, "BY25Q128 Error!", RED);
}
else /* SPI FLASH 正常 */
{
lcd_show_string(30, 130, 200, 16, 16, "SPI FLASH Size:7.25MB", RED);
}
usbd_port_config(0); /* USB先断开 */
delay_ms(500);
usbd_port_config(1); /* USB再次连接 */
delay_ms(500);
/* 提示正在建立连接 */
lcd_show_string(30, 170, 200, 16, 16, "USB Connecting...", RED);
USBD_Init(&USBD_Device, &MSC_Desc, 0); /* 初始化USB */
USBD_RegisterClass(&USBD_Device, USBD_MSC_CLASS); /* 添加类 */
USBD_MSC_RegisterStorage(&USBD_Device, &USBD_DISK_fops);/* 添加MSC类回调函数*/
USBD_Start(&USBD_Device); /* 开启USB */
delay_ms(1800);
while (1)
{
delay_ms(1);
if (usb_sta != g_usb_state_reg) /* 状态改变了 */
{
lcd_fill(30, 190, 240, 210 + 16, WHITE); /* 清除显示 */
if (g_usb_state_reg & 0x01) /* 正在写 */
{/* 提示USB正在写入数据 */
LED1(0);
lcd_show_string(30, 190, 200, 16, 16, "USB Writing...", RED);
}
if (g_usb_state_reg & 0x02) /* 正在读 */
{/* 提示USB正在读出数据 */
LED1(0);
lcd_show_string(30, 190, 200, 16, 16, "USB Reading...", RED);
}
if (g_usb_state_reg & 0x04)
{/* 提示写入错误 */
lcd_show_string(30, 210, 200, 16, 16, "USB Write Err ", RED);
}
else
{
lcd_fill(30, 210, 240, 230 + 16, WHITE); /* 清除显示 */
}
if (g_usb_state_reg & 0x08)
{/* 提示读出错误 */
lcd_show_string(30, 230, 200, 16, 16, "USB Read Err ", RED);
}
else
{
lcd_fill(30, 230, 240, 250 + 16, WHITE); /* 清除显示 */
}
usb_sta = g_usb_state_reg; /* 记录最后的状态 */
}
if (device_sta != g_device_state)
{
if (g_device_state == 1)
{/* 提示USB连接已经建立 */
lcd_show_string(30, 170, 200, 16, 16, "USB Connected ", RED);
}
else
{/* 提示USB被拔出了 */
lcd_show_string(30, 170, 200, 16, 16, "USB DisConnected ", RED);
}
device_sta = g_device_state;
}
tct++;
if (tct == 200)
{
tct = 0;
LED1(1); /* 关闭 LED1 */
LED0_TOGGLE(); /* LED0 闪烁 */
if (g_usb_state_reg & 0x10)
{
offline_cnt = 0; /* USB连接了,则清除offline计数器 */
g_device_state = 1;
}
else /* 没有得到轮询 */
{
offline_cnt++;
if (offline_cnt > 100)
{
g_device_state = 0;/* 20s内没收到在线标记,代表USB被拔出了 */
}
}
g_usb_state_reg = 0;
}
}
}
我们通过前面的移植,我们已经能正常使用USB读卡器的功能了,但我们发现识别成功后,如果USB设备发生故障(比如我们复位开发板),这时主机会认为USB设备错误自动移除USB的连接,被主机断开后的设备不会再被扫描,所以我们设定usbd_port_config()函数用于控制USB的IO,在while循环前,我们先把USB的IO复用成普通IO,再复用成USB的引脚,以保证每次复位USB线都有一个物理“断开”,重新激活USB的枚举过程。
/**
* @brief USB接口配置
* @note 使能/关闭USB接口, 以便每次启动都可以正常
* 连接USB
* @param state : 接口状态
* @arg 0, 断开USB连接
* @arg 1, 使能USB连接
* @retval 无
*/
void usbd_port_config(uint8_t state)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能PORTA时钟 */
if (state)
{
USB->CNTR &= ~(1 << 1); /* PWDN = 0, 退出断电模式 */
/* PA11引脚模式设置,复用功能 */
GPIO_Initure.Pin = GPIO_PIN_11;
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
/* PA12引脚模式设置,复用功能 */
GPIO_Initure.Pin = GPIO_PIN_12;
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
}
else
{
USB->CNTR |= 1 << 1; /* PWDN = 1, 进入断电模式 */
/* PA11引脚模式设置,推挽输出 */
GPIO_Initure.Pin = GPIO_PIN_11;
GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
/* PA12引脚模式设置,推挽输出 */
GPIO_Initure.Pin = GPIO_PIN_12;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET); /* PA11 = 0 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); /* PA12 = 0 */
}
}
60.5 下载验证
在代码编译成功之后,我们通过下载代码战舰STM32F103开发板上,在USB配置成功后(假设已经插入SD卡,注意:USB数据线,要插在开发板的USB_SLAVE口!而不是端口,且P9必须用跳线帽连接PA11和D-以及PA12和D+!!),LCD显示效果如图60.5.1所示:
图60.5.1 USB连接成功
USB识别成功后,电脑提示发现新硬件,并自动安装驱动,如图60.5.2所示:
图60.5.2 USB读卡器被电脑找到
等USB配置成功后,DS1不亮,DS0闪烁,并且在电脑上可以看到我们的磁盘,如图60.5.3所示:
图60.5.3 电脑找到USB读卡器的两个盘符
我们打开设备管理器,在通用串行总线控制器里面可以发现多出了一个USB Mass Storage Device,同时看到磁盘驱动器里面多了2个磁盘,如图60.5.4所示:
图60.5.4 通过设备管理器查看磁盘驱动器
此时,我们就可以通过电脑读写SD卡或者SPI FLASH里面的内容了。在执行读写操作的时候,就可以看到DS1亮,并且会在液晶上显示当前的读写状态。
注意,因为SPI FLASH有写次数据限制,最好不要频繁的往里面写数据,否则很容易将SPI FLASH写坏!!