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


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

本文涉及知识有 FreeRTOS 中事件组、任务通知、定时器、中断管理等概念和常用API及部分经典案例。

事件组(Event Group)

信号量来同步的话任务只能与单个的任务进行同步。有时候某个任务可能会需要与多个任务进行同步,此时信号量就无能为力。

FreeRTOS 事件标志组解决某个任务需要与多个任务进行同步的应用场景。

事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多多对多的同步。

  • 差异

    • 与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。
    • 信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
  • 定义

    • 一个事件组只需要很少的 RAM 空间来保存事件组的状态。
    • configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就是 16 位的,其中有 8 个位用来存储事件组;为 0, 是32 位的,其中有24个位用来存储事件组,一般是24个Bit。
  • 特点

    • 事件标志组

      每一位代表一个事件,任务通过“逻辑 与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。
      • 事件的“逻辑或”也被称作是独立型同步,指的是任务感兴趣的所有事件任一件发生即可被唤醒;
      • 事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。
    • 任务模型

      • 一对多同步模型: 一个任务等待多个事件的触发,这种情况是比较常见的;
      • 多对多同步模型: 多个任务等待多个事件的触发。
      • 事件唤醒机制,当任务因为等待某个或者多个事件发生而进入阻塞态,当事 件发生的时候会被唤醒,其过程具体如下。
  • 注意

    • 事件接收成功后,必须使用 xClearOnExit 选项来清除已接收到的事件类型
    • “逻辑或”触发还是“逻辑与”触发
    • 事件不与任务相关联,事件相互独立,一个 32 位的变量(事件集合,实际 用于表示事件的只有 24 位)每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生)
  • 应用场景

    FreeRTOS 的事件用于事件类型的通讯,无数据传输。
    • 简单方便替换裸机编程中全局变量作为标志的作用。
    • 危险机器的启动,启动检查各项指标,检查各个指标时,采用事件做统一的等待,当所有 的事件都完成并达标,机器才允许启动。
    • 事件可使用于多种场合,它能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步
  • 常用API
// 动态创建:
  EventGroupHandle_t xEventGroupCreate( void ); //返回句柄或者NULL
//静态创建:
 EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *
pxEventGroupBuffer );

// 删除:
 void vEventGroupDelete( EventGroupHandle_t xEventGroup ); //传入句柄

// 设置事件组: 
//正常任务使用:
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                               const EventBits_t uxBitsToSet );
//传入句柄以及需要设置的标志位 
//标志位为0不改变标志位,为1对应标志位置1

//中断中使用:
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t
xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *  pxHigherPriorityTaskWoken ); //传入句柄、标志位、判断是否需要切换任务

// 等待事件组:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait );
//xEventGroup:任务句柄
//uxBitsToWaitFor:判断标志位 //xClearOnExit:是否清楚标志位中的uxBitsToWaitFor这几位 //xWaitForAllBits:判断模式
// pdTRUE: 等待的位,全部为 1;
// pdFALSE: 等待的位,某一个为 1 即可 
//xTicksToWait:等待时间
// 0:立即返回
// portMAX_DELAY:知道接收到为止 //返回值:事件组的值

e.g. 案例: 事件组与操作判断模拟

模拟火箭倒计时点火发射。
  • STM32CubeMX 采用 Interface: CMSIS_V1 无法创建事件组

    • 可以采用 V2 进行创建
    • 或手动参考本文创建事件组
  • 代码

EventGroupHandle_t Event_Handle =NULL; //事件组句柄

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {

  /* 创建 Event_Handle */
  Event_Handle = xEventGroupCreate();
  
  /* Create the thread(s) */
  /* definition and creation of LED_Co */
  osThreadDef(LED_Co, LedTaskCo, osPriorityNormal, 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, osPriorityNormal, 0, 128);
  LEDHandle = osThreadCreate(osThread(LED), NULL);
}


/**
* @brief Function implementing the LED thread.
* @note Task1 -> 检测事件组条件满足(发射火箭):  V1.0.7
* @param argument: Not used
* @retval None
*/
void LedTask(void const * argument)
{
    BaseType_t r_event = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
  for(;;)
  {
      r_event = xEventGroupWaitBits(Event_Handle, /* 事件对象句柄 */
          0x0011,/* 接收线程感兴趣的事: 事件组寄存器值 */
          pdTRUE, /* 退出时清除事件位 */
          pdTRUE, /* 满足感兴趣的所有事件 */
          portMAX_DELAY);/* 指定超时事件,一直等 */
      if(r_event)
      {
            printf ( "Rocket Fire!\n");
            HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);//发起任务调度
      }
      else printf ("Event failed!\n");

      vTaskDelay(500);

  }
}

/**
* @brief Function implementing the LED_Co thread.
* @note Task2 -> 模拟按键事件(倒计时): V1.0.7
* @param argument: Not used
* @retval None
*/
void LedTaskCo(void const * argument)
{
    static long i=10;//sec
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();
  for(;;)
  {
      if(i==1){
          printf("Ready!\r\n");
          xEventGroupSetBits(Event_Handle, 0x0001); //触发事件:第1位置1
      } else printf("CountDown: %ld\r\n", i--);
      vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时10s
  }
  /* USER CODE END LedTaskCo */
}

/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 按键按下(模拟点火): V1.0.7
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
  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)
          {
              xEventGroupSetBits(Event_Handle, 0x0010); //触发事件:第5位置1
              printf("Fire Ready!\r\n");
          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}
  • 实验结果
    任务2倒数10秒,当任务3按键按下后,且倒计时1秒时发射。
03:23:15:510 -> CountDown: 10
03:23:17:861 -> Program Running
03:23:18:859 -> CountDown: 9
03:23:19:862 -> CountDown: 8
03:23:20:859 -> CountDown: 7
03:23:21:859 -> CountDown: 6
03:23:22:858 -> CountDown: 5
03:23:22:862 -> Program Running
03:23:23:858 -> CountDown: 4
03:23:24:858 -> CountDown: 3
03:23:25:858 -> CountDown: 2
03:23:26:858 -> Ready!
03:23:27:858 -> Ready!
03:23:27:864 -> Program Running
03:23:28:858 -> Ready!
03:23:29:858 -> Ready!
03:23:30:857 -> Ready!
03:23:31:857 -> Ready!
03:23:32:223 -> Fire Ready!
03:23:32:390 -> Rocket Fire!
---- 关闭串行端口 /dev/tty.usbmodemIAmSuroy ----

任务通知

FreeRTOS 从 V8.2.0 版本开始提供任务通知这个功能,每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、 事件组,也可以替代长度为 1 的队列(可以保存一个 32 位整数或指针值)。

使用任务通知比通过信号量等 IPC 通信方式解除阻塞的任务要快 45%, 并且更加省 RAM 内存空间(GCC 编译器,-o2 优化级别),任务通知的使用无需创建队列。想要使用任务通知,必须将 FreeRTOSConfig.h 中的宏定义 configUSE_TASK_NOTIFICATIONS 设置为 1,默认即为1。

  • FreeRTOS 提供以下几种方式发送通知给任务

    • 发送通知给任务,如果有通知未读,不覆盖通知值。
    • 发送通知给任务,直接覆盖通知值。
    • 发送通知给任务,设置通知值的一个或多个位,可以做事件组使用。
    • 发送通知给任务,递增通知值,可以当做计数信号量使用。
  • 局限性

    • 只能有一个任务接收通知消息,因为必须指定接收通知的任务。
    • 只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。
  • 任务通知运作机制

    • 任务通知,由于任务通知的数据结构包含在任务控制块中,只要 任务存在,任务通知数据结构就已经创建完毕,可以直接使用
    • 任务控制块中的成员变量 ulNotifiedValue 即32位的通知值,仅允许在任务中等待通知,中断无效。
    • 任务会根据用户指定的 阻塞超时时间进入阻塞状态
    • 模型

      • 消费者: 等待通知的任务
      • 生产者: 发送通知的任务和中断服务函数
      • 发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除

每个任务都有一个结构体: TCB(Task Control Block)

  • 通知状态: uint8_t

    • taskNOT_WAITING_NOTIFICATION: 任务没有在等待通知(默认是这个)
    • taskWAITING_NOTIFICATION: 任务在等待通知
    • taskNOTIFICATION_RECEIVED: 任务接收到了通知,也被称为 pending(有数据了,待处理)
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 也是初始状态*/
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )
  • 通知值: uint32_t

    • 存0和1 -> 作二值信号量
    • 存数字0-N -> 计数信号量
    • 存“信息” -> 作队列
    • 每一位单独作为标志位 -> 事件组
  • 常用API
若在宏定义中关闭,可以节省五个字节的内存
它的函数也分为简化版(仅模拟二值信号量和计数信号量)和全功能版
// [简化版]: 普通任务中使用
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); //传入接收任务的句柄
//使用以后接收的任务其状态将会改为taskNOTIFICATION_RECEIVED,并且使得我们的任务通知加1
//返回pdPASS

//中断中使用
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken); //!!!不论发送是否有效,都是返回pdPASS


// 接收通知:
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t
xTicksToWait );
//xClearCountOnExit:
//     pdTRUE:把通知值清零 -> 二值信号量
//     pdFALSE:如果通知值大于 0,则把通知值减一 -> 计数信号量 
//xTicksToWait: 0:不等待,即刻返回;portMAX_DELAY:一直等待,直到通知值大于0 
//返回清除之前的数值


// [完整版]: 发出通知
//普通任务中使用:
BaseType_t  xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction); //xTaskToNotif:任务句柄,给哪个任务发通知
//ulValue:取决于eAction
//eAction:
//   eNoAction:更新通知状态为"pending",未使用 ulValue,这个选项相当于 二进制信号量
//   eSetBits:通知值 = 原来的通知值|ulValue,按位或,相当于事件组
//   eIncrement:通知值 = 原来的通知值 + 1,未使用 ulValue,相当于计数型 信号量(二值信号量)
//   eSetValueWithoutOverwrite:如果数据未读状态,则此次调用不做任何事返 回pdFAIL,否者写入ulValue
// 相当于队列
//   eSetValueWithOverwrite:直接用ulValue覆盖原来的数值,相当于队列的覆盖操作

//中断中使用:
BaseType_t  xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken );

//接收通知:
BaseType_t  xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
                             uint32_t ulBitsToClearOnExit,
                             uint32_t *pulNotificationValue,
                             TickType_t xTicksToWait );
//ulBitsToClearOnEntry: 在 xTaskNotifyWait 入口处,要清除通知值的哪些位通知状态不是"pending"的情况下,才会清除。它的本意是我想等待某些事件发生,所以先 把"旧数据"的某些位清零。
//ulBitsToClearOnExit: 在 xTaskNotifyWait 出口处,如果不是因为超时退出,而是因为得到了数据而退出时
//pulNotificationValue: 用来取出通知值
//xTicksToWait: 0,不等待,即刻返回; portMAX_DELAY:一直等待
//返回: pdPASS:成功 pdFAIL:没有得到通知

e.g. 案例: 任务通知之模拟二值信号量

无需声明,任务创建时任务即具有一个32位的通知值。

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* definition and creation of Task1 */
  osThreadDef(LED, LedTask, osPriorityNormal, 0, 128);
  LEDHandle = osThreadCreate(osThread(LED), NULL);

  /* definition and creation of Task2 */
  osThreadDef(LED_Co, LedTaskCo, osPriorityNormal, 0, 128);
  LED_CoHandle = osThreadCreate(osThread(LED_Co), NULL);

  /* definition and creation of Task3 */
  osThreadDef(KEY, KeyTask, osPriorityBelowNormal, 0, 128);
  KEYHandle = osThreadCreate(osThread(KEY), NULL);

}

/**
* @brief Function implementing the LED thread.
* @note Task1 -> 任务通知.模拟二值信号量.接收任务1:  V1.0.8
* @param argument: Not used
* @retval None
*/
void LedTask(void const * argument)
{
  for(;;)
  {

      //获取任务通知 ,没获取到则一直等待
      ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
      printf("Receive1_Task Got!\n\n");
      vTaskDelay(20);

  }
}

/**
* @brief Function implementing the LED_Co thread.
* @note Task2 -> 任务通知.模拟二值信号量.接收任务2:  V1.0.8
* @param argument: Not used
* @retval None
*/
void LedTaskCo(void const * argument)
{
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();
  for(;;)
  {

      //模拟二值信号量: 获取任务通知 ,没获取到则一直等待
      ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
      printf("Receive2_Task Got!\n\n");

      vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时10s
  }
}

/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 按键按下(模拟释放二值信号量): V1.0.8
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
    BaseType_t xReturn;
  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)
          {
              osDelay(500);
              if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
              { //长按
                      xReturn = xTaskNotifyGive(LEDHandle);
                      if( xReturn == pdTRUE )
                      printf("Receive1_Task_Handle Send!\r\n");
              } else {
                        //短按
                      xReturn = xTaskNotifyGive(LED_CoHandle);
                      if( xReturn == pdTRUE )
                      printf("Receive2_Task_Handle Send!\r\n");
              }
              while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开

          }
      }
    osDelay(1);
  }
}

结果:

00:37:07:629 -> Receive2_Task Got!
00:37:07:631 -> 
00:37:07:632 -> Receive2_Task_Handle Send!
00:37:09:328 -> Program Running
00:37:09:971 -> Receive2_Task Got!
00:37:09:974 -> 
00:37:09:974 -> Receive2_Task_Handle Send!
00:37:11:464 -> Receive1_Task Got!
00:37:11:467 -> 
00:37:11:467 -> Receive1_Task_Handle Send!
00:37:14:329 -> Program Running
00:37:14:531 -> Receive1_Task Got!
00:37:14:533 -> 
00:37:14:533 -> Receive1_Task_Handle Send!
00:37:16:079 -> Receive1_Task Got!
00:37:16:083 -> 
00:37:16:083 -> Receive1_Task_Handle Send!
00:37:17:176 -> Receive2_Task Got!
00:37:17:179 -> 
00:37:17:179 -> Receive2_Task_Handle Send!
00:37:19:330 -> Program Running

e.g. 案例: 任务通知模拟计数信号量

与二值信号量的区别在于任务优先级、获取任务通知不阻塞不清零。

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* definition and creation of LED_Co */
  osThreadDef(LED_Co, LedTaskCo, osPriorityBelowNormal, 0, 128);
  LED_CoHandle = osThreadCreate(osThread(LED_Co), NULL);

  /* definition and creation of KEY */
  osThreadDef(KEY, KeyTask, osPriorityNormal, 0, 128);
  KEYHandle = osThreadCreate(osThread(KEY), NULL);

}


/**
* @brief Function implementing the LED_Co thread.
* @note Task2 -> 任务通知(模拟计数信号量.获取信号量.短按开走):  V1.0.8
* @param argument: Not used
* @retval None
*/
void LedTaskCo(void const * argument)
{
    uint32_t take_num = pdTRUE;
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();
  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)
                {
      //模拟计数信号量: 获取任务通知 ,没获取到则不等待
      take_num = ulTaskNotifyTake(pdFALSE,0);
      if(take_num > 0)printf("Now cars: %ld\r\n", take_num-1);
                }
              while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开

            }
      vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时10s
  }
}



/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 任务通知(模拟计数信号量.释放信号量.长按停车): V1.0.9
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
    BaseType_t xReturn;
  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)
          {
              osDelay(500);
              if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
              { //长按停车
                      xReturn = xTaskNotifyGive(LED_CoHandle);
                      if( xReturn == pdTRUE )
                      printf("Receive1_Task_Handle Send!\r\n");
              }
              while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开

          }
      }
    osDelay(1);
  }
}

e.g. 案例: 任务通知模拟事件组

任务通知模拟火箭发射: 逻辑与
  • Task1: 即时检测火箭发射准备状态
  • Task2: 模拟倒计时事件
  • Task3: 模拟按键确认发射事件

/**
* @brief Function implementing the LED thread.
* @note Task1 -> 任务通知.模拟事件组.检测发射:  V1.0.10
* @param argument: Not used
* @retval None
*/
void LedTask(void const * argument)
{
    BaseType_t xReturn;
    uint32_t r_event = 0; /* 定义一个事件接收变量 */
    uint32_t last_event = 0;/* 定义一个保存事件的变量 */
  for(;;)
  {

      //获取任务通知 ,没获取到则一直等待
      xReturn = xTaskNotifyWait(0x0, //进入函数的时候不清除任务
               0xffffffff, //退出函数的时候清除所有的bitR
               &r_event, //保存任务通知值
               portMAX_DELAY); //阻塞时间

      if( pdTRUE == xReturn )
      {
          last_event |= r_event;
          if(last_event == (0x01|0x10))
          {
              last_event=0;
              printf ( "Start Fire!\r\n");
              HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
          }else last_event = r_event;
      }
      vTaskDelay(20);

  }
}

/**
* @brief Function implementing the LED_Co thread.
* @note Task2 -> 任务通知.模拟事件组.倒计时:  V1.0.10
* @param argument: Not used
* @retval None
*/
void LedTaskCo(void const * argument)
{
    uint32_t n=10;
    TickType_t xLastWakeTime;
    xLastWakeTime = xTaskGetTickCount();
  for(;;)
  {

      n--;
      if(n==1){
          printf("Task2 Ready...\r\n");
          xTaskNotify(LEDHandle, 0x0001, eSetBits); /* 触发一个事件 1 */
          n=10;
      }
      else printf("CountDown: %ld\r\n", n);

      vTaskDelayUntil(&xLastWakeTime,1000); //绝对延时10s
  }
}

/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 任务通知.模拟事件组.点火: V1.0.10
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
  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("Task3 Ready...\r\n");
              xTaskNotify(LEDHandle, 0x0010, eSetBits); /* 触发一个事件 1 */

          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}

e.g. 案例: 任务通知模拟消息队列

设置接收消息任务为无限阻塞时需要考虑多任务下优先级问题。
  • Task1: 即时检测接收消息队列并输出
  • Task3: 模拟按键确认发送消息队列(发出消息地址,以解决最大传输4字节)
  • 程序代码

/**
* @brief Function implementing the LED thread.
* @note Task1 -> 任务通知.模拟消息队列.接收消息:  V1.0.11
* @param argument: Not used
* @retval None
*/
void LedTask(void const * argument)
{
    BaseType_t xReturn;
    uint32_t r_addr; /* 定义一个事件接收变量 */
  for(;;)
  {

      //获取任务通知 ,没获取到则一直等待
      xReturn = xTaskNotifyWait(0x0, //进入函数的时候不清除任务
               0xffffffff, //退出函数的时候清除所有的bitR
               &r_addr, //保存任务通知值
               0); //阻塞时间, 无限阻塞时需要考虑任务优先级问题

      if( pdTRUE == xReturn )
      {
          printf ( "Receive Task: %s\r\n", (uint8_t *)r_addr); // 邮箱消息
          HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
      }
      vTaskDelay(20);

  }
}


/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 任务通知.模拟队列消息.发送: V1.0.11
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
    BaseType_t xReturn;
    char str[] = "Hi, this is Suroy.\r\n";
  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 = xTaskNotify(LEDHandle, /*任务句柄*/
                          (uint32_t)&str, //发送的数据,最大为 4 字节
                          eSetValueWithOverwrite); /*覆盖当前通知, eSetValueWithoutOverwrite 不覆盖*/
              if( xReturn == pdPASS )
              printf("LEDHandle Send Ok!\r\n");

          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}
  • 串口日志

02:10:52:250 -> Program Running

02:10:53:867 -> LEDHandle Send Ok!

02:10:53:872 -> Receive Task: Hi, this is Suroy.

02:10:53:878 ->

02:10:54:280 -> Program Running

定时器

定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事 件,用户可以自定义定时器的周期与频率。

分类

  • 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入 时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后 芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别, 并且是中断触发方式。
  • 软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件 定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数,为了统一,下文均用回调函 数描述),在回调函数中处理信息。
  • 软件定时器

    • FreeRTOS 提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用软件定时器的回调函数。
    • 单次模式: 当用户创建了定时器并启动了定时器后,定时时间到了,只执行 一次回调函数之后就将该定时器进入休眠状态,不再重新执行。

      • 周期模式: 这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除,如下所示:

FreeRTOS 通过一个启动调度器时自动创建 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,只有设置 FreeRTOSConfig.h 中的宏定义 configUSE_TIMERS 设置为 1 , 将相关代码编译进来,才能正常使用软件定时器相关功能。其内部实现的原理是通过队列去完成的,当执行相关的定时器的函数,其实他就是向队列中发了一个数据。

  • 应用场景

    • 硬件定时器受硬件的限制,数量 上不足以满足用户的实际需求。
    • 适用于对时间精度要求不高的任务,一些辅助型的任务。
  • 运行机制

    • 通常软件定时器以系统节拍周期为计时单位。
    • 在 FreeRTOSConfig.h 中有宏定义configTICK_RATE_HZ,默 认是 1000。系统的时钟节拍周期就为 1ms(1s 跳动 1000 下)
    • 节拍定义了系统中定时器能够分辨的精确度,所定时数值必须是这个节拍周期的整数倍
  • 常用API
任务优先级,可以通过configTIMER_TASK_PRIORITY去设置,一般建议优先级大一点,以防被其他任务抢走导致走时不准。
队列长度,configTIMER_QUEUE_LENGTH 的宏定义,如队列满了,会导致我们的消息发送失败。
回调函数中不能使用相关的阻塞函数!!!
// 定时器创建: 动态创建
TimerHandle_t xTimerCreate( const char * const pcTimerName,
                            const TickType_t xTimerPeriodInTicks,
                            const UBaseType_t uxAutoReload,
                            void * const pvTimerID,
                            TimerCallbackFunction_t
pxCallbackFunction );
//pcTimerName:定时器名字, 用处不大, 尽在调试时用到 
//xTimerPeriodInTicks: 周期, 以 Tick 为单位 //uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性 
//pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器 
//pxCallbackFunction: 回调函数
//返回值: 成功则返回 TimerHandle_t, 否则返回 NULL

//静态创建
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
                                 TickType_t xTimerPeriodInTicks,
                                 UBaseType_t uxAutoReload,
                                 void * pvTimerID,
                                 TimerCallbackFunction_t
pxCallbackFunction,
StaticTimer_t *pxTimerBuffer ); //pcTimerName:定时器名字, 用处不大, 尽在调试时用到
//xTimerPeriodInTicks: 周期, 以 Tick 为单位
//uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性 
//pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器 
//pxCallbackFunction: 回调函数
//pxTimerBuffer: 传入一个 StaticTimer_t 结构体, 将在上面构造定时器 
//返回值: 成功则返回 TimerHandle_t, 否则返回 NULL



// 回调函数类型
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer ); //我们需要按照这种形式去写我们的回调函数命名,就像下面这种
void ATimerCallback( TimerHandle_t xTimer )


// 删除
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
//传入句柄,以及超时时间(队列写入的等待时间)


// 启动定时器: 普通任务中使用:
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
//传入句柄,以及超时时间(队列写入的等待时间)

//中断中使用:
BaseType_t  xTimerStartFromISR( TimerHandle_t xTimer,
                                BaseType_t
*pxHigherPriorityTaskWoken ); //传入句柄,判断任务是否需要切换



// 停止定时器: 普通任务中使用:
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
//传入句柄,以及超时时间(队列写入的等待时间)

//中断中使用:
BaseType_t  xTimerStopFromISR(  TimerHandle_t xTimer,
                                BaseType_t
*pxHigherPriorityTaskWoken ); //传入句柄,判断任务是否需要切换


// 复位定时器: 普通任务中使用:
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
//传入句柄,以及超时时间(队列写入的等待时间

//中断中使用:
BaseType_t xTimerResetFromISR(  TimerHandle_t xTimer,
                                BaseType_t
*pxHigherPriorityTaskWoken ); //传入句柄,判断任务是否需要切换



// 修改周期:
BaseType_t xTimerChangePeriod(  TimerHandle_t xTimer,
                                TickType_t xNewPeriod,
                                TickType_t xTicksToWait );
//xTimer: 哪个定时器
//xNewPeriod: 新周期
//xTicksToWait: 超时时间, 命令写入队列的超时时间

BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, TickType_t xNewPeriod, BaseType_t *pxHigherPriorityTaskWoken );


// 获取ID:
 void *pvTimerGetTimerID( TimerHandle_t xTimer ); //xTimer: 哪个定时器

// 修改ID:
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID ); //xTimer: 哪个定时器
//pvNewID: 新 ID

e.g. 案例: 软件定时器

CubeMX配置

使用CubeMX创建默认周期为1,需要自行修改,手动创建不需要更改周期
  • 开启软件定时器: FreeRTOS -> Config parameters -> Soft Timer definitions -> USE_TIMERS [enable]
  • 开启定时器回调函数: FreeRTOS -> Include parameters -> xTimerPendFunctionCall [enable]
  • Timers and Semaphores -> Add Timer
  • 任务: 软件定时器

    • Timer1: 500ms; Timer2: 1000ms
    • 初始化时手动修改周期,修改周期后自动启动
    • 按键按下切换 Timer1 开关 KeyTask
  • 核心程序

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* Create the timer(s) */
  /* definition and creation of myTimer01 */
  osTimerDef(myTimer01, Callback01);
  myTimer01Handle = osTimerCreate(osTimer(myTimer01), osTimerPeriodic, NULL);

  /* definition and creation of myTimer02 */
  osTimerDef(myTimer02, Callback02);
  myTimer02Handle = osTimerCreate(osTimer(myTimer02), osTimerPeriodic, NULL);

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  xTimerChangePeriod(myTimer01Handle, 500, 200); // 修改定时器1周期:500ms, 发送队列等待时间200ms(任填)
  xTimerChangePeriod(myTimer02Handle, 1000, 200);
  /* USER CODE END RTOS_TIMERS */

  /* definition and creation of KEY */
  osThreadDef(KEY, KeyTask, osPriorityNormal, 0, 128);
  KEYHandle = osThreadCreate(osThread(KEY), NULL);

}

/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 定时器.控制定时器开关: V1.0.12
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
    static BaseType_t xPress = pdFALSE;
  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)
          {
              if(xPress == pdFALSE)
              {
                 xTimerStart(myTimer01Handle, 100); // 启动定时器1: 发送队列超时100ms
                 xPress = pdTRUE;
              }
              else{
                  xTimerStop(myTimer01Handle, 100); // 关闭定时器1: 发送队列超时100ms
                  xTimerStop(myTimer02Handle, 100); // 关闭定时器2
                  printf("Timers Stop!\r\n");
                  xPress = pdFALSE;
              }

          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}

/* Callback01 function */
void Callback01(void const * argument)
{
  /* USER CODE BEGIN Callback01 */
    printf("Timer1\r\n");
  /* USER CODE END Callback01 */
}

/* Callback02 function */
void Callback02(void const * argument)
{
  /* USER CODE BEGIN Callback02 */
    printf("Timer2\r\n");
  /* USER CODE END Callback02 */
}

中断管理

中断是 CPU 的一种常见特性,中断一般由硬件产生,当中断发生后,会中断 CPU 当前正 在执行的程序而跳转到中断对应的服务程序种去执行,ARM Cortex-M 内核的 MCU 具有一个 用于中断管理的嵌套向量中断控制器(NVIC,Nested vectored interrupt controller)。

ARM Cortex-M 的 NVIC 最大可支持 256 个中断源,其中包括 16 个系统中断和 240 个外部中断。

临界段保护简介

临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作。

当产生系统调度、外部中断会打断临界段。在 FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。因此,FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制。

临界段代码

临界段代码也叫做临界区,是指必须完整运行,不能被打断的代码段, 比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。 FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护, 我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。

中断管理简介

ARM Cortex-M 系列内核的中断是由硬件管理的,而 FreeRTOS 是软件,它并不接管由硬件管理的相关中断(接管简单来说就是,所有的中断都由 RTOS 的 软件管理,硬件来了中断时,由软件决定是否响应,可以挂起中断,延迟响应或不响应),只支持简单的开关中断等,所以 FreeRTOS 中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且使能中断,编写中断服务函数,在中断服务函数中使用内核 IPC 通信机制,一般建议使用信号量、消息或事件标志组等标志事件的发生,将事件发布给处理任务,等退出中断后再由相关处理任务具体处理中断。

用户可以自定义配置系统可管理的最高中断优先级的宏定义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY,它是用于配置内核中的 basepri 寄存器的,当 basepri 设置为某个值的时候,NVIC 不会响应比该优先级低的中断,而优先级比之更高的中断则不受影响。就是说当这个宏定义配置为 5 的时候,中断优先级数值在 0、1、2、3、4 的这些中断是不受 FreeRTOS 屏蔽的,也就是说即使在系统进入临界段的时候这些中断也能被触发而不是等到退出临界段的时候才被触发,当然,这些中断服务函数中也不能调用 FreeRTOS 提供的 API 函数接口,而中断优先级在 5 到 15 的这些中断是可以被屏蔽的, 也能安全调用 FreeRTOS 提供的 API 函数接口。

注意

  • 任何中断的优先级都大于任务
  • 中断中必须使用相关中断函数
  • 中断运行时间越少越好: 中断中通常用来改变一些标志位,主要程序可以放在外部调用
  • 判断任务是否需要切换: 通常不会直接进行任务切换,而是进行一些相关标志位的改变;若直接切换的话就可能会有很多不可控的因素导致异常

常用API

// 任务切换函数:

portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );

portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
// 传入的参数是任务是否需要切换


// 开关中断: 优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断都会被关闭,高于的不会受任何影响。
portDISABLE_INTERRUPTS(); //关闭中断
portENABLE_INTERRUPTS(); //打开中断 


/**
* 任务中的中断屏蔽函数: 屏蔽中断
* @note 操作系统中的中断是大于任何的任务,同时中断又不确定什么时候会发生,就可能会导致中断将我们的任务进行打断,虽然它绝大多数时候都会跳转回来,但是这样就有可能会影响到他的时间精确性,或一些其他的意外,因此我们的操作系统中他就提供了屏蔽中断的一些函数供我们使用
*
*/
taskENTER_CRITICAL(); 
//优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断将会被屏蔽 
//优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断不受影响


// 恢复中断:
taskEXIT_CRITICAL(); //恢复中断使能


/**
* 中断中的中断屏蔽函数: 屏蔽中断
* @note 相对较低极的中断中,也可能会被更高级的中断所打断,就可能导致中断反应不及时,所以就需要使用中断中的屏蔽函数,将更高级的中断给屏蔽,防止干扰低级的中断
*
*/
UBaseType_t taskENTER_CRITICAL_FROM_ISR(); //返回当前状态

//恢复中断
taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedInterruptStatus ); //传入屏蔽时候的状态

e.g. 案例: 中断屏蔽

  • CubeMX配置

    • 说明

      • Tout = (ARR+1) * (PCS+1) / Tclk
      • 定时时间(us) = (计数周期+1) * (分频系数+1) / 输入时钟频率(MHz)
    • TIM3: Internal Clock -> 500ms (APR, 5000-1; PCS, 7200-1) -> NVIC Settings [enable]
    • TIM4: Internal Clock -> 1000ms -> NVIC Settings [enable]
    • NVIC

      • TIM3 -> Use FreeRTOS functions [Checked] -> 任意设置
      • TIM4 -> Use FreeRTOS functions [No check] -> 0-5 任意
  • 任务: 中断屏蔽测试
按键按下优先级数值 >= configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断将会被屏蔽,再次按下恢复。

程序代码

主程序逻辑 main.c

配置定时器中断回调函数及开启中断


/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  /* USER CODE BEGIN 2 */
  RetargetInit(&huart1); //重定向printf
  HAL_TIM_Base_Start_IT(&htim3); // 开启TIM3中断
  HAL_TIM_Base_Start_IT(&htim4);
  /* USER CODE END 2 */

  /* Call init function for freertos objects (in freertos.c) */
  MX_FREERTOS_Init();

  /* Start scheduler */
  osKernelStart();

  /* We should never get here as control is now taken by the scheduler */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    // this is Suroy's Project (https://suroy.cn)
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}


/**
  * @brief  Period elapsed callback in non blocking mode
  * @note   This function is called  when TIM2 interrupt took place, inside
  * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
  * a global variable "uwTick" used as application time base.
  * @param  htim : TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM2) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  if (htim->Instance == TIM3) {
    printf("TIM3\r\n");
  }

  if (htim->Instance == TIM4) {
    printf("TIM4\r\n");
  }
  /* USER CODE END Callback 1 */
}

FreeRTOS 主程序 freertos.c

配置按键控制Task

/**
* @brief Function implementing the KEY thread.
* @note Task3 -> 中断管理.屏蔽/恢复中断: V1.0.13
* 优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断将会被屏蔽
* @param argument: Not used
* @retval None
*/
void KeyTask(void const * argument)
{
  /* USER CODE BEGIN KeyTask */
    static BaseType_t xPress = pdFALSE;
  /* Infinite loop */
  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)
          {
              if(xPress == pdFALSE)
              {
                  taskENTER_CRITICAL(); //屏蔽中断
                 printf("TIM NVIC Stop!\r\n");
                 xPress = pdTRUE;
              }
              else{
                  taskEXIT_CRITICAL(); //允许中断
                 printf("TIM NVIC Start!\r\n");
                 xPress = pdFALSE;
              }

          }
          while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET); //检测松开
      }
    osDelay(1);
  }
}

附: 实验项目代码库

https://github.com/zsuroy/iFreeRTOS-demo

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

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


Grows towards sunlight and Carpe Diem