记基于STM32 的 FreeRTOS 学习的移植日志【一】


记录 STM32 + STM32CubeIDE + FreeRTOS 实时操作系统移植学习中的一些心得体会。

由于篇幅有限,本文主要介绍 FreeRTOS 中系统状态、任务调度管理、消息队列、信号量、互斥量等概念和常用API及部分经典案例。

阻塞模式

HAL 库开发常用阻塞用 HAL_Delay() 进行延时,当 FreeRTOS 时使用 osDelay() 进行延时,多任务时各任务优先级一致时,程序无影响;但是如果任务优先级不一致时,会出现高优先级任务运行中使用 HAL_Delay() 延时会导致低优先任务无法运行。

解决如下: 采用 vTaskDelay() 在任务中进行延时,此时会出现当程序中阻塞时会执行到低优先级任务,其原理即时间片轮转

e.g. 案例: LED1、LED2双任务闪烁

程序:Task1 点亮LED;Task2 熄灭LED;两者共同实现闪烁


void LedTask(void const * argument)
{
  /* USER CODE BEGIN LedTask */
  /* Infinite loop */
  for(;;)
  {
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
      HAL_Delay(500); // vTaskDelay(500);
      HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
      HAL_Delay(500);  // vTaskDelay(500);
    osDelay(1);
  }
  /* USER CODE END LedTask */
}

当优先级不同时:

Task1: osPriorityNormal
Task2: osPriorityLow

2低于1的优先级,故任务2不会运行,LED仅点亮。

延时函数切换为 vTaskDelay 之后,程序将在阻塞时运行低优先级程序;故能够实现闪烁效果

系统状态

运行状态(Runing)

该状态表明任务正在执行,此时它占用处理器,FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻, 它的任务状态就变成了运行态。

非运行状态(Not Runing)

阻塞状态(Blocked)、暂停状态(Suspended)、就绪状态(Ready)三种状态的总称。
摘自普中
  • 就绪(Ready): 该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度新创建的任务会初始化为就绪态。
  • 阻塞(Blocked): 如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、 任务正在等待信号量、读写队列或者等待读写事件等。

    有两种方式进入阻塞状态
    • 相对延时函数 void vTaskDelay( const TickType_t xTicksToDelay ); 类似于HAL_Delay();堵塞tick数,单位默认就是毫秒
    • 绝对延时函数 BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ); 指定的唤醒时间已经达到,vTaskDelayUntil()立刻返回(不会有阻塞)
  • 挂起态(Suspended): 处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数; 而把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume()vTaskResumeFromISR() 函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

任务刚创建的时候,它默认是会直接进入我们的就绪状态,但是它并不会直接运行,一般是在那个调度器启动之后,才会有任务进入运行状态,因为各个任务间要比较和判断,启动任务调度器默认是最高优先级的先执行,如果同优先级的话,我们一般是后创建的那个任务会先执行

相对/绝对延时函数

阻塞模式下的两种延时方式

相对延时: 是指两次任务执行的间隔时间是相对的(延时时间=任务执行时间+需要延时的时间)
绝对延时: 是指两次任务执行的间隔时间是绝对的(延时时间=需要延时的时间)

e.g. 案例: 相对延时与绝对延时的比较

  1. CubeMX 生成带串口的工程用于打印输出时间

    • 移植 retarget.c 到项目中,串口重定向 printf
  2. FreeRTOS 配置参数

    • 参数设置: Include Parameters -> 启用 vTaskDelayUntil
    • 程序代码: 见本节末
  3. 解决编译报错

    • main.c 中引入头文件 retarget.h
    • main.c 中绑定串口1的printf重定向 RetargetInit(&huart1);
    • freeRTOS.c 中引入头文件 retarget.h
    • 解决 retarget.c 中函数声明冲突: 注释掉 syscall.c 中冲突函数或者删除
  4. 调试结果

Task1: 采用相对延时;Task2: 采用绝对延时

  • 左侧:Task1(osPriorityNormal)优先级高于Task2(osPriorityLow)

    • 不惧参考性,主要分析同优先级下的情况
  • 右侧:Task1(osPriorityNormal)与Task2(osPriorityNormal)同等优先级

    • 同等优先级时,最后的任务会优先执行
    • 相对延时700ms左右;绝对延时得到更为准确的500ms左右

注:忽略第一个输出,第一个是由于我手动暂停导致的干扰。

附: 程序代码 freeRTOS.c


// 任务1
void LedTask(void const * argument)
{
  for(;;)
  {
      printf("Task1\r\n");
      HAL_Delay(200); // 模拟任务占用时间
      vTaskDelay(500); //相对任务延时
  }
}

// 任务2
void LedTaskCo(void const * argument)
{
  TickType_t xLastWakeTime;
  xLastWakeTime = xTaskGetTickCount();
  for(;;)
  {
      printf("Task2\r\n");
      HAL_Delay(200); // 模拟任务占用时间
      vTaskDelayUntil(&xLastWakeTime,500); //绝对延时500ms
  }
}

任务的管理

  1. 任务句柄(TaskControlBlock, TCB)

    多任务系统中, 任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所 有信息,比如任务的栈指针,任务名称,任务的形参等。系统对任务的全部操作都可以通过这个任务控制块来实现,其是一个新的数据类型,该数据类型在 task.c 这 C 文件中声明 typedef struct tskTaskControlBlock

  2. 基本任务
  • 创建任务: xTaskCreate()
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务 函数
栈大小,单位为 word 参数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, //
void * const pvParameters, // 调用任务函数时传入的
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句 柄, 以后使用它来操作这个任务
  • 删除任务: vTaskDelete()
void vTaskDelete(TaskHandle_t xTaskToDelete);//传入任务句柄 句柄为NULL时,删除自己
为自己时,删除自己 为其他任务时,删除其他任务
  • 暂停任务: vTaskSuspend()
void vTaskSuspend(TaskHandle_t xTaskToSuspend);//传入任务句柄 句柄为NULL时,暂停自己
为自己时,暂停自己; 为其他任务时,暂停其他任务
  • 恢复任务: vTaskResume()
void vTaskResume(TaskHandle_t vTaskResume);//传入任务句柄 无法自己恢复自己,因为被暂停的任务是无法执行的
  1. 其他函数
  • uxTaskPriorityGet():
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )//传入任务句柄
此函数用来获取指定任务的优先级,使用INCLUDE_uxTaskPriorityGet函数的话应该定义为1
  • vTaskPrioritySet():
void vTaskPrioritySet(xTaskHandle pxTask,unsigned
portBASE_TYPEuxNewPriority);
//传入任务句柄,新的优先级
此函数用于改变某一个任务的任务优先级,要使用此函数的话宏 INCLUDE_vTaskPrioritySet应该定义为1
  • uxTaskGetSystemState():
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const
pxTaskStatusArray,
uxArraySize,
const UBaseType_t
uint32_t * const
pulTotalRunTime )
pxTaskStatusArray: 指向 TaskStatus_t 结构体类型的数组首地址,每个任务至少需 要一个TaskStatus_t 结 构 体 , 任 务 的 数 量 可 以 使 用 函 数 uxTaskGetNumberOfTasks()
uxArraySize: 保存任务壮态数组的数组的大小。
pulTotalRunTime: 如果 configGENERATE_RUN_TIME_STATS 为 1 的话此参数用来 保存系统总的运行时间。
返回值: 统计到的任务壮态的个数,也就是填写到数组 pxTaskStatusArray 中的个数, 此值应该等于函数 uxTaskGetNumberOfTasks()的返回值。如果参数uxArraySize 太小的话返回值可能为0
此函数用于获取系统中所有任务的任务壮态,每个任务的壮态信息保存在一个 TaskStatus_t类型的结构体里面,这个结构体里面包含了任务的任务句柄、任务名字、堆 栈、优先级等信息,要使用此函数的话宏 configUSE_TRACE_FACILITY 应该定义为 1

e.g. 案例: 任务的增/删/停/复


void KeyTask(void const * argument)
{
  /* USER CODE BEGIN KeyTask */
  /* Infinite loop */
  for(;;)
  {
      if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
      {
          osDelay(20);
          if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
          {
                  if(LEDHandle == NULL)
                  {
                      printf("Task1 Create\r\n");
                      osThreadDef(LED, LedTask, osPriorityNormal, 0, 128);
                      LEDHandle = osThreadCreate(osThread(LED), (void *)"New Task #1");
                      printf("OK\r\n");
                  } else {
                      printf("Task1 Delete\r\n");
                      vTaskDelete(LEDHandle);
                      LEDHandle = NULL; // 任务删除句柄不会,要手动清除
                  }
          }
          while(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
  /* USER CODE END KeyTask */
}


void KeyTask2(void const * argument)
{
  /* USER CODE BEGIN KeyTask */
  static uint8_t flag=1; //挂起标志
  /* Infinite loop */
  for(;;)
  {
      if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
      {
          osDelay(20);
          if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
          {
                  if(flag == 1)
                  {
                      printf("Task2 Hang\r\n");
                      vTaskSuspend(LED_CoHandle);
                      flag=0;
                  } else {
                      printf("Task2 Resume\r\n");
                      vTaskResume(LED_CoHandle);
                      flag=1;
                  }
              
          }
          while(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
  /* USER CODE END KeyTask */
}

消息队列

消息队列是一种常用于任务间通信的数据结构。通过消息队列服务,任务或 中断服务例程可以将一条或多条消息放入消息队列中,同样,一个或多个任务可 以从消息队列中获得消息。

通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队) 是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出, FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制。

  • 队列的创建
//动态创建
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); //传入参数(队列长度,每个数据的大小:以字节为单位)
//返回 非零:创建成功返回消息队列的句柄 NULL:创建失败

//静态创建
QueueHandle_t   xQueueCreateStatic(UBaseType_t uxQueueLength,
                                    UBaseType_t uxItemSize,
                                    uint8_t *pucQueueStorageBuffer,
                                    StaticQueue_t *pxQueueBuffer);
  • 队列复位

队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset()把队列恢复为初 始状态

BaseType_t xQueueReset( QueueHandle_t pxQueue);//传入队列句柄即可

  • 删除队列

删除队列的函数为 vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内 存

void vQueueDelete( QueueHandle_t xQueue );//传入队列句柄即可

  • 写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在 ISR(中断) 中使用
这两个默认是等效的,一般使用前面的


// 往后写入
BaseType_t xQueueSend(QueueHandle_t xQueue,
                        const void *pvItemToQueue,
                        TickType_t xTicksToWait);


BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
                            const void *pvItemToQueue,
                            TickType_t xTicksToWait);
//参数:
// 需要写的队列、写入数据的指针、等待时间
// 往队列尾部写入数据,如果没有空间,阻塞时间为 xTicksToWait //(如果被设为 0,无法写入数据时函数会立刻返回;如果被设为 portMAX_DELAY,则会 一直阻塞直到有数据可写)


// 往前写入
BaseType_t  xQueueSendToFront(QueueHandle_t xQueue,
                                    const void  *pvItemToQueue,
                                    TickType_t  xTicksToWait);

//参数:
// 需要写的队列、写入数据的指针、等待时间
// 往队列头部写入数据,如果没有空间,阻塞时间为 xTicksToWait //(如果被设为 0,无法写入数据时函数会立刻返回;如果被设为 portMAX_DELAY,则会 一直阻塞直到有数据可写)


//中断中的函数 中断写入
BaseType_t  xQueueSendToBackFromISR(QueueHandle_t xQueue, const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t  xQueueSendToFrontFromISR(QueueHandle_t xQueue,
                                    const void *pvItemToQueue,
                                    BaseType_t *pxHigherPriorityTaskWoken); //对某个队列而言,可能有不止一个任务处于阻塞态在等待其数据有效。调用 xQueueSendToFrontFromISR()或 xQueueSendToBackFromISR()会使得队列数据变 为有效,所以会让其中一个等待任务切出阻塞态。如果调用这两个 API 函数使得一个任务解 除阻塞,并且这个任务的优先级高于当前任务(也就是被中断的任务),那么 API 会在函数内 部将*pxHigherPriorityTaskWoken 设为 pdTRUE。如果这两个 API 函数将此值设为 pdTRUE,则在中断退出前应当进行一次上下文切换。这样才能保证中断直接返回到就绪态任务 中优先级最高的任务中。
  • 读队列
//普通读
 BaseType_t  xQueueReceive( QueueHandle_t xQueue,
                            void * const pvBuffer,
                            TickType_t xTicksToWait );
//参数
// 队列句柄、数据存放地指针、等待时间
//(如果被设为 0,无法读出数据时函数会立刻返回;如果被设为 portMAX_DELAY,则会 一直阻塞直到有数据可读)

//中断读
 BaseType_t  xQueueReceiveFromISR(QueueHandle_t xQueue,
                                    void *pvBuffer,
                                    BaseType_t *pxTaskWoken);
  • 队列查询
// 查询队列可用数据个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue ); //返回队列中可用数据的个数

// 查询队列可用空间个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue ); //返回队列中可用空间的个数
  • 队列覆盖(只有长度是一才可用)
//普通覆盖
 BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
                            const void * pvItemToQueue);
//xQueue: 写哪个队列
//pvItemToQueue: 数据地址
//返回值: pdTRUE 表示成功, pdFALSE 表示失败

//中断覆盖
BaseType_t  xQueueOverwriteFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
  • 队列数据偷窥

正常情况下我们读取完队列中的数据(出队)那个数据就会被移除掉,也就是说他只能被读取一次; 数据偷窥其实就是读取队列中的数据但是并不删除数据。

//普通偷窥
BaseType_t xQueuePeek(QueueHandle_t xQueue,
                        void * const pvBuffer,
                        TickType_t xTicksToWait);
//xQueue: 偷看哪个队列
//pvBuffer: 数据地址, 用来保存复制出来的数据 //xTicksToWait: 没有数据的话阻塞一会 //返回值: pdTRUE 表示成功, pdFALSE 表示失败


//中断偷窥
BaseType_t  xQueuePeekFromISR(QueueHandle_t xQueue,
                                void *pvBuffer,);

e.g. 案例: 队列的基本使用

创建一个消息队列(使用CubeMX创建),按键发送任务,一个接收任务
发送任务一设置等待时间为0,接受任务等待时间为最大(portMAX_DELAY),然后观察实验现象

创建步骤:

  1. STM32CubeMX -> FreeRTOS -> Task and Queue
  2. Create Queue

代码如下:


/**
* @brief Function implementing the LED_Co thread.
* @note 任务2: 消息队列读取(出队)
* @param argument: Not used
* @retval None
*/
void LedTaskCo(void const * argument)
{
  BaseType_t xStatus; //接收消息队列状态
  uint32_t Buf;
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();
  for(;;)
  {

      printf("Task2: ");
      printf(argument);
      printf("\r\n");

      // 消息队列读取(出队)
      printf("Now Buf(Out): %ld\r\n", Buf);
//      xStatus = xQueueReceive(myQueue01Handle, &Buf, 0); //接收任务等待时间: 0, 可以改为阻塞 portMAX_DELAY
      xStatus = xQueueReceive(myQueue01Handle, &Buf, portMAX_DELAY); //接收任务等待时间: 阻塞 portMAX_DELAY,队空时不会运行
//      xStatus = xQueuePeek(myQueue01Handle, &Buf, 0); // 偷窥队列: 读取但不删除元素


      if(xStatus == pdPASS)
      {
         printf("Read Buf: %ld Ok!\r\n", Buf);
      } else {
         printf("Queue Out Failed!\r\n");
      }
      vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时500ms
  }
}


/**
* @brief Function implementing the KEY thread.
* @note 任务3: 消息队列发送(入队),按键按下入队
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
  BaseType_t xStatus; //发送
  uint32_t Buf=2023;
  for(;;)
  {
      if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
      {
          osDelay(10);
          if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
          {
              printf("Now Buf(In): %ld\r\n", Buf);
              xStatus = xQueueSendToBack(myQueue01Handle, &Buf, 0); //入队
              if(xStatus == pdPASS)
              {
                  printf("Write Buf: %ld Ok!\r\n", Buf);
              } else {
                 printf("Queue In Failed!\r\n");
              }
          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}

测试结果:

  1. 程序运行时,系统定时1s输出任务二中的消息队列 myQueue01Handle 的值,由于初始队列为空队,故上机不会运行;
  2. 任务3,检测按下 KEY 键时,将2023写入到消息队列中(入队),此是任务2中可以输出2023,是因为队列有值进入,但输出之后(出队),队列又为空,于是任务2开始阻塞,将等待下次队列非空方可再次输出(即新元素入队)。
  3. 任务2,启用偷窥模式时会持续输出,因为并为对队列出队操作。

e.g. 案例: 手动创建消息队列

QueueHandle_t xQueue;

xQueue = xQueueCreate(1, sizeof(int32_t));

信号量

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持,是操作系统中重要的一部分,FreeRTOS 中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。

抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一 (获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都 将处于阻塞状态。

  • 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞 的任务。
  • 正值,表示有一个或多个释放信号量操作。

二值信号量

二值信号量既可以用于临界资源访问也可以用于同步功能。
二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,细微差别:互斥量有优先级继承机制,二值信号量则没有。

    • 二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步)
    • 互斥量更偏向应用于临界资源的访问。
    • 应用: 同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞, 任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态,如果任务 1 的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量,任务 1 也会得到信号 量,从而达到任务与中断间的同步。
    • 理解:将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满 (因此称为二值),运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。也可以简单将二值信号量理解为逻辑开发中的 flag 标志或者布尔值。
    • 常用API

    创建: 手动创建默认初始值为0,STM32CubeMX创建默认为1

    //动态创建:
    SemaphoreHandle_t xSemaphoreCreateBinary( void ); //无需传入参数
    //返回句柄,非 NULL 表示成功
    //默认初始化内部是空
    
    SemaphoreHandle_t vSemaphoreCreateBinary( void ); //此函数已过时,和上面的区别是信号量创建默认就是1
    
    
    // 静态创建: 几乎不使用,了解即可
    SemaphoreHandle_t   xSemaphoreCreateBinaryStatic(
                                StaticSemaphore_t *pxSemaphoreBuffer);
    
    // 删除
    void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); //传入信号量句柄,无返回值
    
    //Take:获取(清空) 正常任务中使用
    BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait); //传入句柄,以及等待等待时间
    //0:不阻塞,马上返回
    //portMAX_DELAY: 一直阻塞直到成功 //返回:pdTRUE 表示成功
    
    //中断中使用
    BaseType_t  xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken); //传入句柄,以及判断是否需要切换任务
    
    
    //Give:释放(置1) 正常任务中使用
    
    BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //传入信号量句柄,信号量将会被置1
    //返回:
    // pdTRUE 表示成功
    // 如果二进制信号量的计数值已经是 1,再次调用此函数则返回失败
    
    //中断中使用:
    
    BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken); //传入句柄,以及判断是否需要切换任务
    
    

    计数信号量

    计数信号量则可以被认为长度大于 1 的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。
    顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。

    每当某个事件发生时,任务或者中断将释放一 个信号量(信号量计数值加 1),当处理事件时(一般在任务中处理),处理任 务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。

    使用完资源的时候必须归还信号量,否则当计数值为 0 的时候任务就无法访问该资源了。

    • 常用API

    使用时 configSUPPORT_DYNAMIC_ALLOCATION 需要设置为1

    创建: 如果使用的CubeMX的话其默认初始化是最大值

    
    // 动态创建:
      
    SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
                                                UBaseType_t
    uxInitialCount); //参数:最大计数值、初始化计数值 //返回:信号量句柄
    
    //静态创建:
    SemaphoreHandle_t xSemaphoreCreateCountingStatic(
                                     UBaseType_t uxMaxCount,
    *pxSemaphoreBuffer ); //几乎不会使用
    UBaseType_t uxInitialCount
    StaticSemaphore_t
    
    // 删除: 同二值信号量,自行转跳
    // Take/Give: 同二值信号量,自行转跳
    
    // 读取数值:
    UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore ); //传入信号量句柄(二值信号量通用),返回数值

    e.g. 案例: 二值信号量的使用

    创建信号量: STM32CubeMX创建默认为1; 手动创建为0

    1. FreeRTOS -> Timers and Semaphores -> Binary Semaphores
    2. Add

    或手动创建:

    SemaphoreHandle_t BinarySem_Handle =NULL;
    /* 创建 BinarySem */
    BinarySem_Handle = xSemaphoreCreateBinary();
    • 源码
    
    /**
    * @brief Function implementing the LED_Co thread.
    * @note Task2 -> 二值信号量(Take): V1.0.3
    * @param argument: Not used
    * @retval None
    */
    void LedTaskCo(void const * argument)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
    
        TickType_t xLastWakeTime;
        xLastWakeTime = xTaskGetTickCount();
      for(;;)
      {
    
          printf("Task2: ");
          printf(argument);
          printf("\r\n");
    
          //获取二值信号量 xSemaphore,没获取到则一直等待
          xReturn = xSemaphoreTake(myBinarySem01Handle,portMAX_DELAY); //二值信号量句柄  等待时间
          if(pdTRUE == xReturn)
              printf("myBinarySem01Handle Got!\n\n");
          else printf("myBinarySem01Handle Failed!\r\n");
    
          vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时500ms
      }
    }
    
    /**
    * @brief Function implementing the KEY thread.
    * @note Task3 -> 消息队列发送(入队),按键按下入队
    * @param argument: Not used
    * @retval None
    */
    /* USER CODE END Header_KeyTask */
    void KeyTask(void const * argument)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
      for(;;)
      {
          if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
          {
              osDelay(10);
              if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
              {
                  xReturn = xSemaphoreGive( myBinarySem01Handle );//给出二值信号量
                  if( xReturn == pdTRUE )
                      printf("myBinarySem01Handle Released!\r\n");
                  else printf("myBinarySem01Handle Released failed!\r\n");
              }
              while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
          }
        osDelay(1);
      }
    }
    • 结果
    1. 上机会输出二值信号量,因为CubeMX 生成的初始信号量默认值为1
    2. 按下 KEY 后,释放(Give)二值信号,会唤醒Task2进行Take输出二值信号量

    e.g. 案例: 计数信号量的使用

    模拟车库计数器

    创建信号量: STM32CubeMX创建默认为最大值; 手动创建为0

    1. FreeRTOS -> Config Parameters -> USE_COUNTING_SEMAPHORES [enable]
    2. FreeRTOS -> Timers and Semaphores -> Counting Semaphores
    3. Add

    或手动创建:

    SemaphoreHandle_t CountSem_Handle =NULL;
    /* 创建 CountSem */
    CountSem_Handle = xSemaphoreCreateCounting(5,5);
    • 创建计数信号量大小为2

      • Task3: (按键)可以将车停入车库/开出车库
      • Task2: 串口每1秒显示一下车库内车辆数据
    
    /**
    * @brief Function implementing the LED_Co thread.
    * @note Task2 -> 计数信号量(Take): V1.0.4
    * @param argument: Not used
    * @retval None
    */
    void LedTaskCo(void const * argument)
    {
      portLONG count; //计数
        TickType_t xLastWakeTime;
        xLastWakeTime = xTaskGetTickCount();
      for(;;)
      {
    
          printf("Task2: ");
          printf(argument);
          printf("\r\n");
    
          //获取计数信号量 xSemaphoreTake
    
          count = uxSemaphoreGetCount(myCountingSem01Handle); // 获取计数信号量数值,模拟车辆数目
          printf("Cars Got %ld!\n\n", count);
    
          vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时500ms
      }
    }
    
    /**
    * @brief Function implementing the KEY thread.
    * @note Taske3 -> 计数信号量(Give): V1.0.4
    * @param argument: Not used
    * @retval None
    */
    void KeyTask(void const * argument)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
      for(;;)
      {
          if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
          {
              osDelay(10);
              if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
              {
    //              xReturn = xSemaphoreGive( myCountingSem01Handle );//给出二值信号量: 模拟停车(需要改计数信号量初始化中的初值为0)
    //              if( xReturn == pdTRUE )
    //                  printf("Car stop!\r\n");
    //              else printf("No Space!\r\n");
    
                  xReturn = xSemaphoreTake(myCountingSem01Handle, 0);//给出二值信号量: 模拟开走
                  if( xReturn == pdTRUE )
                      printf("Car left!\r\n");
                  else printf("No cars!\r\n");
              }
              while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
          }
        osDelay(1);
      }
    }
    • 结果

    通过按键去停车还有取出车辆,并且间隔1秒打印出车库内的状况。

    注: 若停车需要将初始化计数信号量的初置设置为0;CubeMX初始化初值为最大值。

    互斥信号量

    互斥量(Mutex)又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性, 用于实现对临界资源的独占式处理。

    在 FreeRTOS 操作系统中为了降低优先级翻转问题利用了优先级继承算法。 优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。

    • 优先级继承: 当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同优先级的过程。它能够确保高优先级任务进 入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。
    • 优先级翻转: 任务的优先级在创建的时候就已经是设置好的,高优先级的任务可以打断低优先级的任务,抢占 CPU 的使用权。部分应用场景中,由于资源只有一个,低优先级任务在运行时,高优先级任务需要等待,高优先级任务无法运行而低优先级任务可以运行的现象称为 “优先级翻转”。
    • 无互斥量: 产生优先级翻转

    最高优先级的 H 任务阻塞时间 = M 任务运行时间 + L 任务运行时间

    在这过程中,H 任务的等待时间过长,这对系统来说这是很致命的,所以这种情况不允许出现,而互斥量就是用来降低优先级翻转的产生的危害

    • 使用互斥量: 有效降低优先级反转影响

    图(3):在某一时刻 M 任务被唤醒,由于此时 M 任务的优先级暂时低于 L 任务(2⃣️处优先级继承: H将L提示到H同优先级),所以 M 任务仅在就绪态,而无法获得 CPU 使用权。

    在获得互斥量后,请尽快释放互斥量,同时需要注意的是在任务持有互斥量的这段时间,不得更改任务的优先级。 FreeRTOS 的优先级继承机制不能解决优先级反转,只能将这种情况的影响降低到最小,硬实时系统在一开始设计时就要避免优先级反转发生。

    互斥量与二值信号量最大的不同是: 互斥量具有优先级继承机制,而信号量没有。

    在初始化的时候,互斥量处于开锁的状态,而被任务持有的时候则立刻转为闭锁的状态。

    • 应用场景

      • 互斥量的释放只能在任务中,不允许在中断中释放互斥量。
      • 互斥量: 可能会引起优先级翻转的情况。
      • 递归互斥量更适用于: 任务可能会多次获取互斥量的情况下,避免同一任务多次递归持有而造成死锁的问题。
      • e.g. 多个任务对同一串口进行数据发送: 硬件资源仅有一个,需要加互斥锁进行保护。
    • 互斥量运作机制

    递归信号量

    它可以被同一个任务获取很多次,获取多少次就需要释放多少次。递归信号量与互斥量一样,都实现了优先级继承机制,可以降低优先级反转的危害。

    要想使用该函数必须在 FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATIONconfigUSE_RECURSIVE_MUTEXES 均定义为 1(即表示开启动态内存分配)。

    • 常用API函数

    互斥量系统默认是开启的也就是 #define configUSE_MUTEXES 1

    只有函数的创建是不一样的,删除以及其他操作基本上都是完全相同的。

    // 动态创建:
     SemaphoreHandle_t xSemaphoreCreateMutex( void ); //创建互斥量,成功的话返回句柄,失败返回NULL
    //静态创建:
     SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
    
    // 删除:
     void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); //传入句柄即可删除
    
    //  Give\Take: Take和Give需要成对使用
    
    //正常任务中使用:
    BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //传入句柄
    BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait);//传入句柄以及等待时间
    
    //中断中使用:
    BaseType_t  xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
    BaseType_t  xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
    

    e.g. 案例: 模拟优先级翻转

    代码

      
    /**
      * @brief  FreeRTOS initialization
      * @param  None
      * @retval None
      */
    void MX_FREERTOS_Init(void) {
     /* Create the semaphores(s) */
      /* definition and creation of myBinarySem01 */
      osSemaphoreDef(myBinarySem01);
      myBinarySem01Handle = osSemaphoreCreate(osSemaphore(myBinarySem01), 1);
    
      /* Create the thread(s) */
      /* definition and creation of LED_Co */
      osThreadDef(LED_Co, LedTaskCo, _osPriorityAboveNormal_, 0, 128);
      LED_CoHandle = osThreadCreate(osThread(LED_Co), NULL);
    
      /* definition and creation of defaultTask */
      osThreadDef(defaultTask, StartDefaultTask, _osPriorityNormal_, 0, 128);
      defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
    
      /* definition and creation of LED */
      osThreadDef(LED, LedTask, _osPriorityBelowNormal_, 0, 128);
      LEDHandle = osThreadCreate(osThread(LED), NULL);
    }
    
      
    /* USER CODE BEGIN Header_LedTaskCo */
    /**
    * @brief Function implementing the LED_Co thread.
    * @note Task1 -> 模拟优先级翻转 - 高优先级: V1.0.5
    * @param argument: Not used
    * @retval None
    */
    /* USER CODE END Header_LedTaskCo */
    void LedTaskCo(void const * argument)
    {
    BaseType_t xReturn;
    //TickType_t xLastWakeTime;
      //xLastWakeTime = xTaskGetTickCount();
      for(;;)
      {
      printf("HighPriority_Task Take Sem\n");
      //获取二值信号量 xSemaphore,没获取到则一直等待
      xReturn = xSemaphoreTake(myBinarySem01Handle,/* 二值信号量句柄 */
      portMAX_DELAY); /* 等待时间 */
      if(pdTRUE == xReturn)
      printf("HighPriority_Task Running\n");
      xReturn = xSemaphoreGive( myBinarySem01Handle );//给出二值信号量
    //   vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时500ms
      vTaskDelay(500);
      }
    }
    
    
    /* USER CODE BEGIN Header_StartDefaultTask */
    /**
      * @brief  Function implementing the defaultTask thread.
      * @param  argument: Not used
      * @retval None
      * @note Task2 -> 模拟优先级翻转 - 中优先级任务: V1.0.5
      */
    /* USER CODE END Header_StartDefaultTask */
    void StartDefaultTask(void const *argument)
    {
      for(;;)
      {
      printf("MidPriority_Task Running\n");
      vTaskDelay(500);
      osDelay(1);
      }
    }
    
      
    
    /* USER CODE BEGIN Header_LedTask */
    /**
    * @brief Function implementing the LED thread.
    * @note Task3 -> 模拟优先级翻转 - 低优先级任务:  V1.0.5
    * @param argument: Not used
    * @retval None
    */
    /* USER CODE END Header_LedTask */
    void LedTask(void const *argument)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
      long i;
      for(;;)
      {
        printf("LowPriority_Task Take Sem\n");
      //获取二值信号量 xSemaphore,没获取到则一直等待
      xReturn = xSemaphoreTake(myBinarySem01Handle,/* 二值信号量句柄 */
      portMAX_DELAY); /* 等待时间 */
      if( xReturn == pdTRUE )
      printf("LowPriority_Task Running\n\n");
      for(i=0;i<2000000;i++)//模拟低优先级任务占用信号量
      {
      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);//发起任务调度
      }
      printf("LowPriority_Task Give Sem!\r\n");
      xReturn = xSemaphoreGive( myBinarySem01Handle );//给出二值信号量
      vTaskDelay(500);
      }
    }
    
    
    • 实验结果
      当上电开机之后,系统会调用一次最高优先级的任务,是由于CubeMX在生成二值信号量时给的初值为1(即已经释放二值信号量),此时任务1可以获取该信号量(同时获取之后信号量减1为0);此后,由于高优先级任务 Task1 无法获取信号量进入阻塞,故系统调度余下任务,其中 Task2 中优先级任务 > Task3 低优先级任务,故会先执行 Task2,之后执行 低优先级任务 Task3,因此后面的Task1高优先级任务均会等待 Task2 和 Task3 执行之后才会从阻塞进入就绪到运行状态。(PS: 可以看上方附图.产生优先级翻转解析)
      注: 高优先级 > 中 > 低
    00:23:01:898 -> HighPriority_Task Take Sem
    00:30:49:205 -> HighPriority_Task Running
    00:30:49:210 -> MidPriority_Task Running
    00:30:49:213 -> LowPriority_Task Take Sem
    00:30:49:214 -> LowPriority_Task Running
    00:30:49:217 -> 
    00:30:49:707 -> HighPriority_Task Take Sem
    00:30:49:711 -> MidPriority_Task Running
    00:30:50:214 -> MidPriority_Task Running
    00:30:50:717 -> MidPriority_Task Running
    00:30:51:220 -> MidPriority_Task Running
    00:30:51:400 -> LowPriority_Task Give Sem!
    00:30:51:404 -> HighPriority_Task Running
    00:30:51:724 -> MidPriority_Task Running
    00:30:51:905 -> HighPriority_Task Take Sem
    00:30:51:908 -> HighPriority_Task Running
    00:30:51:913 -> LowPriority_Task Take Sem
    00:30:51:914 -> LowPriority_Task Running
    00:30:51:918 -> 
    00:30:52:226 -> MidPriority_Task Running
    00:30:52:410 -> HighPriority_Task Take Sem
    00:30:52:729 -> MidPriority_Task Running
    00:30:53:232 -> MidPriority_Task Running
    00:30:53:735 -> MidPriority_Task Running
    ---- 关闭串行端口 /dev/tty.usbmodemIamSuroy ----
    

    e.g. 案例: 互斥量优先级继承

    代码

    /**
      * @brief  FreeRTOS initialization
      * @param  None
      * @retval None
      */
    void MX_FREERTOS_Init(void) {
     /* Create the semaphores(s) */
      /* definition and creation of myBinarySem01 */
        
      //互斥量
      osMutexDef(myMutex01);
      myMutex01Handle = osMutexCreate(osMutex(myMutex01));
      
      
      /* Create the thread(s) */
      /* definition and creation of LED_Co */
      osThreadDef(LED_Co, LedTaskCo, _osPriorityAboveNormal_, 0, 128);
      LED_CoHandle = osThreadCreate(osThread(LED_Co), NULL);
    
      /* definition and creation of defaultTask */
      osThreadDef(defaultTask, StartDefaultTask, _osPriorityNormal_, 0, 128);
      defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
    
      /* definition and creation of LED */
      osThreadDef(LED, LedTask, _osPriorityBelowNormal_, 0, 128);
      LEDHandle = osThreadCreate(osThread(LED), NULL);
    }
    
      
    /* USER CODE BEGIN Header_LedTaskCo */
    /**
    * @brief Function implementing the LED_Co thread.
    * @note Task1 -> 模拟优先级翻转 - 高优先级: V1.0.6
    * @param argument: Not used
    * @retval None
    */
    /* USER CODE END Header_LedTaskCo */
    void LedTaskCo(void const * argument)
    {
    BaseType_t xReturn;
    //TickType_t xLastWakeTime;
      //xLastWakeTime = xTaskGetTickCount();
      for(;;)
      {
      printf("HighPriority_Task Take Sem\n");
      //获取互斥量 xSemaphore,没获取到则一直等待
      xReturn = xSemaphoreTake(myMutex01,/* 互斥量句柄 */
      portMAX_DELAY); /* 等待时间 */
      if(pdTRUE == xReturn)
      printf("HighPriority_Task Running\n");
      xReturn = xSemaphoreGive( myMutex01 );//给出互斥量
    //   vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时500ms
      vTaskDelay(500);
      }
    }
    
    
    /* USER CODE BEGIN Header_StartDefaultTask */
    /**
      * @brief  Function implementing the defaultTask thread.
      * @param  argument: Not used
      * @retval None
      * @note Task2 -> 模拟优先级翻转 - 中优先级任务: V1.0.6
      */
    /* USER CODE END Header_StartDefaultTask */
    void StartDefaultTask(void const *argument)
    {
      for(;;)
      {
      printf("MidPriority_Task Running\n");
      vTaskDelay(500);
      osDelay(1);
      }
    }
    
      
    
    /* USER CODE BEGIN Header_LedTask */
    /**
    * @brief Function implementing the LED thread.
    * @note Task3 -> 模拟优先级翻转 - 低优先级任务:  V1.0.6
    * @param argument: Not used
    * @retval None
    */
    /* USER CODE END Header_LedTask */
    void LedTask(void const *argument)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
      long i;
      for(;;)
      {
        printf("LowPriority_Task Take Sem\n");
      //获取互斥量 xSemaphore,没获取到则一直等待
      xReturn = xSemaphoreTake(myMutex01,/* 互斥量句柄 */
      portMAX_DELAY); /* 等待时间 */
      if( xReturn == pdTRUE )
      printf("LowPriority_Task Running\n\n");
      for(i=0;i<2000000;i++)//模拟低优先级任务占用信号量
      {
      HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);//发起任务调度
      }
      printf("LowPriority_Task Give Sem!\r\n");
      xReturn = xSemaphoreGive( myMutex01 );//给出互斥量
      vTaskDelay(500);
      }
    }
    
    
    • 实验结果
      当上电开机之后,系统会调用一次最高优先级的任务,是由于CubeMX在生成互斥量时给的初值为1(即已经释放互斥量),此时任务1可以获取该信号量(同时获取之后信号量减1为0);然后,由于高优先级任务 Task1 无法获取信号量进入阻塞,故系统调度余下任务,其中 Task2 中优先级任务 > Task3 低优先级任务,故会先执行 Task2,之后执行 低优先级任务 Task3;最后从此Task1高优先级任务均会阻塞等待互斥量释放,同时将 低优先级任务Task3提到同级(即高优先级),待Task3执行之后才会从阻塞进入就绪到运行状态,方运行Task2 中优先级任务。(PS: 可以看上方附图.互斥量运作机制解析)
    01:27:59:410 -> HighPriority_Task Take Sem
    01:28:01:356 -> HighPriority_Task Running
    01:28:01:356 -> MidPriority_Task Running
    01:28:01:359 -> LowPriority_Task Take Sem
    01:28:01:363 -> LowPriority_Task Running
    01:28:01:363 -> 
    01:28:01:853 -> HighPriority_Task Take Sem
    01:28:03:501 -> LowPriority_Task Give Sem!
    01:28:03:505 -> HighPriority_Task Running
    01:28:03:508 -> MidPriority_Task Running
    01:28:04:006 -> HighPriority_Task Take Sem
    01:28:04:009 -> HighPriority_Task Running
    01:28:04:012 -> LowPMidPriority_Task Running
    01:28:04:016 -> riority_Task Take Sem
    01:28:04:018 -> LowPriority_Task Running
    01:28:04:020 -> 
    01:28:04:511 -> HighPriority_Task Take Sem
    01:28:06:159 -> LowPriority_Task Give Sem!
    01:28:06:163 -> HighPriority_Task Running
    01:28:06:167 -> MidPriority_Task Running
    01:28:06:665 -> HighPriority_Task Take Sem
    01:28:06:668 -> HighPriority_Task Running
    01:28:06:671 -> LowPriority_Task Take Sem
    01:28:06:674 -> LowPriority_Task Running
    01:28:06:677 -> 
    01:28:07:170 -> HighPriority_Task Take Sem
    ---- 关闭串行端口 /dev/tty.usbmodemIamSuroy ----
    

    声明:Grows towards sunlight |版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

    转载:转载请注明原文链接 - 记基于STM32 的 FreeRTOS 学习的移植日志【一】


    Grows towards sunlight and Carpe Diem