本篇讲点大家最常用的命令行和驱动,干货满满,赶紧收藏阅读吧。
1. 命令行
先回顾下之前的启动过程:
- ENTRY(_start)(arch/arm/lib/vectors.S)
- vectors(arch/arm/cpu/armv8/exceptions.S)
- reset(arch/arm/cpu/armv8/start.S)
- lowlevel_init(arch/arm/cpu/armv8/start.S)
- _main(arch/arm/lib/crt0_64.S)
- board_init_f_alloc_reserve(common/init/board_init.c)
- board_init_f(common/board_f.c)
- c_runtime_cpu_setup(arch/arm/cpu/armv8/start.S)
- board_init_r(common/board_r.c)
- run_main_loop(common/board_r.c)
- main_loop(common/main.c)
board_init_f是uboot重定位前的流程,它包括一些基础模块的初始化和重定位相关的准备工作。以下为该函数在armv8架构下可能的执行流程,图中虚线框表示该流程是可配置的,实线框表示是必选的。
board_init_r是uboot重定位后需要执行的流程,它包含基础模块、硬件驱动以及板级特性等的初始化,并最终通过run_main_loop启动os会进入命令行窗口。
1.1 main_loop到进入命令行
board_init_r里面执行函数的数组最后一个元素就是run_main_loop,然后调用main_loop,这里开始已经全是C语言函数了。这里我们从命令行打印开始看下,搜索:Hit any key to stop autoboot 可以找到abortboot_single_key()函数,调用顺序如下:
main_loop
autoboot_command
abortboot
abortboot_single_key
main_loop的定义如下
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop"); //打印出启动进度
if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
env_set("ver", version_string); /* 设置版本号环境变量 */
cli_init();
if (IS_ENABLED(CONFIG_USE_PREBOOT))
run_preboot_environment_command();
if (IS_ENABLED(CONFIG_UPDATE_TFTP))
update_tftp(0UL, NULL, NULL);
if (IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK_EARLY)) {
/* efi_init_early() already called */
if (efi_init_obj_list() == EFI_SUCCESS)
efi_launch_capsules();
}
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s);
cli_loop();
panic("No CLI available");
}
autoboot_command(s);的入参是从bootdelay_process获取的
void autoboot_command(const char *s)
{
debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
这里遇到一个debug打印,我们知道printf是可以打印的,这个debug怎么能让打印呢? 在
/* Show a message if DEBUG is defined in a file */
#define debug(fmt, args...) \
debug_cond(_DEBUG, fmt, ##args)
#ifdef DEBUG
#define _DEBUG 1
#else
#define _DEBUG 0
#endif
这里我们不使用DEBUG版本,可以直接修改:debug_cond(true, fmt, ##args)之后编译执行如下:
make u-boot && make -f qemu_v8.mk run-only
可见这个bootcmd是一串命令,在autoboot_command()函数中执行如下:
autoboot_command
abortboot //手动输入中断执行命令
run_command_list //如果没手动输入命令则执行变量s对应的命令,启动linux
1.2 进入命令行
abortboot()函数检测是否有手动输入,就进入命令行,这里有个倒计时: CONFIG_BOOTDELAY中定义
include/generated/autoconf.h
#define CONFIG_BOOTDELAY 10
这个是生成的,由config文件里面定义
configs/qemu_arm64_defconfig中
CONFIG_BOOTDELAY=10
如果手动输入了命令,则autoboot_command()执行完返回,继续执行cli_loop();
cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行各种操作就是有 cli_loop 来处理的,此函数定义在文件 common/cli.c 中
void cli_loop(void)
{
bootstage_mark(BOOTSTAGE_ID_ENTER_CLI_LOOP);
#ifdef CONFIG_HUSH_PARSER
parse_file_outer();
/* This point is never reached */
for (;;);
#elif defined(CONFIG_CMDLINE)
cli_simple_loop();
#else
printf("## U-Boot command line is disabled. Please enable CONFIG_CMDLINE\n");
#endif /*CONFIG_HUSH_PARSER*/
}
parse_file_outer调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命 令行输入,然后解析并执行相应的命令,函数 parse_stream_outer 定义在文件 common/cli_hush.c中
static int parse_stream_outer(struct in_str *inp, int flag)
{
do { //循环处理输入命令
initialize_context(&ctx);
update_ifs_map();
rcode = parse_stream(&temp, &ctx, inp, //命令解析
flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n');
if (rcode != 1 && ctx.old_flag == 0) {
done_word(&temp, &ctx);
done_pipe(&ctx,PIPE_SEQ);
run_list(ctx.list_head); //执行解析出来的命令
code = run_list(ctx.list_head);
}
run_list就不再分析了,里面的处理逻辑代码还是挺好。最后通过调用cmd_process函数来处理命令。
enum command_ret_t cmd_process(int flag, int argc, char *const argv[],
int *repeatable, ulong *ticks)
{
/* 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) {
int newrep;
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv, &newrep);
if (ticks)
*ticks = get_timer(*ticks);
*repeatable &= newrep;
}
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
return rc;
}
主要就是find_cmd和cmd_call,
find_cmd--》
ll_entry_start--》__u_boot_list_2_"#_list"_1"
find_cmd_tbl
cmd_call--》
result = cmdtp->cmd_rep(cmdtp, flag, argc, argv, repeatable);
可见其从section段找到一个结构体,然后执行里面的回调函数:
struct cmd_tbl {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
/*
* Same as ->cmd() except the command
* tells us if it can be repeated.
* Replaces the old ->repeatable field
* which was not able to make
* repeatable property different for
* the main command and sub-commands.
*/
int (*cmd_rep)(struct cmd_tbl *cmd, int flags, int argc,
char *const argv[], int *repeatable);
/* Implementation function */
int (*cmd)(struct cmd_tbl *cmd, int flags, int argc,
char *const argv[]);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
const 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
};
那么这个section里面怎么定义命令行呢?答案就是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)
//最后一个参数设置成 NULL 就 是 U_BOOT_CMD
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
ll_entry_declare(struct cmd_tbl, _name, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp);
//数据类型声明
#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)
//数据定义
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp) \
{ #_name, _maxargs, \
_rep ? cmd_always_repeatable : cmd_never_repeatable, \
_cmd, _usage, _CMD_HELP(_help) _CMD_COMPLETE(_comp) }
我们以version命令为例,在cmd/version.c中
U_BOOT_CMD(
version, 1, 1, do_version,
"print monitor, compiler and linker version",
""
);
可见命令行相关的代码都是cmd目录下。设 置 变 量 _u_boot_list_2_cmd_2_version 存储 在.u_boot_list_2_cmd_2_version 段中。 u-boot.lds 链接脚本中有一个名为“.u_boot_list”的段,所有.u_boot_list 开头的段都存放到.u_boot.list 中
. = ALIGN(8);
__u_boot_list : {
KEEP(*(SORT(__u_boot_list*)));
}
1.3 添加或打开命令行
首先uboot支持了很多命令,但是不是默认就打开的,例如smc命令,在cmd/smccc.c中
#ifdef CONFIG_CMD_SMC
U_BOOT_CMD(
smc, 9, 2, do_call,
"Issue a Secure Monitor Call",
"<fid> [arg1 ... arg6] [id]\n"
" - fid Function ID\n"
" - arg SMC arguments, passed to X1-X6 (default to zero)\n"
" - id Secure OS ID / Session ID, passed to W7 (defaults to zero)\n"
);
#endif
CONFIG_CMD_SMC需要在configs/qemu_arm64_defconfig中定义就可以了。那么我们想自己添加一个命令行:
- configs/qemu_arm64_defconfig添加宏控
CONFIG_CMD_HELLO=y
- cmd/Kconfig里面添加config定义
config CMD_HELLO bool "hello" help hello support.
- cmd/Makefile中包含上这个c函数的.o文件
obj-$(CONFIG_CMD_HELLO) += hello.o
- cmd目录下新建一个hello.c里面利用U_BOOT_CMD注册命令和实现命令执行函数
```
include
int do_hello(int argc, char const *argv[])
{
printf("thatway1989 HelloWorld\n");
return 0;
}
U_BOOT_CMD(
hello, 1, 1, do_hello,
"hello -just for test uboot command",
"hello -hello help.................."
)

# 2. 驱动管理

为了方便对硬件和驱动的管理,uboot还引入了类似linux内核的设备树和驱动模型特性。
## 2.1 设备树
调试过linux驱动的都清楚,linux驱动的配置文件和开关都是dts设备树里面,可以参考之前的文章:XXX
>设备树是一种通过dts文件来描述SOC属性,通过将设备的具体配置信息与驱动分离,以达到利用一份代码适配多款设备的机制。dts文件包含了一系列层次化结构的节点和属性,它可以通过dtc编译器编译成适合设备解析的二进制dtb文件。uboot设备树的使用包含以下流程:为目标板添加dts文件、选择一个运行时使用的dtb文件、使能设备树。
我们使用的代码,设备树在arch/arm/dts/qemu-arm64.dts中定义,现在没有用到所以是空的。
configs/qemu_arm64_defconfig里面可以选择一个默认的dts文件:
CONFIG_DEFAULT_DEVICE_TREE="qemu-arm64"
uboot最终的镜像会和dtb打包在一个镜像文件中,因此在编译流程中就需要知道最终被使用的dtb。有时在编译时希望使用一个不是默认指定的dts,则可以通过在编译命令中添加DEVICE_TREE=zzz方式指定新的dts文件
.config中我们可以看到:
#
Device Tree Control
# CONFIG_OF_CONTROL=y CONFIG_OF_REAL=y
CONFIG_OF_LIVE is not set
CONFIG_OF_SEPARATE=y
CONFIG_OF_EMBED is not set
CONFIG_OF_BOARD=y CONFIG_OF_HAS_PRIOR_STAGE=y CONFIG_OF_OMIT_DTB=y CONFIG_DEVICE_TREE_INCLUDES="" CONFIG_OF_LIST="qemu-arm64"
CONFIG_MULTI_DTB_FIT is not set
CONFIG_OF_TAG_MIGRATE=y
CONFIG_OF_DTB_PROPS_REMOVE is not set
通过配置CONFIG_OF_CONTROL选项即可使能设备树的支持
uboot与dtb可以有以下几种打包组合方式:
1. 若定义了CONFIG_OF_EMBED选项,则在链接时会为dtb指定一个以__dtb_dt_begin开头的单独的段,dtb的内容将被直接链接到uboot.bin镜像中。官方建议这种方式只在开发和调试阶段使用,而不要用于生产阶段
2. 若定义了CONFIG_OF_SEPARATE选项,dtb将会被编译为u-boot.dtb文件,而uboot原始镜像被编译为u-boot-nodtb.bin文件,并通过以下命令将它们连接为最终的uboot.bin文件:
cat u-boot-nodtb.bin u-boot.dtb >uboot.bin
## 2.2 驱动模块
Uboot驱动模型与linux的设备模型比较类似,利用它可以将设备与驱动分离。对上可以为同一类设备提供统一的操作接口,对下可以为驱动提供标准的注册接口,从而提高代码的可重用性和可移植性。同时,驱动模型通过树形结构组织uboot中的所有设备,为系统对设备的统一管理提供了方便。
driver结构体用于表示一个驱动,在include/dm/device.h中定义:
struct driver { char name; enum uclass_id id; const struct udevice_id of_match; int (bind)(struct udevice dev); int (probe)(struct udevice dev); int (remove)(struct udevice dev); int (unbind)(struct udevice dev); int (of_to_plat)(struct udevice dev); int (child_post_bind)(struct udevice dev); int (child_pre_probe)(struct udevice dev); int (child_post_remove)(struct udevice dev); int priv_auto; int plat_auto; int per_child_auto; int per_child_plat_auto; const void ops; / driver-specific operations */ uint32_t flags;
if CONFIG_IS_ENABLED(ACPIGEN)
struct acpi_ops *acpi_ops;
endif
};
例如rk3399的dmc驱动,drivers/ram/rockchip/sdram_rk3399.c
static const struct udevice_id rk3399_dmc_ids[] = { { .compatible = "rockchip,rk3399-dmc" }, { } };
U_BOOT_DRIVER(dmc_rk3399) = { .name = "rockchip_rk3399_dmc", .id = UCLASS_RAM, .of_match = rk3399_dmc_ids, .ops = &rk3399_dmc_ops,
if defined(CONFIG_TPL_BUILD) || \
(!defined(CONFIG_TPL) && defined(CONFIG_SPL_BUILD))
.of_to_plat = rk3399_dmc_of_to_plat,
endif
.probe = rk3399_dmc_probe,
.priv_auto = sizeof(struct dram_info),
if defined(CONFIG_TPL_BUILD) || \
(!defined(CONFIG_TPL) && defined(CONFIG_SPL_BUILD))
.plat_auto = sizeof(struct rockchip_dmc_plat),
endif
};
U_BOOT_DRIVER宏就是声明驱动的,通过.of_match中的.compatible来找到dts中的配置项,另外驱动加载的时候会执行.probe函数,驱动对外提供了.ops操作函数
U_BOOT_DRIVER宏的定义如下:
define U_BOOT_DRIVER(__name) \
ll_entry_declare(struct driver, __name, driver)
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)
可见跟上面命令行的定义是一致的,都使用u-boot.lds 链接脚本中有一个名为“.u_boot_list”的段。.u_boot_list_2类型section在内存中的布局图:

对于class类型的驱动,使用宏UCLASS_DRIVER。uclass用于表示一类具有相同功能的设备,从而可以为其抽象出统一的设备访问接口,方便其它模块对它的调用。
define UCLASS_DRIVER(__name) \
ll_entry_declare(struct uclass_driver, __name, uclass_driver)
struct uclass_driver { const char name; enum uclass_id id; int (post_bind)(struct udevice dev); int (pre_unbind)(struct udevice dev); int (pre_probe)(struct udevice dev); int (post_probe)(struct udevice dev); int (pre_remove)(struct udevice dev); int (child_post_bind)(struct udevice dev); int (child_pre_probe)(struct udevice dev); int (child_post_probe)(struct udevice dev); int (init)(struct uclass class); int (destroy)(struct uclass *class); int priv_auto; int per_device_auto; int per_device_plat_auto; int per_child_auto; int per_child_plat_auto; uint32_t flags; };
每个udevice都属于一个uclass,使用宏UCLASS_DRIVER定义。所有的udevice结构体可以通过parent、child_head和sibling_node连接在一起,并且最终挂到gd的dm_root节点上,这样我们就可以通过gd->dm_root遍历所有的udevice设备。下图是udevice的连接关系,其中每个节点的parent指向其父节点,sibling指向其兄弟节点,而child指向子节点。

在uboot中实际的设备可以通过以下两种方式定义:
1. devicetree方式:这种方式通过devicetree维护设备信息,uboot在驱动模型初始化时,通过解析设备树获取设备信息,并完成其与驱动等的绑定
2. 硬编码方式:这种方式可通过下面的宏定义一个设备:
define U_BOOT_DRVINFO(__name) \
ll_entry_declare(struct driver_info, __name, driver_info)
```
2.3 驱动初始化
驱动模型初始化主要完成udevice、driver以及ucalss等之间的绑定关系,其主要包含以下部分:
- udevice与driver的绑定
- udevice与uclass的绑定
- uclass与uclass_driver的绑定
该流程通过dm_init_and_scan函数实现,它会分别扫描由U_BOOT_DRVINFO以及devicetree定义的设备,为它们分配udevice结构体,并完成其与driver和uclass之间的绑定关系等操作。需要注意的是该函数在board_init_f和board_init_r中都会被调用,其中board_init_f主要是为了解析重定位前需要使用的设备节点,这种类型节点在devicetree中会增加u-boot,dm-pre-reloc属性。
后记:
uboot和linux整体上套路有点像,还有其他的一些OS,例如make menuconfig使用的Kconfig和configs/qemu_arm64_defconfig,还有目录定义,以及设备树、命令行等机制。估计是一个机制比较好大家都抄着用,也对应技术使用者来说用的越多接触一个新系统越容易上手,对于新手或许是调试打印全局查找大法,但是老手直接看目录或许都能找到源码在哪里,而这算是内功,多看多用就更有手感。
读书和思考: 最近看《认知觉醒》,分享给大家一些(1):
- 人脑有三层:本能脑、情绪脑、理智脑。生活中做的大部分决策往往源于本能和情绪,而非理智。为了生存,思考🤔锻炼💃🏻这种耗能行为,会被本能脑排斥,形成目光短浅、即时满足、避难趋易、急于求成。
- 缺乏耐心和寻求舒适是人的天性,需要用元认知来审视监督自己,然后利用理智脑延迟满足来跟本能脑和情绪脑和谐相处。选择正确的方向,遵循刻意练习的原则,在舒适区边缘一点点拓展自己的能力范围。
- 利用潜意识去抓取内心的想法和做出一些选择,审视自己的第一反应。学习的时候可以不用面面俱到,但是之后让自己想下那些东西有用让自己印象深刻,然后针对这点去思考拓展实践,读万卷书不如行千里路。
- 无法把自己在注意力投入到重要的事情上,像刷手机视频、打游戏、刷剧和对有兴趣可以即时满足的小事投入过多的时间等,就是元认知出问题了。你能意识到自己在想什么,进而意识到这些想法是否明智,再进一步纠正那些不明智的想法,最终做出更好的选择。一句话:多反思。