1. 为什么要在STM32上跑OpenHarmony如果你玩过STM32肯定对FreeRTOS、RT-Thread这些实时操作系统不陌生。它们让单片机开发从“裸奔”变成了“有组织有纪律”任务调度、内存管理、通信同步都方便多了。那为什么我还要折腾着把OpenHarmony的LiteOS-M内核移植到STM32F407上呢这可不是为了赶时髦。我最早接触LiteOS-M还是在它没并入OpenHarmony项目的时候。那时候它就是个纯粹的、超轻量的物联网内核代码干净实时性也不错。后来它成了OpenHarmony小型系统的核心我心里其实有点打鼓变得“鸿蒙”了会不会变臃肿会不会更难移植直到OpenHarmony 3.1版本发布我仔细研究了它的LiteOS-M部分发现内核本身依然保持着精悍的身材而且因为背靠大项目代码质量、文档虽然还是有点少和长期维护性反而更好了。对我来说在STM32这类资源受限的MCU上移植LiteOS-M有几个实实在在的好处。第一技术栈统一。如果你未来想涉足OpenHarmony标准系统跑在更高性能处理器上的先在MCU上熟悉它的内核机制、编程接口比如CMSIS-RTOS V2兼容的API上手会快很多。第二生态潜力。OpenHarmony的软总线、分布式能力是它的招牌虽然目前在LiteOS-M上还是基础阶段但提前熟悉这套框架等它功能完善了你就能快速做出有互联能力的智能设备。第三纯粹的挑战乐趣。把一个小而美的现代内核亲手搬到熟悉的硬件上跑起来看着串口打出“System Running!!!”那种成就感是直接用现成SDK无法比拟的。当然我得实话实说如果你只是需要一个稳定的RTOS来做个产品FreeRTOS、RT-Thread依然是更成熟、社区支持更立体的选择。但如果你是个喜欢折腾、想窥探一下大型开源项目内核奥秘并且为未来技术布局的开发者那这次移植之旅绝对值得一试。我这次用的是STM32F407ZGT6Cortex-M4内核有192KB RAM和1MB Flash资源足够让LiteOS-M撒开欢儿跑还能留出充足空间给你的应用。2. 动手前的准备工作环境与工具链老话说工欲善其事必先利其器。在开始敲代码之前咱们得先把场子搭好。这次移植我选择了一套相对“极客”但非常高效自由的工具组合VSCode GCC Arm工具链 OpenOCD。为什么不直接用Keil或者IAR主要是为了和OpenHarmony源码的编译风格保持一致它原生就支持GCC编译用Makefile组织构建。而且这套组合全免费跨平台后期集成自动化脚本也方便。硬件方面你需要一块STM32F407的开发板我用的就是常见的“正点原子探索者”芯片是F407ZGT6。一个J-Link或者ST-Link调试器以及一根USB转串口线用于打印调试信息。软件环境首先去ARM官网下载最新的GCC Arm Embedded Toolchain比如arm-none-eabi-gcc把它加到你的系统环境变量里确保在命令行能直接调用。然后安装OpenOCD这是用来连接调试器、下载程序、调试的开源工具。当然代码编辑器我强推VSCode装上C/C插件和ARM汇编语法高亮插件体验就非常好了。还有一个关键工具是STM32CubeMX。别担心我们不是要用它生成HAL库工程然后导入Keil那一套。我们只是借用它强大的图形化配置功能来生成最基础的Makefile工程初始化时钟、引脚、外设。这能省去我们手动编写启动文件、链接脚本等大量重复劳动。你需要确保CubeMX生成工程时选择“Makefile”作为Toolchain/IDE。另外记得在CubeMX里把HAL库的时基源Timebase Source从默认的SysTick改成其他的定时器比如TIM1因为SysTick之后要留给LiteOS-M内核做系统心跳避免冲突。最后是Git用来拉取开源代码。准备好这些你的开发环境就基本就绪了。我建议你先用CubeMX生成一个最简单的LED闪烁Makefile工程然后用VSCode配合GCC和OpenOCD编译下载成功确保工具链这条路是通的后面再引入操作系统内核会顺利很多。3. 获取与整理OpenHarmony LiteOS-M源码源码是移植的根基。OpenHarmony的代码托管在国内的Gitee上访问速度比GitHub快不少。我们直接拉取3.1 Release版本的LiteOS-M内核源码。打开你的终端或VSCode的集成终端找一个合适的目录执行克隆命令git clone https://gitee.com/openharmony/kernel_liteos_m.git拉取完成后别急着关终端。LiteOS-M内核编译还需要一些第三方库的支持比如C库接口、CMSIS接口等。这些依赖项在源码仓库里通过子模块或独立的仓库管理。我们需要手动把它们拉取到third_party目录下。进入内核源码目录并创建第三方库目录cd kernel_liteos_m mkdir ./third_party cd third_party然后依次拉取三个必要的仓库git clone https://gitee.com/openharmony/third_party_bounds_checking_function.git ./bounds_checking_function git clone https://gitee.com/openharmony/third_party_cmsis.git ./cmsis git clone https://gitee.com/openharmony/third_party_musl.git ./musl这个过程可能会花点时间取决于你的网络。拉取完成后third_party目录下应该有三个文件夹bounds_checking_function、cmsis、musl。它们分别提供了安全函数、ARM CMSIS标准接口和一个轻量化的C库实现。这些是内核编译和运行的基础组件缺一不可。现在你的本地源码结构就清晰了。kernel_liteos_m目录下是内核主体arch目录包含不同CPU架构的代码我们关注arm/cortex-m4。kernel目录是核心实现components是一些可选组件。而third_party就是我们刚拉下来的依赖。你可以用VSCode打开整个kernel_liteos_m文件夹方便后续浏览和编辑代码。4. 构建属于你的第一个LiteOS-M工程有了纯净的内核源码我们需要创建一个“靶子”工程也就是让内核运行的目标板工程。这里我们用STM32CubeMX来搭这个架子。打开STM32CubeMX新建一个工程选择你的芯片型号STM32F407ZGTx。然后进行必要的配置时钟树Clock Configuration根据你的板载晶振配置系统时钟到最高168MHzF407的极限确保内核和外设都能跑在最佳性能。外设至少使能一个串口比如USART1模式为异步Asynchronous。为了方便高效打印建议同时开启这个串口的DMA发送功能选择DMA模式为Normal并添加一个DMA通道给USART1_TX。这是为后续实现非阻塞的printf打下基础。关键设置在Project Manager标签页将Toolchain / IDE选为Makefile。在Code Generator里勾选“生成外设初始化代码为.c/.h对”。还有一个极其重要的步骤在Pinout Configuration标签页的System Core-SYS里把Timebase Source从SysTick改为其他的定时器比如TIM1。这是因为HAL库的延时函数HAL_Delay()默认使用SysTick而LiteOS-M内核也要独占SysTick作为系统时钟节拍两者冲突会导致系统异常。配置完成后点击GENERATE CODE生成工程。关键一步来了不要将工程生成到任意目录而是请将它生成到我们内核源码的targets目录下。例如你可以在targets下新建一个文件夹叫My_STM32F407_Project然后将CubeMX工程生成到这个路径。这样做的目的是让我们的板级工程和内核源码保持在一个相对固定的目录结构中后续编写编译脚本时路径处理会简单明了。生成完成后targets/My_STM32F407_Project目录下就会有完整的CubeMX生成的Makefile工程包含Inc、Src、Drivers等目录以及最重要的Makefile文件。现在这个工程还是一个纯粹的HAL库裸机工程接下来我们就要把LiteOS-M内核“注入”进去。5. 编写编译脚本让内核源码参与构建现在我们有两大块代码一是CubeMX生成的裸机工程在targets下二是LiteOS-M内核源码在上一级目录。我们需要编写一个编译脚本告诉GCC编译器如何找到所有散落的源码文件并把它们编译、链接成一个完整的可执行文件。我采用的方法是创建一个自定义的.mk文件比如叫los.mk在其中清晰地归类并指定所有源码路径和头文件路径然后在主Makefile中包含它。这样做的好处是逻辑清晰和CubeMX生成的Makefile解耦以后升级CubeMX重新生成工程时不会覆盖我们的修改。在你的工程目录例如targets/My_STM32F407_Project下新建一个文件los.mk。这个文件将包含以下几部分内容第一部分定位内核顶层目录。# Topdir LITEOSTOPDIR : ../../ LITEOSTOPDIR : $(realpath $(LITEOSTOPDIR))这里定义了LITEOSTOPDIR变量指向内核源码的根目录。realpath函数用于获取绝对路径避免后续因相对路径引起的编译错误。第二部分添加内核核心源码。# Common Kernel Sources C_SOURCES $(wildcard $(LITEOSTOPDIR)/kernel/src/*.c) \ $(wildcard $(LITEOSTOPDIR)/kernel/src/mm/*.c) \ $(wildcard $(LITEOSTOPDIR)/components/cpup/*.c) \ $(wildcard $(LITEOSTOPDIR)/components/power/*.c) \ $(wildcard $(LITEOSTOPDIR)/components/backtrace/*.c) \ $(wildcard $(LITEOSTOPDIR)/components/exchook/*.c) \ $(wildcard $(LITEOSTOPDIR)/components/signal/*.c) \ $(wildcard $(LITEOSTOPDIR)/utils/*.c) C_INCLUDES -I$(LITEOSTOPDIR)/utils \ -I$(LITEOSTOPDIR)/kernel/include \ -I$(LITEOSTOPDIR)/components/cpup \ -I$(LITEOSTOPDIR)/components/power \ -I$(LITEOSTOPDIR)/components/backtrace \ -I$(LITEOSTOPDIR)/components/exchook \ -I$(LITEOSTOPDIR)/components/signalC_SOURCES使用wildcard函数通配符自动添加指定目录下的所有.c文件。C_INCLUDES则添加对应的头文件搜索路径。这里包含了内核的基础功能任务、内存、CPU占用率统计、低功耗、异常钩子等。第三部分添加第三方库源码。# Third party related C_SOURCES $(wildcard $(LITEOSTOPDIR)/third_party/bounds_checking_function/src/*.c)\ $(wildcard $(LITEOSTOPDIR)/kal/cmsis/*.c)\ $(wildcard $(LITEOSTOPDIR)/kal/posix/src/*.c) C_INCLUDES -I$(LITEOSTOPDIR)/third_party/bounds_checking_function/include \ -I$(LITEOSTOPDIR)/third_party/bounds_checking_function/src\ -I$(LITEOSTOPDIR)/third_party/cmsis/CMSIS/RTOS2/Include \ -I$(LITEOSTOPDIR)/third_party/musl/porting/liteos_m/kernel/include\ -I$(LITEOSTOPDIR)/kal/cmsis \ -I$(LITEOSTOPDIR)/kal/posix/include \ -I$(LITEOSTOPDIR)/kal/posix/musl_src/internal这部分添加了安全函数、CMSIS-RTOS V2适配层以及POSIX接口的实现是内核提供标准API的基石。第四部分添加与芯片架构相关的源码。# Arch related - Cortex-M4 ASM_SOURCES $(wildcard $(LITEOSTOPDIR)/arch/arm/cortex-m4/gcc/*.s) ASMS_SOURCES $(wildcard $(LITEOSTOPDIR)/arch/arm/cortex-m4/gcc/*.S) C_SOURCES $(wildcard $(LITEOSTOPDIR)/arch/arm/cortex-m4/gcc/*.c) C_INCLUDES -I. \ -I$(LITEOSTOPDIR)/arch/include \ -I$(LITEOSTOPDIR)/arch/arm/cortex-m4/gcc CFLAGS -nostdinc -nostdlib ASFLAGS -imacros $(LITEOSTOPDIR)/kernel/include/los_config.h -DCLZCLZ这里指定了Cortex-M4架构的汇编启动文件、上下文切换汇编代码以及CPU特定功能。-nostdinc -nostdlib告诉编译器不要使用标准C库的头文件和库文件因为我们使用自带的musl库。ASFLAGS中的-imacros预加载了内核的主配置文件。最后在主Makefile中引入我们的脚本。打开CubeMX生成的Makefile找到定义C_SOURCES、C_INCLUDES、ASM_SOURCES的地方通常在这些变量定义之后添加一行include los.mk这样在执行make时los.mk中定义的所有源文件和路径就会合并到整个编译过程中。完成这一步理论上你已经可以尝试编译了但还缺少关键的内核配置。6. 内核的“遥控器”详解配置文件与内存布局编译通过不代表能运行。LiteOS-M内核高度可裁剪几乎所有功能比如最大任务数、信号量数量、内存池大小甚至是否启用软件定时器都通过宏定义来控制。我们需要提供一个板级配置文件来定制这些参数。在工程目录下与los.mk同级创建一个target_config.h文件。这个文件的内容基本是参考内核源码中的los_config.h但根据我们的STM32F407资源情况进行调整。我提供一个最基础的配置示例并解释几个关键项#ifndef _TARGET_CONFIG_H #define _TARGET_CONFIG_H #include stm32f4xx.h #include stm32f4xx_it.h /* 系统时钟与Tick配置 */ #define OS_SYS_CLOCK SystemCoreClock // 使用HAL库定义的系统时钟频率 #define LOSCFG_BASE_CORE_TICK_PER_SECOND (1000UL) // 系统Tick频率1000Hz /* 任务模块配置 */ #define LOSCFG_BASE_CORE_TSK_LIMIT 24 // 最大支持24个任务 #define LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE (0x500U) // 默认任务栈大小 /* 信号量、互斥锁、队列配置 */ #define LOSCFG_BASE_IPC_SEM 1 // 启用信号量 #define LOSCFG_BASE_IPC_SEM_LIMIT 48 // 最大信号量数量 #define LOSCFG_BASE_IPC_MUX 1 // 启用互斥锁 #define LOSCFG_BASE_IPC_QUEUE 1 // 启用消息队列 /* 软件定时器配置 */ #define LOSCFG_BASE_CORE_SWTMR 1 // 启用软件定时器 /* 内存管理配置 */ #define LOSCFG_MEM_MUL_POOL 1 // 启用多内存池 #define OS_SYS_MEM_NUM 20 // 系统内存池数量 /* 硬件中断配置先设为0用系统默认中断 */ #define LOSCFG_PLATFORM_HWI 0 #define LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT 0 #endif /* _TARGET_CONFIG_H */这个文件就像是内核的“遥控器”你在这里决定了内核最终以什么形态运行。比如LOSCFG_BASE_CORE_TSK_LIMIT设小了创建过多任务就会失败栈大小设小了任务容易溢出。对于F407192KB的RAM比较充裕可以适当给大一点。这里我暂时关闭了内核的硬件中断接管功能LOSCFG_PLATFORM_HWI设为0意味着中断向量表还是由STM32的HAL库/启动文件管理我们稍后手动将两个关键中断服务函数“钩”到内核上。这是一种简化移植的策略。接下来是内存布局由链接脚本.ld文件决定。CubeMX生成的链接脚本通常只考虑应用程序。我们需要微调它确保内核的堆栈和内存池被正确放置。找到你的工程里的链接脚本如STM32F407ZGTx_FLASH.ld主要做两处修改明确栈顶地址在MEMORY区域定义后SECTIONS区域开始前可以定义栈顶地址确保它指向RAM起始位置对于F407通常是0x20000000。_sstack 0x20000000; /* 定义栈起始地址 */标记代码段起始在.text段内部可以记录代码段的起始地址有时内核或调试工具需要这个信息。.text : { . ALIGN(4); _stext .; /* 记录.text段起始地址 */ KEEP(*(.isr_vector)) *(.text*) /* ... 其他内容 ... */ } FLASH这些修改确保了内核在初始化时能知道从哪里开始分配栈空间以及代码的准确位置。7. 打通任督二脉中断与系统心跳的对接前面我们把内核编译进去了也配置好了但此时下载程序芯片很可能毫无反应或者跑飞。根本原因在于系统的“心跳”和“任务调度开关”还没交给内核。对于Cortex-M内核有两个异常/中断是实时操作系统的生命线SysTick 异常提供周期性的时钟滴答Tick内核靠它来计时、进行延时管理、驱动软件定时器。这就是系统的“心跳”。PendSV 异常用于上下文切换。当内核决定从一个任务切换到另一个任务时会触发PendSV在这里执行保存旧任务现场、恢复新任务现场的操作。这就是“任务调度开关”。在我们的配置中LOSCFG_PLATFORM_HWI0中断向量表仍由启动文件管理SysTick和PendSV的中断服务函数名是固定的SysTick_Handler和PendSV_Handler。我们需要在这两个函数里调用LiteOS-M内核提供的接口。打开工程中stm32f4xx_it.c文件首先在文件顶部添加内核头文件#include los_arch_context.h #include los_tick.h然后找到PendSV_Handler函数和SysTick_Handler函数。CubeMX生成的代码通常会在函数体内预留了USER CODE BEGIN和USER CODE END的注释块。我们就在这些注释块之间添加内核调用void PendSV_Handler(void) { /* USER CODE BEGIN PendSV_IRQn 0 */ HalPendSV(); // LiteOS-M的上下文切换函数 /* USER CODE END PendSV_IRQn 0 */ /* ... 其他代码 ... */ } void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ OsTickHandler(); // LiteOS-M的系统Tick处理函数 /* USER CODE END SysTick_IRQn 0 */ /* ... 其他代码 ... */ }HalPendSV()是架构相关的汇编函数负责实际的寄存器保存与恢复。OsTickHandler()会更新内核时钟检查是否有任务延时到期并触发一次任务调度请求。经过这两处简单的“嫁接”内核就真正“活”了获得了时间基准和调度能力。8. 让内核“开口说话”实现调试串口与printf调试嵌入式系统没有日志输出就像在黑暗中摸索。我们必须实现printf函数到串口的重定向。之前用CubeMX配置了USART1和它的DMA发送就是为了高效、非阻塞地输出。在main.c文件中我们需要重写_write函数这是GCC工具链中底层IO函数最终调用的接口。同时利用LiteOS-M的信号量机制确保DMA传输完成前不会发生数据覆盖。首先在main.c的全局变量区域定义信号量和任务相关的变量osSemaphoreId_t UART1_TX_DMA_SemaphoreHandle; const osSemaphoreAttr_t UART1_TX_DMA_Semaphore_attributes { .name UART1_TX_DMA_Semaphore, };然后实现_write函数#if 1 // 使用我们自己的实现 int _write(int fd, char *ptr, int len) { osStatus_t result; (void)fd; // 未使用参数 // 判断内核是否已启动 if (osKernelGetState() osKernelInactive) { // 内核未启动使用阻塞式发送此时调度器未运行不能用信号量 HAL_UART_Transmit(huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY); return len; } else { // 内核已启动尝试获取DMA信号量 result osSemaphoreAcquire(UART1_TX_DMA_SemaphoreHandle, osWaitForever); if (result osOK) { // 获取成功启动DMA传输 HAL_UART_Transmit_DMA(huart1, (uint8_t*)ptr, len); return len; } else { return -1; // 获取信号量失败理论上不会发生因为等待时间是forever } } } #endif这个函数逻辑是系统启动前简单阻塞发送系统启动后通过信号量互斥访问DMA确保上一次串口DMA传输完成后再启动新的传输。接着在串口DMA传输完成中断回调函数中释放信号量void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 判断是否是USART1 osSemaphoreRelease(UART1_TX_DMA_SemaphoreHandle); } }最后别忘了在main函数初始化部分创建这个信号量初始值为1表示DMA空闲UART1_TX_DMA_SemaphoreHandle osSemaphoreNew(1, 1, UART1_TX_DMA_Semaphore_attributes);这样一个线程安全的、高效的printf通道就搭建好了。无论你在内核的哪个任务里调用printf日志都能有序地从串口输出。9. 点亮第一盏灯创建测试任务验证移植成果所有艰苦的移植工作都是为了最终能跑起我们自己的任务。现在让我们在main函数里创建一个简单的测试任务来验证整个系统是否运转正常。在main.c中继续定义任务相关的变量osThreadId_t uart_taskHandle; const osThreadAttr_t uart_task_attributes { .name uart_task, .stack_size 512 * 4, // 分配2KB栈空间 .priority (osPriority_t)osPriorityNormal, };然后编写任务函数实体。这个任务每隔一秒通过我们实现的printf打印一条信息void Uart_Task(void *argument) { for(;;) { printf([%lu] Hello, OpenHarmony LiteOS-M on STM32F407!\r\n, osKernelGetTickCount()); osDelay(1000); // 延时1000个Tick即1秒 } } // 函数声明如果放在main函数后面的话 void Uart_Task(void *argument);接下来在main函数的初始化部分按照标准流程启动内核int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); // ... 其他外设初始化 // 初始化LiteOS-M内核 osKernelInitialize(); // 创建串口调试信号量 UART1_TX_DMA_SemaphoreHandle osSemaphoreNew(1, 1, UART1_TX_DMA_Semaphore_attributes); // 创建测试任务 uart_taskHandle osThreadNew(Uart_Task, NULL, uart_task_attributes); // 启动内核开始调度 osKernelStart(); // 正常情况下程序永远不会运行到这里 while (1) {} }现在激动人心的时刻到了。在VSCode终端中进入你的工程目录执行make clean make进行完整编译。如果一切顺利你会看到编译成功并生成一个.elf或.bin文件。使用OpenOCD命令或者配置好VSCode的调试/下载任务将程序烧录到STM32F407开发板。给板子上电打开串口调试助手如Putty、MobaXterm等配置正确的串口号和波特率通常是115200。你应该能看到终端里每隔一秒就打印出一行带有递增Tick计数的信息[1000] Hello, OpenHarmony LiteOS-M on STM32F407! [2000] Hello, OpenHarmony LiteOS-M on STM32F407! [3000] Hello, OpenHarmony LiteOS-M on STM32F407!看到这些规律性的输出恭喜你这意味着LiteOS-M内核已经在你的STM32F407上成功运行起来了。任务调度、系统延时、串口输出这些基础功能全部工作正常。你可以尝试创建更多不同优先级的任务使用信号量、消息队列进行通信或者点个LED灯尽情体验这个你亲手移植过来的操作系统吧。整个移植过程就像搭积木从准备材料环境、源码到搭建框架工程、编译脚本再到安装核心部件内核配置、中断对接最后通电测试串口、任务。每一步的坑我都踩过比如忘记改SysTick时基导致HAL_Delay卡死或者链接脚本没设对导致内存访问错误。但当你看到串口稳定输出信息的那一刻所有这些折腾都值了。这份完整的、可运行的工程就是你探索OpenHarmony在MCU世界更广阔可能性的坚实起点。