Linux系统移植:U-Boot 启动流程(下)

Posted 嵌入式up笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux系统移植:U-Boot 启动流程(下)相关的知识,希望对你有一定的参考价值。

目录

Linux系统移植:U-Boot 启动流程(下)

一、run_main_loop 函数详解

uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核,这段过程就是由 run_main_loop 函数实现,函数代码如下:

static int run_main_loop(void)

#ifdef CONFIG_SANDBOX
	sandbox_main_loop_init();
#endif
	/* main_loop() can return to retry autoboot, if so just run it again */
	for (;;)
		main_loop();
	return 0;

函数执行在 main_loop() 死循环,函数代码如下:

/* We come here after U-Boot is initialised and ready to process commands */
void main_loop(void)

	const char *s;

	bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

#ifndef CONFIG_SYS_GENERIC_BOARD
	puts("Warning: Your board does not use generic board. Please read\\n");
	puts("doc/README.generic-board and take action. Boards not\\n");
	puts("upgraded by the late 2014 may break or be removed.\\n");
#endif

#ifdef CONFIG_VERSION_VARIABLE
	setenv("ver", version_string);  /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */

	cli_init();

	run_preboot_environment_command();

#if defined(CONFIG_UPDATE_TFTP)
	update_tftp(0UL, NULL, NULL);
#endif /* CONFIG_UPDATE_TFTP */

	s = bootdelay_process();
	if (cli_process_fdt(&s))
		cli_secure_boot_cmd(s);

	autoboot_command(s);

	cli_loop();

其中 bootstage_mark_name 函数,打印出启动进度

定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,设置换将变量 ver 的值为 version_string,也就是设置版本号环境变量

cli_init 函数,初始化 shell 相关的变量

run_preboot_environment_command 函数,获取环境变量 perboot 的内容,perboot是一些预启动命令,一般不使用这个环境变量

bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容,然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值

如果 定义了 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 就会实现,如果没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在本 uboot 中没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false

autoboot_command 函数,此函数就是检查倒计时是否结束,如果倒计时自然结束那么就执行函数
run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动如果倒计时结束之前按下按键,那么就会执行 cli_loop 函数,这个就是命令处理函数,负责接收好处理输入的命令,执行对应的指令

二、cli_loop 函数详解

cli_loop 函数是 uboot 的命令行处理函数,在 uboot 中输入各种命令就是由 cli_loop 来处理的,函数原型如下:

void cli_loop(void)

#ifdef CONFIG_SYS_HUSH_PARSER
	parse_file_outer();
	/* This point is never reached */
	for (;;);
#else
	cli_simple_loop();
#endif /*CONFIG_SYS_HUSH_PARSER*/

定义宏 CONFIG_SYS_HUSH_PARSER,后面调用函数 parse_file_outer,后面是个死循环,不会执行到

parse_file_outer 函数如下:

#ifndef __U_BOOT__
static int parse_file_outer(FILE *f)
#else
int parse_file_outer(void)
#endif

	int rcode;
	struct in_str input;
#ifndef __U_BOOT__
	setup_file_in_str(&input, f);
#else
	setup_file_in_str(&input);
#endif
	rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
	return rcode;

先调用函数 setup_file_in_str 初始化变量 input 的成员变量,之后调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令

函数 parse_stream_outer 中使用 do-while 循环就是处理输入命令,在循环中调用函数 parse_stream 进行命令解析之后调用 run_list 函数来执行解析出来的命令而函数 run_list 会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令

三、 cmd_process 函数详解

cmd_process 函数用来处理命令行的命令,uboot 使用宏 U_BOOT_CMD 来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,

#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help)		\\
	U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)

宏 U_BOOT_CMD_COMPLETE 如下

#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \\
	ll_entry_declare(cmd_tbl_t, _name, cmd) =			\\
		U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd,	\\
						_usage, _help, _comp);

ll_entry_declar 定义在文件 include/linker_lists.h 中,定义如下

#define ll_entry_declare(_type, _name, _list)				\\
	_type _u_boot_list_2_##_list##_2_##_name __aligned(4)		\\
			__attribute__((unused,				\\
			section(".u_boot_list_2_"#_list"_2_"#_name)))

_type 为 cmd_tbl_t 结构体,ll_entry_declare 就是定义了一个 cmd_tbl_t 变量,这里用到了 C 语言中的 “##” 连接符。其中的 “##_list” 表示用 _list 的值来替换,“##_name” 就是用 _name 的值来替换

宏 U_BOOT_CMD_MKENT_COMPLETE 定义在文件 include/command.h 中,内容如下:

#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \\
								  _usage, _help, _comp) \\
	 #_name, _maxargs, _rep, _cmd, _usage, \\
	  _CMD_HELP(_help) _CMD_COMPLETE(_comp) 

上面代码中的 # 就是将 _name 传递过来的值字符串化,U_BOOT_CMD_MKENT_COMPLETE 又用到了_CMD_HELP 和 _CMD_COMPLETE,这两个宏的定义如下

#ifdef CONFIG_AUTO_COMPLETE
# define _CMD_COMPLETE(x) x,
#else
# define _CMD_COMPLETE(x)
#endif
#ifdef CONFIG_SYS_LONGHELP
# define _CMD_HELP(x) x,
#else
# define _CMD_HELP(x)
#endif

_CMD_COMPLETE 和 _CMD_HELP 就 是 取 自 身 的 值 , 然 后 在 加 上 一 个 ‘ , ‘,

以一个具体的命令为例,来看一下 U_BOOT_CMD 经过展开以后究竟是个什么模样的,比如 dhcp 命令:

U_BOOT_CMD(
    dhcp, 3, 1, do_dhcp,
    "boot image via network using DHCP/TFTP protocol",
    "[loadAddress] [[hostIPaddr:]bootfilename]"
);

将其每一步运行函数都展开

U_BOOT_CMD(
    dhcp, 3, 1, do_dhcp,
    "boot image via network using DHCP/TFTP protocol",
    "[loadAddress] [[hostIPaddr:]bootfilename]"
);
//1、将 U_BOOT_CMD 展开后为:
U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp,
    "boot image via network using DHCP/TFTP protocol",
    "[loadAddress] [[hostIPaddr:]bootfilename]",
    NULL)
//2、将 U_BOOT_CMD_COMPLETE 展开后为:
ll_entry_declare(cmd_tbl_t, dhcp, cmd) = \\
U_BOOT_CMD_MKENT_COMPLETE(dhcp, 3, 1, do_dhcp, \\
    "boot image via network using DHCP/TFTP protocol", \\
    "[loadAddress] [[hostIPaddr:]bootfilename]", \\
    NULL);
//3、将 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE 展开后为:
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \\
__attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \\
  "dhcp", 3, 1, do_dhcp, \\
    "boot image via network using DHCP/TFTP protocol", \\
    "[loadAddress] [[hostIPaddr:]bootfilename]",\\
    NULL

dhcp 命令最终展开结果:

cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \\
    __attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \\
     "dhcp", 3, 1, do_dhcp, \\
    "boot image via network using DHCP/TFTP protocol", \\
    "[loadAddress] [[hostIPaddr:]bootfilename]",\\
    NULL

先定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,此变量 4字节对齐

使用 _attribute_ 关键字设置变量 _u_boot_list_2_cmd_2_dhcp 存储在 .u_boot_list_2_cmd_2_dhcp 段中

u-boot.lds 链接脚本中有一个名为 “.u_boot_list” 的段,所有 .u_boot_list 开头的段都存放到 .u_boot.list 中

cmd_tbl_t 是个结构体

     "dhcp", 3, 1, do_dhcp, \\
    "boot image via network using DHCP/TFTP protocol", \\
    "[loadAddress] [[hostIPaddr:]bootfilename]",\\
    NULL

就是初始化这结构体,cmd_tbl_t 结构体定义在文件 include/command.h 中:

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 * const []);
	char		*usage;		/* Usage message	(short)	*/
#ifdef	CONFIG_SYS_LONGHELP
	char		*help;		/* Help  message	(long)	*/
#endif
#ifdef CONFIG_AUTO_COMPLETE
	/* do auto completion on the arguments */
	int		(*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
#endif
;

typedef struct cmd_tbl_s	cmd_tbl_t;

上面的代码初始化后,结构体的数值如下:

_u_boot_list_2_cmd_2_dhcp.name = "dhcp"
_u_boot_list_2_cmd_2_dhcp.maxargs = 3
_u_boot_list_2_cmd_2_dhcp.repeatable = 1
_u_boot_list_2_cmd_2_dhcp.cmd = do_dhcp
_u_boot_list_2_cmd_2_dhcp.usage = "boot image via network using DHCP/TFTP protocol"
_u_boot_list_2_cmd_2_dhcp.help = "[loadAddress] [[hostIPaddr:]bootfilename]"
_u_boot_list_2_cmd_2_dhcp.complete = NULL

在 uboot 的命令行中输入“dhcp”这个命令的时候,最终经过一系列处理后执行的就是 do_dhcp 这个函数

总结:uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个 cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。uboot 中的每个命令都存储在.u_boot_list段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数

下面看一下 cmd_process 具体代码:

enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
			       int *repeatable, ulong *ticks)

	enum command_ret_t rc = CMD_RET_SUCCESS;
	cmd_tbl_t *cmdtp;

	/* Look up command in command table */
	cmdtp = find_cmd(argv[0]);
	if (cmdtp == NULL) 
		printf("Unknown command '%s' - try 'help'\\n", argv[0]);
		return 1;
	

	/* found - check max args */
	if (argc > cmdtp->maxargs)
		rc = CMD_RET_USAGE;

#if defined(CONFIG_CMD_BOOTD)
	/* avoid "bootd" recursion */
	else if (cmdtp->cmd == do_bootd) 
		if (flag & CMD_FLAG_BOOTD) 
			puts("'bootd' recursion detected\\n");
			rc = CMD_RET_FAILURE;
		 else 
			flag |= CMD_FLAG_BOOTD;
		
	
#endif

	/* If OK so far, then do the command */
	if (!rc) 
		if (ticks)
			*ticks = get_timer(0);
		rc = cmd_call(cmdtp, flag, argc, argv);
		if (ticks)
			*ticks = get_timer(*ticks);
		*repeatable &= cmdtp->repeatable;
	
	if (rc == CMD_RET_USAGE)
		rc = cmd_usage(cmdtp);
	return rc;

先调用函数 find_cmd 在命令表中找到指定的命令,函数原型如下:

cmd_tbl_t *find_cmd(const char *cmd)

	cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
	const int len = ll_entry_count(cmd_tbl_t, cmd);
	return find_cmd_tbl(cmd, start, len);

传入 cmd 命令,然后通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址,然后通过函数 ll_entry_count 得到数组长度,也就是命令表的长度,最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令

最后调用函数 cmd_call 来执行具体的命令,执行指令函数如下:

static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])

	int result;

	result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
	if (result)
		debug("Command failed, result=%d\\n", result);
	return result;

所以具体的调用流程就是处理字符串,创建对应的 cmd_tbl_t 存储单元,初始化 cmd_tbl_t 变量,然后 cmd_process 中会查找检测 cmd_tbl 的返回值,然后调用 cmd_call 函数执行指令,如果返回值为 CMD_RET_USAGE 的话就会调用 cmd_usage 函数输出命令的用法,其实就是输出 cmd_tbl_t 的 usage 成员变量

以上就是 U-Boot 流程分析的最后一部分!

以上是关于Linux系统移植:U-Boot 启动流程(下)的主要内容,如果未能解决你的问题,请参考以下文章

Linux系统移植:U-Boot 启动流程(上)

Linux系统移植:U-Boot 启动流程(中)

Linux系统移植:U-Boot 启动流程(中)

Linux系统移植:正点原子 U-Boot 移植

Linux系统移植:U-Boot常用指令(下)

Linux系统移植:U-Boot 顶层 Makefile 分析(下)