FreeRTOS——信号量与互斥量(基于百问网DshanMCU-F103实现小车游戏)

一、信号量的特性

1.1 信号量常规操作

信号量名称解读:

  • 信号:起通知作用。
  • 量:还可以用来表示资源的数量。
    • 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)。
    • 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)。
  • 支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1。

信号量的"give"、"take"双方并不需要相同,可以用于生产者-消费者场合:

  • 生产者为任务A、B,消费者为任务C、D。
  • 一开始信号量的计数值为0,如果任务C、D想获得信号量,会有两种结果:
    • 阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)。
    • 即刻返回失败:不等。
  • 任务A、B可以生产资源,就是让信号量的计数值增加1,并且把等待这个资源的顾客唤醒。
  • 唤醒谁?谁优先级高就唤醒谁,如果大家优先级一样就唤醒等待时间最长的人。

二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为1。
在这里插入图片描述

1.2 信号量与队列对比

队列信号量
可以容纳多个数据, 创建队列时有2部分内存: 队列结构体、存储数据的空间只有计数值,无法容纳其他数据。 创建信号量时,只需要分配信号量结构体
生产者:没有空间存入数据时可以阻塞生产者:用于不阻塞,计数值已经达到最大时返回失败
消费者:没有数据时可以阻塞消费者:没有资源时可以阻塞

1.3 两种信号量对比

信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。差别列表如下:

二进制信号量计数型信号量
被创建时初始值为0被创建时初始值可以设定
其他操作是一样的其他操作是一样的

二、信号量函数

使用信号量时,先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量。

2.1 创建信号量

对于二进制信号量、计数型信号量,它们的创建函数不一样:

二进制信号量计数型信号量
动态创建xSemaphoreCreateBinary 计数值初始值为0xSemaphoreCreateCounting
vSemaphoreCreateBinary(过时了) 计数值初始值为1
静态创建xSemaphoreCreateBinaryStaticxSemaphoreCreateCountingStatic

创建二进制信号量的函数原型如下:

/* 创建一个二进制信号量,返回它的句柄。
 * 此函数内部会分配信号量结构体 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinary(void);

/* 创建一个二进制信号量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

示例代码:

/* 设置信号量句柄 */
static SemaphoreHandle_t g_xSemaTicks;

    /* 创建二进制型信号量 */
    g_xSemaTicks = xSemaphoreCreateBinary();
    /* 最大值是1,再次添加,不生效 */
    xSemaphoreGive(g_xSemaTicks);

创建计数型信号量的函数原型如下:

/* 创建一个计数型信号量,返回它的句柄。
 * 此函数内部会分配信号量结构体 
 * uxMaxCount: 最大计数值
 * uxInitialCount: 初始计数值
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

/* 创建一个计数型信号量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * uxMaxCount: 最大计数值
 * uxInitialCount: 初始计数值
 * pxSemaphoreBuffer: StaticSemaphore_t结构体指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, 
                                                 UBaseType_t uxInitialCount, 
                                                 StaticSemaphore_t *pxSemaphoreBuffer );

示例代码:

/* 设置信号量句柄 */
static SemaphoreHandle_t g_xSemaTicks;
    /* 创建计数值信号量 */
    g_xSemaTicks = xSemaphoreCreateCounting(3,2);

2.2 删除信号量

对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存

vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下:

/* xSemaphore: 信号量句柄,你要删除哪个信号量 */
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

2.3获取/释放信号量

二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:

在任务中使用在ISR中使用
givexSemaphoreGivexSemaphoreGiveFromISR
takexSemaphoreTakexSemaphoreTakeFromISR

xSemaphoreGive的函数原型如下:

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

示例:

    /* 释放信号量 */
    xSemaphoreGive(g_xSemaTicks);

xSemaphoreGiveFromISR函数的参数与返回值列表如下:

BaseType_t xSemaphoreGiveFromISR(
              SemaphoreHandle_t xSemaphore,
              BaseType_t *pxHigherPriorityTaskWoken);

参数说明:

参数说明
xSemaphore信号量句柄,释放哪个信号量
pxHigherPriorityTaskWoken如果释放信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
返回值pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败

xSemaphoreTake的函数原型如下:

BaseType_t xSemaphoreTake(
                SemaphoreHandle_t xSemaphore,
                TickType_t xTicksToWait);

参数说明:

参数说明
xSemaphore信号量句柄,获取哪个信号量
xTicksToWait如果无法马上获得信号量,阻塞一会: 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick个数,可以使用*pdMS_TO_TICKS()*来指定阻塞时间为若干ms
返回值pdTRUE表示成功

示例

    /* 获得信号量 */
    xSemaphoreTake(g_xSemaTicks,portMAX_DELAY);

xSemaphoreTakeFromISR的函数原型如下:

BaseType_t xSemaphoreTakeFromISR(
                     SemaphoreHandle_t xSemaphore,
                     BaseType_t *pxHigherPriorityTaskWoken);

参数说明:

参数说明
xSemaphore信号量句柄,获取哪个信号量
pxHigherPriorityTaskWoken如果获取信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE
返回值pdTRUE表示成功

三、 互斥量

斥量是一种特殊的二进制信号量。

使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。

3.1 创建互斥量

创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:

/* 创建一个互斥量,返回它的句柄。
 * 此函数内部会分配互斥量结构体 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );

/* 创建一个互斥量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );

要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:

#define configUSE_MUTEXES 1

代码示例:

static SemaphoreHandle_t g_xI2CMutex;

    void GetI2C(void)
    {
        /* 获取互斥量 */
        xSemaphoreTake(g_xI2CMutex,portMAX_DELAY);
    }

    void PutI2C(void)
    {
        /* 释放互斥量 */
        xSemaphoreGive(g_xI2CMutex);
    }
	/* 创建互斥量 */
	g_xI2CMutex = xSemaphoreCreateMutex();

3.2 其他函数

要注意的是,互斥量不能在ISR中使用。

各类操作函数,比如删除、give/take,跟一般是信号量是一样的。

/*
 * xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
 */
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

/* 释放(ISR版本) */
BaseType_t xSemaphoreGiveFromISR(
              SemaphoreHandle_t xSemaphore,
              BaseType_t *pxHigherPriorityTaskWoken);
/* 获得 */
BaseType_t xSemaphoreTake(
              SemaphoreHandle_t xSemaphore,
              TickType_t xTicksToWait);
/* 获得(ISR版本) */
xSemaphoreGiveFromISR(
              SemaphoreHandle_t xSemaphore,
              BaseType_t *pxHigherPriorityTaskWoken );

3.3 优先级继承

示例的问题在于,car1低优先级任务获得了锁,但是它优先级太低而无法运行。

如果能提升car1任务的优先级,让它能尽快运行、释放锁,"优先级反转"的问题不就解决了吗?

把car1任务的优先级提升到什么水平?car3也想获得同一个互斥锁,不成功而阻塞时,它会把car1的优先级提升得跟car3一样。

这就是优先级继承:

  • 假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁。
  • 任务B说:你既然持有宝剑,又不给我,那就继承我的愿望吧。
  • 于是任务A就继承了任务B的优先级。
  • 这就叫:优先级继承。
  • 等任务A释放互斥锁时,它就恢复为原来的优先级。
  • 互斥锁内部就实现了优先级的提升、恢复。

示例中通过创建互斥量解决优先级反转的问题,FreeRTOS为互斥量赋予了优先级继承的特性。优先级继承会暂时提高获得互斥量的任务的优先级,使得含有互斥量的任务的优先级和想要获取互斥量的任务中的最高优先级一样。互斥量无法彻底避免优先级反转的问题,但能显著降低事情发生的概率。

3.4 死锁

死锁:是因为线程在加锁时使用不当,造成的所有的线程都被阻塞的情况,并且线程的阻塞无法被打开,属于无解状态,就是死锁。

使用互斥量时可能会出现死锁的情况,比如:

任务A、B要求获取互斥量M、N才能运行,而二者分别已经被两个任务所获取,两个任务都因无法获取另一个互斥量进入阻塞,阻塞后也再无法释放互斥量,造成死锁;

同时也存在**“自我死锁”**:

一个线程获取互斥量,其调用的函数内部也要获取互斥量,导致阻塞而无法释放互斥量,造成死锁;

为解决自我死锁(一个线程需要获取多次临界资源),设计了递归锁。递归函数是先层层调用N次再层层返回N次,而递归锁能多次获取,take了N次以后,需要giveN次才完成释放(重复加锁🔒)。

函数原型:

/* 创建一个递归锁,返回它的句柄。
 * 此函数内部会分配互斥量结构体 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );

/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );

/* 获得 */
BaseType_t xSemaphoreTakeRecursive(
               SemaphoreHandle_t xSemaphore,
               TickType_t xTicksToWait);

参数解释:

递归锁一般互斥量
创建xSemaphoreCreateRecursiveMutexxSemaphoreCreateMutex
获得xSemaphoreTakeRecursivexSemaphoreTake
释放xSemaphoreGiveRecursivexSemaphoreGive

四、代码实现(汽车游戏)

4.1 使用信号量方式:

关键代码

game2.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "cmsis_os.h"
#include "FreeRTOS.h"                   // ARM.FreeRTOS::RTOS:Core
#include "task.h"                       // ARM.FreeRTOS::RTOS:Core
#include "event_groups.h"               // ARM.FreeRTOS::RTOS:Event Groups
#include "semphr.h"                     // ARM.FreeRTOS::RTOS:Core


#include "draw.h"
#include "resources.h"

#include "driver_lcd.h"
#include "driver_rotary_encoder.h"
#include "driver_ir_receiver.h"
#include "driver_mpu6050.h"


#define CAR_COUNT	3
#define CAR_WIDTH	12
#define CAR_LENGTH	15
#define ROAD_SPEED	6

#define NOINVERT	false
#define INVERT		true

static uint32_t g_xres, g_yres, g_bpp;
static uint8_t *g_framebuffer;

struct car{
	int x;
	int y;
	int control_key;
};

struct car g_cars[3] = 
{
	{0,0,IR_KEY_1},
	{0,17,IR_KEY_2},
	{0,34,IR_KEY_3},
};

static const byte carImg[] ={
	0x40,0xF8,0xEC,0x2C,0x2C,0x38,0xF0,0x10,0xD0,0x30,0xE8,0x4C,0x4C,0x9C,0xF0,
	0x02,0x1F,0x37,0x34,0x34,0x1C,0x0F,0x08,0x0B,0x0C,0x17,0x32,0x32,0x39,0x0F,
};

static const byte roadMarking[] ={
	0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,
};


static const byte clearImg[30] ={0};

static SemaphoreHandle_t g_xSemaTicks;

/* 显示汽车 */
static void ShowCar(struct car *pcar)
{
	draw_bitmap(pcar->x, pcar->y, carImg, 15, 16, NOINVERT, 0);
    draw_flushArea(pcar->x, pcar->y, 15, 16);
}
 /* 隐藏汽车 */
static void HideCar(struct car *pcar)
{
	draw_bitmap(pcar->x, pcar->y, clearImg, 15, 16, NOINVERT, 0);
    draw_flushArea(pcar->x, pcar->y, 15, 16);
}

static void Car1Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10,sizeof(struct ir_data));
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);
	
	ShowCar(pcar);
	
	/* 获得信号量 */
	xSemaphoreTake(g_xSemaTicks,portMAX_DELAY);
	
	while(1)
	{
			if(pcar->x <g_xres -CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar);
				/* 刷新汽车位置 */
				pcar->x += 3;
				if(pcar->x > g_xres -CAR_LENGTH)
				{
					pcar->x = g_xres -CAR_LENGTH;
				}
					/* 重新显示汽车 */
				ShowCar(pcar);
				
				vTaskDelay(50);
				
				if(pcar->x == g_xres -CAR_LENGTH)
				{
					xSemaphoreGive(g_xSemaTicks);
					vTaskDelete(NULL);
				}
					
			}
	}
}

static void Car2Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
	ShowCar(pcar);
	vTaskDelay(1000);
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10,sizeof(struct ir_data));
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);
	
	while(1)
	{
			if(pcar->x <g_xres -CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar);
				/* 刷新汽车位置 */
				pcar->x += 3;
				if(pcar->x > g_xres -CAR_LENGTH)
				{
					pcar->x = g_xres -CAR_LENGTH;
				}
					/* 重新显示汽车 */
				ShowCar(pcar);
				
				mdelay(100);
				
				if(pcar->x == g_xres -CAR_LENGTH)
				{
					//xSemaphoreGive(g_xSemaTicks);
					vTaskDelete(NULL);
				}	
			}
	}
}

static void Car3Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
	
	ShowCar(pcar);
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10,sizeof(struct ir_data));
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);
	
	vTaskDelay(2000);
	
	/* 获得信号量 */
	xSemaphoreTake(g_xSemaTicks,portMAX_DELAY);
	
	while(1)
	{
			if(pcar->x <g_xres -CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar);
				/* 刷新汽车位置 */
				pcar->x += 3;
				if(pcar->x > g_xres -CAR_LENGTH)
				{
					pcar->x = g_xres -CAR_LENGTH;
				}
					/* 重新显示汽车 */
				ShowCar(pcar);
				
				vTaskDelay(50);
				
				if(pcar->x == g_xres -CAR_LENGTH)
				{
					xSemaphoreGive(g_xSemaTicks);
					vTaskDelete(NULL);
				}
					
			}
	}
}

void car_game(void)
{
	int x;
	int i,j;
    g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
    draw_init();
    draw_end();
	
	/* 创建二进制型信号量 */
	g_xSemaTicks = xSemaphoreCreateBinary();
	/* 最大值是1,再次添加,不生效 */
	xSemaphoreGive(g_xSemaTicks);
	
	/* 画出路标 */
	for(i=0;i<3;i++)
	{
		for(j=0;j<8;j++)
		{
			draw_bitmap(16*j, 16+17*i, roadMarking, 8, 1, NOINVERT, 0);
			draw_flushArea(16*j, 16+17*i, 8, 1);
		}	
	}
	/* 创建三个汽车任务 */
	xTaskCreate(Car1Task, "car1", 128, &g_cars[0], osPriorityNormal, NULL);
	xTaskCreate(Car2Task, "car2", 128, &g_cars[1], osPriorityNormal+2, NULL);
	xTaskCreate(Car3Task, "car3", 128, &g_cars[2], osPriorityNormal+3, NULL);

}

freertos.c

    /* 启动汽车游戏 */
    extern void car_game(void);
    car_game();

上机实验:

成功在OLED屏幕中绘制出三辆小车,并根据信号量的获取与释放,优先级不同让三个小车实现了不同时刻到达终点的效果。

4.2 互斥量实现方式

本次实现是基于信号量的项目基础上,通过引入互斥量操作,能够让三个汽车任务变得更加顺畅。

关键代码:

draw.c

extern void GetI2C(void);
extern void PutI2C(void);

    /* 抢占I2C资源,使用互斥量 */
    void draw_flushArea(byte x, byte y, byte w, byte h)
    {
        //taskENTER_CRITICAL();
        GetI2C();
        LCD_FlushRegion(x, y, w, h);
        PutI2C();
        //taskEXIT_CRITICAL();
    }

在I2C资源调用时,基于同步与互斥通信的代码会出现屏幕卡死的的现象,由于的全局变量没有设置保护措施,在这里引入互斥量的操作解决这个问题。

freertos.c

static SemaphoreHandle_t g_xI2CMutex;

    void GetI2C(void)
    {
        /* 获取互斥量 */
        xSemaphoreTake(g_xI2CMutex,portMAX_DELAY);
    }

    void PutI2C(void)
    {
        /* 释放互斥量 */
        xSemaphoreGive(g_xI2CMutex);
    }
	/* 创建互斥量 */
	g_xI2CMutex = xSemaphoreCreateMutex();

将game2.c中创建信号量的代码更改成创建互斥量的代码,然后通过在各个任务函数中获取和释放,使得小车能够实现不同时间到达终点。

	/* 创建互斥量 */
	g_xSemaTicks = xSemaphoreCreateMutex();

上机实验:

成功在OLED屏幕中绘制出三辆小车,并根据互斥量的获取与释放,优先级不同让三个小车实现了不同时刻到达终点的效果。

参考:
《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103)

  • 24
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值