嵌入式之uboot的命令体系学习笔记

注:以下内容来自朱老师物联网大讲堂课件

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.参数介绍

  1. aligned ,指定对象的对齐格式(以字节为单位),如 __ attribute__ ((aligned
    (8))),表示8字节对齐方式。
  2. packed,使用该属性对struct 或者union
    类型进行定义,设定其类型的每一个变量的内存约束。就是告诉编译器取消结构在编译过程中的优化对齐(使用1字节对齐),按照实际占用字节数进行对齐,是GCC特有的语法。
  3. at ,绝对定位,可以把变量或函数绝对定位到Flash中,或者定位到RAM。如:
    __ attribute__ ((at(0X20001000)))。
  4. 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)猜测:我们可以在这里面设置各种控制硬件的指令,这样我们就能通过软件来调试硬件了。


版权声明:本文为qq_44034198原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。