RTOS任务间通信和全局变量有什么区别?

strongerHuang 2021-04-15 00:00

关注+星标公众,不错过精彩内容

转自 | Mculover666


1. 知识点回顾

队列(queue)是一种只能在一端插入元素、在另一端删除元素的数据结构,遵循先入先出(FIFO)的规则。

环形队列(ring queue)可以方便的重复利用这段内存空间,同样遵循先入先出(FIFO)的规则。

优先级队列(prio queue)不遵循FIFO,而是根据元素的优先级进行出队,优先级最高的先出队。

「本文的所有内容都是基于这两个数据结构」,TencentOS-tiny中环形队列和优先级队列的实现和使用示例请阅读文章:

  • 数据结构 | TencentOS-tiny中队列、环形队列、优先级队列的实现及使用

2. 消息队列

2.1. 什么是消息队列

消息队列,Message Queue,顾名思义包含两部分:消息+队列,或者可以理解为消息的队列。

① 消息是什么?

两个不同的任务之间传递数据时,这个数据就称之为消息,这个消息可以是一个整型值,浮点值,甚至一个结构体,一个指针……所以,在使用不同的RTOS的消息队列时,「一定要注意传递的是值还是该值的地址」

传递值的缺点是值的长度有大有小,导致整个消息队列的长度有大有小。

一个指针的长度是固定的4字节,传递值的时候,无论值是什么类型,只传递该值的地址。

传递地址当然也有缺陷,当动态任务task1中定义了一个局部变量,然后把该局部变量的地址传给了task2,随即task1因为某种原因被销毁,内存回收,导致指向该局部变量的指针变为野指针,非常危险,不过不用慌,小问题,在编程的时候注意避免即可。

「在TencentOS-tiny中,消息队列中传递的消息指的是地址,邮箱队列传递的消息是值」

② 队列是什么?

消息队列如果底层使用环形队列存储消息,则成为消息队列,遵循:先送入的消息先被取出。

消息队列如果底层使用优先级队列存储消息,则成为优先级消息队列,遵循:优先级最高的消息最先被取出。

「在TencentOS-tiny中,这两种消息队列都有,下面一一讲述。」

③ pend-post机制

无论是什么队列,都存在两种情况:当队列满了的时候,元素再入队会发生错误;当队列为空的时候,元素出队同样会发生错误。

这种问题可以巧妙的在队列基础之上用pend-post机制解决,即等待-释放机制。

当队列「满了」的时候,前来入队的task1可以选择pend一段时间或者永久等待,「一旦有元素被task2出队」,调用post释放一个信号,「唤醒等待中的task1」

同样,当队列「空了」的时候,前来出队的task1可以选择pend一段时间或者永久等待,「一旦有元素被task2入队」,调用post释放一个信号,「唤醒等待中的task1」

是不是很巧妙?

接下来上源码!上Demo!一看便知~

2.2. 消息队列的实现

TencentOS-tiny中消息队列的实现在 tos_message_queue.htos_message_queue.c中。

typedef struct k_message_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;
    k_ring_q_t  ring_q;
k_msg_q_t;

一个pend_obj对象用来实现pend-post机制,一个ring_q环形队列用来存储消息。

是不是和我讲述的没错?学透了之后,其实一切都没有那么神秘的~

再来看看从消息队列中获取消息的API实现:

__API__ k_err_t tos_msg_q_pend(k_msg_q_t *msg_q, void **msg_ptr, k_tick_t timeout)
{
  //省略了部分源码
    TOS_CPU_INT_DISABLE();

    if (tos_ring_q_dequeue(&msg_q->ring_q, msg_ptr, K_NULL) == K_ERR_NONE) {
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }
    
    pend_task_block(k_curr_task, &msg_q->pend_obj, timeout);

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return err;
}

向消息队列中存放消息的API实现如下:

__STATIC__ k_err_t msg_q_do_post(k_msg_q_t *msg_q, void *msg_ptr, opt_post_t opt)
{
    //省略了部分源码
    TOS_CPU_INT_DISABLE();

    if (pend_is_nopending(&msg_q->pend_obj)) {
        err = tos_ring_q_enqueue(&msg_q->ring_q, &msg_ptr, sizeof(void*));
        if (err != K_ERR_NONE) {
            TOS_CPU_INT_ENABLE();
            return err;
        }
        TOS_CPU_INT_ENABLE();
        return K_ERR_NONE;
    }

    if (opt == OPT_POST_ONE) {
        msg_q_task_recv(TOS_LIST_FIRST_ENTRY(&msg_q->pend_obj.listk_task_t, pend_list), msg_ptr);
    } else { // OPT_POST_ALL
        TOS_LIST_FOR_EACH_ENTRY_SAFE(task, tmp, k_task_t, pend_list, &msg_q->pend_obj.list) {
            msg_q_task_recv(task, msg_ptr);
        }
    }

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return K_ERR_NONE;
}

从源码中可以看到,如果opt标志为 OPT_POST_ONE,表示唤醒一个,则唤醒该消息队列等待列表上任务优先级最高的那个;如果opt标志为 OPT_POST_ALL,则全部唤醒。

2.3. 消息队列的使用示例

#define MESSAGE_MAX     10

uint8_t msg_pool[MESSAGE_MAX * sizeof(void *)];

k_msg_q_t msg_q;

void entry_task_receiver(void *arg)
{
    k_err_t err;
    void *msg_received;

    while (K_TRUE) {
        err = tos_msg_q_pend(&msg_q, &msg_received, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            printf("receiver: msg incoming[%s]\n", (char *)msg_received);
        }
    }
}

void entry_task_sender(void *arg)
{
    char *msg_prio_0 = "msg 0 without priority";
    char *msg_prio_1 = "msg 1 without priority";
    char *msg_prio_2 = "msg 2 without priority";

    printf("sender: post a message 2 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_2);

    printf("sender: post a message 1 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_1);

    printf("sender: post a message 0 without priority\n");
    tos_msg_q_post(&msg_q, msg_prio_0);
}

执行结果如下:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666
sender: post a message 2 without priority
sender: post a message 1 without priority
sender: post a message 0 without priority
receiver: msg incoming[msg 2 without priority]
receiver: msg incoming[msg 1 without priority]
receiver: msg incoming[msg 0 without priority]

3. 优先级消息队列

3.1. 优先级消息队列的实现

实现和消息队列类似,通过在优先级队列的基础上加上pend-post机制来实现。

TencentOS-tiny中优先级消息队列的实现在tos_priority_message_queue.htos_priority_message_queue.c中。

typedef struct k_priority_message_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;

    void       *prio_q_mgr_array;
    k_prio_q_t  prio_q;
k_prio_msg_q_t;

其中pend_obj用于挂载等待该优先级消息队列的任务,prio_q和prio_q_mgr_array合起来实现优先级队列。

消息入队和消息出队的API实现与消息队列的实现思想一模一样,这里不再讲解。

3.2. 优先级消息队列的使用示例

#define MESSAGE_MAX     10

uint8_t msg_pool[MESSAGE_MAX * sizeof(void *)];

k_prio_msg_q_t prio_msg_q;

void entry_task_receiver(void *arg)
{
    k_err_t err;
    void *msg_received;

    while (K_TRUE) {
        err = tos_prio_msg_q_pend(&prio_msg_q, &msg_received, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            printf("receiver: msg incoming[%s]\n", (char *)msg_received);
        }
    }
}

void entry_task_sender(void *arg)
{
    char *msg_prio_0 = "msg with priority 0";
    char *msg_prio_1 = "msg with priority 1";
    char *msg_prio_2 = "msg with priority 2";

    printf("sender: post a message with priority 2\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_2, 2);

    printf("sender: post a message with priority 1\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_1, 1);

    printf("sender: post a message with priority 0\n");
    tos_prio_msg_q_post(&prio_msg_q, msg_prio_0, 0);
}

运行结果如下:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666
sender: post a message with priority 2
sender: post a message with priority 1
sender: post a message with priority 0
receiver: msg incoming[msg with priority 0]
receiver: msg incoming[msg with priority 1]
receiver: msg incoming[msg with priority 2]

将第2节的结果和第3节的结果对比,就会发现同样的消息发送顺序,因为使用不同的消息队列,任务获取到的消息顺序截然不同。

4. 邮箱队列

4.1. 不同之处

消息队列和邮箱队列的不同之处,在于底层队列每个元素类型不一样,看一眼源码便知。

消息队列传递的消息是地址,所以在初始化消息队列的时候,环形队列中每个元素都是空指针类型:

__API__ k_err_t tos_msg_q_create(k_msg_q_t *msg_q, void *pool, size_t msg_cnt)
{
 //部分源码省略

 //重点:队列中每个元素类型大小是sizeof(void*)
    err = tos_ring_q_create(&msg_q->ring_q, pool, msg_cnt, sizeof(void *));
    if (err != K_ERR_NONE) {
        return err;
    }

    return K_ERR_NONE;
}

而邮箱队列传递的是值,所以在初始化底层用到的环形队列时,每个元素的大小是由用户指定的:

__API__ k_err_t tos_mail_q_create(k_mail_q_t *mail_q, void *pool, size_t mail_cnt, size_t mail_size)
{
 //省略了部分源码
 
 //重点:每个元素的大小是mail_size,由用户传入参数指定
    err = tos_ring_q_create(&mail_q->ring_q, pool, mail_cnt, mail_size);
    if (err != K_ERR_NONE) {
        return err;
    }

    return K_ERR_NONE;
}

4.2. 邮箱队列的实现

这有什么好实现的~一个环形队列+pend-post对象即可。

TencentOS-tiny中邮箱队列的实现在tos_mail_queue.htos_mail_queue.c中。

typedef struct k_mail_queue_st {
    knl_obj_t   knl_obj;

    pend_obj_t  pend_obj;
    k_ring_q_t  ring_q;
k_mail_q_t;

是不是没什么区别~至于操作的API,更没啥区别,不写了,划水划水。

4.3. 邮箱队列的使用示例

#define MAIL_MAX    10

typedef struct mail_st {
    char   *message;
    int     payload;
mail_t;

uint8_t mail_pool[MAIL_MAX * sizeof(mail_t)];

k_mail_q_t mail_q;

void entry_task_receiver_higher_prio(void *arg)
{
    k_err_t err;
    mail_t mail;
    size_t mail_size;

    while (K_TRUE) {
        err = tos_mail_q_pend(&mail_q, &mail, &mail_size, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            TOS_ASSERT(mail_size == sizeof(mail_t));
            printf("higher: msg incoming[%s], payload[%d]\n", mail.message, mail.payload);
        }
    }
}

void entry_task_receiver_lower_prio(void *arg)
{
    k_err_t err;
    mail_t mail;
    size_t mail_size;

    while (K_TRUE) {
        err = tos_mail_q_pend(&mail_q, &mail, &mail_size, TOS_TIME_FOREVER);
        if (err == K_ERR_NONE) {
            TOS_ASSERT(mail_size == sizeof(mail_t));
            printf("lower: msg incoming[%s], payload[%d]\n", mail.message, mail.payload);
        }
    }
}

void entry_task_sender(void *arg)
{
    int i = 1;
    mail_t mail;

    while (K_TRUE) {
        if (i == 2) {
            printf("sender: send a mail to one receiver, and shoud be the highest priority one\n");
            mail.message = "1st time post";
            mail.payload = 1;
            tos_mail_q_post(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 3) {
            printf("sender: send a message to all recevier\n");
            mail.message = "2nd time post";
            mail.payload = 2;
            tos_mail_q_post_all(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 4) {
            printf("sender: send a message to one receiver, and shoud be the highest priority one\n");
            mail.message = "3rd time post";
            mail.payload = 3;
            tos_mail_q_post(&mail_q, &mail, sizeof(mail_t));
        }
        if (i == 5) {
            printf("sender: send a message to all recevier\n");
            mail.message = "4th time post";
            mail.payload = 4;
            tos_mail_q_post_all(&mail_q, &mail, sizeof(mail_t));
        }
        tos_task_delay(1000);
        ++i;
    }
}

运行结果为:

TencentOS-tiny Port on STM32L431RCT6 By Mculover666

sender: send a mail to one receiver, and shoud be the highest priority one
higher: msg incoming[1st time post], payload[1]

sender: send a message to all recevier
higher: msg incoming[2nd time post], payload[2]
lower: msg incoming[2nd time post], payload[2]

sender: send a message to one receiver, and shoud be the highest priority one
higher: msg incoming[3rd time post], payload[3]

sender: send a message to all recevier
higher: msg incoming[4th time post], payload[4]
lower: msg incoming[4th time post], payload[4]

此示例主要演示了两点:1. 如何使用邮箱队列直接传递值;2. 唤醒一个等待任务和唤醒所有等待任务的区别。

5. 优先级邮箱队列

看到这里,这个不能再讲了吧~

TencentOS-tiny中实现在tos_priority_mail_queue.ctos_priority_mail_queue.h中。

可以自己尝试根据前面的demo,编写出一个使用优先级邮箱队列的demo,测试高优先级的邮件是否会被先收到,然后将结果与第4节的实验结果进行对比。

越到文末我越浪,划水已经不能满足了,博主要去摸鱼~

6. 总结

按照惯例,对本文所讲的内容进行一个总结。

本文主要讲述了用于任务间通信的一些内核对象,主要有四个:消息队列和优先级消息队列,邮箱队列和优先级邮箱队列。

接下来列出一些重要的点:

「在使用RTOS中的一些用于任务间通信的量时,要注意传递的是值还是地址。TencentOS-tiny中消息队列传输的是地址,而邮箱队列传递的是值。」

「消息队列和邮箱队列基于环形队列实现,遵循FIFO规则;而优先级消息队列和优先级邮箱队列基于优先级队列实现,遵循按照元素优先级取出的规则。」

最后来回答题目中的问题:任务间通信为什么不使用全局变量?

① 无论是消息队列还是邮箱队列,都是利用了全局变量可以被随意访问的特性,所以使用时都会被定义为全局变量。

② 普通全局变量可用于一些简单的任务间通信场合。

③ 相较于普通全局变量,加入队列机制可以存储多个消息,加入pend-post机制可以拥有任务等待和唤醒的机制,用于解决队列已满或队列为空的问题。

------------ END ------------


推荐阅读:
嵌入式专栏精选教程
精选汇总 | STM32、单片机
精选汇总 | RTOS、操作系统

迎关注我的公众号 回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

欢迎关注我的视频号:


点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

strongerHuang 作者黄工,高级嵌入式软件工程师,分享嵌入式软硬件、物联网、单片机、开发工具、电子等内容。
评论
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 79浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 46浏览
  • 日前,商务部等部门办公厅印发《手机、平板、智能手表(手环)购新补贴实施方案》明确,个人消费者购买手机、平板、智能手表(手环)3类数码产品(单件销售价格不超过6000元),可享受购新补贴。每人每类可补贴1件,每件补贴比例为减去生产、流通环节及移动运营商所有优惠后最终销售价格的15%,每件最高不超过500元。目前,京东已经做好了承接手机、平板等数码产品国补优惠的落地准备工作,未来随着各省市关于手机、平板等品类的国补开启,京东将第一时间率先上线,满足消费者的换新升级需求。为保障国补的真实有效发放,基于
    华尔街科技眼 2025-01-17 10:44 211浏览
  •  光伏及击穿,都可视之为 复合的逆过程,但是,复合、光伏与击穿,不单是进程的方向相反,偏置状态也不一样,复合的工况,是正偏,光伏是零偏,击穿与漂移则是反偏,光伏的能源是外来的,而击穿消耗的是结区自身和电源的能量,漂移的载流子是 客席载流子,须借外延层才能引入,客席载流子 不受反偏PN结的空乏区阻碍,能漂不能漂,只取决于反偏PN结是否处于外延层的「射程」范围,而穿通的成因,则是因耗尽层的过度扩张,致使跟 端子、外延层或其他空乏区 碰触,当耗尽层融通,耐压 (反向阻断能力) 即告彻底丧失,
    MrCU204 2025-01-17 11:30 164浏览
  • 随着消费者对汽车驾乘体验的要求不断攀升,汽车照明系统作为确保道路安全、提升驾驶体验以及实现车辆与环境交互的重要组成,日益受到业界的高度重视。近日,2024 DVN(上海)国际汽车照明研讨会圆满落幕。作为照明与传感创新的全球领导者,艾迈斯欧司朗受邀参与主题演讲,并现场展示了其多项前沿技术。本届研讨会汇聚来自全球各地400余名汽车、照明、光源及Tier 2供应商的专业人士及专家共聚一堂。在研讨会第一环节中,艾迈斯欧司朗系统解决方案工程副总裁 Joachim Reill以深厚的专业素养,主持该环节多位
    艾迈斯欧司朗 2025-01-16 20:51 168浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 93浏览
  • 在物联网(IoT)短距无线通信生态系统中,低功耗蓝牙(BLE)数据透传是一种无需任何网络或基础设施即可完成双向通信的技术。其主要通过简单操作串口的方式进行无线数据传输,最高能满足2Mbps的数据传输速率,可轻松实现设备之间的快速数据同步和实时交互,例如传输传感器数据、低采样率音频/图像与控制指令等。低功耗蓝牙(BLE)数据透传解决方案组网图具体而言,BLE透传技术是一种采用蓝牙通信协议在设备之间实现数据透明传输的技术,设备在通信时会互相验证身份和安全密钥,具有较高的安全性。在不对MCU传输数据进
    华普微HOPERF 2025-01-21 14:20 71浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 73浏览
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 99浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 101浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 134浏览
  • 本文介绍瑞芯微开发板/主板Android配置APK默认开启性能模式方法,开启性能模式后,APK的CPU使用优先级会有所提高。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。源码修改修改源码根目录下文件device/rockchip/rk3562/package_performance.xml并添加以下内容,注意"+"号为添加内容,"com.tencent.mm"为AP
    Industio_触觉智能 2025-01-17 14:09 141浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦