RISCV Linux kernel 启动代码分析之七:早期使用opensbi的串口打印

文摘   2024-11-20 08:38   湖南  

一. 前言

适配内核到新的平台,基本环境搭建好之后,首要的就是要调通调试串口,方便后面的信息打印。正常流程init/main.cstart_kernel入口,要到console_init之后才能真正打印,前面的打印都是缓存在printkringbuffer中的。如果在console_init前就异常了,此时就看不到打印信息,为了调试console_init前的状态,需要能更早的打印。内核提供了一种early打印的方式,尤其是riscv平台我们可以直接ecall调用opensbi的打印,这样opensbi适配好之后这里就可以直接使用。这一篇就来分析下kernelearly打印数据流。

二. 配置

使能early打印需要做一些配置

menuconfig

Device Drivers  --->  

Character devices --->

Serial drivers  --->    

[*] Early console using RISC-V SBI  

对应配置项drivers/tty/serial/Kconfig

SERIAL_EARLYCON_RISCV_SBI

config SERIAL_EARLYCON_RISCV_SBI    bool "Early console using RISC-V SBI"    depends on RISCV_SBI_V01    select SERIAL_CORE    select SERIAL_CORE_CONSOLE    select SERIAL_EARLYCON    help      Support for early debug console using RISC-V SBI. This enables      the console before standard serial driver is probed. This is enabled      with "earlycon=sbi" on the kernel command line. The console is      enabled when early_param is processed.

依赖RISCV_SBI_V01,该选项配置之后默认也会配置

SERIAL_CORE

SERIAL_CORE_CONSOLE

SERIAL_EARLYCON

其中RISCV_SBI_V01是默认使能的,依赖于RISCV_SBI

config RISCV_SBI_V01    bool "SBI v0.1 support"    default y    depends on RISCV_SBI    help      This config allows kernel to use SBI v0.1 APIs. This will be      deprecated in future once legacy M-mode software are no longer in use.

RISCV_SBI默认也是y,依赖!RISCV_M_MODE

# set if we are running in S-mode and can use SBI callsconfig RISCV_SBI    bool    depends on !RISCV_M_MODE    default y

RISC_M_MODE又依赖!MMU,即如果不适用MMU则内核跑M模式

# set if we run in machine mode, cleared if we run in supervisor modeconfig RISCV_M_MODE    bool    default !MMU

MMU默认是y

config MMU    bool "MMU-based Paged Memory Management Support"    default y    help      Select if you want MMU-based virtualised addressing space      support by paged memory management. If unsure, say 'Y'.

配置后output/.config

CONFIG_SERIAL_EARLYCON_RISCV_SBI=y

output/include/config/auto.conf

CONFIG_SERIAL_EARLYCON_RISCV_SBI=y

output/include/generated/autoconf.h

#define CONFIG_SERIAL_EARLYCON_RISCV_SBI 1

drivers/tty/serial/Makefile中编译对应的文件 earlycon-riscv-sbi.c

obj-$(CONFIG_SERIAL_EARLYCON_RISCV_SBI) += earlycon-riscv-sbi.o

三. 相关代码

3.1 Kernelecall调用opensbi的打印

drivers/tty/serial/earlycon-riscv-sbi.c

其中early_sbi_setup设置对应的写接口

static int __init early_sbi_setup(struct earlycon_device *device,                  const char *opt){    device->con->write = sbi_console_write;    return 0;}

实现如下

static void sbi_console_write(struct console *con,                  const char *s, unsigned n){    struct earlycon_device *dev = con->data;    uart_console_write(&dev->port, s, n, sbi_putc);}

最终实际是调用sbi_putc接口写

static void sbi_putc(struct uart_port *port, int c){    sbi_console_putchar(c);}

sbi_console_putchararch/riscv/kernel/sbi.c中实现

通过ecall调用opensbi中的实现,

前面我们看到CONFIG_RISCV_SBI_V01是使能的

#ifdef CONFIG_RISCV_SBI_V01/** * sbi_console_putchar() - Writes given character to the console device. * @ch: The data to be written to the console. * * Return: None */void sbi_console_putchar(int ch){    sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR, 0, ch, 0, 0, 0, 0, 0);}EXPORT_SYMBOL(sbi_console_putchar);

sbi_ecall

arch/riscv/kernel/sbi.c中实现

struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0,            unsigned long arg1, unsigned long arg2,            unsigned long arg3, unsigned long arg4,            unsigned long arg5){    struct sbiret ret;
    register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0);    register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1);    register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2);    register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3);    register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4);    register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5);    register uintptr_t a6 asm ("a6") = (uintptr_t)(fid);    register uintptr_t a7 asm ("a7") = (uintptr_t)(ext);    asm volatile ("ecall"              : "+r" (a0), "+r" (a1)              : "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7)              : "memory");    ret.error = a0;    ret.value = a1;    return ret;}

其中第一个参数extSBI_EXT_0_1_CONSOLE_PUTCHAR=1放在寄存器a7中。

3.2 Kernel中初始化路径

3.2.1命令行参数param_setup_earlycon(line)

配置参数

要使能earlycon功能,需要给内核传入参数earlycon=xxx,

xxx表示对应的驱动和参数,我们这里是sbi使用sbi的串口输出,如果没有指定xxx则从设备树的chosen节点解析串口。

参考《https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html》下搜索earlycon

对应流程如下,如下位置对early后面是否有参数进行不同的处理

可以手动添加参数:

arch/riscv/Kconfig中如下配置CMDLINE

config CMDLINE    string "Built-in kernel command line"    help      For most platforms, the arguments for the kernel's command line      are provided at run-time, during boot. However, there are cases      where either no arguments are being provided or the provided      arguments are insufficient or even invalid.
      When that occurs, it is possible to define a built-in command      line here and choose how the kernel should use it later on.

menuconfig配置

Boot options --->   

(earlycon=sbi) Built-in kernel command line

配置完对应output/.config

CONFIG_CMDLINE="earlycon=sbi"

include/generated/autoconf.h

#define CONFIG_CMDLINE "earlycon=sbi"

参数解析过程

我们就来分析下earlycon=sbi时的路径。

drivers/tty/serial/earlycon.c

early_param("earlycon", param_setup_earlycon);

其中include/linux/init.h

#define early_param(str, fn)                        \    __setup_param(str, fn, fn, 1)
#define __setup_param(str, unique_id, fn, early)            \    static const char __setup_str_##unique_id[] __initconst     \        __aligned(1) = str;                     \    static struct obs_kernel_param __setup_##unique_id      \        __used __section(".init.setup")             \        __attribute__((aligned((sizeof(long)))))        \        = { __setup_str_##unique_id, fn, early }

展开为

__setup_param("earlycon", param_setup_earlycon, param_setup_earlycon,1)

继续展开

static const char __setup_str_ param_setup_earlycon[] __initconst

__aligned(1) = “earlycon”;

static struct obs_kernel_param __setup_param_setup_earlycon

__used __section(".init.setup")  

__attribute__((aligned((sizeof(long)))))

={__setup_str_param_setup_earlycon, param_setup_earlycon, 1}

即定义了一个static struct obs_kernel_param类型结构体变量__setup_param_setup_earlycon

放置在段.init.setup中按照sizeof(long)对齐,结构体内容是

{.str=__setup_str_param_setup_earlycon,

.setup_func=param_setup_earlycon,

.early= 1}

__setup_str_param_setup_earlycon即前面的字符数组内容是earlycon

param_setup_earlycon是回调函数

其中结构体 struct obs_kernel_paraminclude/linux/init.h


struct obs_kernel_param {    const char *str;    int (*setup_func)(char *);    int early;};

include/asm-generic/vmlinux.lds.h

#define INIT_SETUP(initsetup_align)                 \        . = ALIGN(initsetup_align);             \        __setup_start = .;                  \        KEEP(*(.init.setup))                    \        __setup_end = .;
#define INIT_DATA_SECTION(initsetup_align)              \    .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {       \        INIT_DATA                       \        INIT_SETUP(initsetup_align)             \        INIT_CALLS                      \        CON_INITCALL                        \        INIT_RAM_FS                     \    }

arch/riscv/kernel/vmlinux.lds.S

INIT_DATA_SECTION(16)

所以上述变量放在了.init.setup

开始位置是__setup_start

init/main.c中申明变量以便访问

extern const struct obs_kernel_param __setup_start[], __setup_end[];

如下函数遍历上述.init.setup段,遍历每个结构体,回调对应的setup_func,这里即param_setup_earlycon

static bool __init obsolete_checksetup(char *line){    const struct obs_kernel_param *p;    bool had_early_param = false;
    p = __setup_start;    do {        int n = strlen(p->str);        if (parameqn(line, p->str, n)) {            if (p->early) {                /* Already done in parse_early_param?                 * (Needs exact match on param part).                 * Keep iterating, as we can have early                 * params and __setups of same names 8( */                if (line[n] == '\0' || line[n] == '=')                    had_early_param = true;            } else if (!p->setup_func) {                pr_warn("Parameter %s is obsolete, ignored\n",                    p->str);                return true;            } else if (p->setup_func(line + n))                return true;        }        p++;    } while (p < __setup_end);    return had_early_param;}

obsolete_checksetup这里打断点,我们来跟踪分析函数的处理过程

此时我们看到

unknown_bootoption传过来的参数值为param=earlyconval=sbi

正是我们配置的参数。

至于参数是怎么解析的参考parse_args

调用路径是start_kernel->pares_args->unknown_bootoption

执行完repair_env_string

参数param变为了earlycon=sbi

继续单步进入obsolete_checksetup

然后从__setup_start开始遍历段,需要满足参数即linep->str参数匹配这里line就是earlycon=sbi,通过函数parameqn匹配,然后要满足p->early=1

再来看我们定义的结构体

early_param("earlycon", param_setup_earlycon);

会满足这两个条件

static const char __setup_str_ param_setup_earlycon[] = earlycon;

static struct obs_kernel_param __setup_param_setup_earlycon

{.str=__setup_str_param_setup_earlycon,

.setup_func=param_setup_earlycon,

.early= 1}

接下来

line[n] == '='满足

这里直接返回true,所以如下路径是不通的,这里路只能使early不带参数时,走early_init_dt_scan_chosen_stdout查找/chosen的节点。

此时就不会调用param_setup_earlycon(line)

而只有此时不带参数即只有early时才会调用setup_funcparam_setup_earlycon,传入的参数line=

实际early=sbi这里走到是另外一条路,如下图所示

对应do_early_param时会回调setup_func,但是此时传递的参数param_setup_earlycon(line)中的linevalsbi了。和上面一条路传递的参数early不一样了。

do_early_paramparam=earlyval=sbi

param_setup_earlycon(line)line=sbi

而如果走obsolete_checksetup这边的路径,line传入的参数为空

此时回调

param_setup_earlycon(line)

时走early_init_dt_scan_chosen_stdout

注册处理

EARLYCON_DECLARE(sbi, early_sbi_setup);在指定段中放置结构体指针,

初始化时遍历该结构体指针,找到结构体,调用其setup函数进行初始化。

include/linux/serial_core.h

#define _OF_EARLYCON_DECLARE(_name, compat, fn, unique_id)      \    static const struct earlycon_id unique_id           \         EARLYCON_USED_OR_UNUSED __initconst            \        = { .name = __stringify(_name),             \            .compatible = compat,               \            .setup = fn  };                 \    static const struct earlycon_id EARLYCON_USED_OR_UNUSED     \        __section("__earlycon_table")               \        * const __PASTE(__p, unique_id) = &unique_id
#define OF_EARLYCON_DECLARE(_name, compat, fn)              \    _OF_EARLYCON_DECLARE(_name, compat, fn,             \                 __UNIQUE_ID(__earlycon_##_name))#define EARLYCON_DECLARE(_name, fn) OF_EARLYCON_DECLARE(_name, "", fn)

就是定义了一个结构体

struct earlycon_id

include/linux/serial_core.h

struct earlycon_id {    char    name[15];    char    name_term;  /* In case compiler didn't '\0' term name */    char    compatible[128];    int (*setup)(struct earlycon_device *, const char *options);};

static const struct earlycon_id __earlycon_sbi

其成员

.setup = early_sbi_setup

然后定义了一个指针变量__p__earlycon_sbi指向了这个结构体

static const struct earlycon_id * const __p__earlycon_sbi = & __earlycon_sbi

该指针放在了段__earlycon_table

source/include/asm-generic/vmlinux.lds.h

#ifdef CONFIG_SERIAL_EARLYCON#define EARLYCON_TABLE() . = ALIGN(8);              \             __earlycon_table = .;          \             KEEP(*(__earlycon_table))      \             __earlycon_table_end = .;#else#define EARLYCON_TABLE()#endif

符号__earlycon_table表示该段的开始

drivers/tty/serial/earlycon.c

setup_earlycon

如果是走右边这条路

drivers/of/fdt.c

early_init_dt_scan_chosen_stdout

最终都是注册控制台register_console

而走左边的sbi路径会回调early_sbi_setup将写接口改为

device->con->write = sbi_console_write;调用sbi打印。

3.3. Opensbi代码中实现

lib/sbi/sbi_ecall.c

ecall处理入口如下

int sbi_ecall_handler(struct sbi_trap_regs *regs){    int ret = 0;    struct sbi_ecall_extension *ext;    unsigned long extension_id = regs->a7;    unsigned long func_id = regs->a6;    struct sbi_trap_info trap = {0};    unsigned long out_val = 0;    bool is_0_1_spec = 0;
    ext = sbi_ecall_find_extension(extension_id);    if (ext && ext->handle) {        ret = ext->handle(extension_id, func_id,                  regs, &out_val, &trap);        if (extension_id >= SBI_EXT_0_1_SET_TIMER &&            extension_id <= SBI_EXT_0_1_SHUTDOWN)            is_0_1_spec = 1;    } else {        ret = SBI_ENOTSUPP;    }

ext即传过来的a7,待打印字符通过a0传递。以下即获取该ext

  ext = sbi_ecall_find_extension(extension_id);

include/sbi/sbi_ecall_interface.h中定义对应的ext宏。

#define SBI_EXT_0_1_CONSOLE_PUTCHAR     0x1

#define SBI_EXT_0_1_CONSOLE_GETCHAR     0x2

然后回调对应的处理接口

      ret = ext->handle(extension_id, func_id,

                regs, &out_val, &trap);

Handle回调在如下地方注册

sbi_ecall_init->

ret = sbi_ecall_register_extension(&ecall_legacy);

sbi_ecall_register_extensionlib/sbi/sbi_ecall.c中实现

int sbi_ecall_register_extension(struct sbi_ecall_extension *ext){    struct sbi_ecall_extension *t;
    if (!ext || (ext->extid_end < ext->extid_start) || !ext->handle)        return SBI_EINVAL;    sbi_list_for_each_entry(t, &ecall_exts_list, head) {        unsigned long start = t->extid_start;        unsigned long end = t->extid_end;        if (end < ext->extid_start || ext->extid_end < start)            /* no overlap */;        else            return SBI_EINVAL;    }    SBI_INIT_LIST_HEAD(&ext->head);    sbi_list_add_tail(&ext->head, &ecall_exts_list);    return 0;}

ecall_legacy

lib/sbi/sbi_ecall_legacy.c中定义

struct sbi_ecall_extension ecall_legacy = {    .extid_start = SBI_EXT_0_1_SET_TIMER,    .extid_end = SBI_EXT_0_1_SHUTDOWN,    .handle = sbi_ecall_legacy_handler,};

所以最终回调其handle

sbi_ecall_legacy_handlerlib/sbi/sbi_ecall_legacy.c中实现

该函数就根据ext=SBI_EXT_0_1_CONSOLE_PUTCHAR,最终调用sbi_putc发送一个字符

    case SBI_EXT_0_1_CONSOLE_PUTCHAR:        sbi_putc(regs->a0);        break;    case SBI_EXT_0_1_CONSOLE_GETCHAR:        ret = sbi_getc();        break;

四. 仿真调试

查看early_sbi_setup的调用路径

参数为sarlycon=sbi

setup_earlycon

在如下位置打断点运行到到断点处,查看调用栈

hb setup_earlycon

c

hb early_sbi_setup

c

bt

对应如下路径

参数为earlycon

此时走设备树/chosen的串口

early_init_dt_scan_chosen_stdout

hb early_init_dt_scan_chosen_stdout

c

查看sbi_console_write调用路径

hb sbi_console_write

c

bt

看到在console_init();之前此时就可以打印了,之前是必须要在console_init();之后才真正打印,之前是打印在ringbuffer中的缓存的。

最终是调用ecall完成打印

五. 总结

如果要使能early通过opensbi打印,要使配置参数early=sbi,且使能early打印,此时在console_init之前就可以进行直接打印。























嵌入式Lee
嵌入式软硬件技术:RTOS,GUI,FS,协议栈,ARM,总线,嵌入式C,开发环境 and blablaba....多年经验分享,非硬货不发,带你扒开每一个技术背后的根本原理。
 最新文章