Rt-Thread学习笔记-----邮箱(八)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前言

前面讲了RT-Thread的信号量、互斥量以及事件集这些都是线程间的同步方式。在我们进行实际的项目开发的时候,经常会涉及到一个线程更新某个全局变量值,然后另外一个线程去读取这个全局变量值,根据这个全局变量值的不同而去执行不同的操作,在RT-Thread 中则提供了更多的工具帮助在不同的线程中间传递信息,包括邮箱、消息队列、信号用于线程间的通信方式。本文将RT-Thread的邮箱服务,包括邮箱工作机制、工作管理方式以及应用示例,基于战舰开发板进行实验,单片机为STM32F103ZET6。

一、邮箱的基本概念

邮箱在操作系统中是一种常用的 IPC 通信方式,邮箱可以在线程与线程之间、中断与线程之间进行消息的传递,此外,邮箱相比于信号量与消息队列来说,其开销更低,效率更高,所以常用来做线程与线程、中断与线程间的通信。邮箱中的每一封邮件只能容纳固定的 4 字节内容(STM32 是 32 位处理系统,一个指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针),当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。

线程能够从邮箱里面读取邮件消息,当邮箱中的邮件是空时,根据用户自定义的阻塞时间决定是否挂起读取线程;当邮箱中有新邮件时,挂起的读取线程被唤醒,邮箱也是一种异步的通信方式。

通过邮箱,线程或中断服务函数可以将一个或多个邮件放入邮箱中。同样,一个或多个线程可以从邮箱中获得邮件消息。当有多个邮件发送到邮箱时,通常应将先进入邮箱的邮件先传给线程,也就是说,线程先得到的是最先进入邮箱的消息,即先进先出原则(FIFO),同时 RT-Thread 中的邮箱支持优先级,也就是说在所有等待邮件的线程中优先级最高的会先获得邮件。

RT-Thread 中使用邮箱实现线程异步通信工作,具有如下特性:

  1. 邮件支持先进先出方式排队与优先级排队方式,支持异步读写工作方式。
  2. 发送与接收邮件均支持超时机制。
  3. 一个线程能够从任意一个消息队列接收和发送邮件。
  4. 多个线程能够向同一个邮箱发送邮件和从中接收邮件。
  5. 邮箱中的每一封邮件只能容纳固定的 4字节内容(可以存放地址)。
  6. 当队列使用结束后,需要通过删除邮箱以释放内存。

邮箱与消息队列很相似,消息队列中消息的长度是可以由用户配置的,但邮箱中邮件的大小却只能是固定容纳 4 字节的内容,所以,使用邮箱的开销是很小的,因为传递的只能是 4 字节以内的内容,那么其效率会更高。

二、邮箱的工作机制

创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量,接着再初始化消息队列,此时消息队列为空。

RT-Thread 操作系统的邮箱对象由多个元素组成,当邮箱被创建时,它就被分配了邮箱控制块:邮箱名称、邮箱缓冲区起始地址、邮箱大小等。同时每个邮箱对象中包含着多个邮件框,每个邮件框可以存放一封邮件;所有邮箱中的邮件框总数即是邮箱的大小,这个大小可在邮箱创建时指定。

线程或者中断服务程序都可以给邮箱发送邮件,非阻塞方式的邮件发送过程能够安全的应用于中断服务中,中断服务函数、定时器向线程发送消息的有效手段,而阻塞方式的邮件发送只能应用于线程中。当发送邮件时,当且仅当邮箱还没满邮件的时候才能进行发送,如果邮箱已满,可以根据用户设定的等待时间进行等待,当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程,当等待时间到了还未完成发送邮件,或者未设置等待时间,此时发送邮件失败,发送邮件的线程或者中断程序会收到一个错误码(-RT_EFULL)。线程发送邮件可以带阻塞,但在中断中不能采用任何带阻塞的方式发送邮件。

接收邮件时,根据邮箱控制块中的 entry判断队列是否有邮件,如果邮箱的邮件非空,那么可以根据 out_offset 找到最先发送到邮箱中的邮件进行接收。在接收时如果邮箱为空,如果用户设置了等待超时时间,系统会将当前线程挂起,当达到设置的超时时间,邮箱依然未收到邮件时,那么线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收线程中。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。

当邮箱不再被使用时,应该删除它以释放系统资源,一旦操作完成,邮箱将被永久性的删除。
在这里插入图片描述

三、邮箱的应用场景

RT-Thread 操作系统的邮箱中可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设定,每个邮件大小为 4 字节。当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。

与系统其它通信方式相比,邮箱的通信开销更低,效率更高,无论是什么消息,传递的都是 4 个字节的邮件,所以经常应用在众多领域,另外其实现的发送/接收阻塞机制,能很好应用于线程与线程,中断与线程之间的通讯。

其实邮箱中每封邮件的大小为 4 个字节,在 32 位系统中,刚好能存放一个指针,所以,邮箱也特别适合那种仅传递地址情况。

四、邮箱的相关函数

1、创建动态邮箱函数:

创建邮箱对象时会先从对象管理器中分配一个邮箱对象,然后给邮箱动态分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件数目和发送邮件在邮箱中的偏移量,动态创建一个邮箱对象可以调用如下的函数接口:

rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag);

(1)入口参数:

name:邮箱名称。
size:邮箱容量。
flag:邮箱标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO。

(2)返回值:

RT_NULL:创建失败。
邮箱对象的句柄:创建成功。

2、删除动态邮箱函数:

当用 rt_mb_create() 创建的邮箱不再被使用时,应该删除它来释放相应的系统资源,一旦操作完成,邮箱将被永久性的删除。删除邮箱时,如果有线程被挂起在该邮箱对象上,内核先唤醒挂起在该邮箱上的所有线程(线程返回值是 RT_ERROR),然后再释放邮箱使用的内存,最后删除邮箱对象。删除邮箱的函数接口如下:

rt_err_t rt_mb_delete (rt_mailbox_t mb);

(1)入口参数:

mb:要删除的邮箱对象的句柄。

(2)返回值:

RT_EOK:成功。

3、创建静态邮箱函数:

这里所说的创建静态邮箱和《RT-Thread编程指南》所讲的初始化邮箱是一样的,跟动态创建邮箱类似,只是初始化邮箱用于静态邮箱对象的初始化。与创建邮箱不同的是,静态邮箱对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中,其余的初始化工作与创建邮箱时相同。初始化邮箱时,该函数接口需要获得用户已经申请获得的邮箱对象控制块,缓冲区的指针,以及邮箱名称和邮箱容量(能够存储的邮件数)。函数接口如下:

rt_err_t rt_mb_init(rt_mailbox_t mb,
                    const char  *name,
                    void        *msgpool,
                    rt_size_t    size,
                    rt_uint8_t   flag);

(1)入口参数:

mb:邮箱对象的句柄。
name:邮箱名称。
msgpool:缓冲区指针。
size:邮箱容量。
flag:邮箱标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO。

(2)返回值:

RT_EOK:成功。

注意:这里的 size 参数指定的是邮箱的容量,即如果 msgpool 指向的缓冲区的字节数是 N,那么邮箱容量应该是 N/4。

4、删除静态邮箱函数:

这里所说的删除静态邮箱和《RT-Thread编程指南》所讲的脱离邮箱是一样的,脱离邮箱将把静态初始化的邮箱对象从内核对象管理器中脱离,内核先唤醒所有挂在该邮箱上的线程(线程获得返回值是 RT_ERROR),然后将该邮箱对象从内核对象管理器中脱离。脱离邮箱使用下面的接口:

rt_err_t rt_mb_detach(rt_mailbox_t mb);

(1)入口参数:

mb:邮箱对象的句柄。

(2)返回值:

RT_EOK:成功。

5、发送邮件函数:

线程或者中断服务程序可以通过邮箱给其他线程发送邮件,发送的邮件可以是 32 位任意格式的数据,一个整型值或者一个指向缓冲区的指针。当邮箱中的邮件已经满时,发送邮件的线程或者中断程序会收到 RT_EFULL 的返回值。函数接口如下:

rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value);

(1)入口参数:

mb:邮箱对象的句柄。
value:邮件内容。

(2)返回值:

RT_EOK:发送成功。
RT_EFULL:邮箱已经满了。

6、等待方式发送邮件函数:

用户也可以通过如下的函数接口向指定邮箱发送邮件:

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 参数等待邮箱中因为收取邮件而空出空间。如果设置的超时时间到达依然没有空出空间,这时发送线程将被唤醒并返回错误码。

(1)入口参数:

mb:邮箱对象的句柄。
value:邮件内容。
timeout:超时时间。

(2)返回值:

RT_EOK:发送成功。
RT_ETIMEOUT:超时。
RT_ERROR:失败,返回错误。

7、接收邮件函数:

接收邮件时,接收者需指定接收邮件的邮箱句柄,并指定接收到的邮件存放位置以及最多能够等待的超时时间。接收邮件函数接口如下:

rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t* value, rt_int32_t timeout);

(1)入口参数:

mb:邮箱对象的句柄。
value:邮件内容。
timeout:超时时间。

(2)返回值:

RT_EOK:发送成功。
RT_ETIMEOUT:超时。
RT_ERROR:失败,返回错误。

五、邮箱实验

#include "mailbox.h"
#include "board.h"
/*
******************************************************************
*                               变量
******************************************************************
*/
/* 定义线程控制块 */
static rt_thread_t receive_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
/* 定义邮箱控制块 */
static rt_mailbox_t test_mail = RT_NULL;

/************************* 全局变量声明 ****************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
char test_str1[] = "this is a mail test 1";/* 邮箱消息test1 */
char test_str2[] = "this is a mail test 2";/* 邮箱消息test2 */
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void receive_thread_entry(void* parameter);
static void send_thread_entry(void* parameter);

/*
*************************************************************************
*                             main 函数
*************************************************************************
*/
/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int Mail_Box_Init(void)
{	
    /* 
	 * 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,
	 * 即在component.c文件中的rtthread_startup()函数中完成了。
	 * 所以在main函数中,只需要创建线程和启动线程即可。
	 */
	rt_kprintf("这是一个RTT邮箱消息实验!\n");
	rt_kprintf("按下K1 | K2进行邮箱实验测试!\n");
  /* 创建一个邮箱 */
  test_mail = rt_mb_create("test_mail", 			/* 邮箱名字 */
                            10,         			/* 邮箱大小 */
                            RT_IPC_FLAG_FIFO);		/* 信号量模式 FIFO(0x00)*/
  if (test_mail != RT_NULL)
    rt_kprintf("邮箱创建成功!\n\n");
    
	receive_thread =                          /* 线程控制块指针 */
    rt_thread_create( "receive",              /* 线程名字 */
                      receive_thread_entry,   /* 线程入口函数 */
                      RT_NULL,            	  /* 线程入口函数参数 */
                      512,                 	  /* 线程栈大小 */
                      3,                   	  /* 线程的优先级 */
                      20);                 	  /* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (receive_thread != RT_NULL)
        rt_thread_startup(receive_thread);
    else
        return -1;
    
  send_thread =                            /* 线程控制块指针 */
    rt_thread_create( "send",              /* 线程名字 */
                      send_thread_entry,   /* 线程入口函数 */
                      RT_NULL,             /* 线程入口函数参数 */
                      512,                 /* 线程栈大小 */
                      2,                   /* 线程的优先级 */
                      20);                 /* 线程时间片 */
                   
    /* 启动线程,开启调度 */
   if (send_thread != RT_NULL)
        rt_thread_startup(send_thread);
    else
        return -1;
}

/*
*************************************************************************
*                             线程定义
*************************************************************************
*/

static void receive_thread_entry(void* parameter)
{		
  rt_err_t uwRet = RT_EOK;
  char *r_str;
  /* 任务都是一个无限循环,不能返回 */
  while(1)
	{
    /* 等待接邮箱消息 */
    uwRet = rt_mb_recv(test_mail, 					/* 邮箱对象句柄 */
                      (rt_uint32_t*)&r_str, 		/* 接收邮箱消息 */
                      RT_WAITING_FOREVER);			/* 指定超时事件,一直等 */
    
      if(RT_EOK == uwRet) 							/* 如果接收完成并且正确 */
      {
        rt_kprintf ( "邮箱的内容是:%s\n\n",r_str);		
        LED1_TOGGLE;       //LED1	反转
      }
      else
        rt_kprintf ( "邮箱接收错误!错误码是0x%x\n",uwRet);	
  }
}

static void send_thread_entry(void* parameter)
{	
  rt_err_t uwRet = RT_EOK;
    /* 任务都是一个无限循环,不能返回 */
  while (1)
  {     //如果KEY1被单击
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )      
		{
      rt_kprintf ( "KEY1被单击\n" );
			/* 发送一个邮箱消息1 */
			uwRet = rt_mb_send(test_mail,(rt_uint32_t)&test_str1); 
      if(RT_EOK == uwRet)
        rt_kprintf ( "邮箱消息发送成功\n" );
      else
        rt_kprintf ( "邮箱消息发送失败\n" );
		}
      //如果KEY2被单击
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) 
		{
      rt_kprintf ( "KEY2被单击\n" );	
			/* 发送一个邮箱2 */
			uwRet = rt_mb_send(test_mail,(rt_uint32_t)&test_str2); 
      if(RT_EOK == uwRet)
        rt_kprintf ( "邮箱消息发送成功\n" );
      else
        rt_kprintf ( "邮箱消息发送失败\n" );
		}
		rt_thread_delay(20);     //每20ms扫描一次		
  }
}


//---------------------------------------------------------
//						实验二
//---------------------------------------------------------
//创建一个邮箱,两个线程,其中一个线程用于发送邮件,另外一个
//线程由于接收邮件。通过按下不同按键发送不同的邮件内容,根据
//读取到右邮件内容执行不同操作。当读取到内容为KEY0按下时点亮
//RGB红灯,其他熄灭,当读取到内容为KEY1按下时点亮RGB蓝灯,其
//他熄灭,当读取到内容为KEY0按下时点亮RGB绿灯,其他熄灭。
//---------------------------------------------------------
#if 0

/* 线程句柄 */
static rt_thread_t thread1 = RT_NULL;
static rt_thread_t thread2 = RT_NULL;
 
/* 邮箱句柄 */
static rt_mailbox_t mailbox1 = RT_NULL;
 
 
char mailbox_msg_key0_press[] = "mailbox_msg_key0_press";
char mailbox_msg_key1_press[] = "mailbox_msg_key1_press";
char mailbox_msg_key2_press[] = "mailbox_msg_key2_press";
 
 
/**************************************************************
函数名称 : thread1_recv_mailbox_msg
函数功能 : 线程1入口函数,用于接收邮件
输入参数 : parameter:入口参数
返回值  	 : 无
备注		 : 无
**************************************************************/
void thread1_recv_mailbox_msg(void *parameter)
{
	char *mb_msg;
	
	while(1)
	{
		if(rt_mb_recv(mailbox1, (rt_uint32_t *)&mb_msg, RT_WAITING_FOREVER) == RT_EOK)
		{
			rt_kprintf("recv mb_msg:%s\r\n", mb_msg);
 
			if(0 == strcmp(mb_msg, "mailbox_msg_key0_press"))
			{
				LED_R(0);
				LED_B(1);
				LED_G(1);
			}
			else if(0 == strcmp(mb_msg, "mailbox_msg_key1_press"))
			{
				LED_R(1);
				LED_B(0);
				LED_G(1);
			}
			else if(0 == strcmp(mb_msg, "mailbox_msg_key2_press"))
			{
				LED_R(1);
				LED_B(1);
				LED_G(0);
			}
		}
		rt_thread_mdelay(1);
	}
}
 
/**************************************************************
函数名称 : thread2_send_mailbox_msg
函数功能 : 线程2入口函数,用于发送邮件
输入参数 : parameter:入口参数
返回值  	 : 无
备注		 : 无
**************************************************************/
void thread2_send_mailbox_msg(void *parameter)
{
	u8 key;
	
	while(1)
	{
		key = key_scan(0);
		
		if(key== KEY0_PRES)
		{
			rt_mb_send(mailbox1, (rt_uint32_t)&mailbox_msg_key0_press);
		}
		else if(key== KEY1_PRES)
		{
			rt_mb_send(mailbox1, (rt_uint32_t)&mailbox_msg_key1_press);
		}
		else if(key== KEY2_PRES)
		{
			rt_mb_send(mailbox1, (rt_uint32_t)&mailbox_msg_key2_press);
		}
		
		rt_thread_mdelay(1);
	}
}
 
 
void rtthread_mailbox_test(void)
{
	mailbox1 = rt_mb_create("mailbox1", 12, RT_IPC_FLAG_FIFO);	/* FIFO模式 */
 
	if(mailbox1 != RT_NULL)
	{
		rt_kprintf("RT-Thread create mailbox successful\r\n");
	}
	else
	{
		rt_kprintf("RT-Thread create mailbox failed\r\n");
		return;
	}
 
	thread1 = rt_thread_create("thread1",
				thread1_recv_mailbox_msg,
				NULL,
				512,
				3,
				20);
 
	if(thread1 != RT_NULL)
	{
		rt_thread_startup(thread1);;
	}
	else
	{
		rt_kprintf("create thread1 failed\r\n");
		return;
	}
 
	thread2 = rt_thread_create("thread2",
				thread2_send_mailbox_msg,
				NULL,
				1024,
				2,
				20);
 
	if(thread2 != RT_NULL)
	{
		rt_thread_startup(thread2);;
	}
	else
	{
		rt_kprintf("create thread2 failed\r\n");
		return;
	}
}


#endif

下载编译:rtthread邮箱实验工程

在这里插入图片描述
参考文献:
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》

2、《RT-THREAD 编程指南》

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Terry.Z_1009

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值