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

原创 嵌入式Lee 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之前就可以进行直接打印。























评论
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 70浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 51浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 66浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 41浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 102浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 83浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 106浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 65浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 98浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 86浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 37浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦