第7章 任务

核心主题就是:LiteOS 中任务如何创建、如何启动、如何调度、如何延时、挂起、恢复和删除。


一、本章总体框架

可以合起来理解成一条主线:

先学会“创建任务”,再学会“管理任务”。

也就是:

  1. 创建任务
    • 硬件初始化;
    • 创建单任务;
    • 创建多任务;
    • 理解任务控制块 TCB;
    • 理解 LiteOS 启动流程。
  2. 任务管理
    • 任务基本概念;
    • 调度器基本概念;
    • 任务状态与状态迁移;
    • 常用任务函数;
    • 常见错误代码;
    • 任务设计要点;
    • 任务管理实验。

本章最重要的几个关键词是:

任务、任务栈、任务控制块、任务ID、任务优先级、就绪态、运行态、阻塞态、调度器、PendSV、LOS_TaskCreate、LOS_TaskDelay、LOS_TaskSuspend、LOS_TaskResume、LOS_TaskDelete。


二、为什么 LiteOS 要使用任务?

在裸机程序中,我们通常把所有逻辑都写在 while(1) 里面。

比如:

while (1)
{
    扫描按键();
    控制LED();
    读取传感器();
    发送串口数据();
}

这种方式简单,但问题是:

  • 所有功能耦合在一起;
  • 某个函数执行时间过长会影响其他功能;
  • 很难保证实时性;
  • 功能变多后代码很乱;
  • 不方便维护。

LiteOS 使用任务机制后,可以把不同功能拆成不同任务:

LED_Task();
Key_Task();
Sensor_Task();
Uart_Task();
Network_Task();

每个任务独立运行,由 LiteOS 调度器统一管理。

所以,任务机制的意义是:

把复杂程序拆成多个相对独立的执行单元,提高程序结构清晰度、实时性和可维护性。


三、任务的基本概念

在 LiteOS 中,任务可以理解为一个独立运行的函数。每个任务都在独立环境中运行,并且每个任务都有自己的任务栈。系统中任务越多,占用的 SRAM 就越多,因此一个系统能运行多少任务,取决于 SRAM 大小和任务栈配置。

一个任务通常具有以下内容:

内容 含义
任务入口函数 任务真正执行的函数
任务栈 保存任务运行过程中的局部变量、函数调用、上下文等
任务控制块 TCB 保存任务状态、优先级、ID、栈指针等信息
任务ID 任务创建成功后系统返回的唯一标识
任务优先级 决定任务被调度的先后顺序
任务状态 表示任务当前处于就绪、运行、阻塞或退出等状态

LiteOS 里通常认为:

一个任务就是一个线程。


四、任务函数应该怎么写?

LiteOS 中的任务主体通常是一个无限循环函数,不能直接返回。

任务示例:

static void Test1_Task(void)
{
    while (1)
    {
        LED2_TOGGLE;
        LOS_TaskDelay(1000);
    }
}

这里有两个重点。

1. 任务必须是死循环

任务一般写成:

while (1)
{
    ...
}

因为任务是长期存在的执行单元,不应该像普通函数那样执行完就返回。

2. 任务循环中必须有阻塞或延时

例如:

LOS_TaskDelay(1000);

如果任务中没有延时、信号量等待、消息队列等待等阻塞操作,那么这个任务会一直占用 CPU。

特别是高优先级任务,如果不主动阻塞,就会导致低优先级任务永远得不到执行机会。

所以要记住一句话:

每个任务的循环中必须有阻塞点,否则可能长期占用 CPU,影响其他任务运行。


五、硬件初始化:为什么先初始化硬件?

在创建任务之前, 先了解硬件初始化。

这是因为任务里要控制 LED、串口、按键等外设,所以必须先完成硬件初始化。

硬件初始化放在 BSP_Init() 函数中:

static void BSP_Init(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

    LED_GPIO_Config();
}

在后面的完整工程中,还会加入:

USART_Config();
Key_GPIO_Config();

这里要注意:

BSP_Init() 执行时,LiteOS 还没有启动,本质上仍然属于裸机初始化阶段。

也就是说:

  1. 先初始化硬件;
  2. 再初始化 LiteOS 内核;
  3. 再创建任务;
  4. 最后启动调度器。

六、为什么要先测试硬件?

在操作系统运行之前,先点亮 LED 测试硬件是否正常。

例如:

BSP_Init();
LED1_ON;
while (1);

这一步的目的很重要:

先确认硬件本身没问题,再开始调试操作系统。

如果硬件初始化都不正确,后面任务不运行、LED 不闪烁时,就很难判断到底是硬件问题、任务问题还是调度器问题。

所以开发顺序应该是:

先裸机验证硬件,再加入 LiteOS 任务。


七、LiteOS 创建任务需要哪些东西?

创建一个任务通常需要四类内容:

  1. 任务函数;
  2. 任务ID变量;
  3. 任务参数结构体;
  4. 调用任务创建函数。

1. 任务函数

任务函数就是任务真正执行的内容。

例如:

static void Test1_Task(void)
{
    while (1)
    {
        LED2_TOGGLE;
        LOS_TaskDelay(1000);
    }
}

2. 任务ID变量

任务创建成功后,系统会返回一个任务ID。

用户需要提前定义变量保存它:

UINT32 Test1_Task_Handle;

以后如果要删除、挂起、恢复、查询这个任务,都需要使用这个任务ID。

例如:

LOS_TaskSuspend(Test1_Task_Handle);
LOS_TaskResume(Test1_Task_Handle);
LOS_TaskDelete(Test1_Task_Handle);

3. 任务参数结构体

LiteOS 使用 TSK_INIT_PARAM_S 配置任务参数。

常用字段包括:

TSK_INIT_PARAM_S task_init_param;

task_init_param.usTaskPrio = 5;
task_init_param.pcName = "Test1_Task";
task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Test1_Task;
task_init_param.uwStackSize = 1024;

含义如下:

参数 含义
usTaskPrio 任务优先级
pcName 任务名称
pfnTaskEntry 任务入口函数
uwStackSize 任务栈大小,单位是字节
uwArg 任务参数
uwResved 保留字段

注意:

LiteOS 中优先级数值越小,优先级越高。

例如:

优先级 4 > 优先级 5

4. 调用 LOS_TaskCreate 创建任务

任务创建函数是:

UINT32 LOS_TaskCreate(UINT32 *puwTaskID, TSK_INIT_PARAM_S *pstInitParam);

使用方式:

uwRet = LOS_TaskCreate(&Test1_Task_Handle, &task_init_param);

如果返回 LOS_OK,表示任务创建成功。

如果返回其他错误代码,说明创建失败。


八、任务控制块 TCB 是什么?

每个任务都有一个任务控制块,英文是:

Task Control Block,TCB。

它是操作系统用来管理任务的数据结构。

TCB 内容包括:

  • 任务栈指针;
  • 任务状态;
  • 任务优先级;
  • 任务栈大小;
  • 任务栈顶;
  • 任务ID;
  • 任务入口函数;
  • 任务阻塞在哪个信号量;
  • 任务阻塞在哪个互斥锁;
  • 任务参数;
  • 任务名称;
  • 事件;
  • 事件掩码;
  • 消息指针等。

可以这样理解:

TCB 就是任务的“身份证 + 状态表 + 调度信息表”。

操作系统通过 TCB 知道:

  • 这个任务叫什么;
  • 优先级是多少;
  • 现在是就绪、运行还是阻塞;
  • 栈在哪里;
  • 入口函数是什么;
  • 是否在等待信号量、互斥锁、事件或消息。

九、任务栈是什么?

每个任务都有独立的任务栈。

任务栈用于保存:

  • 局部变量;
  • 函数调用信息;
  • 返回地址;
  • CPU 寄存器上下文;
  • 中断或任务切换时的现场信息。

如果任务栈太小,可能会出现:

  • 栈溢出;
  • 程序跑飞;
  • HardFault;
  • 任务异常。

如果任务栈太大,会浪费 SRAM。

所以任务栈大小要根据任务复杂度设置。

例如:

task_init_param.uwStackSize = 1024;

表示该任务栈大小为 1024 字节。


十、LiteOS 中任务控制块和任务栈从哪里来?

LiteOS 支持动态内存分配,任务控制块和任务栈的内存空间都由 LiteOS 从 SRAM 中分配。系统通过 OS_SYS_MEM_SIZE 指定 LiteOS 可以管理的内存大小,这个宏通常在 target_config.h 中配置。创建任务时,系统会使用类似 LOS_MemAllocAlign 的函数从内存池中分配内存,并进行 8 字节对齐。

也就是说:

创建任务不是只创建一个函数,而是要给任务分配 TCB 和任务栈。

如果系统内存不足,任务就会创建失败,常见错误是:

LOS_ERRNO_TSK_NO_MEMORY

十一、LiteOS 内核初始化做了什么?

在创建任务之前,必须先调用:

LOS_KernelInit();

LOS_KernelInit() 主要完成以下工作:

  1. 初始化系统内存;
  2. 如果系统接管中断,则初始化中断入口;
  3. 初始化任务管理相关底层结构;
  4. 初始化任务控制块数组和链表;
  5. 初始化任务监视功能;
  6. 初始化信号量、互斥锁、消息队列等 IPC 资源;
  7. 如果使用软件定时器,则初始化软件定时器;
  8. 创建空闲任务。

这里最重要的是空闲任务。

空闲任务的作用

CPU 不能“什么都不执行”。

当系统中没有其他就绪任务时,必须有一个任务运行,这就是空闲任务。

空闲任务的优先级最低。 当用户任务就绪后,会抢占空闲任务。

所以:

空闲任务是系统兜底运行的任务,保证系统在没有用户任务可运行时仍然有任务可执行。


十二、创建单任务的完整流程

创建单任务的典型流程如下:

int main(void)
{
    UINT32 uwRet = LOS_OK;

    BSP_Init();

    uwRet = LOS_KernelInit();
    if (uwRet != LOS_OK)
    {
        return LOS_NOK;
    }

    uwRet = Creat_Test1_Task();
    if (uwRet != LOS_OK)
    {
        return LOS_NOK;
    }

    LOS_Start();

    while (1);
}

它可以总结为:

硬件初始化 → LiteOS 内核初始化 → 创建任务 → 启动调度器。

其中任务创建函数类似:

static UINT32 Creat_Test1_Task(void)
{
    UINT32 uwRet = LOS_OK;
    TSK_INIT_PARAM_S task_init_param;

    task_init_param.usTaskPrio = 5;
    task_init_param.pcName = "Test1_Task";
    task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Test1_Task;
    task_init_param.uwStackSize = 1024;

    uwRet = LOS_TaskCreate(&Test1_Task_Handle, &task_init_param);

    return uwRet;
}

十三、创建多任务

创建两个任务:

  • Test1_Task:控制 LED2,每 1000 Tick 打印一次;
  • Test2_Task:控制 LED1,每 500 Tick 打印一次。

两个任务的优先级不同:

Test1_Task 优先级 = 5
Test2_Task 优先级 = 4

因为 LiteOS 中优先级数值越小,优先级越高,所以:

Test2_Task 的优先级高于 Test1_Task

这意味着如果两个任务同时就绪,调度器会优先运行 Test2_Task


多任务创建流程

多任务创建通常写在统一函数中:

static UINT32 AppTaskCreate(void)
{
    UINT32 uwRet = LOS_OK;

    uwRet = Creat_Test1_Task();
    if (uwRet != LOS_OK)
    {
        return uwRet;
    }

    uwRet = Creat_Test2_Task();
    if (uwRet != LOS_OK)
    {
        return uwRet;
    }

    return LOS_OK;
}

这样做的好处是:

把所有任务创建代码集中管理,结构更清楚。

如果以后增加第 3 个、第 4 个任务,只需要继续在 AppTaskCreate() 中调用对应创建函数。


十四、创建多任务时容易出错的地方

创建更多任务时,最容易忽略的是:

  1. 任务栈大小;
  2. 任务优先级;
  3. 任务函数中是否有阻塞;
  4. 系统最大任务数量配置;
  5. SRAM 是否足够。

例如:

task_init_param.uwStackSize = 1024;

如果任务内部调用函数较多、局部变量较大,1024 字节可能不够。

再比如:

task_init_param.usTaskPrio = 4;

如果优先级设置不合理,高优先级任务一直运行,低优先级任务就可能得不到执行。


十五、LiteOS 启动流程

整体可以分成三层:

  1. 启动文件阶段;
  2. main() 函数阶段;
  3. LiteOS 调度器阶段。

1. 启动文件阶段

系统上电后,首先执行启动文件中的复位函数:

Reset_Handler
    IMPORT  SystemInit
    IMPORT  __main
    LDR     R0, =SystemInit
    BLX     R0
    LDR     R0, =__main
    BX      R0

也就是说:

上电后先执行 Reset_Handler,然后调用 SystemInit,最后进入 C 运行环境并执行 main()


2. main 函数阶段

进入 main() 后,通常执行:

BSP_Init();
LOS_KernelInit();
Creat_Test1_Task();
Creat_Test2_Task();
LOS_Start();

也就是:

硬件初始化 → 内核初始化 → 创建任务 → 启动调度器。

LOS_Start() 之前,任务虽然已经创建,但还不会真正运行。


3. 调度器启动阶段

调用:

LOS_Start();

之后,LiteOS 开始调度任务。

PPT 中提到,LOS_Start()启动系统时钟节拍,并调用 LOS_StartToRun(),后者通过汇编实现真正进入任务运行。任务切换包括获取就绪列表中最高优先级任务、保存切出任务上下文、恢复切入任务上下文等。

可以总结为:

LOS_Start() 一执行,系统就从“顺序执行 main 函数”进入“多任务调度运行”。


十六、LiteOS 的两种启动方式

PPT 中讲了两种启动方式。

第一种:main 中创建所有任务后启动调度器

流程是:

int main(void)
{
    BSP_Init();
    LOS_KernelInit();

    Creat_Test1_Task();
    Creat_Test2_Task();

    LOS_Start();
}

特点:

  • 简单直观;
  • 适合初学;
  • 所有任务在调度器启动前创建完成。

第二种:先创建启动任务,再在启动任务中创建其他任务

流程是:

int main(void)
{
    BSP_Init();
    LOS_KernelInit();

    Creat_App_Task();

    LOS_Start();
}

然后在启动任务中:

void App_Task_Start(void)
{
    Creat_Test1_Task();
    Creat_Test2_Task();

    Delete_App_Task();
}

这种方式的特点是:

  • main 中只创建一个启动任务;
  • 启动任务负责创建其他应用任务;
  • 所有应用任务创建完成后,启动任务删除自己。

这种方式在复杂工程中更常见,因为系统初始化逻辑更集中。


十七、调度器是什么?

调度器是操作系统中决定“哪个任务运行”的核心模块。

PPT 中讲到,LiteOS 调度器是基于优先级的全抢占式调度器。除中断处理函数、调度器闭锁和临界段等特殊情况外,其他部分都可以被抢占。

也就是说:

如果一个更高优先级任务变为就绪态,它会立即抢占当前低优先级任务。

例如:

当前运行的是优先级 5 的任务。 这时优先级 3 的任务就绪了。 因为 3 的优先级更高,所以系统会立即切换到优先级 3 的任务。


十八、LiteOS 的任务优先级

LiteOS 默认支持 32 个优先级:

0 ~ 31

其中:

  • 0 是最高优先级;
  • 31 是最低优先级;
  • 31 通常分配给空闲任务,用户一般不建议使用。

这点非常容易考:

LiteOS 中优先级数值越小,优先级越高。


十九、任务状态

PPT 中列出了四种基本任务状态:

  1. 就绪态 Ready;
  2. 运行态 Running;
  3. 阻塞态 Blocked;
  4. 退出态 Dead。

1. 就绪态 Ready

任务已经具备运行条件,但还没有获得 CPU。

例如任务创建成功后,通常进入就绪态。


2. 运行态 Running

任务正在占用 CPU 执行。

在单核 MCU 上,同一时刻只有一个任务处于运行态。


3. 阻塞态 Blocked

任务暂时不能运行,正在等待某个条件。

例如:

  • 调用了 LOS_TaskDelay()
  • 等待信号量;
  • 等待消息队列;
  • 等待事件;
  • 等待互斥锁。

4. 退出态 Dead

任务已经结束或被删除,不再参与调度。


二十、任务状态迁移

PPT 中列出了任务状态迁移关系:

  1. 创建任务 → 就绪态;
  2. 就绪态 → 运行态;
  3. 运行态 → 就绪态;
  4. 运行态 → 阻塞态;
  5. 阻塞态 → 就绪态;
  6. 就绪态 → 阻塞态;
  7. 运行态或阻塞态 → 退出态。

可以这样理解:

状态变化 触发原因
创建 → 就绪 创建任务成功
就绪 → 运行 被调度器选中
运行 → 就绪 被更高优先级任务抢占或时间片用完
运行 → 阻塞 调用延时、等待信号量、等待队列等
阻塞 → 就绪 等待条件满足或延时时间到
运行/阻塞 → 退出 任务被删除

二十一、任务切换是怎么发生的?

在 Cortex-M 处理器中,LiteOS 使用 PendSV 实现任务切换。

PPT 中提到,LiteOS 会向 0xE000ED04 地址写入 0x10000000,也就是将 SCB 寄存器对应位设置为 1,从而触发 PendSV 中断。真正的任务切换在 PendSV 中断中完成。

简单过程是:

  1. 系统发现需要任务切换;
  2. 触发 PendSV;
  3. 保存当前任务上下文;
  4. 从就绪列表选择最高优先级任务;
  5. 恢复新任务上下文;
  6. 新任务开始运行。

可以背成:

SysTick 提供系统节拍,调度器决定是否切换,PendSV 完成上下文切换。


二十二、LOS_TaskCreate:任务创建函数

LOS_TaskCreate() 用于创建任务。

函数原型:

UINT32 LOS_TaskCreate(UINT32 *puwTaskID, TSK_INIT_PARAM_S *pstInitParam);

它做的事情大致是:

  1. 检查任务参数是否合法;
  2. 分配任务控制块;
  3. 分配任务栈;
  4. 设置任务入口函数、优先级、任务名、栈大小;
  5. 返回任务ID;
  6. 将任务加入就绪队列;
  7. 如果调度器已经启动,并且新任务优先级更高,则可能立即触发调度。

使用示例:

UINT32 Test1_Task_Handle;
TSK_INIT_PARAM_S task_init_param;

task_init_param.usTaskPrio = 5;
task_init_param.pcName = "Test1_Task";
task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Test1_Task;
task_init_param.uwStackSize = 1024;

uwRet = LOS_TaskCreate(&Test1_Task_Handle, &task_init_param);

二十三、LOS_TaskDelay:任务延时函数

任务延时函数是:

extern UINT32 LOS_TaskDelay(UINT32 uwTick);

它的作用是:

让当前任务阻塞指定 Tick 数,让出 CPU。

例如:

LOS_TaskDelay(1000);

表示当前任务延时 1000 个 Tick。

如果系统配置为:

LOSCFG_BASE_CORE_TICK_PER_SECOND = 1000

那么 1000 Tick 大约是 1 秒。


LOS_TaskDelay 的注意事项

PPT 中明确提到:

  1. 不允许在中断中调用;
  2. 不允许在任务调度被锁定时调用;
  3. 传入 0 Tick 时,当前任务主动让出 CPU;
  4. 不允许在系统初始化之前调用;
  5. 延时不是绝对精确延时;
  6. 每个任务循环中最好要有延时或阻塞。

尤其要记住:

LOS_TaskDelay() 会让任务从运行态进入阻塞态,延时时间到后再回到就绪态。


二十四、LOS_TaskDelete:任务删除函数

任务删除函数是:

UINT32 LOS_TaskDelete(UINT32 uwTaskID);

作用是:

根据任务ID删除指定任务,并回收相关资源。

删除任务时,LiteOS 会根据任务状态做不同处理:

  • 如果任务未创建,返回错误;
  • 如果任务在就绪态,从就绪队列移除;
  • 如果任务在阻塞态,从阻塞列表移除;
  • 如果任务在延时状态,从延时列表移除;
  • 如果删除的是当前正在运行的任务,不能立刻彻底删除,而是先加入回收列表,等调度完成后再回收资源;
  • 如果删除的是非当前任务,则释放任务栈并回收任务控制块。

使用示例:

uwRet = LOS_TaskDelete(Test_Task_Handle);
if (uwRet != LOS_OK)
{
    printf("任务删除失败\n");
}

二十五、LOS_TaskSuspend:任务挂起函数

任务挂起函数是:

extern UINT32 LOS_TaskSuspend(UINT32 uwTaskID);

作用是:

挂起指定任务,使其从就绪队列中移除,不再参与调度。

挂起任务后,即使它原本具备运行条件,也不会被调度器选中。

注意:

  • 不能挂起未创建任务;
  • 不能重复挂起已经挂起的任务;
  • 不能挂起 IDLE 任务;
  • 不能挂起软件定时器任务;
  • 如果任务正在运行且调度器被锁定,也不能挂起。

二十六、LOS_TaskResume:任务恢复函数

任务恢复函数是:

extern UINT32 LOS_TaskResume(UINT32 uwTaskID);

作用是:

恢复已经挂起的任务,使其重新参与调度。

如果恢复后的任务是当前就绪任务中优先级最高的任务,系统会立即进行任务切换。

简单记:

LOS_TaskSuspend()  // 让任务暂停
LOS_TaskResume()   // 让任务恢复

二十七、任务挂起与恢复实验

PPT 中的任务管理实验创建了两个任务:

  1. LED_Task
  2. Key_Task

其中:

  • LED_Task 用来控制 LED 闪烁,显示任务正在运行;
  • Key_Task 用来检测按键;
  • 按下 KEY1 时挂起 LED 任务;
  • 按下 KEY2 时恢复 LED 任务。

核心逻辑是:

if (Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON)
{
    LOS_TaskSuspend(LED_Task_Handle);
}
else if (Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY_ON)
{
    LOS_TaskResume(LED_Task_Handle);
}
LOS_TaskDelay(20);

这说明任务管理函数可以通过任务ID控制任务状态。


二十八、常见 Task 错误代码

LiteOS 的任务相关函数大多有返回值。返回 LOS_OK 表示成功,返回其他值表示错误。

PPT 中列出了一些常见错误代码。这里按考试重点整理:

错误代码 含义 解决思路
LOS_ERRNO_TSK_NO_MEMORY 内存不足 增大可管理内存,减小任务栈或任务数量
LOS_ERRNO_TSK_PTR_NULL 任务参数为空 检查参数指针
LOS_ERRNO_TSK_PRIOR_ERROR 任务优先级错误 检查优先级范围
LOS_ERRNO_TSK_ENTRY_NULL 任务入口函数为空 定义任务函数
LOS_ERRNO_TSK_NAME_EMPTY 任务名为空 设置任务名称
LOS_ERRNO_TSK_STKSZ_TOO_SMALL 任务栈太小 增大任务栈
LOS_ERRNO_TSK_ID_INVALID 任务ID无效 检查任务ID
LOS_ERRNO_TSK_NOT_CREATED 任务未创建 先创建任务
LOS_ERRNO_TSK_ALREADY_SUSPENDED 任务已经挂起 不要重复挂起
LOS_ERRNO_TSK_NOT_SUSPENDED 任务未挂起 先挂起再恢复
LOS_ERRNO_TSK_DELAY_IN_INT 在中断中延时 不要在中断服务程序中调用延时
LOS_ERRNO_TSK_DELAY_IN_LOCK 调度锁定时延时 解锁调度器后再延时
LOS_ERRNO_TSK_TCB_UNAVAILABLE 没有空闲任务控制块 增加任务控制块数量
LOS_ERRNO_TSK_OPERATE_IDLE 操作空闲任务 不要操作 IDLE 任务
LOS_ERRNO_TSK_STKSZ_TOO_LARGE 任务栈过大 减小任务栈

考试一般不会要求全部背,但要理解常见错误原因。


二十九、任务设计要点

PPT 中任务设计要点包括四类:

  1. 中断服务程序;
  2. 普通任务;
  3. 空闲任务;
  4. 任务执行时间。

1. 中断服务程序要短

中断服务程序 ISR 应该尽量短小。

因为中断执行期间通常会影响任务调度,ISR 太长会影响系统实时性。

正确做法是:

中断中只做必要处理,把复杂工作交给任务完成。

例如:

  • 中断中设置标志位;
  • 中断中释放信号量;
  • 中断中发送消息;
  • 任务中处理具体业务。

2. 普通任务要有阻塞点

普通任务不能一直死循环空转。

错误写法:

while (1)
{
    LED_TOGGLE;
}

这种任务会一直占用 CPU。

正确写法:

while (1)
{
    LED_TOGGLE;
    LOS_TaskDelay(1000);
}

3. 空闲任务不要随便操作

空闲任务是系统必须存在的最低优先级任务,通常不建议用户随意删除、挂起或占用。

系统没有其他任务运行时,空闲任务才运行。


4. 控制任务执行时间

任务执行时间过长会影响其他任务响应。

尤其是高优先级任务,应该避免长时间占用 CPU。

如果任务需要执行大量计算,可以考虑:

  • 分段执行;
  • 加入延时;
  • 使用消息队列;
  • 降低优先级;
  • 优化算法。

三十、本章重点背诵版

下面是考前可以直接背的内容。


1. LiteOS 创建任务流程

LiteOS 创建任务的一般流程是:先进行板级硬件初始化,然后调用 LOS_KernelInit() 初始化内核,接着定义任务函数、任务ID变量和任务参数结构体,再调用 LOS_TaskCreate() 创建任务,最后调用 LOS_Start() 启动任务调度器。


2. 任务函数特点

LiteOS 中的任务函数通常是一个不带返回值的无限循环函数,不能直接返回。任务循环中必须包含延时、等待信号量、等待消息队列等阻塞操作,否则任务会一直占用 CPU,导致低优先级任务无法运行。


3. 任务控制块 TCB

任务控制块是操作系统用于管理任务的数据结构,保存任务栈指针、任务状态、任务优先级、任务ID、任务名称、任务入口函数、任务栈大小等信息。LiteOS 通过任务控制块掌握任务运行状态并实现任务调度。


4. 任务栈

每个任务都有独立的任务栈,用于保存局部变量、函数调用信息、返回地址和任务上下文等内容。任务栈太小可能导致栈溢出,任务栈太大则浪费 SRAM。


5. LiteOS 启动流程

LiteOS 启动流程为:系统上电后执行启动文件中的 Reset_Handler,调用 SystemInit 完成系统初始化,然后进入 main() 函数。在 main() 中完成硬件初始化、LiteOS 内核初始化、任务创建,最后调用 LOS_Start() 启动调度器,系统进入多任务运行状态。


6. LiteOS 调度器

LiteOS 调度器是基于优先级的全抢占式调度器。系统中优先级数值越小,任务优先级越高。当有更高优先级任务进入就绪态时,会立即抢占当前低优先级任务运行。


7. 任务状态

LiteOS 任务基本状态包括就绪态、运行态、阻塞态和退出态。任务创建成功后进入就绪态,被调度器选中后进入运行态,调用延时或等待资源时进入阻塞态,被删除后进入退出态。


8. 常用任务函数

LiteOS 常用任务函数包括:

LOS_TaskCreate();   // 创建任务
LOS_TaskDelete();   // 删除任务
LOS_TaskDelay();    // 任务延时
LOS_TaskSuspend();  // 挂起任务
LOS_TaskResume();   // 恢复任务

9. LOS_TaskDelay 注意事项

LOS_TaskDelay() 的延时单位是 Tick,不允许在中断中调用,不允许在调度器锁定时调用。调用后当前任务会进入阻塞态,延时时间到达后重新进入就绪态。


10. 挂起与恢复

LOS_TaskSuspend() 根据任务ID挂起指定任务,使其不再参与调度;LOS_TaskResume() 恢复被挂起的任务,使其重新进入可调度状态。如果恢复后的任务优先级最高,系统会立即切换到该任务运行。


三十一、考试可能这样问

题目1:简述 LiteOS 创建任务的过程。

答:

LiteOS 创建任务的过程包括:首先进行硬件初始化,然后调用 LOS_KernelInit() 初始化 LiteOS 内核;接着定义任务函数、任务ID变量和任务参数结构体 TSK_INIT_PARAM_S,配置任务入口函数、任务名称、任务优先级和任务栈大小;然后调用 LOS_TaskCreate() 创建任务;最后调用 LOS_Start() 启动任务调度器,使系统进入多任务运行状态。


题目2:任务控制块 TCB 的作用是什么?

答:

任务控制块 TCB 是操作系统用于管理任务的数据结构,保存任务栈指针、任务状态、任务优先级、任务ID、任务名称、任务入口函数、任务栈大小等信息。LiteOS 通过 TCB 记录任务运行情况,并根据 TCB 中的信息完成任务调度、挂起、恢复和删除等操作。


题目3:为什么任务函数中必须有阻塞?

答:

LiteOS 是基于优先级的抢占式调度系统。如果任务函数中没有延时、信号量等待、消息队列等待等阻塞操作,该任务会一直占用 CPU,导致同优先级或低优先级任务无法获得运行机会。因此任务循环中必须设置适当的阻塞点,例如调用 LOS_TaskDelay()


题目4:LiteOS 任务调度的原则是什么?

答:

LiteOS 采用基于优先级的全抢占式调度机制。优先级数值越小,任务优先级越高。当有比当前运行任务优先级更高的任务进入就绪态时,调度器会立即进行任务切换,使高优先级任务抢占 CPU 运行。对于同优先级任务,可以采用时间片轮转方式调度。


题目5:LiteOS 任务有哪些基本状态?

答:

LiteOS 任务基本状态包括就绪态、运行态、阻塞态和退出态。任务创建成功后进入就绪态;被调度器选中后进入运行态;调用延时函数或等待信号量、消息队列等资源时进入阻塞态;任务被删除后进入退出态。


题目6:LOS_TaskDelay 的作用和注意事项是什么?

答:

LOS_TaskDelay() 用于使当前任务延时指定 Tick 数,并让出 CPU。调用后任务进入阻塞态,延时时间到达后重新进入就绪态。该函数不允许在中断中调用,不允许在调度器锁定时调用,也不能在系统初始化前调用。任务延时并不是绝对精确延时。


题目7:LOS_TaskSuspend 和 LOS_TaskResume 有什么作用?

答:

LOS_TaskSuspend() 用于挂起指定任务,使其从就绪列表中移除,不再参与调度;LOS_TaskResume() 用于恢复被挂起的任务,使其重新进入可调度状态。如果恢复后的任务优先级高于当前运行任务,系统会立即进行任务切换。


题目8:LiteOS 是如何启动的?

答:

系统上电后首先执行启动文件中的 Reset_Handler,然后调用 SystemInit 完成系统初始化,并进入 main() 函数。在 main() 中先进行板级硬件初始化,再调用 LOS_KernelInit() 初始化 LiteOS 内核,然后创建用户任务,最后调用 LOS_Start() 启动调度器。调度器启动后,系统从就绪任务中选择最高优先级任务运行,进入多任务调度状态。


三十二、本章一句话总结

这一章可以这样总结:

第七章节主要讲 LiteOS 的任务创建与任务管理:创建任务时需要任务函数、任务ID、任务参数和任务栈,系统通过任务控制块 TCB 管理任务;LiteOS 采用基于优先级的抢占式调度,任务在就绪态、运行态、阻塞态和退出态之间转换,并通过 LOS_TaskCreateLOS_TaskDelayLOS_TaskSuspendLOS_TaskResumeLOS_TaskDelete 等函数完成任务的创建、延时、挂起、恢复和删除。