详解sd协议以及裸机和u-boot中的sd卡驱动(2)

本文为该系列的第二篇。

3. sd卡驱动

3.1 引入

经过第2章我们知道,要想实现读写sd卡,需要按照sd协议规定的基本传输单位(命令、响应、数据)以及流程(初始化、读、写),向sd卡发送信号或者从sd卡接收信号。

为了简化cpu的操作,人们在soc内部设计了一个sd控制器,这个sd控制器专门按照sd规定的协议进行收发信号,并把我们上述提到的基本传输单位命令、响应以及数据的内容简化到一个个寄存器,比如s3c2440内部的sd控制器中包含有:

  • SDICmdArg寄存器:用来保存命令的参数,当我们需要发送命令到sd卡时,对于命令的参数,只需要写入该寄存器即可;

  • SDICmdCon寄存器:低八位用来保存命令的编号,当我们需要发送命令到sd卡时,对于命令的编号,只需要写入该寄存器即可;

  • SDIRSP0——SDIRSP3寄存器:用来保存响应的内容,当我们需要读取sd卡发来的响应值时,便可以读取该寄存器;

  • SDIDAT寄存器:用来保存数据的内容,当我们需要读取从sd卡发来的数据或者向sd卡写入数据时,便可以操作该寄存器;

等等还有很多寄存器,上面只是简单举例说明。

现在我们来回答问题2:cpu如何控制sd控制器来发出各种信号?

答案便是:通过读写其内部的寄存器,这也是sd驱动的主要内容。

现在,我想大家应该明白了,所谓sd卡驱动即是:

通过sd控制器提供的简化功能,按照sd协议的规定通过cpu读写其相应的寄存器,以此来达到向sd卡发出或者从sd卡接收一系列信号的目的,最终完成读写sd卡的功能。

下面从裸机、u-boot层面对sd卡驱动进行分析,并着重比较其实现的异同。

3.2 裸机中的sd卡驱动

注:本裸机驱动来自我之前写的移植u-boot 2020文章中的spl工程。

现在我们已经知道,sd卡驱动的内容主要便是按照sd协议的规定读写sd控制器中的寄存器,这在裸机驱动中表现得最为明显。

对于裸机中的sd卡驱动,主要围绕如下三个功能的实现而展开:

  • 初始化sd卡

  • 读sd卡

  • 写sd卡

3.2.1 初始化sd卡

初始化的过程就是按照sd协议中规定的流程进行的。

过程如下:

  1. 初始化连接sd卡的gpio引脚以及sd控制器;

  2. 等待74个CLK

  3. 发送CMD0复位卡

  4. 发送CMD1检测是否为sd卡或者mmc卡

  5. 发送CMD2获取sd卡的CID

  6. 发送CMD3设置sd卡或者mmc卡的RCA

  7. 发送CMD9获取sd卡的CSD

  8. 发送CMD7选中sd卡

  9. 发送CMD13查询是否为传输状态

  10. 发送ACMD6设置总线带宽

代码如下:

 u8 sd_init()
 {
 int i;
 
 debug("\r\nSDI初始化开始!");
 
 GPEUP  = 0xf83f;   // The pull up 1111 1000 0011 1111 必须上拉
     GPECON = 0xaaaaaaaa; // 1010 1010 1010 1010 1010 1010 1010 1010
     SDICSTA = 0xffff;   //SDI指令状态
     SDIDSTA = 0xffff;//SDI数据状态
 
 SDIPRE = 124; // 400KHz 波特率设置 频率 PCLK/400K -1
     SDICON = 1; // Type A, clk enable SDI控制
     SDIFSTA |= 1 << 16; //FIFO reset
 SDIBSIZE = 0x200; // 512byte(128word) SDI块大小
 SDIDTIMER = 0x7fffff; // Set timeout count 数据传输超时时间
 
 //等待74个CLK
     for(i=0;i<0x1000;i++);
 
 //先执行CMD0,复位
 CMD0();
 
 //判断卡的类型
 if(SDI_MMC_OCR())
 SDCard.sdiType = 1;  //卡为MMC
 else
 SDCard.sdiType = 0;  //卡为SD
 
 //检测SD卡
 if (SDI_SD_OCR()) {
 debug("SD is ready\r\n");
 } else{
 debug("Initialize fail\r\nNo Card assertion\r\n");
         return 0;
    }  
 
 //读CID
 if (CMD2(SDCard.cCardCID)) {
 debug("CID\r\n");
 debug("MID = %d\r\n",SDCard.cCardCID[0]);
 debug("OLD = %d\r\n",(SDCard.cCardCID[1]*0X100)+SDCard.cCardCID[2]);
 debug("生产厂家:%s\r\n",(SDCard.cCardCID+3));
 debug("生产日期:20%d,%d\r\n",((SDCard.cCardCID[13]&0x0f)<<4)+((SDCard.cCardCID[14]&0xf0)>>4),(SDCard.cCardCID[14]&0x0f));
 } else {
 debug("Read Card CID is fail!\r\n");
 return 0;
 }
 
 //设置RCA       MMC的RCA=1   SD的RCA=0
 //MMC
 if (SDCard.sdiType==1) {
 if (CMD3(1,&SDCard.sdiRCA)) {
 SDCard.sdiRCA = 1;
 SDIPRE = 2;  //16MHZ
 debug("MMC Card RCA = 0x%x\r\n",SDCard.sdiRCA);
 debug("MMC Frequency is %dHz\r\n",(PCLK/(SDIPRE+1)));
 } else {
 debug("Read MMC RCA is fail!\r\n");
 return 0;
 }
 //SD
 } else {
 if (CMD3(0,&SDCard.sdiRCA)) {
 SDIPRE = 1; // Normal clock=25MHz
 debug("SD Card RCA = 0x%x\r\n",SDCard.sdiRCA);
 debug("SD Frequency is %dHz\r\n",(PCLK/(SDIPRE+1)));
 } else {
 debug("Read SD RCA is fail!\r\n");
 return 0;
 }
 }
 
 //读CSD
 if (CMD9(SDCard.sdiRCA,SDCard.lCardCSD)) {
 SDCard.lCardSize = (((SDCard.lCardCSD[1]&0x0000003f)<<16)+((SDCard.lCardCSD[2]&0xffff0000)>>16)+1)*512;
 SDCard.lSectorSize = ((SDCard.lCardCSD[2]>>6)&0x0000007f)+1;
 debug("Read Card CSD OK!\r\n");
 debug("0x%08x\r\n",SDCard.lCardCSD[0]);
 debug("0x%08x\r\n",SDCard.lCardCSD[1]);
 debug("0x%08x\r\n",SDCard.lCardCSD[2]);
 debug("0x%08x\r\n",SDCard.lCardCSD[3]);
 debug("卡容量为:%dKB,%dMB\r\n",SDCard.lCardSize,SDCard.lCardSize/1024);
 } else {
 debug("Read Card CSD Fail!\r\n");
 return 0;
 }
 
 //选中卡 CMD7 进入传输状态
 //1表示选中卡
 if (select_card(1,SDCard.sdiRCA)) {
 debug("Card sel desel OK!\r\n");
 } else {
 debug("Card sel desel fail!\r\n");
 return 0;
 }
 
 //CMD13 查询是否为传输状态
 while ((CMD13(SDCard.sdiRCA) & 0x1e00) != 0x800);
 
 //设置总线带宽 ACMD6
 if (Set_bus_Width(SDCard.sdiType,1,SDCard.sdiRCA)) {
 SDCard.sdiWide = 1;
 debug("Bus Width is 4bit\r\n");
 } else {
 SDCard.sdiWide = 0;
 debug("Bus Width is 1bit\r\n");
 }
 
 return 1;
 }

3.2.2 读sd卡

读sd卡的过程也是按照sd协议中规定的流程进行的。

过程如下:

  1. 设置sd控制器;

  2. 发送CMD18多块读sd卡

  3. 通过sd控制器中的SDIDAT寄存器获取读到的数据到指定内存地址

  4. 清除sd控制器中的状态寄存器

  5. 发送CMD12停止多块读传输

代码如下:

 /*
  * sd_read_sector - 从SD卡中读出指定块起始地址的单个或多个数据块
  *
  * @addr 被读块的起始地址
  * @buffer 用于接收读出数据的缓冲区
  * @block_num 读的块数
  *
  * @return
  *0 读块操作不成功
  *1 读块操作成功
  *
  */
 u8 sd_read_sector(u32 *buf, u32 addr, u32 block_num)
 {
 u32 i=0;
 u32 status=0;
 
 SDIDTIMER = 0x7fffff; // Set timeout count
 SDIBSIZE = 0x200; // 512byte(128word)
 SDIFSTA = SDIFSTA | (1 << 16); // FIFO reset
 SDIDCON = (block_num << 0) | (2 << 12) | (1 << 14) | (SDCard.sdiWide << 16) | (1 << 17) | (1 << 19) | (2 << 22);
 
 //debug("enter sd_read_sector(): src_block = %d, block_num = %d\r\n", addr, block_num);
 
 //发送读多个块指令
 while (CMD18(addr) != 1)
 SDICSTA = 0xF << 9;
 
 //开始接收数据到缓冲区
 while (i < block_num * 128) {
 //检查是否超时和CRC校验是否出错
 if (SDIDSTA & 0x60) {
 //清除超时标志和CRC错误标志
 SDIDSTA = (0x3 << 0x5);
 return -1;
 }
 
 status = SDIFSTA;
 //如果接收FIFO中有数据
 if ((status & 0x1000) == 0x1000) {
 *buf = SDIDAT;
 buf++;
 i++;
 }
 }
 
 SDIDCON = SDIDCON & ~(7 << 12);
 SDIFSTA = SDIFSTA & 0x200;//Clear Rx FIFO Last data Ready
 SDIDSTA = 0x10;//Clear data Tx/Rx end detect
 
 //发送结束指令
 while (CMD12() != 1)
 SDICSTA = 0xF << 9;
 debug("leave sd_read_sector(): src_block = %d, block_num = %d\r\n", addr, block_num);
 
 return 0;
 }

3.2.3 写sd卡

与读sd卡过程相似,写sd卡的过程也是按照sd协议中规定的流程进行的。

过程如下:

  1. 设置sd控制器;

  2. 发送CMD25多块写sd卡

  3. 将指定内存地址处的要写入sd卡的数据写到sd控制器中的SDIDAT寄存器

  4. 设置sd控制器;

  5. 发送CMD12停止多块写传输

代码如下:

 /*
  * sd_write_sector - 向SD卡的一个或多个数据块写入数据
  *
  * @addr 被写块的起始地址
  * @buffer 用于发送数据的缓冲区
  * @block_num 块数
  *
  * @return
  *0 数据写入操作失败
  *1 数据写入操作成功
  *
  */
 u8 sd_write_sector(u32 *buf, u32 addr, u32 block_num)
 {
 u16 i = 0;
 u32 status = 0;
 
 SDIDTIMER = 0x7fffff; // Set timeout count
 SDIBSIZE = 0x200; // 512byte(128word)
 SDIFSTA = SDIFSTA | (1 << 16); // FIFO reset
 SDIDCON = (block_num << 0) | (3 << 12) | (1 << 14) | (1 << 16) | (1 << 17) | (1 << 20) | (2 << 22);
 
 //发送写多个块指令
 while (CMD25(addr) != 1)
 SDICSTA = 0xF << 9;
 
 //开始传递数据到缓冲区
 while (i < block_num * 128) {
 status = SDIFSTA;
 
 //如果发送FIFO可用,即FIFO未满
 if ((status & 0x2000) == 0x2000) {
 SDIDAT = *buf;
 buf++;
 i++;
 }
 }
 
 SDIDCON = SDIDCON & ~(7 << 12);
 
 //发送结束指令
 while (CMD12() != 1)
 SDICSTA=0xF<<9;
 
 //等待数据发送结束
 do {
 status = SDIDSTA;
 } while ((status & 0x2) == 0x2);
 
 SDIDSTA = status;
 SDIDSTA = 0xf4;
 
 return 0;
 }

3.2.4 具体命令实现举例

在前面的sd卡初始化、读、写函数中,主要的内容均是按照sd协议的规定发送一系列的命令,那具体每个命令是怎么实现的呢?下面举例说明:

  • CMD9:获取卡的CSD寄存器的值

    过程如下:

    1. 指定使用哪个sd卡,将卡的RCA作为命令的参数写入SDICARG寄存器

    2. 将命令的编号写入SDICCON寄存器,同时指定响应类型为长响应,并启动向sd卡发送该命令

    3. 读取sd控制器中的状态寄存器,检查命令是否发送完成

    4. 待命令发送完成后,读取SDIRSP0—SDIRSP3寄存器获取响应值,也即sd卡的CSD寄存器的值

    代码如下:

     //获取卡的CSD寄存器的值
     u8 CMD9(u16 iRCA,u32 *lCSD)
     {
     SDICARG = iRCA<<16; // (RCA,stuff bit)
     SDICCON = (0x1<<10)|(0x1<<9)|(0x1<<8)|0x49; // long_resp, wait_resp, start
     
     if(!SDI_Check_CMD_End(9, 1))
     return 0;
     
     *(lCSD+0) = SDIRSP0;
     *(lCSD+1) = SDIRSP1;
     *(lCSD+2) = SDIRSP2;
     *(lCSD+3) = SDIRSP3;
     
         return 1;
     }
  • CMD18:读取多个数据块

    过程如下:

    1. 指定读取sd卡的地址,将地址命令的参数写入SDICARG寄存器

    2. 将命令的编号写入SDICCON寄存器,并启动向sd卡发送该命令

    3. 读取sd控制器中的状态寄存器,检查命令是否发送完成

    代码如下:

     //读取多个数据块
     u8 CMD18(u32 addr)
     {
         //STEP1:发送指令
         SDICARG = addr; //设定指令参数
         SDICCON = (1<<9)|(1<<8)|0x52; //发送CMD18指令
         
         if(SDI_Check_CMD_End(18,1))
          return 1;
         else
          return 0;
     }
  • CMD25:写入多个数据块

    过程如下:

    1. 指定写入sd卡的地址,将地址命令的参数写入SDICARG寄存器

    2. 将命令的编号写入SDICCON寄存器,并启动向sd卡发送该命令

    3. 读取sd控制器中的状态寄存器,检查命令是否发送完成

    代码如下:

     //写入多个数据块
     u8 CMD25(u32 addr)
     {
         //STEP1:发送指令
         SDICARG = addr; //设定指令参数
         SDICCON = (1<<9)|(1<<8)|0x59; //发送CMD25指令
         
         if(SDI_Check_CMD_End(25,1))
        return 1;
         else
        return 0;
     }

3.2.5 优缺点总结

  • 优点:

    代码简单,容易理解,函数功能清晰明确,代码流程基本按照sd协议的规定走下去,便于初学者学习寄存器的操作与sd协议

  • 缺点:

    复用性、扩展性差,当需要同时支持多个sd控制器的时候,改动会非常大

3.3 u-boot中的sd卡驱动

相比于裸机,u-boot中的sd卡驱动在对sd控制器中的各个寄存器的操作以及对sd协议的遵循两方面基本相同,主要区别在于拥有良好的扩展性。所以,相比于寄存器与协议两方面,这里会更着重分析其组织框架,探究其支持良好扩展性的原因。

3.3.1 框架概述

下图是我总结的sd驱动的整体框架图:

attachments-2020-07-qTTqcX0a5f11d02d2b8bd.png

其中,

  • sd协议层

    sd协议相关的内容,由于sd协议硬件无关,相比于3.2 裸机中的sd卡驱动,u-boot中将该内容单独作为一层,并且已经作为公共部分实现好,任何的sd卡驱动均可以复用该层的内容,也就是说在sd卡驱动中不必涉及协议相关的内容,只需关注于对sd控制器中的寄存器操作,这样,便实现了硬件操作与sd协议相分离

  • sd核心层

    由于不同的sd控制器针对sd协议中发送命令、接收响应等操作需要不同的硬件操作,所以sd核心层的作用在于向下屏蔽不同sd控制器所带来的不同的硬件操作,向上提供统一的接口给sd协议层

  • sd驱动层

    sd卡驱动在上述框架中的位置为sd driver,不具有通用性,需要针对具体的sd控制器进行编写,这也是移植sd卡驱动时需要做的部分

关于框架图中其他的内容,主要涉及u-boot中的驱动模型,请参考这篇博客:https://blog.csdn.net/ooonebook/article/details/53234020,讲的非常好。

3.3.2 协议层

如3.2.1所述,u-boot将sd协议单独设为一层,与sd卡驱动进行了分离,那么本节我们就开始探寻u-boot中对于sd协议是如何实现的。

3.3.2.1 初始化

初始化主要有两个函数,分别用于不同的阶段,下面进行说明:

  1. mmc_initialize函数

    该函数在u-boot的board_r阶段初始化时被调用

     #ifdef CONFIG_MMC
     static int initr_mmc(void)
     {
     puts("MMC:   ");
     mmc_initialize(gd->bd);
     return 0;
     }
     #endif

    主要完成sd卡早期的初始化工作,

    1. 调用sd卡驱动中的probe函数,进行sd控制器层面的初始化

    2. 按照sd协议发送一些命令,以探测是否有卡插入以及是sd卡还是mmc卡等

    代码如下:

     int mmc_initialize(bd_t *bis)
     {
     static int initialized = 0;
     int ret;
     if (initialized)/* Avoid initializing mmc multiple times */
     return 0;
     initialized = 1;
     
     #if !CONFIG_IS_ENABLED(BLK)
     #if !CONFIG_IS_ENABLED(MMC_TINY)
     mmc_list_init();
     #endif
     #endif
     ret = mmc_probe(bis);
     if (ret)
     return ret;
     
     #ifndef CONFIG_SPL_BUILD
     print_mmc_devices(',');
     #endif
     
     mmc_do_preinit();
     return 0;
     }
  2. mmc_init函数

    该函数在u-boot启动之后的多个地方被调用,比如mmc命令、块设备驱动、dfu驱动等等,相比于mmc_initialize函数的早期初始化功能,该函数按照sd协议进行完全的初始化,具体代码如下:

     int mmc_init(struct mmc *mmc)
     {
     int err = 0;
     __maybe_unused ulong start;
     #if CONFIG_IS_ENABLED(DM_MMC)
     struct mmc_uclass_priv *upriv = dev_get_uclass_priv(mmc->dev);
     
     upriv->mmc = mmc;
     #endif
     if (mmc->has_init)
     return 0;
     
     start = get_timer(0);
     
     if (!mmc->init_in_progress)
     err = mmc_start_init(mmc);
     
     if (!err)
     err = mmc_complete_init(mmc);
     if (err)
     pr_info("%s: %d, time %lu\n", __func__, err, get_timer(start));
     
     return err;
     }
  3. mmc_start_init函数

    这里我们选择两个初始化函数都会调用的mmc_start_init函数进行说明,一路追踪下去发现,最终会调用如下核心内容:

        ......
           
        /* Reset the Card */
        err = mmc_go_idle(mmc);
       
        if (err)
        return err;
       
        /* The internal partition reset to user partition(0) at every CMD0*/
        mmc_get_blk_desc(mmc)->hwpart = 0;
       
     
  • 发表于 2020-07-18 00:22
  • 阅读 ( 201 )
  • 分类:经验分享

1 条评论&回复

请先 登录 后评论
渐进
渐进

12 篇文章

作家榜 »

  1. 百问网-周老师 18 文章
  2. st_ashang 14 文章
  3. 渐进 12 文章
  4. zxq 11 文章
  5. helloworld 8 文章
  6. 谢工 5 文章
  7. Litchi_Zheng 5 文章
  8. 星星之火 5 文章