linux内核学习初笔记(6)uboot第二阶段我的分析

uboot第一阶段分析完毕,根据第一阶段最后的分析,程序跳转到了ram中继续执行start_armboot这个函数。

下面来看看这个函数主要完成了什么功能。

首先要说明一个结构体,之前提到过,叫全局数据结构体,用于保存一些全局数据,在uboot第一阶段设置栈指针时,其实就已经为这个结构体在内存中留出了空间,它的地址就是在堆的地址下方,栈的上方。

全局数据结构体定义如下所示:

typedef struct global_data {
 bd_t  *bd;
 unsigned long flags;
 unsigned long baudrate;
 unsigned long have_console; /* serial_init() was called */
 unsigned long reloc_off; /* Relocation Offset */
 unsigned long env_addr; /* Address  of Environment struct */
 unsigned long env_valid; /* Checksum of Environment valid? */
 unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
 unsigned char vfd_type; /* display type */
#endif
#if 0
 unsigned long cpu_clk; /* CPU clock in Hz!  */
 unsigned long bus_clk;
 unsigned long ram_size; /* RAM size */
 unsigned long reset_status; /* reset status register at boot */
#endif
 void  **jt;  /* jump table */
} gd_t;

其中的bd_t也是一个结构体,后面可以发现,uboot为bd_t也在内存分配了空间,就在gd_t之下。

bd_t的定义如下:

typedef struct bd_info {
    int   bi_baudrate; /* serial console baudrate */
    unsigned long bi_ip_addr; /* IP Address */
    unsigned char bi_enetaddr[6]; /* Ethernet adress */
    struct environment_s        *bi_env;
    ulong         bi_arch_number; /* unique id for this board */
    ulong         bi_boot_params; /* where this board expects params */
    struct    /* RAM configuration */
    {
 ulong start;
 ulong size;
    }    bi_dram[CONFIG_NR_DRAM_BANKS];
} bd_t;

在start_armboot函数中的第一条就是:

DECLARE_GLOBAL_DATA_PTR

这个宏展开就是:

#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

DECLARE_GLOBAL_DATA_PTR 定义一个gd_t 全局数据结构的指针,这个指针存放在指定的寄
存器r8 中。这个声明也避免编译器把r8 分配给其它的变量。任何想要访问全局数据区的代码,
只要代码开头加入“DECLARE_GLOBAL_DATA_PTR”一行代码,然后就可以使用gd 指针来访问全局
数据区了。

在下面的程序:

/* Pointer is writable since we allocated a register for it */
 gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
 /* compiler optimization barrier needed for GCC >= 3.4 */
 __asm__ __volatile__("": : :"memory");

 memset ((void*)gd, 0, sizeof (gd_t));
 gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
 memset (gd->bd, 0, sizeof (bd_t));

由上面程序可见,先在内存中为gd_t分配了空间,然后让前面定义的gd指针指向它的首地址,接着也分配了bd_t结构体的空间,这个空间在gd_t之下,然后将gd_t中的bd指针赋值为它的首地址。

继续贴程序:

 for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
  if ((*init_fnc_ptr)() != 0) {
   hang ();
  }
 }

这段程序从表面上来看就是在执行一个函数指针数组中的函数。这个函数指针数组是:

init_fnc_t *init_sequence[] = {
 cpu_init,  /* basic cpu dependent setup */
 board_init,  /* basic board dependent setup */
 interrupt_init,  /* set up exceptions */
 env_init,  /* initialize environment */
 init_baudrate,  /* initialze baudrate settings */
 serial_init,  /* serial communications setup */
 console_init_f,  /* stage 1 init of console */
 display_banner,  /* say that we are here */
 dram_init,  /* configure available RAM banks */
 display_dram_config,
#if defined(CONFIG_VCMA9) || defined (CONFIG_CMC_PU2)
 checkboard,
#endif
 NULL,
};

这里还需要补充一点儿C语言的知识。我们都知道数组名本身就是一个指针,而且是指针常量,指针值本身不能随意更改,这个指针常量指向的就是数组中的第一个变量。如果数组中的内容本身就是指针,如本例所示,那数组名就是双重指针,这很好理解,首先它是一个指针,并且它指向数组中第一个元素也是一个指针,也就是这个指针指向一个指针,那它自然就是双重指针了。所以根据分析,我们这里的数组名init_sequence就是一个双重指针。

我们在start_armboot函数之前已经定义了一个双重指针:init_fnc_t **init_fnc_ptr;那么这个init_fnc_t 是什么数据类型呢,在找找,发现以下代码:

typedef int (init_fnc_t) (void);

很显然,这里我们用typedef定义了一种新的数据类型,这种数据类型应该是一种函数,而且这种函数的参数为void,返回值为int。

init_fnc_t **init_fnc_ptr就是定义了一个init_fnc_t这样类型函数的双重指针。而我们init_sequence数组中的每个函数其实都是参数为void,返回值为int的函数,所以数组名init_sequence就也是参数为void,返回值为int类型函数的双重指针。这样,我们很好理解,在for语句中:init_fnc_ptr = init_sequence。*init_fnc_ptr是判断函数指针是否存在。++init_fnc_ptr是指向数组的下一个元素,即指向下一个函数指针。for循环中的if语句:if ((*init_fnc_ptr)() != 0)就是用双重函数指针调用数组中的这些函数。*init_fnc_ptr就是提取init_fnc_ptr指向的内容,自然就是得到数组中的某个元素,即某个函数指针,然后使用函数指针直接调用这个函数。如果函数返回为非0,则调用hang(),停止执行。

下面来看看函数指针数组中的每个函数究竟完成什么功能。

1、cpu_init();定义于cpu/arm920t/cpu.c 

分配IRQ,FIQ 栈底地址,由于没有定义CONFIG_USE_IRQ,所以相当于空实现。

2、board_init:极级初始化,定义于board/smdk2410/smdk2410.c

其中有这么两句:

S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
 S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();

这两句是得到2410时钟控制寄存器和GPIO控制寄存器的基地址并将其转为S3C24X0_CLOCK_POWER和S3C24X0_GPIO类型的指针,而这两种类型是两种已经定义的结构体类型,这个指针认为在这个基地址起始的地方有这样一个结构体,又可以看到这两种结构体中的数据的定义分配方式与2410的时钟控制寄存器和GPIO控制寄存器的分布情况是一样的。所以通过这两个指针就可以访问这些控制寄存器的值,避免经常与地址打交道(uboot的代码真是好神气)以后还会经常有这样类似的代码。

设置PLL 时钟,GPIO,使能I/D cache.
设置bd 信息:gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;//板子的ID,没啥意
义。
gd->bd->bi_boot_params = 0x30000100;//内核启动参数存放地址

LOCKTIME用于设置PLL设置好后的时钟锁定之间。

3、interrupt_init

初始化2410 的PWM timer 4,使其能自动装载计数值,恒定的产生时间中断信号,但是中断被屏蔽了用不上。PWM4技术的频率是100hz,即10ms周期。这个函数里面有几个全局变量,后面udelay函数会用到,后面再分析。

4、env_init

这个函数用于初始化环境变量相关参数,因为环境变量可以保存在nand或者nor中,所以这个函数有不同的实现方式。

先说说在nor中的实现:

定义于common/env_flash.c功能:指定环境区的地址。default_environment 是默认的环境参数设置。

 

 if (crc32(0, env_ptr->data, ENV_SIZE) == env_ptr->crc) {
  gd->env_addr  = (ulong)&(env_ptr->data);
  gd->env_valid = 1;
  return(0);
 }
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 0;

这里要分清楚两种情况,一种是我们第一次从norflash启动uboot或者不是第一次启动但是之前并没有将环境变量保存在norflash中,另一种情况当然就是我们已经将环境变量保存在了norflash中了。我们这里需要判断这两种情况,env_ptr指针:env_t *env_ptr = (env_t *)CFG_ENV_ADDR;指向的就是norflash中环境结构体的首地址。另外有必要看一下环境变量结构体的定义:

typedef struct environment_s {
 unsigned long crc;  /* CRC32 over data bytes */
#ifdef CFG_REDUNDAND_ENVIRONMENT
 unsigned char flags;  /* active/obsolete flags */
#endif
 unsigned char data[ENV_SIZE]; /* Environment data  */
} env_t;

所以我们判断时就是看看norflash的环境变量结构体的数据做CRC校验能不能和结构体已经保存的CRC校验码一致,若是一致说明norflash中已经保存了有效的环境变量,则将gd的env_valid置1,说明环境变量有效。并且在gd中记录下norflash中环境变量结构体中环境变量数组的地址。否则,若norflash中没有环境变量,则使用默认的环境变量。

再说说在nand中的实现:

int env_init(void)
{
 DECLARE_GLOBAL_DATA_PTR;

   gd->env_addr  = (ulong)&default_environment[0];
 gd->env_valid = 1;

 return (0);
}

可以看到,nand中的实现不需要判断了,因为这时还没有初始化nand,不能从nand中随便读取到数据,所以就使用默认的环境变量。并且这里假设env_valid为1,这是有原因的,下面在环境变量的重定位函数中会知道。

5、init_baudrate

直接上代码吧:

 DECLARE_GLOBAL_DATA_PTR;

 uchar tmp[64]; /* long enough for environment variables */
 int i = getenv_r ("baudrate", tmp, sizeof (tmp));
 gd->bd->bi_baudrate = gd->baudrate = (i > 0)
   ? (int) simple_strtoul (tmp, NULL, 10)
   : CONFIG_BAUDRATE;

 return (0);

显然是得到环境变量中保存的波特率值,并且将其赋值给全局变量数据结构。

这里我们深入到getenv_r函数内部看看:

int getenv_r (char *name, char *buf, unsigned len)
{
 int i, nxt;

 for (i=0; env_get_char(i) != '\0'; i=nxt+1) {//env_get_char函数就是得到环境变量数组中的第i个字符
  int val, n;

  for (nxt=i; env_get_char(nxt) != '\0'; ++nxt) {//依次搜寻,直到'\0',相当于把从i开始的一项环境变量遍历一遍
   if (nxt >= CFG_ENV_SIZE) {//nxt记录的是总的搜寻序号值,CFG_ENV_SIZE是环境变量结构体的大小
    return (-1);
   }
  }
  if ((val=envmatch((uchar *)name, i)) < 0)//这里的i实际是指向当前遍历的那一项环境变量的首字符序号。envmatch函数在这一项环境变量字符串中搜寻与name匹配的环境变量的值的字符串首序号~(说起来很别扭啊,同志们自己感悟啊),找到之后将序号的值返回。
   continue;                  //
若环境变量不能和name匹配,则跳到下一个项环境变量,注意因为在上面第二个for语句中,已经将nxt的值指向了当前环境变量项的末尾,所以跳出这个内部for语句后,i=nxt+1,则i又成功指向了下一项环境变量字符串首字符的序号
  /* found; copy out */
  n = 0;
  while ((len > n++) && (*buf++ = env_get_char(val++)) != '\0')//最后依次将val开始的环境变量值拷贝到buf
   ;
  if (len == n)//如果拷贝的长度在len之内,则肯定buf已经拷贝了字符串末尾字符'\0',否则len==n的话就要在buf后加上字符串末尾
   *buf = '\0';
  return (n);
 }
 return (-1);
}

再回到init_baudrate函数:

get_env_r函数返回的i值保存的是实际拷贝到tmp的环境变量的长度。最后用simple_strtoul函数将字符串tmp转为unsigned long赋值给全局数据结构体的响应数据。所以init_baudrate函数归根结底就是使用环境变量值来初始化全局数据区中波特率的值。

6、serial_init

从函数的名字可知道是初始化串口的用的。

里面又调用了serial_setbrg ()这个函数。

serial_setbrg ()代码如下:

DECLARE_GLOBAL_DATA_PTR;
 S3C24X0_UART * const uart = S3C24X0_GetBase_UART(UART_NR);
 int i;
 unsigned int reg = 0;

 /* value is calculated so : (int)(PCLK/16./baudrate) -1 */
 reg = get_PCLK() / (16 * gd->baudrate) - 1;

 /* FIFO enable, Tx/Rx FIFO clear */
 uart->UFCON = 0x07;
 uart->UMCON = 0x0;
 /* Normal,No parity,1 stop,8 bit */
 uart->ULCON = 0x3;
 /*
  * tx=level,rx=edge,disable timeout int.,enable rx error int.,
  * normal,interrupt or polling
  */
 uart->UCON = 0x245;
 uart->UBRDIV = reg;

注释已经写的很明白了,还是一样得到串口控制寄存器的基地址将其 转为一种S3C24X0_UART结构体指针,该指针就指向控制寄存器基地址,认为这个基地址这里有这样一个结构体,又因为这个结构体内部数据布局与控制寄存器布局一样,所以通过这个指针就可以以结构的形式访问串口控制寄存器组。这里唯一的疑问在于UART_NR的值,因为2440和2410都有三个串口,那么我们初始化的串口应该由UART_NR指定,这个值:

#define UART_NR S3C24X0_UART0

S3C24X0_UART0又是枚举类型中的一项:

typedef enum {
 S3C24X0_UART0,
 S3C24X0_UART1,
 S3C24X0_UART2
} S3C24X0_UARTS_NR;

所以S3C24X0_UART0的值是0,UART_NR也是0。

主要来说,serial_setbrg就是根据刚才init_baudrate函数执行时从环境变量得到并且 保存在全局数据结构体中的波特率值来设置串口的波特率,并且设置串口的其他寄存器值,比如串口的一些协议,数据位,停止位,校验位等,还有串口收发FIFO的设置,具体内容可以参考2440手册。

再说明一点,get_PCLK()这个函数定义在speed.c文件,这个文件只包含了2410或者2400的情况,我们移植到2440时,当然需要修改这里面的代码,主要是添加一些宏判断。

7、console_init_f

控制台前期初始化

由于标准设备还没有初始化(gd->flags & GD_FLG_DEVINIT=0),这时控制台使用串口作
为控制台
函数只有一句:gd->have_console = 1;

8、display_banner

打印一些版本和地址信息

9、dram_init

初始化内存RAM 信息。board/smdk2410/smdk2410.c
其实就是给gd->bd 中内存信息表赋值而已。
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;

10、display_dram_config

打印dram_init中设置的ram的相关信息

这个函数指针数组终于研究完了。

下面继续往下面走,到了flash_init函数:

 int i, j;
 ulong size = 0;

 for (i = 0; i < CFG_MAX_FLASH_BANKS; i++) {//依次访问每个flash
  ulong flashbase = 0;   
  //基地址为0

  flash_info[i].flash_id =//设置id
#if defined(CONFIG_AMD_LV400)
   (AMD_MANUFACT & FLASH_VENDMASK) |
   (AMD_ID_LV400B & FLASH_TYPEMASK);
#elif defined(CONFIG_AMD_LV800)
   (AMD_MANUFACT & FLASH_VENDMASK) |
   (AMD_ID_LV800B & FLASH_TYPEMASK);
#else
#error "Unknown flash configured"
#endif
   flash_info[i].size = FLASH_BANK_SIZE;//设置flash的总容量
  flash_info[i].sector_count = CFG_MAX_FLASH_SECT;//设置flash的扇区数目
  memset (flash_info[i].protect, 0, CFG_MAX_FLASH_SECT);//
清零各个扇区的保护位
  if (i == 0)
   flashbase = PHYS_FLASH_1;
  else
   panic ("configured too many flash banks!\n");
  for (j = 0; j < flash_info[i].sector_count; j++)
{//依次设置flash的每个扇区的起始地址,在start数组中保存。第一个扇区为16KB,第二个第三个为8KB,第四个为32KB,以后的每个都是64KB
   if (j <= 3) {
    /* 1st one is 16 KB */
    if (j == 0) {
     flash_info[i].start[j] =
      flashbase + 0;
    }

    /* 2nd and 3rd are both 8 KB */
    if ((j == 1) || (j == 2)) {
     flash_info[i].start[j] =
      flashbase + 0x4000 + (j -
              1) *
      0x2000;
    }

    /* 4th 32 KB */
    if (j == 3) {
     flash_info[i].start[j] =
      flashbase + 0x8000;
    }
   } else {
    flash_info[i].start[j] =
     flashbase + (j - 3) * MAIN_SECT_SIZE;
   }
  }
  size += flash_info[i].size;//用size记录所有flash的总容量
 }

 flash_protect (FLAG_PROTECT_SET,//flash_protect函数用于将flash中的一段空间所在的响应扇区的保护位设置为1,表示被保护,不能随意修改。
         CFG_FLASH_BASE,
         CFG_FLASH_BASE + monitor_flash_len - 1,
         &flash_info[0]);

 flash_protect (FLAG_PROTECT_SET,
         CFG_ENV_ADDR,
         CFG_ENV_ADDR + CFG_ENV_SIZE - 1, &flash_info[0]);

 return size;

首先,要明白,每一块flash都对应一个flash_info_t结构体用来控制这个flash,所有的flash_info_t结构体放在一起就是数组flash_info[CFG_MAX_FLASH_BANKS],在这个函数中主要是初始化每个flash控制结构体。

总的来说,flash_init()的操作就是读取ID 号,ID 号指明了生产商和设备号,根据这些信息设置size,sector_count,flash_id.以及start[]、protect[]。

移植到2440时,必须要根据自己板子的norflash情况修改这段的代码。

下面继续回到start_armboot:

mem_malloc_init (_armboot_start - CFG_MALLOC_LEN)

进入这个函数,可以看到这个函数实现很简单,就是将内存中保留的堆区清零,这个堆区应该是在uboot在ram中的基地址以下。堆的大小是CFG_MALLOC_LEN

下面继续start_armboot,执行到了env_relocate ()函数。该函数实现环境的重定位。进入这个函数:

env_ptr = (env_t *)malloc (CFG_ENV_SIZE);//在堆中为环境申请存储空间

env_get_char = env_get_char_memory;//以后要访问环境变量中的字符都是通过这个函数

if (gd->env_valid == 0) {                          //根据前面env_init函数的执行分析,可知当从norflash启动并且flash中没有备份的环境变量时,env_valid就等于0。执行下面的代码,主要是将堆栈中的环境变量区清零,并且将默认环境变量从可执行文件拷贝到环境变量区,然后更新crc检验结果,并且置env_valid为1.表明全局数据区记录的环境变量地址有效
#if defined(CONFIG_GTH) || defined(CFG_ENV_IS_NOWHERE) /* Environment not changable */
  puts ("Using default environment\n\n");
#else
  puts ("*** Warning - bad CRC, using default environment\n\n");
  SHOW_BOOT_PROGRESS (-1);
#endif

  if (sizeof(default_environment) > ENV_SIZE)
  {
   puts ("*** Error - default environment is too large\n\n");
   return;
  }

  memset (env_ptr, 0, sizeof(env_t));
  memcpy (env_ptr->data,
   default_environment,
   sizeof(default_environment));
#ifdef CFG_REDUNDAND_ENVIRONMENT
  env_ptr->flags = 0xFF;
#endif
  env_crc_update ();
  gd->env_valid = 1;
 }
 else {
  env_relocate_spec ();//当从norflash启动但是有备份或者从nandflash启动时执行该函数。
 }
 gd->env_addr = (ulong)&(env_ptr->data);

env_relocate_spec ()函数根据从norflash启动和nandflash启动有两种实现方式,如果是norflash启动,则功能就是把flash中的环境变量拷贝的堆中的环境变量区中,如果是nandflash启动,也是读取nandflash中的环境变量拷贝到堆中环境变量区,然后判断读取是否成功,还要判断读取的内容是否通过CRC校验(如果之前没有在nand中备份环境变量,则会发生这种情况),那么就将可执行文件中的默认环境变量数组拷贝到堆中,并且更新CRC检验值,置env_valid为1。

最后,从env_relocate_spec ()函数返回后,再把堆中环境变量数组的首地址保存在全局数据结构体中。

再次回到start_armboot:

gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");//将环境变量中保存的ip地址取到全局数据区中。

下面继续贴程序: 

/* MAC Address */
 {          //这里是一个新的作用域
  int i;
  ulong reg;
  char *s, *e;
  uchar tmp[64];

  i = getenv_r ("ethaddr", tmp, sizeof (tmp));//i中保存读取到的字符数
  s = (i > 0) ? tmp : NULL;

  for (reg = 0; reg < 6; ++reg) {
   gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;//将s转存成unsigned long,每次转8位,一共48位,相当于6个字节
   if (s)
    s = (*e) ? e + 1 : e;//
  }
 }

这里要说明,使用simple_strtoul 函数时,当遇到非数字的字符时,就停止转换。而s中保存的物理地址字符串是六个字节存放的,每个字节之间应该有空格隔开。这样每次用simple_strtoul 转换一个字节的数据,一共转换六次即可。simple_strtoul 函数的参数e是一个指针,将指针的地址传递给函数是 为了改变这个指针值本身(C语言的知识:如果这里只是传递e本身,则在函数里面这就是一个局部变量,不会改变e的真正值,要改变e的值,必须将其地址传入,然后在函数内用指针方式来改变其值,又因为e本身就是指针,所以这里的参数类型为双重指针)。每次返回后,e都指向最后一个字符,即空格或者最后的字符串结束符,如果是结束符,则循环将结束,s赋值为e,否则s赋值为e+1,即s指向下一个字节,开始下一次转换。

start_armboot再往下执行到了devices_init ()程序。

该函数主要实现如下:

devlist = ListCreate (sizeof (device_t));
drv_system_init ();

ListCreate函数代码如下:

 list_t list;

 list = (list_t) (NewHandle (sizeof (ListStruct)));  /* create empty list */
 if (list) {
  (*list)->signature = LIST_SIGNATURE;
  (*list)->numItems = 0;
  (*list)->listSize = 0;
  (*list)->itemSize = elementSize;
  (*list)->percentIncrease = kDefaultAllocationPercentIncrease;
  (*list)->minNumItemsIncrease =
    kDefaultAllocationminNumItemsIncrease;
 }

 return list;

主要是在内存中为设备链表控制结构分配了空间,并用双重指针list指向它。

NewHandle函数代码:

void *memPtr;
 HandleRecord *hanPtr;

 memPtr = calloc (numBytes, 1);
 hanPtr = (HandleRecord *) calloc (sizeof (HandleRecord), 1);
 if (hanPtr && (memPtr || numBytes == 0)) {
  hanPtr->ptr = memPtr;
  hanPtr->size = numBytes;
  return (Handle) hanPtr;
 } else {
  free (memPtr);
  free (hanPtr);
  return NULL;
 }

NewHandle函数在堆中为设备链表结构体分配了空间,并且用一个双重指针指向他。注意这里hanPtr本身就是HandleRecord结构体类型的指针,HandleRecord的第一个参数又是一个指针,并且赋值为hanPtr->ptr = memPtr,即指向设备链表结构体分配的空间,所以zaireturn的时候将hanPtr强制转换成双重指针,那么通过这个双重指针最终访问的就是设备链表结构体。

回到ListCreate函数,这个函数在得到设备链表控制结构体的指针后就初始化这个结构体。

设备链表控制结构体定义如下:

 typedef struct ListStructTag
    {
    int signature;              /* debugging aid */
    int percentIncrease;        /* %of current size to increase by when list is out of space */
    int minNumItemsIncrease;    /* fixed number of items to increase by when list is out of space */
    int listSize;               /* number of items than can fit in the currently allocated memory */
    int itemSize;               /* the size of each item in the list (same for every item) */
    int numItems;               /* number of items currently in the list */
    unsigned char itemList[1];  /* resizable array of list elements */
    } ListStruct;

注释已经很明确了,不用多解释,其中itemSize就是链表中每一项的大小,我们在链表中存放的是设备结构体,所以用形参设备结构体的大小来初始化它。

设备结构体定义如下:

typedef struct {
 int flags;   /* Device flags: input/output/system */
 int ext;   /* Supported extensions   */
 char name[16];  /* Device name    */

/* GENERAL functions */

 int (*start) (void);  /* To start the device   */
 int (*stop) (void);  /* To stop the device   */

/* OUTPUT functions */

 void (*putc) (const char c); /* To put a char   */
 void (*puts) (const char *s); /* To put a string (accelerator) */

/* INPUT functions */

 int (*tstc) (void);  /* To test if a char is ready... */
 int (*getc) (void);  /* To get that char   */

/* Other functions */

 void *priv;   /* Private extensions   */
} device_t;

可以看出device_t主要是用于输入输出设备的。

drv_system_init ()函数:

 device_t dev;

 memset (&dev, 0, sizeof (dev));

 strcpy (dev.name, "serial");
 dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT | DEV_FLAGS_SYSTEM;

 dev.putc = serial_putc;
 dev.puts = serial_puts;
 dev.getc = serial_getc;
 dev.tstc = serial_tstc;

device_register (&dev);

该函数定义了一个设备结构,并初始化为串口,将输入输出等函数指针都指向串口的输入输出函数。

最终通过调用device_register (&dev)函数将串口设备加入了设备链表结构体中的设备链表中。

下面回到start_armboot:

到了函数jumptable_init ()。这个函数就是用于将一些函数指针登记到全局数据结构体中。

下面是函数console_init_r ()。进入函数看看代码:

DECLARE_GLOBAL_DATA_PTR;

 device_t *inputdev = NULL, *outputdev = NULL;
 int i, items = ListNumItems (devlist);

/* Scan devices looking for input and output devices */
 for (i = 1;                 //依次访问设备链表中的每一个设备
      (i <= items) && ((inputdev == NULL) || (outputdev == NULL));
      i++
     ) {
  device_t *dev = ListGetPtrToItem (devlist, i);//得到该设备指针

  if ((dev->flags & DEV_FLAGS_INPUT) && (inputdev == NULL)) { //判断该设备是否为输入设备,如果是则记录在inputdev,刚才我们定义串口设备时定义了这个flag,所以这里应该inputdev保存刚才定义的串口设备结构体的指针
   inputdev = dev;
  }
  if ((dev->flags & DEV_FLAGS_OUTPUT) && (outputdev == NULL)) {//判断该设备是否为输出设备,如果是则记录在outputdev,刚才我们定义串口设备时定义了这个flag,所以这里应该inputdev保存刚才定义的串口设备结构体的指针

   outputdev = dev;
  }
 }

 /* Initializes output console first */
 if (outputdev != NULL) {
  console_setfile (stdout, outputdev);//更新stdio_devices数组中记录的标准输出设备为outputdev,这里就是串口设备
  console_setfile (stderr, outputdev);//更新stdio_devices数组中记录的标准错误输出设备为outputdev,即串口设备
 }

 /* Initializes input console */
 if (inputdev != NULL) {
  console_setfile (stdin, inputdev);更新stdio_devices数组中记录的标准输入设备为inputdev,即串口设备
 }

 gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */该flag表明标准设备已经初始化好,可以使用,在之后的输入或者输出时就可以使用标准设备了

/* Setting environment variables */
 for (i = 0; i < 3; i++) {
  setenv (stdio_names[i], stdio_devices[i]->name);//把标准输入输出设备名称保存到环境变量
 }

回到start_armboot

enable_interrupts ()函数:

其实什么都没干,因为没有定义中断使能。

cs8900_get_enetaddr (gd->bd->bi_enetaddr);

该函数代码如下:

int i;
 unsigned char env_enetaddr[6];
 char *tmp = getenv ("ethaddr");
 char *end;

 for (i=0; i<6; i++) {
  env_enetaddr[i] = tmp ? simple_strtoul(tmp, &end, 16) : 0;
  if (tmp)
   tmp = (*end) ? end+1 : end;
 }

 /* verify chip id */
 if (get_reg_init_bus (PP_ChipID) != 0x630e)
  return;
 eth_reset ();
 if ((get_reg (PP_SelfST) & (PP_SelfSTAT_EEPROM | PP_SelfSTAT_EEPROM_OK)) ==
   (PP_SelfSTAT_EEPROM | PP_SelfSTAT_EEPROM_OK)) {

  /* Load the MAC from EEPROM */
  for (i = 0; i < 6 / 2; i++) {
   unsigned int Addr;

   Addr = get_reg (PP_IA + i * 2);
   addr[i * 2] = Addr & 0xFF;
   addr[i * 2 + 1] = Addr >> 8;
  }

  if (memcmp(env_enetaddr, "\0\0\0\0\0\0", 6) != 0 &&
      memcmp(env_enetaddr, addr, 6) != 0) {
   printf ("\nWarning: MAC addresses don't match:\n");
   printf ("\tHW MAC address:  "
    "%02X:%02X:%02X:%02X:%02X:%02X\n",
    addr[0], addr[1],
    addr[2], addr[3],
    addr[4], addr[5] );
   printf ("\t\"ethaddr\" value: "
    "%02X:%02X:%02X:%02X:%02X:%02X\n",
    env_enetaddr[0], env_enetaddr[1],
    env_enetaddr[2], env_enetaddr[3],
    env_enetaddr[4], env_enetaddr[5]) ;
   debug ("### Set MAC addr from environment\n");
   memcpy (addr, env_enetaddr, 6);
  }
  if (!tmp) {
   char ethaddr[20];
   sprintf (ethaddr, "%02X:%02X:%02X:%02X:%02X:%02X",
     addr[0], addr[1],
     addr[2], addr[3],
     addr[4], addr[5]) ;
   debug ("### Set environment from HW MAC addr = \"%s\"\n",    ethaddr);
   setenv ("ethaddr", ethaddr);
  }

 }

在setenv中调用了int console_assign (int file, char *devname); /* Assign the console 重定向标准输入输出*/

这个函数功能就是把名为devname的设备重定向为标准IO文件file(stdin,stdout,stderr)。其执行过程是在devlist中查找devname的设备,返回这个设备的device_t指针,并把指针值赋给std_device[file]。

该函数首先从环境变量中取得MAC地址;然后再从网卡的e2rom中得到实际的MAC地址,并将两者进行比较,如果不相同则返回环境变量中获得的地址(这种情况一般不会发生);可能发生的情况是第一次启动时环境变量中没有保存MAC地址,这时执行最后一个if语句,将网卡中获得的实际MAC地址拷贝到环境变量中去,然后返回该MAC值。

 

再次返回start_armboot:

if ((s = getenv ("loadaddr")) != NULL) {
  load_addr = simple_strtoul (s, NULL, 16);
 }
#if (CONFIG_COMMANDS & CFG_CMD_NET)
 if ((s = getenv ("bootfile")) != NULL) {
  copy_filename (BootFile, s, sizeof (BootFile));
 }
#endif /* CFG_CMD_NET */

就是从环境变量区得到内核加载地址和启动文件。

eth_initialize(gd->bd);

初始化网卡,这之前,全局变量区已经从保存好了ip地址和mac地址。

 for (;;) {
  main_loop ();
 }跳到main_loop()执行。

main_loop代码:

这个函数里有太多编译选项,对于smdk2410,去掉所有选项后等效下面的程

void main_loop()
{
static char lastcommand[CFG_CBSIZE] = { 0, };
int len;
int rc = 1;
int flag;
char *s;
int bootdelay;
s = getenv ("bootdelay"); //自动启动内核等待延时
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;
debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);
s = getenv ("bootcmd"); //取得环境中设置的启动命令行
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "");
if (bootdelay >= 0 && s && !abortboot (bootdelay))
{
run_command (s, 0);//执行启动命令行,smdk2410.h 中没有定义
CONFIG_BOOTCOMMAND,所以没有命令执行。
}
for (;;) {
len = readline(CFG_PROMPT);//读取键入的命令行到console_buffer
flag = 0; /* assume no special flags for now */
if (len > 0)
strcpy (lastcommand, console_buffer);//拷贝命令行到lastcommand.
else if (len == 0)
flag |= CMD_FLAG_REPEAT;
if (len == -1)
puts ("\n");
else
rc = run_command (lastcommand, flag); //执行这个命令行。
if (rc <= 0) {
/* invalid command or not repeatable, forget it */
lastcommand[0] = 0;
}
}
其中abortboot()用于内核启动延时等待,用户若不再设置的延时时间内按下任意键,则linux内核就自动启动,执行bootcmd启动命令,如果按下了任意按键,则if条件为假,继续执行uboot代码。abortboot()代码:

 int abort = 0;

printf("Hit any key to stop autoboot: %2d ", bootdelay);

while ((bootdelay > 0) && (!abort)) {
  int i;

  --bootdelay;
  /* delay 100 * 10ms */
  for (i=0; !abort && i<100; ++i) {
   if (tstc()) { /* we got a key press */
    abort  = 1; /* don't auto boot */
    bootdelay = 0; /* no more delay */

(void) getc();

break;
   }
   udelay (10000);
  }

  printf ("\b\b\b%2d ", bootdelay);
 }

 putc ('\n');

return abort;

后面在无限for循环中调用readline函数,该函数实际上就是等待用户输入cmd,然后将有效的cmd保存在console_buffer中,然后返回,然后run_command执行该命令。

readline函数:

char   *p = console_buffer;
 int n = 0;    /* buffer index  */
 int plen = 0;   /* prompt length */
 int col;    /* output column cnt */
 char c;

 /* print prompt */
 if (prompt) {
  plen = strlen (prompt);
  puts (prompt);
 }
 col = plen;

 for (;;) {

WATCHDOG_RESET();  /* Trigger watchdog, if needed */

c = getc();

  /*
   * Special character handling
   */
  switch (c) {
  case '\r':    /* Enter  */
  case '\n':
   *p = '\0';
   puts ("\r\n");
   return (p - console_buffer);

  case '\0':    /* nul   */
   continue;

  case 0x03:    /* ^C - break  */
   console_buffer[0] = '\0'; /* discard input */
   return (-1);

  case 0x15:    /* ^U - erase line */
   while (col > plen) {
    puts (erase_seq);
    --col;
   }
   p = console_buffer;
   n = 0;
   continue;

  case 0x17:    /* ^W - erase word  */
   p=delete_char(console_buffer, p, &col, &n, plen);
   while ((n > 0) && (*p != ' ')) {
    p=delete_char(console_buffer, p, &col, &n, plen);
   }
   continue;

  case 0x08:    /* ^H  - backspace */
  case 0x7F:    /* DEL - backspace */
   p=delete_char(console_buffer, p, &col, &n, plen);
   continue;

  default:
   /*
    * Must be a normal character then
    */
   if (n < CFG_CBSIZE-2) {
    if (c == '\t') { /* expand TABs  */

puts (tab_seq+(col&07));
     col += 8 - (col&07);
    } else {
     ++col;  /* echo input  */
     putc (c);
    }
    *p++ = c;
    ++n;
   } else {   /* Buffer full  */
    putc ('\a');
   }
  }
 }

注意的是,readline首先通过串口打印了提示符:"SMDK2410 # ",这是通过该函数的形参CFG_PROMPT完成的。可以修改这个宏定义来改变命令提示符:

#define CFG_PROMPT  "SMDK2410 # " /* Monitor Command Prompt */

runcommand函数代码可以参考我转发的博文uboot中mainloop分析一文。

首先要搞清楚以下几个基本概念:

1、命令结构体定义

struct cmd_tbl_s {
 char  *name;  /* Command Name   */
 int  maxargs; /* maximum number of arguments */
 int  repeatable; /* autorepeat allowed?  */
     /* Implementation function */
 int  (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
 char  *usage;  /* Usage message (short) */
#ifdef CFG_LONGHELP
 char  *help;  /* Help  message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
 /* do auto completion on the arguments */
 int  (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};

 2、下面这个宏定义,用于设定命令结构体的section属性

#define Struct_Section  __attribute__ ((unused,section (".u_boot_cmd")))

3、下面这条宏定义用于定义一个命令结构体,并且设定其section属性(使用上面定义的宏Struct_Section),并且用大括号中的数据初始化这个结构体。

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}

举例:

例如:定义一个命令boot

U_BOOT_CMD(boot, 0, 0, fun, "boot xxx");


展开以后会变成:

cmd_tbl_t __u_boot_cmd_boot __attribute___((unused, section(".u_boot_cmd"))) = {"boot", 0, 0, fun, "boot xxx"}

大部分基本不变,将Struct_Section展开了,还将##name换成了boot, 将#name换成了"boot"。应该不难看出##/#的作用吧。

这样,我们就清楚了如何添加一个uboot命令:

应该在include/com_confdefs.h 中定义一个命令选项标志位。
在板子的配置文件中添加命令自己的选项。

按照u-boot 的风格,可以在common/下面添加自己的cmd_*.c,

并且定义自己的命令结构体变量,如U_BOOT_CMD(
mycommand, 2, 1, do_mycommand,
"my command!\n",
"...\n"
" ..\n"
);
然后实现自己的命令处理函数do_mycommand(cmd_tbl_t *cmdtp, int flag, int argc, char*argv[])。

 

uboot第二阶段分析就到这里,再打下去,手就抽筋了,欢迎回复啊

 

 

 

 

你可能感兴趣的:(linux内核学习初笔记(6)uboot第二阶段我的分析)