简介:
本文主要介绍在u-boot-1.1.6中代码的运行过程,以此来了解在u-boot中如何实现引导并启动内核。这里我们主要介绍u-boot第三阶段的代码。而第三阶段的代码主要讲解的是在u-boot中,我们进入u-boot命令行界面后,如何通过run_command函数来运行各种命令,同时也讲解run_command函数的实现机理。而在本文的末尾我们会以一个menu shell的命令来对run_command函数进行说明。
声明:
本文主要是看了韦东山老师的视频后所写,希望对你有所帮助。
进入u-boot界面:
我想用过u-boot的同学都会有这样的经历,那就是在u-boot启动的几秒内如果你按下空格键(这里的几秒根据各种u-boot设置的不同而有所不同),这时程序就会进入u-boot命令行界面。而在u-boot命令行界面中你可以通过u-boot特定的命令行来对单板进行一些操作,如向nand 或者nor中烧写程序。如果设置了网络相关的代码,你甚至可以使用网络来下载内核或者使用nfs文件系统。而如果你没有按下空格键,那么u-boot将不会进入命令行界面而会直接加载并启动内核。那么在u-boot中实现这个功能的代码是哪个那?
在common\main.c中main_loop函数内有这样的代码:
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
# ifdef CONFIG_AUTOBOOT_KEYED
int prev = disable_ctrlc(1); /* disable Control C checking */
# endif
{
printf("Booting Linux ...\n");
run_command (s, 0);
}
# ifdef CONFIG_AUTOBOOT_KEYED
disable_ctrlc(prev); /* restore Control C checking */
# endif
}
而上面代码的重点为函数:abortboot 。该函数的功能是,如果你在bootdelay秒内按下空格键,那么他会为你跳转到命令行界面,而如果你没有按下空格键,那么u-boot将为你自动加载并启动内核。或者说的更精确一点就是当我们按下按键时abortboot 函数返回1 ,这时程序将不会进入自动启动的分支,这时他会进入到u-boot命令行界面。而如果这时没有按键按下,那么abortboot 函数返回0 ,这时程序进入自动重启分支,u-boot以默认方式启动内核。因此在这里abortboot 函数至关重要。我们看一下这个函数的实现代码:
static __inline__ int abortboot(int bootdelay)
{
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 */
/* consume input */
if (getc() == ' ') {
abort = 1; /* don't auto boot */
bootdelay = 0; /* no more delay */
break;
}
}
udelay (10000);
}
printf ("\b\b\b%2d ", bootdelay);
}
putc ('\n');
return abort;
}
从上面代码中我们很容易看到我们上面所说的流程,这里我结合代码在跟大家讲解一下,进入代码我们首先看到的是用printf打印语句打印出来的提示:
printf("Hit any key to stop autoboot: %2d ", bootdelay);
这里提示我们可以按下任意键来终止自动启动。而后跟着的是一个时间参数bootdelay,也就是说在bootdelay秒后u-boot将自动启动内核。而在这之前,如果你按下任意键将终止自动启动的过程。接下来程序进入while循环来为u-boot倒计时,如果在bootdelay秒内依然没有按键按下这时abort还是初始化时的0 。那么程序返回abort 为0的值,然后u-boot自动启动内核。如果这个时候有按键按下abort值将设置为1 。
这里我们先假设按下了按键,那么程序就不会自动启动内核而是进入了u-boot命令行界面,而u-boot是如何进入命令行界面的那?进入命令行界面的函数是哪个那?这就是我们要弄明白的。我们先讲解程序是如何进入命令行界面的。这里我们看代码会发现有这样一条代码:
run_command("menu", 0);
我们就是通过这条代码进入的命令行界面。而关于这条代码的详细介绍,我们在讲解完run_command函数后再讲解。
run_command 函数:
当程序进入u-boot命令行界面后,我们就可以使用一些简单的命令行来对单板进行控制了。而我们在u-boot中写入的命令行是如何被识别出来的,或者说u-boot是如何知道我们要执行什么样的命令的那?我们看run_command函数的代码来了解上面的问题。
/****************************************************************************
* 返回值:
* 1 - 命令可执行,并可以重复执行
* 0 - 命令可执行,但不可以重复执行,中断命令一般被认为是不可重复执行的
* -1 - 命令不可执行 (命令没有被识别, bootd recursion 或者太多的参数)
* (如果命令为 NULL 或者 "" 再或者 大于 CFG_CBSIZE-1,这将被认定为不可识别命令)
*
* 提醒:
*
* 我们必须临时创建一个命令的副本,因为我们的命令可能是从getenv()函数中获得,而getenv()
* 函数是返回一个指向环境变量数据的指针,但是这个指针在创建或者修改环境变量的时候可能会被
* 不经意的更改 (例如"bootp").
*/
int run_command (const char *cmd, int flag)
{
cmd_tbl_t *cmdtp;
char cmdbuf[CFG_CBSIZE]; /* working copy of cmd */
char *token; /* start of token in cmdbuf */
char *sep; /* end of token (separator) in cmdbuf */
char finaltoken[CFG_CBSIZE];
char *str = cmdbuf;
char *argv[CFG_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) >= CFG_CBSIZE) {
puts ("## Command too long!\n");
return -1;
}
strcpy (cmdbuf, cmd);
/*
* 分离命令,并检测无用的重复命令
*/
while (*str) {
/*
* 找到分离的命令或者到字符串的结尾
* 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;
}
/*
* 将token指向分离的命令
*/
token = str;
if (*sep) {
str = sep + 1; /* start of command for next pass */
*sep = '\0';
}
else
str = sep; /* no more commands for next pass */
/* 在token中寻找宏,并代替 */
process_macros (token, finaltoken);
/* 提取参数 */
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
/* 在命令列表中寻找命令 */
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
/* 在上面找到对应的命令后,检测命令的参数个数 */
if (argc > cmdtp->maxargs) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
/* 上面一切正常,执行命令 */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
repeatable &= cmdtp->repeatable;
/* 用户是否停止命令? */
if (had_ctrlc ())
return 0; /* if stopped then not repeatable */
}
return rc ? rc : repeatable;
}
上面代码中详细的描述了如何处理命令行的参数,程序中将他们分为下面几步:
1. 命令行字符串的检测工作;
2. 找到分离的命令或者到字符串的结尾
3. 将token指向分离的命令
4. 在token中寻找宏,并代替
5. 提取参数
6. 在命令列表中寻找命令
7. 在上面找到对应的命令后,检测命令的参数个数
8. 上面一切正常,执行命令
9. 用户是否停止命令?
下面我们结合代码来分析上面所说的步骤,先说命令行字符串的检测工作,这里我们要做的是,
清除以前的Control + c命令:
clear_ctrlc(); /* forget any previous Control C */
检测命令行字符串是否为空字符串:
if (!cmd || !*cmd) {
return -1; /* empty command */
}
检测命令行字符串长度是否超过控制器缓冲长度:
#define CFG_CBSIZE 256 /* Console I/O Buffer Size */
if (strlen(cmd) >= CFG_CBSIZE) {
puts ("## Command too long!\n");
return -1;
}
拷贝命令行字符串到cmdbuf:
strcpy (cmdbuf, cmd);
做完上面相关的准备工作,那么下面我们就要真正对字符串进行操作了,下面程序就要对命令行字符串进行命令的分离了。我想大家在u-boot中可能用过这样的命令:
nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
或者
tftp 30000000 u-boot.bin;protect off all;erase 0 3ffff;cp.b 30000000 0 40000
上面两条命令行代码中都包含多条命令,但是在u-boot中,这多条命令可以从上面的代码中挑选出来,并可以很好的执行。这就要归功于在run_command函数中进行了很好的命令分离。下面我们以命令行
nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
为例来对上面的过程进行说明:下面要运行的代码就是要对多条命令的分离了,先看代码:
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;
}
上面代码的作用就是从命令行代码中找到第一个';' 。而在';'之前有'\'则忽略这个';' ,即在如果是正则表示的'\;'表示没有分隔能力的分号。同时我们也可以知道在分号之前的内容就是第一个命令了。既然我们已经找到了第一个分号,或者说我们找到了第一条命令的结束位置,那么我们现在要做的是什么那?我们是在继续解析命令行寻找下一个命令,还是先执行这条命令然后再去解析命令行。这就要在看代码:
token = str;
if (*sep) { //str字符串中存在多个命令
str = sep + 1; /* start of command for next pass */
*sep = '\0';
}
else //str字符串中只有一个命令
str = sep; /* no more commands for next pass */
从上面看u-boot选择了后面一种情况,即先执行这条命令,等命令行执行完后然后再去解析命令行。而在上面代码中也分了两种情况,一种是一条命令行代码中存在多个命令(如我们上面所说的),而另一种是str字符串中只有一个命令,即str中没有分号,这时的情况就比较简单了。我们以一条命令行中存在多条命令的情况来分析这段代码,上面代码处理之前命令行为:
而上面的代码处理后的命令行变为:
这时候我们就可以看出,token指向的字符串到'\0'处结束为第一个命令。而后的str指向下一个命令的开始位置。为下一个循环做准备。既然我们已经找到了第一条命令:
nand read.jffs2 0x30007FC0 kernel
下面我们就要详细的分析这条命令了,毕竟这条命令也是有很多分离的参数组成。而我们要做的下一步工作为预处理,就像编译器编译程序一样,要先将程序中那些预处理命令以及宏用其原定义的内容替换。而在这里也是一样的,在命令预处理函数中程序首先要将命令行中的宏定义替换为定义前的内容,这里所使用的函数为:
/* find macros in this token and replace them */
process_macros (token, finaltoken);
上面函数中有两个参数,这两个参数都为字符指针,第一个参数表示没有去宏定义的命令行的首地址,而第二参数表示将原有内容代替了宏定义的命令行的首地址。这里使用了指针实现了值的传递。
而函数的详细代码为:
static void process_macros (const char *input, char *output)
{
char c, prev;
const char *varname_start = NULL;
int inputcnt = strlen (input);
int outputcnt = CFG_CBSIZE;
int state = 0; /* 0 = waiting for '$' */
/* 1 = waiting for '(' or '{' */
/* 2 = waiting for ')' or '}' */
/* 3 = waiting for ''' */
prev = '\0'; /* previous character */
while (inputcnt && outputcnt) {
c = *input++;
inputcnt--;
if (state != 3) {
/* remove one level of escape characters */
if ((c == '\\') && (prev != '\\')) {
if (inputcnt-- == 0)
break;
prev = c;
c = *input++;
}
}
switch (state) {
case 0: /* Waiting for (unescaped) $ */
if ((c == '\'') && (prev != '\\')) {
state = 3;
break;
}
if ((c == '$') && (prev != '\\')) {
state++;
} else {
*(output++) = c;
outputcnt--;
}
break;
case 1: /* Waiting for ( */
if (c == '(' || c == '{') {
state++;
varname_start = input;
} else {
state = 0;
*(output++) = '$';
outputcnt--;
if (outputcnt) {
*(output++) = c;
outputcnt--;
}
}
break;
case 2: /* Waiting for ) */
if (c == ')' || c == '}') {
int i;
char envname[CFG_CBSIZE], *envval;
int envcnt = input - varname_start - 1; /* Varname # of chars */
/* Get the varname */
for (i = 0; i < envcnt; i++) {
envname[i] = varname_start[i];
}
envname[i] = 0;
/* Get its value */
envval = getenv (envname);
/* Copy into the line if it exists */
if (envval != NULL)
while ((*envval) && outputcnt) {
*(output++) = *(envval++);
outputcnt--;
}
/* Look for another '$' */
state = 0;
}
break;
case 3: /* Waiting for ' */
if ((c == '\'') && (prev != '\\')) {
state = 0;
} else {
*(output++) = c;
outputcnt--;
}
break;
}
prev = c;
}
if (outputcnt)
*output = 0;
}
上面的程序使用状态机的思想来实现宏定义查找替换的功能,而这个思想在程序设计中是非常有效,同时也是非常强大的。而这里我们还是以一个例子来为大家说明上面的代码。这里我需要说明的一点是,上面所说的宏并不是我们在C语言中所使用的宏,而是在脚本中使用的宏,他的形式为:$(XXXX)或者${XXXX},这里的XXXX为宏定义的定义名。这上面的函数就是在命令行中寻找是否存在上面这种形式的宏。我们以下面的命令行为例来对上面的程序进行说明:
file := arch/arm/boot
nand read.jffs2 0x30007FC0 $(file)/kernel
在这里我们先定义一个宏file,他所代表的内容为:arch/arm/boot 这个文件目录。而上面的程序就是要将这个$(file)还原为arch/arm/boot 。我们现在按着上面的程序一步一步的来分析。
我们先看下面介绍状态机的图:
现在我们结合上面的图以及命令行来讲解,首先程序获得命令行的首字母,然后他进入状态0,这个时候他就要查看一下这个字母是否为'$',显然我们命令行的首字母为字符'n'不是dollar符($符),那么程序依然在状态0并将这个字母写入到输出字符串中。然后程序进行下一轮循环,以这种方式程序依次遍历命令行中的字符并查找所遍历的字符是否为$符,与此同时命令行的内容也不断的写入到输出字符串中。直到我们循环到命令行中的宏命令$(file)时,这时字符变量c的值为'$',此时符合进入状态1的条件,因此程序进入状态1。
而在下一轮的循环中字符变量c被赋予了新的值'(' 。而我们在这个状态中要检测的也正好是:c是否为'(' 或者 '{',所以此时符合进入状态2的条件。在状态1中我们需要注意的是:varname_start = input; 上面程序的作用就是替换宏,而当我们找到了宏变量的时候我们就要用一个字符指针变量来指向这个位置,来标识出这个位置就是宏定义开始的位置,以此来为程序在后面做宏替换做铺垫。上面的情况是新的一轮循环中c获得了'('的值,而如果程序在其他的情况下c的值不为'('时,那么程序的状态将会跳回到状态0,然后重新检查。
当程序进入状态2后,他要检测的就是:c是否为')' 或者 '}'。如果是,那么在两个括号之间的内容就是宏定义的名称,如果不是,那么程序继续循环来找到这个后括号。在上面的命令行中宏定义的名称为file,所以我们上面的代码会先在状态2中运行到')',这时找到‘)’后程序就要截取从这个位置到状态1中宏定义开始的位置的内容,并查找这个宏定义了,最后将这个宏定义的原有内容写入到输出字符串中。当完成上面的操作后状态机进入状态0开始继续循环遍历命令行来检测是否还存在宏定义。而在状态2中最主要的是查找和输出到输出字符串中的代码,他们的代码为:
/* Get its value */
envval = getenv (envname);
/* Copy into the line if it exists */
if (envval != NULL)
while ((*envval) && outputcnt) {
*(output++) = *(envval++);
outputcnt--;
}
/* Look for another '$' */
state = 0;
而上面的getenv 函数就是查找宏定义,并将原有内容的首地址返回的函数。
讲到这里关于使用状态机来替换宏定义的操作就完成了。做完预处理之后程序接下来要做的就是从处理好的命令行中提取各个参数了,因为只有分离为单个的参数时程序才可以用于比较已有的命令从而找到该命令的操作函数,而如果是一个命令行的话将很难与已有的数据进行比较。而提取参数其实就是解析命令行并从中找出空格符或制表符来分离各个参数,其中分离参数的具体代码为:
/* Extract arguments */
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
上面的两个参数分别为命令行字符串和解析的参数,第一个参数为字符指针,而第二个参数的类型为字符指针数组。而该函数的返回值为分离参数的个数。写到这里我想说明一下为什么要使用字符指针数组来存放提取的各个参数。这是因为程序要将一个长的字符串分解为多个分离的字符串,并且还要很方便的访问到各个分离的字符串中的内容,这就要使用指针数组了。因为这个可以更方便的去访问各个参数的内容。
而解析命令行的代码为:
int parse_line (char *line, char *argv[])
{
int nargs = 0;
while (nargs < CFG_MAXARGS) {
/* skip any white space */
while ((*line == ' ') || (*line == '\t')) {
++line;
}
if (*line == '\0') { /* end of line, no more args */
argv[nargs] = NULL;
return (nargs);
}
argv[nargs++] = line; /* begin of argument string */
/* find end of string */
while (*line && (*line != ' ') && (*line != '\t')) {
++line;
}
if (*line == '\0') { /* end of line, no more args */
argv[nargs] = NULL;
return (nargs);
}
*line++ = '\0'; /* terminate current arg */
}
printf ("** Too many args (max. %d) **\n", CFG_MAXARGS);
return (nargs);
}
从上面的代码看,程序在提取参数前要先对传入的命令行做一些简单的处理:跳过传入命令行中的空格符和制表符。而如果完成上面的操作之后命令行的内容变为'\0',此时表明我们传入的命令行为空。此时程序将不会做任何的处理而直接返回。如果此时命令行不为空,那么程序就要开始提取参数了。程序首先会将argv的第一个字符指针指向命令行的首地址,然后程序对命令行向下遍历来寻找空格符或者制表符,当程序找到空格符或者制表符后在此处写入'\0',然后程序将argv的第二个字符指针指向当前写入'\0'的下一个地址处。然后依次类推直到你查到命令行的结尾,即找到命令行的'\0'。在这个过程中你就将命令行中的各个参数提取到了argv中。而当你每提取一个参数的时候argc就会加一来记录参数的个数。
下面我们用一个图来说明上面的处理过程:
提取参数前的命令行和argv:
提取参数后的命令行和argv:
解析好了参数后就轮到了这里的重点了,程序前面所做的这些铺垫就是为了这里的这步函数,那就是在命令列表中查询在上面解析出来的命令,如果可以找到那么万事大吉,程序直接运行命令的操作函数。但是如果没有找到就只能说明我们在上面所解析出来的命令为无效的命令,更确切的说:你所输入的命令为是u-boot所不支持的命令。当然了在u-boot中你可以自己设置一个命令。但这是后话了,我们只说现在代码的分析。那么我们看命令查询函数的代码为:
/* 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;
}
在这里程序通过find_cmd函数来查找在命令行列表中是否存在我们所输入的命令,如果存在那么find_cmd函数将返回一个cmd_tbl_t的结构体,而在这个结构体中存放着与命令操作相关的参数和函数。find_cmd函数的参数为上面参数解析中的第一个参数即argv[0]。
而find_cmd函数的代码为:
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value */
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
*/
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) { /* exactly one match */
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
看完上面的代码大家可能会有两个疑问:这个cmd_tbl_s 的结构体到底是什么样的或者说他都有什么内容?而第二个要问的问题是,命令去哪里寻找或者说命令放在那里?
我们一个一个回答上面问题,首先先来介绍一下cmd_tbl_s 结构体,这个结构体是用于存放命令的结构体,他中描述了一个结构体应该有的一些性质。下面我们看这个结构体中都有什么:
struct cmd_tbl_s {
char *name; /* 命令的名称 */
int maxargs; /* 最大参数个数 */
int repeatable; /*自动可重复 */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* 短的使用信息 */
#ifdef CFG_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
};
上面这些参数就是一个命令应该有的参数,或者也可以说是一个命令区别于其他命令的地方。而在上面的命令查找中就是用命令名称(name)这个参数从命令列表中查找对应的命令以及该命令所有的特性。
回答完第一个问题,现在我们来回答第二个问题,命令在那里?我在上面说了命令在命令列表中,但是我想大家对这个答案一定很不满意。确实我也对这个答案不满意。既然大家对这个答案都不满意,那么我们就应该用代码来说话,从代码中找到我们满意的答案。我们看这部分代码:
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
从上面的代码我们知道命令列表是从__u_boot_cmd_start到__u_boot_cmd_end,在这之间存放着各个命令。而这两个变量是在哪里定义的那?我们搜索代码发现在u-boot的连接文件中有这两个变量:
.u_boot_cmd : {
__u_boot_cmd_start = .;
*(.u_boot_cmd)
__u_boot_cmd_end = .;
}
uboot_end_data = .;
从上面我们知道了命令行存放在那里,他与我们在清bss段时说的定位区间相似。讲到这里我想大家又要问了:我们知道了命令存放在哪里,但是我们是如何将定义的命令存放到这个区间那?
这里我们要介绍一下u-boot命令的定义方式:命令宏U_BOOT_CMD
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
#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}
在上面我们可以看到命令宏U_BOOT_CMD的定义,而从这里也可以看到__u_boot_cmd_##name(其中##为连字符),他就表示了命令应该在命令行中以何种方式存放,而宏Struct_Section 指定了我们的命令行代码放在哪个位置。以及如果定义一个命令要输入哪些参数。
好了回答完上面的两个问题我们对命令的结构和命令的查找就有了一定的了解。下面我们在分析命令查找的代码就容易多了。其实就是从命令列表中取出命令的结构体cmd_tbl_t ,并用该结构体中的参数name与我们要查找的命令的名称相比较,如果查到就返回这个命令的结构体。这里需要我们注意的一点是:(如程序中注释所说的)有些命令允许加对该命令加以修饰,即在命令名后加'.'再加修饰,如cp.b命令就是在cp命令后加'.b'来对cp命令进行修饰,来表示复制一个字节的数据。而程序从命令行中查找着这个命令的时候要先忽略cp.d中的'.d',而只查找cp命令。所以程序在查找命令前要对待查找的命令进行处理,即找到待查找命令中的符号'.'所在的位置。
而我们看后面的代码会发现,如果在命令列表中找到了对应的命令也不能高兴的太早了,因为在后面的代码中还有对这个命令参数个数的检测,如果参数的个数超出了u-boot设置的阈值,那么程序一样会报错,而命令一样不会被执行。因此只有我们的参数个数合理了才能进行下一步——运行命令。而检测参数个数是否超出阈值的代码为:
/* found - check max args */
if (argc > cmdtp->maxargs) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
上面的cmdtp就是在上面程序中找到的命令的结构体,在这个结构体中有一项参数就是最大参数个数,而每个命令有其自己的最大参数个数。
在上面检测完参数后,这一步程序才要真正的开始运行命令,他的代码为:
/* OK - call function to do the command */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
上面的函数调用使用了回调函数来完成。而这些回调函数在定义命令时就已经写好了,在这里直接使用。而我们从上面的回调函数中可以看出这些函数的格式是一样的。这之后命令行的函数就运行了。
上面的解释只讲了命令行运行的机制,但是我们并不能对其有更加深入的了解,因此我们在这里用一个例子来对上面的这个过程进行说明。同时我们也可以接着开始时关于按与不按空格键所有的不同反应来进行说明。我们知道当u-boot启动过程中,如果我们不按空格键,那么u-boot会自动加载并启动内核。而如果我们按下空格键那么将会打断自动加载和启动这个过程而进入到u-boot的命令行模式。这时候你就可以通过向命令行中写命令来控制单板了。这里我们直接用run_command函数来运行一段命令。
我们要运行的命令为:run_command("menu", 0);
而我们在代码中查menu会找到
当我们进入该搜索结果得:
U_BOOT_CMD(
menu, 3, 0, do_menu,
"menu - display a menu, to select the items to do something\n",
" - display a menu, to select the items to do something"
);
从上面对命令宏U_BOOT_CMD的分析可以知道:这个命令的名称为menu ,这个命令的最大参数个数为3,而这个命令是可重复的,同时也知道这个命令的操作函数为do_menu函数。而下面的两段字符串分别为短的帮助信息和长的帮助信息。所以如果我们想更好的理解这个命令的操作细节,我们就要看他的操作函数的代码了。do_menu函数代码为:
int do_menu (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
menu_shell();
return 0;
}
上面的函数主要是调用menu_shell函数,而menu_shell函数的代码为:
void menu_shell(void)
{
char c;
char cmd_buf[200];
char *p = NULL;
unsigned long size;
unsigned long offset;
struct mtd_info *mtd = &nand_info[nand_curr_device];
while (1)
{
main_menu_usage();
c = awaitkey(-1, NULL);
printf("%c\n", c);
switch (c)
{
···············
case 'b':
{
printf("Booting Linux ...\n");
strcpy(cmd_buf, "nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0");
run_command(cmd_buf, 0);
break;
}
case 'f':
{
···········
}
case 'r':
{
strcpy(cmd_buf, "reset");
run_command(cmd_buf, 0);
break;
}
case 'q':
{
return;
break;
}
}
}
}
上面的代码其实很简单,我先说一下他的主要意思,首先程序会打印这个菜单列表的使用方法,即你可以在键盘上输入一个单独的字符来代替一长串命令行的字符,例如你可以输入字符'b'就表示boot内核,他等价于在run_command函数中输入"nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0" 。当程序打印完用法之后他就要等待一个串口的输入字符,如果没有字符输入那么他将一直等待。而当有字符输入时,他会根据switch语句找到这个输入字符对应的命令并执行该命令,如果没有找到则直接返回。该函数的思路就是这样,我们现在来看一下代码,看是否跟我说的一样。
程序先打印菜单列表的使用方法,他是用main_menu_usage函数来完成这个操作的,而main_menu_usage函数就为我们列出了各个字符代表的命令行,我们通过看这个函数就可以简单的操作u-boot的命令行,而不需要自己去敲那些命令行代码,而main_menu_usage函数的内容为:
void main_menu_usage(void)
{
printf("\r\n##### 100ask Bootloader for OpenJTAG #####\r\n");
printf("[n] Download u-boot to Nand Flash\r\n");
if (bBootFrmNORFlash())
printf("[o] Download u-boot to Nor Flash\r\n");
printf("[k] Download Linux kernel uImage\r\n");
printf("[j] Download root_jffs2 image\r\n");
// printf("[c] Download root_cramfs image\r\n");
printf("[y] Download root_yaffs image\r\n");
printf("[d] Download to SDRAM & Run\r\n");
printf("[z] Download zImage into RAM\r\n");
printf("[g] Boot linux from RAM\r\n");
printf("[f] Format the Nand Flash\r\n");
printf("[s] Set the boot parameters\r\n");
printf("[b] Boot the system\r\n");
printf("[r] Reboot u-boot\r\n");
printf("[q] Quit from menu\r\n");
printf("Enter your selection: ");
}
同时我们使用awaitkey函数来从串口获得一个字符,他的函数为:
char awaitkey(unsigned long delay, int* error_p)
{
int i;
char c;
if (delay == -1) {
while (1) {
if (tstc()) /* we got a key press */
return getc();
}
}
else {
for (i = 0; i < delay; i++) {
if (tstc()) /* we got a key press */
return getc();
udelay (10*1000);
}
}
if (error_p)
*error_p = -1;
return 0;
}
获得字符之后程序就要通过switch语句来寻找该字符对应的命令行了。这里我删除了一些代码以方便你可以更清晰的了解这个过程,这里我只留下了三个经典的case,他们分别是boot内核,重启u-boot,以及没有找到对应的字符。而其他的命令与他们相似,大家可以举一反三。
讲到这里第三阶段就讲完了,本来想在本文中将boot内核的内容讲完,但是写到这里发现自己已经写了太多的东西,如果继续往下写读者可能都要疲倦了,毕竟这不是小说,会有一定的烧脑。所以将boot内核的内容放到了第四阶段来完成,希望我的文章对大家有益。