探索FreeRTOS的功能:线程,消息队列,邮箱,信号量,互斥量,任务通知,延时,虚拟定时器

简介: 探索FreeRTOS的功能:线程,消息队列,邮箱,信号量,互斥量,任务通知,延时,虚拟定时器

探索FreeRTOS的功能:线程,消息队列,邮箱,信号量,互斥量,任务通知,延时,虚拟定时器

使用的是由ST公司封装的CMSIS V1的API。开发环境是STM32CUBEIDE V1.7.0,STM32F4 Package 1.26.2。记录下一些开发时候用到的功能和用法~

线程

操作系统与裸机的最大区别就是线程啦

  • 线程的定义创建和初始化均可以在STM32CubeMX中完成。在CUBEMX-FREERTOS-Tasks and Queues来创建。选择Tasks下的Add即可完成创建。

    消息队列Queues在CubeMX里面也可以创建但是灵活度不够高最好按需求手动创建,后文会提到。

  • 在Code Generation Option选择As Weak会创建弱函数,可以很方便的用自己在其他位置写的函数来直接覆盖这个弱符号函数。
  • 创建后的线程会自动在freertos.cMX_FREERTOS_Init()函数完成初始化。

    osThreadId testHandle;                    // 定义线程的ID,用于对线程的各种操作(如修改优先级,中止/开始线程等
    void TestTask(void const * argument);    // 线程对应的函数体的声明
    
    osThreadDef(test, TestTask, osPriorityBelowNormal, 0, 256);    // 线程定义,参数分别为:线程的名称,线程函数体,线程优先级,线程实例化个数,线程分配的栈空间
    testHandle = osThreadCreate(osThread(test), NULL);            // 创建线程,并赋值给对应的线程ID
    
    // 线程的具体实现
    __weak void TestTask(void const * argument)
    {
      /* USER CODE BEGIN TestTask */
      /* Infinite loop */
      for(;;)
      {
        osDelay(1);
      }
      /* USER CODE END TestTask */
    }
    AI 代码解读
  • CubeMX在配置FreeRTOS时默认使用与HAL库相同的SysTick滴答定时器。为了避免时钟线混乱冲突,需要在System Core-SYS-Timebase Source选择一个其他的定时器。一定注意要切换时钟源!

    STM32的TIM分为高级定时器、通用定时器和基本定时器。其中基本定时器的功能最简单,只有定时的功能,一般用作时钟基源;通用定时器在基本的定时功能的基础上多出了输出比较和输入捕获功能。输出比较可以输出周期性的方波(比如PWM波和PPM波),输入捕获可以读取输入信号的高电平和低电平的时间进而可以计算出信号的周期和占空比;高级定时器除了上述功能之外,还有还包含无互补信号输出以及带刹车(断路)功能等电机控制(日常用不太到的)高级功能

  • 修改Config Parameters-MINIMAL_STACK_SIZE 为256。默认的128堆栈可能会不太够用
  • 每一个线程都有四种状态:挂起、阻塞、就绪和运行状态,每一种状态的特点从它的命名就可以猜出来。任务调度器在每一次切换任务的时候都会检查有没有优先级更高的线程处于就绪(ready)状态,如果有,则暂停当前执行的线程,转而执行优先级更高的线程。另外在操作系统中线程的优先级可以是一样的,当两个线程的优先级是一样时,任务调度器会不断在这两个线程间来回切换,近似相当于两个线程同步执行

消息队列

线程往往不是相互独立的,需要不同的线程之间进行通信。在FreeRTOS中线程的通信可以使用信号量,互斥量,队列,邮箱,任务通知进行通信。

信号、信号量、互斥量用于进程之间的触发,但对进程间的数据交换无能为力。进程间数据交换最简单的方式是全局变量,但即使在简单的系统中,把握和灵活应用全局变量也是不小的挑战,因为全局变量会引起一系列不可预知错误。

在RTOS中,消息队列和邮箱队列是进程间数据交互最为有效、安全的方式。

消息队列和邮箱队列的工作方式基本一样,唯一的区别是消息队列中传输的是待交换数据,而邮箱队列中传输是指向待交换数据的指针。

队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同 一队列写入和读出。

消息队列的创建

QueueHandle_t messageQueue[QUEUE_NUM] = {NULL};    // 声明消息队列句柄
bool_t messageQueueCreateFlag = false;            // 消息队列创建完成标志位

enum MessageQueue_e {
    // 0-(MOTOR_NUM-1)给电机
    IMUANGLE = 4,
    QUEUE_NUM
};

/**
  * @brief    消息队列创建
  */
void MessageQueueCreate(void)
{
    uint8_t i = 0;
    // 电机消息队列
    for (i = 0; i < IMUANGLE; i++) {
        messageQueue[i] =  xQueueCreate(1, sizeof(motor_measure_t *));    // 创建FIFO的长度为1 指向motor_measure_t结构的指针的队列
    }
    // IMU消息队列
    messageQueue[IMUANGLE] =  xQueueCreate(1, 3 * sizeof(float));

    // 校验是否创建失败
    for (i = 0; i < QUEUE_NUM; i++) {
        if (messageQueue[i] == NULL) {
            Error_Handler();
        }
    }
    messageQueueCreateFlag = true;
}
AI 代码解读
  • 创建队列API函数是xQueueCreate(),但其实这是一个宏。真正被执行的函数是xQueueGenericCreate()
  • 程序中演示了给存放电机数据的结构体指针,以及存放欧拉角的数组angle[3]创建消息队列
  • 自己写的这个MessageQueueCreate()函数需添加至MX_FREERTOS_Init中初始化。
  • 建议设置一个消息队列创建成功的标志位。这样可以避免中断在消息队列完成创建前写入数据导致程序卡死。
  • 消息队列创建失败可能是由于 heap 堆栈空间不够

消息发送

  • xQueueSend

    向队列尾部发送一个队列消息。等价于xQueueSendToBack()该函数不能在中断服务程序里面被调用,中断中必须使用带有中断安全保护功能的函数xQueueSendFromISR

    所有的xQueueSend都是一个宏,实际执行函数为xQueueGenericSend()。后缀为FromISR的实际执行函数为xQueueGenericSendFromISR()

    BaseType_t xQueueGenericSend ( 
        QueueHandle_t xQueue,                // 队列句柄
        const void * const pvItemToQueue,    // 指针,指向要入队的项目
        TickType_t xTicksToWait,            // 如果队列满,等待队列空闲的最大时间
        const BaseType_t xCopyPosition        // 入队位置。可以选择从队列尾入队,从队列首入队和覆盖式入队
    )
    AI 代码解读
  • xQueueSendFromISR

    xQueueSend()的中断保护版本,用于在中断服务程序中向队列尾部发送一个队列消息,等价于 xQueueSendToBackFromISR()

  • xQueueSendToFront

    向队列队首发送一个消息。中断版本为 xQueueSendToFrontFromISR ()

  • xQueueOverwrite

    向队列尾部发送一个队列消息。中断版本为xQueueSendOverwriteFromISR ()

  • uxQueueMessagesWaiting

    返回队列中当前有效数据单元个数。中断版本为uxQueueMessagesWaitingFromISR()

  • xQueueIsQueueEmptyFromISR & xQueueIsQueueFullFromISR

    查询队列是否为空/满,只能在中断中使用。返回pdFALSEpdTRUE

  • 遇到问题可以参考FreeRTOS消息官方API文档 https://www.freertos.org/a00018.html

消息接收

接收 API 和发送 API 差不多, 也是实现了几个宏, 但是实际实现的函数是xQueueGenericReceivexQueueGenericReceiveFromISR这两个。

  • xQueueReceive

    用于从一个队列中接收消息并把消息从队列中删除。读取后会把消息从队列中删除。同样在中断服务程序里面须使用带有中断保护功能的xQueueReceiveFromISR() 来代替。

  • xQueueReceiveFromISR

    xQueueReceive () 的中断版本。

  • xQueuePeek

    从队列首接收到数据后,并不从队列中删出接收到的单元,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。中断版本为xQueuePeekFromISR ()

// 某中断回调函数
void ISR(void)
{
    static BaseType_t xHigherPriorityTaskWoken = pdFALSE;    // 不请求上下文切换
    // 中断处理...
    xQueueOverwriteFromISR(messageQueue, (void *)&data, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);            // 判断是否请求上下文切换
}
AI 代码解读

不是很建议在中断中接收消息队列,中断执行往往越快越好。

// 任务中向消息队列复写数据
xQueueOverwrite(messageQueue, (void *)&data);
// 任务中取出消息队列数据 等待时间为1ms
xQueueReceive(messageQueue, &useData, (1 / portTICK_RATE_MS));
AI 代码解读

延时

  • osDelay()为CMSIS-RTOS层。内部其实使用vTaskDelay来实现。在程序执行到这条语句后,当前任务阻塞(不是挂起),任务调度器转而判断其他哪个线程得以执行,当时间到了之后线程变为就绪状态,等待任务调度器调用。

    相比于HAL_Delay,HAL会一直不停的调用获取系统时间的函数,直到指定的时间流逝然后退出,故其占用了全部CPU时间。

  • vTaskDelay()为相对延时。任务每次延时都是从调用延时函数vTaskDelay()开始算起的,延时是相对于这一时刻开始的,所以叫做相对延时函数。如果执行任务的过程中发生中断,那么任务A执行的周期就会变长,周期也会改变,延时效果不是很精确。
  • vTaskDelayUntil()为绝对延时。绝对延时能够提供精度更高的定时效果。

    延时的时间单位为系统节拍时钟周期。如果1节拍不是1ms或是想要规范标准化代码可以使用5 / portTICK_RATE_MSpdMS_TO_TICKS(5UL)

    如果是需要周期运行的任务程序(比如电机PID控制),最好用绝对延时替换相对延时保证任务运行精确

void TestTask(void const * argument)
{    
    portTickType xLastWakeTime;
    const portTickType xFrequency = pdMS_TO_TICKS(5UL);        // 绝对延时5ms
    xLastWakeTime = xTaskGetTickCount();                    // 用当前tick时间初始化 pxPreviousWakeTime

    while(1)
    {
        // 任务绝对延时
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
        // 任务内容...
    }
}
AI 代码解读

信号量

  • FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(互斥量)和递归互斥信号量(递归互斥量)。
  • 互斥量和信号量使用相同的 API 函数,都直接或间接调用通用队列创建函数xQueueGenericCreate()来实现。
  • 互斥量和信号量在用法上不同。

    • 互斥量和递归互斥量可以看成特殊的信号量。
    • 信号量用于任务间同步或者任务和中断间同步;互斥量用于互锁,用于保护同时只能有一个任务访问的资源,为资源上一把锁。
    • 信号量用于同步时,一般是一个任务(或中断)给出信号,另一个任务获取信号;互斥量必须在同一个任务中获取信号,同一个任务给出信号。互斥量不能用在中断服务程序中,信号量可以。
    • 互斥量具有优先级继承,信号量没有。

二进制信号量

  • 二进制信号量既可以用于互斥功能也可以用于同步功能。与互斥量的区别在于不包含优先级继承机制。
  • 二进制信号量实际上是创建了一个队列,队列项有1个,但是队列项的大小为0
  • 创建二进制信号量API为xSemaphoreCreateBinary()

计数信号量

  • 计数信号量则可以被认为长度大于1的队列。不必关心存储在队列中的数据,只需关心队列是否为空。
  • 创建计数信号量API为 xSemaphoreCreateCounting()

    两个参数分别为:最大计数值(当信号到达这个值后就不再增长了);创建信号量时的初始值

  • 获取信号量API为xSemaphoreTake.带中断保护版本为xSemaphoreTakeFromISR()
  • 信号量和互斥量(除递归互斥量外)释放的API接口函数都是相同的xSemaphoreGive()(不带中断保护)xSemaphoreGiveFromISR()为带保护版本。这个宏真正调用的函数是xQueueGenericSend()
SemaphoreHandle_t xSemaphore = NULL;        // 信号量句柄

void TestTask(void const * argument)
{
    xSemaphore = xSemaphoreCreateBinary();    // 创建二进制信号量
    if (xSemaphore == NULL) {
        Error_Handler();
    }
    
    while(1)
    {
        // 等待信号量 阻塞时间设置为最多为10ms
        // 也可以设置成等待到永远让任务一直等待 portMAX_DELAY / osWaitForever
        if (xSemaphoreTake(xSemaphore, (TickType_t)(10 / portTICK_RATE_MS)) == pdTRUE) {
             // 任务内容...
        } else {
            
        }
        // 也可以换成用whlie的死循环写法
        while (xSemaphoreTake(xSemaphore, (TickType_t)(10 / portTICK_RATE_MS)) != pdPASS)
        {
        }
    }
}

// 任务中发送信号量
osSemaphoreRelease(xSemaphore);    
// 任务中释放信号量
xSemaphoreGive(xSemaphore);    
// 中断中释放信号量
static BaseType_t xHigherPriorityTaskWoken = pdFALSE;    // 不请求上下文切换
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);            // 判断是否请求上下文切换
AI 代码解读
  • 也可以设置成任务等待信号量到永远portMAX_DELAY / osWaitForever

互斥量

  • 互斥信号量(互斥量)和递归互斥信号量(递归互斥量)
  • 用于互锁的互斥量可以充当保护资源的令牌。当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问同一资源。
  • 如果一个互斥量(令牌)正在被一个低优先级任务使用,此时一个高优先级企图获取这个互斥量,高优先级任务会因为得不到互斥量而进入阻塞状态,正在使用互斥量的低优先级任务会临时将自己的优先级提升,提升后的优先级与与进入阻塞状态的高优先级任务相同。

    这个优先级提升的过程叫做优先级继承。这个机制用于确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”影响降低到最小。

    不过优先级继承不能解决优先级反转,只能将这种情况的影响降低到最小。硬实时系统在一开始设计时就要避免优先级反转发生。

  • 互斥量不可以用在中断服务程序中。因为互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义。并且中断不能因为等待互斥量而阻塞。
  • 创建互斥量API为xSemaphoreCreateMutex()

    递归互斥量还没有完全弄明白就先不写了(逃

    更多可以参考FreeRTOS信号量官方API文档 https://www.freertos.org/a00113.html

任务通知

任务通知是在FreeRTOS版本V8.2.0中推出了全新的功能。在大多数情况下,任务通知可以替代二进制信号量、计数信号量、事件组,可以替代长度为1的队列(可以保存一个32位整数或指针值)。并且任务通知速度更快、使用的RAM更少。不过任务通知并不能完全代替信号量。比如一个任务只能阻塞到一个通知上,如想要实现多个任务阻塞到同一个事件上,只能使用信号量了。

发送通知

  • xTaskNotifyGive()

    发送通知(无通知值)。实际调用函数为xTaskGenericNotify。中断保护版本为vTaskNotifyGiveFromISR

    BaseType_t xTaskGenericNotify( 
            TaskHandle_t xTaskToNotify,                // 被通知的任务句柄
            uint32_t ulValue,                        // 更新的通知值
            eNotifyAction eAction,                    // 枚举类型,指明更新通知值的方法
            uint32_t *pulPreviousNotificationValue )// 回传未被更新的任务通知值。如果不需要回传未被更新的任务通知值,这里设置为NULL。
    AI 代码解读
  • xTaskNotify()

    发送通知。中断保护版本为xTaskNotifyFromISR

  • xTaskNotifyAndQuery()

    发送通知并查询当前通知值。中断保护版本为xTaskNotifyAndQueryFromISR

等待通知

等待通知API函数只能用在任务中,没有带中断保护版本。

  • ulTaskNotifyTake()

    用于实现轻量级的二进制信号量和计数信号量。和发送通知API函数xTaskNotifyGive(FromISR)配合使用

    如果第一个参数xClearCountOnExit设置为pdFALSE,则用来实现二进制信号量,函数退出时将通知值清零;如果第一个参数设置为pdTRUE,则用来实现计数信号量,函数退出时,将通知值减一

  • xTaskNotifyWait ()

    全功能版的等待通知。

    TaskHandle_t task_local_handler = NULL;
    
    void Task(void const * argument)
    {
        task_local_handler = xTaskGetHandle(pcTaskGetName(NULL));        // 获取当前任务的任务句柄
        
        while(1)
        {
            while (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) != pdPASS);    // 等待通知
        }
    }
    
    void xxxISR()
    {
        static BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        vTaskNotifyGiveFromISR(task_local_handler, &xHigherPriorityTaskWoken);    // 发送通知
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
    AI 代码解读

虚拟定时器

功能相当于基本定时器,能实现毫秒级的定时执行。

虚拟定时器的回调函数和线程不一样,它不能有死循环。

osTimerId superviseTimerHandle;            // 定义虚拟定时器的ID
osTimerDef(superviseTimer, supervise);    // 定义一个虚拟定时器,指定了定时器的回调函数是supervise()
superviseTimerHandle = osTimerCreate(osTimer(superviseTimer), osTimerPeriodic, NULL);    // 创建一个虚拟定时器实例,并指定了定时器模式为osTimerPeriodic模式(连续模式,还有一种模式是只执行一次的osTimerOnce)
osTimerStart(superviseTimerHandle, (5 / portTICK_RATE_MS));    // 启动虚拟定时器,配置定时器5毫秒执行一次

/* 虚拟定时器的回调函数 */
void supervise(void const * argument)
{
    /* USER CODE BEGIN supervise */
    // 内容...
    /* USER CODE END supervise */
}
AI 代码解读

这篇记录一下接触FreeRTOS的一点学习笔记。完整的工程可以看下刚开源的飞机云台程序。


参考资料

FreeRTOS官方API文档 https://www.freertos.org/a00106.html

[野火]FreeRTOS 内核实现与应用开发实战—基于STM32 https://doc.embedfire.com/rtos/freertos/zh/latest/index.html

掌握 FreeRTOS™ 实时内核 https://freertoskernel.asicfans.com/

目录
打赏
0
0
0
0
3
分享
相关文章
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
32 0
如何处理线程池关闭时未完成的任务?
总之,处理线程池关闭时未完成的任务需要综合考虑多种因素,并根据实际情况选择合适的处理方式。通过合理的处理,可以最大程度地减少任务丢失和数据不一致等问题,确保系统的稳定运行和业务的顺利开展。
196 64
线程池关闭时未完成的任务如何保证数据的一致性?
保证线程池关闭时未完成任务的数据一致性需要综合运用多种方法和机制。通过备份与恢复、事务管理、任务状态记录与恢复、数据同步与协调、错误处理与补偿、监控与预警等手段的结合,以及结合具体业务场景进行分析和制定策略,能够最大程度地确保数据的一致性,保障系统的稳定运行和业务的顺利开展。同时,不断地优化和改进这些方法和机制,也是提高系统性能和可靠性的重要途径。
159 62
|
2月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
132 17
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
100 12
|
5月前
|
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
这篇文章介绍了Spring Boot 3中GraalVM Native Image Support的新特性,提供了将Spring Boot Web项目转换为可执行文件的步骤,并探讨了虚拟线程在Spring Boot中的使用,包括如何配置和启动虚拟线程支持。
324 9
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
如何用java的虚拟线程连接数据库
本文介绍了如何使用Java虚拟线程连接数据库,包括设置JDK版本、创建虚拟线程的方法和使用虚拟线程连接MySQL数据库的示例代码。
113 6
如何用java的虚拟线程连接数据库
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
150 5
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
210 23
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
95 2