概述
01 前言
IAP功能的实现有各种各样的方法,有些方法需要上位机,比如USART-IAP。有些则不需要上位机,也不需要专门驱动,而USB MSD IAP则是其中一种方法。其优点有:
- 不需要专门上位机和驱动,直接将设备接入Windows系统即可方便使用,文件传输进度也能够显示
- USB传输速度较快(FS:12Mbit/s,HS:480Mbit/s)且可靠
随着国内各MCU厂商产品线的扩展,已经有很多带USB相关外设的MCU系列可以选择,比如L072、F072、F103、F107、F4xx等。
下图是利用PC电脑实现MSD IAP的整体架构。
02 MSD IAP需要解决的问题
要实现USB MSD IAP功能,需要解决以下几个问题以改善体验:
-
没有文件系统导致每次MCU掉电重新枚举都需要格式化的问题
可以使用伪文件系统的方法解决。当MSD枚举时,MCU将伪文件系统的相关信息回应给USB Host,从而使USB Host认为MSD已存在文件系统,不需要格式化。 -
IAP状态的提示问题
可以使用文件系统种的文件目录和读写地址信息来作状态判断,以作出提示。
下面是上述问题的解决,及MSD IAP的实现过程。
03 USB MSD枚举
使用官方MSC例程即可实现。要注意的是,需要将block地址转换成数据字节地址,以进行后续伪FAT32系统读写操作及IAP的操作。
#define MEMORY_BLOCK_SIZE 512
// 读数据
USBD_STA_T USBD_FS_MemoryReadData(uint8_t lun, uint8_t* buffer, uint32_t blockAddr,
uint16_t blockLength)
{
USBD_STA_T usbStatus = USBD_OK;
uint32_t index;
uint8_t *bufferTemp = (uint8_t*)buffer;
uint64_t readAddr = blockAddr * MEMORY_BLOCK_SIZE;
for(index = 0; index < blockLength; index++)
{
IAP_FAT32_Read(bufferTemp, (uint32_t)readAddr);
readAddr += MEMORY_BLOCK_SIZE;
bufferTemp += MEMORY_BLOCK_SIZE;
}
return usbStatus;
}
// 写数据
USBD_STA_T USBD_FS_MemoryWriteData(uint8_t lun, uint8_t* buffer, uint32_t blockAddr,
uint16_t blockLength)
{
USBD_STA_T usbStatus = USBD_OK;
uint32_t index;
uint8_t *bufferTemp = (uint8_t*)buffer;
uint64_t writeAddr = blockAddr * MEMORY_BLOCK_SIZE;
for(index = 0; index < blockLength; index++)
{
IAP_FAT32_Write(bufferTemp, (uint32_t)writeAddr);
writeAddr += MEMORY_BLOCK_SIZE;
bufferTemp += MEMORY_BLOCK_SIZE;
}
return usbStatus;
}
04 伪FAT32文件系统实现
在windows系统下,一般FAT32文件系统由保留区、FAT区及文件和目录数据区三个部分构成。
当USB枚举成MSD设备后,主机将会尝试读取该MSD设备的DBR、FAT及设备数据等内容。那么如果在主机读写操作的过程,正确应答文件系统的内容,则可以伪造一个文件系统,如下图所示。
那么接下来看来FAT32文件系统的具体内容。
启动扇区(DBR)
启动扇区从第一扇区开始,它保存了每个扇区的字节数,一个簇的扇区数,FAT表的起始位置,FAT表的个数以及FAT表的扇区数等信息。记录这些信息的数据结构是 BPB(BIOS Parameter Block),其数据结构如下。
字段 | 偏移量 | 字节数 | 含义 |
---|---|---|---|
BS_JmpBoot | 0 | 3 | 跳转指令 |
BS_OEMName | 3 | 8 | OEM名称 |
BPB_BytsPerSec | 11 | 2 | 每扇区字数 |
BPB_SecPerClus | 13 | 1 | 每簇扇区数 |
BPB_RsvdSecCnt | 14 | 2 | 保留扇区数 |
BPB_NumFATs | 16 | 1 | FAT个数 |
BPB_RootEntCnt | 17 | 2 | 根目录项数(FAT32已无该限制) |
BPB_TotSec16 | 19 | 2 | 扇区总数(小于32M使用) |
BPB_Media | 21 | 1 | 存储介质描述 |
BPB_FATSz16 | 22 | 2 | 每FAT表占用扇区数 (小于32M使用) |
BPB_SecPerTrk | 24 | 2 | 逻辑每磁道扇区数 |
BPB_NumHeads | 26 | 2 | 逻辑磁头数 |
BPB_HiddSec | 28 | 4 | 系统隐含扇区数 |
BPB_TotSec32 | 32 | 4 | 扇区总数(大于32M使用) |
BPB_FATSz32 | 36 | 4 | 每FAT表扇区数(大于32M使用) |
BPB_ExtFlags | 40 | 2 | 标记 |
BPB_FSVer | 42 | 2 | 版本 (通常为0) |
BPB_RootClus | 44 | 4 | 根目录起始簇 |
BPB_FSInfo | 48 | 2 | Boot占用扇区数 |
BPB_BkBootSec | 50 | 2 | 备份引导扇区位置 |
BPB_Reserved | 52 | 12 | 保留 |
BS_DrvNum | 64 | 1 | |
BS_Reserved | 65 | 1 | 保留 |
BS_BootSig | 66 | 1 | 扩展引导标记 |
BS_VolID | 67 | 4 | 序列号 |
BS_VolLab | 71 | 11 | 卷标 |
BS_FilSysType | 82 | 8 | 文件系统 |
C语言中可以用结构体来表示。
/**
* @brief BIOS paramter block
*/
typedef struct
{
uint8_t BS_JmpBoot[3];
uint8_t BS_OEMName[8];
uint16_t BPB_BytsPerSec;
uint8_t BPB_SecPerClus;
uint16_t BPB_RsvdSecCnt;
uint8_t BPB_NumFATs;
uint16_t BPB_RootEntCnt;
uint16_t BPB_TotSec16;
uint8_t BPB_Media;
uint16_t BPB_FATSz16;
uint16_t BPB_SecPerTrk;
uint16_t BPB_NumHeads;
uint32_t BPB_HiddSec;
uint32_t BPB_TotSec32;
/* FAT32 Structure */
uint32_t BPB_FATSz32;
uint16_t BPB_ExtFlags;
uint16_t BPB_FSVer;
uint32_t BPB_RootClus;
uint16_t BPB_FSInfo;
uint16_t BPB_BkBootSec;
uint8_t BS_Reserved1[12];
uint8_t BS_DrvNum;
uint8_t BS_Reserved2;
uint8_t BS_BootSig;
uint32_t BS_VolID;
uint8_t BS_VolLab[11];
uint8_t BS_FilSysType[8];
} FAT32_PBP_T;
其具体实现如下,可以用数组或结构体形式定义:
static const uint8_t FAT32_BPB[] = {
0xEB, /*00 - BS_jmpBoot */
0xFE, /*01 - BS_jmpBoot */
0x90, /*02 - BS_jmpBoot */
'M','S','D','O','S','5','.','0', /* 03-10 - BS_OEMName */
0x00, 0x02, /*11 - BPB_BytesPerSec = 512 */
0x01, /*13 - BPB_Sec_PerClus = 2K*1 = 2K*/
0x7C, 0x11, /*14 - BPB_RsvdSecCnt = 0x117C * 512 / 1024 == 2238KB */
0x02, /*16 - BPB_NumFATs = 2 */
0x00, 0x00, /*17 - BPB_RootEntCnt = 512 */
0x00, 0x00, /*19 - BPB_TotSec16 = 0 */
0xF8, /*21 - BPB_Media = 0xF8 */
0x00, 0x00, /*22 - BPBFATSz16 = 0x0000 */
0x3F, 0x00, /*24 - BPB_SecPerTrk = 0x003F */
0xFF, 0x00, /*26 - BPB_NumHeads = 0x00FF */
0x3F, 0x00, 0x00, 0x00, /*28 - BPB_HiddSec = 0x0000003F */
0xC1, 0xC0, 0x03, 0x00, /*32 - BPB_TotSec32 = 0x0003C0C1 120MB */
0x42, 0x07, 0x00, 0x00, /*36 - BPB_FATSz32 = 0x00000742 */
0x00, 0x00, /*40 - BPB_ExtFlags = 0x0000 */
0x00, 0x00, /*42 - BPB_FSVer = 0x0000 */
0x02, 0x00, 0x00, 0x00, /*44 - BPB_RootClus = 0x00000002 */
0x01, 0x00, /*48 - BPB_FSInfo = 0x0001 */
0x06, 0x00, /*50 - BS_Reserved */
0x00, 0x00, 0x00, 0x00, /*52 - BS_Reserved */
0x00, 0x00, 0x00, 0x00, /*56 - BS_Reserved */
0x00, 0x00, 0x00, 0x00, /*60 - BS_Reserved */
0x80, /*64 - BS_DrvNum = 0x80 */
0x00, /*65 - BS_Reserved1 = 0 , dirty bit = 0*/
0x29, /*66 - BS_BootSig = 0x29 */
0xB0, 0x49, 0x90, 0x02, /*67 - BS_VolID = 0x029049B0 */
'N','O',' ','N','A','M','E',' ',' ',' ',' ', /*71 - BS_VolLab */
'F','A','T','3','2',' ',' ',' ' /*82 - BS_FilSysType */
};
信息扇区(FSINFO)
FSINFO扇区一般位于保留区的启动扇区与BPB之后,用来记录文件系统中空闲簇的数量以及下一可用簇的簇号等信息,以供操作系统作为参考。其数据结构如下:
字段 | 偏移量 | 字节数 | 含义 |
---|---|---|---|
FSI_LeadSig | 0 | 4 | 值为0x41615252,这个标记用来表示该扇区为FSInfo扇区 |
FSI_Reserved1 | 4 | 480 | 保留,FAT32格式化时应将此区域全部设置为0 |
FSI_StrucSig | 484 | 4 | 值为0x61417272,这个标记用来表明扇区已被使用 |
FSI_Free_Count | 488 | 4 | 保存最新的剩余簇数量,如果为0xFFFFFFFF表示剩余簇未知,需要重新计算 |
FSI_Nxt_Free | 492 | 4 | 该值指示从哪里开始寻找剩余簇,通常设定为驱动程序最后分配出去的簇号 |
FSI_Reserved2 | 496 | 12 | 保留 |
FSI_TrailSig | 508 | 4 | 值为0xAA550000,标记扇区结束 |
C语言中可以用结构体来表示。
/**
* @brief FS info
*/
typedef struct
{
uint32_t FSI_LeadSig;
uint8_t FSI_Reserved1[480];
uint32_t FSI_StrucSig;
uint32_t FSI_Free_Count;
uint32_t FSI_Nxt_Free;
uint8_t FSI_Reserved2[12];
uint32_t FSI_TrailSig;
} FAT32_FSINFO_T;
文件分配表(FAT1和FAT2)
FAT扇区是一组与数据簇号对应的列表,每个表项占用四个字节,其由两个完全相同的FAT(File Allocation Table)文件分配表单组成。表项数值对应的含义如下:
表项数值 | 描述 |
---|---|
0x00000000 | 空闲簇,即表示可用 |
0x00000001 | 保留簇 |
0x00000002 - 0x0FFFFFEF | 被占用的簇,其值指向下一个簇号 |
0x0FFFFFF0 - 0x0FFFFFF6 | 保留值 |
0x0FFFFFF7 | 坏簇 |
0x0FFFFFF8 - 0x0FFFFFFF | 文件最后一个簇 |
保留前5号表项为固定值。
表项 | 值 | 含义 |
---|---|---|
0号 | 0x0FFFFFF8 | FAT表起始固定标识 |
1号 | 0x0FFFFFFF | 不使用,默认值 |
2号 | 0x0FFFFFFF | 标识文件结束 |
3号 | 0x0FFFFFFF | 标识文件结束 |
4号 | 0x0FFFFFFF | 标识文件结束 |
数据区
数据区是存放用户数据的区域,位于FAT2之后。其中短文件的数据结构如下:
字段 | 偏移量 | 字节数 | 含义 |
---|---|---|---|
DIR_Name | 0 | 11 | 短文件名 |
DIR_Attr | 11 | 1 | 文件属性 0x01:ATTR_READ_ONLY(只读) 0x02:ATTR_HIDDEN(隐藏) 0x04:ATTR_SYSTEM(系统) 0x08:ATTR_VOLUME_ID(卷标) 0x10:ATTR_DIRECTORY(目录) 0x20:ATTR_ARCHIVE(存档) |
DIR_NTRes | 12 | 1 | SFN信息 0x18,文件名和扩展名都小写 0x10,文件名大写而扩展名小写 0x08,文件名小写而扩展名大写 0x00,文件名和扩展名都大写 |
DIR_CrtTimeTenth | 13 | 1 | 文件创建时间,精确到10ms的值 |
DIR_CrtTime | 14 | 2 | 文件创建时间 |
DIR_CrtDate | 16 | 2 | 文件创建日期 |
DIR_LstAccDate | 18 | 2 | 最后访问日期 |
DIR_FstClusHI | 20 | 2 | 文件起始簇号的高16位 |
DIR_WrtTime | 22 | 2 | 文件最近修改时间 |
DIR_WrtDate | 24 | 2 | 文件最近修改日期 |
DIR_FstClusLO | 26 | 2 | 文件起始簇号的低16位 |
DIR_FileSize | 28 | 4 | 文件大小(单位:字节) |
C语言中可以用结构体来表示。
/**
* @brief Director entry information
*/
typedef struct
{
uint8_t DIR_Name[11];
uint8_t DIR_Attr;
uint8_t DIR_NTRes;
uint8_t DIR_CrtTimeTenth;
uint16_t DIR_CrtTime;
uint16_t DIR_CrtDate;
uint16_t DIR_LstAccDate;
uint16_t DIR_FstClusHI;
uint16_t DIR_WrtTime;
uint16_t DIR_WrtDate;
uint16_t DIR_FstClusLO;
uint32_t DIR_FileSize;
} FAT32_DIR_ENTRY_T;
其具体实现如下,定义卷标名字为BOOTLOADER
。创建第一个文件,文件名用指针传递,方便动态修改:
#define FAT32_VOLUME_LABEL "BOOTLOADER "
uint8_t FAT32_StatusFileName[FAT32_FILE_NAME_SIZE] = {
'S','T','A','T','U','S',' ',' ','T','X','T'
};
/**
* @brief fat32 read root director
* @param buffer: read buffer
* @param fileName: file name
* @retval FAT32 status
*/
USER_STATUS_T FAT32_ReadDirEntry(uint8_t *buffer, uint8_t *fileName)
{
USER_STATUS_T status = USER_OK;
fat32Info.dir = (FAT32_DIR_ENTRY_T*)buffer;
memset(buffer, 0, FAT32_SECTOR_SIZE);
memcpy(fat32Info.dir->DIR_Name, FAT32_VOLUME_LABEL, 11);
fat32Info.dir->DIR_Attr = FAT32_ATTR_VOLUME_ID;
fat32Info.dir->DIR_NTRes = 0x00;
fat32Info.dir->DIR_CrtTimeTenth = 0x00;
fat32Info.dir->DIR_CrtTime = 0x0000;
fat32Info.dir->DIR_CrtDate = 0x0000;
fat32Info.dir->DIR_LstAccDate = 0x0000;
fat32Info.dir->DIR_FstClusHI = 0x0000;
fat32Info.dir->DIR_WrtTime = FAT32_MAKE_TIME(0,0);
fat32Info.dir->DIR_WrtDate = FAT32_MAKE_DATE(10,02,2023);
fat32Info.dir->DIR_FstClusLO = 0x0000;
fat32Info.dir->DIR_FileSize = 0x00000000;
++fat32Info.dir;
memcpy(fat32Info.dir->DIR_Name, fileName, 11);
fat32Info.dir->DIR_Attr = FAT32_ATTR_ARCHIVE;
fat32Info.dir->DIR_NTRes = 0x10;
fat32Info.dir->DIR_CrtTimeTenth = 0x00;
fat32Info.dir->DIR_CrtTime = FAT32_MAKE_TIME(0,0);
fat32Info.dir->DIR_CrtDate = FAT32_MAKE_DATE(10,02,2023);
fat32Info.dir->DIR_LstAccDate = FAT32_MAKE_DATE(10,02,2023);
fat32Info.dir->DIR_FstClusHI = (uint16_t)(FAT32_FST_CLUS >> 16);
fat32Info.dir->DIR_WrtTime = FAT32_MAKE_TIME(0,0);
fat32Info.dir->DIR_WrtDate = FAT32_MAKE_DATE(10,02,2023);
fat32Info.dir->DIR_FstClusLO = (uint16_t)(FAT32_FST_CLUS);
fat32Info.dir->DIR_FileSize = 0x00000000;
return status;
}
各扇区地址的计算
各扇区地址计算如下,也可以通过winhex工具分析得到:
[!note] Boot扇区
Boot扇区 = 0x0000 ~ 0x0C00
[!note] FAT1表头地址
FAT1表头地址 = 保留扇区数 × 每扇区字节数 = BPB_RsvdSecCnt x BPB_BytsPerSec
[!note] FAT2表头地址
FAT2表头地址 =(保留扇区数 + FAT1表扇区数)× 每扇区字节数
[!note] 数据区根目录地址
数据区根目录地址 = (保留扇区数 + FAT表扇区数 × FAT表个数 +(根目录首簇号 - 2)× 每簇扇区数)× 每扇区字节数
= (BPB_RsvdSecCnt + BPB_FATSz32 x 2 + ( BPB_RootClus- 2) x BPB_SecPerClus) x 512
[!note] 文件起始偏移地址
文件起始地址偏移 =(保留扇区数 + FAT表扇区数 × FAT表个数 +(文件起始簇号 - 2)× 每簇扇区数)× 每扇区字节数
= (BPB_RsvdSecCnt + BPB_FATSz32 x 2 + ( (DIR_FstClusHI | DIR_FstClusLO) - 2) x BPB_SecPerClus) x 512
APP固件文件起始地址的计算
FAT文件系统写入文件时会访问数据区根目录地址,根据上述扇区地址的计算可以知道根目录的地址。
当FAT文件系统访问根目录地址时会将文件目录信息写入,然后再在偏移地址之后写入文件内容,这时获取数据并转换成数据区文件结构即可根据文件起始偏移地址的公式来计算。
[!note] APP固件起始地址范围
APP固件起始地址范围 -> BIN文件起始偏移地址 ~ DIR_FileSize(BIN),最大为USER_APP_SIZE。
其中USER_APP_SIZE = FLASH_SIZE - BOOTLOADER_SIZE
使用winhex分析
使用winhex工具可以清晰看到各个扇区的数据,方便分析。至于winhex工具的使用,网上有很多文章介绍,这里不赘述。
05 Flash操作的实现
Flash的操作和普遍的IAP程序一样,包括Flash的解锁、上锁、擦除和写入等操作。网上有很多例子,这些不再赘述。
06 MSD Bootloader实现
Bootloader架构
MSD Bootloader的程序结构如下,由USB、Flash驱动、伪FAT32系统、MSC类处理及Bootloader应用程序代码构成。
资源使用和分配情况
使用AC6的image size
优化等级后,MSD Bootloader约使用15KB Flash资源,如果使用F4xx系列,刚好使用第一个扇区存放bootload程序即可。
//==============================================================================
Total RO Size (Code + RO Data) 14728 ( 14.38kB)
Total RW Size (RW Data + ZI Data) 11936 ( 11.66kB)
Total ROM Size (Code + RO Data + RW Data) 14936 ( 14.59kB)
//==============================================================================
所以APP和Bootloader空间划分如下所示。
程序流程
程序运行流程如下图所示,首先枚举MSD设备,接着以伪FAT32文件系统应答和处理主机的操作和数据,最后更新状态文件或者直接跳转到APP程序中。
这些流程中,是以USB Host访问的地址及数据来判断属于哪个过程的,这些地址的计算可以参考上述伪文件系统章节的扇区和APP固件起始地址的计算。
状态判断和切换
Bootloader程序初始化时会创建第一个文件,用于指示Bootloader状态。该文件的文件名变化和IAP过程的关系如下图所示。
07 MSD IAP的使用过程
将USB口接入电脑,然后按住KEY1按键并复位MCU,那么程序就会自动进入MSD BOOTLOADER模式。同时在电脑上枚举出以BOOTLOADER
命名的U盘,而MINI Board上的LED3会持续闪烁。
初始化完成后可以看到U盘中的状态文件名变为READY.txt
。
然后拷贝或者发送APP的BIN文件到U盘。注意该BIN文件的偏移地址大小需要根据实际来设定,这里设定为0x4000。
等待文件传输完成后,程序将自动重新枚举。如果IAP成功则状态文件名变更为SUCCESS.txt
,如果失败则为ERROR.txt
。
然后复位(释放KEY1)即可进入APP程序中,也可以设置IAP完成后自动跳转到APP。
参考资料
FAT Filesystem (elm-chan.org)
STM32F103_MSD_BOOTLOADER
最后
以上就是贤惠书本为你收集整理的STM32的USB MSD IAP应用的全部内容,希望文章能够帮你解决STM32的USB MSD IAP应用所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复