注:以下内容来自朱老师物联网大讲堂课件
1.uboot命令体系基础
1.1 使用uboot命令
uboot启动后进入命令行环境下,在此输入命令按回车结束,uboot会收取这个命令然后解析,然后执行。
1.2 uboot命令体系实现代码在哪里
uboot命令体系的实现代码在uboot/common/cmd_xxx.c中。有若干个.c文件和命令体系有关。(还有command.c main.c也是和命令有关的)
1.3 每个命令对应一个函数
(1)每一个uboot的命令背后都对应一个函数。这就是uboot实现命令体系的一种思路和方法。这个东西和我们在裸机第十六部分shell中实现shell命令的方法是一样的。
(2)我们要找到每一个命令背后所对应的那个函数,而且要分析这个函数和这个命令是怎样对应起来的。
(3)一般情况命令的函数名就是对应命令前面加上do_,如:do_help
1.4 命令参数以argc&argv传给函数
(1)有些uboot的命令还支持传递参数。也就是说命令背后对应的函数接收的参数列表中有argc和argv,例如我们之前讲到过的bootm指令
举例分析,以help命令为例:
help命令背后对应的函数名叫:do_help。在uboot/common/command.c的236行。
int do_help (cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])
2.uboot命令解析和执行过程分析
2.1 从main_loop说起
(1)uboot启动的第二阶段,在初始化了所有该初始化的东西后,进入了一个死循环,死循环的循环体就是main_loop。
(2)main_loop函数执行一遍,就是一个boot倒计时、获取命令、解析命令、执行命令的过程。
main_loop函数中bootdelay的流程梳理
第一步:定义bootdelay 变量
第二步:判断环境变量中是否有设置bootdelay ,没有则设置默认值
第三步:判断bootdelay 是否大于0,getenv是否读取正常,abortboot 函数的作用是倒计时控制
abortboot函数解析
static __inline__ int abortboot(int bootdelay)
{
int abort = 0;
//如果有菜单则输出菜单
#ifdef CONFIG_MENUPROMPT
printf(CONFIG_MENUPROMPT);
#else
//我们这里执行的是else这部分,也就是我们启动时显示倒计时
printf("Hit any key to stop autoboot: %2d ", bootdelay);
#endif
#if defined CONFIG_ZERO_BOOTDELAY_CHECK
/*
* Check if key already pressed
* Don't check if bootdelay < 0
*/
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循环实现的作用就是我们启动时倒计时和回车破坏自动启动
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 */
# ifdef CONFIG_MENUKEY
menukey = getc();
# else
(void) getc(); /* consume input */
# endif
break;
}
udelay(10000);
}
printf("\b\b\b%2d ", bootdelay);
}
putc('\n');
#ifdef CONFIG_SILENT_CONSOLE
if (abort)
gd->flags &= ~GD_FLG_SILENT;
#endif
return abort;
}
2.1.1 知识点:#ifdef和#if defined的差别
ifdef 只能在两者中选择是否有定义
#ifdef XXX
....
#else
....
#endif
if defined可以在多个中选择是否有定义
#if defined xxx1
....
#elif defined xxx2
....
#elif defined xxx3
....
#endif
2.1.2 知识点:哈希表
2.2 run_command函数
2.2.1 cmd_tbl_s 类型说明
首先我们看到了定义了cmd_tbl_t 类型的指针*cmdtp,如下程序为结构体原型
分析可以知道这个结构体包含了指令的一般信息:指令名、最大传参个数、是否可以自动repeat、指针的函数指针、短说明、长说明、自动补全
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
};
typedef struct cmd_tbl_s cmd_tbl_t;
(1)name:命令名称,字符串格式。
(2)maxargs:命令最多可以接收多少个参数
(3)repeatable:指示这个命令是否可重复执行。重复执行是uboot命令行的一种工作机制,就是直接按回车则执行上一条执行的命令。
(4)cmd:函数指针,命令对应的函数的函数指针,将来执行这个命令的函数时使用这个函数指针来调用。
(5)usage:命令的短帮助信息。对命令的简单描述。
(6)help:命令的长帮助信息。细节的帮助信息。
(7)complete:函数指针,指向这个命令的自动补全的函数。
总结:uboot的命令体系在工作时,一个命令对应一个cmd_tbl_t结构体的一个实例,然后uboot支持多少个命令,就需要多少个结构体实例。uboot的命令体系把这些结构体实例管理起来,当用户输入了一个命令时,uboot会去这些结构体实例中查找(查找方法和存储管理的方法有关)。如果找到则执行命令,如果未找到则提示命令未知。
2.2.2 run_command函数实现过程
关键部分
(1)控制台命令获取
(2)取出变量的值通过process_macros 函数
(3)指令解析。parse_line函数把"md 30000000 10"解析成argv[0]=md, argv[1]=30000000 argv[2]=10;
(4)找出指令argv[0],通过find_cmd函数
(5)跳转至函数指针执行执行
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;
//忽略Control C退出
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;
}
//将cmd复制到定义的数组中
strcpy (cmdbuf, cmd);
/* Process separators and check for invalid
* repeatable commands
*/
//通过数组指针访问存放指令的数组
while (*str) {
/*
* Find separator, or string end
* Allow simple escape of ';' by writing "\;"
*/
//判断单引号是否是转义字符表示',如果不是则对单引号内的任何符号原样输出
for (inquotes = 0, sep = str; *sep; sep++) {
if ((*sep=='\'') &&
(*(sep-1) != '\\'))
inquotes=!inquotes;
//通过查找;分离单个指令到token
//inquotes=0表示上一个提取完成,并查找到了分隔符;
//str的首字符不等于;
//该字符不是转义字符
if (!inquotes &&
(*sep == ';') && /* separator */
( sep != str) && /* past string start */
(*(sep-1) != '\\')) /* and NOT escaped */
break;
}
/*
* Limit the token to data between separators
*/
token = str;
//判断是否提取结束
//没结束则str指针指向分隔符后下一个指令
//例如:setenv;printenv sep指向;号时,让str指向printenv
//sep自身解引用赋值为'\0',表示上一个指令结束
if (*sep) {
str = sep + 1; /* start of command for next pass */
*sep = '\0';
}
else
str = sep; /* no more commands for next pass */
/* find macros in this token and replace them */
//替换转义字符,如何含有转义字符,则通过该函数进行对应的替换
//遇到'单引号,说明是一对单引号包含的内容,遇到\'则表示单个字符直接输出'
//去除\
//遇到$符号后面未接括号(和{,就原样输出$;
//如果$后面接括号就将里面的变量的值显示出来,类似与宏替换。
process_macros (token, finaltoken);
//parse_line函数判断字符串finaltoken之间的空格来判断中有几个参数并存放在argv数组中
/* Extract arguments */
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
//通过查找自定义cmd段,找出对应的指令
/* 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) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
#if defined(CONFIG_CMD_BOOTD)
/* avoid "bootd" recursion */
if (cmdtp->cmd == do_bootd) {
if (flag & CMD_FLAG_BOOTD) {
puts ("'bootd' recursion detected\n");
rc = -1;
continue;
} else {
flag |= CMD_FLAG_BOOTD;
}
}
#endif
//跳转至该指令的函数指针处执行
/* OK - call function to do the command */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
//是否支持重复
repeatable &= cmdtp->repeatable;
/* Did the user stop this? */
if (had_ctrlc ())
return -1; /* if stopped then not repeatable */
}
return rc ? rc : repeatable;
}
2.2.2.1 parse_line函数
//函数传参:line表示传进来的展开的字符串,argv表示需要输出的字符串数组,
//一个元素表示一个参数
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);
}
2.2.2.2 find_cmd函数
命令集中查找命令。find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
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);
//在我们的链接脚本中,我们有自定义__u_boot_cmd_start到__u_boot_cmd_end的
//地址空间用来存放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 */
}
2.2.2.3 执行函数
执行命令。最后用函数指针的方式调用执行了对应函数。
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
/* OK - call function to do the command */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
思考:关键点就在于find_cmd函数如何查找到这个命令是不是uboot的合法支持的命令?这取决于uboot的命令体系机制(uboot是如何完成命令的这一套设计的,命令如何去注册、存储、管理、索引。)。
3. uboot如何处理命令集
3.1 可能的管理方式
(1)数组。结构体数组,数组中每一个结构体成员就是一个命令的所有信息。
(2)链表。链表的每个节点data段就是一个命令结构体,所有的命令都放在一条链表上。这样就解决了数组方式的不灵活。坏处是需要额外的内存开销,然后各种算法(遍历、插入、删除等)需要一定复杂度的代码执行。
(3)有第三种吗?uboot没有使用数组或者链表,而是使用了一种新的方式来实现这个功能。
3.2 uboot实现命令管理的思路
(1)填充1个结构体实例构成一个命令
(2)给命令结构体实例附加特定段属性(用户自定义段),链接时将带有该段属性的内容链接在一起排列(挨着的,不会夹杂其他东西,也不会丢掉一个带有这种段属性的,但是顺序是乱序的)。
(3)uboot重定位时将该段整体加载到DDR中。加载到DDR中的uboot镜像中带有特定段属性的这一段其实就是命令结构体的集合,有点像一个命令结构体数组。
(4)段起始地址和结束地址(链接地址、定义在u-boot.lds中)决定了这些命令集的开始和结束地址。
4. uboot命令定义具体实现分析
U_BOOT_CMD宏基本分析
我们使用version 指令来举例
4.1 知识点1:宏定义和函数
在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为宏定义
宏定义函数:
优点:预处理阶段进行简单的替换,执行效率明显高于普通函数,因此,简短并且被频繁调用的函数经常用宏定义函数来代替实现
缺点:
1、没有参数检查,会影响程序安全
2、如果函数比较复杂,函数体规模比较大,使用宏定义函数就会增加程序的大小
3、宏定义函数的调用有可能改变函数的原生语义,比如涉及到运算符优先级的函数时,调用宏定义函数可能会改变函数的原生语义,所以使用时要格外小心
普通函数:
具有参数检查,压栈,出栈,参数传递等工作,程序更加安全,但是执行效率会低于宏定义函数
函数体只会存在一个,每次调用都会转向函数体的位置执行函数功能,适合复杂函数的定义
我们这里的U_BOOT_CMD 就是使用的宏定义函数
4.2 知识点2:#和##
##连接符号,用在带参数的宏定义中将两个子串在编译时候联接起来,组成一个新的字串。但是不可以把##符放在字串最前面或最后面。
#符是把传过来的参数当成字符串进行替代,其中的参数都不能是变量
4.3 __attribute __
1.常规介绍:
__ attribute __ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute ) 和类型属性(Type Attribute )
__ attribute __ 书写特征是:__ attribute__ 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。
__ attribute__ 语法格式为:attribute ((attribute-list))
2.参数介绍
- aligned ,指定对象的对齐格式(以字节为单位),如 __ attribute__ ((aligned
(8))),表示8字节对齐方式。 - packed,使用该属性对struct 或者union
类型进行定义,设定其类型的每一个变量的内存约束。就是告诉编译器取消结构在编译过程中的优化对齐(使用1字节对齐),按照实际占用字节数进行对齐,是GCC特有的语法。 - at ,绝对定位,可以把变量或函数绝对定位到Flash中,或者定位到RAM。如:
__ attribute__ ((at(0X20001000)))。 - section ,提到section,就得说RO RI ZI了,在ARM编译器编译之后,代码被划分为不同的段,RO Section(ReadOnly)中存放代码段和常量,RW Section(ReadWrite)中存放可读写静态变量和全局变量,ZI Section(ZeroInit)是存放在RW段中初始化为0的变量。
__ attribute__ ((section(“section_name”))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。
如上 __attribute __ 的资料摘自----->C语言__attribute__的使用
所以我们如下的就很清楚了,附加了用户自定义段属性,以保证链接时将这些数据结构链接在一起排布。
我们以version为例,我们得到了如下的代码,但这个代码里面有个Struct_Section
是如下宏
链接脚本中的自定义段
cmd_tbl_t __u_boot_cmd_version Struct_Section = {"version", 1, 1, do_version, "version - print monitor version\n", NULL}
//Struct_Section 宏定义替换后
cmd_tbl_t __u_boot_cmd_version __attribute__ ((unused,section (".u_boot_cmd"))) = {"version", 1, 1, do_version, "version - print monitor version\n", NULL}
5. 实践练习:uboot中增加自定义命令
5.1 在已有的c文件中直接添加命令
(1)在uboot/common/command.c中添加一个命令,叫:donke
(2)在已有的.c文件中添加命令比较简单,直接使用U_BOOT_CMD宏即可添加命令,给命令提供一个do_xxx的对应的函数这个命令就可以了。
(3)添加完成后要重新编译工程(make distclean; make x210_sd_config; make),然后烧录新的uboot去运行即可体验新命令。
(4)还可以在函数中使用argc和argv来验证传参。
5.2 自建一个c文件并添加命令
(1)在uboot/common目录下新建一个命令文件,叫cmd_donke.c(对应的命令名就叫mycmd,对应的函数就叫do_mycmd函数),然后在c文件中添加命令对应的U_BOOT_CMD宏和函数。注意头文件包含不要漏掉。
(2)在uboot/common/Makefile中添加上cmd_donke.o,目的是让Make在编译时能否把cmd_donke.c编译链接进去。
(3)重新编译烧录。重新编译步骤是:make distclean; make x210_sd_config; make
体会:uboot命令体系的优点
(1)uboot的命令体系本身稍微复杂,但是他写好之后就不用动了。我们后面在移植uboot时也不会去动uboot的命令体系。我们最多就是向uboot中去添加命令,就像本节课所做的这样。
(2)向uboot中添加命令非常简单。
(3)猜测:我们可以在这里面设置各种控制硬件的指令,这样我们就能通过软件来调试硬件了。