zoukankan      html  css  js  c++  java
  • 两年前实习时的文档——MMC学习总结


    概述

    驱动程序实际上是硬件与应用程序之间的中间层。在Linux操作系统中,设备驱动程序对各种不同的设备提供了一致的訪问接口,把设备映射成一个特殊的设备文件,用户程序能够像其它文件一样对设备文件进行操作。

    Linux2.6引入了新的设备管理机制kobject,通过这个数据结构使全部设备在底层都具有统一的接口,kobject提供主要的对象管理,是构成Linux2.6设备模型的核心结构,它与sysfs文件系统紧密联系,每一个在内核中注冊的kobject对象都相应于sysfs文件系统中的一个文件夹。

    在这些内核对象机制的基础上,Linux的设备模型包含设备结构device、驱动程序driver、总线bus和设备类结构class几个关键组件。

    一个现实的linux设备和驱动通常都须要挂接在一种总线上,比較常见的总线有USB、PCI总线等。可是,在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设却不依附于此类总线。基于这种背景下,2.6内核增加了platform虚拟总线。Platform机制将设备本身的资源注冊进内核,由内核统一管理,在驱动程序使用这些资源时使用统一的接口,这样提高了程序的可移植性。

    Platform设备概念的引入是可以更好地描写叙述设备的资源信息。Platform设备是系统中自治的实体,包含基于port的设备、外围总线和集成入片上系统平台的大多数控制器,它们通常直接通过CPU的总线寻址。每一个platform设备被赋予一个名称,并分配一定数量的资源。

    Platform总线对增加到该总线的设备和驱动分别封装了结构体——platform_device和platform_driver而且提供了相应的注冊函数。

    图1 Platform虚拟总线

    由上图可知,在platform虚拟总线上我们分别对device和driver进行注冊,这样我们可以更加方便的进行驱动设备的管理。这样当有总线或者设备注冊到该虚拟总线上时,内核自己主动的调用platform_match函数将platform_device绑定到platform_driver上。


    2  SDIO启动过程

    在kernel启动时,内核会自己主动的调用MODULE_INIT宏对模块进行载入,MODULE_INIT声明了模块的入口函数。在MMC中我们模块的运行顺序例如以下图所看到的:


    图2 SDIO启动过程

    在这个过程中内核首先调用xxx_init函数,init函数对device进行注冊,不管什么设备在内核中都会调用driver_register函数,driver_register函数经过一系列的调用,终于会调用探測函数probe(后面具体解说)。probe函数会对模块进行探測工作,不断的对SD/MMC/SDIO卡进行扫描。


    3  platform相关

    3.1  platform数据结构

    Linux在启动的时候就注冊了platform总线,看内核源代码:

    struct bus_type platform_bus_type = {

           .name             ="platform",

           .dev_attrs       = platform_dev_attrs,

           .match            =platform_match,

           .uevent           =platform_uevent,

           .pm         =&platform_dev_pm_ops,

    };

    能够看到,总线中定义了.match函数,当有总线或者设备注冊到platform总线时,内核自己主动调用.match函数,推断device和driver的name是否一致。

    platform_device的结构体定义例如以下:

    struct platform_device {

           constchar       * name;//设备名字,这将取代device->dev_id,用作sys/device下显示文件夹名

           int          id;//设备ID,用于给插入该总线而且具有同样name的设备编号,假设仅仅有一个设备的话填-1

           structdevice   dev;//结构体中内嵌的device结构体

           u32         num_resources;//资源数

           structresource * resource;//用于存放资源的数组

           conststruct platform_device_id     *id_entry;

           /*arch specific additions */

           structpdev_archdata      archdata;

    };

    能够看出,在platform_device中定义了name,而且内嵌了structdevice结构体。另外,包括的structresource例如以下:

    struct resource {

           resource_size_tstart;

           resource_size_tend;

           constchar *name;

           unsignedlong flags;

           structresource *parent, *sibling, *child;

    };

    platform_driver的结构体定义例如以下:

    struct platform_driver {

           int(*probe)(struct platform_device *);

           int(*remove)(struct platform_device *);

           void(*shutdown)(struct platform_device *);

           int(*suspend)(struct platform_device *, pm_message_t state);

           int(*resume)(struct platform_device *);

           structdevice_driver driver;

           conststruct platform_device_id *id_table;

    };

    能够看出,在platform_driver中内嵌了structdevice_driver,以及一些回调函数。structdevice_driver的详细定义例如以下:

    struct device_driver {

           constchar              *name;

           int(*probe) (struct device *dev);

           int(*remove) (struct device *dev);

           void(*shutdown) (struct device *dev);

           int(*suspend) (struct device *dev, pm_message_t state);

           int(*resume) (struct device *dev);

           conststruct attribute_group **groups;

           conststruct dev_pm_ops *pm;

           structdriver_private *p;

    };

    此处定义了name用于和platform_device匹配。由于platform_device和platform_driver的匹配就是通过内嵌的structdevice和structdevice_driver的name进行匹配的。

    static int platform_match(structdevice *dev, struct device_driver *drv)

    {

           structplatform_device *pdev = to_platform_device(dev);

           structplatform_driver *pdrv = to_platform_driver(drv);

           /*match against the id table first */

           if(pdrv->id_table)

                  returnplatform_match_id(pdrv->id_table, pdev) != NULL;

           /*fall-back to driver name match */

           return(strcmp(pdev->name, drv->name) == 0);

    }

    在数据结构设计上,总线、设备及驱动三者相互关联platform  device包括device,依据device能够获得对应的bus及driver。

    设备加入到总线上之后形成一个双向循环链表,依据总线能够获得其上挂接的全部device,进而获得了platform  device。依据device也能够获得驱动该总线上全部设备对应的driver。Platform包括driver,依据driver获得对应的bus,进而获得bus上全部的device,进一步获得platformdevice。依据name对driver和platform  device进行匹配,匹配成功后将device与对应的driver关联起来,即实现了platformdevice与platformdriver的关联。

    匹配成功后调用driver的probe进而调用platformdriver的probe,在probe里实现驱动特定的功能。

    Match函数仅仅是简单的进行字符串匹配,这就是强调platformdevice和platform  driver中那么属性须要一致的原因。

    3.2  platform device

    3.2.1  register

    注冊一个platform层的设备。注冊后,会在sys/device文件夹下创建一个以name命名的文件夹,而且创建软连接到/sys/bus/platform/device下。

    int platform_device_register(structplatform_device *pdev)

    {

           device_initialize(&pdev->dev);

           returnplatform_device_add(pdev);

    }

    当中,第一步device_initialize(在kernel目录下)用于初始化一个structdevice。函数的定义例如以下:

    void device_initialize(struct device*dev)

    {

           dev->kobj.kset= devices_kset;

           kobject_init(&dev->kobj,&device_ktype);

           INIT_LIST_HEAD(&dev->dma_pools);

           mutex_init(&dev->mutex);

           lockdep_set_novalidate_class(&dev->mutex);

           spin_lock_init(&dev->devres_lock);

           INIT_LIST_HEAD(&dev->devres_head);

           device_pm_init(dev);

           set_dev_node(dev,-1);

    }

    第二步,加入一个platform device到device层 。函数的定义例如以下:

    int platform_device_add(structplatform_device *pdev)

    {

           inti, ret = 0;

           if(!pdev)

                  return -EINVAL;

           if(!pdev->dev.parent)

           pdev->dev.parent= &platform_bus;//假设p->dev.parent不存在则赋值&platform_bus

           pdev->dev.bus= &platform_bus_type;//设置pdev->dev.bus的bus类型

           if(pdev->id != -1)    //pdev->id!=-1说明存在多于一个的设备

                  dev_set_name(&pdev->dev,"%s.%d", pdev->name, pdev->id);

           Else   //否则对唯一的设备进行命名

                  dev_set_name(&pdev->dev,"%s", pdev->name);

           for(i = 0; i < pdev->num_resources; i++) {

        //遍历资源而且资源增加到资源数组中

                  struct resource *p, *r =&pdev->resource[i];

                  if (r->name == NULL)

                         r->name= dev_name(&pdev->dev);

                  p = r->parent;

                  if (!p) {

                         if(resource_type(r) == IORESOURCE_MEM)

                                p = &iomem_resource;

                         elseif (resource_type(r) == IORESOURCE_IO)

                                p = &ioport_resource;

                  }

                  if (p && insert_resource(p, r)) {

                         printk(KERN_ERR

                                "%s: failed to claim resource%d ",

                                dev_name(&pdev->dev), i);

                         ret= -EBUSY;

                         gotofailed;

                  }

           }

           pr_debug("Registeringplatform device '%s'. Parent at %s ",

                   dev_name(&pdev->dev),dev_name(pdev->dev.parent));

           ret= device_add(&pdev->dev);//(在core/core.c中定义)

           if(ret == 0)

                  return ret;

     failed:

           while(--i >= 0) {

                  struct resource *r =&pdev->resource[i];

                  unsigned long type = resource_type(r);

                  if (type == IORESOURCE_MEM || type ==IORESOURCE_IO)

                         release_resource(r);

           }

           returnret;

    }

    3.2.2  unregister

    platform_device_unregister用于注销一个platform-leveldevice。函数的定义例如以下:

    voidplatform_device_unregister(struct platform_device *pdev)

    {

           platform_device_del(pdev);

           platform_device_put(pdev);

    }

    在注销一个设备时,第一步调用platform_device_del函数。此函数的定义例如以下:

    void platform_device_del(struct platform_device*pdev)

    {

           inti;

           if(pdev) {

                  device_del(&pdev->dev);

    //遍历全部的resource假设是IORESOURCE_MEM或者IORESOURCE_IO类型则调用release_resource函数释放掉resource。

                  for (i = 0; i <pdev->num_resources; i++) {

                         structresource *r = &pdev->resource[i];

                         unsignedlong type = resource_type(r);

                         if(type == IORESOURCE_MEM || type == IORESOURCE_IO)

                                release_resource(r);

                  }

           }

    }

    此函数的作用是:移除一个platform-leveldevice。当中的IOSOURCE_MEM和IORESOURCE_IR是CPU对外设I/O端口物理地址的两种编制方式。Resource是一个指向platform资源数组的指针,该数组有num_resource个资源,以下是资源结构体的定义(linux/ioport.h):

    struct resource {

           resource_size_tstart;  //起始地址

           resource_size_tend;   //终止地址

           constchar *name;     //名称

           unsignedlong flags;    //标志

           structresource *parent, *sibling, *child;

    };

    在structplatform_device中能够设置多种资源信息。资源的flags标志包含:

    #define  IORESOURCE_IO      0x00000100   //IO资源

    #define  IORESOURCE_MEM   0x00000200  //内存资源

    #define  IORESOURCE_IRQ    0x00000400  //中断资源

    #define  IORESOURCE_DMA   0x00000800  //DMA资源

    第二步,调用platform_device_put函数。

    void platform_device_put(structplatform_device *pdev)

    {

           if(pdev)

                  put_device(&pdev->dev);

    }

    销毁一个platformdevice,而且释放全部与这个platformdevice相关的内存。

    3.3  platform driver

    3.3.1  register

    通过调用函数platform_driver_register实现为platformlevel的设备注冊一个驱动。注冊成功后,内核会在/sys/bus/platform/driver/文件夹下创建一个名字为driver->name的文件夹。详细的函数定义例如以下:

    int platform_driver_register(structplatform_driver *drv)

    {

           drv->driver.bus= &platform_bus_type;

           if(drv->probe)

                  drv->driver.probe =platform_drv_probe;

           if(drv->remove)

                  drv->driver.remove =platform_drv_remove;

           if(drv->shutdown)

                  drv->driver.shutdown =platform_drv_shutdown;

           returndriver_register(&drv->driver);

    }

    此函数首先对struct  platform_driver变量的driver进行赋值。然后调用driver_register而且返回。driver_register函数的定义例如以下:

    int driver_register(structdevice_driver *drv)

    {

           intret;

           structdevice_driver *other;

           BUG_ON(!drv->bus->p);

    //假设driver的方法和总线上的方法不能匹配则驱动的名称须要更新

           if((drv->bus->probe && drv->probe) ||

               (drv->bus->remove &&drv->remove) ||

               (drv->bus->shutdown &&drv->shutdown))

                  printk(KERN_WARNING "Driver '%s'needs updating - please use "

                         "bus_typemethods ", drv->name);

           other= driver_find(drv->name, drv->bus);

           if(other) {

                  put_driver(other);

                  printk(KERN_ERR "Error: Driver '%s'is already registered, "

                         "aborting... ",drv->name);

                  return -EBUSY;

           }

           ret= bus_add_driver(drv);//将驱动载入到总线上,假设成功就返回

           if(ret)

                  return ret;

           ret= driver_add_groups(drv, drv->groups);

           if(ret)

                  bus_remove_driver(drv);

           returnret;

    }

    3.3.2  unregister

    通过调用函数platform_driver_unregister函数实现platform_driver级设备的注销。

    void platform_driver_unregister(structplatform_driver *drv)

    {

           driver_unregister(&drv->driver);

    }

    在此函数中调用了driver_unregister函数,将驱动从系统中移除。函数的详细定义例如以下:

    void driver_unregister(structdevice_driver *drv)

    {

           if(!drv || !drv->p) {

                  WARN(1, "Unexpected driverunregister! ");

                  return;

           }

           driver_remove_groups(drv,drv->groups);

           bus_remove_driver(drv);

    }

    分两步走,第一步调用driver_remove_groups。函数的详细定义例如以下:

    static voiddriver_remove_groups(struct device_driver *drv,

                                 const struct attribute_group **groups)

    {

           inti;

           if(groups)

                  for (i = 0; groups[i]; i++)

                         sysfs_remove_group(&drv->p->kobj,groups[i]);

    }

    当中,调用了

    static inline voidsysfs_remove_group(struct kobject *kobj,

                                     const struct attribute_group *grp)

    {

    }

    第二步从总线中将驱动删除。调用例如以下函数:

    void bus_remove_driver(structdevice_driver *drv)

    {

           if(!drv->bus)

                  return;

           if(!drv->suppress_bind_attrs)

                  remove_bind_files(drv);

           driver_remove_attrs(drv->bus,drv);

           driver_remove_file(drv,&driver_attr_uevent);

           klist_remove(&drv->p->knode_bus);

           pr_debug("bus:'%s': remove driver %s ", drv->bus->name, drv->name);

           driver_detach(drv);

           module_remove_driver(drv);

           kobject_put(&drv->p->kobj);

           bus_put(drv->bus);

    }

    此函数的作用是:将驱动从它控制的设备中卸载,而且从驱动的总线链表中将它移除。


     

    probe函数

     在kernel载入模块的时候启动了设备注冊函数,由于全部的设备的driver都继承自device_driver,所以从driver_register看起,函数的源代码例如以下:

    int driver_register(structdevice_driver * drv)
    {
     if ((drv->bus->probe &&drv->probe) ||
         (drv->bus->remove &&drv->remove) ||
         (drv->bus->shutdown &&drv->shutdown)) {
       printk(KERN_WARNING "Driver '%s'needs updating - please use bus_type methods ", drv->name);
     }
     klist_init(&drv->klist_devices,NULL, NULL);
     return bus_add_driver(drv);
    }

    klist_init不相关,不用管他,详细再去看bus_add_driver:

    int bus_add_driver(structdevice_driver *drv)
    {
    1.先kobject_set_name(&drv->kobj,"%s", drv->name);
    2.再kobject_register(&drv->kobj)
    3.然后调用了:driver_attach(drv)
    }


    int driver_attach(struct device_driver * drv)
    {
     return bus_for_each_dev(drv->bus,NULL, drv, __driver_attach);
    }

    真正起作用的是__driver_attach:

    static int __driver_attach(structdevice * dev, void * data)
    {
    ……
     if (!dev->driver)
       driver_probe_device(drv, dev);
    ……
    }


    int driver_probe_device(struct device_driver * drv, struct device * dev)
    {
    ……

    //1.先是推断bus是否match:
     if (drv->bus->match &&!drv->bus->match(dev, drv))
       goto done;
    //2.再详细运行probe:
     ret = really_probe(dev, drv);
    ……

    }

    really_probe才是我们要找的函数:
    static int really_probe(struct device *dev, struct device_driver *drv)
    {
    ……

    //1.先是调用的驱动所属总线的probe函数:
     if (dev->bus->probe) {
       ret = dev->bus->probe(dev);
       if (ret)
        goto probe_failed;

       } else if (drv->probe) {
    //2.再调用你的驱动中的probe函数:
       ret = drv->probe(dev);
       if (ret)
        goto probe_failed;
     }
    ……
    }

    当中,drv->probe(dev),才是真正调用的驱动实现的详细的probe函数。从此出開始probe函数正式被调用。

    至此,platform成功挂接到platform  bus上了,并与特定的设备实现了绑定,并对设备进行了probe处理。


    request函数

    当SDIO设备启动之后,probe函数会调用mmc_alloc_host函数不断的检測连接的MMC/SD/SDIO卡,而且通过device_add完毕对设备的加入,当设备加入完毕之后,会调用mmc_blk_probe完毕驱动定义的特定功能。函数的调用关系图例如以下:


    图3 request调用过程

    Request函数的定义例如以下:

    static void

    v8sdio_request( struct mmc_host *mmc,struct mmc_request *mrq )

    {

           unsignedlong iflags = 0;

           structv8sdio_host *host = mmc_priv( mmc );//首先将struct  mmc_host设备转化成

                                           //struct  v8sdio_host

    #if IRQ_STAT_DBG  

           if(host->id == DEBUG_CHN )

           {

                  do_gettimeofday( &tv_now );//获取时间

                  timersub( &tv_now, &tv_last,&tv_delta );

                  if( tv_delta.tv_sec >= 5 )

                  {

                         u32i = 0;

                         tv_last.tv_sec  = tv_now.tv_sec;

    //                   tv_last.tv_usec= tv_now.tv_usec;

                         printk(" " );

                         printk("[sdio%d_irq]: irq_ALL = %u ", host->id, host->irq_cnt[0] );

                         printk("[sdio%d_irq]: irq_ERR = %u ", host->id, host->irq_cnt[1] );

                         printk("[sdio%d_irq]: irq_DMA = %u ", host->id, host->irq_cnt[2] );

                         printk("[sdio%d_irq]: irq_CMD = %u ", host->id, host->irq_cnt[3] );

                         printk("[sdio%d_irq]: irq_TRN = %u ", host->id, host->irq_cnt[4] );

                         for(i = 0; i <= MAX_OPCODE; i++ )

                         {

                                if( cmd_stats[i] )

                                {

                                printk( "[sdio%d_cmd]: cmd[%02u] =%u ", host->id, i, cmd_stats[i] );

                                }

                         }    

                  }

           }

    #endif

           V8LOGV(V8TAG_SDIO, "" );

           if(host->mrq )

                  V8LOGW( V8TAG_SDIO, "[Ch%d]host->mrq is NOT NULL.", host->id );

           clk_enable(host->aclk );   //使能主机aclk

           host->mrq= mrq;        //请求队列赋值

           local_irq_save(iflags);    //保存本地中断标志

           v8sdio_prepare_data(host, mrq );  //开启DMA通道,而且填充lli

           v8sdio_start_command(host, mrq->cmd, mrq->data );  //開始运行命令

           local_irq_restore(iflags);            //又一次保存本地中断标志

    }


    rescan过程

    内核通过mmc_rescan(drivers/mmc/core/core.c)不断扫描MMC/SD卡:

    void mmc_rescan(struct work_struct*work)

    {

           structmmc_host *host =

                  container_of(work, struct mmc_host,detect.work);

           u32ocr;

           interr;

           unsignedlong flags;

           intextend_wakelock = 0;

           spin_lock_irqsave(&host->lock,flags);

           if(host->rescan_disable) {

                  spin_unlock_irqrestore(&host->lock,flags);

                  return;

           }

           spin_unlock_irqrestore(&host->lock,flags);

           mmc_bus_get(host);             //取得总线

           /*假设是个已经注冊过的卡, 检查它是否存在 */

           if((host->bus_ops != NULL) && host->bus_ops->detect &&!host->bus_dead)

                  host->bus_ops->detect(host);

           /*假设卡已经被移除,总线将被标记为死卡

            * —声明唤醒锁

            * 使得用户空间可以对应*/

           if(host->bus_dead)

                  extend_wakelock = 1;

           mmc_bus_put(host);

           mmc_bus_get(host);

           /*假设当前还有卡,将它停止*/

           if(host->bus_ops != NULL) {

                  mmc_bus_put(host);

                  goto out;

           }

           /*检查新插入的卡 */

           /*

            * 仅仅有我们可以加入新的处理器, 所以在这里释放锁是安全的。

             */

           mmc_bus_put(host);

           if(host->ops->get_cd && host->ops->get_cd(host) == 0)

                  goto out;

           mmc_claim_host(host);

           mmc_power_up(host);

    #ifdef CONFIG_MMC_VC088X

                  if(host->caps & MMC_CAP_SDIO_IRQ){

                         sdio_reset(host);

                  }

    #else

           sdio_reset(host);

    #endif

           mmc_go_idle(host);//发送CMD0使卡进入IDLE状态

           mmc_send_if_cond(host,host->ocr_avail);

           /*

            * 首先我们搜索SDIO...

            */

           err= mmc_send_io_op_cond(host, 0, &ocr);

           if(!err) {

                  if (mmc_attach_sdio(host, ocr))

                         mmc_power_off(host);

                  extend_wakelock = 1;

                  goto out;

           }

           /*

            * ...然后普通的SD...

            */

           err= mmc_send_app_op_cond(host, 0, &ocr);

           if(!err) {

                  if (mmc_attach_sd(host, ocr))

                         mmc_power_off(host);

                  extend_wakelock = 1;

                  goto out;

           }

           /*

            * ...最后MMC.

            */

           err= mmc_send_op_cond(host, 0, &ocr);

           if(!err) {

                  if (mmc_attach_mmc(host, ocr))

                         mmc_power_off(host);

                  extend_wakelock = 1;

                  goto out;

           }

           mmc_release_host(host);

           mmc_power_off(host);

    out:

           if(extend_wakelock)

                  wake_lock_timeout(&mmc_delayed_work_wake_lock,HZ / 2);

           else

                  wake_unlock(&mmc_delayed_work_wake_lock);

           if(host->caps & MMC_CAP_NEEDS_POLL)

                  mmc_schedule_delayed_work(&host->detect,HZ);

    }

    在设置完每一个卡进入空状态,而无论当前卡是存在于何种状态之后调用了mmc_send_if_cond命令,此命令的定义例如以下:

    int mmc_send_if_cond(struct mmc_host*host, u32 ocr)

    {

           structmmc_command cmd;

           interr;

           staticconst u8 test_pattern = 0xAA;

           u8result_pattern;

           /*

            * To support SD 2.0 cards, we must alwaysinvoke SD_SEND_IF_COND

            * before SD_APP_OP_COND. This command willharmlessly fail for

            * SD 1.0 cards.

            */

           cmd.opcode= SD_SEND_IF_COND;

           cmd.arg= ((ocr & 0xFF8000) != 0) << 8 | test_pattern;

           cmd.flags= MMC_RSP_SPI_R7 | MMC_RSP_R7 | MMC_CMD_BCR;

           err= mmc_wait_for_cmd(host, &cmd, 0);

           if(err)

                  return err;

           if(mmc_host_is_spi(host))

                  result_pattern = cmd.resp[1] & 0xFF;

           else

                  result_pattern = cmd.resp[0] & 0xFF;

           if(result_pattern != test_pattern)

                  return -EIO;

           return0;

    }

    流程图例如以下:

                                  图7  SD卡的状态图

    ⑴取得总线
        ⑵检查总线操作结构指针bus_ops,假设为空,则又一次利用各总线对port进行扫描,检測顺序依次为:SDIO、NormalSD、MMC。当检測到相应的卡类型后,就使用mmc_attach_bus()把相相应的总线操作与host连接起来。

    voidmmc_attach_bus(struct mmc_host *host, const struct mmc_bus_ops *ops)
    {
        ...
        host->bus_ops = ops;
        ...
    }

    ⑶初始化卡按下面流程初始化:
        ①发送CMD0使卡进入IDLE状态
        ②发送CMD8,检查卡是否SD2.0。SD1.1是不支持CMD8的,因此在SD2.0Spec中提出了先发送CMD8,如响应为无效命令,则卡为SD1.1,否则就是SD2.0(请參考SD2.0Spec)。
       ③发送CMD5读取OCR寄存器。
       ④发送ACMD55、CMD41,使卡进入工作状态。MMC卡并不支持ACMD55、CMD41,假设这步通过了,则证明这张卡是SD卡。
       ⑤假设d步骤错误,则发送CMD1推断卡是否为MMC。SD卡不支持CMD1,而MMC卡支持,这就是SD和MMC类型的推断根据。
       ⑥假设ACMD41和CMD1都不能通过,那这张卡恐怕就是无效卡了,初始化失败。

     

     

  • 相关阅读:
    LeetCode Flatten Binary Tree to Linked List
    LeetCode Longest Common Prefix
    LeetCode Trapping Rain Water
    LeetCode Add Binary
    LeetCode Subsets
    LeetCode Palindrome Number
    LeetCode Count and Say
    LeetCode Valid Parentheses
    LeetCode Length of Last Word
    LeetCode Minimum Depth of Binary Tree
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/4059520.html
Copyright © 2011-2022 走看看