本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)
文章目录
邮箱的基本概念
邮箱在操作系统中是一种常用的 IPC 通信方式,邮箱可以在线程与线程之间。中断与线程之间进行消息的传递,此外,邮箱相比于信号量与消息队列来说,其开销更低,效率更高,所以常用来做线程与线程、中断与线程间的通信。邮箱中的每一封邮件只能容纳固定的 4 字节内容(STM32是 32 位处理系统,一个指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针),当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。
——原文
邮箱和消息队列很相似,只不过邮箱的大小只能是固定的4字节,但消息队列可以是任意长度。
邮箱的运作机制
邮箱运行原理如下图所示,下图中,线程或中断服务函数把4字节长度的邮件发送到邮箱中,线程等待队列里有若干个线程正在等待接收邮箱。
线程能够从邮箱里面读取邮件消息,当邮箱中的邮件是空时,根据用户自定义的阻塞时间决定是否挂起读取线程;当邮箱中有新邮件时,挂起的读取线程被唤醒,邮箱也是一种异步的通信方式。
通过邮箱,线程或中断服务函数可以将一个或多个邮件放入邮箱中。同样,一个或多个线程可以从邮箱中获得邮件消息。当有多个邮件发送到邮箱时,通常应将先进入邮箱的邮件先传给线程,也就是说,线程先得到的是最先进入邮箱的消息,即先进先出原则 (FIFO),同时 RT-Thread 中的邮箱支持优先级,也就是说在所有等待邮件的线程中优先级最高的会先获得邮件。——原文
邮箱的应用场景
RT-Thread 的邮箱可以存放固定的邮件数量,这个数量在邮箱初始化或创建是确定。虽然一个邮件的大小只能为4个字节,但是可以用一指向缓存区(数组,每个元素4个字节)的指针作为邮件,这样就能发送较大的内容。
与其他通信方式比较,邮箱的特点是开销低,效率高。邮箱发送接收都支持阻塞等待,很适合线程间、中断与线程间通信,但 要注意不能在中断里使用阻塞的方式发送邮件。
由于邮箱邮件大小只能为4个字节,且32位系统的指针正好为4字节,所以邮箱很适合用来传递指针数据。
邮箱的使用技巧
在实际项目开发中,很多信息是结构体变量,可以使用邮箱传递吗?答案是没问题,只需用一个结构体指针。
//定义一个结构体指针(结构体成员自定义)
struct temp *pTemp;
//申请动态堆空间
pTemp = (struct temp*)rt_malloc(sizeof(struct temp));
//发送方在数据处理完成后,将这个消息指针发送给一个邮箱,邮箱名为mb
rt_mb_send(mb, (rt_uint32_t)pTemp);
由于上面的消息指针的内存空间是我们手动申请的,在线程接收到邮箱信息且处理完成后,还需要手动释放消息指针的空间
struct temp *pTemp;
if(rt_mb_recv(mb, (rt_uint32_t *)&pTemp) == RT_EOK)
{
//数据处理完成后,释放消息指针内存空间
rt_free(pTemp);
}
邮箱控制块
struct rt_mailbox
{
struct rt_ipc_object parent; /**< 继承自ipc_object类 */
rt_ubase_t *msg_pool; /**< 邮箱缓冲区的开始地址 */
rt_uint16_t size; /**< 邮箱缓冲区的大小 */
rt_uint16_t entry; /**< 邮箱中邮件的数目 */
rt_uint16_t in_offset; /**< 邮箱缓冲的入口指针 */
rt_uint16_t out_offset; /**< 邮箱缓冲的出口指针 */
rt_list_t suspend_sender_thread; /**< 发送线程的挂起等待队列 */
};
邮箱的函数接口
创建邮箱 rt_mb_create()
rt_mailbox_t
rt_mb_create
(const char* name, rt_size_t size, rt_uint8_t flag);
创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量。使用 RT_IPC_FLAG_PRIO 优先级 flag 创建的 IPC 对象,在多个线程等待资源时,将由优先级高的线程优先获得资源。而使用 RT_IPC_FLAG_FIFO 先进先出 flag 创建的IPC 对象,在多个线程等待资源时,将按照先来先得的顺序获得资源。
参数 | 描述 |
---|---|
name | 邮箱的名称 |
size | 邮箱容量 |
flag | 邮箱的标志 |
删除邮箱 rt_mb_delete()
rt_err_t
rt_mb_delete
(rt_mailbox_t mb);
删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程获得返回值是-RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。
参数 | 描述 |
---|---|
mb | 邮箱对象的句柄 |
初始化邮箱 rt_mb_init()
rt_err_t
rt_mb_init
(rt_mailbox_t mb, const char* name, void* msgpool,
rt_size_t size, rt_uint8_t flag)
初始化邮箱时,该函数接口需要获得用户已经申请获得的邮箱对象控制块,缓冲区的指针,以及邮箱名称和邮箱容量。使用 RT_IPC_FLAG_PRIO 优先级 flag 创建的 IPC 对象,在多个线程等待资源时,将由优先级高的线程优先获得资源。而使用 RT_IPC_FLAG_FIFO 先进先出 flag 创建的IPC 对象,在多个线程等待资源时,将按照先来先得的顺序获得资源。
参数 | 描述 |
---|---|
mb | 邮箱对象的句柄 |
name | 邮箱的名称 |
msgpool | 缓冲区指针 |
size | 邮箱容量 |
flag | 邮箱的标志 |
邮箱接收 rt_mb_recv()
rt_err_t
rt_mb_recv
(rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);
接收邮件时,接收者需指定接收邮件的邮箱句柄,并指定接收到的邮件存放位置以及最多能够等待的超时时间。如果接收时设定了超时,当指定的时间内依然未收到邮件时,将返回-RT_ETIMEOUT。
参数 | 描述 |
---|---|
mb | 邮箱对象的句柄 |
value | 邮件内容 |
timeout | 指定超时时间 |
邮箱发送 rt_mb_send()(非阻塞)
rt_err_t
rt_mb_send
(rt_mailbox_t mb, rt_uint32_t value);
发送的邮件可以是32位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到-RT_EFULL 的返回值。
参数 | 描述 |
---|---|
mb | 邮箱对象的句柄 |
value | 邮件内容 |
邮箱发送 rt_mb_send_wait()(阻塞)
rt_err_t
rt_mb_send_wait
(rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout);
rt_mb_send_wait与rt_mb_send的区别在于,如果邮箱已经满了,那么发送线程将根据设定的timeout参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒返回错误码。
参数 | 描述 |
---|---|
mb | 邮箱对象的句柄 |
value | 邮件内容 |
timeout | 指定超时时间 |
邮箱实验
要想在RT-Thread中使用邮箱通信,需要先修改rtconfigh
配置文件。可以使用下面两种方法:
- 取消注释,开启宏定义,
- 或者使用
Configuration Wizard
向导进行图形化配置,
此实验参考原文对应实验代码,只包含man()函数,外设初始化相关代码下面未给出。
本实验需要创建两个线程,分别是接收线程和发送线程,接收线程负责接收邮箱信息并打印到串口,发送线程负责扫描按键,检测到按键按下,则向邮箱发送对应的邮箱信息。
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t recv_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
// 定义邮箱控制块
static rt_mailbox_t test_mb = RT_NULL;
// 实验需要用到的全局变量
char test_str1[] = "this is a mail test 1";
char test_str2[] = "this is a mail test 2";
/******************************************************************************
* @ 函数名 : recv_thread_entry
* @ 功 能 : 接收线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void recv_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
char *r_str;
while(1)
{
// 等待接收邮箱信息
uwRet = rt_mb_recv(test_mb, // 邮箱对象句柄
(rt_uint32_t*)&r_str, // 接收邮箱信息
RT_WAITING_FOREVER); // 超时一直等
if(uwRet == RT_EOK)
{
rt_kprintf("邮箱接收到的内容:%s\n\n", r_str);
LED0_TOGGLE; // LED0 反转
}
else
{
rt_kprintf("邮箱接收错误!\n");
}
}
}
/******************************************************************************
* @ 函数名 : send_thread_entry
* @ 功 能 : 发送线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void send_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
while(1)
{
// KEY0 被按下
if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
{
rt_kprintf("send:KEY0被单击\n");
// 发送邮箱信息1
uwRet = rt_mb_send(test_mb, (rt_uint32_t)&test_str1);
if(uwRet == RT_EOK)
rt_kprintf("邮箱信息发送成功!\n\n");
else
rt_kprintf("邮箱信息发送失败!\n\n");
}
// WK_UP 被按下
if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
{
rt_kprintf("send:WK_UP被单击\n");
// 发送邮箱2
uwRet = rt_mb_send(test_mb, (rt_uint32_t)&test_str2);
if(uwRet == RT_EOK)
rt_kprintf("邮箱信息发送成功!\n\n");
else
rt_kprintf("邮箱信息发送失败!\n\n");
}
rt_thread_delay(20); //每20ms扫描一次
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个邮箱
test_mb = // 邮箱控制块指针
rt_mb_create("test_mb", // 邮箱初始值
10, // 邮箱大小
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_mb != RT_NULL)
rt_kprintf("邮箱创建成功!\n");
// 创建一个动态线程
recv_thread = // 线程控制块指针
rt_thread_create("recv", // 线程名字
recv_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(recv_thread != RT_NULL)
rt_thread_startup(recv_thread);
else
return -1;
// 创建一个动态线程
send_thread = // 线程控制块指针
rt_thread_create("send", // 线程名字
send_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
实验现象
按下KEY0时,send线程向recv线程发送邮箱信息1,按下WK_UP时,send线程向recv线程发送邮箱信息2;recv线程接收到邮箱信息时,打印接收到的内容: