FatFs文件系统


1、文件系统

  文件系统负责管理和存储文件信息的软件机构,在磁盘上组织文件的方法。

  对于 SPI Flash 芯片或者 SD 卡之类的大容量设备,我们需要一种高效的方式来管理它的存储内容。这些管理方式即为文件系统,它是为了存储和管理数据,而在存储介质建立的一种组织结构,这些结构包括操作系统引导区、目录和文件。 常见的 windows 下的文件系统格式包括 FAT32、 NTFS、 exFAT。 在使用文件系统前,要先对存储介质进行格式化。格式化先擦除原来内容,在存储介质上新建一个文件分配表和目录。这样,文件系统就可以记录数据存放的物理地址,剩余空间。

  使用文件系统时, 数据都以文件的形式存储。写入新文件时,先在目录中创建一个文件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。具体还涉及到逻辑地址、簇大小、不连续存储等一系列辅助结构或处理过程。

  文件系统的存在使我们在存取数据时,不再是简单地向某物理地址直接读写,而是要遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的物理地址,使用目录或链表的方式来获知下一段的位置。SPI Flash 芯片驱动只完成了向物理地址写入数据的工作,而根据文件系统格式的逻辑转换部分则需要额外的代码来完成。实质上,这个逻辑转换部分可以理解为当我们需要写入一段数据时,由它来求解向什么物理地址写入数据、以什么格式写入及写入一些原始数据以外的信息(如目录)。这个逻辑转换部分代码我们也习惯称之为文件系统。

2、FatFs 文件系统简介

  上面提到的逻辑转换部分代码(文件系统)即为FatFs 文件系统要点,文件系统庞大而复杂,它需要根据应用的文件系统格式而编写,而且一般与驱动层分离开来,很方便移植,所以工程应用中一般是移植现成的文件系统源码。

  常用的文件系统:FAT/FATFS(小型嵌入式系统)、  NTFS (WINDOWS)、CDFS(光盘)、exFAT(内存)。

  FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统。它完全是由 AISI C 语言编写并且完全独立于底层的 I/O 介质。因此它可以很容易地不加修改地移植到其他的处理器当中,如 8051、 PIC、 AVR、 SH、 Z80、 H8、 ARM 等。 FatFs 支持 FAT12、 FAT16、FAT32 等格式利用文件系统的各种函数, 所以可以利用FatFs对 SPI Flash 芯片以“文件”格式进行读写操作。

  FATFS优点:免费开源,专门为小型嵌入式系统设计,c编写,支持FAT12, FAT16 与 FAT32,支持多种存储媒介,有独立的缓冲区,可对多个文件进行读写,可裁剪的文件系统(极为重要)。

  FATFS的特点:

  • Windows兼容的FAT文件系统(支持FAT12/FAT16/FAT32)与平台无关,移植简单
  • 代码量少、效率高
  • 多种配置选项
  • 支持多卷(物理驱动器或分区, 最多10个卷)
  • 多个ANSI/OEM代码页包括DBCS
  • 支持长文件名、 ANSI/OEM 或Unicode
  • 支持RTOS
  • 支持多种扇区 大小
  • 只读、最小化的API和I/O缓冲区等

  FatFs 文件系统的源码可以从 fatfs 官网下载:http://elm-chan.org/fsw/ff/00index_e.html

3、FatFs 的目录结构

  在移植 FatFs 文件系统到开发板之前,我们先要到 FatFs 的官网获取源码, 最新版本为R0.11a,官网有对 FatFs 做详细的介绍,有兴趣可以了解。

  解压之后可看到里面有 doc 和src 这两个文件夹。 doc 文件夹里面是一些使用帮助文档; src 才是 FatFs 文件系统的源码。

 

  3.1、doc文件夹  

打开 doc 文件夹,可看到如下图的文件目录:

 

   其中 en 和 ja 这两个文件夹里面是编译好的 html 文档,讲的是 FATFS 里面各个函数的使用方法,这些函数都是封装得非常好的函数,利用这些函数我们就可以操作 SPI Flash 芯片。这两个文件夹的唯一区别就是 en 文件夹下的文档是英文的, ja 文件夹下的是日文的。 img 文件夹包含 en 和 ja 文件夹下文件需要用到的图片,还有四个名为 app.c 文件,内容都是 FatFs 具体应用例程。 00index_e.html 和00index_j.html 是一些关于 FATFS 的简介,至于另外两个文件可以不看。

  3.2、src文件夹

  打开 src 文件夹,可看到如下图文件目录:

  option 文件夹下是一些可选的外部 c 文件,包含了多语言支持需要用到的文件和转换函数。

  00history.txt 介绍了 FatFs 的版本更新情况。
  00readme.txt 说明了当前目录下 diskio.c 、 diskio.h、 ff.c、 ff.h、 integer.h 的功能。

  diskio.c 文件是 FatFs 移植最关键的文件,它为文件系统提供了最底层的访问 SPI Flash芯片的方法, FatFs 有且仅有它需要用到与 SPI Flash 芯片相关的函数。
  diskio.h 定义了FatFs 用到的宏,以及 diskio.c 文件内与底层硬件接口相关的函数声明。

  源码文件功能简介如下:

  integer.h:文件中包含了一些数值类型定义。
  diskio.c:包含底层存储介质的操作函数,这些函数需要用户自己实现,主要添加底层驱动函数。
  ff.c:FatFs 核心文件,文件管理的实现方法。该文件独立于底层介质操作文件的函数,利用这些函数实现文件的读写。
  cc936.c:本文件在 option 目录下,是简体中文支持所需要添加的文件,包含了简体中文的 GBK 和 Unicode 相互转换功能函数。
  ffconf.h:这个头文件包含了对 FatFs 功能配置的宏定义,通过修改这些宏定义就可以裁剪 FatFs 的功能。如需要支持简体中文,需要把 ffconf.h 中的_CODE_PAGE的宏改成 936 并把上面的 cc936.c 文件加入到工程之中。

  建议阅读这些源码的顺序为: integer.h --> diskio.c --> ff.c 。

  阅读文件系统源码 ff.c 文件需要一定的功底,建议读者先阅读 FAT32 的文件格式,再去分析 ff.c 文件。若仅为使用文件系统,则只需要理解 integer.h 及 diskio.c 文件并会调用ff.c 文件中的函数就可以了。

4、FatFs 程序结构图

 

  用户应用程序需要由用户编写,想实现什么功能就编写什么的程序,一般我们只用到f_mount()、 f_open()、 f_write()、 f_read()就可以实现文件的读写操作。

  FatFs 组件是 FatFs 的主体,文件都在源码 src 文件夹中,其中 ff.c、 ff.h、 integer.h 以及diskio.h 四个文件我们不需要改动,只需要修改 ffconf.h 和 diskio.c 两个文件。

  底层设备输入输出要求实现存储设备的读写操作函数、存储设备信息获取函数等等。

5、 FatFs 底层设备驱动函数

  FatFs 文件系统与底层介质的驱动分离开来,对底层介质的操作都要交给用户去实现,它仅仅是提供了一个函数接口而已。下表 为 FatFs 移植时用户必须支持的函数。通过表25-1 我们可以清晰知道很多函数是在一定条件下才需要添加的,只有前三个函数是必须添加的。我们完全可以根据实际需求选择实现用到的函数。

  前三个函数是实现读文件最基本需求。接下来三个函数是实现创建文件、修改文件需要的。为实现格式化功能,需要在 disk_ioctl 添加两个获取物理设备信息选项。我们一般只有实现前面六个函数就可以了,已经足够满足大部分功能。
  为支持简体中文长文件名称需要添加 ff_convert 和 ff_wtoupper 函数,实际这两个已经在 cc936.c 文件中实现了,我们只要直接把 cc936.c 文件添加到工程中就可以了。

  后面六个函数一般都不用。如真有需要可以参考 syscall.c 文件(src\option 文件夹内)。 

  底层设备驱动函数是存放在 diskio.c 文件,我们的目的就是把 diskio.c 中的函数接口与SPI Flash 芯片驱动连接起来。总共有五个函数,分别为设备状态获取(disk_status)、设备初始化(disk_initialize)、扇区读取(disk_read)、扇区写入(disk_write)、其他控制(disk_ioctl)。

6、 FatFs测试

   6.1、Flash

  FatFs 属于软件组件,不需要附带其他硬件电路。我们使用 SPI Flash 芯片作为物理存储设备,其硬件电路在上一章已经做了分析,这里就直接使用。

  FatFs 移植步骤

  FatFs 组件文件添加到工程中,需要添加 ff.cdiskio.c cc936.c 三个文件,FatFs_test.c为用户测试程序。 

  如果现在编译工程,可以发现有两个错误,一个是来自 diskio.c 文件,提示有一些头文件没找, diskio.c 文件内容是与底层设备输入输出接口函数文件,不同硬件设计驱动就不同,需要的文件也不同;另外一个错误来自 cc936.c 文件,提示该文件不是工程所必需的,这是因为 FatFs 默认使用日语,我们想要支持简体中文需要修改 FatFs 的配置,即修改 ffconf.h 文件。至此,将 FatFs 添加到工程的框架已经操作完成,接下来要做的就是修改文件和 ffconf.h 文件。

  (1)、ffconf.h文件配置

  ffconf.h 文件是 FatFs 功能配置文件,我们可以对文件内容进行修改,使得 FatFs 更符合我们的要求。 ffconf.h 对每个配置选项都做了详细的使用情况说明。

  下面只列出修改的配置,其他配置采用默认即可:

//ffconf.h
#define _USE_MKFS     1
#define _CODE_PAGE    936
#define _USE_LFN      2
#define _VOLUMES      2
#define _MIN_SS       512
#define _MAX_SS       4096

_USE_MKFS:格式化功能选择,为使用 FatFs 格式化功能,需要把它设置为 1。
_CODE_PAGE:语言功能选择,并要求把相关语言文件添加到工程宏。为支持简体中文文件名需要使用“936”。
_USE_LFN:长文件名支持,默认不支持长文件名,这里配置为 2,支持长文件名,并指定使用栈空间为缓冲区。
_VOLUMES:指定物理设备数量,这里设置为 2,包括预留 SD 卡和 SPI Flash 芯片。
_MIN_SS、_MAX_SS:指定扇区大小的最小值和最大值。 SD 卡扇区大小一般都为 512 字节, SPI Flash 芯片扇区大小一般设置为 4096 字节,所以需要把_MAX_SS 改为 4096

   (2)、底层设备驱动函数

/*-----------------------------------------------------------------------*/
/* Low level disk I/O module skeleton for FatFs     (C)ChaN, 2014        */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be        */
/* attached to the FatFs via a glue function rather than modifying it.   */
/* This is an example of glue functions to attach various exsisting      */
/* storage control modules to the FatFs module with a defined API.       */
/*-----------------------------------------------------------------------*/

#include "diskio.h"        /* FatFs lower layer API */
#include "ff.h"
#include "main.h"
//#include "usbdisk.h"    /* Example: Header file of existing USB MSD control module */
//#include "atadrive.h"    /* Example: Header file of existing ATA harddisk control module */
//#include "sdcard.h"        /* Example: Header file of existing MMC/SDC contorl module */


/*为每个设备定义一个物理编号*/
/* Definitions of physical drive number for each drive 每个驱动器的物理驱动器号的定义*/
#define ATA           0    /* Example: Map ATA harddisk to physical drive 0  将一个硬盘映射到物理驱动器0*/
#define MMC           1    /* Example: Map MMC card to physical drive 1      将MMC卡映射到物理驱动器1*/
#define USB           2    /* Example: Map USB to physical drive 2           将USB映射到物理驱动器2*/
#define SD           3    /* Example: Map SD to physical drive 3            将SD卡映射到物理驱动器3*/
#define Flash       4    /* Example: Map FLASH to physical drive 4         将Flash映射到物理驱动器4*/



/*-----------------------------------------------------------------------*/
/* Get Drive Status  获取设备状态                                       */
/*-----------------------------------------------------------------------*/
/*BYTE pdrv    :Physical drive nmuber to identify the drive 设备物理编号,通过物理驱动器编号来识别驱动器*/
DSTATUS disk_status (BYTE pdrv)
{
    DSTATUS status = STA_NOINIT & 0x00;
    //int result;

    switch(pdrv) 
    {
        case ATA:
            //result = ATA_disk_status();//获取设备状态
            break;

        case MMC:
            //result = MMC_disk_status();
            break;

        case USB:
            //result = USB_disk_status();
            break;

        case SD:
            //result = SD_disk_status();
            break;

        case Flash :
            //result = FLASH_disk_status();
            //SPI Flash状态检测:读取SPI Flash 设备ID
            if(FLASH_ID == FLASH_Read_Jedec_ID())          //设备ID读取结果正确
            {
                status = STA_NOINIT & 0x00;   
            }
            else    //设备ID读取结果错误
            {      
                status = STA_NOINIT;
            }
            break;

        default:
            status = STA_NOINIT;
            break;
    }

    return status;
}



/*-----------------------------------------------------------------------*/
/* Inidialize a Drive  设备初始化                                                        */
/*-----------------------------------------------------------------------*/
/*BYTE pdrv    :Physical drive nmuber to identify the drive 设备物理编号,通过物理驱动器编号来识别驱动器*/
DSTATUS disk_initialize(BYTE pdrv)
{
    DSTATUS status = STA_NOINIT & 0x00;
    //int result;

    switch(pdrv)
    {
        case ATA:
            //result = ATA_disk_initialize();//设备初始化 
            break;

        case MMC:
            //result = MMC_disk_initialize();
            break;

        case USB:
            //result = USB_disk_initialize();
            break;

        case SD:
            //result = SD_disk_initialize();
            break;

        case Flash:
            //result = FLASH_disk_initialize();
            SPI_1_Config_Init();
            int i=500;
            while(--i);                
            Flash_PowerOn_Mode();    //唤醒Flash     
            status = disk_status(Flash);  //获取SPI Flash芯片状态
            break;

        default:
            status = STA_NOINIT;
            break;
    }

    return status;
}



/*-----------------------------------------------------------------------*/
/* Read Sector(s)   读扇区:读取扇区内容到指定存储区                                                    */
/*-----------------------------------------------------------------------*/
/*BYTE pdrv:Physical drive nmuber to identify the drive 设备物理编号(0...)*/
/*BYTE *buff:Data buffer to store read data 数据缓存区*/
/*DWORD sector:Sector address in LBA 扇区首地址*/
/*UINT count:Number of sectors to read 扇区个数(1...128)*/
DRESULT disk_read(BYTE pdrv,BYTE *buff,    DWORD sector,UINT count)
{
    DRESULT status = STA_NOINIT & 0x00;
    //int result;

    switch (pdrv) 
    {
        case ATA:
            //result = ATA_disk_read(buff, sector, count);//读取扇区内容到指定存储区  
            break;

        case MMC:
            //result = MMC_disk_read(buff, sector, count);
            break;

        case USB:
            //result = USB_disk_read(buff, sector, count);
            break;

        case SD:
            //result = DS_disk_read(buff, sector, count);
            break;

        case Flash:
            //result = FLASH_disk_read(buff, sector, count);
            
            /*开发板使用的 SPI Flash 芯片型号为 W25Q128FV,每个扇区大小为 4096 个字节(4KB),总共有 16M 字节空间,为兼
            容后面实验程序,我们只将后部分 10MB 空间分配给 FatFs 使用,前部分 6MB 空间用于其他实验需要,即 FatFs 是从
            6MB 空间开始,为实现这个效果需要将所有的读写地址都偏移 1536 个扇区空间。    
            对于 SPI Flash 芯片,主要是使用 SPI_FLASH_BufferRead()实现在指定地址读取指定长度的数据,它接收三个参数,
            第一个参数为指定数据存放地址指针。第二个参数为指定数据读取地址,这里使用左移运算符,左移12位实际是乘以4096,
            这与每个扇区大小是息息相关的。第三个参数为读取数据个数,也是需要使用左移运算符。*/

            sector = sector + 1536; //扇区偏移6MB,外部Flash文件系统空间放在SPI Flash后面10MB空间
            FLASH_Read_Buffer(sector <<12, count<<12,buff);
            status = RES_OK;
            break;

        default:
            status = RES_PARERR;
            break;                   
    }

    return status;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)  写扇区:将数据写入指定扇区空间上                                                         */
/*-----------------------------------------------------------------------*/
/*BYTE pdrv: Physical drive nmuber to identify the drive 设备物理编号(0...)*/
/*const BYTE *buff: Data to be written 写入数据的缓存区*/
/*DWORD sector: Sector address in LBA 扇区首地址*/
/*UINT count: Number of sectors to write 扇区个数(1...128)*/
#if _USE_WRITE
DRESULT disk_write(BYTE pdrv,const BYTE *buff,DWORD sector,UINT count)
{
    DRESULT status = STA_NOINIT & 0x00;
    uint32_t write_addr; 
    //int result;
    
    if(!count)  //扇区个数为0
    {
        status = STA_NOINIT;
        return status;        
    }



    switch (pdrv) 
    {
        case ATA:
            //result = ATA_disk_write(buff, sector, count);//将数据写入指定扇区空间上  
            break;

        case MMC:
            //result = MMC_disk_write(buff, sector, count);
            break;

        case USB:
            //result = USB_disk_write(buff, sector, count);
            break;

        case SD:
            //result = SD_disk_write(buff, sector, count);
            break;

        case Flash:
            //result = FLASH_disk_write(buff, sector, count);
            /*扇区偏移6MB,外部Flash文件系统空间放在Flash后面10MB空间 */
            sector = sector + 1536;    
            write_addr = sector<<12;  
            FLASH_Erase_Sector(write_addr);    //写入数据前先擦除    
            FLASH_Write_Buffer(write_addr,count<<12,(uint8_t *)buff);    
            status = RES_OK;
            break;

        default:
            status = STA_NOINIT;
            break;  
    }

    return status;
}
#endif



/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions  其他控制                                                 */
/*-----------------------------------------------------------------------*/
/*BYTE pdrv:Physical drive nmuber (0..) 设备物理编号 */
/*BYTE cmd:Control code 控制指令,包括发出同步信号、获取扇区数目、获取扇区大小、获取擦除块数量等等指令*/
/*void *buff:Buffer to send/receive control data 写入或者读取数据地址指针*/
#if _USE_IOCTL
DRESULT disk_ioctl (BYTE pdrv,BYTE cmd,void *buff)
{
    DRESULT status = STA_NOINIT & 0x00;
    int result;

    switch (pdrv)
    {
        case ATA:
            break;

        case MMC:
            break;

        case USB:
            break;

        case SD:
            break;

        case Flash:
            switch (cmd) 
            {        
                case GET_SECTOR_COUNT:    //扇区数量:2560*4096/1024/1024=10(MB)
                    *(DWORD * )buff = 2560;        
                    break;

                case GET_SECTOR_SIZE :    //扇区大小 
                    *(WORD * )buff = 4096;
                    break;

                case GET_BLOCK_SIZE :     //同时擦除扇区个数
                    *(DWORD * )buff = 1;
                    break;

                default:
                    status = STA_NOINIT;
                    break;                                 
            }                    
            break;

        default:
            status = STA_NOINIT;
            break;
    }

    return status;
}
#endif


/*
get_fattime 函数用于获取当前时间戳,在 ff.c 文件中被调用。 FatFs 在文件创建、被修改时会记录时间,这里我们直接使用赋值方法
设定时间戳。为更好的记录时间,可以使用控制器 RTC 功能,具体要求返回值格式为:
        bit31:25 ——从 1980 至今是多少年,范围是 (0..127) ;
        bit24:21 ——月份,范围为 (1..12) ;
        bit20:16 ——该月份中的第几日,范围为(1..31) ;
        bit15:11——时,范围为 (0..23);
        bit10:5 ——分,范围为 (0..59);
        bit4:0 ——秒/ 2,范围为 (0..29) 
*/
__weak DWORD get_fattime(void) 
{
    //返回当前时间戳
    return      (     (DWORD)(2015 - 1980) << 25)        //Year 2015 
                |     ((DWORD)1 << 21)                //Month 1
                |     ((DWORD)1 << 16)                //Mday 1
                |     ((DWORD)0 << 11)                //Hour 0
                |     ((DWORD)0 << 5)                      //Min 0
                |     ((DWORD)0 >> 1);                //Sec 0
}

  FatFs 的第一步工作就是使用 f_mount 函数挂载工作区。 f_mount 函数有三个形参,第一个参数是指向 FATFS 变量指针,如果赋值为 NULL 可以取消物理设备挂载。第二个参 数为逻辑设备编号,使用设备根路径表示,与物理设备编号挂钩,在代码中我们定义 SPI Flash 芯片物理编号为 1,所以这里使用“1:”。第三个参数可选 0 或 1, 1 表示立即挂载, 0 表示不立即挂载,延迟挂载。 f_mount 函数会返回一个 FRESULT 类型值,指示运行情况。

FRESULT f_mount (
    FATFS* fs,            /* Pointer to the file system object (NULL:unmount)*/
    const TCHAR* path,    /* Logical drive number to be mounted/unmounted */
    BYTE opt            /* 0:Do not mount (delayed mount), 1:Mount immediately */
)

  如果 f_mount 函数返回值为 FR_NO_FILESYSTEM,说明没有 FAT 文件系统,比如新出厂的 SPI Flash 芯片就没有 FAT 文件系统。我们就必须对物理设备进行格式化处理。使用 f_mkfs 函数可以实现格式化操作。 f_mkfs 函数有三个形参,第一个参数为逻辑设备编号;第二参数可选 0 或者 1, 0 表示设备为一般硬盘, 1 表示设备为软盘。第三个参数指定扇区大小,如果为 0,表示通过代码中 disk_ioctl 函数获取。格式化成功后需要先取消挂载原来设备,再重新挂载设备。

FRESULT f_mkfs (
    const TCHAR* path,    /* Logical drive number */
    BYTE sfd,            /* Partitioning rule 0:FDISK, 1:SFD */
    UINT au                /* Size of allocation unit in unit of byte or sector */
)

 

  在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用 f_open 函数打开文件,不再使用文件必须使用 f_close 函数关闭文件,这个跟电脑端操作文件步骤类似。 f_open 函数有三个形参,第一个参数为文件对象指针。第二参数为目标文件,包含绝对路径的文件名称和后缀名。第三个参数为访问文件模式选择,可以是打开已经存在的文件模式、读模式、写模式、新建模式、总是新建模式等的或运行结果。比如对于写测试,使用 FA_CREATE_ALWAYS 和 FA_WRITE 组合模式,就是总是新建文件并进行写模式。f_close 函数用于不再对文件进行读写操作关闭文件, f_close 函数只要一个形参,为文件对象指针。 f_close 函数运行可以确保缓冲区完全写入到文件内。

FRESULT f_open (
    FIL* fp,            /* Pointer to the blank file object */
    const TCHAR* path,    /* Pointer to the file name */
    BYTE mode            /* Access mode and file open mode flags */
)
FRESULT f_close (
    FIL *fp        /* Pointer to the file object to be closed */
)

  成功打开文件之后就可以使用 f_write 函数和 f_read 函数对文件进行写操作和读操作。这两个函数用到的参数是一致的,只不过一个是数据写入,一个是数据读取。 f_write 函数第一个形参为文件对象指针,使用与 f_open 函数一致即可。第二个参数为待写入数据的首地址,对于 f_read 函数就是用来存放读出数据的首地址。第三个参数为写入数据的字节数,对于 f_read 函数就是欲读取数据的字节数。第四个参数为 32 位无符号整形指针,这里使用fnum 变量地址赋值给它,在运行读写操作函数后, fnum 变量指示成功读取或者写入的字节个数。

FRESULT f_write (
    FIL* fp,            /* Pointer to the file object */
    const void *buff,    /* Pointer to the data to be written */
    UINT btw,            /* Number of bytes to write */
    UINT* bw            /* Pointer to number of bytes written */
)
FRESULT f_read (
    FIL* fp,         /* Pointer to the file object */
    void* buff,        /* Pointer to data buffer */
    UINT btr,        /* Number of bytes to read */
    UINT* br        /* Pointer to number of bytes read */
)

  最后,不再使用文件系统时,使用 f_mount 函数取消挂载。

   注意:使用文件系统时,引脚初始化需要放在系统中


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM