RT-Thread
学习笔记
邮箱工作机制
邮箱控制块
邮箱管理函数接口
创建与删除
初始化与脱离
发送邮件
接收邮件
应用示例
总结
RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
邮箱用于线程间通信(异步通信方式),如多个线程可以将它们检测到的按键状态、ADC采样值等信号发送到邮箱,其他线程向该邮箱取走需要的信息。
1 邮箱工作机制
- 邮箱中的每一封邮件只能容纳固定的4字节内容,当传递更多字节数据时,可以把指向一个数据缓冲区的指针发送到邮箱。
- 非阻塞方式的邮件收发可以在线程和中断上下文,阻塞方式的邮件收发只能在线程上下文中。
- 发送邮件:如果邮箱没满,将邮件复制到邮箱中;若满了,发送线程可以选择挂起等待或直接返回
-RT_EFULL
。如果发送线程选择挂起等待,在超时时间内,当邮箱中的邮件被收取空出空间时,该线程将被唤醒继续发送,否则超时返回-RT_EFULL
。 - 接收邮件:邮箱为空时,接收线程可以选择挂起等待。当超时时间到了依然未收到邮件,则选择挂起的线程被唤醒并返回
- RT_ETIMEOUT
;若邮箱中存在邮件,接收线程将复制邮箱中的4个字节邮件到接收缓存中。
- 邮箱缓冲区为循环队列(尾插头删),接收邮件指针
mb->out_offset
为队头,发送邮件指针mb->in_offset
为队尾。
2 邮箱控制块
struct rt_mailbox
{
struct rt_ipc_object parent;
rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */
rt_uint16_t size; /* 邮箱缓冲区的大小 */
rt_uint16_t entry; /* 邮箱中邮件的数目 */
rt_uint16_t in_offse; /* 邮箱缓冲区的入口指针(偏移量) */
rt_uint16_t out_offset; /* 邮箱缓冲区的出口指针(偏移量) */
rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */
};
typedef struct rt_mailbox* rt_mailbox_t;
-
mb->entry == 0
:邮箱空 -
mb->entry == mb->size
:邮箱满
3 邮箱管理函数接口
3.1 创建与删除
动态创建一个邮箱对象:
/**
* @param name 邮箱对象名称
* @param size 邮箱容量
* @param flag 邮箱标志: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
*
* @return 成功-rt_mailbox_t对象, 失败-RT_NULL
*/
rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
{
rt_mailbox_t mb;
RT_DEBUG_NOT_IN_INTERRUPT;
/* 分配邮箱对象 */
mb = (rt_mailbox_t)rt_object_allocate(RT_Object_Class_MailBox, name);
if (mb == RT_NULL)
return mb;
/* 设置接收线程等待模式 */
mb->parent.parent.flag = flag;
/* 初始化邮箱对象 */
rt_ipc_object_init(&(mb->parent)); // 调用rt_list_init(&(ipc->suspend_thread));
/* 初始化邮箱 */
mb->size = size;
mb->msg_pool = RT_KERNEL_MALLOC(mb->size * sizeof(rt_uint32_t)); // x4 每逢邮箱4 bytes
if (mb->msg_pool == RT_NULL)
{
/* 删除邮箱对象 */
rt_object_delete(&(mb->parent.parent)); // 已从内存堆分配空间, 故需要删除
return RT_NULL;
}
mb->entry = 0;
mb->in_offset = 0;
mb->out_offset = 0;
/* 额外初始化发送邮件挂起线程的链表, 接收线程挂起链表由ipc父类管理 */
rt_list_init(&(mb->suspend_sender_thread));
return mb;
}
- 将当前邮箱数目
entry
、邮箱出入口指针out_offse
和in_offse
(数组索引)均初始化为0 - 将接收、发送线程挂起链表分别初始化(前驱指针与后继指针均指向自身)
- 实际邮箱大小为邮件数量
size
* 每封邮件大小4
删除由rt_mb_create()
创建的邮箱:
rt_err_t rt_mb_delete(rt_mailbox_t mb)
{
RT_DEBUG_NOT_IN_INTERRUPT;
/* 唤醒挂起在该邮箱上的所有接收线程, 被唤醒的线程返回-RT_ERROR */
rt_ipc_list_resume_all(&(mb->parent.suspend_thread));
/* 唤醒挂起在该邮箱上的所有私有发送线程, 被唤醒的线程返回-RT_ERROR */
rt_ipc_list_resume_all(&(mb->suspend_sender_thread));
/* 释放邮箱缓冲区内存 */
RT_KERNEL_FREE(mb->msg_pool);
/* 删除邮箱对象 */
rt_object_delete(&(mb->parent.parent));
return RT_EOK;
}
- 先唤醒挂起在该邮箱上的所有接收和发送线程, 被唤醒的线程返回-RT_ERROR
- 释放邮箱缓冲区内存(动态申请的),然后删除邮箱对象
3.2 初始化与脱离
静态邮箱对象的初始化(其缓冲区是提前申请好的):
/**
* @param mb 静态邮箱对象
* @param name 邮箱对象名称
* @param msgpool 缓冲区指针(缓冲区需提前创建)
* @param size 邮箱容量
* @param flag 邮箱标志:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
*
* @return successful-RT_EOK
*/
rt_err_t rt_mb_init(rt_mailbox_t mb,
const char *name,
void *msgpool,
rt_size_t size,
rt_uint8_t flag)
{
RT_ASSERT(mb != RT_NULL);
/* 初始化object内核对象 */
rt_object_init(&(mb->parent.parent), RT_Object_Class_MailBox, name);
mb->parent.parent.flag = flag;
/* 初始化邮箱对象 */
rt_ipc_object_init(&(mb->parent));
mb->msg_pool = msgpool;
mb->size = size;
mb->entry = 0;
mb->in_offset = 0;
mb->out_offset = 0;
/* 额外初始化发送邮件挂起线程的链表, 接收线程挂起链表由ipc父类管理 */
rt_list_init(&(mb->suspend_sender_thread));
return RT_EOK;
}
- 动态创建的邮箱缓冲区是由系统从堆区分配的,静态初始化的邮箱缓冲区需要提前创建好。然后传入该缓冲区指针
- 邮箱容量
size
=sizeof(msg_pool) / 4
(msg_pool
为提前创建的邮箱缓冲区)
脱离rt_mb_init()
初始化的邮箱对象:
rt_err_t rt_mb_detach(rt_mailbox_t mb)
{
/* 唤醒挂起在该邮箱上的所有接收线程 */
rt_ipc_list_resume_all(&(mb->parent.suspend_thread));
/* 唤醒挂起在该邮箱上的所有私有发送线程, */
rt_ipc_list_resume_all(&(mb->suspend_sender_thread));
/* 脱离邮箱对象 */
rt_object_detach(&(mb->parent.parent));
return RT_EOK;
}
3.3 发送邮件
等待方式发送邮件:
/**
* @param mb 邮箱对象的句柄
* @param value 邮件内容
* @param timeout 等待时间
*
* @return 返回操作状态
*/
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
rt_uint32_t tick_delta;
/* 初始化系统时间差 */
tick_delta = 0;
thread = rt_thread_self();
RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mb->parent.parent)));
temp = rt_hw_interrupt_disable();
/*1 无阻塞调用 */
if (mb->entry == mb->size && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/*2 邮箱已满 */
while (mb->entry == mb->size)
{
/* 重置线程错误码 */
thread->error = RT_EOK;
// 这段代码是有必要的: 防止邮箱满时被误唤醒,timeout接近临界值
// 此时tick_delta(代码取指耗费时间)可能会>timeout,使得timeout为负值然后被置0
if (timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
RT_DEBUG_IN_THREAD_CONTEXT;
/* 挂起当前线程*/
rt_ipc_list_suspend(&(mb->suspend_sender_thread),
thread,
mb->parent.parent.flag);
/* 有等待时间(非永久等待),启动线程内置定时器 */
if (timeout > 0)
{
tick_delta = rt_tick_get(); // 获取当前系统时间
RT_DEBUG_LOG(RT_DEBUG_IPC, ("mb_send_wait: start timer of thread:%s\n",
thread->name));
/* 重置线程超时时间并开始定时 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
rt_schedule();
/* 超时时间到了邮箱还是满的 */
if (thread->error != RT_EOK)
{
return thread->error;
}
temp = rt_hw_interrupt_disable();
/* 如果不是永远等待(timeout == -1),则重新计算剩余超时时间 */
if (timeout > 0) // 防止邮箱满时被误唤醒
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
}
/* 将要发送的信息放入邮件中 */
mb->msg_pool[mb->in_offset] = value;
/* 邮件入口指针向后偏移,如果越界则置0 */
++ mb->in_offset;
if (mb->in_offset >= mb->size)
mb->in_offset = 0;
/* 邮件总数量+1 */
mb->entry ++;
// 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
if (!rt_list_isempty(&mb->parent.suspend_thread))
{
rt_ipc_list_resume(&(mb->parent.suspend_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
- 邮箱已满:
-
timeout == 0
:即无阻塞调用,直接返回-RT_EFULL
-
timeout < 0
(RT_WAITING_FOREVER == -1
):永久阻塞挂起直到邮箱非满(有线程接收了邮件) -
timeout > 0
:将线程内置定时器超时时间设为timeout
并启动,等待邮箱非满时被唤醒。可能存在被误唤醒的情况,因此用tick_delta
来记录定时器启动开始往后流逝的tick,如果被误唤醒则将剩余时间作为定时器超时时间并再次启动。但考虑到CPU取指消耗的时间(个人猜测),所以timeout
可能会被减为0,此时会返回-RT_EFULL
。
- 邮箱未满:
- 将要发送的信息放入邮件中,邮件入口指针(循环队列队尾指针)
mb->in_offset
向后偏移一位,如果越界则置零,从头开始;再让邮件总量mb->entry
+1。 - 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。
无等待发送邮件:
rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value)
{
return rt_mb_send_wait(mb, value, 0);
}
发送紧急邮件:
rt_err_t rt_mb_urgent(rt_mailbox_t mb, rt_ubase_t value)
{
register rt_ubase_t temp;
temp = rt_hw_interrupt_disable();
// 邮箱已满
if (mb->entry == mb->size)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* 倒回先前的位置 */
if (mb->out_offset > 0)
{
mb->out_offset --;
}
else
{
mb->out_offset = mb->size - 1;
}
/* 将要发送的信息放入邮件中 */
mb->msg_pool[mb->out_offset] = value;
/* 邮件总数量+1 */
mb->entry ++;
// 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
if (!rt_list_isempty(&mb->parent.suspend_thread))
{
_ipc_list_resume(&(mb->parent.suspend_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
采用无阻塞方式发送,邮件被直接插入到队头,使得接收者能够优先收到紧急邮件。
- 邮箱已满:直接返回
-RT_EFULL
- 邮箱未满:
- 让邮件出口指针
mb->out_offset
(循环队列队头指针)倒回到前一个位置,然后在该位置插入邮件,再让邮件总量mb->entry
+1。 - 如果接收线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。
3.4 接收邮件
/**
* @param mb 邮箱对象的句柄
* @param value 存放邮件内容的指针
* @param timeout 等待时间
*
* @return 成功-RT_EOK 失败-其他值
*/
rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
rt_uint32_t tick_delta;
tick_delta = 0;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
/*1 对于非阻塞调用,直接返回-RT_ETIMEOUT */
if (mb->entry == 0 && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
/*2 邮箱为空 */
while (mb->entry == 0)
{
/* reset error number in thread */
thread->error = RT_EOK;
/* 重置线程错误码 */
if (timeout == 0)
{
rt_hw_interrupt_enable(temp);
thread->error = -RT_ETIMEOUT;
return -RT_ETIMEOUT;
}
RT_DEBUG_IN_THREAD_CONTEXT;
/* 挂起当前线程 */
_ipc_list_suspend(&(mb->parent.suspend_thread),
thread,
mb->parent.parent.flag);
/* 有等待时间(非永久等待),启动线程内置定时器 */
if (timeout > 0)
{
tick_delta = rt_tick_get(); // 获取当前系统时间
/* 重置线程超时时间并开始定时 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
rt_schedule();
/* 超时时间到了邮箱还是满的 */
if (thread->error != RT_EOK)
{
return thread->error;
}
temp = rt_hw_interrupt_disable();
/* 如果不是永远等待(timeout == -1),则重新计算剩余超时时间 */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
}
/* 向接收邮件指针中填入邮件 */
*value = mb->msg_pool[mb->out_offset];
// 邮件出口指针向后移动
++ mb->out_offset;
if (mb->out_offset >= mb->size)
mb->out_offset = 0;
/* 邮箱总量-1 */
if(mb->entry > 0)
{
mb->entry --;
}
// 如果发送线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度
if (!rt_list_isempty(&(mb->suspend_sender_thread)))
{
_ipc_list_resume(&(mb->suspend_sender_thread));
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
- 邮箱为空:
-
timeout == 0
:即无阻塞调用,直接返回-RT_ETIMEOUT
-
timeout < 0
(RT_WAITING_FOREVER == -1
):永久阻塞挂起直到邮箱非空(有线程发送了邮件) -
timeout > 0
:将线程内置定时器超时时间设为timeout
并启动,等待邮箱非空时被唤醒。
- 邮箱非空:
- 将队头邮件填入到接收指针
*value
中,然后mb->out_offset
向后移动,邮件总量mb->entry
-1。 - 如果发送线程阻塞链表不为空,则唤醒该阻塞链表中第一个线程,并发起线程调度。
4 应用示例
/*
* Date Author
* 2022-02-12 issac wan
*/
#include <rtthread.h>
#define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 5
struct send_mb
{
rt_uint32_t a;
char* str;
};
struct send_mb urgent_mb_struct =
{
.a = 520,
.str = "logic"
};
static struct rt_mailbox mb; // 静态邮箱对象
static char mb_pool[256]; // 静态邮箱缓冲区
static const char* common_mb_str1 = "common_mb_str1";
static const char* common_mb_str2 = "common_mb_str2";
static const char* urgent_mb_str = "urgent_mb_str";
static const char* over_mb_str = "over";
static rt_sem_t sem;
static rt_thread_t thread1;
static rt_thread_t thread2;
static void thread1_entry(void* param)
{
void* ret_str;
while(1)
{
if (RT_EOK == rt_mb_recv(&mb, (rt_uint32_t *)&ret_str, RT_WAITING_FOREVER))
{
if (ret_str == urgent_mb_str)
{
my_printf("%s", (char*)ret_str);
rt_sem_take(sem, RT_WAITING_FOREVER);
// 信号量资源同步后,说明收到了结构体紧急邮件,因此非阻塞获取
rt_mb_recv(&mb, (rt_uint32_t *)&ret_str, 0);
my_printf("%d %s", ((struct send_mb*)ret_str)->a, ((struct send_mb*)ret_str)->str);
}
else if (ret_str == "over")
break;
else
my_printf("%s", (char*)ret_str);
}
rt_thread_mdelay(200);
}
my_printf("%s", (char*)ret_str);
rt_mb_detach(&mb); // 将邮箱对象脱离
rt_sem_delete(sem); // 删除信号量对象
}
static void thread2_entry(void* param)
{
rt_uint8_t cnt = 0;
while(cnt++ < 10)
{
if (cnt & 0x1) // 奇数
rt_mb_send(&mb, (rt_uint32_t)common_mb_str1);
else // 偶数
rt_mb_send(&mb, (rt_uint32_t)common_mb_str2);
rt_thread_mdelay(100);
}
rt_mb_urgent(&mb, (rt_uint32_t)urgent_mb_str); // 发送紧急邮件
rt_thread_mdelay(300); // 等待线程1收到紧急邮件
rt_mb_urgent(&mb, (rt_uint32_t)&urgent_mb_struct);
rt_sem_release(sem); // 释放资源唤醒线程1
rt_mb_send(&mb, (rt_uint32_t)over_mb_str);
}
int mailbox_sample(void){
rt_err_t result;
/* 静态初始化邮箱 */
result = rt_mb_init(&mb,
"test_mb",
mb_pool,
sizeof(mb_pool) / 4, // 邮箱中的邮件数目(一封邮件占4字节 )
RT_IPC_FLAG_FIFO);
if (result != RT_EOK)
return -RT_ERROR;
sem = rt_sem_create("sem", 0, RT_IPC_FLAG_PRIO);
thread1 = rt_thread_create("thread1",
thread1_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY - 1,
THREAD_TIMESLICE);
if (thread1 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread1);
thread2 = rt_thread_create("thread2",
thread2_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread2 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread2);
return RT_EOK;
}
INIT_APP_EXPORT(mailbox_sample);
- 线程1每200ms接收一封邮件,线程2每100ms发送一封普通邮件
- 当线程2普通邮件发送完后,会发生一封紧急邮件,通知线程1接下来要发送结构体变量
- 使用信号量进行同步,线程2成功发送结构体变量后,会释放信号量,唤醒线程1,此时线程1采用非阻塞的方式收取该邮件。
打印结果如下:
[3]common_mb_str1
[205]common_mb_str2
[407]common_mb_str1
[609]common_mb_str2
[811]common_mb_str1
[1013]common_mb_str2
[1215]urgent_mb_str
[1417]520 logic
[1618]common_mb_str1
[1820]common_mb_str2
[2022]common_mb_str1
[2224]common_mb_str2
[2426]over
5 总结
- 邮箱用于线程间异步通信,每封邮件4字节,可以用指针变量来接收邮件,来访问其数据缓冲区,实现不定长、任意类型的数据接收。
- 邮箱缓冲区为循环队列(尾插头删),元素大小4字节。
mb->msg_pool
为队列缓冲区首地址,mb->size
为队列最大容量,mb->out_offset
为队头,mb->in_offset
为队尾,用mb->entry
记录队列元素个数。 mb->entry == 0
表示邮箱空,mb->entry == mb->size
表示邮箱满- 发送邮件的线程阻塞挂起在
mb->suspend_sender_thread
链表上,接收邮件的线程阻塞挂起在mb->parent.suspend_thread
链表上。 - 静态初始化的邮箱需要先创建好缓冲区,将其地址传入
- 发送邮箱可以选择阻塞与非阻塞方式,其中紧急发送属于非阻塞,使队头指针
mb->out_offset
移动到其前一个位置,在此处插入邮件,接收线程可以直接从队头取走邮件。 - 发送局部变量时,需要注意其生命周期,如果线程或函数被系统回收了,那么邮件接收者使用的接收指针,会指向非法内存,成了野指针。
END