基于 RT-Thread 的智能家居 DIY 教程连载(2)——在实际项目中运用消息队列与邮箱

RT-Thread 的邮箱与内存池实战指南

在实际项目中灵活运用邮箱与内存池

千呼万唤始出来,智能家居 DIY 教程连载第二篇终于登场了!本文将重点给大家介绍如何将内存池与邮箱运用到实际项目中去。一起来看看吧~

目录

本文文章目录如下,通过以下内容,给大家剖析第二周的任务重点难点。

    1. 第二周任务回顾
    1. 软件包的获取
    1. IPC 之邮箱实战指南(重点)
    • 3.1 为什么要使用邮箱
    • 3.2 邮箱工作原理举例介绍
    • 3.3 在项目中运用邮箱
    1. nrf24l01 的多通道数据接收
    1. 结果
    1. 开源代码
    1. 注意事项

1. 任务清单

为了更好的讲解邮箱和内存池,我罗列了一份任务清单:

  • 通过 ENV 工具获取 nrf24l01软件包,并加载到 MDK 工程里面
  • 了解多线程间的通信,了解 IPC 中邮箱和消息队列的特性,并能灵活使用,实现 ds18b20 线程与 nrf24l01 线程之间的数据通信
  • 修改 nrf24l01 软件包,实现多点通信功能

上述任务的重点,是要学习去灵活运用邮箱和消息队列。

2. 软件包的获取

软件包可以通过 env 工具十分方便的获取到,并且加载到工程里面去,env 工具的下载链接可以在官网找到,env 下载链接。env 的使用方法可以查看这里进行学习。

在这里插入图片描述
值得注意的是,在 env 中获取软件包是需要依赖于 git 的,可以去 git 官网获得下载,git 官网链接

本周任务中,我们需要用到 nrf24l01 的软件包,只需要在 menuconfig 中选中 nrf24l01 即可:

RT-Thread online packages  --->
    peripheral libraries and drivers  --->
        [*] nRF24L01: Single-chip 2.4GHz wireless transceiver.  --->

选中之后需要将该软件包获取到本地来,在 env 中输入 pkgs --update 命令回车即可。我们在工程目录的 packages 目录下,可以看到,nrf24l01 软件包被获取到本地来了,如下图所示:

在这里插入图片描述
不过该软件包现在仅仅只是获取到本地,尚未加载到 MDK 工程当中来。我们在 env 中输入 scons --target=mdk5 命令回车即可,执行完该命令之后打开 MDK5 工程,发现 nrf24l01 软件包成功加载到工程里面去了,如下图所示:

在这里插入图片描述

3. IPC 之邮箱实战指南

3.1 为什么要使用邮箱

我们需要通过 nrf24l01 无线模块进行数据发送与接收,定义:通过 nrf24l01 发送数据的是发送节点,通过 nrf24l01 接收数据的是接收节点。(本 DIY 整个项目需要至少用到两个发送节点。)

在发送节点创建一个线程,用于无线发送数据。具体的,nrf24l01 的软件包提供了哪些 API,是如何通过这些 API 实现发送功能的,可以参考该软件包的 samples,路径为:…\packages\nrf24l01-latest\examples。

还记得第一周的任务吗?在 main 函数中创建了一个线程,用于获取 ds18b20 温度数据的。同理的,我们在 main 函数中再创建一个线程,该线程是用来通过 nrf24l01 发送数据的,线程入口函数是 nrf24l01_send_entry

int main(void)
{
    rt_thread_t ds18b20_thread, nrf24l01_thread;
    
    ds18b20_thread = rt_thread_create("18b20tem", read_temp_entry, "temp_ds18b20",
                                      640, RT_THREAD_PRIORITY_MAX / 2, 20);
    if (ds18b20_thread != RT_NULL)
    {
        rt_thread_startup(ds18b20_thread);
    }
    
    nrf24l01_thread  = rt_thread_create("nrfsend", nrf24l01_send_entry, RT_NULL,
                                        1024, RT_THREAD_PRIORITY_MAX / 2, 20);
    if (nrf24l01_thread != RT_NULL)
    {
        rt_thread_startup(nrf24l01_thread);
    }
    
    return RT_EOK;
}

这时候,我们的程序当中就存在了两个线程了,ds18b20_thread 线程用来获取温度数据,nrf24l01_thread 线程用来向外无线发送温度数据,那么问题来了:

  • ds18b20_thread 线程如何将温度数据给 nrf24l01_thread 线程?
  • 如果ds18b20_thread 线程采集温度数据过快,nrf24l01_thread 线程来不及发送,怎么办?
  • 如果nrf24l01_thread 线程发送数据过快,ds18b20_thread 线程来不及采集温度数据,怎么办?

这时候,IPC 中的邮箱(mailbox)可以很好的解决以上问题。不过这里,我们需要将邮箱与内存池(mempool)搭配一起使用。往往而言,在实际项目中,邮箱和内存池这两个 IPC 是经常需要配套着一起使用的。为什么,且慢慢看来。

3.2 邮箱工作原理举例介绍

RT-Thread 的文档中心已经有对邮箱和内存池原理上的详细讲解,请点击此链接跳转至于邮箱,此链接跳转至内存池。这里不在赘述。这里通过举一个生活中的例子,去帮助大家理解邮箱和内存池。

如今,很多人购物都是通过电商平台购买,那避免不了是要收快递的。

在这里插入图片描述
我们拟定一个生活场景。小区内放置有快递柜,快递柜里面有很多快递箱,快递箱里面可以存放快递,快递员把快递存放到快递箱之后,会发短信通知你过来取快递,还会告诉你编号是多少,通过编号你可以找到你的快递存放在快递柜的哪个快递箱里面。

上面这个模型中有几个名词,我们抽取出来:快递、快递柜、快递箱、快递员、你自己、短信、编号。

我们将上面这个生活场景和 IPC 中的邮箱和内存池一一对应起来:

  • 快递:采集到的温度数据
  • 快递柜:内存池
  • 快递箱:内存池里面的内存块
  • 快递员:ds18b20_thread 线程
  • 你自己:nrf24l01_thread 线程
  • 短信:邮箱中的一封邮件
  • 编号:内存块地址指针

邮箱和内存池的使用,其实和上面那个收快递的生活场景是一样的:

  • 在程度的一开始,即 main 函数中,我们创建一个邮箱和一个内存池
  • ds18b20_thread 线程里:
    • 首先,每当该线程采集到一个温度数据(有快递来了),就在内存池里面申请一个内存块(快递员找一个空快递箱)
    • 然后,把本次采集到的这个温度数据放到内存块里(快递放入快递箱),再把内存块的地址放在邮箱里(快递编号)
    • 最后,邮件发送出去(发短信告知用户)
  • nrf24l01_thread 线程里:
    • 首先,接收ds18b20_thread 线程发送过来的邮件(用户收到短信)
    • 然后,根据邮箱中的存放的地址,知道了当前温度数据存放在哪个内存块里面,也就是说,nrf24l01_thread 线程找到了(收到了)当前温度数据。(根据短信里的编号知道快递放在哪里了)
    • 最后,用完了这个内存块要及时释放掉(快递取出来了,快递箱空了)

3.3 在项目中运用邮箱

通过代码解读一下。

main 函数中创建一个邮箱和一个内存池是这么做的:

tmp_msg_mb = rt_mb_create("temp_mb0", MB_LEN, RT_IPC_FLAG_FIFO); /* 创建邮箱 */
tmp_msg_mp = rt_mp_create("temp_mp0", MP_LEN, MP_BLOCK_SIZE);    /* 创建内存池 */

ds18b20_thread 线程的入口函数是 read_temp_entry,如下:

static void read_temp_entry(void *parameter)
{
    struct tmp_msg *msg;
    rt_device_t dev = RT_NULL;
    rt_size_t res;

    dev = rt_device_find(parameter);
    if (dev == RT_NULL)
    {
        rt_kprintf("Can't find device:%s\n", parameter);
        return;
    }

    if (rt_device_open(dev, RT_DEVICE_FLAG_RDWR) != RT_EOK)
    {
        rt_kprintf("open device failed!\n");
        return;
    }
    rt_device_control(dev, RT_SENSOR_CTRL_SET_ODR, (void *)100);

    while (1)
    {
        res = rt_device_read(dev, 0, &sensor_data, 1);
        if (res != 1)
        {
            rt_kprintf("read data failed!size is %d\n", res);
            rt_device_close(dev);
            return;
        }
        else
        {
            /* 申请一块内存 要是内存池满了 就挂起等待 */
            msg = rt_mp_alloc(tmp_msg_mp, RT_WAITING_FOREVER);
            msg->timestamp = sensor_data.timestamp;
            msg->int_value = sensor_data.data.temp;
            rt_mb_send(tmp_msg_mb, (rt_ubase_t)msg);
            msg = NULL;
        }
        rt_thread_mdelay(100);
    }
}

在上述代码中,该线程采集到一个温度数据之后,就会在内存池中申请内存块:

msg = rt_mp_alloc(tmp_msg_mp, RT_WAITING_FOREVER);

将温度数据存放到刚刚申请到的内存块里面:

msg->int_value = sensor_data.data.temp;

将这个存放着温度数据的内存块的地址给邮箱,然后发送邮件:

rt_mb_send(tmp_msg_mb, (rt_ubase_t)msg);

nrf24l01_thread 线程的入口函数是 nrf24l01_send_entry,如下:

static void nrf24l01_send_entry(void *parameter)
{
    struct tmp_msg *msg;
    struct hal_nrf24l01_port_cfg halcfg;
    nrf24_cfg_t cfg;
    uint8_t rbuf[32 + 1] = {0};
    uint8_t tbuf[32] = {0};

    nrf24_default_param(&cfg);
    halcfg.ce_pin = NRF24L01_CE_PIN;
    halcfg.spi_device_name = NRF24L01_SPI_DEVICE;
    cfg.role = ROLE_PTX;
    cfg.ud = &halcfg;
    cfg.use_irq = 0;
    nrf24_init(&cfg);

    while (1)
    {
        rt_thread_mdelay(100);
        
        if (rt_mb_recv(tmp_msg_mb, (rt_ubase_t*)&msg, RT_WAITING_FOREVER) == RT_EOK)
        {
            if (msg->int_value >= 0)
            {
                rt_sprintf((char *)tbuf, "temp:+%3d.%dC, ts:%d",
                           msg->int_value / 10, msg->int_value % 10, msg->timestamp);
            }
            else
            {
                rt_sprintf((char *)tbuf, "temp:-%2d.%dC, ts:%d",
                           msg->int_value / 10, msg->int_value % 10, msg->timestamp);
            }
            rt_kputs((char *)tbuf);
            rt_kputs("\n");
            rt_mp_free(msg); /* 释放内存块 */
            msg = RT_NULL;   /* 请务必要做 */
        }
        if (nrf24_ptx_run(rbuf, tbuf, rt_strlen((char *)tbuf)) < 0)
        {
            rt_kputs("Send failed! >>> ");
        }
    }
}

在上述代码中,nrf24l01 软件包提供了发送数据的 API nrf24_ptx_run

该线程接收ds18b20_thread 线程发送过来的邮件,并收到了温度数据:

rt_mb_recv(tmp_msg_mb, (rt_ubase_t*)&msg, RT_WAITING_FOREVER)

将温度数据发送出去:

nrf24_ptx_run(rbuf, tbuf, rt_strlen((char *)tbuf))

用完的内存块释放掉:

rt_mp_free(msg);
msg = RT_NULL;

还有两个问题没有解答:

  • 如果ds18b20_thread 线程采集温度数据过快,nrf24l01_thread 线程来不及发送,怎么办?
  • 如果nrf24l01_thread 线程发送数据过快,ds18b20_thread 线程来不及采集温度数据,怎么办?

这两个问题其实就是解决供过于求和供不应求的问题。

有没有留意到,申请内存块的代码上有一个 RT_WAITING_FOREVER,接收邮件的代码上也有一个 RT_WAITING_FOREVER

这两个 RT_WAITING_FOREVER 就是用来解决上面两个问题的。

当内存池满了的时候,再也申请不到内存块了,这时候申请内存块里面的 RT_WAITING_FOREVER 会使得 ds18b20_thread 线程阻塞,并挂起,然后 MCU 就会去干别的事情去了,不断的在 nrf24l01_thread 线程中发送存放在内存池中的温度数据,并释放掉内存块。等一有内存块可以申请了,ds18b20_thread 线程被唤醒,又会往里面塞数据了。

同理的,如果内存池是空的,里面没有数据,接收邮件里面的 RT_WAITING_FOREVER 会使得nrf24l01_thread 线程阻塞,并挂起,然后 MCU 就会去干别的事情去了,在 ds18b20_thread 线程中采集温度,并申请内存块塞数据进去,内存块一旦有数据,就会发邮箱,另外一边一有邮箱收到了,就又开始工作了。

4. nrf24l01 的多通道数据接收

nrf24l01 的多通道数据接收与其底层驱动相关,会在后期单独写一篇文章介绍放到 GitHub 上,敬请期待。

5. 结果

在这里插入图片描述
这里只是展示三个发送节点的情况,接收节点这边代码上已经把六个节点全部支持了。手头板子够多的话,把六个发送节点全部弄出来也是OK的。

6. 开源代码

为了更进一步便于大家学习,第二周任务的代码已经开源啦~ 请点击这里查看

7. 注意事项

在这里插入图片描述

  • RECEIVE(stm32l475-atk-pandora) 是接收节点的工程,支持6个数据通道接收数据
  • SEND1(stm32f407-atk-explorer) 是发送节点1的工程,使用 nrf24l01 的通道0传输,消息队列 demo 工程
  • SEND2(stm32f407-atk-explorer) 是发送节点2的工程,使用 nrf24l01 的通道1传输,消息队列 demo 工程
  • SEND3(stm32f103-dofly-M3S) 是发送节点3的工程,使用 nrf24l01 的通道2传输,消息队列 demo 工程
  • SEND4(stm32f103-dofly-M3S) 是发送节点4的工程,使用 nrf24l01 的通道3传输,消息队列 demo 工程
  • SEND5(stm32f103-dofly-M3S) 是发送节点5的工程,使用 nrf24l01 的通道4传输,消息队列 demo 工程
  • SEND6(stm32f103-dofly-M3S) 是发送节点6的工程,使用 nrf24l01 的通道5传输,消息队列 demo 工程
  • SEND1(stm32f407-atk-explorer) (mailbox+mempool)是发送节点1的工程,使用 nrf24l01 的通道0传输,邮箱+内存池 demo 工程
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智能家居软件设计可以分为两个部分,一个是设备控制,另一个是云端管理。下面我将分别介绍这两个部分的设计思路。 1. 设备控制 设备控制主要涉及到传感器数据采集、控制器控制、数据上传等方面。在这里,我们可以采用RT-Thread作为操作系统,因为RT-Thread是一个轻量级的实时操作系统,非常适合在嵌入式系统运行。同时,RT-Thread还提供了许多常用的驱动程序,可以方便地与各种传感器和控制器进行交互。 在设备控制方面,我们可以使用OneNET平台提供的MQTT协议进行通信。通过MQTT协议,设备可以向OneNET平台上传数据,也可以从OneNET平台获取控制指令。同时,OneNET平台还提供了Web API,可以方便地与其他系统进行集成。 2. 云端管理 在云端管理方面,我们需要设计一个Web后台管理系统,用于管理设备、用户和数据。在这里,我们可以使用Python的Django框架进行开发。Django是一个非常成熟的Web框架,可以快速地搭建一个高效、安全的后台管理系统。 在Web后台管理系统,我们可以实现以下功能: - 设备管理:添加、删除、修改设备信息; - 用户管理:添加、删除、修改用户信息; - 数据管理:查看设备上传的数据,并进行分析、统计等处理; - 控制指令:向设备发送控制指令; - 集成OneNET平台:通过OneNET平台提供的Web API,实现与其他系统的集成。 综上所述,基于RT-Thread及OneNET的智能家居软件设计可以实现设备控制和云端管理两个方面的功能。通过此设计,用户可以方便地监控和控制家的各种设备,并且可以通过云端管理系统对数据进行分析和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值