0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

RTOS内功修炼记(一)— 任务到底应该怎么写?

冬至子 来源:Mculover666 作者:Mculover666 2023-12-01 16:36 次阅读

本篇文章讲述了任务的三大元素:任务控制块、任务栈、任务入口函数,并讲述了编写RTOS任务入口函数时三个重要的注意点。

1. 知识点回顾

在正式开始讲解内容之前,我会先回顾一下基础知识点,请确保你已经了解并掌握。

1.1. 任务的创建方法

在用户层调用API创建一个任务,通常的流程如下:

① 创建一个数组作为任务栈:

#define TASK1_STACK_SIZE    512  
k_stack_t task1_stack[TASK1_STACK_SIZE];

② 创建一个任务控制块:

k_task_t    task1;

③ 编写任务入口函数:

void task1_entry(void *arg)  
{  
    while(1)  
    {  
        printf("task1 is runningrn");  
        tos_task_delay(1000);  
    }  
}

④ 调用系统API创建任务:

ret = tos_task_create(&task1,  
                      "task1",  
                      task1_entry,  
                      NULL,  
                      TASK1_PRO,  
                      task1_stack,  
                      TASK1_STACK_SIZE,  
                      10);

创建之后任务为就绪态(处于系统就绪队列中),等待系统调度器调度执行。

1.2. STM32内存分布

阅读之后,你应该要知道,STM32(Cortex-M3)中Flash和SRAM的内存空间如下:

image.png

其中Flash存储空间中又分为文本段、只读数据段、复制数据段:

image.png

其中SRAM存储空间中又分为data数据段、bss数据段、堆空间、栈空间:

image.png

并且还要知道不同的变量类型,它对应的存储位置在哪里,如果没有,一定要阅读上文之后再回来看,这是理解之后内容的基础。

1.3. Cortex-M3/4系列内核

CrortexM3/4系列内核中的寄存器组都有16个寄存器,如图所示,寄存器组通常都是CPU用于数据处理和运行控制的,希望你可以大概知道每个寄存器的作用:

image.png

① R0-R12:通用寄存器,用于数据操作;

② R13:栈顶指针,有两个互斥的指针MSP和PSP,在任一时刻只能使用其中一个;

③ R14:连接寄存器,调用子程序时存放返回地址;

④ R15:程序计数器,PC指针指向哪里,CPU就执行哪里的代码;

在RTOS内核中,这16个寄存器组的值称之为 「上下文环境」 ,即当前任务运行时这16个寄存器中的值称为上文环境,下一个任务运行时这16个寄存器的值称为下文环境, 「上下文切换」 就是指将这16寄存器组的值修改为下一个任务的值。

1.4. 栈

栈是一种 「只能在一端插入或者删除元素」 的数据结构,规则为: 「先入后出」 (FILO)。

image.png

C语言程序运行的时候,栈是非常非常非常重要的,在裸机程序中,栈顶指针由寄存器R13给出。

栈的作用,一方面是局部变量的存储,局部变量的定义会被汇编为PUSH 指令,将局部变量中的内容压入栈中,在函数执行完毕之后出栈,该局部变量被销毁;另一方面是函数调用时的参数传递,也会被压入栈中,在函数执行完毕后出栈。

2. 任务控制块长啥样

任务控制块是一个任务的核心,广义的讲: 「内核所有对任务的操作,其实都是在操作任务控制块」

任务控制块类型k_task_t是一个结构体类型:

typedef struct k_task_st    k_task_t;

当定义了一个任务控制块时,该结构体变量没有初始值,所以 「存储位置在STM32内部SRAM中的bss段内」

任务控制块的结构体类型定义如下:

/**  
 * task control block  
 */  
struct k_task_st {  
    k_stack_t          *sp;                     /**< task stack pointer. This lady always comes first, we count on her in port_s.S for context switch. */  
  
    knl_obj_t           knl_obj;                /**< just for verification, test whether current object is really a task. */  
  
    char                name[K_TASK_NAME_MAX];  /**< task name */  
    k_task_entry_t      entry;                  /**< task entry */  
    void               *arg;                    /**< argument for task entry */  
    k_task_state_t      state;                  /**< just state */  
    k_prio_t            prio;                   /**< just priority */  
  
    k_stack_t          *stk_base;               /**< task stack base address */  
    size_t              stk_size;               /**< stack size of the task */  
  
  
  
    k_list_t            stat_list;              /**< list for hooking us to the k_stat_list */  
  
    k_tick_t            tick_expires;           /**< if we are in k_tick_list, how much time will we wait for? */  
  
    k_list_t            tick_list;              /**< list for hooking us to the k_tick_list */  
    k_list_t            pend_list;              /**< when we are ready, our pend_list is in readyqueue; when pend, in a certain pend object's list. */  
      
  
    pend_obj_t         *pending_obj;            /**< if we are pending, which pend object's list we are in? */  
    pend_state_t        pend_state;             /**< why we wakeup from a pend */  
};

此处引用的源码 「不完整」 ,方便阅读起见,所有使用宏开关配置的定义全部省略。

任务控制块中的内容主要分为三部分:

① 任务栈栈顶指针sp:接下来会重点讲解;

② 任务的全部信息:任务名称、任务状态、任务优先级、任务入口函数及参数、任务栈地址和大小;

③ 任务的链表:后续文章中重点讲解。

3. 任务栈

3.1. 任务栈是什么

任务栈类型 k_stack_t 是一个 uint8_t 类型:

typedef uint8_t             k_stack_t;

当定义了一个任务栈数组时:

#define TASK1_STACK_SIZE    512  
k_stack_t task1_stack[TASK1_STACK_SIZE];

本质上还是一个uint8_t类型的全局变量数组,该全局变量数组没有初始值,所以 「存储位置仍在STM32内部SRAM中的bss段内」

在使用该数组的时候,只通过指针sp访问,假装它是一个栈,在使用上和栈的使用方式一模一样,所以称之为任务栈。

3.2. 任务栈中有什么(作用)

在创建任务的API中,有这样一句代码来初始化任务栈,并且返回任务栈的栈顶指针sp:

task- >sp = cpu_task_stk_init((void *)entry, arg, (void *)task_exit, stk_base, stk_size);

查看cpu_task_stk_init函数的定义,会发现 「不同的CPU结构,该函数的实现不同」

image.png

为什么不同的CPU结构,会导致任务栈的初始化代码实现不同呢?

不急,让我们先来看看如何来初始化任务栈, 「Cortex-M系列芯片的内核对应的都是ARM v7m架构」 ,选取此架构中的 cpu_task_stk_init 函数实现来探索问题的答案。

① 获取任务栈栈顶指针的地址并对齐:

cpu_data_t *sp;  
  
sp = (cpu_data_t *)&stk_base[stk_size];  
sp = (cpu_data_t *)((cpu_addr_t)sp & 0xFFFFFFF8);

② PendSV异常发生时自动保存的寄存器:

/* auto-saved on exception(pendSV) by hardware */  
*--sp = (cpu_data_t)0x01000000u;    /* xPSR     */  
*--sp = (cpu_data_t)entry;          /* entry    */  
*--sp = (cpu_data_t)exit;           /* R14 (LR) */  
*--sp = (cpu_data_t)0x12121212u;    /* R12      */  
*--sp = (cpu_data_t)0x03030303u;    /* R3       */  
*--sp = (cpu_data_t)0x02020202u;    /* R2       */  
*--sp = (cpu_data_t)0x01010101u;    /* R1       */  
*--sp = (cpu_data_t)arg;            /* R0: arg  */

③ 手动保存/加载的寄存器:

*--sp = (cpu_data_t)0x11111111u;    /* R11      */  
*--sp = (cpu_data_t)0x10101010u;    /* R10      */  
*--sp = (cpu_data_t)0x09090909u;    /* R9       */  
*--sp = (cpu_data_t)0x08080808u;    /* R8       */  
*--sp = (cpu_data_t)0x07070707u;    /* R7       */  
*--sp = (cpu_data_t)0x06060606u;    /* R6       */  
*--sp = (cpu_data_t)0x05050505u;    /* R5       */  
*--sp = (cpu_data_t)0x04040404u;    /* R4       */

④ 返回当前栈顶指针:

return (k_stack_t *)sp;

初始化后任务栈中的内容如下:

image.png

任务切换的大致流程是触发PendSV异常,在异常处理函数中使用汇编语言实现任务切换,也就是 「上下文切换」 ,在接下来的文章中会专门讲述任务切换。

当该任务被调度执行时,CPU会自动将任务栈中最前面的8个寄存器值加载到CPU寄存器中,完成 「下文环境切换」 ,此时:

  • 栈顶指针寄存器R13中的值是该任务的任务栈的sp指针;
  • 程序计数器指针PC指向的是该任务的入口函数entry;

接下来CPU中的环境就是该任务的环境,该任务开始运行。

因为栈顶指针指向的是该任务的任务栈,所以此时若在任务的入口函数中传递参数,调用函数,创建局部变量, 「所有数据都被压入到该任务的任务栈中」 ,与STM32内部的栈空间毫无关系。

同理,当任务执行完毕时(不一定是程序结束,而是调度器需要去调度执行别的任务了),因为栈具有 「后入先出」 的规则,CPU再将当前寄存器组的值压入到栈中,完成 「上文环境保存」 ,下次再需要被加载时,这些寄存器组的值将首先出栈。

最后揭晓问题答案,因为 「不同的CPU架构,CPU寄存器组的数量、功能都不同」 ,所以需要针对每种CPU架构都要有一个实现。

4. 任务到底应该怎么写

在学习RTOS的时候,我们的关注点都是“如何创建任务”,将重点放在了创建任务的API上,而忽略了一些最重要的问题。

重点①: 「任务入口函数,并不是一个普通的函数」

任务入口函数,通常它都伪装成了一个普通函数,不像main函数那样鹤立鸡群,所以很多时候我们觉得它就是一个普通函数调用,实则不然。

「每一个任务的entry,首先应该是一个独立的裸机程序。」

为什么这么说?因为多任务操作系统的机制是抢占式调度和时间片轮转,无论再怎么牛逼,也无法改变CPU中只有一个CPU的事实,所以无论在任何一个时刻,系统中都只有唯一一个任务在运行。

重点②: 「每写一行代码,都要思考任务栈是否足够」

在任务入口函数中创建的局部变量,函数调用,函数传参,都使用的是该任务的任务栈,和STM32内部栈空间没有任何关系,所以在编写的时候一定要时刻思考自己指定的任务栈大小是否足够,特别是在开辟局部变量数组的时候,调用一些库的API的时候。

而在任务入口函数中,如果定义的是static变量,则不会存放到任务栈中,存放位置在STM32内部SRAM中的bss区域内。

除此之外,其余代码都属于可执行代码,存放在Flash中Text区域中的Executable Code段,大可不必太在意。

重点③: 「尽量尽量要主动释放CPU,切忌浪费CPU」

在裸机程序中,如果你动不动喜欢写个死循环延时,尚可原谅,但是在RTOS系统中,如果一个任务在死循环做无用功,而导致其它任务得不到调度执行,将是不可饶恕的。

在编写任务入口函数的时候,一定要遵循“不使用,就让出”的原则,做一个高素质的任务,最普遍的做法是使用系统提供的delay函数来延时。

这样做有非常多的优点,一方面是防止系统发生堵塞,导致其它任务得不到运行;另一方面是使系统中的空闲任务可以在空闲的时候回收系统内存资源,进入低功耗模式等骚操作。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    30

    文章

    5032

    浏览量

    117746
  • STM32
    +关注

    关注

    2240

    文章

    10674

    浏览量

    348809
  • RTOS
    +关注

    关注

    20

    文章

    776

    浏览量

    118798
  • Cortex-M3
    +关注

    关注

    9

    文章

    268

    浏览量

    59165
  • SRAM控制器
    +关注

    关注

    0

    文章

    11

    浏览量

    5861
收藏 人收藏

    评论

    相关推荐

    电子工程师应该修炼的九大内功

    ,平均年龄在29岁,美国电子工程师的年龄平均在40岁左右。在这个朝气蓬勃的年龄,电子工程师如何实现快速个人成长呢?本文作者结合网上的资源,整理了一下就大内功修养路径,和电子工程师们分享。
    发表于 11-03 10:31 3279次阅读

    RTOS信号量、队列通信原理

    有深入理解RTOS原理,或阅读过RTOS源码的同学应该知道:RTOS实现任务间通信通常是由一系列指针进行操作实现的。
    发表于 08-16 10:07 1417次阅读

    嵌入式RTOS任务栈 和 系统栈

    简介明了带你了解嵌入式RTOS任务栈 和 系统栈
    的头像 发表于 05-16 09:57 2057次阅读
    嵌入式<b class='flag-5'>RTOS</b>的 <b class='flag-5'>任务</b>栈 和 系统栈

    RTOS中的线程、进程和协程详解

    看到有小伙伴在讨论【RTOS任务属于线程还是进程】的话题,这里就来分析一下OS中的线程、进程和协程的这几个概念,同时一起看看RTOS中的任务到底
    的头像 发表于 11-09 12:36 998次阅读
    <b class='flag-5'>RTOS</b>中的线程、进程和协程详解

    RTOS的多任务同步和通讯

    、邮箱、事件标记、管道、信号和条件变量等。深入理解和实现RTOS深入理解和实现RTOS_连载6_多任务同步和通讯.pdf (379.46 KB )
    发表于 02-18 06:35

    架构师修炼课程介绍

    架构师的长处之就是善于看到问题的本质。不过,什么是看到问题的本质?程序员应该如何修炼这个能力?本文从位菜鸟程序员的编程生涯开始说起,介绍透过问题看本质这
    发表于 07-11 08:02

    嵌入式软件工程师的内功修炼

    嵌入式软件的内功,21ic家也曾经多次强调。相信大家都看过武侠小说或电视,金老前辈的甚是出名,里面有“天下武功出少林”说,为什么呢?就是因为少林有本《易筋经》,“扫地僧”也就是当时江湖的大神,学了
    发表于 11-03 15:33

    RTOS中的多任务切换的相关资料分享

    浅谈RTOS中的多任务切换(基于UC/OS iii)文章目录浅谈RTOS中的多任务切换(基于UC/OS iii). 简介二.主要变量1.全
    发表于 12-06 07:08

    RTOS任务性能分析实现经验分享

    1、如何利用公式评估RTOS任务的系统资源占用呢在实践中,我们应该如何利用上述公式评估 RTOS
    发表于 04-15 18:16

    到底该不该用RTOS?看完你就有答案了

    到底该不该用RTOS,看完你就有答案了
    的头像 发表于 02-25 16:17 3126次阅读

    LEDs状态灯任务(线程)设计 (基于RTOS

    LEDs状态灯任务(线程)设计(基于RTOS
    的头像 发表于 03-12 11:30 2057次阅读

    浅析OS中的线程、进程和协程与RTOS任务属于那种

    今天为大家讲解讲解OS中的线程、进程和协程的这几个概念,同时一起看看RTOS中的任务到底属于哪一种。
    的头像 发表于 04-19 10:06 2799次阅读
    浅析OS中的线程、进程和协程与<b class='flag-5'>RTOS</b><b class='flag-5'>任务</b>属于那种

    电池制造内功修炼法打开产线智识之窗

    制造能力是制造业永恒修炼的“内功”。电池制造的要求正从ppm向ppb演进,对良率的追求近乎极限,数字化技术成为伸向极限的触手,新技术的应用正助力电池制造成本、效率、体系迈向新阶段,推动极限制造变革。
    的头像 发表于 04-03 11:05 615次阅读

    RTOS中的任务是线程?进程?还是协程?

    今天为大家讲解讲解OS中的线程、进程和协程的这几个概念,同时一起看看RTOS中的任务到底属于哪一种。
    的头像 发表于 06-04 17:19 1232次阅读
    <b class='flag-5'>RTOS</b>中的<b class='flag-5'>任务</b>是线程?进程?还是协程?

    RTOS任务间通信为什么不用全局变量?

    RTOS任务间通信为什么不用全局变量?原因在于使用全局变量存在诸多弊端。
    发表于 07-05 09:06 426次阅读