本帖最后由 susutata 于 2023-2-20 16:53 编辑
#申请原创#@21小跑堂
基于APM32F407的USB MSD IAP应用
01 前言IAP功能的实现有各种各样的方法,有些方法需要上位机,比如USART-IAP。有些则不需要上位机,也不需要专门驱动,而USB MSD IAP则是其中一种方法。其优点有: 随着国内各MCU厂商产品线的扩展,已经有很多带USB相关外设的MCU系列可以选择,比如L072、F072、F103、F107、F4xx等。 下图是利用PC电脑实现MSD IAP的整体架构。 02 MSD IAP需要解决的问题要实现USB 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),其数据结构如下。
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之后,用来记录文件系统中空闲簇的数量以及下一可用簇的簇号等信息,以供操作系统作为参考。其数据结构如下:
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)文件分配表单组成。表项数值对应的含义如下:
保留前5号表项为固定值。
### 数据区数据区是存放用户数据的区域,位于FAT2之后。其中短文件的数据结构如下:
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工具分析得到: Boot扇区 = 0x0000 ~ 0x0C00
FAT1表头地址 = 保留扇区数 × 每扇区字节数 = BPB_RsvdSecCnt x BPB_BytsPerSec
FAT2表头地址 =(保留扇区数 + FAT1表扇区数)× 每扇区字节数
数据区根目录地址 = (保留扇区数 + FAT表扇区数 × FAT表个数 +(根目录首簇号 - 2)× 每簇扇区数)× 每扇区字节数 = (BPB_RsvdSecCnt + BPB_FATSz32 x 2 + ( BPB_RootClus- 2) x BPB_SecPerClus) x 512
文件起始地址偏移 =(保留扇区数 + FAT表扇区数 × FAT表个数 +(文件起始簇号 - 2)× 每簇扇区数)× 每扇区字节数= (BPB_RsvdSecCnt + BPB_FATSz32 x 2 + ( (DIR_FstClusHI | DIR_FstClusLO) - 2) x BPB_SecPerClus) x 512
### APP固件文件起始地址的计算FAT文件系统写入文件时会访问数据区根目录地址,根据上述扇区地址的计算可以知道根目录的地址。
当FAT文件系统访问根目录地址时会将文件目录信息写入,然后再在偏移地址之后写入文件内容,这时获取数据并转换成数据区文件结构即可根据文件起始偏移地址的公式来计算。
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。
参考资料01 FAT Filesystem (elm-chan.org) 02 MSD_BOOTLOADER |