内核基础:LiteOS-M内核特性、任务调度机制、内存管理初探

好,咱们今天聊聊LiteOS-M的内核。说实话,我第一次接触这个内核的时候,第一反应是——真小。但小归小,五脏俱全。你想想看,一个跑在MCU上的轻量系统,资源就那么点,内核要是做得太臃肿,那还玩什么?

LiteOS-M是华为专门为IoT设备设计的轻量级实时内核。我习惯把它比作一个精干的管家——不占地方,但活儿干得漂亮。它支持ARM Cortex-M、RISC-V、Xtensa等多种架构,移植起来也比较灵活。

内核特性概览

先说说它有哪些看家本领:

  • 硬实时性:任务响应时间可预测,毫秒级甚至微秒级。我在项目中遇到过需要精确控制电机PWM的场景,LiteOS-M的定时器精度完全够用。
  • 极小体积:ROM可以裁剪到几KB,RAM占用也很低。说白了,哪怕你只有4KB RAM的单片机,也能跑起来。
  • 模块化设计:任务管理、内存管理、IPC、软定时器……这些模块可以按需裁剪。我一般建议初学者先保留基础模块,跑通了再慢慢加。
  • 中断延迟低:中断响应速度很快,这对实时性要求高的场景特别重要。

核心要点:LiteOS-M不是Linux的简化版,它是从零设计的RTOS。它的设计哲学是——够用就好,绝不浪费一个字节。

任务调度机制

任务调度,说白了就是内核决定“现在该让谁跑”。LiteOS-M用的是基于优先级的抢占式调度,配合时间片轮转。

什么意思呢?

  • 每个任务都有一个优先级,数字越小优先级越高(0是最高)。
  • 高优先级的任务就绪了,会立刻抢占低优先级任务的CPU。
  • 如果多个任务优先级相同,就轮流跑,每人分一个时间片。

嗯,这里要注意:优先级不要设太多。我见过有人一口气设了32个优先级,结果调度开销反而上去了。我个人习惯,一般用5~8个优先级就够用了。

来看一个简单的任务创建示例:

UINT32 TaskSampleEntry(VOID)
{
    printf("任务1正在运行\n");
    return LOS_OK;
}

UINT32 CreateSampleTask(VOID)
{
    UINT32 ret;
    UINT32 taskID;
    TSK_INIT_PARAM_S taskParam = {0};

    taskParam.pfnTaskEntry = (TSK_ENTRY_FUNC)TaskSampleEntry;
    taskParam.uwStackSize  = 0x1000;
    taskParam.pcName       = "SampleTask";
    taskParam.usTaskPrio   = 5;  // 优先级5

    ret = LOS_TaskCreate(&taskID, &taskParam);
    if (ret != LOS_OK) {
        printf("创建任务失败\n");
        return LOS_NOK;
    }
    return LOS_OK;
}

这段代码看着简单,但有几个坑:

  • 栈大小:0x1000是4KB,对于简单任务够了。但如果你任务里用了大量局部变量或递归,栈可能溢出。我曾经在调试一个协议栈任务时,栈设小了,结果跑着跑着就莫名其妙重启……查了两天才发现是栈溢出。
  • 任务入口函数:不能返回!如果返回了,任务就结束了。我建议在入口函数里写个while(1)循环。

避坑指南:我曾经在任务里用了printf,结果发现打印太频繁,把CPU时间都吃掉了。高优先级任务里尽量别做耗时操作,否则低优先级任务永远没机会跑。

任务状态与切换

任务有几种状态:就绪、运行、阻塞、挂起、退出。调度器只在就绪队列里挑任务跑。

为什么会阻塞?比如任务在等一个信号量,或者调用了LOS_TaskDelay()。这时候CPU就会让给别的任务。

我画了一张任务状态流转图,帮你理清关系:

就绪 运行 阻塞 挂起 调度 时间片用完/抢占 等待资源 资源就绪 挂起操作 恢复操作 LiteOS-M 任务状态流转图

你看,任务从就绪到运行,靠的是调度器。从运行到阻塞,往往是任务自己调用了阻塞API。从阻塞回到就绪,是等待的条件满足了。

内存管理初探

内存管理这块,LiteOS-M提供了两种方式:静态内存动态内存

静态内存:说白了就是预先划分好一块固定大小的内存池,每次分配固定大小的块。优点是快、无碎片。缺点是不够灵活。

动态内存:支持任意大小分配,用完了释放。但会有碎片问题。

我一般这样选:

  • 如果任务数量固定、消息大小固定,用静态内存。比如我做过一个传感器采集任务,每次发32字节的数据,用静态内存池就很合适。
  • 如果数据大小变化大,比如HTTP请求的响应长度不确定,那就用动态内存。

来看一个动态内存分配的示例:

VOID *pMem = LOS_MemAllOC(m_aucSysMem0, 1024);
if (pMem != NULL) {
    memset(pMem, 0, 1024);
    // 使用内存...
    LOS_MemFree(m_aucSysMem0, pMem);
} else {
    printf("内存分配失败\n");
}

警告:动态内存分配后一定要检查返回值!我见过太多人直接拿NULL指针用,结果系统直接挂掉。另外,记得释放,否则内存泄漏会让你头疼。

关于内存碎片,我有个小技巧:尽量分配大小相近的内存块。比如你总是分配64、128、256字节,碎片会少很多。如果一会儿分配3字节,一会儿分配1024字节,碎片就会很严重。

嗯,说到这,我想起一个项目。当时做智能门锁,内存只有64KB。任务多、消息多,动态内存用了一段时间后,分配越来越慢。后来我改用静态内存池,把关键消息用固定大小分配,问题就解决了。

小结

LiteOS-M的内核设计很务实。任务调度简单直接,内存管理也给了你选择权。我个人觉得,掌握好任务优先级的设计和内存分配策略,就抓住了这个内核的精髓。

你想想看,一个系统跑得稳不稳,很大程度上取决于这两点。任务优先级设错了,低优先级任务可能永远饿死。内存管理没用好,系统跑几天就崩了。这些都是我在实际项目中踩过的坑,希望你少走弯路。

我的建议:刚开始移植时,先跑通一个任务、一个定时器。别一上来就搞复杂。等基础稳了,再慢慢加功能。这样出了问题也好定位。


公众号:蓝海资料掘金营,微信deep3321