MMC/SD介绍及SDI主机控制器

首先我们来理清几个概念:

  1. MMC:(Multi Media Card)由西门子公司和首推CF的SanDisk于1997年推出的多媒体记忆卡标准。
  2. SD:(Secure Digital Memory Card)由日本松下、东芝及美国SanDisk公司于1999年8月共同开发研制的新一代记忆卡标准,已完全兼容MMC标准。
  3. SDIO:(Secure Digital Input and Output Card)安全数字输入输出卡。SDIO是在SD标准上定义了一种外设接口,通过SD的I/O接脚来连接外围设备,并且通过SD上的 I/O数据接位与这些外围设备进行数据传输。是目前较热门的技术,如下图中的一些设备:GPS、相机、Wi-Fi、调频广播、条形码读卡器、蓝牙等。
  4. 工作模式:工作模式是针对主机控制器来说的。也就是说,S3C2440中的SDI控制器可以在符合MMC的标准下工作,或者可以在符合SD的标准下工作,或者可以在符合SDIO的标准下工作。故就分别简称为:MMC模式、SD模式和SDIO模式。
  5. 传输模式:传输模式也是针对主机控制器来说的,指控制器与卡之间数据的传输模式,或者说是总线类型。S3C2440中的SDI控制器可支持SPI、1位和4位的三种传输模式(总线类型)。那么什么又是SPI呢?请参考这里:SPI协议简介;至于1位和4位又是什么意思呢?他们是指传输数据总线的线宽,具体参考数据手册。下面使用表格列出了MMC、SD、SDIO的电气特性及性能和不同工作模式下支持的传输模式情况:
  6. 那么,我们现在怎样让主机控制器在我们所要求的工作模式和传输模式上工作呢?很简单,就是对主机控制器的各个寄存器进行相应的配置即可。下面来简单介绍一下SDI主机控制器的结构和各寄存器的用途。

    S3C2440内的SDI主机控制器结构图如下:

    如上图所示,SDI主机控制器是使用1个串行时钟线与5条数据线同步进行信息移位和采样。传输频率通过设定SDIPRE寄存器的相应位的设定来控制,可以修改频率来调节波特率数据寄存器的值。

    各主要寄存器介绍,对于具体的寄存器位的设置就参考数据手册:

    1. SDICON:控制寄存器,完成SD卡基础配置,包括大小端,中断允许,模式选择,时钟使能等。
    2. SDIPRE:波特率预定标器寄存器,对SDCLK的配置。
    3. SDICmdArg:指令参数寄存器,指令的参数存放在这里。
    4. SDICCON:控制指令形式的寄存器,配置SPI还是SDI指令,指令的反馈长度,是否等待反馈,是否运行指令,指令的索引等。
    5. SDICmdSta:指令状态寄存器,指令是否超时,传送,结束,CRC是否正确等。
    6. SDIRSP0-3:反映SD的状态。
    7. SDIDTimer:设置超时时间。
    8. SDIBSize:模块大小寄存器。
    9. SDIDatCon:数据控制寄存器,配置是几线传输,数据发送方向,数据传送方式等。
    10. SDIDatSta:数据状态寄存器,数据是否发送完,CRC效验,超时等。
    11. SDIFSTA:FIFO状态寄存器,DMA传输是否判断FIFO。
    12. SDIIntMsk:中断屏蔽寄存器。
    13. SDIDAT:SDI数据寄存器。

    SDI主机控制器在SD/MMC工作模式下的设置步骤:(注意:因为SD模式兼容MMC模式,所以我们只需了解SD模式的即可,而SDIO的工作模式则是针对SDIO设备的,所以这里就不讨论了)

    1. 设置SDICON寄存器来配置适当的时钟及中断使能;
    2. 设置SDIPRE寄存器适当的值;
    3. 等待74个SDCLK时钟以初始化卡;
    4. 命令操作步骤:
      a. 写命令参数32位到SDICmdArg寄存器;
      b. 设置命令类型并通过设置SDICCON寄存器开始命令传输;
      c. 当SDICSTA寄存器的特殊标志被置位,确认命令操作完成;
      d. 如果命令类型相应,标志是RspFin,否则标志是CmdSend;
      e. 通过对相应位写1,清除SDICmdSta的标志。
    5. 数据操作步骤:
      a. 写数据超时时间到SDIDTimer寄存器;
      b. 写模块大小到SDIBSize寄存器(通常是0x80字节);
      c. 确定模块模式、总线线宽、DMA等且通过设置SDIDatCon寄存器开始数据传输;
      d. 发送数据->写数据到SDIDAT寄存器,当发送FIFO有效(TFDET置位),或一半(TFHalf置位),或空(TFEmpty置位);
      e. 接收数据->从数据寄存器SDIDAT读数据,当接收FIFO有效(RFDET置位),或满(RFFull置位),或一半(RFHalf置位),或准备最后数据(RFLast置位);
      f. 当SDIDatSta寄存器的DatFin标志置位,确认数据操作完成;
      g. 通过对相应位写1,清除SDIDatSta的标志。

    三、MMC/SD协议

    这里我并不是要讨论MMC/SD的整个通信协议(详细的协议请看MMC/SD规范),而是遵循MMC/SD协议了解一下MMC/SD在被驱动的过程中卡所处的各种阶段和状态。根据协议,MMC/SD卡的驱动被分为:卡识别阶段和数据传输阶段。在卡识别阶段通过命令使MMC/SD处于:空闲(idle)、准备(ready)、识别(ident)、等待(stby)、不活动(ina)几种不同的状态;而在数据传输阶段通过命令使MMC/SD处于:发送(data)、传输(tran)、接收(rcv)、程序(prg)、断开连接(dis)几种不同的状态。所以可以总结MMC/SD在工作的整个过程中分为两个阶段和十种状态。下面使用图形来描述一下在两个阶段中这十种状态之间的转换关系。

    卡识别阶段,如下图:

    数据传输阶段,如下图:

    四、MMC/SD设备驱动在Linux中的结构层次

        我们翻开MMC/SD设备驱动代码在Linux源码中的位置/linux-2.6.30.4/drivers/mmc/,乍一看,分别有card、core和host三个文件夹,那哪一个文件才是我们要找的驱动代码文件啊?答案是他们都是。不是吧,听起来有些可怕,三个文件夹下有多少代码啊。呵呵,还是先放下对庞大而又神秘代码的恐惧感吧,因为在实际驱动开发中,其实只需要在host文件夹下实现你具体的MMC/SD设备驱动部分代码,现在你的心情是不是要好点了。具体的MMC/SD设备是什么意思呢?他包括RAM芯片中的SDI控制器(支持对MMC/SD卡的控制,俗称MMC/SD主机控制器)和SDI控制器与MMC/SD卡的硬件接口电路。

        那为什么刚才又说card、core和host都是MMC/SD设备的驱动呢?这就好比我们建房子,建房子首先要的是什么,地皮对吧, 有了地皮然后要到政府部门备案,备案后才能开始建,是这样的吧。在Linux中MMC/SD卡的记忆体都当作块设备。那么,我们这里的card层就是要把操作的数据以块设备的处理方式写到记忆体上或从记忆体上读取,就好比是在地皮上填沙石、挖地基等;core层则是将数据以何种格式,何种方式在MMC/SD主机控制器与MMC/SD卡的记忆体(即块设备)之间进行传递,这种格式、方式被称之为规范或协议,就好比到政府部门备案,备案就会要求你的房子应该按照怎样的行业标准进行建造;最后只剩下host层了,上面也讲到了,host层下的代码就是你要动手实现的具体MMC/SD设备驱动了,就好比现在地皮买好挖好了,建房的标准也定好了,剩下的就需要人开始动工了。
    那么, card、core和host这三层的关系,我们用一幅图来进行描述,图如下:
        从这幅图中的关系可以看出,整个MMC/SD模块中最重要的部分是Core核心层,他提供了一系列的接口函数,对上提供了将主机驱动注册到系统,给应用程序提供设备访问接口,对下提供了对主机控制器控制的方法及块设备请求的支持。对于主机控制器的操作就是对相关寄存器进行读写,而对于MMC/SD设备的请求处理则比较复杂。那么在主机驱动层中的一个请求处理是怎么通过核心层提交到块设备请求层的呢?
    在网上找到一副图来说明他们之间的关联和处理流程,如下图:

     
    命令、数据发送流程如下图:
    其中,黑色粗线部分为命令发送或者数据发送都要经过的流程,橙色方框部分判断所有类型的请求是否完成。
    下面我们就来具体实例分析一个MMC/SD卡设备驱动程序。
    五、实例分析MMC/SD卡设备驱动程序
    1. Mini2440开发板的MMC/SD硬件接口电路原路图如下:

      从电路原理图上可以看出,SD分别使用S3C2440的复用IO端口GPE7-10作为4根数据信号线、使用GPE6作命令信号线、使用GPE5作时钟信号线,使用复用端口GPG8的外部中断功能来作SD卡的插拔检测,使用GPH8端口来判断SD卡是否写有保护。

    2. MMC/SD卡驱动程序的重要数据结构,该结果位于Core核心层,主要用于核心层与主机驱动层的数据交换处理。定义在/include/linux/mmc/host.h中:

      struct mmc_host
      {
          struct device *parent;
          struct device class_dev;
          int           index;
          const struct  mmc_host_ops*ops;
          unsigned int  f_min;
          unsigned int  f_max;
          u32           ocr_avail;

      #define MMC_VDD_165_195 0x00000080    /* VDD voltage 1.65 - 1.95 */
      #define MMC_VDD_20_21   0x00000100    /* VDD voltage 2.0 ~ 2.1 */
      #define MMC_VDD_21_22   0x00000200    /* VDD voltage 2.1 ~ 2.2 */
      #define MMC_VDD_22_23   0x00000400    /* VDD voltage 2.2 ~ 2.3 */
      #define MMC_VDD_23_24   0x00000800    /* VDD voltage 2.3 ~ 2.4 */
      #define MMC_VDD_24_25   0x00001000    /* VDD voltage 2.4 ~ 2.5 */
      #define MMC_VDD_25_26   0x00002000    /* VDD voltage 2.5 ~ 2.6 */
      #define MMC_VDD_26_27   0x00004000    /* VDD voltage 2.6 ~ 2.7 */
      #define MMC_VDD_27_28   0x00008000    /* VDD voltage 2.7 ~ 2.8 */
      #define MMC_VDD_28_29   0x00010000    /* VDD voltage 2.8 ~ 2.9 */
      #define MMC_VDD_29_30   0x00020000    /* VDD voltage 2.9 ~ 3.0 */
      #define MMC_VDD_30_31   0x00040000    /* VDD voltage 3.0 ~ 3.1 */
      #define MMC_VDD_31_32   0x00080000    /* VDD voltage 3.1 ~ 3.2 */
      #define MMC_VDD_32_33   0x00100000    /* VDD voltage 3.2 ~ 3.3 */
      #define MMC_VDD_33_34   0x00200000    /* VDD voltage 3.3 ~ 3.4 */
      #define MMC_VDD_34_35   0x00400000    /* VDD voltage 3.4 ~ 3.5 */
      #define MMC_VDD_35_36   0x00800000    /* VDD voltage 3.5 ~ 3.6 */

      unsigned long       caps;        /* Host capabilities */

      #define MMC_CAP_4_BIT_DATA    (1<< 0)/* Can the host do 4 bit transfers */
      #define MMC_CAP_MMC_HIGHSPEED (1<< 1)/* Can do MMC high-speed timing */
      #define MMC_CAP_SD_HIGHSPEED  (1<< 2)/* Can do SD high-speed timing */
      #define MMC_CAP_SDIO_IRQ     (1<< 3)/* Can signal pending SDIO IRQs */
      #define MMC_CAP_SPI          (1<< 4)/* Talks only SPI protocols */
      #define MMC_CAP_NEEDS_POLL    (1<< 5)/* Needs polling for card-detection */
      #define MMC_CAP_8_BIT_DATA    (1<< 6)/* Can the host do 8 bit transfers */

      /* host specific block data */
          unsigned int    max_seg_size;   /* see blk_queue_max_segment_size */
          unsigned short  max_hw_segs;    /* see blk_queue_max_hw_segments */
          unsigned short  max_phys_segs;  /* see blk_queue_max_phys_segments */
          unsigned short  unused;
          unsigned int    max_req_size;   /* maximum number of bytes in one req */
          unsigned int    max_blk_size;   /* maximum size of one mmc block */
          unsigned int    max_blk_count;  /* maximum number of blocks in one req */

      /* private data */
          spinlock_t      lock;  /* lock for claim and bus ops */

      struct mmc_ios  ios;   /* current io bus settings */
          u32             ocr;   /* the current OCR setting */

      /* group bitfields together to minimize padding */
          unsigned int        use_spi_crc:1;
          unsigned int        claimed:1;    /* host exclusively claimed */
          unsigned int        bus_dead:1;   /* bus has been released */
      #ifdef CONFIG_MMC_DEBUG
          unsigned int        removed:1;    /* host is being removed */
      #endif

      struct mmc_card     *card;        /* device attached to this host */

      wait_queue_head_t   wq;

      struct delayed_work    detect;

      const struct mmc_bus_ops*bus_ops;  /* current bus driver */
          unsigned int        bus_refs;       /* reference counter */

      unsigned int        sdio_irqs;
          struct task_struct  *sdio_irq_thread;
          atomic_t            sdio_irq_thread_abort;

      #ifdef CONFIG_LEDS_TRIGGERS
          struct led_trigger  *led;        /* activity led */
      #endif

      struct dentry       *debugfs_root;

      unsigned long       private[0] ____cacheline_aligned;
      };

    3. MMC/SD卡驱动程序的头文件中一些变量的定义,这些变量在驱动中都会用到。先不用看这些变量将用做什么,等驱动中用到时自然就明白了。代码如下:

      #define S3CMCI_DMA 0

      enum s3cmci_waitfor
      {
          COMPLETION_NONE,
          COMPLETION_FINALIZE,
          COMPLETION_CMDSENT,
          COMPLETION_RSPFIN,
          COMPLETION_XFERFINISH,
          COMPLETION_XFERFINISH_RSPFIN,
      };

      struct s3cmci_host
      {
          struct platform_device    *pdev;

          structs3c24xx_mci_pdata  *pdata;
          struct mmc_host           *mmc;
          struct resource           *mem;
          struct clk                *clk;
          void __iomem              *base;
          int                       irq;
          int                       irq_cd;
          int                       dma;

      unsigned long             clk_rate;
          unsigned long             clk_div;
          unsigned long             real_rate;
          u8                        prescaler;

      unsigned                  sdiimsk;
          unsigned                  sdidata;
          int                       dodma;
          int                       dmatogo;

      struct mmc_request        *mrq;
          int                       cmd_is_stop;

      spinlock_t                complete_lock;
          enum s3cmci_waitfor       complete_what;

      int                       dma_complete;

      u32                       pio_sgptr;
          u32                       pio_bytes;
          u32                       pio_count;
          u32                       *pio_ptr;
      #define XFER_NONE             0
      #define XFER_READ             1
      #define XFER_WRITE            2
          u32                       pio_active;

      int                       bus_width;

      char                      dbgmsg_cmd[301];
          char                      dbgmsg_dat[301];
          char                      *status;

      unsigned int              ccnt, dcnt;
          struct tasklet_struct     pio_tasklet;

      #ifdef CONFIG_CPU_FREQ
          struct notifier_block     freq_transition;
      #endif
      };

    4. MMC/SD卡驱动程序的加载与卸载部分:
      在Linux中,MMC/SD设备是被作为平台设备添加到系统的。可以查看内核代码:/arch/arm/plat-s3c24xx/devs.c中为MMC/SD主机控制器SDI定义了平台设备和平台设备资源,然后在/arch/arm/mach-s3c2440/mach-smdk2440.c中的系统初始化的时候添加到系统中。如下:

      //平台设备资源
      static struct resource s3c_sdi_resource[]={
          [0] = {
              .start = S3C24XX_PA_SDI,
              .end = S3C24XX_PA_SDI+ S3C24XX_SZ_SDI- 1,
              .flags = IORESOURCE_MEM,
          },
          [1] = {
              .start = IRQ_SDI,
              .end = IRQ_SDI,
              .flags = IORESOURCE_IRQ,
          }

      };

      //定义SDI平台设备
      struct platform_device s3c_device_sdi = {
          .name         = "s3c2410-sdi",
          .id         =-1,
          .num_resources     = ARRAY_SIZE(s3c_sdi_resource),
          .resource     = s3c_sdi_resource,
      };

      EXPORT_SYMBOL(s3c_device_sdi);

      //添加SDI平台设备到平台设备列表
      static struct platform_device*smdk2440_devices[] __initdata={
          &s3c_device_usb,
          &s3c_device_sdi,
          &s3c_device_lcd,
          &s3c_device_wdt,
          &s3c_device_rtc,
          &s3c_device_dm9000,
          .
          .
          .
      };

      //平台设备添加到系统
      static void __init smdk2440_machine_init(void)
      {
          .
          .
          .
          platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
          smdk_machine_init();
      }

      所以,MMC/SD设备驱动程序的加载和卸载部分很简单,就是注册和注销平台设备,代码如下:

      //加载
      static int __init s3cmci_init(void)
      {
          platform_driver_register(&s3cmci_driver);
          return 0;
      }

      //卸载
      static void __exit s3cmci_exit(void)
      {
          platform_driver_unregister(&s3cmci_driver);
      }

      //平台设备操作结构体
      static struct platform_driver s3cmci_driver={
          .driver.name    ="s3c2410-sdi",//名称和平台设备定义中的对应
          .driver.owner   = THIS_MODULE,
          .probe          = s3cmci_probe,//平台设备探测接口函数
          .remove        = __devexit_p(s3cmci_remove),//__devexit_p的作用以前将过
          .shutdown      = s3cmci_shutdown,
          .suspend        = s3cmci_suspend,
          .resume         = s3cmci_resume,
      };

    5. 平台探测函数s3cmci_probe的讲解:

      staticint __devinit s3cmci_probe(struct platform_device*pdev)
      {
          //该结构体定义在头文件中,现在实例一个名为host的结构体指针为结构体中的成员赋值做准备
          struct s3cmci_host *host;
          //实例一个名为mmc的结构体指针,用于与Core核心层中的mmc_host结构体指针相关联
          struct mmc_host    *mmc;
          int ret;
          
          //初始化一个名为complete_lock的自旋锁以备后用,该自旋锁的定义在s3cmci_host结构体中
          spin_lock_init(&host->complete_lock);
          
          //初始化一个名为pio_tasklet的tasklet,用于实现中断的底半部机制,底半部服务函数为pio_tasklet,
          //将host结构体变量作为服务函数的参数。注意:这里tasklet的变量名与服务函数名称同名了(这是可以的)。
          tasklet_init(&host->pio_tasklet, pio_tasklet,(unsignedlong) host);

      //分配mmc_host结构体指针的内存空间大小,该函数在host.c中实现,这里要注意一点,为什么参数
          //是s3cmci_host结构体的大小,到host.c中看,实际这里分配的是mmc_host加s3cmci_host的大小。
          mmc = mmc_alloc_host(sizeof(struct s3cmci_host),&pdev->dev);
          if (!mmc)
          {
              ret = -ENOMEM;
              goto probe_out;
          }

      //调用mmc_priv函数将mmc_host和s3cmci_host结构体的对象关联起来,mmc_priv定义在host.h中
          host = mmc_priv(mmc);
          
          //下面就开始初始化s3cmci_host结构体的各成员
          host->mmc     = mmc;
          host->pdev    = pdev;

         host->pdata   =pdev->dev.platform_data;
          
          //SDI主机控制器的中断屏蔽寄存器和数据寄存器,他们定义在mach-s3c2410/include/mach/regs-sdi.h中
          host->sdiimsk    = S3C2440_SDIIMSK;
          host->sdidata    = S3C2440_SDIDATA;
          
          //complete_what定义在s3cmci_host结构体中,用来记录请求处理所处的当前状态,这里初始化为
          //COMPLETION_NONE即无状态,定义在头文件的s3cmci_waitfor中,里面枚举了6种状态。
          host->complete_what= COMPLETION_NONE;
          
          //pio_active定义在s3cmci_host结构体中,用来标记请求处理数据在FIFO方式下的数据方向是读还是写
          host->pio_active     = XFER_NONE;
          
          //dodma和dma方便用于标记是否要使用DMA数据传输方式和DMA通道资源,0表示不使用DMA功能
          host->dodma        = 0;
          host->dma    = S3CMCI_DMA;
          
          //从SDI平台设备资源中获取SDI的IO端口资源,该资源在plat-s3c24xx/devs.c的s3c_sdi_resource中指定的
          host->mem= platform_get_resource(pdev, IORESOURCE_MEM, 0);
          if (!host->mem)
          {
              dev_err(&pdev->dev,"failed to get io memory region resouce.\n");
              ret = -ENOENT;
              goto probe_free_host;
          }
          //申请SDI的IO端口资源所占用的IO空间(要注意理解IO空间和内存空间的区别)
          host->mem= request_mem_region(host->mem->start, RESSIZE(host->mem), pdev->name);
          if (!host->mem)
          {
              dev_err(&pdev->dev,"failed to request io memory region.\n");
              ret = -ENOENT;
              goto probe_free_host;
          }

      //将SDI的IO端口占用的这段IO空间映射到内存的虚拟地址,ioremap定义在io.h中。
        //注意:IO空间要映射后才能使用,以后对虚拟地址的操作就是对IO空间的操作。
          host->base= ioremap(host->mem->start, RESSIZE(host->mem));
          if (!host->base){
              dev_err(&pdev->dev,"failed to ioremap() io memory region.\n");
              ret = -EINVAL;
              goto probe_free_mem_region;
          }

      //同样从SDI平台设备资源中获取SDI的中断号
          host->irq= platform_get_irq(pdev, 0);
          if (host->irq== 0)
          {
              dev_err(&pdev->dev,"failed to get interrupt resouce.\n");
              ret = -EINVAL;
              goto probe_iounmap;
          }

      //申请SDI的中断服务,服务函数为s3cmci_irq,主要参数为host
          if (request_irq(host->irq, s3cmci_irq, 0, DRIVER_NAME, host))
          {
              dev_err(&pdev->dev,"failed to request mci interrupt.\n");
              ret = -ENOENT;
              goto probe_iounmap;
          }

      //在SDI未准备好之前先屏蔽SDI的中断功能
          disable_irq(host->irq);

      //根据开发板原理图分别设置GPG8、GPH8端口为SD卡插入拔出的检测和有无写保护的检查,

          //注意:其实有没有写保护就是检查SD卡侧面有个移动按钮的开关,MMC卡无此功能

         host->pdata->gpio_detect= S3C2410_GPG8;

         host->pdata->gpio_wprotect= S3C2410_GPH8;

         //获取GPG8复用端口中断功能的中断号
          host->irq_cd=s3c2410_gpio_getirq(host->pdata->gpio_detect);
          //GPG8是复用端口,要使用中断功能则要配置成中断功能,GPG8对应的中断功能是外部中断EINT16,这个数据手册上有讲到
          s3c2410_gpio_cfgpin(S3C2410_GPG8, S3C2410_GPG8_EINT16);

      //申请SDI的卡检测中断服务,服务函数为s3cmci_irq_cd,主要参数也为host
          if (request_irq(host->irq_cd, s3cmci_irq_cd, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, DRIVER_NAME, host))
          {
              dev_err(&pdev->dev,"can't get card detect irq.\n");
              ret = -ENOENT;
              goto probe_free_irq;
          }

      //获取DMA通道并申请DMA中断,S3C2440中DMA的使用在后续的文章中再了解
          if (s3c2410_dma_request(S3CMCI_DMA,&s3cmci_dma_client,NULL)< 0)
          {
              dev_err(&pdev->dev,"unable to get DMA channel.\n");
              ret = -EBUSY;
              goto probe_free_irq_cd;
          }

      //从平台时钟队列中获取SDI的时钟源,在arch/arm/plat-s3c24xx/s3c2410-clock.c中有定义
          host->clk= clk_get(&pdev->dev,"sdi");
          if (IS_ERR(host->clk))
          {
              dev_err(&pdev->dev,"failed to find clock source.\n");
              ret = PTR_ERR(host->clk);
              host->clk=NULL;
              goto probe_free_host;
          }

      //启动获取的时钟源
          ret = clk_enable(host->clk);
          if (ret)
          {
              dev_err(&pdev->dev,"failed to enable clock source.\n");
              goto clk_free;
          }

      //通过SDI的时钟源获取CPU的PCLK频率,这里为什么要获得CPU的PCLK频率呢,
          //通过数据手册SDI控制器的方框图得知,SDI的时钟频率(SDCLK)=PCLK/(Prescaler+1)
          host->clk_rate= clk_get_rate(host->clk);
          host->clk_div    = 1;//设置预分频值,即:Prescaler的值
          
          //下面对mmc_host进行初始化
          mmc->ops      =&s3cmci_ops;    //SDI主机控制器操作结构体
          mmc->ocr_avail = MMC_VDD_32_33| MMC_VDD_33_34;  //设置工作电压范围
          mmc->caps      =MMC_CAP_4_BIT_DATA;              //设置总线宽度为4位
          mmc->f_min    = host->clk_rate/(host->clk_div* 256); //设置最小工作频率
          mmc->f_max    = host->clk_rate/ host->clk_div;  //设置最大工作频率
          mmc->max_blk_count  = 4095;
          mmc->max_blk_size   = 4095;
          mmc->max_req_size   = 4095* 512;
          mmc->max_seg_size   = mmc->max_req_size;
          mmc->max_phys_segs  = 128;
          mmc->max_hw_segs    = 128;

      //Linux的通知链机制,实现到后面再讲
          ret = s3cmci_cpufreq_register(host);
          if (ret)
          {
              dev_err(&pdev->dev,"failed to register cpufreq\n");
              goto free_dmabuf;
          }

      //将SDI host设备注册到系统中
          ret = mmc_add_host(mmc);
          if (ret)
          {
              dev_err(&pdev->dev,"failed to add mmc host.\n");
              goto free_cpufreq;
          }

      //将SDI host设备的数据赋值给系统平台设备
          platform_set_drvdata(pdev, mmc);

      return 0;

      //以下是错误处理
       free_cpufreq:
          s3cmci_cpufreq_deregister(host);

      free_dmabuf:
          clk_disable(host->clk);

      clk_free:
          clk_put(host->clk);

      probe_free_irq_cd:
          if (host->irq_cd>= 0)
              free_irq(host->irq_cd, host);

      probe_free_irq:
          free_irq(host->irq, host);

      probe_iounmap:
          iounmap(host->base);

      probe_free_mem_region:
          release_mem_region(host->mem->start, RESSIZE(host->mem));

      probe_free_host:
          mmc_free_host(mmc);
       probe_out:
          return ret;
      }

      Linux的通知链机制

      //Linux的通知链机制,需要内核配置时的支持。
      //这个通知链机制在MMC/SD卡驱动中应用的目的是,当CPU频率发生改变时,MMC/SD时钟频率也要改变。
      //具体通知链机制的原理请看下一篇转载的文章。
      #ifdef CONFIG_CPU_FREQ

      static int s3cmci_cpufreq_transition(struct notifier_block*nb,unsigned long val,void*data)
      {
          struct s3cmci_host *host;
          struct mmc_host *mmc;
          unsigned long newclk;
          unsigned long flags;

      host = container_of(nb,struct s3cmci_host, freq_transition);
          newclk = clk_get_rate(host->clk);
          mmc = host->mmc;

      if ((val== CPUFREQ_PRECHANGE&& newclk > host->clk_rate)||
           (val == CPUFREQ_POSTCHANGE&& newclk< host->clk_rate)){
              spin_lock_irqsave(&mmc->lock, flags);

      host->clk_rate= newclk;

      if (mmc->ios.power_mode!= MMC_POWER_OFF&&
               mmc->ios.clock!= 0)
                  s3cmci_set_clk(host,&mmc->ios);

      spin_unlock_irqrestore(&mmc->lock, flags);
          }

      return 0;
      }

      static inlineint s3cmci_cpufreq_register(struct s3cmci_host*host)
      {
          host->freq_transition.notifier_call= s3cmci_cpufreq_transition;

      return cpufreq_register_notifier(&host->freq_transition, CPUFREQ_TRANSITION_NOTIFIER);
      }

      static inlinevoid s3cmci_cpufreq_deregister(struct s3cmci_host*host)
      {
          cpufreq_unregister_notifier(&host->freq_transition, CPUFREQ_TRANSITION_NOTIFIER);
      }

      #else//如果内核配置时没有选择此功能支持,则其实现为空即可
      static inlineint s3cmci_cpufreq_register(struct s3cmci_host*host)
      {
          return 0;
      }

      从探测函数中可以看到,我们接下来要实现的功能就很清晰了。他们分别是:
      a. s3cmci_ops SDI主机控制器操作接口函数功能;
      b. s3cmci_irq_cd SDI的卡检测中断服务功能;
      c. s3cmci_irq SDI的中断服务功能;

6. s3cmci_ops SDI主机控制器操作接口函数功能分析:

staticstruct mmc_host_ops s3cmci_ops=
{
    .request = s3cmci_request,//实现host的请求处理(即:命令和数据的发送和接收)
    .set_ios = s3cmci_set_ios,//通过核心层传递过来的ios,配置host寄存器(使能时钟、总线带宽等)
    .get_ro  = s3cmci_get_ro,//通过读取GPIO端口来判断卡是否写有保护
    .get_cd  = s3cmci_card_present,//通过读取GPIO端口来判断卡是否存在
};

mmc_host_ops结构体定义了对host主机进行操作的各种方法,其定义在Core核心层的host.h中,也就是Core核心层对Host主机层提供的接口函数。这里各种方法的函数原型如下:

void  (*request)(struct mmc_host *host,struct mmc_request*req);
void  (*set_ios)(struct mmc_host*host,struct mmc_ios *ios);
int   (*get_ro)(struct mmc_host*host);
int   (*get_cd)(struct mmc_host*host);

从各函数原型上看,他们都将mmc_host结构体作为参数,所以我在刚开始的时候就说过mmc_host结构体是MMC/SD卡驱动中比较重要的数据结构。 可以这样说,他是Core层与Host层进行数据交换的载体。那么,这些接口函数何时会被调用呢?答案可以在Core层的core.c和sd.c中找到,我们可以看到如下部分代码:

staticvoid mmc_start_request(struct mmc_host*host,struct mmc_request *mrq)
{
    ......
    host->ops->request(host, mrq);//导致s3cmci_request被调用
}

static inlinevoid mmc_set_ios(struct mmc_host*host)
{
    ......
    host->ops->set_ios(host,ios);//导致s3cmci_set_ios被调用
}

void mmc_rescan(struct work_struct*work)
{
    ......//导致s3cmci_card_present被调用
    if (host->ops->get_cd&& host->ops->get_cd(host)== 0)
            goto out;
    ......
}

static int mmc_sd_init_card(struct mmc_host*host, u32 ocr,
    struct mmc_card *oldcard)
{
    ......
    /* Check if read-only switch is active.*/
    if (!oldcard)
    {   //导致s3cmci_get_ro被调用
        if (!host->ops->get_ro|| host->ops->get_ro(host)< 0)
        {
            printk(KERN_WARNING "%s: host does not "
                "support reading read-only "
                "switch. assuming write-enable.\n",
                mmc_hostname(host));
        }
        else
        {
            if (host->ops->get_ro(host)> 0)
                mmc_card_set_readonly(card);
        }
    }
    ......
}

好了,我们开始分析每个接口函数的具体实现吧,从简单的开始吧。 判断卡是否存在,如下代码:

staticint s3cmci_card_present(struct mmc_host*mmc)
{
    //从mmc_host的对象中获取出s3cmci_host结构体的数据,在s3cmci_probe函数中进行关联的
    struct s3cmci_host *host = mmc_priv(mmc);
    struct s3c24xx_mci_pdata *pdata = host->pdata;
    int ret;

//判断有无设置卡检测引脚端口,引脚在s3cmci_probe函数中已设置
    if (pdata->gpio_detect== 0)
        return -ENOSYS;

//从设置的卡检测引脚中读出当前的电平值,来判断卡是插入存在的还是被拔出不存在的
    ret = s3c2410_gpio_getpin(pdata->gpio_detect)? 0 : 1;
    return ret ^ pdata->detect_invert;
}

获取卡是否写有保护,其实实现跟卡检查类似,代码如下:

staticint s3cmci_get_ro(struct mmc_host*mmc)
{
    //从mmc_host的对象中获取出s3cmci_host结构体的数据,在s3cmci_probe函数中进行关联的
    struct s3cmci_host *host = mmc_priv(mmc);
    struct s3c24xx_mci_pdata *pdata = host->pdata;
    int ret;

//判断有无设置卡写保护引脚端口,引脚在s3cmci_probe函数中已设置
    if (pdata->gpio_wprotect== 0)
        return 0;

//从设置的卡写保护引脚中读出当前的电平值,来判断卡是否写有保护
    ret = s3c2410_gpio_getpin(pdata->gpio_wprotect);

if (pdata->wprotect_invert)
        ret = !ret;

return ret;
}

配置host寄存器的时钟和总线宽度,代码如下:

staticvoid s3cmci_set_ios(struct mmc_host*mmc,struct mmc_ios *ios)
{
    //从mmc_host的对象中获取出s3cmci_host结构体的数据,在s3cmci_probe函数中进行关联的
    struct s3cmci_host *host = mmc_priv(mmc);
    u32 mci_con;

//读取SDI控制寄存器的值
    mci_con = readl(host->base+ S3C2410_SDICON);

//ios结构体参数从Core层传递过来,根据不同的电源状态来配置SDI各寄存器
    switch (ios->power_mode)
    {
        case MMC_POWER_ON:
        case MMC_POWER_UP:
            //根据开发板引脚连接情况配置SDI控制器的各信号线,包括:时钟线、命令线和四条数据线
            s3c2410_gpio_cfgpin(S3C2410_GPE5, S3C2410_GPE5_SDCLK);
            s3c2410_gpio_cfgpin(S3C2410_GPE6, S3C2410_GPE6_SDCMD);
            s3c2410_gpio_cfgpin(S3C2410_GPE7, S3C2410_GPE7_SDDAT0);
            s3c2410_gpio_cfgpin(S3C2410_GPE8, S3C2410_GPE8_SDDAT1);
            s3c2410_gpio_cfgpin(S3C2410_GPE9, S3C2410_GPE9_SDDAT2);
            s3c2410_gpio_cfgpin(S3C2410_GPE10, S3C2410_GPE10_SDDAT3);
    
            if (host->pdata->set_power)
                host->pdata->set_power(ios->power_mode,ios->vdd);
    
            break;
    
        case MMC_POWER_OFF:
        default:
            //如果电源状态为关闭或者默认情况下,关闭SDI的时钟信号
            s3c2410_gpio_setpin(S3C2410_GPE5, 0);
            s3c2410_gpio_cfgpin(S3C2410_GPE5, S3C2410_GPE5_OUTP);
    
            //根据数据手册的SDICON寄存器位的介绍,此处是将整个sdmmc时钟复位
            mci_con |= S3C2440_SDICON_SDRESET;
    
            if (host->pdata->set_power)
                host->pdata->set_power(ios->power_mode,ios->vdd);
    
            break;
    }

//设置SDI波特率预定标器寄存器以确定时钟,看其定义部分
    s3cmci_set_clk(host,ios);

//根据SDI当前的时钟频率来设置寄存器的使能时钟位
    if (ios->clock)
        mci_con |= S3C2410_SDICON_CLOCKTYPE;
    else
        mci_con &=~S3C2410_SDICON_CLOCKTYPE;

//将计算好的值写回SDI控制寄存器
    writel(mci_con, host->base+ S3C2410_SDICON);

//下面只是一些调试信息,可以不要
    if ((ios->power_mode== MMC_POWER_ON)||(ios->power_mode== MMC_POWER_UP))
    {
        dbg(host, dbg_conf,"running at %lukHz (requested: %ukHz).\n",
            host->real_rate/1000,ios->clock/1000);
    }
    else
    {
        dbg(host, dbg_conf,"powered down.\n");
    }

//设置总线宽度
    host->bus_width=ios->bus_width;
}

//设置SDI波特率预定标器寄存器以确定时钟
static void s3cmci_set_clk(struct s3cmci_host*host,struct mmc_ios *ios)
{
    u32 mci_psc;

//根据SDI工作时钟频率范围来确定时钟预分频器值
    for (mci_psc= 0; mci_psc< 255; mci_psc++)
    {
        host->real_rate= host->clk_rate/(host->clk_div*(mci_psc+1));

if (host->real_rate<=ios->clock)
            break;
    }

//根据数据手册描述,SDI波特率预定标器寄存器只有8个位,所以最大值为255
    if (mci_psc> 255)
        mci_psc = 255;

host->prescaler= mci_psc;//确定的预分频器值
    
    //将预分频器值写于SDI波特率预定标器寄存器中
    writel(host->prescaler, host->base+ S3C2410_SDIPRE);

if (ios->clock== 0)
        host->real_rate= 0;
}

MMC/SD请求处理,这是Host驱动中比较重要的一部分。请求处理的整个流程请参考(一)中的流程图,他很好的描述了一个请求是怎样从Host层发出,通过Core层提交到Card层被块设备处理的。下面看代码:

staticvoid s3cmci_request(struct mmc_host*mmc,struct mmc_request *mrq)
{
    //从mmc_host的对象中获取出s3cmci_host结构体的数据,在s3cmci_probe函数中进行关联的
    struct s3cmci_host *host = mmc_priv(mmc);

//s3cmci_host结构体定义的status主要是记录请求过程所处的阶段及状态,方便调试时使用
    host->status="mmc request";
    //请求处理主要包括MMC/SD命令和数据处理,所以定义cmd_is_stop来区分是哪种请求
    host->cmd_is_stop= 0;
    //将Core层的mmc_request对象保存到Host层中以备使用
    host->mrq= mrq;

//在开始发出一个请求前先要检测一下卡是否还存在,否则提交到了块设备层而没有请求处理的对象发生错误
    if (s3cmci_card_present(mmc)== 0)
    {
        dbg(host, dbg_err,"%s: no medium present\n",__func__);
        host->mrq->cmd->error=-ENOMEDIUM;
        mmc_request_done(mmc, mrq);//如果卡不存在则马上结束这次请求
    }
    else
    {
        s3cmci_send_request(mmc);//如果卡还存在则发出请求
    }
}

//发送请求
static void s3cmci_send_request(struct mmc_host*mmc)
{
    //从mmc_host的对象中获取出s3cmci_host结构体的数据,在s3cmci_probe函数中进行关联的
    struct s3cmci_host *host = mmc_priv(mmc);
    //取出在s3cmci_request函数中保存的mmc_request对象以使用
    struct mmc_request *mrq = host->mrq;
    //在s3cmci_request函数中设置的cmd_is_stop初始值为0,表示当前是命令请求
    struct mmc_command *cmd = host->cmd_is_stop? mrq->stop: mrq->cmd;

//清空SDI命令状态寄存器、数据状态寄存器和FIFO状态寄存器
    writel(0xFFFFFFFF, host->base+ S3C2410_SDICMDSTAT);
    writel(0xFFFFFFFF, host->base+ S3C2410_SDIDSTA);
    writel(0xFFFFFFFF, host->base+ S3C2410_SDIFSTA);

//如果当前这次的请求是数据请求
    if (cmd->data)
    {
        //进入数据请求处理设置,主要是数据控制寄存器的配置
        int res = s3cmci_setup_data(host, cmd->data);

if (res)
        {
            //如果在数据请求设置中出现异常,则马上结束这次请求
            dbg(host, dbg_err,"setup data error %d\n", res);
            cmd->error= res;
            cmd->data->error= res;

mmc_request_done(mmc, mrq);
            return;
        }

//判断数据处理的方式是DAM还是FIFO,在s3cmci_probe函数中初始的是0,所以没有使用DMA的方式
        if (host->dodma)
            res = s3cmci_prepare_dma(host, cmd->data);
        else
            res = s3cmci_prepare_pio(host, cmd->data);

if (res)
        {
            //如果请求处理数据失败则也要马上结束这次请求
            dbg(host, dbg_err,"data prepare error %d\n", res);
            cmd->error= res;
            cmd->data->error= res;

mmc_request_done(mmc, mrq);
            return;
        }
    }

//否则这次请求是命令请求
    s3cmci_send_command(host, cmd);

//还记得在s3cmci_probe中SDI未准备好是屏蔽了SD中断,所以这里就使能中断
    enable_irq(host->irq);
}

//数据请求处理设置,主要是数据控制寄存器的配置
static int s3cmci_setup_data(struct s3cmci_host*host,struct mmc_data *data)
{
    u32 dcon, imsk, stoptries= 3;

/*如果不是数据处理请求则清零SDI数据控制寄存器*/
    if (!data)
    {
        writel(0, host->base+ S3C2410_SDIDCON);
        return 0;
    }

//根据SDI模块大小寄存器描述,如果在多模块下BlkSize必须分配字大小即:BlkSize[1:0]=00
    //所以这里与上3(即:二进制的11)来判断的是单模块
    if ((data->blksz& 3)!= 0)
    {
        //如果在单模块处理的情况下,模块数大于1了,就出现异常
        if (data->blocks> 1)
        {
            pr_warning("%s: can't do non-word sized block transfers (blksz %d)\n",__func__, data->blksz);
            return -EINVAL;
        }
    }

//循环判断数据是否正在传输中(发送或者接收)
    while (readl(host->base+ S3C2410_SDIDSTA)&(S3C2410_SDIDSTA_TXDATAON| S3C2410_SDIDSTA_RXDATAON))
    {
        dbg(host, dbg_err,"mci_setup_data() transfer stillin progress.\n");

//如果正在传输中则立刻停止传输
        writel(S3C2410_SDIDCON_STOP, host->base+ S3C2410_SDIDCON);
        //接着立刻复位整个MMC/SD时钟
        s3cmci_reset(host);

//这里应该是起到一个延迟的效果。因为硬件停止传输到复位MMC/SD需要一点时间,而循环判断非常快。
        //如果在这个时间内硬件还处在数据传输中而没有复位好,则异常
        if ((stoptries--)== 0)
        {
            return -EINVAL;
        }
    }

dcon = data->blocks& S3C2410_SDIDCON_BLKNUM_MASK;

//如果使用DMA传输,则使能SDI数据控制寄存器的DMA
    if (host->dodma)
        dcon |= S3C2410_SDIDCON_DMAEN;

//如果设置总线宽度为4线,则使能SDI数据控制寄存器的总线宽度模式为宽总线模式(即:4线模式)
    if (host->bus_width== MMC_BUS_WIDTH_4)
        dcon |= S3C2410_SDIDCON_WIDEBUS;

//配置SDI数据控制寄存器的数据传输模式为模块数据传输
    if (!(data->flags& MMC_DATA_STREAM))
        dcon |= S3C2410_SDIDCON_BLOCKMODE;

if (data->flags& MMC_DATA_WRITE)
    {
        //数据发送命令响应收到后开始数据传输
        dcon |= S3C2410_SDIDCON_TXAFTERRESP;
        //数据发送模式
        dcon |= S3C2410_SDIDCON_XFER_TXSTART;
    }

if (data->flags& MMC_DATA_READ)
    {
        //数据发送命令响应收到后开始数据接收
        dcon |= S3C2410_SDIDCON_RXAFTERCMD;
        //数据接收模式
        dcon |= S3C2410_SDIDCON_XFER_RXSTART;
    }

//FIFO传输的大小使用字传输类型
    dcon |= S3C2440_SDIDCON_DS_WORD;
    
    //数据传输开始
    dcon |= S3C2440_SDIDCON_DATSTART;

//将以上配置的值写入SDI数据控制寄存器生效
    writel(dcon, host->base+ S3C2410_SDIDCON);

//配置模块大小寄存器的块大小值
    writel(data->blksz, host->base+ S3C2410_SDIBSIZE);

//出现FIFO失败SDI中断使能;数据接收CRC错误SDI中断使能;数据接收超时SDI中断使能;数据计时器为0SDI中断使能
    imsk = S3C2410_SDIIMSK_FIFOFAIL | S3C2410_SDIIMSK_DATACRC | S3C2410_SDIIMSK_DATATIMEOUT| S3C2410_SDIIMSK_DATAFINISH;
    enable_imask(host, imsk);//使能中断

//将配置的值写入SDI中断屏蔽寄存器,使之生效
    writel(0x007FFFFF, host->base+ S3C2410_SDITIMER);

return 0;
}

//复位整个MMC/SD时钟
static void s3cmci_reset(struct s3cmci_host*host)
{
    u32 con = readl(host->base+ S3C2410_SDICON);

    con|= S3C2440_SDICON_SDRESET;
    writel(con, host->base+ S3C2410_SDICON);
}

//使能中断
static inline u32 enable_imask(struct s3cmci_host*host, u32 imask)
{
    u32 newmask;

newmask = readl(host->base+ host->sdiimsk);
    newmask |= imask;

writel(newmask, host->base+ host->sdiimsk);

return newmask;
}

//屏蔽中断
static inline u32 disable_imask(struct s3cmci_host*host, u32 imask)
{
    u32 newmask;

newmask = readl(host->base+ host->sdiimsk);
    newmask &=~imask;

writel(newmask, host->base+ host->sdiimsk);

return newmask;
}

//清空中断屏蔽寄存器
static inlinevoid clear_imask(struct s3cmci_host*host)
{
    writel(0, host->base+ host->sdiimsk);
}

//使用DMA传输数据方式,注意:这里就不讲如何使用DMA的具体细节了,以后再讲。
//对于驱动中相关DMA操作的方法都在plat-s3c24xx/dma.c中定义了。
static int s3cmci_prepare_dma(struct s3cmci_host*host,struct mmc_data *data)
{
    int dma_len, i;
    
    //判断DMA传输的方向是读还是写
    int rw =(data->flags& MMC_DATA_WRITE)? 1 : 0;

//根据传输的方向来配置DMA相关寄存器
    s3cmci_dma_setup(host, rw? S3C2410_DMASRC_MEM: S3C2410_DMASRC_HW);
    //s3c2410_dma_ctrl函数将根据标志flag来控制DMA传输的开始、停止等操作
    s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_FLUSH);

//合并data->sg上相邻的段,映射一个发散/汇聚DMA操作

    //返回值是传送的DMA缓冲区数,可能会小于sg_len,也就是说sg_len与dma_len可能是不同。
    dma_len = dma_map_sg(mmc_dev(host->mmc), data->sg, data->sg_len,
             (rw)? DMA_TO_DEVICE: DMA_FROM_DEVICE);

if (dma_len== 0)
        return -ENOMEM;

host->dma_complete= 0;//初始DMA操作的状态
    host->dmatogo= dma_len;//保存合并后的段数

for (i = 0; i < dma_len; i++)
    {
        int res;

//分配一个数据段管理结构体,并将各数据段穿成单向链表,以及加载一个数据段到DMA通道

        //sg_dma_address返回的是总线(DMA)的地址,sg_dma_len返回的是缓存区的长度
        res = s3c2410_dma_enqueue(host->dma,(void*) host, sg_dma_address(&data->sg[i]), sg_dma_len(&data->sg[i]));

if (res)
        {
            s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_FLUSH);
            return -EBUSY;
        }
    }

//开始DMA数据传输,数据传输会在接收到请求后真正开始
    s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_START);

return 0;
}

//根据传输的方向来配置DMA相关寄存器,详细描述请查看数据手册DMA章节
static void s3cmci_dma_setup(struct s3cmci_host*host,enum s3c2410_dmasrc source)
{
    static enum s3c2410_dmasrc last_source=-1;
    static int setup_ok;

if (last_source== source)
        return;

last_source = source;

//配置DMA源或者目标硬件类型和地址,这里DMA使用的是物理地址,不是虚拟地址。
    s3c2410_dma_devconfig(host->dma, source, 3, host->mem->start+ host->sdidata);

//这个判断的作用是让下面的代码只执行一次,以后不在被执行
    if (!setup_ok)
    {
        //配置DMA控制寄存器中的传输数据大小单位
        s3c2410_dma_config(host->dma, 4, 0);
        //设置DMA回调函数为s3cmci_dma_done_callback,当一段数据传输完后该函数被调用
        s3c2410_dma_set_buffdone_fn(host->dma, s3cmci_dma_done_callback);
        s3c2410_dma_setflags(host->dma, S3C2410_DMAF_AUTOSTART);
        setup_ok = 1;
    }
}

//DMA回调函数, 当一段数据传输完后该函数被调用
static void s3cmci_dma_done_callback(struct s3c2410_dma_chan*dma_ch,void *buf_id,int size,
                 enum s3c2410_dma_buffresult result)
{
    struct s3cmci_host *host = buf_id;//这个s3cmci_host类型的参数是在s3c2410_dma_enqueue的时候传递进来的

unsigned long iflags;
    u32 mci_csta, mci_dsta, mci_fsta, mci_dcnt;

mci_csta = readl(host->base+ S3C2410_SDICMDSTAT);//命令状态寄存器的值
    mci_dsta = readl(host->base+ S3C2410_SDIDSTA);//数据状态寄存器的值
    mci_fsta = readl(host->base+ S3C2410_SDIFSTA);//FIFO状态寄存器的值
    mci_dcnt = readl(host->base+ S3C2410_SDIDCNT);//数据保留计数器寄存器的值

spin_lock_irqsave(&host->complete_lock, iflags);

//如果DMA返回错误,则调到错误处理处进行错误处理
    if (result!= S3C2410_RES_OK)
    {
        goto fail_request;
    }

host->dmatogo--;//合并data->sg上相邻后的段数递减

//如果合并的段数不为0,即所有的段还没有处理完
    if (host->dmatogo)
    {
        goto out;
    }

//否则,标识这次DMA操作真正完成了
    host->complete_what= COMPLETION_FINALIZE;

out:
    //切换到中断底半部执行
    tasklet_schedule(&host->pio_tasklet);
    spin_unlock_irqrestore(&host->complete_lock, iflags);
    return;

fail_request:
    host->mrq->data->error=-EINVAL;
    host->complete_what= COMPLETION_FINALIZE;
    //如果DMA请求失败,则屏蔽SDI中断
    writel(0, host->base+ host->sdiimsk);
    goto out;
}

//使用FIFO传输数据方式。具体操作就是调用do_pio_write往FIFO中填充数据,当64字节的FIFO少于33字节时就会产生中断;
//或者是从SD读数据,则先使能中断,当FIFO多于31字节时时,则会调用中断服务程序,中断服务程序中将会调用do_pio_read读出FIFO的数据。
static int s3cmci_prepare_pio(struct s3cmci_host*host,struct mmc_data *data)
{
    //跟DMA类似,这里同样要判断FIFO传输的方向是读还是写
    int rw =(data->flags& MMC_DATA_WRITE)? 1 : 0;

host->pio_sgptr= 0;
    host->pio_bytes= 0;
    host->pio_count= 0;
    host->pio_active= rw? XFER_WRITE: XFER_READ;//记录FIFO操作状态共三种:读、写和无操作,定义在驱动头文件中

if (rw)//写
    {
        //FIFO写操作
        do_pio_write(host);
        //使能中断。根据数据手册SDI中断屏蔽寄存器的描述,当发送FIFO半填满就产生SDI中断
        enable_imask(host, S3C2410_SDIIMSK_TXFIFOHALF);
    }
    else //读
    {
        //使能中断。根据数据手册SDI中断屏蔽寄存器的描述,当接收FIFO半填满或者接收FIFO有最后数据就产生SDI中断
        enable_imask(host, S3C2410_SDIIMSK_RXFIFOHALF| S3C2410_SDIIMSK_RXFIFOLAST);
    }

return 0;
}

//FIFO写操作(即填充FIFO)
static void do_pio_write(struct s3cmci_host*host)
{
    void __iomem *to_ptr;
    int res;
    u32 fifo;
    u32 *ptr;

//SDI数据寄存器的虚拟地址
    to_ptr = host->base+ host->sdidata;

//检查FIFO中当前的剩余空间
    while ((fifo= fifo_free(host))> 3)
    {
        if (!host->pio_bytes)
        {
            //从分散聚集列表中获取要写的数据缓存,这里主要是获取缓存的长度和开始地址
            res = get_data_buffer(host,&host->pio_bytes,&host->pio_ptr);
            if (res)
            {
                host->pio_active= XFER_NONE;
                return;
            }
        }

//如果FIFO剩余空间比这一次要写入的数据段长度要大
        if (fifo>= host->pio_bytes)
            fifo = host->pio_bytes;
        else
            fifo -= fifo& 3;

host->pio_bytes-= fifo;//更新还剩下没写完的缓存长度
        host->pio_count+= fifo;
        fifo = (fifo+ 3)>> 2;//将字节数转化为字数
        ptr = host->pio_ptr;
        
        while (fifo--)//写入FIFO
            writel(*ptr++, to_ptr);
            
        host->pio_ptr= ptr;//更新当前地址指针的位置

}

//FIFO半填满时发生MMC/SD中断
    enable_imask(host, S3C2410_SDIIMSK_TXFIFOHALF);
}

//FIFO读操作
static void do_pio_read(struct s3cmci_host*host)
{
    int res;
    u32 fifo;
    u32 *ptr;
    u32 fifo_words;
    void __iomem *from_ptr;

//设置SDI波特率预定标器寄存器的值
    writel(host->prescaler, host->base+ S3C2410_SDIPRE);

//SDI数据寄存器的虚拟地址
    from_ptr = host->base+ host->sdidata;

//检测FIFO中当前的数据个数
    while ((fifo= fifo_count(host)))
    {
        if (!host->pio_bytes)
        {
            //从分散聚集列表中获取要读数据缓存,这里主要是获取缓存的长度和开始地址的指针位置
            res = get_data_buffer(host,&host->pio_bytes,&host->pio_ptr);
            if (res)
            {
                host->pio_active= XFER_NONE;
                host->complete_what= COMPLETION_FINALIZE;
                return;
            }
        }

//如果FIFO中当前的数据个数比这一次要读出的数据段长度要大
        if (fifo>= host->pio_bytes)
            fifo = host->pio_bytes;
        else
            fifo -= fifo& 3;

host->pio_bytes-= fifo;//更新还剩下没读完的缓存长度
        host->pio_count+= fifo;
        fifo_words = fifo >> 2;//将字节数转化为字数
        ptr = host->pio_ptr;
        
        while (fifo_words--)//从FIFO中读出数据
            *ptr++= readl(from_ptr);
            
        host->pio_ptr= ptr;//更新当前地址指针的位置

//如果fifo中的数据非字对齐则读取非对齐部分
        if (fifo& 3)
        {
            u32 n = fifo & 3;
            u32 data = readl(from_ptr);
            u8 *p = (u8 *)host->pio_ptr;

while (n--)
            {
                *p++= data;
                data >>= 8;
            }
        }
    }

//请求的数据已读完
    if (!host->pio_bytes)
    {
        res = get_data_buffer(host,&host->pio_bytes,&host->pio_ptr);
        if (res)
        {
            host->pio_active= XFER_NONE;
            host->complete_what= COMPLETION_FINALIZE;
            return;
        }
    }

//接收FIFO半满或者接收FIFO有最后数据时发生MMC/SD中断
    enable_imask(host, S3C2410_SDIIMSK_RXFIFOHALF| S3C2410_SDIIMSK_RXFIFOLAST);
}

//检测FIFO中当前的数据个数
static inline u32 fifo_count(struct s3cmci_host*host)
{
    //读取SDI FIFO状态寄存器
    u32 fifostat = readl(host->base+ S3C2410_SDIFSTA);

//FIFO中的数据个数是保存在寄存器的0-6位,所以与上S3C2410_SDIFSTA_COUNTMASK得出数据个数值
    //S3C2410_SDIFSTA_COUNTMASK定义在regs-sdi.h中为:0x7f,即:1111111
    fifostat &= S3C2410_SDIFSTA_COUNTMASK;
    return fifostat;
}

//检查FIFO中当前的剩余空间
static inline u32 fifo_free(struct s3cmci_host*host)
{
    //这里跟检测FIFO中当前的数据个数是一样的
    u32 fifostat = readl(host->base+ S3C2410_SDIFSTA);

fifostat &= S3C2410_SDIFSTA_COUNTMASK;
    return 63 - fifostat;//用FIFO的总容量-FIFO中当前的数据个数=剩余空间
}

//MMC/SD核心为mrq->data成员分配了一个struct scatterlist的表,用来支持分散聚集,
//使用这种方法,使物理上不一致的内存页,被组装成一个连续的数组,避免了分配大的缓冲区的问题
static inlineint get_data_buffer(struct s3cmci_host*host, u32 *bytes, u32**pointer)
{
    struct scatterlist *sg;

//FIFO当前的操作状态验证
    if (host->pio_active== XFER_NONE)
        return -EINVAL;

//MMC/SD请求及数据有效性验证
    if ((!host->mrq)||(!host->mrq->data))
        return -EINVAL;

//数据缓存的入口有没有超过分散列表的范围
    if (host->pio_sgptr>= host->mrq->data->sg_len)
        return -EBUSY;

//从分散聚集列表中获取一段数据缓存
    sg = &host->mrq->data->sg[host->pio_sgptr];

*bytes = sg->length;//该段数据缓存的长度
    *pointer = sg_virt(sg);//该段数据缓存的入口地址(为虚拟地址),相当于一个游标的意思

host->pio_sgptr++;//准备下一段数据缓存的入口

return 0;
}

//以上三段代码是对发送数据请求处理的,下面是发送命令请求
static void s3cmci_send_command(struct s3cmci_host*host,struct mmc_command *cmd)
{
    u32 ccon, imsk;

//出现CRC状态错误|命令响应超时|接收命令响应|命令发出|响应CRC校验失败时,将产生SDI中断
    imsk = S3C2410_SDIIMSK_CRCSTATUS | S3C2410_SDIIMSK_CMDTIMEOUT |
        S3C2410_SDIIMSK_RESPONSEND | S3C2410_SDIIMSK_CMDSENT|
        S3C2410_SDIIMSK_RESPONSECRC;

//将值写入SDI中断屏蔽寄存器中
    enable_imask(host, imsk);

//判断请求所处在何种状态
    if (cmd->data)
        //如果有数据传输,则设当前任务为完成数据传输且接收命令响应状态
        host->complete_what= COMPLETION_XFERFINISH_RSPFIN;
    else if(cmd->flags& MMC_RSP_PRESENT)
        host->complete_what= COMPLETION_RSPFIN;
    else
        //命令发送状态
        host->complete_what= COMPLETION_CMDSENT;

//设置命令参数寄存器
    writel(cmd->arg, host->base+ S3C2410_SDICMDARG);

ccon = cmd->opcode& S3C2410_SDICMDCON_INDEX;
    ccon |= S3C2410_SDICMDCON_SENDERHOST| S3C2410_SDICMDCON_CMDSTART;//命令操作开始

if (cmd->flags& MMC_RSP_PRESENT)
        ccon |= S3C2410_SDICMDCON_WAITRSP;//主设备等待响应

if (cmd->flags& MMC_RSP_136)
        ccon |= S3C2410_SDICMDCON_LONGRSP;//主设备接收一个136位长的响应

//设置命令控制寄存器,开始命令的传输
    writel(ccon, host->base+ S3C2410_SDICMDCON);
}

7. s3cmci_irq_cd SDI的卡检测中断服务功能

//当MMC/SD卡插入卡槽时引发的中断
static irqreturn_t s3cmci_irq_cd(int irq,void*dev_id)
{
    //这个dev_id参数是申请中断时传递过来的
    struct s3cmci_host *host = (struct s3cmci_host*)dev_id;

//调用核心层中的方法将将struct delayed_work detect加入共享工作队列,
    //其处理函数为核心层中的mmc_rescan方法,用于卡的识别并初始化。
    mmc_detect_change(host->mmc, msecs_to_jiffies(500));

return IRQ_HANDLED;
}

8. s3cmci_irq SDI的中断服务功能。我们从第6小节中对MMC/SD各种请求处理的代码中和(一)中“命令、数据发送流程图”中可以看出,在这个中断服务中将要处理很多请求相关的事情。但对于中断服务来说,这样会严重影响系统的性能,所以这正是为什么要在驱动中实现中断的底半部机制。下面看代码进行分析。

//MMC/SD卡中断服务程序
static irqreturn_t s3cmci_irq(int irq,void*dev_id)
{
    //dev_id参数是申请中断的时候传递过来的s3cmci_host结构体,void类型的指针可以存放任何的数据类型
    struct s3cmci_host *host = dev_id;
    struct mmc_command *cmd;
    u32 mci_csta, mci_dsta, mci_fsta, mci_dcnt, mci_imsk;
    u32 mci_cclear, mci_dclear;
    unsigned long iflags;

//关中断并保持状态字
    spin_lock_irqsave(&host->complete_lock, iflags);

//分别读命令状态、数据状态、数据保留计数器、FIFO状态、中断屏蔽寄存器的值
    mci_csta = readl(host->base+ S3C2410_SDICMDSTAT);
    mci_dsta = readl(host->base+ S3C2410_SDIDSTA);
    mci_dcnt = readl(host->base+ S3C2410_SDIDCNT);
    mci_fsta = readl(host->base+ S3C2410_SDIFSTA);
    mci_imsk = readl(host->base+ host->sdiimsk);
    mci_cclear = 0;
    mci_dclear = 0;

//如果当前没有请求状态或者请求已经完成了,则恢复中断什么都不做
    if ((host->complete_what== COMPLETION_NONE)||(host->complete_what== COMPLETION_FINALIZE))
    {
        host->status="nothing to complete";
        clear_imask(host);
        goto irq_out;
    }

//如果核心层无MMC/SD请求,则恢复中断什么都不做
    if (!host->mrq)
    {
        host->status="no active mrq";
        clear_imask(host);
        goto irq_out;
    }

//获取当前发送命令有无完成
    cmd = host->cmd_is_stop? host->mrq->stop: host->mrq->cmd;

//如果发送命令完成了,则恢复中断什么都不做
    if (!cmd)
    {
        host->status="no active cmd";
        clear_imask(host);
        goto irq_out;
    }

//判断在数据传输状态时使用的传输方式
    if (!host->dodma)
    {
        //不是DMA传输。如果是FIFO写,则切换到底半部去进行FIFO的写操作
        if ((host->pio_active== XFER_WRITE)&&(mci_fsta & S3C2410_SDIFSTA_TFDET))
        {
            disable_imask(host, S3C2410_SDIIMSK_TXFIFOHALF);
            tasklet_schedule(&host->pio_tasklet);
            host->status="pio tx";
        }

//如果是FIFO读,则切换到底半部去进行FIFO的读操作
        if ((host->pio_active== XFER_READ)&&(mci_fsta & S3C2410_SDIFSTA_RFDET))
        {
            disable_imask(host, S3C2410_SDIIMSK_RXFIFOHALF| S3C2410_SDIIMSK_RXFIFOLAST);
            tasklet_schedule(&host->pio_tasklet);
            host->status="pio rx";
        }
    }

//命令响应超时
    if (mci_csta& S3C2410_SDICMDSTAT_CMDTIMEOUT)
    {
        dbg(host, dbg_err,"CMDSTAT: error CMDTIMEOUT\n");
        cmd->error=-ETIMEDOUT;
        host->status="error: command timeout";
        goto fail_transfer;
    }

//命令发送结束
    if (mci_csta& S3C2410_SDICMDSTAT_CMDSENT)
    {
        if (host->complete_what== COMPLETION_CMDSENT)
        {
            host->status="ok: command sent";
            goto close_transfer;
        }

mci_cclear |= S3C2410_SDICMDSTAT_CMDSENT;
    }

//收到命令响应,CRC校验失败
    if (mci_csta& S3C2410_SDICMDSTAT_CRCFAIL)
    {
        if (cmd->flags& MMC_RSP_CRC)
        {
            if (host->mrq->cmd->flags& MMC_RSP_136)
            {
                dbg(host, dbg_irq,"fixup: ignore CRC fail with long rsp\n");
            } else{
                /* note, we used to fail the transfer
                 * here, but it seems that this is just
                 * the hardware getting it wrong.
                 *
                 * cmd->error = -EILSEQ;
                 * host->status = "error: bad command crc";
                 * goto fail_transfer;
                */
            }
        }

mci_cclear |= S3C2410_SDICMDSTAT_CRCFAIL;
    }

//收到命令响应,响应结束
    if (mci_csta& S3C2410_SDICMDSTAT_RSPFIN)
    {
        //如果当前任务是完成,接收命令响应
        if (host->complete_what== COMPLETION_RSPFIN)
        {
            host->status="ok: command response received";
            goto close_transfer;//停止传输

}
        
        //当前任务是完成数据传输和接收命令响应
        if (host->complete_what== COMPLETION_XFERFINISH_RSPFIN)
            //标记当前任务为完成数据传输
            host->complete_what= COMPLETION_XFERFINISH;

//清除收到命令响应标志
        mci_cclear |= S3C2410_SDICMDSTAT_RSPFIN;
    }

if (!cmd->data)
        goto clear_status_bits;

//FIFO失败
    if (mci_fsta& S3C2440_SDIFSTA_FIFOFAIL)
    {
        dbg(host, dbg_err,"FIFO failure\n");
        host->mrq->data->error=-EILSEQ;
        host->status="error: 2440 fifo failure";
        goto fail_transfer;
    }

//接收CRC错误
    if (mci_dsta& S3C2410_SDIDSTA_RXCRCFAIL)
    {
        dbg(host, dbg_err,"bad data crc (outgoing)\n");
        cmd->data->error=-EILSEQ;
        host->status="error: bad data crc (outgoing)";
        goto fail_transfer;
    }

//发送数据后,CRC状态错误
    if (mci_dsta& S3C2410_SDIDSTA_CRCFAIL)
    {
        dbg(host, dbg_err,"bad data crc (incoming)\n");
        cmd->data->error=-EILSEQ;
        host->status="error: bad data crc (incoming)";
        goto fail_transfer;
    }

//数据/忙接收超时
    if (mci_dsta& S3C2410_SDIDSTA_DATATIMEOUT)
    {
        dbg(host, dbg_err,"data timeout\n");
        cmd->data->error=-ETIMEDOUT;
        host->status="error: data timeout";
        goto fail_transfer;
    }

//数据计数器为0,和本次请求的全部数据传输结束
    if (mci_dsta& S3C2410_SDIDSTA_XFERFINISH)
    {
        //如果当前任务是完成数据传输则结束数据传输
        if (host->complete_what== COMPLETION_XFERFINISH)
        {
            host->status="ok: data transfer completed";
            goto close_transfer;
        }

//如果当前任务是完成数据传输和接收命令响应
        if (host->complete_what== COMPLETION_XFERFINISH_RSPFIN)
            //标记当前任务为完成 接收命令响应
            host->complete_what= COMPLETION_RSPFIN;

//清除数据传输完标志
        mci_dclear |= S3C2410_SDIDSTA_XFERFINISH;
    }

//清除状态字
clear_status_bits:
    writel(mci_cclear, host->base+ S3C2410_SDICMDSTAT);
    writel(mci_dclear, host->base+ S3C2410_SDIDSTA);

goto irq_out;

//传输失败
fail_transfer:
    host->pio_active= XFER_NONE;

//传输结束
close_transfer:
    host->complete_what= COMPLETION_FINALIZE;

clear_imask(host);
    tasklet_schedule(&host->pio_tasklet);

goto irq_out;

irq_out:
    dbg(host, dbg_irq,"csta:0x%08x dsta:0x%08x fsta:0x%08x dcnt:0x%08x status:%s.\n",
     mci_csta, mci_dsta, mci_fsta, mci_dcnt, host->status);

//开中断并恢复状态字
    spin_unlock_irqrestore(&host->complete_lock, iflags);
    return IRQ_HANDLED;
}

//MMC/SD卡中断底半部程序
static void pio_tasklet(unsignedlong data)
{
    //data参数是在s3cmci_probe中的tasklet_init的时候传递过来的
    struct s3cmci_host *host = (struct s3cmci_host*) data;

//在执行底半部程序的时候屏蔽中断
    disable_irq(host->irq);

//判断如果当前存在FIFO的写状态,则进行FIFO的写操作
    if (host->pio_active== XFER_WRITE)
        do_pio_write(host);

//判断如果当前存在FIFO的读状态,则进行FIFO的读操作
    if (host->pio_active== XFER_READ)
        do_pio_read(host);

//判断如果当前的请求状态为完成状态,则准备进行完成请求处理
    if (host->complete_what== COMPLETION_FINALIZE)
    {
        //清空中断屏蔽寄存器
        clear_imask(host);
        
        //FIFO状态验证
        if (host->pio_active!= XFER_NONE)
        {
            if (host->mrq->data)
                host->mrq->data->error=-EINVAL;
        }

//完成请求处理
        finalize_request(host);
    }
    else
        //当前请求状态为其他,则使能中断继续请求处理
        enable_irq(host->irq);
}

//完成请求处理
static void finalize_request(struct s3cmci_host*host)
{
    struct mmc_request *mrq = host->mrq;
    struct mmc_command *cmd = host->cmd_is_stop? mrq->stop: mrq->cmd;
    int debug_as_failure = 0;

//如果当前请求状态不为完成状态,则为错误
    if (host->complete_what!= COMPLETION_FINALIZE)
        return;

if (!mrq)
        return;

if (cmd->data&&(cmd->error== 0)&&(cmd->data->error== 0))
    {
        if (host->dodma&&(!host->dma_complete))
        {
            dbg(host, dbg_dma,"DMA Missing!\n");
            return;
        }
    }

//读响应寄存器
    cmd->resp[0]= readl(host->base+ S3C2410_SDIRSP0);
    cmd->resp[1]= readl(host->base+ S3C2410_SDIRSP1);
    cmd->resp[2]= readl(host->base+ S3C2410_SDIRSP2);
    cmd->resp[3]= readl(host->base+ S3C2410_SDIRSP3);

writel(host->prescaler, host->base+ S3C2410_SDIPRE);

if (cmd->error)
        debug_as_failure = 1;

if (cmd->data&& cmd->data->error)
        debug_as_failure = 1;

dbg_dumpcmd(host, cmd, debug_as_failure);

//清空命令参数、数据配置、命令配置、中断屏蔽寄存器
    writel(0, host->base+ S3C2410_SDICMDARG);
    writel(S3C2410_SDIDCON_STOP, host->base+ S3C2410_SDIDCON);
    writel(0, host->base+ S3C2410_SDICMDCON);
    writel(0, host->base+ host->sdiimsk);

if (cmd->data&& cmd->error)
        cmd->data->error= cmd->error;

//有数据请求,有传输停止命令,数据传输命令已发送
    if (cmd->data&& cmd->data->stop&&(!host->cmd_is_stop))
    {
        host->cmd_is_stop= 1;
        s3cmci_send_request(host->mmc);//传输停止命令

return;
    }

if (!mrq->data)
        goto request_done;

//计算已传输的数据量
    if (mrq->data->error== 0)
    {
        mrq->data->bytes_xfered=(mrq->data->blocks* mrq->data->blksz);
    }
    else
    {
        mrq->data->bytes_xfered= 0;
    }

if (mrq->data->error!= 0)
    {
        if (host->dodma)
            s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_FLUSH);

//清除和复位FIFO状态寄存器
        writel(S3C2440_SDIFSTA_FIFORESET | S3C2440_SDIFSTA_FIFOFAIL, host->base+ S3C2410_SDIFSTA);
    }

//完成请求
request_done:
    host->complete_what= COMPLETION_NONE;
    host->mrq=NULL;
    mmc_request_done(host->mmc, mrq);
}

MMC, SDIO, SD相关推荐

  1. cmd52命令发送 mmc_Linux SD/MMC/SDIO驱动分析(新)

    一.SD/MMC/SDIO概念区分 SD(SecureDigital)与 MMC(MultimediaCard) SD 是一种 flash memory card 的标准,也就是一般常见的 SD 记忆 ...

  2. cmd52命令发送 mmc_乾坤合一~Linux SD/MMC/SDIO驱动分析(上)

    一.SD/MMC/SDIO概念区分 SD(SecureDigital)与 MMC(MultimediaCard) SD 是一种 flash memory card 的标准,也就是一般常见的 SD 记忆 ...

  3. SD/MMC/SDIO 驱动分析

    [来 源]:http://blog168.chinaunix.net/space.php?uid=25678596&do=blog&id=2102933 参考资料: 1.SD Memo ...

  4. cmd的发送 mmc_Linux SD/MMC/SDIO驱动分析

    一.SD/MMC/SDIO概念区分 SD(SecureDigital)与 MMC(MultimediaCard) SD 是一种 flash memory card 的标准,也就是一般常见的 SD 记忆 ...

  5. MMC / eMMC / SD

    MMC的全称是"MultiMediaCard"――所以也通常被叫做"多媒体卡",是一种小巧大容量的快闪存储卡,特别应用于移动电话和数字影像及其他移动终端中. M ...

  6. SDIO—SD 卡读写测试

    1.SDIO 简介 SD 卡 (Secure Digital Memory Card) 在我们生活中已经非常普遍了,控制器对 SD 卡进行读写通信操作一般有两种通信接口可选,一种是 SPI 接口,另外 ...

  7. MMC,SD,MINI SD,SDHC,SDHC的区别总结

    MMC,SD,MINI SD,SDHC,SDHC的区别总结 MMC卡(MultiMedia Card) 多媒体卡(MMC)是一种小型(24x32或18x1.4mm)可擦除固态存储卡,其全称为Multi ...

  8. MMC、SD、TF、SDIO、SDMMC简介

    MMC 概念 MMC的全称是"MultiMediaCard"――所以也通常被叫做"多媒体卡",是一种小巧大容量的快闪存储卡,特别应用于移动电话和数字影像及其他移 ...

  9. sd 卡驱动在2.6内核的编写.sd/mmc/sdio kernel,sd/mmc/sdio 内核

    [转帖请注明出处:blog.csdn.net/lanmanck] sd卡驱动主要参照已有的文件即可,2410,9260都挺好.其实写驱动主要是搞清楚工作流程即可.我这里写一些心得与大家分享下,基于2. ...

最新文章

  1. 安装oracle到create inventory时卡住了怎么办_「推荐」wacom数位板怎么用?教你如何正确的安装数位板驱动...
  2. Jupyter中出现ModuleNotFoundError: No module named ‘matplotlib‘的解决办法
  3. java积分签到功能_大河客户端积分商城上线,看新闻就能换取各种超实用奖品,来约...
  4. 深度学习用于图片的分类和检测总结
  5. 谷歌雇程序员提升开源安全
  6. java 用流创建流_成为Java流大师–第1部分:创建流
  7. 多模态语义分析_情感分析、多模态NLP、多语言翻译...这场NLP知识盛宴不可错过!...
  8. 中的数组怎么转成结构体_PLC知识,什么是数组和结构体?
  9. Linux内核编译与安装[转]
  10. 2008年8月30日人力资源系统启动会
  11. c语言设计函数型号发生器,基于51单片机函数信号发生器
  12. springboot学习笔记
  13. effective stl 第31条:了解与各种排序相关的选择
  14. Android 12.0关机界面全屏显示(UI全屏显示)
  15. 新手游戏创业如何推广游戏?
  16. 2016.11.18多态
  17. NOIP2017 滚粗记
  18. 单片机毕业设计 STM32自动泊车系统 - 智能小车 自动停车
  19. 密码管理工具-KeePass使用教程
  20. Java修炼之凡界篇 筑基期 第04卷 方法 第01话 什么是方法

热门文章

  1. php表单提交至数据库
  2. 金笛短信猫应用消防调度指挥系统
  3. 100颗宝石5个海盗怎么分
  4. AcrelCloud-1000变电所运维云平台在榆林市科技馆的应用
  5. 徘徊在十字路口的韩国能源行业
  6. flash 第六章 动画-时间轴和帧
  7. 电子信息工程专业生产实习报告
  8. FAT32转NTFS及失败解决方案
  9. ILSVRC2016目标检测任务回顾——视频目标检测(VID)
  10. iOS逆向—收据验证