嘛。。这是一个关于个人使用FatFS文件系统的一点小的经验。
我知道大家都会百度和谷歌,关于文件系统有什么用,文件系统怎么移植上自己的平台,看看资料也就懂了,在这里不再详述(打字太慢一分钟50-60字懒得写)。本系列默认已经可以将设备模拟成u盘,并且已经通过修改diskio.c,可以实现ff.c中的各项功能(不能实现的自行面壁)。FatFS项目官网 http://elm-chan.org/fsw/ff/00index_e.html,在这里面可以下载到最新的版本以及查看各API的说明。
在开发过程中为了将存储空间的一大部分给用户存储数据,常常需要把存储器模拟成USB大容量存储设备(比如智能手表)。但是我们也有很多素材文件以及数据库之类的文件,不希望被用户看见和修改(比如widget用到的图片或者字库之类)。
通常我们可以想到可以实现划分和挂载两个分区,第一个分区用来存储(zuo)用户(wei)可见(you)的文件(pan),第二个分区用来存储系统文件。
如图为16G的iphone的分区。可见有1.1G的空间用来存储系统数据,此部分普通用户是接触不到的。
废话少说,本系列比起网上当前各种抄来抄去的中文资料,主要是要实现这么几个创新点:
①普通用户只能看见第一个分区,并且在电脑上也只能接触到第一个分区。(第二个分区根本接触不到,在设备管理器里面也看不到,用U盘量产工具也看不到)
②开发者可以看见两个分区,在电脑上都能挂载,方便开发者进行素材的替换、字库的更新等操作。
需要注意的是,本文适合存储芯片不可拆卸的情况,比如外部flash或者EMMC,想用SD卡实现这个那就别看了(没必要模拟成usb大容量存储设备,用户一拆卡,放在读卡器里面,你还模拟成usb大容量存储设备个毛线)。。
那么,我们开始了。
一.分区
首先,我们需要先分成两个区。
uint8_t work[512]; /* Work area (larger is better for process time) */
DWORD plist[] = {95, 5, 0, 0}; /* Divide drive into two partitions */
res = f_fdisk(0, plist, work); /* Divide physical drive 0 */
第一个分区占95%,第二个分区占5%。经过f_mkfs掉两个分区之后,我们将设备模拟成u盘,可以在设备管理器->磁盘管理中,里面看见下面的情景:
(额,这个图是90%对10%的了。。凑合看)
第一个分区是可以正常访问的,但是第二个不能挂载。为什么捏?
原来, 虽然fatfs是支持多个分区的,但windows认为可移动闪存设备只有一个分区(注意,移动硬盘除外)。多余的分区不能挂载。
偷懒的人可能会想,哈哈,这下好,正好实现了第二个分区隐藏的功能。
全文完!
……
且慢,没完,这时候就怕遇见爱折腾的用户。
你是否想过,此时可以打开cmd,在diskpart工具里面clean掉整个磁盘;也可以动用U盘量产工具,挂载出来第二个分区。。总之,这时,第二个分区的文件有覆灭和被改动的风险!!
永远不要低估用户的好奇心!!!
二.让用户只能看见第一个分区
想解决这个问题,就要消除用户在磁盘管理器里面看见这个分区的可能性。
也就是说,在通过USB访问这个磁盘的时候,只访问和第一个分区相关的block。那么,我们只需要知道第一个分区所占的最后一个block即可。
可以使用以下函数:
FRESULT f_getlastblock_vol1(BYTE pdrv, DWORD percentage, DWORD *last_block)
{
UINT n, sz_cyl, tot_cyl, p_cyl, e_hd;
DWORD sz_disk, sz_part;
UINT retry = 0; //重试次数,确保磁盘在获取容量之前已经初始化
while (retry < 2)
{
if (disk_ioctl(pdrv, GET_SECTOR_COUNT, &sz_disk) != FR_OK)
{
retry ++;
disk_initialize(pdrv);
}
else
{
break;
}
}
if (retry >= 2)
{
return FR_NOT_READY;
}
/* Determine the CHS without any consideration of the drive geometry */
for (n = 16; n < 256 && sz_disk / n / 63 > 1024; n *= 2);
if (n == 256) n--;
e_hd = n - 1;
sz_cyl = 63 * n;
tot_cyl = sz_disk / sz_cyl;
p_cyl = (DWORD)tot_cyl * percentage / 100; /* Number of cylinders */
sz_part = (DWORD)sz_cyl * p_cyl;
*last_block = sz_part - 1;
return FR_OK;
}
这样,就可以获得第一个分区所占的最后一个block的位置。在将设备模拟成usb大容量存储设备的时候,只需要访问到第*last_block个block就可以啦。
(别问我怎么写访问哪些block的函数。。。这个每个平台都不一样。一般改改“获取flash容量”的hal函数的返回值对应的函数就好了)
这样下来,挂载在电脑里面的景象是这样的:
三.让开发者能看见两个分区
想解决这个问题,需要看看USB协议。
为什么windows只认u盘的第一个分区,但是可以认移动硬盘的多个分区呢?
原因是windows驱动程序在挂载设备的时候会发一个SCSI指令来查询这是什么设备,发的是INQUIRY命令,返回的状态位有一个Removable Media Bit((抱歉,有RMB真的是可以为所欲为的)
所以,我们的核心方法就是欺骗windows,让windows认为这是个移动硬盘。
翻一翻你的程序,你会在USB协议相关的地方找到类似这个的玩意:
ATTR_RWDATA_IN_NONCACHED_RAM uint8_t INQUIRE_DATA[] = {
0x00,
0x80, //这里非常重要!!!!要认出两个分区的话就改成0x00,一个分区的话就改成0x80
0x00,
0x01,
0x1f, /*length*/
0x00,
0x00,
0x00,
'M', /*Vendor Identification*/
'E',
'D',
'I',
'A',
'T',
'E',
'K',
' ', /*Product Identification*/
'F',
'L',
'A',
'S',
'H',
' ',
'D',
'I',
'S',
'K',
' ',
' ',
' ',
' ',
' ',
' ', /*Product Revision Level*/
' ',
' ',
' '
};
程序中0x80的地方的最高位就是Removable Media Bit,改成0即可欺骗windows认为这是个移动硬盘。
可以写点程序进行这一位的切换:
USB_DISK_TYPE disk_type = FLASH_DISK;//切换这个设备为u盘(只能在电脑上识别第一个分区)和移动硬盘(所有分区都能识别到)
/* switch udisk type */
typedef enum
{
EXTERNAL_HARDDRIVE = (uint8_t)0x00,
FLASH_DISK = (uint8_t)0x80
}USB_DISK_TYPE;
void USBSetDiskType(USB_DISK_TYPE usb_disk_type)
{
if(usb_disk_type == EXTERNAL_HARDDRIVE)
{
INQUIRE_DATA[1] = 0x00;
disk_type = EXTERNAL_HARDDRIVE;
}
if(usb_disk_type == FLASH_DISK)
{
INQUIRE_DATA[1] = 0x80;
disk_type = FLASH_DISK;
}
}
USB_DISK_TYPE USBGetDiskType(USB_DISK_TYPE usb_disk_type)
{
return usb_disk_type;
}
改后,记得把访问block的范围改成直到获取全磁盘(两个分区都包含)的最后一个block,确保两个磁盘被正确挂载。
我们可以写个开发者模式or普通用户模式的切换函数。
static kal_bool UsbmsReadCapacityModeSdc(uint32_t *max_lba, uint32_t *sec_len)
{
/*
* *max_lba is the capacity of store the Udisk, the unit is block.
* *sec_len is the number of bytes about one block.
*/
*sec_len = FF_MIN_SS;
USB_DISK_TYPE usb_disk_type = USBGetDiskType(disk_type); //得知当前的磁盘到底按照u盘(只挂载第一分区)来挂载还是按照移动硬盘来挂载
//cap is the SD/eMMC card capacity, the unit is bytes.
uint64_t cap;
hal_sd_get_capacity(HAL_SD_PORT_0, &cap);
if(usb_disk_type == EXTERNAL_HARDDRIVE)
{
*max_lba = cap / *sec_len - 1;
}
else if(usb_disk_type == FLASH_DISK)
{
f_getlastblock_vol1(0, 95, (DWORD *)max_lba); //这里的百分比要按照volume1占总磁盘体积的实际百分比来赋值
}
return KAL_TRUE;
}
两个分区都挂载好的磁盘是这样的。这样开发者就可以开心的在电脑端修改系统文件啦。
这样,我们就实现了两种模式的切换~达到了类似IOS的封闭效果,既保证了用户对U盘的操作,也保证了系统文件的安全性。
全文完。