image.png 本篇讲点大家最常用的命令行和驱动,干货满满,赶紧收藏阅读吧。

1. 命令行

image.png

先回顾下之前的启动过程:

  1. ENTRY(_start)(arch/arm/lib/vectors.S
  2. vectors(arch/arm/cpu/armv8/exceptions.S
  3. reset(arch/arm/cpu/armv8/start.S
  4. lowlevel_init(arch/arm/cpu/armv8/start.S
  5. _main(arch/arm/lib/crt0_64.S
  6. board_init_f_alloc_reserve(common/init/board_init.c)
  7. board_init_f(common/board_f.c)
  8. c_runtime_cpu_setup(arch/arm/cpu/armv8/start.S
  9. board_init_r(common/board_r.c)
  10. run_main_loop(common/board_r.c)
  11. main_loop(common/main.c)

board_init_f是uboot重定位前的流程,它包括一些基础模块的初始化和重定位相关的准备工作。以下为该函数在armv8架构下可能的执行流程,图中虚线框表示该流程是可配置的,实线框表示是必选的。

image.png

board_init_r是uboot重定位后需要执行的流程,它包含基础模块、硬件驱动以及板级特性等的初始化,并最终通过run_main_loop启动os会进入命令行窗口。

image.png

1.1 main_loop到进入命令行

image.png

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

image.png

可见这个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中定义就可以了。那么我们想自己添加一个命令行:

  1. configs/qemu_arm64_defconfig添加宏控
    CONFIG_CMD_HELLO=y
    
  2. cmd/Kconfig里面添加config定义
    config CMD_HELLO
     bool "hello"
     help
       hello support.
    
  3. cmd/Makefile中包含上这个c函数的.o文件
    obj-$(CONFIG_CMD_HELLO) += hello.o
    
  4. 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.................."
)

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f6459d6259474b6d85e236f0b3ccda07~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGhhdHdheTE5ODk=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA1MjExMTIyNzY5NzMzNiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734935841&x-orig-sign=LFvsCfmPZeSV2s98g90Dc5xuYdA%3D)

# 2. 驱动管理

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7a185cdc2b4d4aceaa9e8e6dc61f9827~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGhhdHdheTE5ODk=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA1MjExMTIyNzY5NzMzNiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734935841&x-orig-sign=Am2PxO13e%2FVvlDbq%2B1d1KRKzecM%3D)

为了方便对硬件和驱动的管理,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在内存中的布局图:

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b5a115da1f934150a5f3eb99a4e8fc27~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGhhdHdheTE5ODk=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA1MjExMTIyNzY5NzMzNiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734935841&x-orig-sign=Y8rpTRxP0e11exJ06fB4gTOJMQk%3D)

对于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指向子节点。

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/57065d53b5894be28c92ab59be7115dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGhhdHdheTE5ODk=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA1MjExMTIyNzY5NzMzNiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734935841&x-orig-sign=AidKyk22mpFiAF4uM4KAgj7fv24%3D)

在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等之间的绑定关系,其主要包含以下部分:

  1. udevice与driver的绑定
  2. udevice与uclass的绑定
  3. 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):

  1. 人脑有三层:本能脑、情绪脑、理智脑。生活中做的大部分决策往往源于本能和情绪,而非理智。为了生存,思考🤔锻炼💃🏻这种耗能行为,会被本能脑排斥,形成目光短浅、即时满足、避难趋易、急于求成。
  2. 缺乏耐心和寻求舒适是人的天性,需要用元认知来审视监督自己,然后利用理智脑延迟满足来跟本能脑和情绪脑和谐相处。选择正确的方向,遵循刻意练习的原则,在舒适区边缘一点点拓展自己的能力范围。
  3. 利用潜意识去抓取内心的想法和做出一些选择,审视自己的第一反应。学习的时候可以不用面面俱到,但是之后让自己想下那些东西有用让自己印象深刻,然后针对这点去思考拓展实践,读万卷书不如行千里路。
  4. 无法把自己在注意力投入到重要的事情上,像刷手机视频、打游戏、刷剧和对有兴趣可以即时满足的小事投入过多的时间等,就是元认知出问题了。你能意识到自己在想什么,进而意识到这些想法是否明智,再进一步纠正那些不明智的想法,最终做出更好的选择。一句话:多反思。

results matching ""

    No results matching ""