本文旨在帮助开发者构建完整的 openvela 开发体系。前半部分介绍如何在 Ubuntu 环境下通过 VS Code 插件完成项目的创建、编译及调试;后半部分深入解析系统从上电到 Shell 启动的底层流程,实现从"上手使用"到"理解原理"的进阶。

一. 开发环境概述

在开始之前,请确保开发环境满足以下软硬件要求。

  • 硬盘:至少 40 GB 可用空间(用于存放源代码及编译产物)。

  • 内存:至少 16 GB RAM。

  • 系统版本:Ubuntu 22.04(支持 arm64 或 x86_64 架构)。

关于 openvela 项目的创建、编译、调试及应用开发的详细操作步骤,请参见 openvela 官方文档中的 VS Code 插件使用指南:https://gitee.com/open-vela/docs/blob/dev/zh-cn/quickstart/vscode_plugin_usage.md

二. 系统启动流程详解

本小节将详细解析 openvela 系统的 BSP(Board Support Package,板级支持包)初始化流程、启动脚本机制以及核心函数调用关系,旨在为开发者提供一份清晰、结构化的启动过程指南。

1. 板级初始化流程

openvela 系统的启动过程遵循明确的 BSP 初始化序列。该序列从内核入口 nx_start 开始,在不同任务上下文中分阶段执行初始化函数,直至进入 idle 循环。核心初始化顺序如下图所示。

每个关键函数的作用、执行上下文和相关配置项如下表所示。

函数

功能描述

nx_start

操作系统入口点

board_early_initialize

板级早期硬件初始化,此阶段内核核心组件尚未就绪,禁止调用阻塞函数

board_late_initialize

板级主要驱动初始化,此时内核核心组件已就绪,允许调用阻塞函数

board_app_initialize

由 boardctl(BOARDIOC_INIT) 调用,初始化应用层,注意此时文件系统尚未挂载

rc.sysinit

挂载文件系统并初始化核心启动服务

board_app_finalinitialize

由 boardctl(BOARDIOC_FINALINIT) 调用,初始化依赖文件系统的驱动

rcS

启动用户空间核心应用和服务

2. 每个阶段的启动过程

本节按启动时序将整个过程分为 5 个阶段,逐一展示各阶段的核心函数调用关系。从 Idle Task 中的内核入口开始,经过 AppBringUp 线程创建、NSH 任务启动与初始化,直至进入用户交互和命令执行。

(1)启动 AppBringUp

系统上电后,内核入口 nx_start 在 Idle Task 上下文中完成早期硬件初始化,随后通过 nx_bringup 创建工作队列并启动 AppBringUp 线程(若使能 CONFIG_BOARD_LATE_INITIALIZE),否则直接进入应用启动流程。具体调用关系如下:

// --- 在 Idle Task (空闲任务) 上下文中 ---
nx_start()
  |
  +--> board_early_initialize()
  |
  +--> nx_bringup()
          |
          +--> nx_workqueues();
          |
          +--> nx_create_initthread()
                  |
#ifdef CONFIG_BOARD_LATE_INITIALIZE
                  +--> nxthread_create AppBringUp
                          |
                          +--> nx_start_task()
                                   |
                                   +--> nx_start_application()
#else
                  |
                  +--> nx_start_application()
#endif

(2)启动 NSH

AppBringUp 线程(或直接在 Idle Task 中)调用 nx_start_application,依次完成 ROMFS 挂载、板级后期初始化、C++ 运行库初始化,最终通过 task_spawn 启动 NSH 任务。具体调用关系如下:

nx_start_application()
  |
#ifdef CONFIG_ETC_ROMFS
  +--> nx_romfsetc()
#endif  
  |
#ifdef CONFIG_BOARD_LATE_INITIALIZE
  +--> board_late_initialize()
#endif
  |
  +--> lib_cxx_initialize()
  |
#if defined(CONFIG_INIT_ENTRY)
#  ifdef CONFIG_BUILD_PROTECTED
  |
  +--> task_spawn CONFIG_INIT_ENTRYNAME,USERSPACE->us_entrypoint
#  else
  |
  +--> task_spawn CONFIG_INIT_ENTRYNAME,CONFIG_INIT_ENTRYPOINT
#  endif
#elif defined(CONFIG_INIT_FILE)
#ifdef CONFIG_INIT_MOUNT
  // 内核模式下
  +--> nx_mount CONFIG_INIT_MOUNT_SOURCE,CONFIG_INIT_MOUNT_TARGET
#endif
  +--> exec_spawn(CONFIG_INIT_FILEPATH...)
#endif

(3)NSH 初始化流程

NSH 任务启动后,nsh_initialize 按顺序完成符号表注册、板级应用初始化(board_app_initialize)、一级启动脚本 rc.sysinit 执行、网络初始化、最终板级初始化(board_app_finalinitialize)以及二级启动脚本 rcS 执行。具体调用关系如下:

// --- 在 Nsh Task 上下文中 ---
nsh_initialize()
  |
#if defined(CONFIG_NSH_SYMTAB)
  +--> boardctl(BOARDIOC_APP_SYMTAB, (uintptr_t)&symdesc);
#endif
  |
#ifdef CONFIG_NSH_ARCHINIT
  +--> boardctl(BOARDIOC_INIT, 0);
      |
      +--> board_app_initialize()
#endif
  |
#if defined(CONFIG_ETC_ROMFS) && !defined(CONFIG_NSH_DISABLESCRIPT)
  +--> nsh_sysinitscript(&pstate->cn_vtbl); //一级脚本,解析rc.sysinit
#endif
  |
#ifdef CONFIG_NSH_NETINIT
  +--> netinit_bringup();
#endif
  |
#if defined(CONFIG_NSH_ARCHINIT) && defined(CONFIG_BOARDCTL_FINALINIT)
  +--> boardctl(BOARDIOC_FINALINIT, 0);
      |
      +--> board_app_finalinitialize()
#endif
  |
#if defined(CONFIG_ETC_ROMFS) && !defined(CONFIG_NSH_DISABLESCRIPT)
  +--> nsh_initscript(&pstate->cn_vtbl);  //二级脚本,解析rcS
#endif

(4)NSH 交互流程

如果使能 CONFIG_NSH_CONSOLE,那么 NSH 就会从 stdin(Standard Input,标准输入)上进行 readline,解析命令或执行内建应用(Builtin App)。具体调用关系如下:

nsh_consolemain
    |
    +---> struct console_stdio_s * pstate = nsh_newconsole(true)
    |
    +---> nsh_clone_console(pstate) //复制 stderr/stdout
    |
    +---> for(;;)
        |
        +--> nsh_wait_inputdev(pstate, msg) //等待输入设备
        |
        +--> nsh_session(pstate, NSH_LOGIN_LOCAL, argc, argv)

nsh_session 是 NSH 的核心交互入口。进入后首先根据配置执行登录验证(控制台登录或 Telnet 登录),随后处理启动参数——若带 -c 选项则直接执行指定命令,若传入脚本路径则执行脚本。完成初始化后进入主循环,持续从 stdin 读取用户输入并调用 nsh_parse 逐行解析执行。具体调用关系如下:

nsh_session
    |
#ifdef CONFIG_NSH_CONSOLE_LOGIN
    +--> nsh_login(pstate)
#elif CONFIG_NSH_TELNET_LOGIN
    +--> nsh_telnetlogin
#endif
    |
    +--> if (login != NSH_LOGIN_NONE) nsh_loginscript(vtbl);
    | 
    //处理启动参数,带-c执行命令,或者直接执行脚本
    +--> if (strcmp(argv[1], "-c") == 0) nsh_parse(vtbl, argv[2]);
    +--> nsh_script(vtbl, argv[0], argv[1], true);
    |
    +---> for(;;)
        |
        +--> ret = readline_fd(pstate->cn_line, CONFIG_NSH_LINELEN,
                               INFD(pstate), OUTFD(pstate)); 
        | 
        +--> nsh_parse(vtbl, pstate->cn_line);     

(5)NSH 执行命令或者 app

当用户在 NSH 交互界面输入命令时,nsh_parse 负责逐条解析并执行。对于内建应用(Builtin App),通过 posix_spawn 创建新进程运行;对于文件系统中的可执行文件,通过 nsh_fileapp 加载执行;其余则作为 NSH 内置命令处理。具体调用关系如下:

nsh_parse
    |
    +--> for (; ; )
            |
            +--> nsh_parse_command(vtbl, start, NULL);
                    |
                    +---> nsh_execute(vtbl, 4, sh_argv, param);
                            |
#if defined(CONFIG_NSH_BUILTIN_APPS) && !defined(CONFIG_NSH_BUILTIN_AS_COMMAND)
                            +--> nsh_builtin(vtbl, argv[0], argv, param); 
                                    |
                                    +--> exec_builtin(cmd, argv, param);
                                            |
                                            +-->  posix_spawn(&pid, builtin->name, ...)
                                    |
                                    +--> waitpid(ret, &rc, WUNTRACED)
#endif
                            |
#ifdef CONFIG_NSH_FILE_APPS 
                            +--> nsh_fileapp(vtbl, argv[0], argv, param)
#endif
                            |
                            +--> nsh_command(vtbl, argc, argv);

三. 启动脚本机制

openvela 的启动脚本是系统初始化的重要组成部分,负责在内核完成基本初始化后挂载文件系统、启动核心服务和用户应用。本节介绍启动脚本的存放位置、加载机制以及生成方式。

1. 启动脚本位置与加载方式

openvela 使用启动脚本 rcS 和 rc.sysinit 来完成系统启动配置。启动脚本由 NSH 任务通过 nshlib 加载并解析,其位置由以下配置项指定:

  • CONFIG_ETC_ROMFSMOUNTPT/CONFIG_NSH_SYSINITSCRIPT

  • CONFIG_ETC_ROMFSMOUNTPT/CONFIG_NSH_INITSCRIPT

通常,启动脚本存放于 /etc 目录下。/etc 的内容以 ROMFS 文件系统形式与 openvela 内核镜像一同编译链接,系统启动后自动挂载。相关配置如下:

CONFIG_FS_ROMFS=y
CONFIG_ETC_ROMFS=y
CONFIG_ETC_ROMFSMOUNTPT="/etc"
CONFIG_NSH_SYSINITSCRIPT="init.d/rc.sysinit"
CONFIG_NSH_INITSCRIPT="init.d/rcS"

2. 启动脚本生成方式

openvela 的 /etc 内容由不同的板级目录生成,可通过 genromfs 和 xxd 工具生成 etc_romfs.c 文件,并编译到内核。例如,生成文件路径为 boards/arm/at32/at32f437-mini/src/etc_romfs.c,对应的生成脚本路径为 boards/arm/at32/at32f437-mini/tool/mkromfs.sh。

常见方式是直接从 boards///src/etc 目录构建 /etc 内容。例如,模拟器的示例目录为 boards/sim/sim/sim/src/etc。

/etc 目录下的所有内容由其上级目录的 Makefile 控制。其中 RCSRCS 用于指定启动脚本,RCRAWS 用于指定加入 etc 目录下的其他文件和目录。示例如下:

ifeq ($(CONFIG_ETC_ROMFS),y)
  RCSRCS = etc/init.d/rc.sysinit etc/init.d/rcS
  RCRAWS = etc/group etc/passwd
endif

四. 启动时序与函数调用栈分析

下面一系列图以 qemu-arm-64-v8a-ap 为例展示了 openvela 启动流程中的核心函数调用关系,以帮助开发者更直观地理解整个过程。

系统启动流程概览

下图展示了 openvela 从上电到进入用户交互的完整启动时序概览,后续各小节将按阶段逐一展开分析。

 

阶段一:汇编引导

上电后,处理器从 __start 进入启动汇编流程。这一阶段的核心任务是为 C 语言运行环境做准备:将 .data 段从 ROM 拷贝到 RAM、将 .bss 段清零,确保全局变量和静态变量处于正确的初始状态。随后初始化 EL1(Exception Level 1,异常等级 1)运行环境与缓存——EL1 是 ARM64 架构下操作系统内核运行的特权级别,在此级别下内核拥有对硬件资源的完整访问权限。完成特权级配置后,进入芯片/板级启动路径,开启 MMU(Memory Management Unit,内存管理单元)并完成板级初始化,使系统从物理地址空间切换到虚拟地址空间。最终跳转到 nx_start,正式进入内核启动阶段。

阶段二:内核初始化

nx_start 是 openvela 内核的 C 语言入口函数,负责按顺序初始化操作系统的各核心子系统。首先完成任务管理与调度器初始化,使内核具备多任务调度能力;接着初始化内存管理子系统和硬件抽象层;随后依次建立 VFS(Virtual File System,虚拟文件系统)根节点、注册中断处理框架、启动系统时钟和定时器、初始化信号机制等基础设施——这些子系统是后续驱动注册和用户任务运行的基础,必须在此阶段优先就绪。全部核心子系统初始化完成后,nx_start 调用架构相关的驱动初始化,最终进入 nx_bringup 开始系统任务的创建与启动。

阶段三:系统带起(System BringUp)

nx_bringup 负责系统级 bring-up:设置基础环境变量,启动页工作线程与各类工作队列,创建 init 线程;随后进入 nx_start_application,完成 ROMFS /etc 环境准备(/etc 目录以 ROMFS 文件系统形式挂载,其中存放了启动脚本和系统配置文件)与 C++ 运行库初始化,并根据配置启动 nsh_main 或执行指定的 init 文件。

阶段四:应用环境(NSH)

进入 nsh_main 后,系统到达用户可感知的第一个交互界面。NSH 首先完成自身初始化(提示符/readline 配置、可选的 USB trace、符号表注册、通过 boardctl 触发板级应用初始化),然后依次执行 rc.sysinit 与 rcS 两级启动脚本(含网络 bring-up 与 finalinit),完成文件系统挂载、核心服务启动和用户应用加载。全部初始化完成后,NSH 创建控制台会话进入 nsh_session,开始循环读取用户输入、解析并执行命令,为开发者提供交互式 Shell 环境。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐