资讯动态

LiteOS移植STM32F103ZET6:启动、SysTick与内存池调优

发布时间:2026/10/23 12:19:25 来源:尧图企业网站定制
简介这份资源记录了华为LiteOS轻量级物联网操作系统移植到STM32F103ZET6Cortex-M3内核微控制器的完整工程面向已掌握标准库外设驱动、希望从裸机开发进阶到RTOS的嵌入式学习者。内容围绕内核参数与任务栈配置、中断服务程序与内核的协同、时钟源与预分频选择、首个LED任务的创建与调度展开可用于理解LiteOS的任务管理、信号量与消息队列机制。压缩包共506个文件、约8.49MB其中115个头文件与79个C源文件构成内核与芯片外设驱动主体另有30个汇编启动文件、13个makefile及Keil工程、axf与hex等编译产物并附一份docx说明便于直接编译验证与二次裁剪。目录保持了原始工程结构驱动源码、头文件与配置文件分层清晰方便按模块对照阅读。目前已有1842人学习下载适合作为对照移植流程、排查调度与中断异常的基础模板。1. 从裸机到内核LiteOS 移植到 STM32F103ZET6 先盯启动与节拍把 LiteOS 移植到 STM32F103ZET6最容易让人误判的是“编译通过就等于移植完成”。我在一块 64KB SRAM 的 F103ZET6 板上见过太多次串口能打印、任务也创建了跑几分钟后 HardFault最后追到 SysTick 被裸机延时函数重复配置。这个场景的核心不是把内核源码拖进工程而是把启动文件、时钟节拍、中断优先级和内存池四件事对齐。适合已经能点灯、能串口输出准备把裸机循环改成任务调度的从业者也适合手里有 LiteOS 内核源码包、想先跑最小闭环再裁剪的人。先别急着开队列和文件系统把 tick 跑稳比什么都值钱。下面这套顺序按“工程骨架—裁剪配置—最小业务—排错—稳定性”推照着做能少走几晚弯路。2. 工程骨架与启动链路把 LiteOS 塞进 512KB Flash 的最小闭环2.1 目录分层与芯片启动文件对接移植第一件事不是改内核而是把目录切开。内核源码、芯片启动文件、板级配置、应用任务混在一个文件夹里后面查 map 文件会很痛苦。我一般按下面这种分层放kernel尽量保持原样arch只放 Cortex-M3 相关移植层target放los_config.h、时钟、串口和中断入口app只放业务任务。LiteOS_STM32F103/ ├── app/ │ ├── main.c │ └── app_task.c ├── kernel/ │ ├── base/ │ ├── extended/ │ └── include/ ├── arch/ │ └── arm/cortex-m3/ │ ├── los_hw.c │ ├── los_hwi.c │ └── los_timer.c ├── target/ │ ├── los_config.h │ ├── board.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── drivers/ └── startup/ └── startup_stm32f103xe.s这段目录结构的关键在边界arch里的文件负责上下文切换、中断接管和 tick 入口target里的文件负责“这块板子怎么跑起来”。启动文件要选高密度型号对应的startup_stm32f103xe.s不要拿中密度或小容量型号的启动文件凑合。STM32F103ZET6 是 512KB Flash、64KB SRAM启动文件里的向量表数量和链接脚本的 Flash/RAM 长度必须与芯片匹配否则链接能过运行到某个中断就飞。参数上先记两个数Flash 512KBSRAM 64KB。后面所有裁剪和内存池都围绕 64KB 做账。启动文件里的Stack_Size只代表主栈和中断栈不是 LiteOS 任务栈任务栈从内核内存池里分配这两个概念混了后面一定翻车。第一次移植先把Heap_Size设成 0C 库堆先不用避免和内核内存池抢同一段 SRAM。2.2 时钟、SysTick 与中断向量重映射F103ZET6 常见做法是外部 8MHz 晶振经 PLL 九倍频到 72MHz。裸机工程里可能已经有SystemInit移植时不要又在Board_ClockInit里配一遍重复初始化 HSE 会导致起振等待卡死。我一般把时钟配置收口到一个函数裸机点灯和内核启动都调它串口波特率也按 72MHz 算。/* target/board.c72MHz 主频HSE 8MHzPLL x9 */ void Board_ClockInit(void) { RCC-CR | RCC_CR_HSEON; while ((RCC-CR RCC_CR_HSERDY) 0) { /* 实际工程加超时计数避免晶振不起振时死等 */ } RCC-CFGR (RCC-CFGR ~RCC_CFGR_PLLMULL) | RCC_CFGR_PLLMULL9; RCC-CFGR | RCC_CFGR_PLLSRC; RCC-CR | RCC_CR_PLLON; while ((RCC-CR RCC_CR_PLLRDY) 0) {} RCC-CFGR (RCC-CFGR ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL; while ((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL) {} } void Board_SysTickInit(void) { /* 100Hz72MHz / 100 72000024 位重装值上限内 */ SysTick-LOAD 720000U - 1U; SysTick-VAL 0U; SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; }逻辑说明先切 PLL 再切系统时钟最后才开 SysTick。参数里720000U - 1U对应 100Hz 节拍也就是一个 tick 10ms如果后面把LOSCFG_BASE_CORE_TICK_PER_SECOND改成 1000重装值要同步改成 72000不能只改配置宏。24 位重装值最大 1677721572MHz 下最低能跑到四十多赫兹再低就要用其他定时器分频别硬压 SysTick。中断向量表也要确认。直接从 Flash 启动时SCB-VTOR指向应用起始地址如果工程前面有 bootloader地址要改成应用偏移否则中断会跳到旧向量表。/* target/board.c确认向量表基址bootloader 场景要改成应用偏移 */ void Board_VectorInit(void) { SCB-VTOR 0x08000000U; /* 从 Flash 直接启动时按链接地址对齐 */ }这段代码解决的是“程序能跑但中断进错函数”的问题。参数只有一个基址但坑在链接地址如果链接脚本把应用放到0x08008000这里还写0x08000000串口中断和 SysTick 都会乱。移植时先看链接脚本的起始地址再决定这个值别凭印象填。2.3 第一次编译链接从 map 文件看 RAM 是否溢出第一次链接不要只看“0 Error”。打开 map 文件看三块向量表、.data/.bss、主栈。启动文件里的栈先给 2KB堆给 0等内核跑稳再谈动态分配。/* startup_stm32f103xe.s先给主栈和中断栈留 2KBC 库堆暂时关闭 */ Stack_Size EQU 0x00000800 Heap_Size EQU 0x00000000Stack_Size是主栈不是任务栈0x800等于 2KB对最小闭环够用。Heap_Size设 0 是为了避免malloc和 LiteOS 内存池抢地址。后面如果串口打印用了printf先确认没有大数组落在栈上否则 2KB 主栈也会被压穿。编译完用 size 工具看一眼arm-none-eabi-size build/app.elf输出里text是 Flash 占用data是初始化数据bss是零初始化区。64KB SRAM 的账要这样算data bss 主栈 内核内存池 外设缓冲不能超过 64KB。第一次移植把内核内存池先给 16KB 到 20KB任务栈每个 1KB先跑两个任务。map 文件里如果看到某个大数组悄悄进了.bss先把它挪走或改小再继续调内核。3. 内核裁剪与节拍配置72MHz 下 tick 不跳的排查思路3.1 los_config.h 里真正影响 RAM 的开关LiteOS 的配置头文件是移植期最容易改出问题的地方。第一次不要全开先把任务、信号量、内存管理、中断和打印留下队列、互斥锁、软件定时器按需再开。下面是一组最小闭环常用开关具体宏名以手里源码包的los_config.h为准但思路一样先关大模块再逐个打开。/* target/los_config.h最小闭环配置先跑通再逐项打开 */ #define LOSCFG_BASE_CORE_TSK_LIMIT 8 #define LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE 0x400 #define LOSCFG_BASE_CORE_TICK_PER_SECOND 100 #define LOSCFG_BASE_IPC_SEM 1 #define LOSCFG_BASE_IPC_MUX 0 #define LOSCFG_BASE_IPC_QUEUE 0 #define LOSCFG_MEMORY_BESTFIT 1 #define LOSCFG_MEMORY_POOL_EXTERNAL 0 #define LOSCFG_PLATFORM_HWI 1 #define LOSCFG_KERNEL_PRINT 1TSK_LIMIT设 8 表示最多 8 个任务不是必须创建 8 个每个任务默认栈 1KB8 个全用就是 8KB。TICK_PER_SECOND设 100配合 2.2 的 720000 重装值。SEM先开因为串口收帧和任务同步用得上MUX和QUEUE先关少一份内核对象就少一份 RAM 和初始化路径。LOSCFG_MEMORY_POOL_EXTERNAL先关用内部内存池跑通后面再考虑外部 SRAM。参数怎么改任务多了先加TSK_LIMIT但每个任务栈按实际水位给不要一律 1KB。串口打印开了以后printf的缓冲和格式化栈会吃 RAM最好改成短格式输出。配置宏改完要全量重编不要只编一个文件内核很多条件编译藏在头文件里。3.2 SysTick 重装值与优先级分组tick 不跳十有八九是中断入口被裸机工程占着或者优先级高于内核临界区。F103 是 Cortex-M3中断优先级数值越大抢占优先级越低。常见做法是把 SysTick 和 PendSV 设到最低抢占优先级SVC 稍高保证内核临界区不被节拍打断。/* target/board.c中断优先级数值越大抢占优先级越低 */ void Board_IrqPriorityInit(void) { NVIC_SetPriority(SysTick_IRQn, 0x0F); NVIC_SetPriority(PendSV_IRQn, 0x0F); NVIC_SetPriority(SVC_IRQn, 0x00); }逻辑说明SysTick 只负责推进节拍PendSV 负责任务切换两者都不应该压过串口、定时器等业务中断的紧急程度。参数0x0F是按 4 位优先级给的如果工程用了优先级分组先确认分组设置再调这几个数。不要一边用HAL_Delay一边让内核接管 SysTickHAL_Delay会等 tick内核也在等 tick轻则延时变长重则启动阶段直接卡死。中断入口要收口。启动文件里通常是弱定义stm32f1xx_it.c里可能又写了一份LiteOS 移植层里还有一份三份同名函数只能活一个。下面这种写法只保留内核需要的入口业务中断单独挂。/* target/stm32f1xx_it.c内核入口只留一份函数名以源码声明为准 */ void SysTick_Handler(void) { LOS_TickHandler(); /* 推进内核节拍 */ } void PendSV_Handler(void) { LOS_IntLock(); LOS_TaskSwitch(); /* 上下文切换 */ LOS_IntUnLock(); } void SVC_Handler(void) { LOS_SvcHandler(); /* 启动第一个任务时进入 */ }这段代码的参数不在函数里而在链接和优先级SysTick_Handler必须指向内核 tickPendSV_Handler必须指向任务切换SVC_Handler负责启动调度。如果编译报重复定义去启动文件里把对应的弱符号段注释掉或者把裸机工程里的同名函数删掉不要靠改名字绕。3.3 内存池与任务栈64KB SRAM 的分配账64KB SRAM 不算宽裕移植时先做一张粗账。下面这张表是保守起点不是固定答案实际按 map 文件和栈水位调。区域起步建议说明主栈 中断栈2KB启动文件Stack_Size中断嵌套深就加到 3KB全局变量 / 外设缓冲8KB串口收发、ADC 缓冲都算这里内核内存池20KB任务栈、内核对象、动态分配都从这里出应用任务栈8KB按 4 到 6 个任务估算单任务 1KB 到 2KB余量剩余不要吃满留 20% 给峰值和日志内存池初始化我一般放在main里、内核启动前切一段静态数组给内核避开启动文件的主栈。/* target/board.c从 SRAM 里切一块给内核堆8 字节对齐 */ #define LOS_HEAP_SIZE (20U * 1024U) static UINT8 g_losHeap[LOS_HEAP_SIZE] __attribute__((aligned(8))); void Board_MemInit(void) { LOS_MemInit(g_losHeap, LOS_HEAP_SIZE); }g_losHeap是静态数组链接后直接占.bss不会和主栈打架。LOS_HEAP_SIZE先给 20KB任务栈每个 1KB创建四五个任务够用。参数怎么改如果创建任务返回失败先看内存池剩余不要盲目加任务栈如果栈水位高先把大局部数组改成静态或全局再考虑加池子。对齐 8 字节是为了兼容内核对象和 64 位访问习惯F103 上虽然 4 字节也能跑但留着更稳。4. 任务、信号量与串口最小可验证业务怎么搭4.1 创建两个任务和一个信号量最小业务不要一上来就搞复杂协议。两个任务足够验证调度一个等信号量一个定时发信号量。串口收到一帧后也发同一个信号量处理任务被唤醒这样能同时验证任务切换、IPC 和中断到任务的路径。#include los_task.h #include los_sem.h static UINT32 g_semId; static UINT32 g_taskA; static UINT32 g_taskB; static VOID TaskA(VOID *arg) { (VOID)arg; while (1) { LOS_SemPend(g_semId, LOS_WAIT_FOREVER); /* 收到信号后处理一帧长日志不要放在这里刷 */ LOS_TaskDelay(1); } } static VOID TaskB(VOID *arg) { (VOID)arg; while (1) { LOS_TaskDelay(100); /* 100 tick 1s 100Hz */ LOS_SemPost(g_semId); } } void AppTaskInit(void) { TSK_INIT_PARAM_S init {0}; LOS_SemCreate(0, g_semId); /* 初值 0先等后发 */ init.pfnTaskEntry TaskA; init.usTaskPrio 5; /* 数值越小优先级越高按源码确认范围 */ init.uwStackSize 0x400; /* 1KB */ init.pcName taskA; LOS_TaskCreate(g_taskA, init); init.pfnTaskEntry TaskB; init.usTaskPrio 6; init.uwStackSize 0x400; init.pcName taskB; LOS_TaskCreate(g_taskB, init); }逻辑说明信号量初值 0TaskA 先阻塞TaskB 每秒 post 一次TaskA 被唤醒后延时 1 tick 再回去等。参数里usTaskPrio的具体范围看源码不要照搬别的 RTOSuwStackSize单位是字节0x400是 1KB。pcName长度别超过内核限制调试时看任务列表能对上名字。第一次跑先把 TaskA 里的处理换成 GPIO 翻转确认调度真的在动再接串口协议。4.2 串口中断到信号量的安全路径串口中断里只做最短的事读数据、清标志、投递信号。不要在中断里pend也不要在中断里打印。下面这段是 USART1 收帧的骨架收到换行符就 post 一次。/* target/stm32f1xx_it.c中断里只投递处理放到任务 */ void USART1_IRQHandler(void) { if (USART1-SR USART_SR_RXNE) { uint8_t ch (uint8_t)USART1-DR; if (ch \n) { LOS_SemPost(g_semId); /* 若源码要求中断安全接口按声明替换 */ } } }逻辑说明RXNE置位说明收到一个字节读DR会清标志。参数上要确认 USART1 的 NVIC 抢占优先级低于内核临界区能屏蔽的级别否则中断里调用内核 IPC 可能破坏临界区。常见做法是先查源码里LOS_SemPost是否允许中断上下文调用如果不确定就在 ISR 里只置一个volatile标志任务里轮询标志再处理。串口波特率先按 115200 配时钟 72MHz 下误差可接受但别在中断里做浮点格式化。4.3 用 GPIO 翻转测调度延迟调度延迟不要靠串口打印猜。拿一个空闲 GPIO在任务入口翻转示波器或逻辑分析仪看周期。下面这段把 PB5 当测试脚每 10 个 tick 翻转一次100Hz 下周期 200ms。/* app/app_task.cPB5 测试脚验证任务周期和调度抖动 */ static VOID TaskBlink(VOID *arg) { (VOID)arg; for (;;) { GPIOB-ODR ^ GPIO_ODR_ODR5; LOS_TaskDelay(10); /* 10 tick 100ms 100Hz */ } }参数LOS_TaskDelay(10)跟着 tick 频率走改成 1000Hz 后同样的 10 就是 10ms不要写死“10ms”的注释。测的时候先把其他任务停掉只留这一个看周期是否稳定再把串口打印任务打开看抖动有没有变大。如果翻转周期明显变长先查中断里有没有长耗时操作再查任务优先级是不是被高优先级任务饿死。这个测试脚后面做压力测试也留着交付前拿它跑一遍最直观。5. 常见问题排查启动卡死、HardFault 与栈溢出的现场记录5.1 启动卡在 Reset_Handler 之后串口无输出现象下载后程序不跑串口没有任何打印调试器停在Reset_Handler或SystemInit里。原因常见有两个一是SystemInit和Board_ClockInit重复配置 HSE晶振起振等待没有超时二是启动文件选错向量表长度和芯片不匹配。解决方式是把时钟初始化收口只留一个入口并在等待 HSE 的循环里加超时计数。先用裸机点灯确认时钟和启动文件没问题再把 LiteOS 加回来。不要在内核启动前调用printf串口还没初始化时打印会卡在底层。5.2 HardFault 定位从 LR 和栈帧找回现场现象跑几分钟后进HardFault_Handler调试器只看得到一片寄存器。原因可能是空指针、任务栈溢出、中断里调用了不可重入函数或者 PendSV 被重复定义。Cortex-M3 异常压栈帧里保存了 R0-R3、R12、LR、PC、xPSR先把 PC 和 LR 取出来映射到 map 文件里找函数。/* target/stm32f1xx_it.cHardFault 里取压栈帧先看 PC/LR */ __attribute__((naked)) void HardFault_Handler(void) { __asm volatile ( tst lr, #4 \n ite eq \n mrseq r0, msp \n mrsne r0, psp \n b hardfault_report\n ); } void hardfault_report(uint32_t *frame) { volatile uint32_t pc frame[6]; volatile uint32_t lr frame[5]; (void)pc; (void)lr; for (;;) {} }frame[6]是出错 PCframe[5]是 LRframe[7]是 xPSR。把 PC 拿去 map 文件里查能落到具体函数如果落在LOS_TaskSwitch附近优先查任务栈和 PendSV 重复定义。参数上注意tst lr, #4判断用的是 MSP 还是 PSP任务上下文一般走 PSP。这个函数只用于定位量产后要换成复位或安全状态。5.3 tick 不走与中断优先级翻车现象任务创建成功但LOS_TaskDelay不返回串口只打印一次。原因通常是 SysTick 没接管或者 SysTick 优先级高于内核临界区节拍被屏蔽后丢失。解决确认SysTick_Handler只调内核 tick 入口删掉裸机工程里的同名弱定义把 SysTick 和 PendSV 设到最低抢占优先级全局搜一下HAL_Delay在内核启动后不要再调。还有一种隐蔽情况调试器暂停时 SysTick 停止恢复后 tick 计数跳变任务延时看起来不准这种不是移植问题先用 GPIO 翻转验证。5.4 栈溢出与内存池耗尽的早期信号现象单任务跑正常加两个任务后偶发 HardFault或者创建任务返回失败串口打印乱码。原因一是任务栈给小了二是内核内存池被日志缓冲吃光。解决先看栈水位把每个任务栈加到 1KB 到 2KB局部大数组改成静态再看内存池剩余关掉不必要的内核模块缩短日志。参数上不要所有任务都给 4KB64KB SRAM 经不起这么花。我一般给每个任务留 30% 余量压测时用最大报文跑栈水位不超过 70% 才敢交付。5.5 串口打印正常但任务周期抖动大现象GPIO 翻转周期忽长忽短串口打印却正常。原因可能是高优先级任务忙等或者中断里做了长耗时操作也可能是printf在任务里重入。解决把打印移到低优先级日志任务业务任务只写环形缓冲中断里只置标志用 GPIO 在中断入口和出口各翻转一次看中断占了多少时间。参数上先把串口波特率固定别在压测时改日志等级降下来只留错误和关键状态。抖动不是玄学绝大多数是优先级和临界区长度的问题。6. 进阶验证与稳定性把移植工程跑成可交付状态6.1 压测与栈水位观测最小闭环跑通后别急着加业务。先让两个任务对发信号量串口每秒打印一次内存池剩余和任务栈水位连续跑两小时。栈水位看每个任务已用栈和分配栈的比值超过 70% 就加栈或改大局部变量内存池剩余低于 20% 就裁剪模块。压测时把 GPIO 测试脚打开用逻辑分析仪看周期抖动比盯串口日志靠谱。6.2 断言、日志与复位原因闭环移植工程要留断言。参数检查、内存分配失败、任务创建失败都走断言断言里记录PC和当前任务名然后复位。复位后读复位原因寄存器区分上电、看门狗和软件复位。日志不要用大缓冲短格式加环形队列低优先级任务慢慢发。这样出问题时能看到“最后一次正常状态”而不是只有一片 HardFault。6.3 交付前检查表与一个具体技巧检查项通过标准时钟72MHz 下串口波特率误差可接受HSE 起振有超时tick100Hz 下 GPIO 翻转周期稳定LOS_TaskDelay可预期中断SysTick/PendSV 最低抢占优先级无重复定义内存压测两小时内存池剩余大于 20%单任务栈水位小于 70%异常HardFault 能定位到函数断言日志可读交付前我会把Stack_Size、内存池大小、任务栈和 tick 频率写进一张配置卡和链接脚本一起归档。一个具体技巧每次改完los_config.h都全量重编并重新看 map不要只编改动文件内核条件编译很容易留下旧对象。从那以后我每次移植新板子都先跑 2.1 的目录对照和 4.3 的 GPIO 翻转再谈业务。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑