003@ uboot第二阶段分析(下)

对于uboot两阶段的小结,很重要!!!

第一阶段:

在脚本uboot.lds中 ENTRY(_start) _start的地址一般是TEXT_BASE

设置异常向量表

  进入svc管理模式 arm状态

  关看门狗 关中断 时钟初始化

  cpu初始化(关mmu 关数据和指令缓存 cpu速率)和内存初始化 在函数cpu_init_crit中完成

  重定位

  设置栈

  清bss断

  跳入到start_armboot函数

第二阶段 就进入到lib_arm/board.c    中的start_kernel函数

分配gd   指针指向的空间和gd->bd   指针指向的空间

执行init_sequence        函数序列,其实主要目的是初始化并填充gd和gd->bd结构体

分配堆空间     mem_malloc_init ,这样才可以初始化环境变量,因为环境变量是要从nand拷贝到内存中的堆空间里

nand初始化      nand_init,因为现在普及采用nand,若是用nor的话,在之前已经初始化了 flash_init    函数

环境变量初始化    env_reloacate 有四个很重要的环境变量参数:bootdelay(启动延时),bootcmd(启动命令),menucmd(菜单),bootargs(启动参数,也即最原始的命令行参数)

串口设置的5个函数,这样就能看到串口打印数据

混杂设备 misc_init_r函数

进入循环执行 main_loop函数,处理启动命令或者用户输入的命令 该函数是在common/main.c     中

    第一种情况是 在bootdelay内不按空格键:s=getenv ("bootcmd");run_command (s, 0);    直接启动内核了

    第二种轻狂就是 在bootdelay内按下空格键进入menu菜单里: s = getenv("menucmd");run_command (s, 0);

     然后进入命令循环获取用户从串口里打印的字符 len = readline         (CFG_PROMPT);run_command (lastcommand, flag); 

之后在run_command里if ((cmdtp = find_cmd(argv[0])) == NULL)--->if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0)即根据     

命令的名字 找到该命令表结构体,调用它的cmd参数 最终还是回到do_xxx  run_command就是来解析命令的

run_command分别解析nand命令和bootm命令,nand命令负责把linux内核读到内存中,bootm负责去启动内核镜像,调用do_bootm----->do_bootm_linux,启动内核

/****************************   开始第二阶段分析   **************************************/

开始第二阶段

第二阶段是在lib_arm/board.c      bootm.c两个文件

start_armboot    是 U-Boot执行的第一个C语言函数,完成系统初始化工作,进入主循环,处理用户输入的命令。

看board.c            文件

#include <common.h>

#include <command.h>

#include <malloc.h>

#include <devices.h>

#include <version.h>

#include <net.h>

#include <serial.h>

#include <nand.h>

#include <onenand_uboot.h>

#ifdef CONFIG_DRIVER_SMC91111

#include "../drivers/net/smc91111.h"

#endif

#ifdef CONFIG_DRIVER_LAN91C96

#include "../drivers/net/lan91c96.h"

#endif

DECLARE_GLOBAL_DATA_PTR;

ulong monitor_flash_len;

#ifdef CONFIG_HAS_DATAFLASH

extern int  AT91F_DataflashInit(void);

extern void dataflash_print_info(void);

#endif

#ifndef CONFIG_IDENT_STRING

#define CONFIG_IDENT_STRING ""

#endif

const char version_string[] =

     U_BOOT_VERSION" (" __DATE__ " - " __TIME__ ")"CONFIG_IDENT_STRING;

#ifdef CONFIG_DRIVER_CS8900

extern void cs8900_get_enetaddr (uchar * addr);

#endif

#ifdef CONFIG_DRIVER_RTL8019

extern void rtl8019_get_enetaddr (uchar * addr);

#endif

#if defined(CONFIG_HARD_I2C) || \

    defined(CONFIG_SOFT_I2C)

#include <i2c.h>

#endif

/*

* Begin and End of memory area for malloc(), and current "brk"

*/

static ulong mem_malloc_start = 0;

static ulong mem_malloc_end = 0;

static ulong mem_malloc_brk = 0;

static

void mem_malloc_init (ulong dest_addr)

{

     mem_malloc_start = dest_addr;

     mem_malloc_end = dest_addr + CFG_MALLOC_LEN;

     mem_malloc_brk = mem_malloc_start;

     memset ((void *) mem_malloc_start, 0,

               mem_malloc_end - mem_malloc_start);

}

void *sbrk (ptrdiff_t increment)

{

     ulong old = mem_malloc_brk;

     ulong new = old + increment;

     if ((new < mem_malloc_start) || (new > mem_malloc_end)) {

          return (NULL);

     }

     mem_malloc_brk = new;

     return ((void *) old);

}

/************************************************************************

* Coloured LED functionality

************************************************************************

* May be supplied by boards if desired

*/

void inline __coloured_LED_init (void) {}

void inline coloured_LED_init (void) __attribute__((weak, alias("__coloured_LED_init")));

void inline __red_LED_on (void) {}

void inline red_LED_on (void) __attribute__((weak, alias("__red_LED_on")));

void inline __red_LED_off(void) {}

void inline red_LED_off(void)          __attribute__((weak, alias("__red_LED_off")));

void inline __green_LED_on(void) {}

void inline green_LED_on(void) __attribute__((weak, alias("__green_LED_on")));

void inline __green_LED_off(void) {}

void inline green_LED_off(void)__attribute__((weak, alias("__green_LED_off")));

void inline __yellow_LED_on(void) {}

void inline yellow_LED_on(void)__attribute__((weak, alias("__yellow_LED_on")));

void inline __yellow_LED_off(void) {}

void inline yellow_LED_off(void)__attribute__((weak, alias("__yellow_LED_off")));

/************************************************************************

* Init Utilities                                   *

************************************************************************

* Some of this code should be moved into the core functions,

* or dropped completely,

* but let's get it working (again) first...

*/

static int init_baudrate (void)

{

     char 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);

}

static int display_banner (void)

{

     printf ("\n\n%s\n\n", version_string);

     debug ("U-Boot code: %08lX -> %08lX  BSS: -> %08lX\n",

            _armboot_start, _bss_start, _bss_end);

#ifdef CONFIG_MODEM_SUPPORT

     debug ("Modem Support enabled\n");

#endif

#ifdef CONFIG_USE_IRQ

     debug ("IRQ Stack: %08lx\n", IRQ_STACK_START);

     debug ("FIQ Stack: %08lx\n", FIQ_STACK_START);

#endif

     return (0);

}

/*

* WARNING: this code looks "cleaner" than the PowerPC version, but

* has the disadvantage that you either get nothing, or everything.

* On PowerPC, you might see "DRAM: " before the system hangs - which

* gives a simple yet clear indication which part of the

* initialization if failing.

*/

static int display_dram_config (void)   //打印显示ram的配置信息

{

     int i;

#ifdef DEBUG

     puts ("RAM Configuration:\n");

     for(i=0; i<CONFIG_NR_DRAM_BANKS; i++) {

          printf ("Bank #%d: %08lx ", i, gd->bd->bi_dram[i].start);

          print_size (gd->bd->bi_dram[i].size, "\n");

     }

#else

     ulong size = 0;

     for (i=0; i<CONFIG_NR_DRAM_BANKS; i++) {

          size += gd->bd->bi_dram[i].size;

     }

     puts("DRAM:  ");

     print_size(size, "\n");

#endif

     return (0);

}

#ifndef CFG_NO_FLASH

static void display_flash_config (ulong size)

{

     puts ("Flash: ");

     print_size (size, "\n");

}

#endif /* CFG_NO_FLASH */

#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)

static int init_func_i2c (void)

{

     puts ("I2C:   ");

     i2c_init (CFG_I2C_SPEED, CFG_I2C_SLAVE);

     puts ("ready\n");

     return (0);

}

#endif

/*

* Breathe some life into the board...

*

* Initialize a serial port as console, and carry out some hardware

* tests.

*

* The first part of initialization is running from Flash memory;

* its main purpose is to initialize the RAM so that we

* can relocate the monitor code to RAM.

*/

/*

* All attempts to come up with a "common" initialization sequence

* that works for all boards and architectures failed: some of the

* requirements are just _too_ different. To get rid of the resulting

* mess of board dependent #ifdef'ed code we now make the whole

* initialization sequence configurable to the user.

*

* The requirements for any new initalization function is simple: it

* receives a pointer to the "global data" structure as it's only

* argument, and returns an integer return code, where 0 means

* "continue" and != 0 means "fatal error, hang the system".

*/

typedef int (init_fnc_t) (void);

int print_cpuinfo (void); /* test-only */

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 */   打印uboot相关信息

#if defined(CONFIG_DISPLAY_CPUINFO)

     print_cpuinfo,          /* display cpu info (and speed) */

#endif

#if defined(CONFIG_DISPLAY_BOARDINFO)

     checkboard,          /* display board info */

#endif

#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)

     init_func_i2c,

#endif

     dram_init,     /* configure available RAM banks */ 

     //配置可用的ram,非常重要,在这里开始进行配置DRAM信息,用到结构体bd->bi_dram[BANK_NR].start

                                                                                                                      bd->bi_dram[BANK_NR].size

     display_dram_config,  //打印显示ram的配置信息

     NULL,

};

init_fnc_t *init_sequence[ ] = {

cpu_init,   /* 基本的处理器相关配置 -- cpu/arm920t/cpu.c */

board_init, /* 基本的板级相关配置 -- board/smdk2410/smdk2410.c */

interrupt_init,  /* 初始化中断处理 -- cpu/arm920t/s3c24x0/interrupt.c */

env_init,      /* 初始化环境变量 -- common/cmd_flash.c */

init_baudrate,  /* 初始化波特率设置 -- lib_arm/board.c */

serial_init,  /* 串口通讯设置 -- cpu/arm920t/s3c24x0/serial.c */

console_init_f,       /* 控制台初始化阶段1 -- common/console.c */

display_banner,       /* 打印u-boot信息 -- lib_arm/board.c */

dram_init,     /* 配置可用的RAM -- board/smdk2410/smdk2410.c */

display_dram_config,  /* 显示RAM的配置大小 -- lib_arm/board.c */

NULL,

};

void start_armboot (void)      //start_armboot函数 非常重要,c function第一个函数!!!

{

     init_fnc_t **init_fnc_ptr;

     char *s;

#if !defined(CFG_NO_FLASH) || defined (CONFIG_VFD) || defined(CONFIG_LCD)

     ulong size;

#endif

#if defined(CONFIG_VFD) || defined(CONFIG_LCD)

     unsigned long addr;

#endif

//在重定位之后,即uboot的代码从flash拷到sdram 此时连接脚本里的_start 等于TEXT_BASE 

     /* Pointer is writable since we allocated a register for it */

    

// 给全局变量gd分配空间大小且指定gd的位置 这里gd是一个结构体,在uboot内存分布中

     是CFG_GBL_DATA_SIZE一共128字节

     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指针所指向的空间清零

     gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));      //给gd中的bd指针分配空间大小

     memset (gd->bd, 0, sizeof (bd_t));                  //gd->bd 所指向的空间清零

     gd->flags |= GD_FLG_RELOC;

     monitor_flash_len = _bss_start - _armboot_start;      //uboot镜像文件的大小

     for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { 

 //执行初始化序列函数 这里是调用了一系列的c函数指针,进行初始化。

比如cpu_init初始化完成各个gpio管脚初始化,board_init完成arch_number设置和boot_params约定存放地址,还有串口初始化等。

          if ((*init_fnc_ptr)() != 0) {

               hang ();

          }

     }

              

               board/smdk2410/flash.c配置flash

#ifndef CFG_NO_FLASH     识别出来是哪一种flash nor还是nand 如果定义了CFG_NO_FLASH这个宏,说明是nand 否则是nor

     /* configure available FLASH banks */

     size = flash_init ();                           //nor型flash的初始化

     display_flash_config (size);

#endif /* CFG_NO_FLASH */

                     定义显示类型 分vfd和lcd两种。vfd一般不用,我们用lcd的

                     在这里定义了帧缓冲,也就显存的的地址和大小

     -----------------------------------------------------------------------------------------------------------

#ifdef CONFIG_VFD           

#     ifndef PAGE_SIZE

#       define PAGE_SIZE 4096  //定义页大小4k

#     endif

     /*

     * reserve memory for VFD display (always full pages)

     */

     /* bss_end is defined in the board-specific linker script */

     addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);

     size = vfd_setmem (addr);

     gd->fb_base = addr; 

#endif /* CONFIG_VFD */

#ifdef CONFIG_LCD      //在内存中配置一块帧缓冲区

     /* board init may have inited fb_base */

     if (!gd->fb_base) {

#          ifndef PAGE_SIZE

#            define PAGE_SIZE 4096

#endif

/*

* reserve memory for LCD display (always full pages)

*/

/* bss_end is defined in the board-specific linker script */

     addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);  按页对其方式保留显存

     size = lcd_setmem (addr);          // 分配帧缓冲区的大小

     gd->fb_base = addr;                    //  帧缓冲区的物理起始地址

     }

#endif /* CONFIG_LCD */

--------------------------------------------------------------------------------------------------------------------

     /* armboot_start is defined in the board-specific linker script */

     mem_malloc_init (_armboot_start - CFG_MALLOC_LEN);     //分配堆空间大小 这样才可以初始化环境变量

    

//初始化nand flash,这是在nand flash启动的s3c2410移植u-boot的关键,根据flash时序编写函数即可

//在include/configs/smdk2410.h中的command definition中增加CONFIG_COMMANDS和CFG_CMD_NAND命令

//nand型flash的初始化

#if defined(CONFIG_CMD_NAND)

     puts ("NAND:  ");        打印标志:   NAND: 64MB

     nand_init();          /* go init the NAND */  //board/smdk2410/smdk2410.c,获取nand的基地址和 大小信息

#endif

#if defined(CONFIG_CMD_ONENAND)  三星的一种特别的flash  onenand,类似于nand

     onenand_init();

#endif

#ifdef CONFIG_HAS_DATAFLASH

     AT91F_DataflashInit();

     dataflash_print_info();

#endif

/* initialize environment */

     env_relocate ();  

//环境变量的初始化 该函数是在commen/env_commen.c中定义的,在该文件中还定义了一个默认下的环境变量

default_enviornment[]  第一次启动时,nand 默认里面是没有环境变量的,则根据板文件的宏选用默认的环境变量。

可以进行修改通过saveenv进行保存到nand里。

//env_relocate将环境变量从存储设备中读取到全局变量env_t env_ptr的data里面,由于该函数从以上堆空间

//中分配空间,所以我们的环境变量都放在了堆空间里面了

env_relocate ----> env_ptr = (env_t *)malloc (CONFIG_ENV_SIZE); gd->env_addr = (ulong)&(env_ptr->data);此处是告诉

环境变量存放在内存中的地址自此, 环境变量已经存放在内存gd->env_addr处,这样就可以获取环境变量或者

修改环境变量保存到nand中去了

#ifdef CONFIG_VFD    //framebuffer初始化

     /* must do this after the framebuffer is allocated */

     drv_vfd_init();  //video的初始化

#endif /* CONFIG_VFD */

#ifdef CONFIG_SERIAL_MULTI  //多串口

     serial_initialize();

#endif

//从环境变量中获取IP地址 以太网接口MAC地址 主要是初始化 gd->bd->bi_ip_addr和gd->bd->bi_enetaddr[]

-----------------------------------------------------------------

     /* IP Address */

     gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");     通过读取环境变量的ip地址

     /* MAC Address */

     {

          int i;

          ulong reg;

          char *s, *e;

          char tmp[64];

          i = getenv_r ("ethaddr", tmp, sizeof (tmp));

          s = (i > 0) ? tmp : NULL;

          for (reg = 0; reg < 6; ++reg) {

               gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;

               if (s)

                    s = (*e) ? e + 1 : e;

          }

#ifdef CONFIG_HAS_ETH1  如果要是有两块以太网卡

          i = getenv_r ("eth1addr", tmp, sizeof (tmp));

          s = (i > 0) ? tmp : NULL;

          for (reg = 0; reg < 6; ++reg) {

               gd->bd->bi_enet1addr[reg] = s ? simple_strtoul (s, &e, 16) : 0;

               if (s)

                    s = (*e) ? e + 1 : e;

          }

#endif

     }

----------------------------------------------------------------------------

     devices_init (); /* get the devices list going. */     注册设备链表,其实也就只注册了一个串口设备

#ifdef CONFIG_CMC_PU2

     load_sernum_ethaddr ();

#endif /* CONFIG_CMC_PU2 */

    

     jumptable_init ();

     console_init_r ();/* fully init console as a device */ 

//控制台设备的初始化阶段2  到这里终于可以从控制台上看到数据打印出来了

    

------------------------------------------------------------------------------------------------------

注意:从串口寄存器的设置到最终在终端上打印信息,是有以下函数组成的。

   在init_sequense里的三个函数和

      init_baudrate,   设置 gd->bd->bi_baudrate

      serial_init,         直接调用serial_setbrg函数初始化UART寄存器:8个数据位,一个开始位,一个停止位,无校验位。。。

      console_init_f,  控制台前期初始化  设置gd->have_console=1

      devices_init,    调用drv_system_init 注册串口设备

      console_init_r   控制台后期初始化,将串口设备指向控制台标准输入设备,标准输出设备,标准错误设备

     In:   serial

     Out: serial

     Err: serial  

打印这三行信息,表明串口作为标准输入设备,标准输出设备,标准错误输出设备,这样就能打印信息了

默认情况下,键盘和鼠标默认为标准输入设备,显示器默认为标准输出设备和标准错误输出设备,printf为标准格式输出

scanf为标准格式输入。标准输入,标准输出设备的重定向即将串口设备作为标准输入设备,将串口做为标准输出设备和

标准错误输出设备

static void drv_system_init (void) 

static void drv_system_init (void) 

    device_t dev;     //定义一个设备结构体 

    memset (&dev, 0, sizeof (dev));//为刚刚定义的结构体分配内存 

    strcpy (dev.name, "serial");    //名称 

    dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT | DEV_FLAGS_SYSTEM;   设备的类型

#ifdef CONFIG_SERIAL_SOFTWARE_FIFO 

    dev.putc = serial_buffered_putc; 

    dev.puts = serial_buffered_puts; 

    dev.getc = serial_buffered_getc; 

    dev.tstc = serial_buffered_tstc; 

#else 

    dev.putc = serial_putc; 

    dev.puts = serial_puts; 

    dev.getc = serial_getc; 

    dev.tstc = serial_tstc; 

#endif 

                             填充该设备结构体并注册

    device_register (&dev);//注册函数  将该串口设备注册到devlist

#ifdef CFG_DEVICE_NULLDEV 

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

    strcpy (dev.name, "nulldev"); 

    dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT | DEV_FLAGS_SYSTEM; 

    dev.putc = nulldev_putc; 

    dev.puts = nulldev_puts; 

    dev.getc = nulldev_input; 

    dev.tstc = nulldev_input; 

    device_register (&dev); 

#endif 

                        

                        

                        

console_init_r     函数

                        

2    int console_init_r (void)

3    {

4        device_t *inputdev = NULL, *outputdev = NULL;  定义两个设备

5        int i, items = ListNumItems (devlist);   //  取得设备链中的设备数 因为只注册了一个设备,即items=1

6        /* Scan devices looking for input and output devices */

7        for (i = 1;

8             (i <= items) && ((inputdev == NULL) || (outputdev == NULL));

9             i++

10            ) {

11            device_t *dev = ListGetPtrToItem (devlist, i);   从devlist中获取该串口设备

12            if ((dev->flags & DEV_FLAGS_INPUT) && (inputdev == NULL)) {   执行

13                inputdev = dev;     即输入设备为该串口设备

14            }

15            if ((dev->flags & DEV_FLAGS_OUTPUT) && (outputdev == NULL)) {   执行

16                outputdev = dev;   即输出设备也为该串口设备

17            }

18        }

   

// 7~18行,在设备链中按注册的顺序查找输入输出设备,在设备注册时 dev.flags表示此设备的类型。

// 比如这里drv_system_init,此设备是第一个注册的设备,且其dev.flags为

// DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT | DEV_FLAGS_SYSTEM 所以上面18行过后,输入输出设备都指定为

// drv_system_init里注册的设备了

console_setfile 是最关键的地方,在这里才是真正的给我们的标准输入设备,标准输出设备,标准错误输出设备填充结构

19        /* Initializes output console first */                               

20        if (outputdev != NULL) {     

通过console_setfile()函数可以看出,控制台有一个包含 3 个 device_t 元素的数组stdio_devices,分别对应

     stdin,stdout,stderr。

通过 stdio_devices[file] = dev 就可以将dev设成设置控制台的某个设备。这样就实现了控制台任意选择设备的功能。

     这和 linux 的设计思想有点类似。

                                                                  

21            console_setfile (stdout, outputdev); 即stdio_devices[stdout]=outputdev

22            console_setfile (stderr, outputdev); 即stdio_devices[stderr]=outputdev

23        }

24        /* Initializes input console */

25        if (inputdev != NULL) {

26            console_setfile (stdin, inputdev); 即stdio_devices[stdin]=inputdev

27        }                      将控制台的标准输入设备,标准输出设备,标注错误输出设备均设为串口设备

   

   

21~27行, console_setfile做如下几件事:

1. 如果初始化该设备时注册了device_t.start,即启动设备的函数,则运行该函数,开启该设备

2. 将设备的指针存入stdio_devices[file],这应该是标准输入标准输出、标准出错。

      #define stdin         0

      #define stdout        1

      #define stderr        2

22行将标准出错定为输出设备,这样有错误信息就会通过输出设备打印出来了

28        gd->flags |= GD_FLG_DEVINIT;    /* device initialization completed */

29        /* Print information */

30        puts ("In:    ");

31        if (stdio_devices[stdin] == NULL) {

32            puts ("No input devices available!\n");

33        } else {

34            printf ("%s\n", stdio_devices[stdin]->name);

35        }

36        puts ("Out:   ");

37        if (stdio_devices[stdout] == NULL) {

38            puts ("No output devices available!\n");

39        } else {

40            printf ("%s\n", stdio_devices[stdout]->name);

41        }

42        puts ("Err:   ");

43        if (stdio_devices[stderr] == NULL) {

44            puts ("No error devices available!\n");

45        } else {

46            printf ("%s\n", stdio_devices[stderr]->name);

47        }

30~47行,将信息打印出来,这里打印出出的信息就为

In:    serial

Out:   serial

Err:   serial

48        /* Setting environment variables */

49        for (i = 0; i < 3; i++) {

50            setenv (stdio_names[i], stdio_devices[i]->name);// 即stdin=serial

51        }                                                        stdout = serial

49~51行 将信息写到环境变量中去

char *stdio_names[MAX_FILES] = { "stdin", "stdout", "stderr" };

这样环境变量里 stdin stdout stderr 都为serial

    52        return (0);

    53    }

   ---------------------------------------------------------------------------------------------------------------------------------------                                             

#if defined(CONFIG_MISC_INIT_R)

     /* miscellaneous platform dependent initialisations */

     misc_init_r ();     //混杂设备的初始化 很重要

#endif

     /* enable exceptions */

     enable_interrupts ();  //使能中断 即打开cpsr_c的第7位 即外中断,该第7位值为0,表示使能外中断

     /* Perform network card initialisation if necessary */

    

     配置几种类型的网卡

#ifdef CONFIG_DRIVER_TI_EMAC

extern void davinci_eth_set_mac_addr (const u_int8_t *addr);

     if (getenv ("ethaddr")) {

          davinci_eth_set_mac_addr(gd->bd->bi_enetaddr);

     }

#endif

#ifdef CONFIG_DRIVER_CS8900

     cs8900_get_enetaddr (gd->bd->bi_enetaddr);

#endif

#if defined(CONFIG_DRIVER_SMC91111) || defined (CONFIG_DRIVER_LAN91C96)

     if (getenv ("ethaddr")) {

          smc_set_mac_addr(gd->bd->bi_enetaddr);

     }

#endif /* CONFIG_DRIVER_SMC91111 || CONFIG_DRIVER_LAN91C96 */

     /* Initialize from environment */

     if ((s = getenv ("loadaddr")) != NULL) {

          load_addr = simple_strtoul (s, NULL, 16);

     }

#if defined(CONFIG_CMD_NET)

     if ((s = getenv ("bootfile")) != NULL) {

          copy_filename (BootFile, s, sizeof (BootFile));

     }

#endif

#ifdef BOARD_LATE_INIT

     board_late_init ();

#endif

#if defined(CONFIG_CMD_NET)

#if defined(CONFIG_NET_MULTI)

     puts ("Net:   ");

#endif

     eth_initialize(gd->bd);

#if defined(CONFIG_RESET_PHY_R)

     debug ("Reset Ethernet PHY\n");

     reset_phy();

#endif

#endif

/*****************************************非常重要**************************************/

/* main_loop()      can return to retry autoboot, if so just run it again. */

// for(;;)与while(1) 是一样的,for(;;)编译成汇编后是无条件转移,while(1)是要0和1进行一下比较的,

     所以从这个方向上看for(;;)是要比while(1)快的因为少了一个比较指令,但现在的编译器都是有一定的

     优化能力的,像while(1)这种会优化成和for(;;)一样的汇编代码

for (;;) {        

     main_loop ();       

//进入主循环  主循环函数处理执行用户命令 main_loop 是在common/main.c       中定义和实现的

}

/* NOTREACHED - no way out of command loop except booting */

}

void hang (void)

{

     puts ("### ERROR ### Please RESET the board ###\n");

     for (;;);

}

#ifdef CONFIG_MODEM_SUPPORT

static inline void mdm_readline(char *buf, int bufsiz);

/* called from main loop (common/main.c) */

extern void  dbg(const char *fmt, ...);

int mdm_init (void)

{

     char env_str[16];

     char *init_str;

     int i;

     extern char console_buffer[];

     extern void enable_putc(void);

     extern int hwflow_onoff(int);

     enable_putc(); /* enable serial_putc() */

#ifdef CONFIG_HWFLOW

     init_str = getenv("mdm_flow_control");

     if (init_str && (strcmp(init_str, "rts/cts") == 0))

          hwflow_onoff (1);

     else

          hwflow_onoff(-1);

#endif

     for (i = 1;;i++) {

          sprintf(env_str, "mdm_init%d", i);

          if ((init_str = getenv(env_str)) != NULL) {

               serial_puts(init_str);

               serial_puts("\n");

               for(;;) {

                    mdm_readline(console_buffer, CFG_CBSIZE);

                    dbg("ini%d: [%s]", i, console_buffer);

                    if ((strcmp(console_buffer, "OK") == 0) ||

                         (strcmp(console_buffer, "ERROR") == 0)) {

                         dbg("ini%d: cmd done", i);

                         break;

                    } else /* in case we are originating call ... */

                         if (strncmp(console_buffer, "CONNECT", 7) == 0) {

                              dbg("ini%d: connect", i);

                              return 0;

                         }

               }

          } else

               break; /* no init string - stop modem init */

          udelay(100000);

     }

     udelay(100000);

     /* final stage - wait for connect */

     for(;i > 1;) { /* if 'i' > 1 - wait for connection

                      message from modem */

          mdm_readline(console_buffer, CFG_CBSIZE);

          dbg("ini_f: [%s]", console_buffer);

          if (strncmp(console_buffer, "CONNECT", 7) == 0) {

               dbg("ini_f: connected");

               return 0;

          }

     }

     return 0;

}

/* 'inline' - We have to do it fast */

static inline void mdm_readline(char *buf, int bufsiz)

{

     char c;

     char *p;

     int n;

     n = 0;

     p = buf;

     for(;;) {

          c = serial_getc();

          /*          dbg("(%c)", c); */

          switch(c) {

          case '\r':

               break;

          case '\n':

               *p = '\0';

               return;

          default:

               if(n++ > bufsiz) {

                    *p = '\0';

                    return; /* sanity check */

               }

               *p = c;

               p++;

               break;

          }

     }

}

#endif     /* CONFIG_MODEM_SUPPORT */

以下是main_loop函数的分析 该文件在common/main.c中

void main_loop (void)

{

#ifndef CFG_HUSH_PARSER                                        // CFG_HUSH_PARSER没有定义,所以执行

     static char lastcommand[CFG_CBSIZE] = { 0, };  CFG_CBSIZE为256 用于记录console buffer size

     int len;

     int rc = 1;

     int flag;

#endif

#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)  

//  执行  定义指针s和int型bootdelay。用于uboot判断无任何按键按下,就执行CONFIG_BOOTCOMMAND宏所对应的命令,这个命令通常用于加载启动操作系统;

     char *s;

     int bootdelay;

#endif

#ifdef CONFIG_PREBOOT   没定义,不执行

     char *p;

#endif

#ifdef CONFIG_BOOTCOUNT_LIMIT    没定义 不执行

     unsigned long bootcount = 0;

     unsigned long bootlimit = 0;

     char *bcs;

     char bcs_set[16];

#endif /* CONFIG_BOOTCOUNT_LIMIT */

#if defined(CONFIG_VFD) && defined(VFD_TEST_LOGO)  没定义 但是我们可以定义,让显示开机log

     ulong bmp = 0;          /* default bitmap */

     extern int trab_vfd (ulong bitmap);

#ifdef CONFIG_MODEM_SUPPORT             没定义

     if (do_mdm_init)

          bmp = 1;     /* alternate bitmap */

#endif

     trab_vfd (bmp);

#endif     /* CONFIG_VFD && VFD_TEST_LOGO */

#ifdef CONFIG_BOOTCOUNT_LIMIT        没定义 不执行

     bootcount = bootcount_load();

     bootcount++;

     bootcount_store (bootcount);

     sprintf (bcs_set, "%lu", bootcount);

     setenv ("bootcount", bcs_set);

     bcs = getenv ("bootlimit");

     bootlimit = bcs ? simple_strtoul (bcs, NULL, 10) : 0;

#endif /* CONFIG_BOOTCOUNT_LIMIT */

#ifdef CONFIG_MODEM_SUPPORT           暂时不支持通话 没定义 不执行

     debug ("DEBUG: main_loop:   do_mdm_init=%d\n", do_mdm_init);

     if (do_mdm_init) {

          char *str = strdup(getenv("mdm_cmd"));

          setenv ("preboot", str);  /* set or delete definition */

          if (str != NULL)

               free (str);

          mdm_init(); /* wait for modem connection */

     }

#endif  /* CONFIG_MODEM_SUPPORT */

#ifdef CONFIG_VERSION_VARIABLE  没定义 不执行

     {

          extern char version_string[];

          setenv ("ver", version_string);  /* set version variable */ 设置环境变量ver=“”

     }

#endif /* CONFIG_VERSION_VARIABLE */

#ifdef CFG_HUSH_PARSER   没定义 不执行

     u_boot_hush_start ();

#endif

#ifdef CONFIG_AUTO_COMPLETE   该宏非常好用,可以让我们自动补齐命令

     install_auto_complete();

#endif

     #ifdef CONFIG_PREBOOT  没定义 不执行

          if ((p = getenv ("preboot")) != NULL) {

     # ifdef CONFIG_AUTOBOOT_KEYED

               int prev = disable_ctrlc(1);     /* disable Control C checking */

     # endif

    

     # ifndef CFG_HUSH_PARSER

               run_command (p, 0);

     # else

               parse_string_outer(p, FLAG_PARSE_SEMICOLON |

                             FLAG_EXIT_FROM_LOOP);

     # endif

    

     # ifdef CONFIG_AUTOBOOT_KEYED

               disable_ctrlc(prev);     /* restore Control C checking */

     # endif

          }

     #endif /* CONFIG_PREBOOT */ 

#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)

     s = getenv ("bootdelay");      //获取环境变量bootdelay的值 即将环境变量bootdelay等号后面的值的地址保存在s ,即s保存字符串的地址,比如是5,即5此时是字符串

     bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;     //如果环境变量中没有bootdelay参数,则就用默认的CONFIG_BOOTDELAY来当作倒数计时

                         bootdelay保存该环境变量的值

     debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay);

# ifdef CONFIG_BOOT_RETRY_TIME    不执行

     init_cmd_timeout ();

# endif     /* CONFIG_BOOT_RETRY_TIME */

#ifdef CONFIG_POST

     if (gd->flags & GD_FLG_POSTFAIL) {

          s = getenv("failbootcmd");

     }

     else

#endif /* CONFIG_POST */

#ifdef CONFIG_BOOTCOUNT_LIMIT   不执行

     if (bootlimit && (bootcount > bootlimit)) {

          printf ("Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n",

                  (unsigned)bootlimit);

          s = getenv ("altbootcmd");

     }

     else

#endif /* CONFIG_BOOTCOUNT_LIMIT */

*******************************************************************************************************************************************

以下是两种情况

第一种情况是 在bootdelay内不按空格键:     s=getenv ("bootcmd");run_command (s, 0);         直接启动内核了

第二种情况就是 在bootdelay内按下空格键: s = getenv("menucmd");run_command (s, 0);      进入命令循环

  len = readline (CFG_PROMPT);run_command (lastcommand, flag);                         之后在run_command里

  if ((cmdtp = find_cmd(argv[0])) == NULL)--->if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0)即根据命令的名字

  找到该命令表结构体,调用它的cmd参数 最终还是回到do_xxx

比如 run_command("bootm")---->根据“bootm”名字会在命令表里去找到相应的命令表,最终用该命令表的

cmd参数即do_bootm去处理宏U_BOOT_CMD定义了一个段属性为.u_boot_cmd的命令表结构体 struct cmd_tbl_t

          s = getenv ("bootcmd");                      //启动命令 非常重要  bootcmd = nand read  目的地址 源地址; bootm 目的地址

     debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");

     if (bootdelay >= 0 && s && !abortboot (bootdelay)) {  //如果倒数计时之内没有按空格键 则直接

     run_command (s, 0)                                                     直接启动内核了

    

# ifdef CONFIG_AUTOBOOT_KEYED  不执行

          int prev = disable_ctrlc(1);     /* disable Control C checking */

# endif

# ifndef CFG_HUSH_PARSER

          run_command (s, 0);  //直接启动内核了

# else

          parse_string_outer(s, FLAG_PARSE_SEMICOLON |

                        FLAG_EXIT_FROM_LOOP);

# endif

# ifdef CONFIG_AUTOBOOT_KEYED

          disable_ctrlc(prev);     /* restore Control C checking */

# endif

     }

# ifdef CONFIG_MENUKEY

     if (menukey == CONFIG_MENUKEY) {

         s = getenv("menucmd");                                     //否则如果在倒数计时之内按下了空格键,就会跑到这里来

         if (s) {

# ifndef CFG_HUSH_PARSER

          run_command (s, 0);                                         //就会进入到菜单里面 当然你需要自己去实现这个菜单

# else

          parse_string_outer(s, FLAG_PARSE_SEMICOLON |

                        FLAG_EXIT_FROM_LOOP);

# endif

         }

     }

#endif /* CONFIG_MENUKEY */

#endif     /* CONFIG_BOOTDELAY */

#ifdef CONFIG_AMIGAONEG3SE

     {

         extern void video_banner(void);

         video_banner();

     }

#endif

     /*

     * Main Loop for Monitor Command Processing                //进入循环 处理用户输入的命令

     */

#ifdef CFG_HUSH_PARSER

     parse_file_outer();

     /* This point is never reached */

     for (;;);

#else                                            //执行else分支 在这里进入循环来处理各种用户输入的命令

     for (;;) {

#ifdef CONFIG_BOOT_RETRY_TIME

          if (rc >= 0) {

               /* Saw enough of a valid command to

               * restart the timeout.

               */

               reset_cmd_timeout();

          }

#endif

          len = readline (CFG_PROMPT);       //读取串口里用户输入的命令 一回车这些输入的字符串就被考入到console_buffer里 len表示获取的字符串长度

          flag = 0;     /* assume no special flags for now */

          if (len > 0)

               strcpy (lastcommand, console_buffer);                  //将用户输入的命令字符串拷贝到lastcommand里

          else if (len == 0)

               flag |= CMD_FLAG_REPEAT;

#ifdef CONFIG_BOOT_RETRY_TIME

          else if (len == -2) {

               /* -2 means timed out, retry autoboot

               */

               puts ("\nTimed out waiting for command\n");

# ifdef CONFIG_RESET_TO_RETRY

               /* Reinit board to run initialization code again */

               do_reset (NULL, 0, 0, NULL);

# else

               return;          /* retry autoboot */

# endif

          }

#endif

          if (len == -1)

               puts ("<INTERRUPT>\n");

          else

               rc = run_command (lastcommand, flag);                              //解析和处理用户输入的命令

          if (rc <= 0) {

               /* invalid command or not repeatable, forget it */

               lastcommand[0] = 0;

          }

     }

#endif /*CFG_HUSH_PARSER*/

}

       

  

  

  

  

  

  

  

       

----------------------------------------------------------------------------------------------------     

----------------------------------------------------------------------------------------------------      

----------------------------------------------------------------------------------------------------

       main_loop又臭又长,去掉宏注释掉的部分就只剩下一点点了。如下:

void main_loop (void)

{

#ifndef CONFIG_SYS_HUSH_PARSER

    static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, };

    int len;

    int rc = 1;

    int flag;

#endif

#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)

    char *s;

    int bootdelay;

#endif

#ifdef CONFIG_AUTO_COMPLETE

    install_auto_complete();                                    //安装自动补全的函数,分析如下 。

#endif

#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)

    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 : "<UNDEFINED>");

    if (bootdelay >= 0 && s && !abortboot (bootdelay)) {          //如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核。abortboot函数的分析见下面。

        /*  重点1 */

       

        run_command (s, 0);         //运行引导内核的命令。这个命令是在配置头文件中定义的。run_command的分析在下面。

    }

#endif    /* CONFIG_BOOTDELAY */

    for (;;) {                                                                    否则就进入uboot命令行执行各个uboot命令了

        len = readline (CONFIG_SYS_PROMPT);   //CONFIG_SYS_PROMPT的意思是回显字符,一般是“>”。这是由配置头文件定义的 readline读入用户输入的字符串,存放在console_buffer

        flag = 0;    /* assume no special flags for now */

        if (len > 0)

            strcpy (lastcommand, console_buffer); //保存输入的数据。

        else if (len == 0)

            flag |= CMD_FLAG_REPEAT;//如果输入数据为零,则重复执行上次的命令,如果上次输入的是一个命令的话

        if (len == -1)

            puts ("<INTERRUPT>/n");

        else

            rc = run_command (lastcommand, flag); //执行命令 ,重点2。

        if (rc <= 0) {//执行失败,则清空记录

            /* invalid command or not repeatable, forget it */

            lastcommand[0] = 0;

        }

    }

}

2。自动补全

common/common.c

int var_complete(int argc, char *argv[], char last_char, int maxv, char *cmdv[])

{

    static char tmp_buf[512];

    int space;

    space = last_char == '/0' || last_char == ' ' || last_char == '/t';

    if (space && argc == 1)

        return env_complete("", maxv, cmdv, sizeof(tmp_buf), tmp_buf);

    if (!space && argc == 2)

        return env_complete(argv[1], maxv, cmdv, sizeof(tmp_buf), tmp_buf);

    return 0;

}

static void install_auto_complete_handler(const char *cmd,

        int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]))

{

    cmd_tbl_t *cmdtp;

    cmdtp = find_cmd(cmd);

    if (cmdtp == NULL)

        return;

    cmdtp->complete = complete; //命令结构体的complete指针指向传入的函数。

}

void install_auto_complete(void)

{

#if defined(CONFIG_CMD_EDITENV)

    install_auto_complete_handler("editenv", var_complete);

#endif

    install_auto_complete_handler("printenv", var_complete);

    install_auto_complete_handler("setenv", var_complete);

#if defined(CONFIG_CMD_RUN)

    install_auto_complete_handler("run", var_complete);

#endif

}

可以看到将editenv、printenv、setenv和run的自动补全函数安装为 var_complete。

var_complete的功能是根据给出的前缀字符串,找出所有前缀相同的命令。

每个命令在内存中用一个cmd_tbl_t 表示。

include/command.h

struct cmd_tbl_s {

    char        *name;        /* 命令名,输入的就是它            */

    int        maxargs;        /* 最大参数个数    */

    int        repeatable;    /* 允许自动重发,也就是在按下空格键之后执行最后一条命令。        */

                   

    int        (*cmd)(struct cmd_tbl_s *, int, int, char *[]); /* 实现命令的参数 */

    char        *usage;        /* 短的提示信息    */

#ifdef    CONFIG_SYS_LONGHELP

    char        *help;        /* 详细的帮助信息。    */

#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

};

typedef struct cmd_tbl_s    cmd_tbl_t;

extern cmd_tbl_t  __u_boot_cmd_start;

extern cmd_tbl_t  __u_boot_cmd_end;

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) /            U_BOOT_CMD宏定义了一个cmd_tbl_t结构的uboot命令,用find_cmd(argv[0])) 参数是命令的名字,根据名字找到该命令结构cmd_tbl_t

cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}

#define U_BOOT_CMD_MKENT(name,maxargs,rep,cmd,usage,help) /

{#name, maxargs, rep, cmd, usage, help}

uboot中的命令使用U_BOOT_CMD这个宏声明来注册进系统,链接脚本会把所有的cmd_tbl_t结构体放在相邻的地方。

链接脚本中的一些内容如下:

. = .;

__u_boot_cmd_start = .;

.u_boot_cmd : { *(.u_boot_cmd) }

__u_boot_cmd_end = .;

可见,__u_boot_cmd_start 和__u_boot_cmd_end 分别对应命令结构体在内存中开始和结束的地址。

3。abortboot        函数的分析

abortboot是uboot在引导期间的延时函数。期间可以按键进入uboot的命令行。

common/main.c

static __inline__ int abortboot(int bootdelay)   该函数 即在延时时间内,若扫描有键按下即返回1,若无键按下 则返回0

{

    int abort = 0;

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

#if defined CONFIG_ZERO_BOOTDELAY_CHECK   //如果定义了这个宏,即使定义延时为0,也会检查一次是否有按键按下。只要在这里执行之前按键,还是能进入uboot的命令行。

    if (bootdelay >= 0) {

        if (tstc()) {    /* we got a key press    */ 测试是否有按键按下

            (void) getc();  /* consume input    */接收按键值

            puts ("/b/b/b 0");

            abort = 1;    /* don't auto boot    */修改标记,停止自动引导

        }

    }

#endif

    while ((bootdelay > 0) && (!abort)) { //如果延时大于零并且停止标记没有赋值则进入延时循环,直到延时完或者接收到了按 键

        int i;

        --bootdelay;

        /* delay 100 * 10ms */ 每秒中测试按键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();  /* consume input    */获取按键

                break;

            }

            udelay(10000);//延时10000us,也就是10ms

        }

        printf("/b/b/b%2d ", bootdelay);//打印当前剩余时间

    }

    putc('/n');

    return abort;//返回结果:1-停止引导,进入命令行; 0-引导内核。

}

可以看到uboot延时的单位是秒,如果想提高延时的精度,比如想进行10ms级的延时,将udelay(10000)改为udelay(100)就可以了 。

run_command 重点

//

int run_command (const char *cmd, int flag)

{

    cmd_tbl_t *cmdtp;

    char cmdbuf[CONFIG_SYS_CBSIZE];    /* working copy of cmd        */

    char *token;            /* start of token in cmdbuf    */

    char *sep;            /* end of token (separator) in cmdbuf */

    char finaltoken[CONFIG_SYS_CBSIZE];

    char *str = cmdbuf;

    char *argv[CONFIG_SYS_MAXARGS + 1];    /* NULL terminated    */

    int argc, inquotes;

    int repeatable = 1;

    int rc = 0;

    clear_ctrlc();        /* forget any previous Control C */

    if (!cmd || !*cmd) {

        return -1;    /* empty command */  空命令

    }

    if (strlen(cmd) >= CONFIG_SYS_CBSIZE) {  //命令太长

        puts ("## Command too long!/n");

        return -1;

    }

    strcpy (cmdbuf, cmd);            //将命令拷贝到临时命令缓冲cmdbuf

    /* Process separators and check for invalid

     * repeatable commands

     */

    while (*str) {  //str指向cmdbuf

        /*

         * Find separator, or string end

         * Allow simple escape of ';' by writing "/;"

         */

        for (inquotes = 0, sep = str; *sep; sep++) { 

        //寻找分割符或者命令尾部。相邻的句子之间是用;隔开的。每次处理一句命令

            if ((*sep=='/'') &&

                (*(sep-1) != '//'))

                inquotes=!inquotes;

            if (!inquotes &&

                (*sep == ';') &&    /* separator        */

                ( sep != str) &&    /* past string start    */

                (*(sep-1) != '//'))    /* and NOT escaped    */

                break;

        }

        /*

         * Limit the token to data between separators

         */

        token = str;        //token指向命令的开头

        if (*sep) {        //如果是分隔符的话,将分隔符替换为空字符

            str = sep + 1;    /* start of command for next pass */str指向下一句的开头

            *sep = '/0';

        }

        else

            str = sep;    /* no more commands for next pass */如果没有其它命令了,str指向命令尾部

        /* find macros in this token and replace them */

        process_macros (token, finaltoken);      

       //将token指向的命令中的宏替换掉,如把$(kernelsize)替换成内核的大小

        /* Extract arguments */

        if ((argc = parse_line (finaltoken, argv)) == 0) {

     //将每一个参数用‘/0’隔开,argv中的每一个指针指向一个参数的起始地址。 返回值为参数的个数

            rc = -1;    /* no command at all */

            continue;

        }

        /* Look up command in command table */

        if ((cmdtp = find_cmd(argv[0])) == NULL) { 

              //第一个参数就是要运行的命令,首先在命令表中找到它的命令结构体的指针

            printf ("Unknown command '%s' - try 'help'/n", argv[0]);

            rc = -1;    /* give up after bad command */

            continue;

        }

        /* found - check max args */

        if (argc > cmdtp->maxargs) { //检查参数个数是否过多

            cmd_usage(cmdtp);

            rc = -1;

            continue;

        }

        /* OK - call function to do the command */

        if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {      //调用命令执行函数。这是最重要的一步。

            rc = -1;

        }

        repeatable &= cmdtp->repeatable;

        //设置命令自动重复执行的标志。也就是只按下enter键是否可以执行最近执行的命令 .

        /* Did the user stop this? */

        if (had_ctrlc ())  

  //检查是否有ctrl+c按键按下,如果有,结束当前命令。本函数并没有从中断接收字符,接收ctrl+c的是一些执行命令的函数。

            return -1;    /* if stopped then not repeatable */

    }

    return rc ? rc : repeatable;

}

                              ---------------------------------------------------------------------------------------------------

其实 main_loop    函数就是处理两个run_command     函数,一个是在开机延迟时间内若没有按下任意键,则

直接启动内核(bootcmd);一个是若按下了,则不会启动内核,处理uboot命令

uboot最核心的东西就是命令,关于uboot命令的章节笔记本上都有,此处略。

现在来看uboot的最后一部分 启动内核

启动内核 s=getenv ("bootcmd");run_command (s, 0);

环境变量bootcmd = nand read  目的地址 源地址 大小; bootm 目的地址

只要是启动内核,就一定要用到bootcmd,执行这两条指令

nand read指令的实现是在cmd_nand.c文件中的do_nand函数中实现的,do_nand函数中有很多关于nand命令

比如 nand write,nand erase等等

***************************************************************************************************************

我们来看下关于nand相关的指令实现的函数 do_nand

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

{

     int i, dev, ret = 0;

     ulong addr, off;

     size_t size;

     char *cmd, *s;

     nand_info_t *nand;

#ifdef CFG_NAND_QUIET

     int quiet = CFG_NAND_QUIET;

#else

     int quiet = 0;

#endif

     const char *quiet_str = getenv("quiet");

     /* at least two arguments please */

     if (argc < 2)  //r如果参数的个数小于2个 退出,也即参数的个数至少是两个 如nand read

    

          goto usage;

     if (quiet_str)

          quiet = simple_strtoul(quiet_str, NULL, 0) != 0;

     cmd = argv[1]; //获取nand 后面的那一个命令

     if (strcmp(cmd, "info") == 0) {  //命令 nand info

          putc('\n');

          for (i = 0; i < CFG_MAX_NAND_DEVICE; i++) {

               if (nand_info[i].name)

                    printf("Device %d: %s, sector size %u KiB\n",

                           i, nand_info[i].name,

                           nand_info[i].erasesize >> 10);

          }

          return 0;

     }

     if (strcmp(cmd, "device") == 0) { // 命令 nand device

          if (argc < 3) {

               if ((nand_curr_device < 0) ||

                   (nand_curr_device >= CFG_MAX_NAND_DEVICE))

                    puts("\nno devices available\n");

               else

                    printf("\nDevice %d: %s\n", nand_curr_device,

                           nand_info[nand_curr_device].name);

               return 0;

          }

          dev = (int)simple_strtoul(argv[2], NULL, 10);

          if (dev < 0 || dev >= CFG_MAX_NAND_DEVICE || !nand_info[dev].name) {

               puts("No such device\n");

               return 1;

          }

          printf("Device %d: %s", dev, nand_info[dev].name);

          puts("... is now current device\n");

          nand_curr_device = dev;

#ifdef CFG_NAND_SELECT_DEVICE

          /*

          * Select the chip in the board/cpu specific driver

          */

          board_nand_select_device(nand_info[dev].priv, dev);

#endif

          return 0;

     }

     if (strcmp(cmd, "bad") != 0 && strcmp(cmd, "erase") != 0 &&

         strncmp(cmd, "dump", 4) != 0 &&

         strncmp(cmd, "read", 4) != 0 && strncmp(cmd, "write", 5) != 0 &&

         strcmp(cmd, "scrub") != 0 && strcmp(cmd, "markbad") != 0 &&

         strcmp(cmd, "biterr") != 0 &&

         strcmp(cmd, "lock") != 0 && strcmp(cmd, "unlock") != 0 )

          goto usage;

     /* the following commands operate on the current device */

     if (nand_curr_device < 0 || nand_curr_device >= CFG_MAX_NAND_DEVICE ||

         !nand_info[nand_curr_device].name) {

          puts("\nno devices available\n");

          return 1;

     }

     nand = &nand_info[nand_curr_device];

     if (strcmp(cmd, "bad") == 0) {

          printf("\nDevice %d bad blocks:\n", nand_curr_device);

          for (off = 0; off < nand->size; off += nand->erasesize)

               if (nand_block_isbad(nand, off))

                    printf("  %08lx\n", off);

          return 0;

     }

     /*

     * Syntax is:

     *   0    1     2       3    4

     *   nand erase [clean] [off size]

     */

     if (strcmp(cmd, "erase") == 0 || strcmp(cmd, "scrub") == 0) {

          nand_erase_options_t opts;

          /* "clean" at index 2 means request to write cleanmarker */

          int clean = argc > 2 && !strcmp("clean", argv[2]);

          int o = clean ? 3 : 2;

          int scrub = !strcmp(cmd, "scrub");

          printf("\nNAND %s: ", scrub ? "scrub" : "erase");

          /* skip first two or three arguments, look for offset and size */

          if (arg_off_size(argc - o, argv + o, nand, &off, &size) != 0)

               return 1;

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

          opts.offset = off;

          opts.length = size;

          opts.jffs2  = clean;

          opts.quiet  = quiet;

          if (scrub) {

               puts("Warning: "

                    "scrub option will erase all factory set "

                    "bad blocks!\n"

                    "         "

                    "There is no reliable way to recover them.\n"

                    "         "

                    "Use this command only for testing purposes "

                    "if you\n"

                    "         "

                    "are sure of what you are doing!\n"

                    "\nReally scrub this NAND flash? <y/N>\n");

               if (getc() == 'y' && getc() == '\r') {

                    opts.scrub = 1;

               } else {

                    puts("scrub aborted\n");

                    return -1;

               }

          }

          ret = nand_erase_opts(nand, &opts);  //真正解析该命令的函数

          printf("%s\n", ret ? "ERROR" : "OK");

          return ret == 0 ? 0 : 1;

     }

     if (strncmp(cmd, "dump", 4) == 0) {

          if (argc < 3)

               goto usage;

          s = strchr(cmd, '.');

          off = (int)simple_strtoul(argv[2], NULL, 16);

          if (s != NULL && strcmp(s, ".oob") == 0)

               ret = nand_dump(nand, off, 1);

          else

               ret = nand_dump(nand, off, 0);

          return ret == 0 ? 1 : 0;

     }

     if (strncmp(cmd, "read", 4) == 0 || strncmp(cmd, "write", 5) == 0) { 

  //命令是 nand read 和nand write 命令的解析

          int read;

          if (argc < 4)

               goto usage;

          addr = (ulong)simple_strtoul(argv[2], NULL, 16);

          read = strncmp(cmd, "read", 4) == 0; /* 1 = read, 0 = write */

          printf("\nNAND %s: ", read ? "read" : "write");

          if (arg_off_size(argc - 3, argv + 3, nand, &off, &size) != 0)

               return 1;

          s = strchr(cmd, '.');

          if (!s || !strcmp(s, ".jffs2") ||

              !strcmp(s, ".e") || !strcmp(s, ".i")) {

               if (read)

                    ret = nand_read_skip_bad(nand, off, &size,

                                   (u_char *)addr);

               else

                    ret = nand_write_skip_bad(nand, off, &size,

                                     (u_char *)addr);

          } else if (s != NULL && !strcmp(s, ".oob")) {

               /* out-of-band data */

               mtd_oob_ops_t ops = {

                    .oobbuf = (u8 *)addr,

                    .ooblen = size,

                    .mode = MTD_OOB_RAW

               };

               if (read)

                    ret = nand->read_oob(nand, off, &ops);

               else

                    ret = nand->write_oob(nand, off, &ops);

          } else {

               printf("Unknown nand command suffix '%s'.\n", s);

               return 1;

          }

          printf(" %d bytes %s: %s\n", size,

                 read ? "read" : "written", ret ? "ERROR" : "OK");

          return ret == 0 ? 0 : 1;

     }

     if (strcmp(cmd, "markbad") == 0) {

          addr = (ulong)simple_strtoul(argv[2], NULL, 16);

          int ret = nand->block_markbad(nand, addr);

          if (ret == 0) {

               printf("block 0x%08lx successfully marked as bad\n",

                      (ulong) addr);

               return 0;

          } else {

               printf("block 0x%08lx NOT marked as bad! ERROR %d\n",

                      (ulong) addr, ret);

          }

          return 1;

     }

     if (strcmp(cmd, "biterr") == 0) {

          /* todo */

          return 1;

     }

     if (strcmp(cmd, "lock") == 0) {

          int tight  = 0;

          int status = 0;

          if (argc == 3) {

               if (!strcmp("tight", argv[2]))

                    tight = 1;

               if (!strcmp("status", argv[2]))

                    status = 1;

          }

/*

* ! BROKEN !

*

* TODO: must be implemented and tested by someone with HW

*/

#if 0

          if (status) {

               ulong block_start = 0;

               ulong off;

               int last_status = -1;

               struct nand_chip *nand_chip = nand->priv;

               /* check the WP bit */

               nand_chip->cmdfunc (nand, NAND_CMD_STATUS, -1, -1);

               printf("device is %swrite protected\n",

                      (nand_chip->read_byte(nand) & 0x80 ?

                      "NOT " : ""));

               for (off = 0; off < nand->size; off += nand->writesize) {

                    int s = nand_get_lock_status(nand, off);

                    /* print message only if status has changed

                    * or at end of chip

                    */

                    if (off == nand->size - nand->writesize

                        || (s != last_status && off != 0))     {

                         printf("%08lx - %08lx: %8d pages %s%s%s\n",

                                block_start,

                                off-1,

                                (off-block_start)/nand->writesize,

                                ((last_status & NAND_LOCK_STATUS_TIGHT) ? "TIGHT " : ""),

                                ((last_status & NAND_LOCK_STATUS_LOCK) ? "LOCK " : ""),

                                ((last_status & NAND_LOCK_STATUS_UNLOCK) ? "UNLOCK " : ""));

                    }

                    last_status = s;

               }

          } else {

               if (!nand_lock(nand, tight)) {

                    puts("NAND flash successfully locked\n");

               } else {

                    puts("Error locking NAND flash\n");

                    return 1;

               }

          }

#endif

          return 0;

     }

     if (strcmp(cmd, "unlock") == 0) {

          if (arg_off_size(argc - 2, argv + 2, nand, &off, &size) < 0)

               return 1;

/*

* ! BROKEN !

*

* TODO: must be implemented and tested by someone with HW

*/

#if 0

          if (!nand_unlock(nand, off, size)) {

               puts("NAND flash successfully unlocked\n");

          } else {

               puts("Error unlocking NAND flash, "

                    "write and erase will probably fail\n");

               return 1;

          }

#endif

          return 0;

     }

usage:

     printf("Usage:\n%s\n", cmdtp->usage);

     return 1;

}

**********************************************************************************************************************************

bootm命令

bootm命令的实现

/* common/cmd_bootm.c */

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

{

……        ……

/* 检查头部 */

if (crc32 (0, (uchar *)data, len) != checksum) {

          puts ("Bad Header Checksum\n");

          SHOW_BOOT_PROGRESS (-2);

          return 1;

     }

……          ……

/*解压缩*/

     switch (hdr->ih_comp) {

     case IH_COMP_NONE:

          if(ntohl(hdr->ih_load) == addr) {

               printf ("   XIP %s ... ", name);

          } else {

#if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG)

               size_t l = len;

               void *to = (void *)ntohl(hdr->ih_load);

               void *from = (void *)data;

               printf ("   Loading %s ... ", name);

               while (l > 0) {

                    size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l;

                    WATCHDOG_RESET();

                    memmove (to, from, tail);

                    to += tail;

                    from += tail;

                    l -= tail;

               }

#else     /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */

               memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);  //将真正的内核移动到加载地址处hdr->ih_load

#endif     /* CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG */

          }

          break;

     case IH_COMP_GZIP:

          printf ("   Uncompressing %s ... ", name);

          if (gunzip ((void *)ntohl(hdr->ih_load), unc_len,

                   (uchar *)data, &len) != 0) {

               puts ("GUNZIP ERROR - must RESET board to recover\n");

               SHOW_BOOT_PROGRESS (-6);

               do_reset (cmdtp, flag, argc, argv);

          }

          break;

#ifdef CONFIG_BZIP2

     case IH_COMP_BZIP2:

          printf ("   Uncompressing %s ... ", name);

          /*

          * If we've got less than 4 MB of malloc() space,

          * use slower decompression algorithm which requires

          * at most 2300 KB of memory.

          */

          i = BZ2_bzBuffToBuffDecompress ((char*)ntohl(hdr->ih_load),

                              &unc_len, (char *)data, len,

                              CFG_MALLOC_LEN < (4096 * 1024), 0);

          if (i != BZ_OK) {

               printf ("BUNZIP2 ERROR %d - must RESET board to recover\n", i);

               SHOW_BOOT_PROGRESS (-6);

               udelay(100000);

               do_reset (cmdtp, flag, argc, argv);

          }

          break;

#endif /* CONFIG_BZIP2 */

     default:

          if (iflag)

               enable_interrupts();

          printf ("Unimplemented compression type %d\n", hdr->ih_comp);

          SHOW_BOOT_PROGRESS (-7);

          return 1;

     }

}

……           ……             ……

switch (hdr->ih_os) {

     default:               /* handled by (original) Linux case */

     case IH_OS_LINUX:                                     //惊奇!!!!!!!!!!!!!!!!!!!在这里调用了do_bootm_linux函数

         do_bootm_linux  (cmdtp, flag, argc, argv,

                    addr, len_ptr, verify);

         break;

     case IH_OS_NETBSD:

         do_bootm_netbsd (cmdtp, flag, argc, argv,

                    addr, len_ptr, verify);

         break;

     case IH_OS_RTEMS:

         do_bootm_rtems (cmdtp, flag, argc, argv,

                    addr, len_ptr, verify);

         break;

     case IH_OS_VXWORKS:

         do_bootm_vxworks (cmdtp, flag, argc, argv,

                     addr, len_ptr, verify);

         break;

     case IH_OS_QNX:

         do_bootm_qnxelf (cmdtp, flag, argc, argv,

                     addr, len_ptr, verify);

         break;

     }

bootm命令调用do_bootm函数。这个函数专门用来引导各种操作系统映像,可以支持引导Linux、vxWorks、QNX等操作系统。引导Linux的时候,调用do_bootm_linux()  函数。

bootm命令主要做三件事情:

1,解析uImage头部信息,获取image_header结构体

2,将真正的内核移动到加载地址处,如果相等就不用移动

3,根据操作系统,选择启动内核的函数,如do_bootm_linux函数

3.do_bootm_linux函数的实现

/* lib_arm/armlinux.c */

void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],

                   ulong addr, ulong *len_ptr, int verify)

{

theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);

… …                 … …

      /* we assume that the kernel is in place */

      printf ("\nStarting kernel ...\n\n");

… …                 … …

      theKernel (0, bd->bi_arch_number, bd->bi_boot_params);  /*启动内核,传递启动参数*/

}

do_bootm_linux()函数是专门引导Linux映像的函数,它还可以处理ramdisk文件系统的映像。这里引导的内核映像和ramdisk映像,必须是U-Boot格式的。

U-Boot格式的映像可以通过mkimage工具来转换,其中包含了U-Boot可以识别的符号。

以下是bootm.c里的do_bootm_linux的源码

int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images)

{

     bd_t     *bd = gd->bd;

     char     *s;

     int     machid = bd->bi_arch_number;  //机器ID

     void     (*theKernel)(int zero, int arch, uint params);

#ifdef CONFIG_CMDLINE_TAG

     char *commandline = getenv ("bootargs");  //获取启动参数的命令行

#endif

     theKernel = (void (*)(int, int, uint))images->ep; // the_kernel即镜像文件的入口地址

     s = getenv ("machid");

     if (s) {

          machid = simple_strtoul (s, NULL, 16);

          printf ("Using machid 0x%x from environment\n", machid);

     }

     show_boot_progress (15);

     debug ("## Transferring control to Linux (at address %08lx) ...\n",

            (ulong) theKernel);

#if defined (CONFIG_SETUP_MEMORY_TAGS) || \        //以下是设立标签,在启动内核之前,交给内核的参数

    defined (CONFIG_CMDLINE_TAG) || \

    defined (CONFIG_INITRD_TAG) || \

    defined (CONFIG_SERIAL_TAG) || \

    defined (CONFIG_REVISION_TAG) || \

    defined (CONFIG_LCD) || \

    defined (CONFIG_VFD)

     setup_start_tag (bd);

#ifdef CONFIG_SERIAL_TAG

     setup_serial_tag (&params);

#endif

#ifdef CONFIG_REVISION_TAG

     setup_revision_tag (&params);

#endif

#ifdef CONFIG_SETUP_MEMORY_TAGS

     setup_memory_tags (bd);    //设置内存TAG,会用到bd->bi_dram[].start bd->bi_dram[].size

#endif

#ifdef CONFIG_CMDLINE_TAG

     setup_commandline_tag (bd, commandline);

#endif

#ifdef CONFIG_INITRD_TAG

     if (images->rd_start && images->rd_end)

          setup_initrd_tag (bd, images->rd_start, images->rd_end);

#endif

#if defined (CONFIG_VFD) || defined (CONFIG_LCD)

     setup_videolfb_tag ((gd_t *) gd);

#endif

     setup_end_tag (bd);

#endif

     /* we assume that the kernel is in place */

     printf ("\nStarting kernel ...\n\n");

#ifdef CONFIG_USB_DEVICE

     {

          extern void udc_disconnect (void);

          udc_disconnect ();

     }

#endif

     cleanup_before_linux ();

     theKernel (0, machid, bd->bi_boot_params);     //启动内核,一去不复返

     /* does not return */

     return 1;

}

第二阶段小结:                                  bootcmd

主要是3个关键函数 start_armboot ----> main_loop ------> do_bootm_linux

当我们在倒数计时时按下空格键进入uboot命令界面,我们若输入boot命令回车,便会调用bootcmd环境变量的两条指令,同样启动内核

我们按下空格时出现的菜单 是需要自己去实现的,在common里创建一个新文件cmd_menu.c里去实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: u-boot是一个开源的引导加载程序,用于嵌入式系统的启动。它通常嵌入在芯片的ROM或闪存中,是系统的第一个执行程序,负责初始化硬件、加载操作系统和其他应用程序。 u-boot的源码是以C语言编写的,具有高度可移植性。它提供了一系列的驱动程序和命令行工具,可以在开发板上进行硬件初始化和测试。 源码的结构分为几个重要的部分:启动代码、中断向量表、初始化代码以及其他功能模块。启动代码是u-boot执行的入口点,在这个阶段,它会初始化一些必要的硬件设备,例如串口、存储器等,同时也会设置中断向量表。 中断向量表是一个由硬件中断信号触发的函数指针数组,u-boot将中断信号与相应的函数关联起来,以便在发生中断时进行相应的处理。 初始化代码是u-boot执行的核心部分,它会通过配置文件或环境变量来读取系统设置,并进行相应的初始化。例如,它会加载并运行操作系统内核镜像,设置内存映射表,配置设备和网络接口等。 此外,u-boot还提供了一些功能模块,例如命令行解析器、文件系统支持、网络协议栈等。这些功能模块可以通过命令行进行操作,以便用户对嵌入式系统进行配置、调试和测试。 对于研究和分析u-boot源码,可以从以下几个方面入手: 1. 启动流程:了解u-boot是如何从ROM或闪存中加载到内存并执行的,包括启动代码和中断向量表的设置。 2. 硬件初始化:了解u-boot是如何初始化硬件设备的,包括串口、存储器、网络接口等。 3. 配置文件和环境变量:了解u-boot是如何通过配置文件或环境变量来读取系统设置的,以及如何进行相应的初始化。 4. 功能模块:了解u-boot提供的功能模块,例如命令行解析器、文件系统支持、网络协议栈等,以及它们的实现方式和使用方法。 通过对u-boot源码的详细分析,可以深入了解嵌入式系统的引导过程、硬件初始化和驱动程序的编写,从而提高嵌入式系统的开发和调试能力。 ### 回答2: Uboot是一种开源的引导加载程序,用于嵌入式系统的启动。它是一个简单而灵活的软件,可以在各种硬件平台上使用,并提供了许多功能和驱动程序。 首先,Uboot的主要功能是加载和运行操作系统。它通过读取存储介质上的引导扇区,将操作系统加载到内存中并启动。此外,Uboot还提供了命令行界面,用户可以在启动过程中进行配置和控制。 Uboot的源代码由若干模块组成,包括引导代码、设备驱动程序、命令行解析器等。其中,引导代码是最关键的部分,负责在硬件启动时初始化系统和设备,并在引导过程中进行加载和启动操作系统。设备驱动程序用于访问硬件设备,例如存储介质、串口等。命令行解析器则负责解析用户输入的命令,并执行相应的操作。 在Uboot的源代码中,可以找到各种初始化和设置函数,以及与硬件平台相关的代码。这些代码通常是与硬件设备的寄存器交互,进行硬件初始化和配置。此外,还有一些与引导过程和加载操作系统相关的代码,用于读取、解析和加载引导扇区以及操作系统镜像。 总的来说,Uboot的源码详细分析涉及到引导代码、设备驱动程序和命令行解析器等多个模块。在分析过程中,需要理解硬件平台的相关知识和操作系统的启动流程,并深入了解Uboot的代码实现和功能。只有这样,才能对Uboot的源码有一个全面的理解,并能根据需求进行相应的修改和定制。 ### 回答3: U-Boot是一款开源的引导加载程序,用于嵌入式系统中启动操作系统。它是最常用的引导加载程序之一,具有广泛的应用。下面,我将对U-Boot源码进行详细分析。 U-Boot源码位于一个git仓库中,可以通过clone仓库获取源码。源码的结构清晰,主要分为三个部分:板级支持包(board support package,BSP),引导和命令。 BSP包含了与硬件相关的代码和配置文件,用于支持不同的硬件平台。其中,包括设备初始化、设备驱动程序和硬件设置等。这些代码主要包括处理器启动代码、时钟初始化、内存初始化以及设备和外设的配置等。 引导部分是U-Boot的核心,其中包括引导过程的各个阶段。首先,它加载引导扇区和主引导程序,其中包括引导加载器。引导加载器根据设备的启动模式选择适当的引导方式。然后,它会加载内核映像和根文件系统,并将控制权转移到内核。 最后,命令部分包含了一系列的命令,用于与用户进行交互。这些命令可以用于启动操作系统、进行系统设置和调试等。U-Boot提供了丰富的命令集,包括boot、setenv、saveenv、printenv等等。 在分析U-Boot源码时,需要了解硬件平台的特性和配置文件。可以根据目标硬件平台的手册和数据手册,对源码进行逐步分析和调试。在分析过程中,可以使用调试工具进行跟踪、断点和单步调试,以便更好地理解源码的执行过程。 总的来说,U-Boot源码的详细分析需要涉及到硬件平台的特性和配置文件,并对引导加载过程和命令解析进行深入研究。通过对U-Boot源码的理解和分析,可以为嵌入式系统的启动和操作提供更好的支持和定制化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值