线程间通信
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;
}
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;
#endif
return ret;
}
功能:接收某个信号。 参数:
- signals:指定要接收信号
- millisec:超时设置
0: 不管有没有成功收到信号都立即返回 osWaitForever:没有收到信号时休眠(阻塞),直到收到信号为止 其它值,比如 100:如果没有收到信号时休眠阻塞 100ms,然后就超时返回,继续往 后运行。 - 返回值:返回类型为 osEvent 这个结构体类型
事实上信号、消息队列、邮箱队列都会用到这个结构体类型。
typedef struct {
osStatus status;
union {
uint32_t v;
void *p;
int32_t signals;
} value;
union {
osMailQId mail_id;
osMessageQId message_id;
} def;
} osEvent;
osStatus status :存放的枚举值用于表示是收到消息成功、失败或者超时。 如果成功接收到信号:status 里面就放 osEventSignal ,表示成功收到信号,所以当检测到 status 的值为osEventSignal 时就表示成功收到了信号。至于说消息队列、邮箱队列的情况后面再介绍。如果接收失败:status 里面放的就是错误码。如果超时:里面放的就是osEventTimeout 。- value:联合体
联合体(union)的使用方法及其本质 C语言 | 联合体详解 【动画教程】研讨共用体,探究大小端存储模式(C语言)
union {
uint32_t v;
void *p;
int32_t signals;
} value;
当使用的信号这种通信方式时,value 里面的放的是信号这个整形数,我们此时应该使用signal 来获取这个整形数,至于v 和p ,后面讲消息队列和邮箱队列时再介绍。
union {
osMailQId mail_id;
osMessageQId message_id;
} def;
使用信号通信时,用不到def 这个成员,同样的后面讲消息队列和邮箱队列时再介绍。
4.5、例子
osEvent evt = osSignalWait(3, 1000);
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 。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
uint8_t keyValue = 0;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
uint8_t KEY_Scan(void);
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
}
void StartTask01(void const * argument)
{
osEvent evt;
static uint8_t flag1=0;
for(;;)
{
evt = osSignalWait(1, osWaitForever);
if((evt.status == osEventSignal) && (1 == evt.value.signals))
{
SEGGER_RTT_printf(0, "recv signal = %d\r\n", evt.value.signals);
if(flag1 == 0)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
flag1 = 1;
}
else if(flag1 == 1)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
flag1 = 0;
}
}
else
{
SEGGER_RTT_printf(0, "出错了\r\n");
}
osDelay(200);
}
}
void StartTask02(void const * argument)
{
int32_t ret = 0;
int keyValue = 0;
for(;;)
{
keyValue = KEY_Scan();
if(1 == keyValue)
{
ret = osSignalSet(myTask01Handle, 1);
if(0x80000000 == ret)
{
SEGGER_RTT_printf(0, "osSignalSet fail\r\n");
}
}
osDelay(200);
}
}
uint8_t KEY_Scan(void)
{
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;
else if(KEY2 == 0)return 2;
}
else if(KEY1 == 1 && KEY2 == 1) key_up = 0;
return 0;
}
如果线程里面要多次调用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;
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;
union {
uint32_t v;
void *p;
int32_t signals;
} value;
union {
osMailQId mail_id;
osMessageQId message_id;
} def;
} osEvent;
- osStatus status
成功接收到消息时:里面放的是osEventMessage ,只要检测到里面放的是osEventMessage ,就表示成功接收到了消息。超时:里面放的是osEventTimeout 。错误:错误码 - value
当通信方式为消息队列时。 如果传输的是一个整形数:value 里面放的这个整形数,使用v 来获取这个整形数如果传递的是一个指针:value 里面放的就是这个指针,此时我们需要使用p 来获取该指针,由于类型为void * ,使用是需要强转为需要的指针类型。 - def
目前使用的是消息队列,因此里面放的是消息队列的ID ,通过osMailQId 即可取出消息队列的ID句柄。 - 例子:后面再举
案例
我们这里使用消息队列来发送按键的键值,并且发送“key k1 pressed”字符串给对方,此时我就需要将消息封装为结构体,然后把结构体变量的指针发送给对方,对方即可取出使用,封装的结构体类型则由我们自己来定义。
typedef struct message
{
uint8_t keyValue;
char buf[15];
}Msg;
具体代码:
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
#include "string.h"
typedef struct message
{
uint8_t keyValue;
char buf[15];
}Msg;
osMessageQId msgqId;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
uint8_t KEY_Scan(void);
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
osMessageQDef(msgq, 10, Msg *);
msgqId = osMessageCreate(osMessageQ(msgq), NULL);
}
void StartTask01(void const * argument)
{
osEvent evt;
static uint8_t flag1=0;
Msg *msgp = NULL;
for(;;)
{
evt = osMessageGet(msgqId, osWaitForever);
if(evt.status == osEventMessage)
{
msgp = (Msg *)evt.value.p;
SEGGER_RTT_printf(0,"%s\r\n", msgp->buf);
if(msgp->keyValue == 1)
{
if(flag1 == 0)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
flag1 = 1;
}
else if(flag1 == 1)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
flag1 = 0;
}
}
}
else
{
SEGGER_RTT_printf(0,"osMessageGet error\r\n");
}
osDelay(200);
}
}
void StartTask02(void const * argument)
{
osStatus ret = 0;
Msg msg;
uint8_t keyValue;
for(;;)
{
keyValue = KEY_Scan();
if(keyValue != 0)
{
msg.keyValue = keyValue;
strcpy(msg.buf, "key k1 pressed");
ret = osMessagePut(msgqId,(uint32_t)&msg, 0);
if(ret != osOK)
{
SEGGER_RTT_printf(0,"osMessagePut error\r\n");
}
}
osDelay(200);
}
}
uint8_t KEY_Scan(void)
{
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;
else if(KEY2 == 0)return 2;
}
else if(KEY1 == 1 && KEY2 == 1) key_up = 0;
return 0;
}
实际上也可以以整形的传递指针,取数据时就通过value 中的v 取,然后再将整形数强制转为指针后再使用,如果以整形的方式来传递指针的话,以上例子中的部分代码需要改为如下形式:
osMessageQDef(msgq, 10, uint32_t);
msgp = (Msg *)evt.value.v;
三、内存池
3.1、内存池的作用
从堆中动态分配空间时我们可以使用malloc 函数,但是为了安全起见,RTOS提供了更加安全的内存池这个东西,作用与malloc一样,所以运行RTOS时完全可以使用内存池来代替C库的malloc 函数。
在前面的消息队列的例子中,发送消息的线程会将msg 的地址发送给对方线程,结构体变量msg 为线程函数的局部变量,而且msg里面包含非常占用空间的数组,由于线程函数的栈比较小(一般指定为 128),为了减少对栈空间的消耗,我们完全可以使用内存池来开辟msg的空间。
不建议发送局部变量指针的原因还有一个,那就是当函数运行结束后栈会被释放,此时操作的局部变量空间就是非法空间。
3.2、宏
osFeature_Pool(打开内存池的宏)
- 宏原型:
#define osFeature_Pool 1 - 作用
为 1:内存池可用 为 0:内存池不可用 通过osFeature_Pool 宏可以选择内存池的 API 是否可用,默认就是 1,因此内存池是可用的。
osPoolDef(定义内存池结构体的宏)
#define osPoolDef(name, no, type) \
const osPoolDef_t os_pool_def_##name = {(no), sizeof(type), NULL}
- 作用:使用
osPoolDef_t 类型来定义内存池的结构体变量(数据结构),后续用于创建内存池。 - 参数
name:指定名字,结构体变量会加os_pool_def_ 前缀。 no:指定内存池的最大块数,内存池是按块来管理的。 type:指定开辟空间时所用的类型 例子:在下面
osPool
- 宏原型:
#define osPool(name) &os_pool_def_##name - 作用:再指定名字前加上
os_pool_def_ 前缀并取地址 例子:
3.3、API
osPoolCreate(创建内存池)
- 函数原型:
osPoolId osPoolCreate(const osPoolDef_t *pool_def) - 功能:使用
osPoolDef 所定义的数据结构来创建一个内存池 - 参数:指定
osPoolDef 宏所定义的结构体变量的地址,此时需要使用osPool 宏 - 返回值:调用成功就返回唯一识别内存池的ID,否者就返回 NULL
- 例子:
osPoolAlloc(开辟内存空间1)
- 函数原型:
void * osPoolAlloc(osPoolId pool_id) - 功能:从
pool_id 所指向的内存池中开辟空间,大小为osPoolDef 第三个参数所指定类型的大小,osPoolAlloc 的特点是开辟空间后不会对空间请 0。 - 参数:
pool_id:内存池 ID - 返回值:调用成功就返回开辟空间的地址(指针),开辟空间失败就返回 NULL
由于返回类型是void * ,因此需要强转为需要的指针类型。 - 例子:在下面
osPoolCAlloc(开辟内存空间2)
- 函数原型:
void * osPoolCAlloc(osPoolId pool_id) - 功能:作用与
osPoolAlloc 一样,只不过这个函数会对开辟的空间进行清零。 - 例子:
osPoolFree(释放开辟的空间)
- 函数原型:
osStatus osPoolFree(osPoolId pool_id,void *block) - 功能:释放开辟的空间
- 参数:
pool_id:内存池 ID block:所开辟空间的指针 - 返回值:
osStatus - 例子:在下面
案例
修改消息队列的例子,将局部变量msg 改为从通过内存池来开辟空间。
这里只给出被做了改动的StartTask02 线程函数的代码,其它代码与消息队列例子的代码完全一样,添加和被改动过的地方会使用=== 来标记。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
#include "string.h"
typedef struct message
{
uint8_t keyValue;
char buf[15];
}Msg;
osMessageQId msgqId;
osPoolId poolId;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
uint8_t KEY_Scan(void);
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
osMessageQDef(msgq, 10, Msg *);
msgqId = osMessageCreate(osMessageQ(msgq), NULL);
osPoolDef(memPl, 8, Msg);
poolId = osPoolCreate(osPool(memPl));
}
void StartTask01(void const * argument)
{
osEvent evt;
static uint8_t flag1=0;
Msg *msgp = NULL;
for(;;)
{
evt = osMessageGet(msgqId, osWaitForever);
if(evt.status == osEventMessage)
{
msgp = (Msg *)evt.value.p;
SEGGER_RTT_printf(0,"%s\r\n", msgp->buf);
if(msgp->keyValue == 1)
{
if(flag1 == 0)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
flag1 = 1;
}
else if(flag1 == 1)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
flag1 = 0;
}
}
}
else
{
SEGGER_RTT_printf(0,"osMessageGet error\r\n");
}
osDelay(200);
}
}
void StartTask02(void const * argument)
{
osStatus ret = 0;
Msg *msgp = NULL;
uint8_t keyValue;
msgp = (Msg *)osPoolCAlloc(poolId);
if(NULL == msgp)
{
SEGGER_RTT_printf(0,"osPoolCAlloc 开辟空间失败\r\n");
}
for(;;)
{
keyValue = KEY_Scan();
if(keyValue != 0)
{
msgp->keyValue = keyValue;
strcpy(msgp->buf, "key k1 pressed");
ret = osMessagePut(msgqId,(uint32_t)msgp, 0);
if(ret != osOK)
{
SEGGER_RTT_printf(0,"osMessagePut error\r\n");
}
}
osDelay(200);
}
}
uint8_t KEY_Scan(void)
{
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;
else if(KEY2 == 0)return 2;
}
else if(KEY1 == 1 && KEY2 == 1) key_up = 0;
return 0;
}
例子代码并没有调用osPoolFree 来释放空间,是因为程序在运行的过程中需要一直使用,不需要释放,程序结束时会自动释放,因此不需要调用osPoolFree 来主动释放,如果在程序结束之前就要释放空间的话,我们就需要调用osPoolFree 来主动释放。
释放举例:
if(osOK != osPoolFree(poolId, msgp))
{
printf("释放失败\r\n");
}
实际上可以不用每次都重新发msg 的地址,只需要发送一次即可,只要对方拿到空间的指针后,两个线程即可以共享操作同一片空间,其中一个线程修改了空间的数据后,另一个线程就从里面读出修改后数据,这其实就是一个共享内存。
四、邮箱队列
在内存池的案例中,我们使用内存池来开辟msg 的空间,然后使用消息队列将空间指针发送给另一个线程,这样一来这两个线程就可以共操作同一个空间,以实现数据交换。
使用消息队列和内存池这种结合方式时,不仅需要创建消息队列,还要创建内存池,这就会比较麻烦,而邮箱队里可以一步到位,所以邮箱队列就是消息队列和内存池相结合后的产物,查看邮箱队列的源码时你会发现,内部封装的就是消息队列和内存池。
所以,邮箱队列的作用就是开辟一段内存空间,然后将空间指针发送给对方线程。
4.1、宏
osFeature_MailQ
- 宏原型:
#define osFeature_MailQ 1 - 作用
为 1:表示邮箱队列可用 为 0:表示邮箱队列不可用 osFeature_MailQ 宏默认为 1,因此邮箱队列可用。
osMailQDef
宏原型
#define osMailQDef(name, queue_sz, type) \
struct os_mailQ_cb *os_mailQ_cb_##name; \
const osMailQDef_t os_mailQ_def_##name = { (queue_sz), sizeof(type), (
&os_mailQ_cb_##name) }
- 作用:定义两个结构体变量
使用osMailQDef_t 定义邮箱队列的结构体变量,变量名的前缀为os_mailQ_def_ 。后续需要使用这个变量来创建邮箱队列。 定义os_mailQ_cb_ 前缀的结构体指针变量,这个指针变量会用于初始化os_mailQ_def_ 前缀的结构体变量。 - 参数
name:指定名字。 queue_sz:指定邮箱队列所挂消息上限。 type:类型,开辟空间时,空间大小就是这个类型的大小。
osMailQ
- 宏原型:
#define osMailQ(name) &os_mailQ_def_##name - 作用:给名字添加
os_mailQ_def_ 前缀,构建出结构体变量名并取地址。
4.2、API
osMailCreate(创建邮箱队列)
- 函数原型:
osMailQId osMailCreate(const osMailQDef_t *queue_def,osThreadId thread_id) - 功能:创建邮箱队列
- 参数
queue_def:指定 osMailQDef 宏所定义结构体变量的地址,需要用到 osMailQ 宏 thread_id:指定接收消息的那个线程的 ID - 返回值:函数调用成功就返回邮箱队列的 ID,否者就返回 NULL
osMailAlloc(在内存池中开辟空间)
- 函数原型:
void *osMailAlloc(osMailQId queue_id, uint32_t millisec) - 功能:在内存池中开辟空间,由于内部封装了内存池,所以就是在内存池中开辟空间。
- 参数
queue_id:邮箱队列的 ID。 millisec:指定超时时间。 0:不管是否成功开辟空间,函数都会立即返回。 - osWaitForever:如果开辟空间不成功会则一直阻塞,直到成功为止。其它值比如 200:如果 200ms 内没有开辟成功就超时返回
- 返回值:函数调用成功就返回开辟空间的指针,否者就返回 NULL
osMailCAlloc
- 函数原型:
void * osMailCAlloc(osMailQId queue_id,uint32_t millisec) *功能:功能与osMailAlloc 相同,只不过会将空间清0。
osMailFree(释放开辟的空间)
- 函数原型:
osStatus osMailFree(osMailQId queue_id, void *mail) - 功能:释放开辟的空间
- 参数
queue_id:邮箱队列的 ID mail:指向空间的指针 - 返回值:osStatus
osMailPut(发送邮箱消息)
- 函数原型:
osStatus osMailPut(osMailQId queue_id, void *mail) - 功能:发送邮箱消息,其实就是将开辟空间的指针发送给对方
- 参数
queue_id:邮箱队列的 ID mail:开辟空间的指针 - 返回值:
osStatus
osMailGet
- 函数原型:
osEvent osMailGet(osMailQId queue_id, uint32_t millisec) - 功能:接收邮箱消息。
- 参数:
queue_id:邮箱队列的 I millisec:超时时间 0:不管是否接受成功,都立即返回 - osWaitForever:如果接收不成功则休眠(阻塞),直到接收到为止
其它值比如 200:在 200ms 内如果没有接收到就超时返回 - osEvent
typedef struct {
osStatus status;
union {
uint32_t v;
void *p;
int32_t signals;
} value;
union {
osMailQId mail_id;
osMessageQId message_id;
} def;
} osEvent;
-
osStatus status 成功接收到邮箱消息时:里面放的是osEventMail ,只要检测到里面放的是osEventMail ,就表示成功接收到了邮箱消息。 超时:里面放的是osEventTimeout 错误:错误码 -
value 传递邮箱消息时,本质传递的是空间的指针,该指针就放在了value 中,此时需要使用p 来获取该指针。 -
def 使用邮箱队列时里面放的是邮箱队列的ID ,此时需要使用message_id 来获取ID 。
案例
将消息队列与内存池相结合的案例改为使用邮箱队列。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
#include "string.h"
typedef struct message
{
uint8_t keyValue;
char buf[15];
}Msg;
osMailQId mailqId;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
uint8_t KEY_Scan(void);
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
osMailQDef(mailq, 8, Msg *);
mailqId = osMailCreate(osMailQ(mailq), myTask01Handle);
}
void StartTask01(void const * argument)
{
osEvent evt;
static uint8_t flag1=0;
Msg *msgp = NULL;
for(;;)
{
evt = osMailGet(mailqId, osWaitForever);
if(evt.status == osEventMessage)
{
msgp = (Msg *)evt.value.p;
SEGGER_RTT_printf(0,"%s\r\n", msgp->buf);
if(msgp->keyValue == 1)
{
if(flag1 == 0)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
flag1 = 1;
}
else if(flag1 == 1)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
flag1 = 0;
}
}
}
else
{
SEGGER_RTT_printf(0,"osMessageGet error\r\n");
}
osDelay(200);
}
}
void StartTask02(void const * argument)
{
osStatus ret = 0;
Msg *msgp = NULL;
uint8_t keyValue;
msgp = (Msg *)osMailCAlloc(mailqId, osWaitForever);
if(NULL == msgp)
{
SEGGER_RTT_printf(0,"osMailCAlloc 开辟空间失败\r\n");
}
for(;;)
{
keyValue = KEY_Scan();
if(keyValue != 0)
{
msgp->keyValue = keyValue;
strcpy(msgp->buf, "key k1 pressed");
ret = osMailPut(mailqId, msgp);
if(ret != osOK)
{
SEGGER_RTT_printf(0,"osMailPut error\r\n");
}
}
osDelay(200);
}
}
uint8_t KEY_Scan(void)
{
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;
else if(KEY2 == 0)return 2;
}
else if(KEY1 == 1 && KEY2 == 1) key_up = 0;
return 0;
}
五、互斥锁
5.1、线程切换对资源操作的影响
我们知道线程间会进行切换,这种切换可能会导致一种问题,那就是当多个线程共享操作同一个资源时,有时需要每个线程的操作必须被完整地执行,然后其它线程才能操作资源,但是线程切换往往可能会中断当前线程的操作,然后当其它线程接着操作该资源时,可能就会干扰上一个线程的操作。
5.2、什么是锁,有什么作用
锁就是一个数据结构,用于标记资源是否可以被操作,如果上锁了就不能操作,否者就可以操作,说白了锁可以防止受到线程切换的影响。
多线程操作同一资源时,为了防止线程间切换所带来的影响,需要加锁,而且用的一定是同一把锁,否则就没有加锁效果。 加锁的原理:如下图所示。
5.3、疑问:什么是互斥?
答:就是当我在操作资源时如果没有操作完的话,你就不能操作,你在操作时如果没有操作完的话我不能操作,我们上图的内容来具体理解什么是互斥。互斥锁所实现的就是互斥功能,互斥功能的作用就是防止切换所带来的影响。
假设 A 线程先运行,最开始时是没有上锁的,A 线程加锁成功,然后开始读写文件,如果读写的代码执行了一部分就切换到了其线程上时,比如切换到了 B 线程,此时由于没有运行解锁代码,因此还处于加锁状态,当 B 线程去加锁时由于 A 线程没有解锁,因此会加锁失败,此时就会休眠(阻塞),不会对文件进行读写,B 线程就不会干扰到 A 线程之前读写数据,特别是写,不会在 A 线程写的数据中间插入 B 线程所写入的数据。
由于 B 线程因为加锁失败而进入休眠,此时就会就让出 CPU,然后切换到其它线程上比如 C线程,此时因为同样的原因,C 线程也会加锁失败然后休眠,当再次切换到 A 线程时继续执行被中断的代码,然后解锁,此时再切换到其它线程上时就可以加锁成功。
总之,通过互斥锁就可以让每个线程在操作资源时,保证自己的操作不会因为线程切换而被其它线程所干扰,当然如果如果这种干扰不会任何负面影响的话,此时完全可以不用加锁。
5.4、宏
osMutexDef
- 宏原型:
#define osMutexDef(name) const osMutexDef_t os_mutex_def_##name = { 0 } - 作用:使用
osMutexDef_t 类型定义互斥锁的结构体变量,名字的前缀为os_mutex_def_ - 参数:指定名字。
osMutexDef(mutex);
等价于
const osMutexDef_t os_mutex_def_mutex = { 0 }
osMutex
- 宏原型:
#define osMutex( name) &os_mutex_def_##name - 作用:在名字前面加
os_mutex_def_ 并取地址
5.5、API
osMutexCreate
- 函数原型:
osMutexId osMutexCreate (const osMutexDef_t *mutex_def) - 功能:创建一个互斥锁
- 参数:指定
osMutexDef 宏所定义结构体变量的地址,此时需要使用osMutex 宏* 返回值:函数调用成功就返回唯一识别互斥锁的 ID,否者就返回 NULL
osMutexDelete
- 函数原型:
osStatus osMutexDelete(osMutexId mutex_id) - 功能:删除互斥锁,此时会将互斥锁所用的数据结构空间都释放
- 参数:互斥锁 ID
- 返回值:
osStatus
osMutexWait
- 函数原型:
osStatus osMutexWait(osMutexId mutex_id, uint32_t millisec) - 功能:加锁,如果之前就已经上锁了,此时会加锁失败
- 参数
mutex_id:互斥锁 ID millisec:超时设置 0:不管加锁否调用成功,函数都会立即返回,这种情况用的很少osWaitForever :如果加锁失败就会永远阻塞,直到加锁成功为止。其它值比如 200:如果 200ms 内没有加锁成功就超时返回。 - 返回值:
osStatus
osMutexRelease
- 函数原型:
osStatus osMutexRelease(osMutexId mutex_id) - 功能:解锁
- 参数:
mutex_id:互斥锁 ID - 返回值:
osStatus
举例:
for(;;)
{
osMutexWait(mutexId, osWaitForever);
SEGGER_RTT_printf(0, "11111111111\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "22222222222\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "33333333333\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "44444444444\r\n");
osDelay(100);
osMutexRelease(mutexId);
osDelay(1000);
}
案例
由于我们没有移植文件系统的代码,因此没办法通过操作文件来举例,所以我们就使用RTT打印来举例。先看如下代码:两个线程都向串口打印输出,串口即为共同操作的资源。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
osMutexId mutexId;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
osMutexDef(mutex);
mutexId = osMutexCreate(osMutex(mutex));
}
void StartTask01(void const * argument)
{
for(;;)
{
osMutexWait(mutexId, osWaitForever);
SEGGER_RTT_printf(0, "11111111111\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "22222222222\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "33333333333\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "44444444444\r\n");
osDelay(100);
osMutexRelease(mutexId);
osDelay(1000);
}
}
void StartTask02(void const * argument)
{
for(;;)
{
osMutexWait(mutexId, osWaitForever);
SEGGER_RTT_printf(0, "aaaaaaaaaaa\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "bbbbbbbbbbb\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "ccccccccccc\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "ddddddddddd\r\n");
osDelay(100);
osMutexRelease(mutexId);
osDelay(1000);
}
}
试验结果
六、信号量
6.1、信号量的作用
信号量分为了二值信号量和多值信号量,二值信号量的作用与互斥锁一样,至于多值信号量的作用后面再介绍。
6.2、宏
osSemaphoreDef
#define osSemaphoreDef (name) const osSemaphoreDef_t os_semaphore_def_##name = { 0 }
- 作用:使用
osSemaphoreDef_t 类型来定义信号量的结构体变量,前缀为 os_semaphore_def_ 。 - 参数:指定名字
osSemaphore
- 宏原型:
#define osSemaphore(name) &os_semaphore_def_##name - 作用:给名字添加
os_semaphore_def_ 前缀并取地址
API
osSemaphoreCreate
- 函数原型:
osSemaphoreId osSemaphoreCreate(const osSemaphoreDef_t *semaphore_def, int32_t count) - 作用:创建信号量
- 参数
semaphore_def:指定osSemaphoreDef 宏所定义结构体变量的指针,此时需要用到osSemaphore 宏。 count:指定初始资源数,有关什么是资源数后面再介绍。 - 返回值:函数调用成功就返回唯一识别信号量的 ID,否者就返回 NULL。
osSemaphoreDelete
- 函数原型:
osStatus osSemaphoreDelete(osSemaphoreId semaphore_id) - 功能:删除信号量,删除时释放信号量数据结构所用的空间
- 参数:
semaphore_id: 信号量 ID - 返回值:osStatus
osSemaphoreWait
- 函数原型:
int32_t osSemaphoreWait(osSemaphoreId semaphore_id, uint32_t millisec) - 功能:使用资源(加锁),资源数-1,表示已经占用了一个资源
- 参数
semaphore_id:信号量 ID millisec:超时设置 0:不管成功与否都立即返回 - osWaitForever:如果无法获取资源则休眠,直到获取到资源位置。
其它值比如 200:200ms 内如果没有获取到资源,此时就是超时返回 - 返回值:函数调用成功就返回减 1 后的资源数,否者就返回-1
osSemaphoreRelease
- 函数原型:
osStatus osSemaphoreRelease(osSemaphoreId semaphore_id) - 功能:释放资源,资源时资源数+1,表示多了一个可用资源
- 参数:信号量 ID
- 返回值:
osStatus
二值信号量
什么是二值信号量
二值信号量的作用就是互斥,所以二值信号量其实也是一个互斥锁。
信号量需要设置资源数,对于二值信号量来说,资源数为就 0 和 1 两个值。
- 为 1:表示有一个资源可用,占用资源后资源数就-1 变为 0资源可用其实就是表示可以加锁。
- 为 0:表示资源不可用,此时不可以占用资源,其实就是不可以加锁只有当线程将资源释放后 0 变 1,此时别人才能占用资源(加锁)。释放资源后资源数从 0 变 1,其实就是在解锁。
对于二值信号量来说,最开始时一定要将资源数设置为 1,如果设置为 0 的话将无法占用资源,也就是说无法加锁,由于只有 0、1 这两种状态,所以要么处于加锁状态,要么处于解锁状态,此时二值信号量就是一个互斥锁。
二值信号量案例
将前面互斥锁的案例,改为使用二值信号量来实现互斥。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "SEGGER_RTT.h"
osSemaphoreId semId;
osThreadId myTask01Handle;
osThreadId myTask02Handle;
void StartTask01(void const * argument);
void StartTask02(void const * argument);
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
osThreadDef(myTask01, StartTask01, osPriorityNormal, 0, 128);
myTask01Handle = osThreadCreate(osThread(myTask01), NULL);
osThreadDef(myTask02, StartTask02, osPriorityNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
osSemaphoreDef(sem);
semId = osSemaphoreCreate(osSemaphore(sem), 1);
}
void StartTask01(void const * argument)
{
for(;;)
{
osSemaphoreWait(semId, osWaitForever);
SEGGER_RTT_printf(0, "zhiguoxin01\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin02\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin03\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin04\r\n");
osDelay(100);
osSemaphoreRelease(semId);
osDelay(1000);
}
}
void StartTask02(void const * argument)
{
for(;;)
{
osSemaphoreWait(semId, osWaitForever);
SEGGER_RTT_printf(0, "zhiguoxin0a\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin0b\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin0c\r\n");
osDelay(100);
SEGGER_RTT_printf(0, "zhiguoxin0d\r\n");
osDelay(100);
osSemaphoreRelease(semId);
osDelay(1000);
}
}
多值信号量
多值信号量的特点
对于二值信号量来说,二值信号量的值只有 0 和 1 这两个,而多值信号量则有多个,比如将初始时资源数设置为 2 的话,就有 0、1、2 三个值,这就是多值的,当然你也可以将资源数设置为 3、4 等值。
多值信号量的作用
僧多粥少时,想保证固定个数的线程去操作资源时,就可以使用多值信号量,我们这里举例理解,比如我的项目有这样一个需求,总共 5 个线程,但是每次只允许 3 个线程能操作文件,剩余两个线程则不能,至于到底是那三个线程能够操作,就看谁占用到了资源,此时就需要用到多值信号量。
创建信号量时将资源数设置为 3: 第一个线程:占用一个资源后,资源数为 2,资源还有两个可用 第二个线程:占用一个资源后,资源数为 1,资源还有一个可用 第三个线程:占用一个资源后,资源数为 0,资源目前不可用
只有三个线程能操作资源,当其中某个释放了资源,然后资源数+1 后,此时其它没有占用资源的线程则可以占用,总之只能有三个线程能操作资源,其它两个线程会因资源为 0 而休眠,直到有人释放资源为止。
创建信号量时设置的资源数到底代表了什么
代表允许多少个线程去操作资源。
- 1:每次只允许有一个线程去操作资源,此时为互斥关系,这就是二值信号量
- 比如 3:每次允许有 3 个线程去操作资源,这就是多值信号量
多值信号量的案例
我们前面在配置时已经使能了“多值信号量”,大家可以自己去创建 5 个线程,然后串口打印,此时可以使用多值信号量来只允许 3 个线程打印,具体代码请自行实现。
|