FreeRTOSFreeRTOS学习笔记(12)— FreeRTOS的线程间通信(CMSIS_API)

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FreeRTOSFreeRTOS学习笔记(12)— FreeRTOS的线程间通信(CMSIS_API)相关的知识,希望对你有一定的参考价值。


FreeRTOS的线程间通信

线程间通信

1、什么是线程间通信

线程间通信就是线程间进行资源(信息)共享。

2、最简单的通信方式

最简单的通信方式:使用全局变量来通信。

在我们前面所举的例子中,LED 线程和按键线程之间就是通过全局变量来通信的,使用全局变量通信的方式是线程间通信的最简单方式。

但是使用全局变量通信不够安全,之所以不够安全是因为除了通信双方线程外,其它所有线程也能访问全局变量,很容易被其它线程篡改内容,因此我们需要一种仅与通信双方有关的专用通信方式,本小节我们就是来介绍这些专用的通信方式。

3、线程间的专用通信方式

线程间的专用通信方式有哪些如下图所示:

Signal Events //信号
    osSignalSet : Set signal flags of a thread.
    osSignalClear : Reset signal flags of a thread.
    osSignalWait : Suspend execution until specific signal flags are set.
Mutexes //互斥锁
    osMutexCreate : Define and initialize a mutex.
    osMutexWait : Obtain a mutex or Wait until it becomes available.
    osMutexRelease : Release a mutex.
    osMutexDelete : Delete a mutex.
Semaphores //信号量
    osSemaphoreCreate : Define and initialize a semaphore.
    osSemaphoreWait : Obtain a semaphore token or Wait until it becomes available.
    osSemaphoreRelease : Release a semaphore token.
    osSemaphoreDelete : Delete a semaphore.
Memory Pool //内存池
    osPoolCreate : Define and initialize a fix-size memory pool.
    osPoolAlloc : Allocate a memory block.
    osPoolCAlloc : Allocate a memory block and zero-set this block.
    osPoolFree : Return a memory block to the memory pool.
Message Queue //消息队列
    osMessageCreate : Define and initialize a message queue.
    osMessagePut : Put a message into a message queue.
    osMessageGet : Get a message or suspend thread execution until message arrives.
Mail Queue //邮箱队列
    osMailCreate : Define and initialize a mail queue with fix-size memory blocks.
    osMailAlloc : Allocate a memory block.
    osMailCAlloc : Allocate a memory block and zero-set this block.
    osMailPut : Put a memory block into a mail queue.
    osMailGet : Get a mail or suspend thread execution until mail arrives.
    osMailFree : Return a memory block to the mail queue.

其实我们在前面就介绍过过:

  • 信号、消息队列、邮箱队列是真正的通信
  • 内存池:其实是 malloc 的替代方式,严格来说不算是通信
  • 互斥锁、信号量:借助通信而实现的资源保护

4、专用通信方式的通信原理

所有线程都是共享RTOS,所以所有的专用通信方式都是由RTOS来提供的,专用通信方式的原理说白了就是让共享的RTOS来转发信息。

一、信号(Signal Events)

1.1、什么是信号通信

通过调用RTOS的信号API来向指定线程发送一个整形数,这个整形数就被称为信号,发送什么整形数,以及收到该整形数后是什么意义,这个是由我们自己来约定的。

假设编程时约定好 A 线程发送 1 给 B 线程时就代表 k1 按键被按下了,那么 1 这个整形数(信号)作用就是告诉 B 线程按键 k1 被按下了,调用 RTOS 的信号 API 来收发信号(整形数),其实就是让 RTOS 转发信号这个整形数给对方线程。

1.2、信号这个整形数的范围

通过查看源码可知0~0x80000000(不包括0x80000000)之间的数都可以用来当作信号,至于说信号的含义是什么,这个就由编程者来自己来约定。

0x80000000之所以不能作为信号,是因为 0x80000000 被用来代表信号错误,后面还会提到0x80000000 这个玩意。如果你不知道这个范围也没关系,因为平时我们根本不会使用那么大数的信号,一般使用 100 以内的就很了不起了

1.3、宏

osFeature_Signals

这个宏的的作用是用来对信号进行限制的,但是ST公司在进行封装时虽然按照 CMSIS 的规则定义了这个宏,但是实际情况是并没有使用这个宏,既然没有使用,因此这里就不再介绍宏的具体作用。

1.4、API

Signal Events 
    osSignalSet : Set signal flags of a thread.
    osSignalClear : Reset signal flags of a thread.
    osSignalWait : Suspend execution until specific signal flags are set.

osSignalClear(清除信号)

这个函数用于清除信号,但是由于这个函数也没有定义,因此这里不再介绍。

osSignalSet(发送信号)

函数原型:int32_t osSignalSet(osThreadId thread_id,int32_t signal)
功能:向指定 ID 的线程发送信号。
参数:

  • thread_id:线程 ID句柄
  • signal:指定要发送的信号
  • 返回值:成功就返回上一次发送的信号,失败就返回 0x80000000(前面提到过)

osSignalWait(接收信号)

函数原型:osEvent osSignalWait (int32_t signals, uint32_t millisec)

osEvent osSignalWait (int32_t signals, uint32_t millisec)
{
  osEvent ret;

#if( configUSE_TASK_NOTIFICATIONS == 1 )
	
  TickType_t ticks;

  ret.value.signals = 0;  
  ticks = 0;
  if (millisec == osWaitForever) {
    ticks = portMAX_DELAY;
  }
  else if (millisec != 0) {
    ticks = millisec / portTICK_PERIOD_MS;
    if (ticks == 0) {
      ticks = 1;
    }
  }  
  
  if (inHandlerMode())
  {
    ret.status = osErrorISR;  /*Not allowed in ISR*/
  }
  else
  {
    if(xTaskNotifyWait( 0,(uint32_t) signals, (uint32_t *)&ret.value.signals, ticks) != pdTRUE)
    {
      if(ticks == 0)  ret.status = osOK;
      else  ret.status = osEventTimeout;
    }
    else if(ret.value.signals < 0)
    {
      ret.status =  osErrorValue;     
    }
    else  ret.status =  osEventSignal;
  }
#else
  (void) signals;
  (void) millisec;
	
  ret.status =  osErrorOS;	/* Task Notification not supported */
#endif
  
  return ret;
}

功能:接收某个信号。
参数:

  • signals:指定要接收信号
  • millisec:超时设置
    0: 不管有没有成功收到信号都立即返回
    osWaitForever:没有收到信号时休眠(阻塞),直到收到信号为止
    其它值,比如 100:如果没有收到信号时休眠阻塞 100ms,然后就超时返回,继续往
    后运行。
  • 返回值:返回类型为 osEvent 这个结构体类型
    事实上信号、消息队列、邮箱队列都会用到这个结构体类型。
typedef struct  {
  osStatus                 status;     ///< status code: event or error information
  union  {
    uint32_t                    v;     ///< message as 32-bit value
    void                       *p;     ///< message or mail as void pointer
    int32_t               signals;     ///< signal flags
  } value;                             ///< event value
  union  {
    osMailQId             mail_id;     ///< mail id obtained by \\ref osMailCreate
    osMessageQId       message_id;     ///< message id obtained by \\ref osMessageCreate
  } def;                               ///< event definition
} osEvent;
  • osStatus status :存放的枚举值用于表示是收到消息成功、失败或者超时。
    如果成功接收到信号:status 里面就放 osEventSignal,表示成功收到信号,所以当检测到 status的值为osEventSignal 时就表示成功收到了信号。至于说消息队列、邮箱队列的情况后面再介绍。如果接收失败:status 里面放的就是错误码。如果超时:里面放的就是osEventTimeout
  • value:联合体

联合体(union)的使用方法及其本质
C语言 | 联合体详解
【动画教程】研讨共用体,探究大小端存储模式(C语言)

  union  {
    uint32_t                    v;     ///< message as 32-bit value
    void                       *p;     ///< message or mail as void pointer
    int32_t               signals;     ///< signal flags
  } value;                             ///< event value

当使用的信号这种通信方式时,value里面的放的是信号这个整形数,我们此时应该使用signal来获取这个整形数,至于vp,后面讲消息队列和邮箱队列时再介绍。

  • def:联合体
  union  {
    osMailQId             mail_id;     ///< mail id obtained by \\ref osMailCreate
    osMessageQId       message_id;     ///< message id obtained by \\ref osMessageCreate
  } def;   

使用信号通信时,用不到def这个成员,同样的后面讲消息队列和邮箱队列时再介绍。

4.5、例子

osEvent evt = osSignalWait(3, 1000);// 在 1s 内如果没有收到信号 3 的话,就超时返回
if(evt.status == osEventSignal)
{
	printf("signal = %d\\r\\n", evt.value.signals);
	... //处理信号通知的事情
	...
}
else if(evt.status == osEventTimeout)
{
	printf("超时\\r\\n");
}
else
{
	printf("出错了\\r\\n");
}

1.6、案例

我们将之前通过全局变量通信的案例,改为使用信号来通信。

按键线程 ——> 信号 ——> LED 线程

为了简单一点,我们这里只使用k1LED1

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * File Name          : freertos.c
  * Description        : Code for freertos applications
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under Ultimate Liberty license
  * SLA0044, the "License"; You may not use this file except in compliance with
  * the License. You may obtain a copy of the License at:
  *                             www.st.com/SLA0044
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "SEGGER_RTT.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
uint8_t keyValue = 0;

/* USER CODE END Variables */
osThreadId myTask01Handle;
osThreadId myTask02Handle;

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
uint8_t KEY_Scan(void);
/* USER CODE END FunctionPrototypes */

void StartTask01(void const * argument);
void StartTask02(void const * argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* USER CODE BEGIN RTOS_MUTEX */
    /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* USER CODE BEGIN RTOS_SEMAPHORES */
    /* add semaphores, ... */
  /* USER CODE END RTOS_SEMAPHORES */

  /* USER CODE BEGIN RTOS_TIMERS */
    /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* USER CODE BEGIN RTOS_QUEUES */
    /* add queues, ... */
  /* USER CODE END RTOS_QUEUES */

  /* Create the thread(s) */
  /* definition and creation of myTask01 */
  osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
  myTask01Handle = osThreadCreate(osThread(myTask01), NULL);

  /* definition and creation of myTask02 */
  osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
  myTask02Handle = osThreadCreate(osThread(myTask02), NULL);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

}

/* USER CODE BEGIN Header_StartTask01 */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartTask01 */
void StartTask01(void const * argument)
{
  /* USER CODE BEGIN StartTask01 */
  /* Infinite loop */
	osEvent evt;
	static uint8_t flag1=0;	
	for(;;)
	{
		//没有收到信号时就一直休眠
		evt = osSignalWait(1, osWaitForever);//接收任务2发送来的信号数字 1,osWaitForever宏定义为0xFFFFFFFF
		if((evt.status == osEventSignal) && (1 == evt.value.signals))  
		{	
			SEGGER_RTT_printf(0, "recv signal = %d\\r\\n", evt.value.signals);			
			if(flag1 == 0) 	//点亮LED1
			{
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
				flag1 = 1;
			}
			else if(flag1 == 1) //熄灭LED1
			{
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
				flag1 = 0;
			}
		}
		//这里为什么没有超时打印?
		//因为如果没有是收到信号,而且也没有出错的话,osWaitForever会导致一直休眠,不会超时返回
		else
		{
			SEGGER_RTT_printf(0, "出错了\\r\\n");
		}
		osDelay(200);	
	}  
  
  /* USER CODE END StartTask01 */
}

/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */
    /* Infinite loop */	
	int32_t ret = 0;
	int keyValue = 0;	
	for(;;)
	{
		keyValue = KEY_Scan(); //获取按键的键值
		if(1 == keyValue)
		{
			ret = osSignalSet(myTask01Handle, 1); //向任务0的句柄ID 发送信号1,表示k1按下了
			if(0x80000000 == ret)
			{
				SEGGER_RTT_printf(0, "osSignalSet fail\\r\\n");//发送失败
			}
		}
		osDelay(200);	//延时200ms
	}
  /* USER CODE END StartTask02 */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
/* 检测k1、k2按键是否按下,并返回各自的键值,这里将k1的键值设定为1,k2的设定为2 */
uint8_t KEY_Scan(void)
{
    //没有key_up,会导致按下按键再松开之前,多次调用KEY_Scan时,每次都会检测到按键
    //被按下了,有了key_up,第一次调用KEY_Scan时返回键值,后面几次调用时会通过
    //key_up检测到已经返回过一次键值了,不再返回键值
    static uint8_t key_up = 0;	
	
	
    int KEY1 = HAL_GPIO_ReadPin(KEY1_GPIO_Port, GPIO_PIN_3);
    int KEY2 = HAL_GPIO_ReadPin(KEY2_GPIO_Port, GPIO_PIN_4);

    if((key_up == 0) && (KEY1 == 0 || KEY2 == 0)) //检测到刚按下进入,如果是按住不放不会进入
    {
        osDelay(100);                      //去抖动
        key_up = 1;                        //设置标志位,表示按下

        if(KEY1 == 0)return 1; 				//如果k1按下就返回1
        else if(KEY2 == 0)return 2; 		//如果k2按下就返回2
    }
    else if(KEY1 == 1 && KEY2 == 1) key_up = 0; 	 //按键松开,清标志位

    return 0;                              //无按键按下或松开了时就返回0
}

/* USER CODE END Application */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

如果线程里面要多次调用osSignalWait来接收多个信号时,不要将其设置为osWaitForever,这会导致多个osSignalWait相互堵,此时我们应该设置一个超时时间,如果没有收到信的话就超时返回,防止一直休眠下去,继续调用后面的osSignalWait,如此就不会照成多个osSignalWait
的相互阻塞。

二、消息队列

2.1、回顾信号

信号这种通信方式有缺点,缺点就是无法进行精确通信,信号只能告诉对方某事情发生了,但是无法告诉对方更多的信息,之所以这样是因为信号无法携带更多信息,这就好比长城上的狼烟信号,只能告诉你敌人来了,但是无法精确告诉你是什么敌人、有多少人、从哪个山头上来的等更多详细的信息,但是消息队列这种通信方式就可以发送更多的详细信息。

2.2、消息队列的实现原理

上图描述了消息队列的原理,RTOS 提供了一个“队列”,发送消息就是将消息挂入队列,取消息就是从队列中将消息取出。

消息队列可以发送简单的整形数,也可以发送复杂数据,发送复杂数据时就通过结构体来封装,然后将结构变量的指针发送给对方,对方即可从结构中取出复杂信息。

2.3、宏

osFeature_MessageQ(打开消息队列的宏)

  • 宏为 1:表示可以使用消息队列的 API
  • 宏为 0:表示不能使用消息队列的 API
    也就说可以通过这个宏来开启和关闭消息队列的 API,这个宏默认就为 1,表示可以使用消息队列的 API,如果不可用的话,我们就没办法使用消息队列了。

osMessageQDef

宏原型

#define osMessageQDef(name, queue_sz, type) \\
const osMessageQDef_t os_messageQ_def_##name = { (queue_sz), sizeof(type)}

这个宏的原理与osThreadDef是一样的。

osMessageQDef(msgq, 10, Msg *); 
等价于
const osMessageQDef_t os_messageQ_def_msgq = { (10), sizeof(Msg *)}
  • 作用:使用 osMessageQDef_t定义一个消息队列的结构体变量,后面创建消息队列时需要用到该变量。
  • 参数
    name:指定的名字,结构体变量名会在前面加上os_messageQ_def_前缀
    queue_sz:指定消息队列可容纳消息的上限,一般指定为 10即可
    type:指定消息的类型,如果是整形就指定为uint32_t等整数类型,如果是指针,就指定指针类型。

2.4、API

osMessageCreate(创建消息)

  • 函数原型:osMessageQId osMessageCreate (const osMessageQDef_t *queue_def, osThreadId thread_id)

  • 功能:使用osMessageQDef宏所定义的结构体变量来创建一个消息队列。这里讲的是CMSIS API,如果是FreeRTOS的原生API的话就是直接调用的xQueueCreate();函数。
  • 参数
    queue_def:通过osMessageQ 宏来指定osMessageQDef宏所定义结构体变量的地址。
    thread_id:指定创建该消息队里的那个线程的 ID,说白了就是记录下谁创建了这个线程,不过一般不需要,不需要时就设置为 NULL。

疑问:为什么是 NULL?
答:线程 ID句柄 的本质是一个指针,所以osThreadId这个类型是一个指针类型,由于是指针类型,不使用这个参数时就应该设置为 NULL。

  • 返回值:返回唯一识别消息队列的 ID,后续就是通过这个 ID 来操作消息队列。
  • 例子:
osMessageQId msgqId; //用于存放消息队列ID 句柄
/* 创建消息队列,创建消息队列的代码也可以放到某个线程函数中 */
osMessageQDef(msgq, 10, Msg *); 
msgqId = osMessageCreate(osMessageQ(msgq), NULL);	

osMessagePut(发送消息)

  • 函数原型:osStatus osMessagePut(osMessageQId queue_id,uint32_t info,uint32_t millisec)
  • 功能:发送消息
  • 参数:
    queue_id:指定消息队列的 ID,用于识别消息队列,然后将消息挂到该消息队列上。
    info:如果发送的是整形数就直接写该整形数,如果是指针则强转为 uint32_t 类型。
    millisec:超时设置
    osWaitForever:表示如果没有发送成功就一直休眠(阻塞),直到发送成功为止
    0:则表示不管发送成功没有都立即返回,发送消息时一般都设置为 0。
  • 其它值,比如 200:在 200ms 内如果没有发送成功的话就超时返回,继续往后运行。
  • 返回值:osStatus
  • 例子:

osMessageGet(接收消息)

  • 函数原型:osEvent osMessageGet(osMessageQId queue_id,uint32_t millisec)
  • 功能:接收消息
  • 参数
    queue_id:消息队列 ID,从指定从什么消息队列上获取取消息。
    millisec:超时设置
    osWaitForever:未接受到消息时一直阻塞,直到收到消息或者出错为止
    0:不管有没有接收到消息,函数被调用后都会立即返回,继续往后运行,不会阻塞
  • 其它值,比如 300:设置具体超时时间 300ms,如果在指定时间内容没有收到消息则超时返回,然后继续往后运行。
  • 返回值:osEvent,信号通信也用到了这个结构体
typedef struct  {
  osStatus                 status;     ///< status code: event or error information
  union  {
    uint32_t                    v;     ///< message as 32-bit value
    void                       *p;     ///< message or mail as void pointer
    int32_t               signals;     ///< signal flags
  } value;                             ///< event value
  union  {
    osMailQId             mail_id;     ///< mail id obtained by \\ref osMailCreate
    osMessageQId       message_id;     ///< message id obtained by \\ref osMessageCreate
  } def;                               ///< event definition
} osEvent;
  • osStatus status
    成功接收到消息时:里面放的是osEventMessage,只要检测到里面放的是osEventMessage,就表示成功接收到了消息。超时:里面放的是osEventTimeout。错误:错误码
  • value
    当通信方式为消息队列时。
    如果传输的是一个整形数:value 里面放的这个整形数,使用v来获取这个整形数如果传递的是一个指针:value里面放的就是这个指针,此时我们需要使用p来获取该指针,由于类型为void *,使用是需要强转为需要的指针类型。
  • def
    目前使用的是消息队列,因此里面放的是消息队列的ID,通过osMailQId即可取出消息队列的ID句柄。
  • 例子:后面再举

案例

我们这里使用消息队列来发送按键的键值,并且发送“key k1 pressed”字符

以上是关于FreeRTOSFreeRTOS学习笔记(12)— FreeRTOS的线程间通信(CMSIS_API)的主要内容,如果未能解决你的问题,请参考以下文章

FreeRTOSFreeRTOS学习笔记— 手写FreeRTOS双向链表/源码分析

FreeRTOSFreeRTOS学习笔记— 开始创建任务并测试任务代码

FreeRTOSFreeRTOS学习笔记— 学习FreeRTOS的编程风格和本质

FreeRTOSFreeRTOS学习笔记— 任务创建删除挂起和恢复

FreeRTOSFreeRTOS学习笔记— FreeRTOS任务与协程

FreeRTOSFreeRTOS学习笔记(10)— FreeRTOS的osThreadDef创建任务(CMSIS_API)