微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Flash设备驱动

在linux系统中,提供了MTD(内存技术设备)系统来建立Flash针对linux的统一,抽象接口,MTD将文件系统与底层的Flash存取器进行了隔离,使得Flash驱动工程师无需关心Flash作为字符设备和块设备与Linux内核接口(由MTD层完成) 在引入MTD后,Linux系统中Flash设备驱动及接口可分四层 (1)硬件驱动层,Flash硬件驱动层负责Flash硬件设备读,写,擦除 LInux MTD 设备的 nor Flash 芯片驱动位于 drivers/mtd/chips 子目录下, NAND Flash的驱动程序则 位于 drivers/mtd/nand 子目录下。 (2)MTD原始设备层:MTD原始设备层有两部分组成,一部分是MTD原始设备的通用代码,另一部分是特定Flash数据,如分区等 (3)MTD设备层:基于MTD原始设备层,Linux系统可以定义出MTD的块设备(主设备号31)和字符设备(主设备号90)MTD字符设备定义在mtdchar.c中实现,通过注册一系列file_operations的函数(lseek,open,clos,read,write等),可实现设备读写控制,MTD 块设备则是定义在一个描述MTD 块设备的结构 mtdblk_dev ,并声明了一个名为 mtdblks 的指针数组,这个数组 中的每个mtdblk_dev 和 mtd_table 中的每一个mtd_info 一一对应。 (4)块设备节点:通过mknod在/dev子目录下建立MTD字符设备节点和MTD块设备节点   LInux MTD系统接口 在引入MTD后,底层Flash驱动直接与MTD原始设备层交互,利用其提供的接口注册设备和分区 用于描述MTD原始设备的数据结构是mtd_info,这其中定义了大量关于MTD的数据和操作函数,mtd_info是表示MTD原始设备结构体,每个分区也被认为是一个mtd_info,这些mtd_info指针被存放在mtd_table的数组里,这个结构体定义如下: struct mtd_info {  u_char type;//内存技术类型  uint32_t flags;//标志位  uint64_t size;  // MTD设备大小Total size of the MTD    unsigned int erasesize_shift;  unsigned int writesize_shift;    unsigned int erasesize_mask;  unsigned int writesize_mask;  // Kernel-only stuff starts here.  const char *name;  int index;    struct nand_ecclayout *ecclayout;    int numeraseregions;  struct mtd_erase_region_info *eraseregions;    int (*erase) (struct mtd_info *mtd,struct erase_info *instr);      int (*point) (struct mtd_info *mtd,loff_t from,size_t len,   size_t *retlen,void **virt,resource_size_t *phys);    void (*unpoint) (struct mtd_info *mtd,size_t len);    unsigned long (*get_unmapped_area) (struct mtd_info *mtd,         unsigned long len,         unsigned long offset,         unsigned long flags);    struct backing_dev_info *backing_dev_info;  int (*read) (struct mtd_info *mtd,size_t *retlen,u_char *buf);  int (*write) (struct mtd_info *mtd,loff_t to,const u_char *buf);    int (*panic_write) (struct mtd_info *mtd,const u_char *buf);  int (*read_oob) (struct mtd_info *mtd,    struct mtd_oob_ops *ops);  int (*write_oob) (struct mtd_info *mtd,    struct mtd_oob_ops *ops);    int (*get_fact_prot_info) (struct mtd_info *mtd,struct otp_info *buf,size_t len);  int (*read_fact_prot_reg) (struct mtd_info *mtd,u_char *buf);  int (*get_user_prot_info) (struct mtd_info *mtd,size_t len);  int (*read_user_prot_reg) (struct mtd_info *mtd,u_char *buf);  int (*write_user_prot_reg) (struct mtd_info *mtd,u_char *buf);  int (*lock_user_prot_reg) (struct mtd_info *mtd,size_t len);    int (*writev) (struct mtd_info *mtd,const struct kvec *vecs,unsigned long count,size_t *retlen);    void (*sync) (struct mtd_info *mtd);    int (*lock) (struct mtd_info *mtd,loff_t ofs,uint64_t len);  int (*unlock) (struct mtd_info *mtd,uint64_t len);    int (*suspend) (struct mtd_info *mtd);  void (*resume) (struct mtd_info *mtd);    int (*block_isbad) (struct mtd_info *mtd,loff_t ofs);  int (*block_markbad) (struct mtd_info *mtd,loff_t ofs);  struct notifier_block reboot_notifier;     struct mtd_ecc_stats ecc_stats;    int subpage_sft;  void *priv;  struct module *owner;  struct device dev;  int usecount;    int (*get_device) (struct mtd_info *mtd);  void (*put_device) (struct mtd_info *mtd); }; mtd_info的type字段给出了底层物理设备的类型,包括MTD_RAM,MTD_ROM,MTD_norFLASH,MTD_NANSFLASH等 flags字段标志可以是MTD_WRITEABLE,MTD_BIT_WRITEABLE,MTD_NO_ERASE,MTD_POWERUP_LOCK等的组合 mtd_info中的read(),write(),read_oob(),write_oob(),erase()是MTD设备驱动要实现的主要函数,在Linux中MTD下层实现了针对nor Flash和NAND Flash通用的mtd_info成员函数 某些内存技术带有额外数据(OOB),例如NAND Flash没512字节就会有16个字节的“额外数据”   Flash驱动中使用如下两个函数注册和注销MTD设备 int add_mtd_device(struct mtd_info *mtd); int del_mtd_device(struct mtd_info *mtd); 下面代码中mtd_part结构体用于表示分区,其mtd_info成员结构体用于描述分区,它会被加入到mtd_table(定义为struct mtd_info *mtd_table[MAX_MTD_DEVICES])中,其大部分成员由其主分区mtd_part->master决定,各种函数也指向主分区相应的函数 struct mtd_part {  struct mtd_info mtd;//分区信息(大部分由master决定)  struct mtd_info *master;//该分区的主分区  uint64_t offset;//该分区的偏移地址  int index;// 主分区号  struct list_head list;  int registered; }; mtd_partition(分区数组)会在MTD原始设备层调用add_mtd_partitions()时传递分区信息用,定义如下: struct mtd_partition {  char *name;   //标示字符串  uint64_t size;   //分区大小  uint64_t offset;  //主MTD内偏移地址  uint32_t mask_flags;  //掩码标志  struct nand_ecclayout *ecclayout; //OOB布局out of band layout for this partition (NAND only)  struct mtd_info **mtdp;   }; Flash驱动中使用如下两个函数注册和注销分区 int add_mtd_partitions(struct mtd_info *master,struct mtd_partition *parts,int nbparts) int del_mtd_partitions(struct mtd_info *master); add_mtd_partitions()会对每一个新建立的分区建立一个mtd_part结构体,并将其加入mtd_partions(分区数组)中,并调用add_mtd_device()将此分区作为MTD设备键入mtd_table,成功时返回0,否则返回-ENOMEM del_mtd_partitions()的作用是对于mtd_partitions上的每一个分区,如果它的主分区是master(要被删除的分区号),则将它从mtd_partitions和mtd_table中删除,这是要调用del_mtd_device; add_mtd_partitions()中新建的mtd_part需要依赖于传进的mtd_partition(分区数组)参数对其进行初始化 int add_mtd_partitions(struct mtd_info *master,         const struct mtd_partition *parts,         int nbparts) {  struct mtd_part *slave;  uint64_t cur_offset = 0;  int i;  printk(KERN_NOTICE "Creating %d MTD partitions on \"%s\":\n",nbparts,master->name);  for (i = 0; i < nbparts; i++) {   slave = add_one_partition(master,parts + i,i,cur_offset);   if (!slave)    return -ENOMEM;   cur_offset = slave->offset + slave->mtd.size;  }  return 0; } static struct mtd_part *add_one_partition(struct mtd_info *master,  const struct mtd_partition *part,int partno,  uint64_t cur_offset) {  struct mtd_part *slave;    slave = kzalloc(sizeof(*slave),GFP_KERNEL);  if (!slave) {   printk(KERN_ERR"memory allocation error while creating partitions for \"%s\"\n",   master->name);   del_mtd_partitions(master);   return NULL;  }  list_add(&slave->list,&mtd_partitions);    slave->mtd.type = master->type;  slave->mtd.flags = master->flags & ~part->mask_flags;  slave->mtd.size = part->size;  slave->mtd.writesize = master->writesize;  slave->mtd.oobsize = master->oobsize;  slave->mtd.oobavail = master->oobavail;  slave->mtd.subpage_sft = master->subpage_sft;  slave->mtd.name = part->name;  slave->mtd.owner = master->owner;  slave->mtd.backing_dev_info = master->backing_dev_info; ... }   MTD用户空间编程 drives/mtd/mtdchar.c文件实现了MTD字符设备接口,通过它用户可以直接操作Flash设备,通过read(),write系统调用可以读写Flash,通过一系列IOCTL命令可以获得Flash设备信息,擦除Flash,读写NAND的OOB等   nor Flash驱动 在Linux中,实现了针对CFI(公共Flash接口),JEDEC等接口的通用nor驱动,这一层驱动直接面对mtd_info的成员函数,这使得nor的芯片级驱动变得十分简单,只需定义具体内存映射情况结构体mao_info并使用指定调用do_mao_probe()探测mtd_info()就可以了 nor Flash驱动的核心是定义map_info结构体,他指定nor Flash的基址,位宽,大小等信息,然后调用do_map_probe()探测芯片就可以了,map_info原型如下: struct map_info {  const char *name;  unsigned long size;  resource_size_t phys; #define NO_XIP (-1UL)  void __iomem *virt;  void *cached;  int bankwidth; #ifdef CONfig_MTD_COMPLEX_MAPPINGS  map_word (*read)(struct map_info *,unsigned long);  void (*copy_from)(struct map_info *,void *,unsigned long,ssize_t);  void (*write)(struct map_info *,const map_word,unsigned long);  void (*copy_to)(struct map_info *,const void *,ssize_t);   #endif    void (*inval_cache)(struct map_info *,ssize_t);    void (*set_vpp)(struct map_info *,int);  unsigned long pfow_base;  unsigned long map_priv_1;  unsigned long map_priv_2;  void *fldrv_priv;  struct mtd_chip_driver *fldrv; }; nor Flash驱动在Linux中实现,主要工作如下: (1)定义map_info的实例,初始化其中的成员,根据目标板的情况name,size,bankwidth和phys赋值 (2)如果Flash需要分区,则定义mtd_partition数组,将实际电路板中Flash分区信息记录在其中 (3)以map_info和探测的接口类型(如“ch_probe”,"jedec_probe"等)为参数调用do_map_probe()探测Flash得到mtd_info do_map_probe()函数原型如下: struct mtd_info *do_map_probe(const char *name,struct map_info *map); 第一个为探测接口类型,常见的如下 do_map_probe("cfi_probe",&xxx_map_info); do_map_probe("jedec_probe",&xxx_map_info); do_map_probe("map_rom",&xxx_map_info); do_map_probe()会根据传入参数name(接口类型),通过get_mtd_chip_driver()得到具体的MTD驱动,如下所示: struct mtd_info *do_map_probe(const char *name,struct map_info *map) {  struct mtd_chip_driver *drv;  struct mtd_info *ret;  drv = get_mtd_chip_driver(name);  if (!drv && !request_module("%s",name))   drv = get_mtd_chip_driver(name);//通过接口类型找到驱动  if (!drv)   return NULL;  ret = drv->probe(map);    module_put(drv->module);  if (ret)   return ret;  return NULL; }  (4)在模块初始化时以mtd_info为参数调用add_mtd_device()或以mtd_info,partition数组为参数调用add_mtd_partitions()注册设备或分区 (5)在模块卸载时调用“反函数”来删除设备或分区 下面是一个nor Flash的驱动 #define WINDOW_SIZE ... #define WINDOW_ADDR ... static struct map_info xxx_map = {//定义并初始化map_info .name = "xxx_Flash",.size = WINDOW_SIZE,//大小 .bankwidth = 1,//总线宽度 .phys = WINDOW_ADDR//物理地址 };   static struct mtd_partition xxx_partitions[] = {//定义数组用于分区 { .name = "Drive A",.offset = 0,//分区偏移地址 .size = 0x0e0000//分区大小 }, ... };   #define NUM_PARTITIONS ARRAY_SIZE(xxx_partitions)   static struct mtd_info *mymtd;   static int __init init_xxx_map(void) { int rc = 0;   xxx_map.virt = ioremap_nocache(xxx_map.phys,xxx_map.size);//物理地址映射为虚拟地址 ... mymtd = do_map_probe("jedec_probe",&xxx_map);//探测nor Flash得到mtd_info mymtd->owner = THIS_MODULE; add_mtd_partitions(mymtd,xxx_partitions,NUM_PARTTITIONS);//添加分区信息 ...   } struct void __exit cleanup_xxx_map(void0 { if(mymtd){ del_mtd_partitions(mymtd);//删除分区 map_destroy(mymtd); } ... }    NAND Flash驱动 和nor Flash非常类似,在Linux内核MTD的下层已经实现了通用的NAND驱动(在driver/mtd/nand/nand_bash.c中)即实现了mtd_info结构体的成员函数 MTD使用nand_chip数据结构表示一个NAND Flash芯片,这个结构体中包含了关于NAND Flash的地址信息,读写方法,ECC模式等,这个结构体原型如下: struct nand_chip {  void  __iomem *IO_ADDR_R;  void  __iomem *IO_ADDR_W;  uint8_t  (*read_byte)(struct mtd_info *mtd);  u16  (*read_word)(struct mtd_info *mtd);  void  (*write_buf)(struct mtd_info *mtd,const uint8_t *buf,int len);  void  (*read_buf)(struct mtd_info *mtd,uint8_t *buf,int len);  int  (*verify_buf)(struct mtd_info *mtd,int len);  void  (*select_chip)(struct mtd_info *mtd,int chip);  int  (*block_bad)(struct mtd_info *mtd,int getchip);  int  (*block_markbad)(struct mtd_info *mtd,loff_t ofs);  void  (*cmd_ctrl)(struct mtd_info *mtd,int dat,        unsigned int ctrl);  int  (*dev_ready)(struct mtd_info *mtd);  void  (*cmdfunc)(struct mtd_info *mtd,unsigned command,int column,int page_addr);  int  (*waitfunc)(struct mtd_info *mtd,struct nand_chip *this);  void  (*erase_cmd)(struct mtd_info *mtd,int page);  int  (*scan_bbt)(struct mtd_info *mtd);  int  (*errstat)(struct mtd_info *mtd,struct nand_chip *this,int state,int status,int page);  int  (*write_page)(struct mtd_info *mtd,struct nand_chip *chip,          const uint8_t *buf,int page,int cached,int raw);  int  chip_delay;  unsigned int options;  int  page_shift;  int  phys_erase_shift;  int  bbt_erase_shift;  int  chip_shift;  int  numchips;  uint64_t chipsize;  int  pagemask;  int  pagebuf;  int  subpagesize;  uint8_t  cellinfo;  int  badblockpos;  nand_state_t state;  uint8_t  *oob_poi;  struct nand_hw_control  *controller;  struct nand_ecclayout *ecclayout;  struct nand_ecc_ctrl ecc;  struct nand_buffers *buffers;  struct nand_hw_control hwcontrol;  struct mtd_oob_ops ops;  uint8_t  *bbt;  struct nand_bbt_descr *bbt_td;  struct nand_bbt_descr *bbt_md;  struct nand_bbt_descr *badblock_pattern;  void  *priv; }; 与nor Flash一样,由于有了MTD层,完成一个NAND Flash驱动在Linux中的工作量也很少,主要工作如下: (1)如果Flash需要分区,则定义mtd_partition数组,将实际电路板中Flash分区信息记录以其中 (2)在模块加载是分配nand_chip的内存,根据目标板NAND控制器的情况初始化nand_chip中的cmd_ctrl(),dev_ready(),ready_byte(),write_buf()等成员(如果不赋值就使用nand_base.c中的函数),注意将mtd_info的priv指向nand_chip (3)以mtd_info为参数调用nand_scan()函数探测NAND Flash的存在得到mtd_info,nand_scan()函数原型如下: int nand_scan(struct mtd_info *mtd,int maxchips); (4)如果要分区则以mtd_info,mtd_partition为参数调用add_mtd_partitions()函数添加分区 下面是一个NAND Flash设备驱动模块 #define CHIP_PHYSICAL_ADDRESS  ... #define NUM_PARTITIONS 2 static const struct mtd_partition partition_info[] = {  {   .name = "NAND FS 0",  .offset = 0,  .size = 8 * 1024 * 1024}, {   .name = "NAND FS 1",  .offset = MTDPART_OFS_APPEND,//表示接着上面结束地址   .size = MTDPART_SIZ_FULL} }; static int __init au1xxx_nand_init(void) {  struct nand_chip *this;  u16 boot_swapboot = 0;   int retval;  u32 mem_staddr;  u32 nand_phys;   //初始化结构体(分配内存)  au1550_mtd = kmalloc(sizeof(struct mtd_info) + sizeof(struct nand_chip),GFP_KERNEL);  if (!au1550_mtd) {   printk("Unable to allocate NAND MTD dev structure.\n");   return -ENOMEM;  }    this = (struct nand_chip *)(&au1550_mtd[1]);   //初始化成员函数  memset(au1550_mtd,sizeof(struct mtd_info));  memset(this,sizeof(struct nand_chip));    au1550_mtd->priv = this;  au1550_mtd->owner = THIS_MODULE;    au_writel(0,MEM_STNDCTL); #ifdef CONfig_MIPS_PB1550    au_writel(au_readl(GPIO2_DIR) & ~(1 << 6),GPIO2_DIR);  boot_swapboot = (au_readl(MEM_STSTAT) & (0x7 << 1)) | ((bcsr->status >> 6) & 0x1);  switch (boot_swapboot) {  case 0:  case 2:  case 8:  case 0xC:  case 0xD:      nand_width = 0;   break;  case 1:  case 9:  case 3:  case 0xE:  case 0xF:      nand_width = 1;   break;  default:   printk("Pb1550 NAND: bad boot:swap\n");   retval = -EINVAL;   goto outmem;  } #endif   #ifdef NAND_STCFG  if (NAND_CS == 0) {   au_writel(NAND_STCFG, MEM_STCFG0);   au_writel(NAND_STTIME,MEM_STTIME0);   au_writel(NAND_STADDR,MEM_STADDR0);  }  if (NAND_CS == 1) {   au_writel(NAND_STCFG, MEM_STCFG1);   au_writel(NAND_STTIME,MEM_STTIME1);   au_writel(NAND_STADDR,MEM_STADDR1);  }  if (NAND_CS == 2) {   au_writel(NAND_STCFG, MEM_STCFG2);   au_writel(NAND_STTIME,MEM_STTIME2);   au_writel(NAND_STADDR,MEM_STADDR2);  }  if (NAND_CS == 3) {   au_writel(NAND_STCFG, MEM_STCFG3);   au_writel(NAND_STTIME,MEM_STTIME3);   au_writel(NAND_STADDR,MEM_STADDR3);  } #endif    mem_staddr = 0x00000000;  if (((au_readl(MEM_STCFG0) & 0x7) == 0x5) && (NAND_CS == 0))   mem_staddr = au_readl(MEM_STADDR0);  else if (((au_readl(MEM_STCFG1) & 0x7) == 0x5) && (NAND_CS == 1))   mem_staddr = au_readl(MEM_STADDR1);  else if (((au_readl(MEM_STCFG2) & 0x7) == 0x5) && (NAND_CS == 2))   mem_staddr = au_readl(MEM_STADDR2);  else if (((au_readl(MEM_STCFG3) & 0x7) == 0x5) && (NAND_CS == 3))   mem_staddr = au_readl(MEM_STADDR3);  if (mem_staddr == 0x00000000) {   printk("Au1xxx NAND: ERROR WITH NAND CHIP-SELECT\n");   kfree(au1550_mtd);   return 1;  }  nand_phys = (mem_staddr << 4) & 0xFFFC0000;  p_nand = (void __iomem *)ioremap(nand_phys,0x1000);    if (NAND_CS == 0)   nand_width = au_readl(MEM_STCFG0) & (1 << 22);  if (NAND_CS == 1)   nand_width = au_readl(MEM_STCFG1) & (1 << 22);  if (NAND_CS == 2)   nand_width = au_readl(MEM_STCFG2) & (1 << 22);  if (NAND_CS == 3)   nand_width = au_readl(MEM_STCFG3) & (1 << 22);    this->dev_ready = au1550_device_ready;  this->select_chip = au1550_select_chip;  this->cmdfunc = au1550_command;    this->chip_delay = 30;  this->ecc.mode = NAND_ECC_SOFT;  this->options = NAND_NO_AUTOINCR;  if (!nand_width)   this->options |= NAND_BUSWIDTH_16;  this->read_byte = (!nand_width) ? au_read_byte16 : au_read_byte;  au1550_write_byte = (!nand_width) ? au_write_byte16 : au_write_byte;  this->read_word = au_read_word;  this->write_buf = (!nand_width) ? au_write_buf16 : au_write_buf;  this->read_buf = (!nand_width) ? au_read_buf16 : au_read_buf;  this->verify_buf = (!nand_width) ? au_verify_buf16 : au_verify_buf;    if (nand_scan(au1550_mtd,1)) {//探测NAND Flash   retval = -ENXIO;   goto outio;  }   //注册分区  add_mtd_partitions(au1550_mtd,partition_info,ARRAY_SIZE(partition_info));  return 0;  outio:  IoUnmap((void *)p_nand);  outmem:  kfree(au1550_mtd);  return retval; } 最后强调的是,在NAND芯片驱动中,如果nand_chip中没有赋值,将使用认值(nand_base.c中的认值),下面是nand_chip中的nand_ecc_ctrl结构体类型成员ecc赋值,定义OOB的分布模式 static struct nand_ecclayout nand_oob_8 = {  .eccbytes = 3, .eccpos = {0,1,2}, .oobfree = {   {.offset = 3,   .length = 2},  {.offset = 6,   .length = 2}} }; static struct nand_ecclayout nand_oob_16 = {  .eccbytes = 6,2,3,6,7}, .oobfree = {   {.offset = 8,   . length = 8}} }; static struct nand_ecclayout nand_oob_64 = {  .eccbytes = 24, .eccpos = {      40,41,42,43,44,45,46,47,     48,49,50,51,52,53,54,55,     56,57,58,59,60,61,62,63}, .oobfree = {   {.offset = 2,   .length = 38}} }; static struct nand_ecclayout nand_oob_128 = {  .eccbytes = 48, .eccpos = {      80,81,82,83,84,85,86,87,     88,89,90,91,92,93,94,95,     96,97,98,99,100,101,102,103,     104,105,106,107,108,109,110,111,     112,113,114,115,116,117,118,119,     120,121,122,123,124,125,126,127},   .length = 78}} }; nor Flash驱动实例 针对S3C2410等平台而言。外接nor Flash的情况下,由于nor Flash直接映射到cpu的内存空间,为了使用nor Flash驱动,我们只需要在BSP的板文件添加相应的信息, 例如nor Flash所在的物理地址和到校,分区信息,总线宽度等,这些信息以platform资源和数据的形式呈现,如下: static struct resource nor_resource = { .start = OMAP_CS0_PHYS,.end = OMAP_CS0_PHYS + SZ_32M - 1,.flags = IORESOURCE_MEM,//表示地址 }; static struct mtd_partition nor_partitions[] = {//分区结构体 {      .name = "bootloader",     .offset = 0,     .size = SZ_128K,     .mask_flags = MTD_WRITEABLE,},{      .name = "params",     .offset = MTDPART_OFS_APPEND,     .mask_flags = 0,{      .name = "kernel",     .size = SZ_2M,     .mask_flags = 0 },{      .name = "rootfs",     .size = MTDPART_SIZ_FULL,}; static struct flash_platform_data nor_data = { .map_name = "cfi_probe",.width = 2,.parts = nor_partitions,.nr_parts = ARRAY_SIZE(nor_partitions),}; static struct platform_device nor_device = { .name = "omapflash",//应该和驱动里的(physmap.c)一致 .id = 0,.dev = { .platform_data = &nor_data,.num_resources = 1,//资源数 .resource = &nor_resource,//资源结构体 }; 下面来总结一下: 由于引入了MTD系统以及MTD下层的通用nor和NAND驱动,Linux中nor和NAND Flash芯片级驱动的设计难度大大降低,对于nor驱动工作仅仅只需在BSP中添加相关的platform信息。 在串口驱动中,讲解了tty_driver到uart_driver的角色转换,在Flash驱动中,讲解了mtd_info向map_info/nand_chip的转移,可以说,Linux驱动这种分层设计思想是贯穿个中Linux驱动框架的。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐