FreeRTOSFreeRTOS学习笔记(12)— FreeRTOS的线程间通信(CMSIS_API)
Posted 果果小师弟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FreeRTOSFreeRTOS学习笔记(12)— FreeRTOS的线程间通信(CMSIS_API)相关的知识,希望对你有一定的参考价值。
线程间通信
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
来获取这个整形数,至于v
和p
,后面讲消息队列和邮箱队列时再介绍。
- 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 线程
为了简单一点,我们这里只使用k1
和 LED1
。
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : freertos.c
* Description : Code for freertos applications
******************************************************************************
* @attention
*
* <h2><center>© 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)