资讯动态

ohos liteos_m内核启动链路全解析:从复位向量到第一个任务

发布时间:2026/10/18 3:49:31 来源:尧图企业网站定制
第一次接触 ohos liteos_m 内核启动时我犯过一个所有嵌入式初学者都会犯的错以为内核启动就是把调试器连上、烧录、跑一个点灯 Demo然后看到 LED 亮就大功告成。直到后来我把 LiteOS-M 往一块只有 64KB Flash 的 MCU 上移植点灯死活不亮连调试器的初始断点都停在了一个莫名奇妙的汇编地址我才老老实实回到 reset 向量从第一条指令开始逐行读启动代码。这之后我才真正理解启动流程不是一个跳过也没关系的例行公事而是整个内核能否正确运行的根基。这篇文章我会完整拆解 ohos liteos_m 内核启动链路从上电复位后的向量表、链接脚本、启动汇编到 C 环境初始化、时钟与内存就绪再到调度器点火、第一个任务上 CPU最后加上我实际移植和排错中遇到过的几个典型坑。内容既适合刚想入门 OpenHarmony 设备开发的读者也适合已经在跑 LiteOS-M 但还没认真研究过启动细节的朋友。读完之后你再碰到板子起不来调度没反应进不了 main这类问题应该能有一个清晰的排查方向而不是全靠猜。1. 为什么 liteos_m 的启动流程值得单独拎出来拆解1.1 不搞清楚启动链路后面所有调试都是盲人摸象很多嵌入式开发者在写裸机程序时对启动流程并不陌生复位、初始化栈、清 BSS、跳 main。但内核场景比裸机复杂得多因为从第一条指令到你的业务任务开始运行中间穿插着一堆我们平时根本看不见的基础设施搭建向量表重定位、系统堆初始化、tick 时钟配置、调度器数据结构准备、第一个任务的上下文切换。如果这套流程中任何一环出了问题表现往往非常隐蔽。我见过最典型的例子是代码编译通过、烧录成功但程序跑起来后要么进不了 main要么进了 main 却卡死在某个while循环里要么内核初始化成功了调度器却永远切不到业务任务上。这些问题不回到启动链路去查光在业务代码层面找一辈子也找不到。LiteOS-M 作为 OpenHarmony 面向 MCU 场景的轻量内核它的启动流程并不算复杂但每一步都有明确的依赖关系。比如内存池没有初始化完成就不能创建任务SysTick 没有启动调度器的时间片和延时机制就是空的SVC/PendSV 的优先级配置不对任务切换就会在中断上下文里出问题。这些依赖关系只有一次性把启动链路通读一遍才能建立直觉。1.2 这篇文章能帮你获得什么这不是一篇泛泛的架构科普而是能跟着源码路径走的实战拆解。我会把从reset_vector到LOS_StartToRun的完整调用链梳理清楚同时把每个环节里为什么要这样做讲明白。比如链接脚本里那个放在 0 地址的向量表为什么必须是栈顶地址开头汇编启动代码为什么要在进入 C 世界之前关中断LOS_KernelInit到底初始化了哪些东西顺序能不能换LOS_StartToRun之后为什么就回不去了调度器启动的那一刻CPU 是怎么从跑启动代码切换到跑第一个任务的这些问题搞懂之后你再回头看 LiteOS-M 的启动过程就不是一串模糊的函数名而是一条有逻辑、有因果、可以推理的链条。即便你将来不是用 LiteOS-M而是换到其他 RTOS这套排查思路同样适用。2. 复位那一刻向量表、链接脚本与进入 C 世界的桥2.1 向量表不是给你看的数据是 CPU 的提线木偶线在 Cortex-M 架构上CPU 上电复位后并不是从什么main函数开始执行的而是由硬件直接从内存地址0x00000000读取初始栈指针MSP再从地址0x00000004读取复位向量也就是第一条要执行的指令地址。这个过程没有任何软件参与纯硬件行为。所以内核能否启动第一决定因素不是代码逻辑而是中断向量表是否正确躺在 Flash 起始位置。LiteOS-M 的向量表结构非常常规偏移 0 放栈顶地址偏移 4 放复位入口函数地址后面依次是 NMI、HardFault、各类外设中断入口。我读启动代码时有个特别深的感触嵌入式开发里的约定往往是硬性的。CPU 不看你的变量名、不看函数名就看向量表第 0 个词是不是一个合法 RAM 地址第 1 个词是不是一个合法的代码地址。如果链接脚本把向量表放错位置哪怕你的 C 代码写得再正确CPU 复位后的第一步就是跑飞。2.2 链接脚本里的三个关键符号栈顶、堆顶和入口地址既然 CPU 复位后直接从向量表拿栈顶和复位入口那么链接脚本就必须保证两件事第一向量表段被放置在目标 MCU 的起始 Flash 地址第二代表栈顶的符号、代表堆起始的符号都要在链接阶段正确计算出来。在 LiteOS-M 工程里链接脚本.ld 文件里通常会有几个非常关键的内存布局定义g_top_of_stack栈顶地址一般指向 RAM 段的最高地址。Cortex-M 的栈是向下生长的所以初始栈指针必须是 RAM 的最高地址加 1。g_sysMemStart和g_sysMemSize系统堆的起始地址与大小。LiteOS-M 在系统堆上实现动态内存分配任务栈、内核对象都从这块内存池里分配。__bss_start、__bss_endbss 段边界启动汇编靠这两个符号清零未初始化全局变量。我最早自己搭工程时犯过一个典型的错栈顶符号定义对了但 RAM 段里还放了一个比较大的全局缓冲导致栈顶被顶到了缓冲区的中间。程序刚启动没几步一压栈就把缓冲区数据踩掉了最后表现为初始化数据莫名错乱。后来排查时才意识到栈顶不只是个符号它必须严格等于 RAM 段末尾而不是某个近似值。2.3 汇编启动的四个动作关中断、设栈、清 BSS、跳转LiteOS-M 在 Cortex-M 上的启动汇编无论具体实现细节如何核心动作基本可以归纳为四步。我按执行顺序逐个说明。第一步关中断。常见写法是直接操作PRIMASK或者BASEPRI在进入 C 代码之前把全局中断屏蔽掉。原因很简单此时栈指针虽然设置了但中断向量表可能还没准备好全局变量还没初始化任何中断到来都会让 CPU 跳到一个不可预知的状态。先关中断让系统在受控且确定的环境里把基本环境搭好。第二步设置栈指针。虽然 CPU 复位时已经从向量表第 0 项拿到了初始 MSP 并自动设置好了但很多启动汇编出于保险考虑还是会显式地写一次栈顶地址因为后续可能需要切换到进程栈 PSP这里先把主栈指针钉稳。第三步清零 BSS 段。所有未初始化但默认值应为 0 的全局变量都在 BSS 段内核里很多标志位、链表头都依赖启动时全为 0这个前提。这一步通常用循环从__bss_start一直清到__bss_end。同时如果数据段要从 Flash 复制到 RAM也是在这个阶段完成的。第四步跳转 C 入口。对于 LiteOS-M 工程C 入口就是main函数。从这一步开始启动接管权从汇编转移到了 C 语言后面的世界就顺理成章多了。这里有一个容易忽略的细节跳转之前栈上不能有残留的临时数据因为 C 语言的函数调用约定要求入口时栈是干净的。如果启动汇编里有额外的压栈操作但忘记恢复平衡跳进 main 后第一句push可能就会异常。3. main 之前的世界时钟、内存与中断控制器的三方会诊3.1 进入 main 之后内核初始化是怎么抢跑的main函数在 LiteOS-M 工程里看起来很简单但短短几行背后是一系列精确排序的初始化。以典型工程为例main函数里通常只有三件大事硬件平台初始化、调用LOS_KernelInit初始化内核、调用LOS_StartToRun启动系统。LOS_KernelInit之所以是内核初始化的核心是因为它把内存、调度器、软件定时器、tick 时钟等模块按依赖顺序全部拉起来。在 LiteOS-M 的设计里这个函数内部会依次做这么几件事调用内存初始化相关逻辑把系统堆内存池建立起来。这个内存池是万事之母任务控制块、任务栈、信号量、互斥锁、消息队列全都要从这里分配。如果内存池没建立后面的创建动作都会失败。初始化调度器核心数据结构。比如就绪队列、任务排序链表、当前任务指针、空闲任务和软件定时器任务的占位。调度器数据结构的初始化必须是无运行时环境也能做的纯内存操作。创建空闲任务和软件定时器任务。这两个任务是内核自己走后门创建的不需要业务代码参与。配置 tick 相关资源。包括 SysTick 定时器的重装载值计算、中断优先级设置以及 tick 处理函数的注册。看起来只是几个函数调用但这里的先后顺序非常严格。比如软件定时器任务的创建依赖内存池已经初始化tick 中断的启动依赖调度器数据结构已经就绪任务切换机制的注册依赖向量表已经准备完整。任何一个步骤错位都可能让后面调度器启动时挂掉。3.2 系统堆是地基中的地基怎么来的在 LiteOS-M 里系统堆不是 malloc 出来的而是在链接阶段就划定的一块 RAM 区域由g_sysMemStart和g_sysMemSize这两个链接脚本符号告知内核。LOS_KernelInit内部的内存初始化逻辑拿到这两个地址后会在这块区域上构建一个内存池管理结构。为什么这块区域必须由链接脚本来定因为内核在启动初期还没有动态分配 RAM的能力它必须先有一个静态划定的合法 RAM 区域然后才能在这个区域上实现内存池的初始化和后续的动态分配。这有点像刚搬进新房子时你得先有一个固定的落脚点才能开始布置家具。实际工程中这块堆的大小往往就是.ld文件里 RAM 段减去栈大小、减去数据段和 BSS 段之后剩下的部分。如果你的业务任务特别多、任务栈特别大就需要在链接脚本里预留足够的堆空间。我见过不少移植项目启动后挂在LOS_MemInit附近本质原因就是链接脚本给堆留的地址越界了或者堆大小算成了负数。3.3 SysTick 优先级调度器能正常运转的隐形前提很多人在启动排查时会忽略一个角色SysTick 的中断优先级配置。在 Cortex-M 内核里SysTick 是一个内核级定时器它的中断号是固定的。LiteOS-M 启动时需要设置 SysTick 的重装载值决定每个 tick 的时间间隔同时还要设置它的中断优先级。如果 SysTick 优先级设置得不合适问题很隐蔽。比如SysTick 优先级被设成了 0最高优先级那么它就可以抢占几乎所有的临界区操作。调度器在修改就绪队列时通常要进入临界区保护如果 SysTick 中断随时插进来临界区保护就形同虚设最终可能导致任务队列状态错乱表现为系统跑一段时间就死掉而不是启动就死。我通常的做法是在启动早期就把 SysTick 的优先级设置为一个可以被核心管理操作屏蔽、但又能被普通外设中断打断的等级同时把 PendSV任务切换软中断的优先级设置得更低。这样 tick 中断负责计时PendSV 负责延迟到安全时刻再做任务切换各司其职。启动顺序上必须先配好优先级再使能中断次序反了就会在初始化过程中冒出一个提前的 tick 中断打乱一切。4. 调度器点火从裸奔到第一个任务的关键一瞬4.1 LOS_StartToRun 到底做了什么为什么是不可返回的当LOS_KernelInit完成初始化后main会调用LOS_StartToRun。从命名来看是开始运行但这个函数实际上代表了一个不可逆的转折点调用它之后CPU 的控制权将从主流程交给调度器本质上不会再回到main里这行调用之后继续执行。为什么不可返回因为在LOS_StartToRun内部调度器会记录当前 CPU 的上下文把当前执行流冻结下来然后从就绪队列中挑出最高优先级的任务把 CPU 上下文切换过去。这样一来CPU 的栈指针、PC、寄存器状态都变成了目标任务的内容。虽然从代码上看后续可能还有几行汇编在执行但那些汇编已经是在新任务的上下文中执行了。我在理解这个概念时用了一个生活类比想象你是一个电影院放映员你正在控制室里做最后检查这就是 main 主干流程当你启动自动放映系统后放映系统接管了一切你这个人就从放映员变成了最前排一个看客你的物理位置被换到了观众席。LOS_StartToRun之后的代码段本质上只是给你看的一个幻觉真正的执行权已经跑到任务上下文里去了。4.2 OsSchedStart 内部的链条从 tick 启动到上下文切换LOS_StartToRun内部真正干活的是调度器启动逻辑在 LiteOS-M 源码里通常体现为OsSchedStart等函数。它做的事情大致分成三段。第一段为任务切换准备硬件地基。包括确认当前使用的栈指针类型、配置任务切换所需的异常机制在 Cortex-M 上是 SVC 和 PendSV、确保 SysTick 已经配置好重装载值和中断优先级。第二段启动 tick 计时。这一步通常就是OsTickStart之类的手段让 SysTick 真正开始计数并使能中断。从这一刻起时间这个概念正式进入了系统后续任务的延时、时间片轮转、软件定时器都有依赖。第三段触发第一次上下文切换。调度器会从就绪队列中找出第一个准备就绪的任务通常是软件定时器任务或者是用户在main里创建的业务任务然后通过异常机制切换到该任务。切换完成后启动代码里的恶龙任务正式退场。很多人以为启动调度器就是简单的while(1)循环实际上不是。调度器启动后CPU 是一路从任务切换异常杀回到线程模式然后带着第一个任务的上下文从任务的入口函数开始跑的。这个过程里栈指针从一个特殊初始状态切换到了任务栈寄存器也从启动值换成了任务入口参数非常精妙。4.3 空闲任务与软件定时器任务内核自带的两条后手在LOS_KernelInit阶段LiteOS-M 会预先创建两个内核任务空闲任务和软件定时器任务。它们的创建时机必须早于LOS_StartToRun因为调度器一旦启动就要求就绪队列里至少有一个可以运行的实体。空闲任务的作用是纯兜底。它的优先级最低当所有业务任务都阻塞等待事件或延时到时CPU 才会切入空闲任务。空闲任务的循环里通常是对 CPU 低功耗模式的处理或者至少是一个不会返回的循环保证系统永不无任务可跑。软件定时器任务则承担了定时器回调的异步执行。它通常位于一个较高的优先级或者以独立任务的形式存在当 tick 触发软件定时器到期事件时内核会向这个任务发送信号然后由这个任务在执行上下文中调用用户的定时器回调函数。这保证了定时器回调不会在中断上下文里执行因为中断上下文里调用复杂的用户代码风险很高。这两个任务的创建细节决定了你在业务代码里能不能创建自己的任务。比如如果系统堆内存太小调度器还没跑起来空闲任务或软件定时器任务创建失败后来你就算调用LOS_TaskCreate也会因为内存不足而报错。所以启动早期任何一步失败最终都可能表现为业务任务创建失败这种看似毫不相关的问题。5. 移植 liteos_m 到新 MCU 时启动链路常见的 4 个坑5.1 启动就卡死在 OsTickInit 附近SysTick 优先级与中断使能顺序我第三次移植 LiteOS-M 时遇到一个现象代码跑进LOS_KernelInit后在 SysTick 相关初始化附近开始异常。起初我以为是时钟频率配置错了反复查了 RCC 分频系数但问题依旧。后来开了单步调试发现不是在配置 SysTick 重装载值时挂的而是在开中断之后一个 tick 中断立刻进来但此时调度器数据结构还未完全就绪中断服务程序访问了空指针。这个问题的根因是OsTickInit里先使能了 SysTick 中断而后面的调度器初始化工作还没完成。在大多数参考实现里初始化顺序通常会把配置 SysTick 但暂不使能中断和使能中断拆开确保所有数据结构都准备好后才开始让 tick 中断运行。如果你的移植版本把这两步合在一起或者你在自己的工程里提前使能了 SysTick就会出现类似崩溃。建议的做法是在启动阶段把 SysTick 的使能动作放在调度器启动的最后一段并确认 PendSV 的优先级配置已经就绪后再开中断。5.2 调度器启动了但永远跑不到业务任务任务优先级与 idle 任务的关系另一个高频问题是LOS_StartToRun调用成功了看起来没死系统似乎也在跑但断点打在业务任务入口却发现永远执行不到。这个大概率是因为你创建的业务任务优先级低于空闲任务或者任务在创建后没有正确进入就绪态。我之前在某个演示工程里因为抄参考代码时把任务优先级写成了 0而 LiteOS-M 的优先级数值通常越小优先级越高空闲任务又常被设定为最低优先级数值最大。如果业务任务优先级数值大于空闲任务那么调度器启动后只会运行空闲任务业务任务永远排不上。排查方法很简单在启动后、LOS_StartToRun之前打印或断点查看任务优先级与空闲任务优先级的比较同时确认LOS_TaskCreate的返回值是成功而不是因为栈分配失败被拒绝了。5.3 进不了 main 函数链接脚本入口符号与向量表段放置位置第四种常见坑是程序烧录后完全没有任何反应调试器复位后 PC 停在 0 或一个奇怪地址。这种问题基本可以直接锁定到向量表或链接脚本而不是 C 代码逻辑。通常要检查两点。第一向量表段是否被链接到芯片指定的起始地址。很多 MCU 默认从 0x08000000 启动 Flash如果你的链接脚本把向量表放到了其他位置CPU 复位后读到的就是空白地址。第二向量表第 0 项是不是有效的栈顶。如果栈顶被链接到一个不存在或压缩过的地址CPU 复位后第一步设置 MSP 就会异常。我加了个小技巧在启动汇编最开头放一条指令设置一个固定的特殊寄存器值如果调试器复位后看到这个寄存器被正确赋值说明入口执行到了如果 PC 直接飞掉那问题绝对在向量表与链接脚本不用浪费时间去查 C 代码。5.4 内存初始化直接挂掉RAM 段布局与系统堆边界越界最后这个坑最隐蔽也最致命。因为它的表现往往不是立刻死机而是系统跑一段时间后随机崩溃。比如某个任务的局部变量把相邻堆内存的内容覆盖了表现为内核的链表节点指针被莫名改写。这类问题的根源就是链接脚本中栈、堆、数据段、BSS 段的布局没有做好隔离尤其是栈和堆的边界重叠。当我把系统堆设成一个固定大小后任务栈一旦分配得过大堆内存就会向栈方向扩张反过来主栈还在往下生长两边就在某个随机时刻撞在一起。解决这个问题的思路是在链接脚本里给主栈和系统堆之间留下足够的安全缓冲并且在启动早期用一个简单的哨兵模式内存检测周期性检查栈顶附近的值有没有被踩坏。对于 liteos_m 这类 MCU 内核内存本来就是稀缺资源但宁可牺牲一点点堆大小也要保证栈与堆的边界清晰否则排错成本会成倍增加。结尾一点补充与后续建议如果让我总结这段研究历程里最有价值的心得那就是任何内核启动问题都不要先怀疑别人写好的启动文件而是先自己把启动链路完整走一遍确认每一环的输入输出是否符合预期。内核启动就像一串多米诺骨牌你花一小时看懂第一块骨牌是怎么倒下去的后面所有骨牌的问题都能顺藤摸瓜找到。最后再分享一个小技巧调试启动代码时不要只在 main 打断点。在复位向量入口、LOS_KernelInit入口、LOS_StartToRun入口分别设置断点然后记录每个断点是否命中、命中的顺序如何这一下就能把问题范围缩小到具体环节。如果你刚刚开始接触 ohos liteos_m 的内核启动拿一块开发板、打开反汇编窗口从第一条指令单步走一遍收获会比读十篇文章都大。后面如果卡在任务切换细节上再继续深挖 PendSV 的上下文切换机制那又是一片全新的知识领域。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑