Zephyr 启动流程

程序入口

  • 使用gcc进行交叉编译时,可以通过链接脚本中添加 ENTRY 指令用于指定程序入口,通过该指令指定的标签即为程序入口,下面是 zephyr/include/zephyr/arch/arm/aarch32/cortex_m/scripts/linker.ld:85 中指定的程序入口:
ENTRY(CONFIG_KERNEL_ENTRY)
  • 此处的程序入口可通过 KConfig 修改,下面是其默认值:
config KERNEL_ENTRY
	string "Kernel entry symbol"
	default "__start"
	help
	  Code entry symbol, to be set at linking phase.
  • 从上面的代码中可以得出如下结论,arm32 默认情况下程序的入口即为__start。
  • 通过 ENTRY 指定程序入口,并不是意味着 __start 对应的程序在链接后位于程序的最前面,而是在程序加载时从符号表查找预定义的入口地址。
  • 其他架构的CPU也可以通过类似的方式查找到入口函数。

Cortex-M 中断向量表机制

  • 虽然通过 ENTRY 指定了入口函数,但是Cortex-M系列处理器并不会查找该符号的位置,使用 ENTRY 只是为了在动态加载程序时可以找到入口,Cortex-M系列处理器是通过中断向量表来确定程序入口。

  • 在 Cortex-M 系列 MCU 中,如果设置为从 flash 启动,flash 前1K 用于存放中断向量表,其中第一个字为程序栈顶指针,第二个字为复位向量,即 ResetHandler 的地址,boot会将第一个字用于初始化栈顶,第二个字作为中保存的地址作为程序跳转地址,从而跳转到程序中运行,详细说明可参考异常模型, 该机制为Cortex-M 系列 MCU 中独有,与此相对的,不同的芯片在系统启动时对可执行程序都有一定的格式要求,需要在程序的头部添加符合操作规范的头。

  • 上电启动后的第一段程序必须符合相应的规范才能被正确执行。

zImage 和 uImage

  • 程序经过编译后会生成一个elf格式的可执行程序,再通过objcopy工具转换为bin文件,但是其体积较大,通过对文件进行压缩并在头部添加一段压缩代码成为 zImage。使用的时候,通过zImage镜像头部的解压缩代码进行自解压,然后执行解压出来的内核镜像。
  • uboot为了启动linux内核,发明了一种内核格式叫uImage。uImage是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。
  • 芯片上电后通常是不能识别 zImage 和 uImage 的,运行的第一段程序通常是 bootloader,例如我们熟知的 u-boot,u-boot 运行之后,可以解析 zImage 和 uImage 加载内核。
  • 在一些虚拟仿真系统中,其本身就是软件模拟芯片运行,且最终目的也是为了加载内核,它可以跳过bootloader 直接加载 zImage 和 uImage。

系统初始化

  • zephyr/arch/arm/core/aarch32/cortex_m/reset.S 中定义了 __start,程序从此处开始往后执行
SECTION_SUBSEC_FUNC(TEXT,_reset_section,__start)

#if defined(CONFIG_DEBUG_THREAD_INFO)
    /* Clear z_sys_post_kernel flag for RTOS aware debuggers */
    movs.n r0, #0
    ldr r1, =z_sys_post_kernel
    strb r0, [r1]
#endif /* CONFIG_DEBUG_THREAD_INFO */

#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)
    /* 复位CONTROL寄存器,在特权模式和非特权模式均使用MSP,
     * 当切换栈指针之后必须使用ISB指令刷新流水线,
     * 以保证在ISB之后执行的指令都使用新的栈
     */
    movs.n r0, #0
    msr CONTROL, r0
    isb
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
    /* 堆栈限制寄存器分别限制 MSP 和 PSP 寄存器可以下降的程度,此处设置为0 */
    movs.n r0, #0
    msr MSPLIM, r0
    msr PSPLIM, r0
#endif /* CONFIG_CPU_CORTEX_M_HAS_SPLIM */

#endif /* CONFIG_INIT_ARCH_HW_AT_BOOT */

#if defined(CONFIG_PM_S2RAM)
    /* 低功耗相关初始化 */
    bl arch_pm_s2ram_resume
#endif /* CONFIG_PM_S2RAM */

#if defined(CONFIG_PLATFORM_SPECIFIC_INIT)
    /* 针对内存,cache,jtag,时钟,中断的一些特殊配置  */
    bl z_arm_platform_init
#endif

#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)
#if defined(CONFIG_CPU_HAS_ARM_MPU)
    /* 操作系统未运行之前使用平坦内存模型,所有内存均不受保护,
     * 为避免在初始化过程中触发读写保护进入异常,一定要关闭MPU
     */
    movs.n r0, #0
    ldr r1, =_SCS_MPU_CTRL
    str r0, [r1]
    dsb
#endif /* CONFIG_CPU_HAS_ARM_MPU */
    /* ARM32使用满递减栈,栈顶指针初始时刻指向高地址,
     * 将MSP指向 z_main_stack 的末尾,以便后续进行函数调用
     */
    ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZE
    msr msp, r0

    /* 清除MPU配置,关闭所有中断,清除被挂起的中断,重置Cache配置等 */
    bl z_arm_init_arch_hw_at_boot
#endif /* CONFIG_INIT_ARCH_HW_AT_BOOT */

    /* 屏蔽中断 */
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE)
    cpsid i
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
    movs.n r0, #_EXC_IRQ_DEFAULT_PRIO
    msr BASEPRI, r0
#else
#error Unknown ARM architecture
#endif

#ifdef CONFIG_WDOG_INIT
    /* 开启看门狗 */
    bl z_arm_watchdog_init
#endif

#ifdef CONFIG_INIT_STACKS
    /* 将栈全部设置为0xaa,可用于监测剩余栈容量 */
    ldr r0, =z_interrupt_stacks
    ldr r1, =0xaa
    ldr r2, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
    bl z_early_memset
#endif

    /* 初始化PSP,将CONTROL的SPSEL位设置为1(在特权模式下使用MSP,非特权模式下使用PSP)
     * 后续操作将使用 z_interrupt_stacks 作为栈进行初始化
     */
    ldr r0, =z_interrupt_stacks
    ldr r1, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
    adds r0, r0, r1
    msr PSP, r0
    mrs r0, CONTROL
    movs r1, #2
    orrs r0, r1 /* CONTROL_SPSEL_Msk */
    msr CONTROL, r0
    isb

    bl z_arm_prep_c

初始化C语言运行环境

  • 在C语言运行之前有一些步骤必须要完成
    • 将bss段清0
    • 从ROM中取出data段初始值并初始化data段
    • 初始化栈指针
    • 开启中断
void z_arm_prep_c(void)
{
	relocate_vector_table();
#if defined(CONFIG_CPU_HAS_FPU)
	z_arm_floating_point_init();
#endif
	z_bss_zero();
	z_data_copy();
#if ((defined(CONFIG_ARMV7_R) || defined(CONFIG_ARMV7_A)) && defined(CONFIG_INIT_STACKS))
	z_arm_init_stacks();
#endif
	z_arm_interrupt_init();
	z_cstart();
	CODE_UNREACHABLE;
}
  • 对于不同的平台还需要增加额外的操作,例如浮点寄存器的初始化,中断向量表的重定向,在系统启动之前还运行了一段芯片内置程序,这段程序将中断向量表的位置设置为 0x00000000,当程序从boot跳转到指定存储器运行之后,首先需要立即关闭中断,避免中断产生并跳转到错误的中断响应函数中,重设中断向量表偏移位置之后,再重新开启中断。

初始化内核开启任务调度

FUNC_NO_STACK_PROTECTOR
FUNC_NORETURN void z_cstart(void)
{
	/* 代码覆盖率测试相关 */
	gcov_static_init();

	/* 调用初始化级别为 INIT_LEVEL_EARLY 的函数进行初始化 */
	z_sys_init_run_level(INIT_LEVEL_EARLY);

	/* z_arm_interrupt_stack_setup 初始化MSP
	 * z_arm_exc_setup 初始化PENDSV、SysTick等中断的优先级,其中PENDSV优先级为最低
	 * z_arm_fault_init 初始化 Fault 中断。
	 * z_arm_cpu_idle_init 初始化 idle 线程
	 * z_arm_clear_faults 清除所有故障标志
	 * z_arm_mpu_init 初始化MPU
	 * z_arm_mmu_init 初始化MMU
	 */
	arch_kernel_init();
	
	/* 日志初始化 */
	LOG_CORE_INIT();

#if defined(CONFIG_MULTITHREADING)
	/* Note: The z_ready_thread() call in prepare_multithreading() requires
	 * a dummy thread even if CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN=y
	 */
	struct k_thread dummy_thread;

	z_dummy_thread_init(&dummy_thread);
#endif
	/* 初始化驱动中的静态节点 */
	z_device_state_init();

	/* 其他的硬件初始化 */
	z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);
	z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);

#ifdef CONFIG_STACK_CANARIES
	/* CONFIG_STACK_CANARIES 用于开启堆栈金丝雀功能,这是一种安全特性,有助于监测堆栈溢出,
	 * 当启动该功能时,系统启动时会生成一个随机数并保存在 __stack_chk_guard 中,
	 * 在函数返回之前会检查该值确保它没有被缓冲区溢出所覆盖。
	 */
	uintptr_t stack_guard;

	z_early_boot_rand_get((uint8_t *)&stack_guard, sizeof(stack_guard));
	__stack_chk_guard = stack_guard;
	__stack_chk_guard <<= 8;
#endif	/* CONFIG_STACK_CANARIES */

#ifdef CONFIG_TIMING_FUNCTIONS_NEED_AT_BOOT
	/* timing_init 函数用于初始化系统计时器 */
	timing_init();
	timing_start();
#endif

#ifdef CONFIG_MULTITHREADING
	/* CONFIG_MULTITHREADING为y时,使用多线程,否则只会有一个 main 线程,
	 * 默认情况下都启用多线程,通过将 main 线程添加到就绪队列中然后开启任务调度
	 */
	switch_to_main_thread(prepare_multithreading());
#else
#ifdef ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING
	/* Custom ARCH-specific routine to switch to main()
	 * in the case of no multi-threading.
	 */
	ARCH_SWITCH_TO_MAIN_NO_MULTITHREADING(bg_thread_main,
		NULL, NULL, NULL);
#else
	bg_thread_main(NULL, NULL, NULL);

	/* LCOV_EXCL_START
	 * We've already dumped coverage data at this point.
	 */
	irq_lock();
	while (true) {
	}
	/* LCOV_EXCL_STOP */
#endif
#endif /* CONFIG_MULTITHREADING */

	/*
	 * Compiler can't tell that the above routines won't return and issues
	 * a warning unless we explicitly tell it that control never gets this
	 * far.
	 */

	CODE_UNREACHABLE; /* LCOV_EXCL_LINE */
}
  • 在进入 z_cstart 之后,首先调用平台相关的内核初始化函数,然后使用模块自动初始化机制根据不同优先级调用相关初始化函数
  • 需要注意的是,同一优先级的模块之间不应该存在依赖关系,被依赖的模块应该先初始化。
  • 在调度器启动之前会调用优先级为 INIT_LEVEL_PRE_KERNEL_1 和 INIT_LEVEL_PRE_KERNEL_2 的初始化函数,此时调度器还未运行,因此这些函数中不应该使用操作系统提供的功能。
  • 在初始化完成之后通过将main线程添加到就绪队列中并开启调度器,main 线程从 bg_thread_main 函数开始运行。

main 线程

从裸机切换到main线程

void arch_switch_to_main_thread(struct k_thread *main_thread, char *stack_ptr,
				k_thread_entry_t _main)
{
	z_arm_prepare_switch_to_main();
	
	/* 将_current 指向 main_thread, _current 始终指向正在运行的线程 */
	_current = main_thread;

#if defined(CONFIG_THREAD_LOCAL_STORAGE) && defined(CONFIG_CPU_CORTEX_M)
	/* On Cortex-M, TLS uses a global variable as pointer to
	 * the thread local storage area. So this needs to point
	 * to the main thread's TLS area before switching to any
	 * thread for the first time, as the pointer is only set
	 * during context switching.
	 */
	extern uintptr_t z_arm_tls_ptr;

	z_arm_tls_ptr = main_thread->tls;
#endif

#ifdef CONFIG_INSTRUMENT_THREAD_SWITCHING
	z_thread_mark_switched_in();
#endif

	/* the ready queue cache already contains the main thread */

#if defined(CONFIG_MPU_STACK_GUARD) || defined(CONFIG_USERSPACE)
	/*
	 * If stack protection is enabled, make sure to set it
	 * before jumping to thread entry function
	 */
	z_arm_configure_dynamic_mpu_regions(main_thread);
#endif

#if defined(CONFIG_BUILTIN_STACK_GUARD)
	/* Set PSPLIM register for built-in stack guarding of main thread. */
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
	/* 堆栈限制寄存器分别限制 MSP 和 PSP 寄存器可以下降的程度,此处设置为main线程的栈起始地址 */
	__set_PSPLIM(main_thread->stack_info.start);
#else
#error "Built-in PSP limit checks not supported by HW"
#endif
#endif /* CONFIG_BUILTIN_STACK_GUARD */

	/* 设置PSP,然后开启中断,然后跳转到 z_thread_entry,z_thread_entry有四个参数
	 * 其中_main和stack_ptr作为调用 z_thread_entry 函数的前2个参数,后面两个参数为0
	 */
	__asm__ volatile (
	"mov   r0,  %0\n\t"	/* Store _main in R0 */
#if defined(CONFIG_CPU_CORTEX_M)
	"msr   PSP, %1\n\t"	/* __set_PSP(stack_ptr) */
#endif

	"movs r1, #0\n\t"
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE) \
	|| defined(CONFIG_ARMV7_R) \
	|| defined(CONFIG_AARCH32_ARMV8_R) \
	|| defined(CONFIG_ARMV7_A)
	"cpsie i\n\t"		/* __enable_irq() */
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
	"cpsie if\n\t"		/* __enable_irq(); __enable_fault_irq() */
	"msr   BASEPRI, r1\n\t"	/* __set_BASEPRI(0) */
#else
#error Unknown ARM architecture
#endif /* CONFIG_ARMV6_M_ARMV8_M_BASELINE */
	"isb\n\t"
	"movs r2, #0\n\t"
	"movs r3, #0\n\t"
	"bl z_thread_entry\n\t"	/* z_thread_entry(_main, 0, 0, 0); */
	:
	: "r" (_main), "r" (stack_ptr)
	: "r0" /* not to be overwritten by msr PSP, %1 */
	);

	CODE_UNREACHABLE;
}

Zephyr 线程入口

  • 在 main 线程启动之后,并没有直接运行main函数,首先会进入一个所有线程的入口函数 z_thread_entry,其第一个参数是线程入口,第二三四个参数是需要传递给线程的参数,如果线程结束时没有调用 k_thread_abrot 结束线程,最终会回到返回此处进行销毁。
  • 使用这种方式的好处就是即使用户没有删除线程,也不会造成系统崩溃,而是触发系统调用终止线程。
  • 在很多成熟操作系统中都会使用这样的方式启动线程。
FUNC_NORETURN void z_thread_entry(k_thread_entry_t entry,
				 void *p1, void *p2, void *p3)
{
#ifdef CONFIG_THREAD_LOCAL_STORAGE
	z_tls_current = z_current_get();
#endif
	entry(p1, p2, p3);

	k_thread_abort(k_current_get());

	/*
	 * Compiler can't tell that k_thread_abort() won't return and issues a
	 * warning unless we tell it that control never gets this far.
	 */

	CODE_UNREACHABLE; /* LCOV_EXCL_LINE */
}

用户线程入口

  • main 线程的入口函数为 bg_thread_main 函数, 因此 z_thread_entry 中的 entry 即为 bg_thread_main,该线程用于做一些必要的初始化工作。
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
	ARG_UNUSED(unused1);
	ARG_UNUSED(unused2);
	ARG_UNUSED(unused3);

#ifdef CONFIG_MMU
	/* Invoked here such that backing store or eviction algorithms may
	 * initialize kernel objects, and that all POST_KERNEL and later tasks
	 * may perform memory management tasks (except for z_phys_map() which
	 * is allowed at any time)
	 */
	z_mem_manage_init();
#endif /* CONFIG_MMU */
	z_sys_post_kernel = true;
	
	/* 调用优先级为INIT_LEVEL_POST_KERNEL的初始化函数,
	 * 此时内核已经开始运行,可以使用操作系统API
	 */
	z_sys_init_run_level(INIT_LEVEL_POST_KERNEL);
#if CONFIG_STACK_POINTER_RANDOM
	z_stack_adjust_initialized = 1;
#endif
	/* 从控制台输出系统启动标识 */
	boot_banner();

#if defined(CONFIG_CPP)
	/* 初始化CPP运行环境 */
	void z_cpp_init_static(void);
	z_cpp_init_static();
#endif

	/* 调用优先级为 INIT_LEVEL_APPLICATION 的初始化函数 */
	z_sys_init_run_level(INIT_LEVEL_APPLICATION);
	/* Zephyr支持静态创建线程,线程对应的信息在编译时确定,
	 * 随代码一起被编译到程序中,系统启动之后从对应地址将线程的信息从flash中读出,
	 * 创建并初始化线程并将其添加到就绪队列中等待操作系统调度。
	 */
	z_init_static_threads();

#ifdef CONFIG_KERNEL_COHERENCE
	__ASSERT_NO_MSG(arch_mem_coherent(&_kernel));
#endif

#ifdef CONFIG_SMP
	if (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) {
		z_smp_init();
	}
	z_sys_init_run_level(INIT_LEVEL_SMP);
#endif

#ifdef CONFIG_MMU
	z_mem_manage_boot_finish();
#endif /* CONFIG_MMU */

#ifdef CONFIG_CPP_MAIN
	extern int main(void);
#else
	extern void main(void);
#endif
	
	/* 跳转到main函数 */
	(void)main();

	/* Mark nonessential since main() has no more work to do */
	z_main_thread.base.user_options &= ~K_ESSENTIAL;

#ifdef CONFIG_COVERAGE_DUMP
	/* Dump coverage data once the main() has exited. */
	gcov_coverage_dump();
#endif
} 
  • 其中包含几个系统运行的重要的操作:
    • 配置MMU(如果存在)
    • 调用优先级为 INIT_LEVEL_POST_KERNEL 的初始化函数
    • CPP运行环境的初始化
    • 调用优先级为 INIT_LEVEL_APPLICATION 的初始化函数
    • 创建通过宏静态创建的线程,并添加到就绪队列。
    • 初始化对称多核处理(如果存在多个处理器并启用了多核调度)。
  • 在这些准备工作完成后跳转到用户编写的main函数中,如果main函数执行并返回,最终会返回到 z_thread_entry 被销毁。
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值