简介面向嵌入式开发者的华为LiteOS移植到STM32F103ZET6的工程资源适合希望在Cortex-M3平台上快速构建低功耗物联网应用的工程师与学习者。资源围绕内核配置、中断服务、硬件初始化、任务创建、时钟源设置等关键移植环节提供了完整可参考的工程实现。压缩包共506个文件大小约8.49MB以.c/.h源码、.o/.d等编译中间文件及.s汇编文件为主并包含工程配置与文档说明便于直接查阅和二次开发。已有1840人学习通过该资源可以直观了解LiteOS在STM32上的启动流程与任务调度机制掌握LED控制任务的创建与调试方法为后续使用信号量、消息队列等RTOS特性打下基础。1. 为什么值得把 LiteOS 移植到 STM32F103ZET6前几天帮 A 同学调一个联网采集设备发现裸机 while(1) 轮询已经扛不住了按键扫描、OLED 刷新、串口解析、传感器读取全挤在一个死循环里任何一个外设稍慢点整机响应就乱套。这时候最省事的方案就是上一套 RTOS而当时手里现成的工程正是 LiteOS 移植到 STM32F103ZET6 的资源包。你先不用纠结它和 FreeRTOS、RT-Thread 谁好——对于 512KB Flash、64KB RAM 的 F103ZET6 来说LiteOS 内核足够精简任务调度、信号量、消息队列、事件标志全都是现成的官方仓库里的目标工程改一改就能跑。这篇我直接把整个移植过程拆开从源码目录结构、时钟与 SysTick 配置、链接脚本裁剪到创建第一个双任务 Demo再把我踩过的五个坑列出来。整套流程走完你手里就是一个能稳定跑 5~6 个任务的工程底座。2. 移植前的准备源码目录结构与工程选型2.1 LiteOS 内核源码怎么组织先看懂三个目录拿到 LiteOS 移植资源包以后第一步不是急着建工程而是把源码层级摸清楚。解压以后核心目录就那么几个我一般会对照着看liteos ├── kernel │ ├── include │ ├── src │ └── los_config.h ├── arch │ └── arm │ ├── cortex-m4或 cortex-m3 │ ├── common │ └── include ├── targets │ ├── STM32F103ZET6 │ │ ├── keil工程文件 │ │ ├── icfIAR链接脚本可选 │ │ └── los_startup.s启动汇编 └── components ├── shell ├── fs └── net说明一下这里的逻辑kernel目录是内核本体负责任务、调度、内存管理的源码都在里面arch目录放的是架构相关代码F103 是 Cortex-M3/M4 内核主要用中断上下文切换、调度汇编那一套targets目录是具体芯片的板级工程里面有现成的 Keil 工程模板。我一般会直接在targets/STM32F103ZET6这个目录上改而不是从零组织工程文件。原因很现实官方已经帮你把 90% 的头文件路径、启动文件、链接脚本配好了你自己从空工程开始逐个添加大概率会栽在编译器的头文件搜索路径和宏定义上。2.2 选哪个 targets 做底子同系列芯片优先如果你下载的资源包里没有正好对应 F103ZET6 的 targets就找一个同内核的芯片工程做底子。比如 F103 是 Cortex-M3那么targets里只要是 Cortex-M3 的工程理论上一半都能直接借用来改。常见做法是这么对照的核对arch/arm下面的目录名确认是cortex-m3系列寄存器层面的中断上下文保存代码可以直接用检查los_config.h的LOSCFG_BASE_CORE_TICK这是系统节拍F103 通常用 SysTick值设成LOSCFG_BASE_CORE_TICK_RATE常见 1000表示 1ms 一个 tick确认LOSCFG_BASE_MEM_NODE_SIZE和系统堆大小能放进 64KB RAM 里再说。这里很容易踩一个心理坑觉得 exFAT 文件系统、网络协议栈这些组件模块加上去更“完整”。但 F103ZET6 的 RAM 只有 64KB你把components/shell、components/net全勾上一个任务的栈分配都会变得捉襟见肘。我一般只保留kernel、arch、targets三块加一个shell调试用其他组件等移植通了再按需加。2.3 工程文件必须准备齐全的三样东西移植到 F103ZET6 时你不只需内核源码还需要一块板级支撑代码。这三样东西决定能否一次编译通过启动文件startup_stm32f103xe.s负责初始化堆栈、向量表并把Reset_Handler指到SystemInit和__main。很多移植工程编译能过但一上电就跑飞就是启动文件的向量表长度和 103ZET6 的中断数量不匹配。链接脚本stm32f103zet6_keil.sctKeil 下或.ldGCC 下Flash 512KB、RAM 64KB 的分布以及内核堆栈区域怎么放全看它。底层驱动USART1串口初始化后面跑任务、打印日志都靠它。我不主张首次移植就引入 HAL 库或标准外设库的大文件包。F103 的串口、GPIO 初始化也就二三十行寄存器操作手写一个精简版足够用。你先让内核跑起来再考虑把这些板级代码整合进更抽象的驱动框架。3. 三个关键移植点时钟、SysTick、链接脚本3.1 时钟配置SystemInit 先把 PLL 拉到 72MHzLiteOS 本身不关心你用什么外设库它只关心los_config.h里配置的时钟节拍是从哪儿来的。但 F103ZET6 的默认内部 8MHz 时钟如果不倍频外设 UART 波特率、SysTick 都会不准所以我第一步先把系统时钟配到 72MHz。用寄存器操作的方式代码会清晰很多void SystemInit(void) { /* 使能外部高速晶振 HSE */ RCC-CR | RCC_CR_HSEON; /* 等待 HSE 就绪一般要几十微秒 */ while ((RCC-CR RCC_CR_HSERDY) 0) {;} /* 打开 FLASH 预取缓冲并设置等待周期为 272MHz 下需要 */ FLASH-ACR | FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2; /* 配置 PLLHSE 8MHz9 倍频得 72MHz */ RCC-CFGR ~RCC_CFGR_PLLSRC; RCC-CFGR | RCC_CFGR_PLLSRC_HSE | (9UL 8); /* PLLMUL 设置为 9 */ /* 使能 PLL 并等待锁定 */ RCC-CR | RCC_CR_PLLON; while ((RCC-CR RCC_CR_PLLRDY) 0) {;} /* AHB、APB1、APB2 预分频APB1 最高 36MHz */ RCC-CFGR | RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV1; /* 切换系统时钟到 PLL */ RCC-CFGR | RCC_CFGR_SW_PLL; while ((RCC-CFGR RCC_CFGR_SWS_PLL) 0) {;} }这段代码的逻辑是先把外部晶振打开等待它稳定然后配置 PLL 的倍频系数PLLMUL为 98MHz x 9 72MHz。接着把总线时钟分频配置好APB1 上外设不要超过 36MHzAPB2 可以维持 72MHz。最后把系统时钟源切到 PLL。需要注意FLASH_ACR_LATENCY_2这一项。F103 在 72MHz 时 Flash 读取必须至少 2 个等待周期否则程序会随机跑飞而且这个 bug 属于“有时跑几小时才出现一次”的玄学问题。你在移植 LiteOS 之前先单独跑一个点灯程序验证时钟是 72MHz再往下走。3.2 SysTickLiteOS 的心跳来源LiteOS 的时间片轮转依赖一个周期性的 tick 中断。F103 上最标准的选择是 SysTick。需要你在los_config.h里确认宏定义并在main初始化时把 SysTick 配置成 1ms 触发一次#define LOSCFG_BASE_CORE_TICK_RATE 1000 /* 1ms 中断一次 */ #define LOSCFG_BASE_CORE_TICK_HW_TIME 0 /* 不使用硬件定时器作为 tick 源 */然后在主程序里做初始化void SysTick_Init(void) { /* 72MHz 时钟重装载值 72000000 / 1000 - 1 71999 */ SysTick-LOAD (uint32_t)(SystemCoreClock / LOSCFG_BASE_CORE_TICK_RATE) - 1UL; SysTick-VAL 0UL; /* 选择内核时钟源开启中断使能计数器 */ SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; }参数说明SystemCoreClock在 3.1 节配置成功后会变成72000000LOAD写 71999 表示每 72000 个时钟周期产生一次中断正好 1ms。TICKINT必须置位否则 tick 中断不会触发LiteOS 调度器会直接瘫痪。还有一个细节容易坑到人中断优先级的设置。LiteOS 的任务切换是在 PendSV 里做的SysTick 的优先级需要高于数值上小于PendSV。你最好在NVIC_SetPriority(SysTick_IRQn, 0xF)这种代码里把它设为最低优先级之一而 PendSV 设置为0xFF这样能避免 tick 打断任务切换造成的不可预知竞争。3.3 链接脚本与内存布局给内核和任务栈留出空间F103ZET6 的 RAM 有 64KB听起来不小但 LiteOS 内核初始化、任务栈、系统堆全都要从这里出。链接脚本里需要明确三段区域全局变量区.data 和 .bss标准启动流程会初始化内核堆区供LOS_MemInit使用的动态内存池任务栈区每个任务栈从系统堆中动态分配。我用 Keil 的分散加载文件做示范直接在targets工程里改.sctLR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (RW ZI) } }这张内存布局图里0x08000000的 512KB 是 Flash0x20000000的 64KB 是 RAM。如果链接脚本的 RAM 长度写小了比如默认只有 20KB没有改成 0x10000内核一初始化就会因为数组越界访问产生 HardFault。我踩过一次这种坑任务刚创建还没跑就死在LOS_MemInit的memset里。更隐蔽的是栈顶地址。startup_stm32f103xe.s里第一条指令会把0x20000000 0x10000这个地址加载到 SP如果链接脚本的 RAM 区域和启动文件里的栈顶不一致设备上电第一跳就废了。所以每次检查链接脚本顺手看一下启动文件里Stack_Size EQU 0x1000和Heap_Size EQU 0x200是否合理。内核动态内存不需要在启动文件里预留太多Heap 主要给自己写的中间层 malloc 用建议先保持 0x200跑起来以后再看。4. 把第一个任务跑起来创建、调度、串口验证4.1 创建两个 LED 翻转任务结构体与参数说明时钟、SysTick、链接脚本都就绪以后就可以写第一个 LiteOS Demo。拿两个 LED 任务验证调度最直观。任务代码我习惯放在单独的文件app_task.c里#include los_task.h #include los_sys.h #include los_typedef.h #include los_compiler.h #include los_config.h #include bsp_gpio.h /* 一个任务的执行体死循环 延时让出 CPU */ static VOID Task1_Entry(VOID *param) { (void)param; while (1) { GPIO_WriteBit(GPIOB, GPIO_Pin_0, 0); /* PB0 输出低点亮 LED */ LOS_TaskDelay(500); /* 延时 500ms让出 CPU */ GPIO_WriteBit(GPIOB, GPIO_Pin_0, 1); LOS_TaskDelay(500); } } static VOID Task2_Entry(VOID *param) { (void)param; while (1) { GPIO_WriteBit(GPIOB, GPIO_Pin_1, 0); /* PB1 输出低点亮另一颗 LED */ LOS_TaskDelay(300); GPIO_WriteBit(GPIOB, GPIO_Pin_1, 1); LOS_TaskDelay(300); } } UINT32 AppTask_Create(VOID) { UINT32 task1Id, task2Id; TSK_INIT_PARAM_S task1 { 0 }; TSK_INIT_PARAM_S task2 { 0 }; task1.pfnTaskEntry (TSK_ENTRY_FUNC)Task1_Entry; task1.uwStackSize 1024; /* 任务栈大小单位字节 */ task1.pcName Task1; task1.usTaskPrio 5; /* 数值越小优先级越高 */ task1.uwArg 0; task2.pfnTaskEntry (TSK_ENTRY_FUNC)Task2_Entry; task2.uwStackSize 1024; task2.pcName Task2; task2.usTaskPrio 6; /* Task2 比 Task1 优先级低 */ task2.uwArg 0; if (LOS_TaskCreate(task1Id, task1) ! LOS_OK) { return LOS_NOK; } if (LOS_TaskCreate(task2Id, task2) ! LOS_OK) { return LOS_NOK; } return LOS_OK; }参数说明uwStackSize不是越大越好F103ZET6 的 RAM 只有 64KB每个任务的栈默认建议 1KB~2KB。1024 字节对简单 LED 翻转足够但如果任务里有snprintf之类带缓冲的库函数调用建议给 2048 字节以上。usTaskPrio的数值 0~31越小优先级越高两个任务写 5 和 6 就能看出优先级抢占的效果。LOS_TaskCreate成功后返回一个任务 ID后面做删除或挂起操作时要用。4.2 main 函数流程KernelInit 到 Start 的正确顺序很多移植失败的案列是初始化顺序反了先创建任务、再初始化内核结果任务创建接口返回错误。LiteOS 的标准流程是这样的int main(void) { /* 第一步基础硬件初始化包括时钟、串口、GPIO */ SystemInit(); SysTick_Init(); USART1_Init(115200); GPIO_Init(); /* 第二步初始化 LiteOS 内核包括动态内存和各模块 */ UINT32 ret LOS_KernelInit(); if (ret ! LOS_OK) { return -1; } /* 第三步内核初始化完成后才能创建任务 */ if (AppTask_Create() ! LOS_OK) { return -1; } /* 第四步启动调度这一步不会返回 */ LOS_Start(); /* 理论上走不到这里 */ return 0; }这里每一步的逻辑是LOS_KernelInit负责把内核的全局数据、任务池就绪表、内存池都准备好返回后系统里还没有任何用户任务但内核对象已经可以创建了。AppTask_Create在这个时间点调用才能成功给新任务分配栈空间。LOS_Start会把当前上下文交给调度器从第一个已就绪的任务开始执行函数不会返回。4.3 用串口打印验证任务切换重定向 printfLED 翻转只能看到两路正弦闪烁想看任务切换的具体节奏还是得靠串口。常见做法是重定向fputc到 USART1然后直接用printf#include stdio.h /* 重定向 printf 到串口1 */ int fputc(int ch, FILE *f) { (void)f; /* 等发送寄存器空 */ while ((USART1-SR USART_SR_TXE) 0) {;} USART1-DR (uint8_t)ch; return ch; }这个函数在 Keil 的 MicroLIB 环境下会被 printf 内部调用。需要注意F103 的USART1-DR是一个 16 位寄存器只写低 8 位。如果你在用标准库而非 MicroLIB多半还要额外处理半主机模式问题启动文件里要多一段__use_no_semihosting的代码否则 printf 会直接卡死。我在验证调度时一般在任务切换的地方打印任务名和时间戳#ifdef LOSCFG_SHELL printf([%s] running, tick %u\r\n, task-pcName, LOS_TickCountGet()); #endif这里的LOS_TickCountGet()返回系统启动以来的 tick 数从打印结果里能清晰看到 Task1 和 Task2 的切换间隔是不是符合 500ms / 300ms 的延时预期。5. 避坑与常见问题移植中最容易翻车的五个地方5.1 任务卡死在一个 while 循环里另一个任务永远不执行现象两个 LED 任务创建后只有一个 LED 闪烁另一个完全不动或者打印日志卡在同一个任务的任务体内。原因SysTick 中断根本没有触发。很多工程从官方 example 拷贝代码时SysTick-CTRL配置没有把TICKINT置位或者 SysTick 在启动汇编里已经被其他初始化函数改写了。另一种常见情况是你在中断服务函数里没有调用LOS_SysTickHandler()tick 计数一直不变时间片轮转无从谈起。解决确认SysTick_Init()里CTRL的 bit1 为 1然后在启动文件或中断表里加一行void SysTick_Handler(void) { LOS_SysTickHandler(); }加上以后在LOS_SysTickHandler里打断点如果能进来就说明 tick 已经跑起来问题转到调度优先级方向。5.2 一上电就 HardFault死在 LOS_MemInit 的 memset现象程序能编译下载后立刻进入硬件错误中断调试器指向LOS_MemInit里的内存清零循环。原因我在 3.3 节提过的链接脚本 RAM 区域和实际芯片不匹配。F103ZET6 是 64KB RAM如果链接脚本里RW_IRAM1只写了 0x0000500020KB内核初始化时访问超出地址的内存直接触发硬件异常。解决把链接脚本里 RAM 长度改成0x00010000同时检查启动文件里Stack_Size和Heap_Size的总和不要超过 64KB。我一般还会把LOSCFG_BASE_MEM_POOL_SIZE显示设置成系统堆的大小比如 8KB避免内核动态内存池和用户全局变量产生重叠。5.3 任务能跑但串口 printf 输出乱码或卡死现象任务切换正常LED 闪烁节奏也对但串口助手收到的全是乱码或者一旦调用 printf整个任务就卡住。原因第一层是波特率不匹配SystemInit倍频到 72MHz 后USART1_Init里的波特率寄存器需要重新计算第二层是半主机模式问题Keil 工程默认使用微库但你把 printf 重定向的fputc写进标准库的坑里。最常见的翻车点是 syscall 文件缺失标准库 printf 会调用_sys_*系列函数。解决工程里添加下面这段代码禁用半主机模式#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x x; } void _ttywrch(int ch) { (void)ch; }加上以后fputc的调用路径就不再依赖调试器了。另外USART1 的波特率寄存器在 72MHz 下算出的值是0x1D给到 115200 波特率不同工具算出来的误差不要超过 2%否则在 115200 波特率下容易偶发乱码。5.4 任务栈分配 2048 字节后LOS_MemInit 失败现象任务创建时返回LOS_NOK错误码指向内存不足或者系统跑着跑着突然在某个任务里申请内存时触发断言。原因64KB RAM 里内核自己的全局对象、空闲任务栈、中断栈和动态内存池都要占用空间。如果你只给内存池留了 4KB进来两个 2KB 栈的任务就把它吃光了。RTOS 的栈空间不是“任务定义多大就占多大”空闲任务和软中断任务还会额外消耗内存。解决把LOSCFG_BASE_MEM_POOL_SIZE调到足够容纳空闲任务和你的业务任务。F103ZET6 上我一般预留 16KB~24KB 的内存池同时把任务栈尽量压小——能 1024 绝不用 2048能用静态分配就不用动态内存。调试阶段你可以用LOS_MemUsed打印当前内存使用量printf(mem used: %d bytes\r\n, LOS_MemUsed(m_aucSysMem0));这个打印能帮你看清每个任务到底真实占用了多少栈空间很多任务栈“看起来”512 字节够用实际一跑下来因为层层调用局部变量数组实际能超过 900 字节。5.5 中断服务函数里调用 LOS_TaskDelay系统直接复位现象定时器中断或者外部中断的 ISR 里尝试调用LOS_TaskDelay或LOS_TaskYield程序立即跑飞。原因LiteOS 调度时基于任务上下文切换你在中断上下文里调用会让调度器状态混乱。这是一个设计层面最常见的误用不是 bug。解决中断里只做标记位设置或释放信号量由普通任务去等待这个信号量后执行耗时操作。比如用LOS_SemPost在 ISR 里唤醒任务任务侧用LOS_SemPend阻塞等待。这样实现的中断通知最稳定不会碰调度器的红线。6. 再进一步用 shell 命令观察任务状态顺手调内存池任务跑通之后移植工作其实只完成了一半。我一直建议在los_config.h里把LOSCFG_SHELL打开这样你能在串口命令行里直接敲task、mem之类的命令查看实时状态。这个能力在排查优先级反转和栈溢出时非常好用任务一卡死顺手上位机发条task命令就能看到哪个任务停留在什么状态。shell 的开法一般在系统初始化时追加一行#ifdef LOSCFG_SHELL UINT32 shellRet LOS_ShellInit(); if (shellRet ! LOS_OK) { printf(shell init failed\r\n); } #endif初始化成功以后再在任务循环里把串口收到的字节喂给 shellF103 上我用最简洁的方式处理void ShellTask(void) { char ch; while (1) { ch USART1_ReceiveByte(); /* 阻塞接收一个字节 */ LOS_ShellInputMsg(ch, 1); } }用task命令能看到每个任务的优先级、状态就绪 / 阻塞 / 挂起、栈使用率。这一步强烈建议新入门的读者跑一次比你 Debug 单步看几百次代码都直观。内存池的大小同样可以通过 shell 验证。F103ZET6 的资源限制摆在那里你不可能像在 Linux 上那样放任内存。我常用这样一组参数做起始配置配置项推荐值说明LOSCFG_BASE_MEM_POOL_SIZE16KB内核动态内存池含任务栈LOSCFG_BASE_TASK_TSK_STACK_SIZE512B默认任务栈大小仅Go裸任务测试用业务任务栈1024B简单逻辑够用涉及 printf 建议 2KBLOSCFG_BASE_CORE_TICK_RATE10001ms 调度节拍平衡功耗和实时性如果后续你的业务任务开始涉及大数组、浮点运算或者 JSON 解析任务栈起步就该放 2KB内存池相应提升到 24KB 左右。F103ZET6 的 64KB RAM 装这些仍然够但你可能得砍掉一部分 no.1 全局缓冲区。从那以后我每次做完移植都会强制走一遍 shell 查询任务状态的流程看一遍各任务的栈水位和内存池剩余量再决定是不是要调参。移植 RTOS 不像点个灯那样一把过真正的调试功夫都花在资源计算和中断优先级这种看不见的地方。希望这篇能帮你少走几趟弯路。本文还有配套的精品资源点击获取