uboot 的环境变量和MMC驱动分析

记录了uboot环境变量的作用、工作方式,对setenv、printenv、saveenv进行了分析;另外还对MMC驱动程序框架进行了分析

记录了uboot环境变量的作用、工作方式,对setenv、printenv、saveenv进行了分析;另外还对MMC驱动程序框架进行了分析

uboot的环境变量基础

环境变量的作用

可以不用修改uboot的源代码,而是通过修改环境变量来影响uboot运行时的一些数据和特性。譬如说通过修改bootdelay环境变量就可以更改系统开机自动启动时倒数的秒数。

环境变量的优先级

  1. uboot代码当中有一个值,环境变量中也有一个值。uboot程序实际运行时规则是:如果环境变量为空则使用代码中的值;如果环境变量不为空则优先使用环境变量对应的值
  2. 譬如machid(机器码)。uboot中在x210_sd.h中定义了一个机器码2456,写死在程序中的不能更改。如果要修改uboot中配置的机器码,可以修改x210_sd.h中的机器码,但是修改源代码后需要重新编译烧录,很麻烦;比较简单的方法就是使用环境变量machid。set machid 0x998类似这样,有了machid环境变量后,系统启动时会优先使用machid对应的环境变量,这就是优先级问题。

环境变量在uboot中工作方式

  1. 默认环境变量,是uboot/common/env_common.c中的default_environment变量,本质是一个字符数组,大小为CFG_ENV_SIZE(16kb),里面内容就是很多个环境变量连续分布组成的,每个环境变量最末端以’\0’结束。
  2. SD卡中环境变量分区,在uboot的raw分区中。SD卡中其实就是给了个分区,专门用来存储而已。存储时其实是把DDR中的环境变量整体的写入SD卡中分区里。所以当我们saveenv时其实整个所有的环境变量都被保存了一遍,而不是只保存更改了的。
  3. DDR中环境变量,在default_environment中,实质是字符数组。在uboot中其实是一个全局变量,链接时在数据段,重定位时default_environment就被重定位到DDR中一个内存地址处了。这个地址处这个全局字符数组就是我们uboot运行时的DDR中的环境变量了。

总结:刚烧录的系统中环境变量分区是空白的,uboot第一次运行时加载的是uboot代码中自带的一份环境变量,叫默认环境变量。我们在saveenv时DDR中的环境变量会被更新到SD卡中的环境变量中,就可以被保存下来,下次开机会在环境变量relocate时会SD卡中的环境变量会被加载到DDR中去。

default_environment中的内容虽然被uboot源代码初始化为一定的值(这个值就是我们的默认环境变量),但是在uboot启动的第二阶段,env_relocate时代码会去判断SD卡中的env分区的crc是否通过。如果crc校验通过说明SD卡中有正确的环境变量存储,则relocate函数会从SD卡中读取环境变量来覆盖default_environment字符数组,从而每次开机可以保持上一次更改过的环境变量。

环境变量相关命令源码解析

printenv

函数声明:

int do_printenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])

这个函数进行的主要操作如下:

  1. 如果只有一个参数,这打印所有的环境变量

    if (argc == 1) {/* Print all env variables */
        // 第一层 `for` 循环是遍历各个变量
        for (i=0; env_get_char(i) != '\0'; i=nxt+1) {
            //nxt移到变量的结尾字符 \0
            for (nxt=i; env_get_char(nxt) != '\0'; ++nxt)
                ;
            //打印变量的所有字符
            for (k=i; k<nxt; ++k)
                putc(env_get_char(k));
            putc  ('\n');
       
            if (ctrlc()) {
                puts ("\n ** Abort\n");
                return 1;
            }
        }
    }
    
    • argc = 1 说明只有 printenv 一个参数

    • 环境变量在uboot中保存的方式如下:

      #if defined(CONFIG_S5PC110) || defined(CONFIG_S5P6442)
      uchar default_environment[CFG_ENV_SIZE] = {
      #ifdef CONFIG_BOOTARGS
          "bootargs="CONFIG_BOOTARGS "\0"
      #endif
      #ifdef CONFIG_BOOTCOMMAND
          "bootcmd=" CONFIG_BOOTCOMMAND "\0"
      #endif
      // 省略....
      

      对于"bootcmd=" CONFIG_BOOTCOMMAND "\0" 编译器会把多个字符串拼接成一个,但是不会给字符串尾部添加 \0,所以在最后要手动添加一个 \0

      于是环境变量的保存方式如下:

      变量1 变量2 变量n
      "mtdpart=80000 400000 3000000\0" "bootdelay=3\0" "baudrate=115200\0"

总结

  1. 找到printenv命令所对应的函数。通过printenv的help可以看出,这个命令有2种使用方法。第一种直接使用不加参数则打印所有的环境变量;第二种是printenv name则只打印出name这个环境变量的值。
  2. 分析do_printenv函数
  3. do_printenv函数首先区分argc=1还是不等于1的情况,若argc=1那么就循环打印所有的环境变量出来;如果argc不等于1,则后面的参数就是要打印的环境变量,给哪个就打印哪个。
  4. argc=1时用双重for循环来依次处理所有的环境变量的打印。第一重for循环就是处理各个环境变量。所以有多少个环境变量则第一重就执行循环多少圈。
  5. 这个函数要看懂,首先要明白整个环境变量在内存中如何存储的问题。
  6. 关键点:第一要明白环境变量在内存中存储的方式;第二要C语言处理字符串的功底要好。

setenv

函数声明:

int do_setenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])

它实际调用的是 _do_setenv(),我们来分析这个函数

  1. 获取环境变量的地址

    uchar *env_data = env_get_addr(0);
    

    这个函数的实现如下:

    uchar *env_get_addr (int index)
    {
        if (gd->env_valid) {
            return ( ((uchar *)(gd->env_addr + index)) );
        } else {
            return (&default_environment[index]);
        }
    }
    

    这里 gd->env_addr = &default_environment,因为在 env_init() 函数中有:

    int  env_init(void)
    {
        ulong addr_default = (ulong)&default_environment[0];
       
        if (! crc1_ok && ! crc2_ok) {
            gd->env_addr  = addr_default;
            gd->env_valid = 0;
    }
    

    在一开始的时候,SD卡的环境变量分区是没有内容的,所以 crc1_okcrc2_ok 都为0

  2. 查找变量

    oldval = -1;
    for (env=env_data; *env; env=nxt+1) {
        // nxt跳到某个环境变量尾部
        for (nxt=env; *nxt; ++nxt)
            ;
        // 如果有这个环境变量名,用oldval记录这个变量名的索引
        if ((oldval = envmatch((uchar *)name, env-env_data)) >= 0)
            break;
    }
    
  3. 删除这个环境变量

    if (oldval >= 0) {
        // ipaddr 和 serial_no 不可以删除
        if ((strcmp (name, "serial#") == 0) ||
            (strcmp (name, "ethaddr") == 0)) {
                printf ("Can't overwrite \"%s\"\n", name);
                return 1;
            }
        //如果是波特率还要切换到新的波特率
        if (strcmp(argv[1],"baudrate") == 0) {
            int baudrate = simple_strtoul(argv[2], NULL, 10);
            gd->baudrate = baudrate;
            serial_setbrg ();
        }
       
        // 用后面的字符串覆盖这个环境变量
        // nxt指向的是一个环境变量的结束符'\0'
        // 如果是最后一个环境变量
        if (*++nxt == '\0') {
            if (env > env_data) {
                env--;
            } else {
                *env = '\0';
            }
        } else {// 不是最后一个环境变量
            for (;;) {
                // 用后面的字符串覆盖这个环境变量
                *env = *nxt++;
                if ((*env == '\0') && (*nxt == '\0'))
                    break;
                ++env;
            }
        }
        *++env = '\0';
       
        // 防止环境变量太多溢出,给环境变量的大小是16KB
        define CFG_ENV_SIZE    0x4000 /* 16KB Total Size of Environment Sector */
    	define ENV_HEADER_SIZE    (sizeof(uint32_t))
    	define ENV_SIZE (CFG_ENV_SIZE - ENV_HEADER_SIZE)
        len = strlen(name) + 2;
        /* add '=' for first arg, ' ' for all others */
        for (i=2; i<argc; ++i) {
            len += strlen(argv[i]) + 1;
        }
        if (len > (&env_data[ENV_SIZE]-env)) {
            printf ("## Error: environment overflow, \"%s\" deleted\n", name);
            return 1;
        }
       
        // 把新的环境变量放到字符串尾部
        // "name = val1 val2 valn"
        for (env=env_data; *env || *(env+1); ++env)
            ;
        // 复制 name
        while ((*env = *name++) != '\0')
            env++;
        // 复制值
        for (i=2; i<argc; ++i) {
            char *val = argv[i];
            // 第一个值是 =val1,后面用空格隔开
            *env = (i==2) ? '=' : ' ';
            while ((*++env = *val++) != '\0')
                ;
        }
        *++env = '\0';
        env_crc_update ();
    }
    

总结

  1. 命令定义和对应的函数在uboot/common/cmd_nvedit.c中,对应的函数为do_setenv
  2. setenv的思路就是:先去DDR中的环境变量处寻找原来有没有这个环境变量,如果原来就有则需要覆盖原来的环境变量,如果原来没有则在最后新增一个环境变量即可。
    • 第1步:遍历DDR中环境变量的数组,找到原来就有的那个环境变量对应的地址。168-174行。
    • 第2步:擦除原来的环境变量,259-265行
    • 第3步:写入新的环境变量,266-273行。
  3. 本来setenv做完上面的就完了,但是还要考虑一些附加的问题。
    • 问题一:环境变量太多超出DDR中的字符数组,溢出的解决方法。
    • 问题二:有些环境变量如baudrate、ipaddr等,在gd中有对应的全局变量。这种环境变量在set更新的时候要同时去更新对应的全局变量,否则就会出现在本次运行中环境变量和全局变量不一致的情况。

saveenv

  1. 在uboot/common/cmd_nvedit.c中,对应函数为do_saveenv

    int do_saveenv (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
    {
        extern char * env_name_spec;
       
        printf ("Saving Environment to %s...\n", env_name_spec);
       
        return (saveenv() ? 1 : 0);
    }
    
  2. 关键的函数是 saveenv()

    从uboot实际执行saveenv命令的输出,和x210_sd.h中的配置(#define CFG_ENV_IS_IN_AUTO)可以分析出:我们实际使用的是env_auto.c中相关的内容。

    // env_auto.c
    printf ("Saving Environment to %s...\n", env_name_spec);
       
    // x210_sd.h
    #define CFG_ENV_IS_IN_AUTO
       
    // env_auto.c
    #if defined(CFG_ENV_IS_IN_AUTO) /* Environment is in Non-Volatile Device */
    char * env_name_spec = "SMDK bootable device";
    
  3. 没有一种芯片叫auto的,env_auto.c中是使用宏定义的方式去条件编译了各种常见的flash芯片(如movinand、norflash、nand等)。然后在程序中读取INF_REG(OMpin内部对应的寄存器)从而知道我们的启动介质,然后调用这种启动介质对应的操作函数来操作。

    int saveenv(void)
    {
    #if defined(CONFIG_S5PC100) || defined(CONFIG_S5PC110) || defined(CONFIG_S5P6442)
        if (INF_REG3_REG == 2)
            saveenv_nand();
        else if (INF_REG3_REG == 3)
            saveenv_movinand();
    }
    

    其中

    #define INF_REG3_OFFSET 0x0c
    #define INF_REG_BASE    0xE010F000
    #define INF_REG3_REG    __REG(INF_REG_BASE+INF_REG3_OFFSET)
    

    查找《S5PV210数据手册》P454 可知 INFO ROM

    寄存器 地址 R/W 描述 初始值
    INFORM3 0xE010_F00C R/W 信息寄存器3 0x0000_0000
    INFORMn Bit 说明 初始值
    INFORM [31:0] 用户定义的信息寄存器。通过XnRESET引脚的断电来清除INFORM0~3寄存器。</br>INFORM4~6 寄存器仅通过断电清除 0x0000_0000

    uboot/cpu/s5pc11x/start.S 中有

    #define PRO_ID_BASE 0xE0000000
    #define OMR_OFFSET  0x04
       
    /* Read booting information */
    ldr r0, =PRO_ID_BASE
    ldr r1, [r0,#OMR_OFFSET]
    bic r2, r1, #0xffffffc1
       
    /* SD/MMC BOOT */
    cmp     r2, #0xc
    moveq   r3, #BOOT_MMCSD
       
    ldr r0, =INF_REG_BASE
    str r3, [r0, #INF_REG3_OFFSET]
    

    这里 0xE000_0000 ~ 0xFFFF_FFFF 是 SFR 特殊功能寄存器地址,OM的地址三星数据手册中没有写明,只能说明三星文档也有疏漏或者有意不写明。但从代码中可以看到 0xE000_000C 就是启动方式的内容。这里我们通过 SD/MMC 方式(编码为:3)启动,并把启动方式保存到了 INF_REG_BASE 寄存器中

  4. do_saveenv内部调用env_auto.c中的saveenv函数来执行实际的环境变量保存操作。

  5. 寄存器地址:E010F000+0C=E010_F00C,含义是用户自定义数据。我们在start.S中判断启动介质后将``#BOOT_MMCSD(就是3,定义在x210_sd.h)写入了这个寄存器,所以这里读出的肯定是3,经过判断就是movinand。所以实际执行的函数是:saveenv_movinand`

    int saveenv_movinand(void)
    {
            movi_write_env(virt_to_phys((ulong)env_ptr));
            puts("done\n");
       
            return 1;
    }
    
  6. 真正执行保存环境变量操作的是:cpu/s5pc11x/movi.c中的movi_write_env函数,这个函数肯定是写sd卡,将DDR中的环境变量数组(其实就是default_environment这个数组,大小16kb,刚好32个扇区)写入iNand中的ENV分区中。

    void movi_write_env(ulong addr)
    {
        movi_write(raw_area_control.image[2].start_blk,
               raw_area_control.image[2].used_blk, addr);
    }
       
    ulong movi_write(ulong start, lbaint_t blkcnt, void *src)
    {
        return mmc_bwrite(0, start, blkcnt, src);
    }
    
  7. raw_area_control是uboot中规划iNnad/SD卡的原始分区表,这个里面记录了我们对iNand的分区,env分区也在这里,下标是2.追到这一层就够了,再里面就是调用驱动部分的写SD卡/iNand的底层函数了。

    CONFIG_EVT1=y // autoconf.mk中定义
    #if defined(CONFIG_EVT1)
            image[2].start_blk = image[1].start_blk + MOVI_BL1_BLKCNT;
    #else
            image[2].start_blk = image[1].start_blk - MOVI_ENV_BLKCNT;
    #endif
            image[2].used_blk = MOVI_ENV_BLKCNT;
            image[2].size = CFG_ENV_SIZE;
            image[2].attribute = 0x10;
            strcpy(image[2].description, "environment");
            dbg("env: %d\n", image[2].start_blk);
    

    我们在 init_raw_area_table() 函数中看到对uboot对各个分区的初始化情况。这里环境变量分区 environment 是第2个分区

getenv

  1. getenv 不可重入版本,实现方式就是去遍历default_environment数组,挨个拿出所有的环境变量比对name,找到相等的直接返回这个环境变量的首地址即可。

    char *getenv (char *name)
    
  2. getenv_r 可重入版本

    int getenv_r (char *name, char *buf, unsigned len)
    
  3. getenv函数是直接返回这个找到的环境变量在DDR中环境变量处的地址,而getenv_r函数的做法是找到了DDR中环境变量地址后,将这个环境变量复制一份到提供的buf中,而不动原来DDR中环境变量。

  4. 差别就是:getenv中返回的地址只能读不能随便乱写,而getenv_r中返回的环境变量是在自己提供的buf中,是可以随便改写加工的。

  5. 总结

    • 功能是一样的,但是可重入版本会比较安全一些,建议使用。
    • 有关于环境变量的所有操作,主要理解了环境变量在DDR中的存储方法,理解了环境变量和gd全局变量的关联和优先级,理解了环境变量在存储介质中的存储方式(专用raw分区),整个环境变量相关的都清楚了。

uboot与linux驱动

uboot本身是裸机程序

  1. 裸机本来是没有驱动的概念的(狭义的驱动的概念就是操作系统中用来具体操控硬件的那部分代码叫驱动)
  2. 裸机程序中是直接操控硬件的,操作系统中必须通过驱动来操控硬件。这两个有什么区别?本质区别就是分层。

uboot的虚拟地址对硬件操作的影响

  1. 操作系统(指的是linux)下MMU肯定是开启的,也就是说linux驱动中肯定都使用的是虚拟地址。而纯裸机程序中根本不会开MMU,全部使用的是物理地址。这是裸机下和驱动中操控硬件的一个重要区别。
  2. uboot早期也是纯物理地址工作的,但是现在的uboot开启了MMU做了虚拟地址映射,这个东西驱动也必须考虑。查uboot中的虚拟地址映射表,发现除了 0x30000000-0x3FFFFFFF 映射到了 0xC0000000-0xCFFFFFFF 之外,其余的虚拟地址空间全是原样映射的。而我们驱动中主要是操控硬件寄存器,而S5PV210的SFR都在 0xExxxxxx 地址空间,因此驱动中不必考虑虚拟地址。

uboot借用(移植)了linux驱动

  1. linux驱动本身做了模块化设计。linux驱动本身和linux内核不是强耦合的,这是linux驱动可以被uboot借用(移植)的关键。
  2. uboot移植了linux驱动源代码。uboot是从源代码级别去移植linux驱动的,这就是linux系统的开源性。
  3. uboot中的硬件驱动比linux简单。linux驱动本身有更复杂的框架,需要实现更多的附带功能,而uboot本质上只是个裸机程序,uboot移植linux驱动时只是借用了linux驱动的一部分而已。

iNand/SD驱动解析

驱动整体比较庞大,涉及很多个文件夹下的很多文件,函数更多,学习时必须有顺序。一般从 start_armboot() 函数开始。我们着重分析 start_armboot 函数中的 mmc 驱动

mmc_initialize

具体实现如下:

int mmc_initialize(bd_t *bis)
{
    struct mmc *mmc;
    int err;

    INIT_LIST_HEAD(&mmc_devices);
    cur_dev_num = 0;

    if (board_mmc_init(bis) < 0)
        cpu_mmc_init(bis);

    mmc = find_mmc_device(0);
    if (mmc) {
        err = mmc_init(mmc);
        if (err)
            err = mmc_init(mmc);
        if (err) {
            printf("Card init fail!\n");
            return err;
        }
    }
    printf("%ldMB\n", (mmc->capacity/(1024*1024/(1<<9))));
    return 0;
}
  1. 函数位于:uboot/drivers/mmc/mmc.c

    #if defined(CONFIG_X210)
        #if defined(CONFIG_GENERIC_MMC)
            puts ("SD/MMC:  ");
            // 初始化函数
            mmc_exist = mmc_initialize(gd->bd);
            if (mmc_exist != 0)
            {
                puts ("0 MB\n");
            }
        #endif
    #endif
    
  2. 从名字可以看出,这个函数的作用就是初始化开发板上MMC系统。MMC系统的初始化应该包含这么几部分:

    • SoC里的MMC控制器初始化
      • MMC系统时钟的初始化
      • SFR初始化
    • SoC里MMC相关的GPIO的初始化
    • SD卡/iNand芯片的初始化
  3. mmc_devices链表全局变量,用来记录系统中所有已经注册的SD/iNand设备。所以向系统中插入一个SD卡/iNand设备,则系统驱动就会向mmc_devices链表中插入一个数据结构表示这个设备。

    • mmc 类 struct mmc *mmc

      struct mmc *mmc;
      

      其中 struct mmc 是 mmc 类,包含了mmc的函数和有关的数据

      struct mmc {
          struct list_head link;
          // 类属性
          char name[32];
          void *priv;
          // 省略......
          u32 capacity;
          struct mmc_ext_csd ext_csd; /* mmc v4 extended card specific */
          block_dev_desc_t block_dev;
          // 类方法
          int (*send_cmd)(struct mmc *mmc,
                  struct mmc_cmd *cmd, struct mmc_data *data);
          void (*set_ios)(struct mmc *mmc);
          int (*init)(struct mmc *mmc);
      };
      
    • 初始化链表

      SD/mmc 链表用的是 linux 的指针

      // linux/list.h
      #define INIT_LIST_HEAD(ptr) do { \
          (ptr)->next = (ptr); (ptr)->prev = (ptr); \
      } while (0)
           
      struct list_head {
          struct list_head *next, *prev;
      };
      

      mmc_devices是一个全局变量,在 mmc.c 文件中定义,让 mmc_devices 头节点指向自身。

      static struct list_head mmc_devices;
      INIT_LIST_HEAD(&mmc_devices)
      
    • 初始化 SoC 端 mmc 控制器

      if (board_mmc_init(bis) < 0)
          cpu_mmc_init(bis);
      

      因为 mmc_init 是 SoC 端的,所以 board_mmc_init 不执行

      int cpu_mmc_init(bd_t *bis)
      {
          setup_hsmmc_clock();
          setup_hsmmc_cfg_gpio();
          return smdk_s3c_hsmmc_init();
      }
      

      在这里初始化了时钟、GPIO以及三星SoC端MMC控制器

    • setup_hsmmc_clock 初始化 MMC 的时钟

      我们使用的是 MMC 的通道0(用于SoC内部的iNand)和通道2(用于外部的SD卡)

      void setup_hsmmc_clock(void)
      {
          /* MMC0 clock src = SCLKMPLL */
          tmp = CLK_SRC4_REG & ~(0x0000000f);
          CLK_SRC4_REG = tmp | 0x00000006;
           
          /* MMC0 clock div */
          tmp = CLK_DIV4_REG & ~(0x0000000f);
          clock = get_MPLL_CLK()/1000000;
          /* 计算分频系数i,限制频率在50MHz一下 
              因为硬件最大只支持 50MHz*/
          for(i=0; i<0xf; i++)
          {
              if((clock / (i+1)) <= 50) {
                  CLK_DIV4_REG = tmp | i<<0;
                  break;
              }
          }
           
      #ifdef USE_MMC2
          /* MMC2 clock src = SCLKMPLL */
          tmp = CLK_SRC4_REG & ~(0x00000f00);
          CLK_SRC4_REG = tmp | 0x00000600;
           
          /* MMC2 clock div */
          tmp = CLK_DIV4_REG & ~(0x00000f00);
          CLK_DIV4_REG = tmp | i<<8;
      #endif
      }
      
    • setup_hsmmc_cfg_gpio 初始化 GPIO

      void setup_hsmmc_cfg_gpio(void)
      {
          /* MMC channel 0 */
          /* 7 pins will be assigned : 
              GPG0[0:6] = CLK, CMD, CDn, DAT[0:3] */
          reg = readl(GPG0CON) & 0xf0000000;
          writel(reg | 0x02222222, GPG0CON);
          reg = readl(GPG0PUD) & 0xffffc000;
          writel(reg | 0x00002aaa, GPG0PUD);
          writel(0x00003fff, GPG0DRV);
           
      #ifdef USE_MMC2
          /* MMC channel 2 */
          /* 7 pins will be assigned : 
              GPG2[0:6] = CLK, CMD, CDn, DAT[0:3] */
          reg = readl(GPG2CON) & 0xf0000000;
          writel(reg | 0x02222222, GPG2CON);
          reg = readl(GPG2PUD) & 0xffffc000;
          writel(reg | 0x00002aaa, GPG2PUD);
          writel(0x00003fff, GPG2DRV);
      #endif
      }
      
    • SoC端MMC控制器初始化 smdk_s3c_hsmmc_init

      int smdk_s3c_hsmmc_init(void)
      {
          err = s3c_hsmmc_initialize(0);
          err = s3c_hsmmc_initialize(2);
          return -1;
      }
      

      最关键的是 s3c_hsmmc_initialize 这个函数。下面我们就来仔细分析一下这个函数的具体过程。

smdk_s3c_hsmmc_init 的分析

static int s3c_hsmmc_initialize(int channel)
{
    struct mmc *mmc;

    mmc = &mmc_channel[channel];

    sprintf(mmc->name, "S3C_HSMMC%d", channel);
    mmc->priv = &mmc_host[channel];
    mmc->send_cmd = s3c_hsmmc_send_command;
    mmc->set_ios = s3c_hsmmc_set_ios;
    mmc->init = s3c_hsmmc_init;

    // 省略......

    switch(channel) {
    case 0:
        mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_0_BASE;
        break;
    case 1:
        mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_1_BASE;
        break;
    case 2:
        mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_2_BASE;
        break;
#ifdef USE_MMC3
    case 3:
        mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_3_BASE;
        break;
#endif
    default:
        printk("mmc err: not supported channel %d\n", channel);
    }

    return mmc_register(mmc);
}
  1. 指定 MMC 类实例对象的内存

    struct mmc *mmc;
    mmc = &mmc_channel[channel];
    

    其中 MMC_Channel 是一个全局变量,它实际就是 MMC 类的实例所在的内存

    struct mmc mmc_channel[MMC_MAX_CHANNEL];
    
  2. 初始化实例中的元素

    // 设置实例的名称
    sprintf(mmc->name, "S3C_HSMMC%d", channel);
    mmc->priv = &mmc_host[channel];
    // 设置实例的方法,这些都是SoC的驱动
    mmc->send_cmd = s3c_hsmmc_send_command;
    mmc->set_ios = s3c_hsmmc_set_ios;
    mmc->init = s3c_hsmmc_init;
    // 设置MMC卡的地址
       
    switch(channel) {
    case 0:
        // #define ELFIN_HSMMC_0_BASE 0xEB000000
    	mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_0_BASE;
    	break;
    case 1:
    	mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_1_BASE;
    	break;
    case 2:
        // #define ELFIN_HSMMC_0_BASE 0xEB200000
    	mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_2_BASE;
    	break;
    #ifdef USE_MMC3
    case 3:
    	mmc_host[channel].ioaddr = (void *)ELFIN_HSMMC_3_BASE;
    	break;
    #endif
    default:
    	printk("mmc err: not supported channel %d\n", channel);
    }
    
  3. 把新的MMC卡注册到系统中

    return mmc_register(mmc);
    

mmc_register

int mmc_register(struct mmc *mmc)
{
    /* Setup the universal parts of the block interface just once */
    mmc->block_dev.if_type = IF_TYPE_MMC;
    // 省略 .......
    mmc->block_dev.block_write = mmc_bwrite;

    // 初始化link,让link->prev, link->next指向link
    INIT_LIST_HEAD(&mmc->link);

    list_add_tail(&mmc->link, &mmc_devices);

    return 0;
}

list_add_tail() 是把 mmc 挂到 mmc_devices 节点的前面

static inline void __list_add(
         struct list_head *new,
         struct list_head *prev,
         struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

这里的 prev = head->prev; next = head

总结

  1. 函数位于:uboot/drivers/mmc/mmc.c
  2. 从名字可以看出,这个函数的作用就是初始化开发板上MMC系统。MMC系统的初始化应该包含这么几部分:SoC里的MMC控制器初始化(MMC系统时钟的初始化、SFR初始化)、SoC里MMC相关的GPIO的初始化、SD卡/iNand芯片的初始化。
  3. mmc_devices链表是全局变量,用来记录系统中所有已经注册的SD/iNand设备。所以向系统中插入一个SD卡/iNand设备,则系统驱动就会向mmc_devices链表中插入一个数据结构表示这个设备。

  4. smdk_s3c_hsmmc_init
    • 函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。
    • 函数内部通过宏定义USE_MMCx来决定是否调用s3c_hsmmc_initialize来进行具体的初始化操作。
  5. s3c_hsmmc_initialize
    1. 函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。
    2. 定义并且实例化一个struct mmc类型的对象(定义了一个指针,并且给指针指向有意义的内存,或者说给指针分配内存),然后填充它的各种成员,最后调用mmc_register函数来向驱动框架注册这个mmc设备驱动。
    3. mmc_register功能是进行mmc设备的注册,注册方法其实就是将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。
    4. 我们在X210中定义了USE_MMC0USE_MMC2,因此在我们的uboot初始化时会调用2次s3c_hsmmc_initialize函数,传递参数分别是0和2,因此完成之后系统中会注册上2个mmc设备,表示当前系统中有2个mmc通道在工作。

find_mmc_device

完成 mmc_devices() 初始化工作之后,我们就继续看 mmc_initialize 的下一步。下一步是根据设备号找到对应的 MMC 卡,在执行初始化,这时的初始化就是 MMC 卡的初始化了,前面是控制器的初始化

mmc = find_mmc_device(0);
if (mmc) {
    err = mmc_init(mmc);
    if (err)
        err = mmc_init(mmc);
    if (err) {
        printf("Card init fail!\n");
        return err;
    }
}

那么下面是对 find_mmc_device() 的解析:

struct mmc *find_mmc_device(int dev_num)
{
    struct mmc *m;
    struct list_head *entry;

    list_for_each(entry, &mmc_devices) {
        m = list_entry(entry, struct mmc, link);

        // 通过设备号比较来找到对应的MMC实例对象
        if (m->block_dev.dev == dev_num)
            return m;
    }

    printf("MMC Device %d not found\n", dev_num);

    return NULL;
}
  1. list_for_each(entry, &mmc_devices) 是让 entry 依次指向 mmc_devices 设备链表中的每一个 device 中的链表 link 成员。注意,这类的成员 link 不一定是结构体 struct mmc 的首地址。

    #define list_for_each(pos, head) \
        for (pos = (head)->next, prefetch(pos->next); pos != (head); \
            pos = pos->next, prefetch(pos->next))
    
  2. list_entry 是返回这个结构体 entry 的地址。因为 link 不一定是首地址,所以要做一个减法

    #define list_entry(ptr, type, member) \
        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
    

    link在结构体中的位置

MMC 卡的初始化 mmc_init

MMC 卡的初始化就是调用 SoC 的具体驱动来向 MMC 卡中的单片机发送指令,让它做好初始化工作。下面的代码只是摘取了整个初始化的大概流程,中间去掉了很多细节。真正的代码不是这样。

int mmc_init(struct mmc *host)
{
    int err;

    err = host->init(host);
    /* Reset the Card */
    err = mmc_go_idle(host);
    /* Test for SD version 2 */
    err = mmc_send_if_cond(host);
    err = mmc_send_app_op_cond(host);
    /* If the command timed out, we check for an MMC card */
    if (err == TIMEOUT) {
        err = mmc_send_op_cond(host);
    } else
    return mmc_startup(host);
}

这些 mmc_ 开头的函数是具体MMC卡的驱动函数,它们通过读写SoC的MMC控制寄存器来操作MMC

总结

find_mmc_device

  1. 这个函数位于:uboot/drivers/mmc/mmc.c中。
  2. 这个函数其实就是通过mmc设备编号来在系统中查找对应的mmc设备(struct mmc的对象,根据上面分析系统中有2个,编号分别是0和2)。
  3. 函数工作原理就是通过遍历mmc_devices链表,去依次寻找系统中注册的mmc设备,然后对比其设备编号和我们当前要查找的设备编号,如果相同则就找到了要找的设备。找到了后调用mmc_init函数来初始化它。

mmc_init

  1. 函数位于:drivers/mmc/mmc.c

  2. 分析猜测这个函数应该要进行mmc卡的初始化了(前面已经进行了SoC端控制器的初始化)

  3. 函数的调用关系为:

    mmc_init
        mmc_go_idle
            mmc_send_cmd
        mmc_send_if_cond
            mmc_send_cmd
        ······
    

    具体分析可以看出,mmc_init函数内部就是依次通过向mmc卡发送命令码(CMD0、CMD2那些)来初始化SD卡/iNand内部的控制器,以达到初始化SD卡的目的。

  • 至此整个MMC系统初始化结束。

  • 整个MMC系统初始化分为2大部分:

    -SoC这一端的MMC控制器的初始化,

    • SD卡这一端卡本身的初始化。
    • 前一步主要是在cpu_mmc_init函数中完成
    • 后一部分主要是在mmc_init函数中完成。
  • 整个初始化完成后去使用sd卡/iNand时,操作方法和mmc_init函数中初始化SD卡的操作一样的方式。读写sd卡时也是通过总线向SD卡发送命令、读取/写入数据来完成的。

  • 顺着操作追下去,到了mmc_send_cmd函数处就断了,真正的向SD卡发送命令的硬件操作的函数找不到。这就是学习驱动的麻烦之处。

  • struct mmc结构体是关键。两部分初始化之间用mmc结构体来链接的,初始化完了后对mmc卡的常规读写操作也是通过mmc结构体来链接的。

struct mmc

  1. 驱动的设计中有一个关键数据结构。譬如MMC驱动的结构体就是struct mmc这些结构体中包含一些变量和一些函数指针,变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体。
  2. 一个驱动工作时主要就分几部分:
    • 驱动构建(构建一个struct mmc然后填充它)
    • 驱动运行时(调用这些函数指针指针的函数和变量)

分离思想

  1. 分离思想就是说在驱动中将操作方法和数据分开。
  2. 操作方法就是函数,数据就是变量
  3. 所谓操作方法和数据分离的意思就是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。

分层思想

  1. 分层思想是指一个整个的驱动分为好多个层次。简单理解就是驱动分为很多个源文件,放在很多个文件夹中。譬如本课程讲的mmc的驱动涉及到drivers/mmc下面的2个文件和cpu/s5pc11x下的好几个文件。
  2. 以mmc驱动为例来分析各个文件的作用:
    • uboot/drivers/mmc/mmc.c:本文件的主要内容是和MMC卡操作有关的方法,譬如MMC卡设置空闲状态的、卡读写数据等。但是本文件中并没有具体的硬件操作函数,操作最终指向的是struct mmc结构体中的函数指针,这些函数指针是在驱动构建的时候和真正硬件操作的函数挂接的(真正的硬件操作的函数在别的文件中)。
    • uboot/drivers/mmc/s3c_hsmmc.c:本文件中是SoC内部MMC控制器的硬件操作的方法,譬如向SD卡发送命令的函数(s3c_hsmmc_send_command),譬如和SD卡读写数据的函数(s3c_hsmmc_set_ios),这些函数就是具体操作硬件的函数,也就是mmc.c中需要的那些硬件操作函数。这些函数在mmc驱动初始化构建时(s3c_hsmmc_initialize函数中)和struct mmc挂接起来备用。
    • mmc.cs3c_hsmmc.c构成了一个分层,mmc.c中调用了s3c_hsmmc.c中的函数,所以mmc.c在上层,s3c_hsmmc.c在下层。这两个分层后我们发现mmc.c中不涉及具体硬件的操作,s3c_hsmmc.c中不涉及驱动工程时的时序操作。因此移植的时候就有好处:譬如我们要把这一套mmc驱动移植到别的SoC上mmc.c就不用动,s3c_hsmmc.c动就可以了;譬如SoC没变但是SD卡升级了,这时候只需要更换mmc.c,不需要更换s3c_hsmmc.c即可。
  3. cpu/s5pc11x/下面还有一个setup_hsmmc.c,也和MMC驱动有关。但是这些代码为什么不能放到drivers目录下去,而要放到cpu目录下去?因为这里面的2个函数(setup_hsmmc_clocksetup_hsmmc_cfg_gpio)都是和SoC有关的初始化函数,这两个函数不能放到drivers目录下去。实际上如果非把这两个函数放在uboot/drivers/mmc/s3c_hsmmc.c文件中也凑活能说过去。