实时操作系统

FreeRTOS学习笔记



FreeRTOS 学习笔记

一、FreeRTOS是什么、有能干什么?

FreeRTOS是一款开源的实时操作系统,在实时操作系统层面上看,可以将每一个小问题看做是一个任务,这些任务在实时操作系统的调度下并行处理。实际上每个任务会执行很短的时间后,切换到下一个任务看起来好像所有的任务是在并行。

FreeRTOS任务运行方式

二、FreeRTOS特点、功能有哪些?

1.多任务系统:

在使用RTOS时,每件事情都是一个独立的任务,每个任务都有自己的运行环境(堆栈空间)。CPU在任意时间点只能运行其中的一个任务,RTOS会根据任务优先程度,快速切换运行所有的任务,这样看起来所有的任务就都好像是在并发运行。

2.任务状态:

FreeRTOS中的任务有运行态就绪态阻塞态挂起态,任一时刻任务只能处于以上4中状态中的一个。

1)运行态:当一个任务正在运行时(任务在前台运行),此时该任务就处于运行态。

2)就绪态:就绪态是指那些准备好的任务,可以拿走CPU使用权,从而进入运行态,但处于就绪态(运行态)的任务还没有执行,因为当前有同优先级的或更高优先级的任务正在运行。????

3)阻塞态:如果一个任务当前正在等在某一外部事件的发生所处的状态,如:任务正在等待队列、信号量、事件组等都会进入阻塞态。任务进入阻塞态是有一定时间限制的,当等待超时时,任务就退出阻塞态,进入就绪态,等待拿到CPU使用权再进入运行态。

4)挂起态:任务进入挂起状态和阻塞态一样,都不会被任务调度器调用。但是处于挂起态的任务是没有超时时间的。

FreeRTOS任务状态转移流程图

3.任务优先级:

FreeRTOS中的每一个任务都可以分配一个从0~(configMAX_PRIORITIES - 1)的优先级,configMAX_PRIORITIES在文件FreeRTOSConfig.h中有定义。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选 择 下 一 个 要 运 行 的 任 务 , Cortex-M 处 理 器 是 支 持 该 指 令 的 ) , 并 且 宏configUSE_PORT_OPTIMISED_TASK_SELECTION 也 设 置 为 了 1 , 那 么 宏configMAX_PRIORITIES 不能超过 32!也就是优先级不能超过 32 级。其他情况下宏configMAX_PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗,宏 configMAX_PRIORITIES最好设置为一个满足应用的最小值。
优先级的数字越低表示任务的优先级越低,0的任务优先级最低,configMAX_PRIORITIES-1的优先级最高。空闲任务的优先级最低,为0。

4.任务控制块:

任务控制块(Task Contrl Block简称TCB),任务控制块负责存储每一个任务中的必要属性和参数。在使用xTaskCreate()创建任务时会自动分配一个任务控制块。

三、FreeRTOS任务相关API函数

1)任务创建和删除API函数:
函数 描述
xTaskCreate() 使用动态方法创建任务
xTaskCreateStatic() 使用静态方法创建任务
xTaskCreateRegisterd() 创建一个使用MPU进行限制的任务,相关内存使用动态内存分配
xTaskDelete() 删除任务
2)任务挂起和恢复API函数:
函数 描述
vTaskSuspend() 挂起一个任务,使其不再执行
vTaskResume() 恢复一个任务,使其进入就绪态
vTaskResumeFromISR() 中断服务函数中恢复一个任务的运行

vTaskSuspend()函数将某一任务设置为挂起态,进入挂起态的任务永远都不会进入运行态,退出挂起态唯一的方法是调用任务恢复函数在vTaskResume()或在vTaskResumeFromISR()

vTaskResumeFromISR函数功能和vTaskResume()一样,不同的是在中断服务函数中只能调用该函数将某个任务从挂起态恢复到就绪态。

3)其他任务函数:
函数 描述
uxTaskPrionrityGet() 查询某任务的优先级
vTaskPrioritySet() 改变某个任务的优先级
uxTaskGetSystemState() 获取系统中某个任务的状态
vTaskGetInfo() 获取某个任务信息
xTaskGetApplicationTaskTag() 获取某个任务的标签(Tag)值
xTaskGetCurrentTaskHandle() 获取当前任务的句柄
xTaskIdleTaskHandle() 获取空闲任务的句柄
eTaskGetState() 获取某个任务的状态,此状态时eTaskState型
pcTaskGetName() 获取某个任务的任务名称
xTaskGetTickCount() 获取系统时间计数器值
xTaskGetSchedulerState() 获取任务调度器状态,开启或未开启
uxTaskGetNumberOfTasks() 获取当前系统中存在的任务数量
vTaskList() 以一种表格形式输出当前系统所有任务详细信息
xTaskGetRenTimeState() 获取每个任务运行时间
xTaskSetApplicationTaskTag() 设置任务标签值(Tag)
SetThreadLocalStoragePointer() 设置线程本地存储指针?
GetThreadLocalStoragePointer() 获取线程本地存储指针?
xTaskGetTickCountFromISR() 在中断服务函数中获取系统时间计数值
4)FreeRTOS常见内核控制函数:
函数 描述
taskYIELD() 任务切换
taskENTER_CRITICAL() 进入临界区,用于任务中
taskEXIT_CRITICAL() 退出临界区,用于任务中
taskENTER_CRITICAL_FROM_ISR() 进入临界区,用于中断服务函数
taskEXIT_CRITICAL_FROM_ISR() 退出临界区,用于中断服务函数
taskDISABLE_INTERRUPTS() 关闭中断
taskENABLE_INTERRUPTS() 打开中断
vTaskStartScheduler() 开启任务调度器
vTaskEndScheduler() 关闭任务调度器
vTaskSuspendAll() 挂起任务调度器
vTaskResumeAll() 恢复任务调度器
vTaskStepTick() 设置系统节拍数

四、FreeRTOS队列:

1)什么是队列,队列在FreeRTOS中起到什么作用?

队列可以在任务与任务、任务与中断之间传递消息,但是在队列中存储有限的、大小固定的数组项目。任务与任务、任务与中断之间交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度。我们在创建队列时需要制定数据项目的类型和队列的长度。由于队列时用于传递消息的,也成为消息队列。

2)队列主要函数:

分类 函数 描述
任务级入队函数 xQueueSend() 发送消息到队列尾部(后向入队)
xQueueSendToBack()
xQueueSendToFront() 发送消息到队列的头部(前向入队)
xQueueOverwrite() 发送消息到队列的尾部,带有覆盖功能,当队列满了将自动覆盖旧消息。
中断级入队函数 xQueueSendFromISR() 发送消息到队列尾部(后向入队)
xQueueSendToBackFromISR()
xQueueSendToFrontFromISR() 发送消息到队列的头部(前向入队)
xQueueOverwriteFromISR()
分类 函数 描述
任务级出队列函数 xQueueReceive() 从队列中读取队列项(消息),读完后将会删除该队列项
xQueuePeek() 从队列中读取队列项(消息),读完之后不删除该队列项
中断级出队函数 xQueueReceiveFromISR()
xQueuePeekFromISR()

3)队列运行流程:

4)队列使用示例程序:

/* 声明队列定义 */
// 队列句柄
QueueHandle_t xQueue;
// 申请队列 参数1队列深度 参数2队列项内容大小
xQueue = xQueueCreate( 10, sizeof(unsigned long));

void TaskA(void *pvParameters) {

    unsigned long pxMessage;
    if( xQueue != NULL ) {

        /* 作用:发送消息
         * 参数1:队列句柄
         * 参数2:队列内容指针
         * 参数3:允许阻塞时间
         */
        xQueueSend( xQueue, (void*)&pxMessage, (TickType_t)0 );
    }
}


void TaskB(void *pvParameters) {

    unsigned long pxMessage;
    /**
      * 作用:接收函数
      * 参数1:队列句柄
      * 参数2:队列内容返回保存指针
      * 参数3:允许阻塞时间
      */
    while(True) {

        if( xQueueReceive( xQueue, &(pxMessage), (TickType_t)10 )) {

            // ...
        }
    }
}

当要传递的内容比较大时,可以用传递指针的方法,提高运行效率。

struct aMessage {

    char ucMessageID;
    char ucData[20];
} xMessage;

QueueHandle_t xQueue;

void TaskA() {

    struct aMessage* pxMessage = NULL;
    // 创建队列,队列内容是指向结构体的指针
    xQueue = xQueueCreate( 10, sizeof(struct aMessage*));
    if( xQueue == NULL ) {

        // failed to create the queue.
    }

    pxMessage = &xMessage;
    // 发送消息,传递内容为消息体的指针。
    xQueueSend( xQueue, (void*)&pxMessage, (TickType_t)0 );
}

/**
  * 接收任务
  */
void TaskB() {

    struct aMessage *pxRxedMessage;
    while(Ture) {

        if( xQueue != NULL ) {

            if( xQueueReceive( xQueue, &(pxRxedMessage), (TickType_t)10 )) {

                // ...
            }
        }
    }
}

在中断中调用的接口。 因为 FreeRTOS 是一个实时操作系统, 为了保证中断发生时的实时响应, 做了优先级设置。 在中断中直接调用普通的系统接口函数可能导致阻塞其他中断, 为了避免这种情况, 提供了特定接口, 中断中调用系统的接口(FromISR后缀),会短期修改该中断的优先级,避免影响其他中断, 保证实时性。 同时, 在中断中调用的接口, 不会阻塞挂起。

五、FreeRTOS信号量:

1.什么是信号量,信号量作用是什么?

信号量主要用于对共享资源的访问和任务同步。

2.信号量分类:

1)二值信号量:

二值信号量通常用于互斥访问或同步,二值信号量与互斥信号量非常相似,二者主要区别在于:互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量适用于同步(任务与任务或任务与中断的同步),二互斥信号量适用于简单的互斥访问。

二值信号量与队列一样,创建信号量函数API允许设置一个阻塞的时间,如果多个任务同时阻塞在同一个信号量,那么优先级高的任务先获得信号量。通俗来说,二值信号量起始是只有一个队列项的队列,这个特殊的 队列要么是满要么是空。任务与中断无需知道该队列存储的数据,只需要知道该队列是否为空即可。

函数 描述
创建二值信号量 vSemaphoreCreateBinary() 动态创建二值信号量(旧版本使用)
xSemaphoreCreateBinary() 动态创建二值信号量(新版本使用)
xSemaphoreCreateBinaryStatic() 静态创建二值信号量
释放信号量 xSemaphoreGive() 任务级信号量释放函数
xSemaphoreFromISR)() 中断级信号量释放函数
获取信号量 xSemaphoreTake() 任务级获取信号量
xSemaphoreTakeFromISR() 中断级获取信号量

SemaphoreHandle_t xSemaphoreCreateBinary( void );返回值:NULL,二值信号量创建失败,其他值:二值信号量创建成功。

SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer);参数为指向StaticSemaphore_t类型的变量,用于保存信号量结构体;返回值:其他值:创建二值信号量的句柄。

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore);参数为要释放的二值信号量句柄。返回值:pdPASS:释放成功;errQUEUE_FULL:释放失败。

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );参数xSemaphore为获取的信号量句柄;xBlockTime为阻塞时间;返回值:pdTRUE:获取信号量成功,pdPASS超时,获取信号量失败。

BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);参数为信号量句柄;pxHigherPriorityTaskWoken标记退出该函数后是否需要进行任务切换。

2)计数型信号量:
3)互斥信号量:
4)递归互斥信号量:

六、消息通知

任务通知描述:

每一个RTOS任务都有一个32位的通知值,任务创建时,这个值被初始化为0。RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务能够解除堵塞状态,前提是这个堵塞事件是因等待通知而引起的。

发送通知的同一时候。也能够可选的改变接收任务的通知值。

  • 不覆盖接收任务的通知值
  • 覆盖接收任务的通知值
  • 设置接收任务通知值的某些位
  • 添加接收任务的通知值

相对于用前必须分别创建队列、二进制信号量、计数信号量或事件组的情况。使用任务通知显然更灵活。更好的是,相比于使用信号量解除任务堵塞。使用任务通知能够快45%、使用更少的RAM(使用GCC编译器,-o2优化级别)。

使用API函数xTaskNotify()xTaskNotifyGive()(中断保护等价函数为xTaskNotifyFromISR()vTaskNotifyGiveFromISR())发送通知,在接收RTOS任务调用API函数xTaskNotifyWait()ulTaskNotifyTake()之前。这个通知都被保持着。假设接收RTOS任务已经由于等待通知而进入堵塞状态。则接收到通知后任务解除堵塞并清除通知。

RTOS任务通知功能默认是使能的。能够通过在文件FreeRTOSConfig.h中设置宏configUSE_TASK_NOTIFICATIONS为0来禁止这个功能。禁止后每一个任务节省8字节内存。

尽管RTOS任务通知速度更快而且占用内存更少。但它也有一些限制:

  1. 仅仅能有一个任务接收通知事件。
  2. 接收通知的任务能够由于等待通知而进入堵塞状态,可是发送通知的任务即便不能马上完毕通知发送也不能进入堵塞状态。

发送通知:

发送通知函数 - 形式1:
BaseType_t xTaskNotify( TaskHandle_txTaskToNotify,
                         uint32_t ulValue,
                         eNotifyAction eAction);
参数描述:
  • xTaskToNotify:被通知的任务句柄。
  • ulValue:通知更新值
  • eAction:枚举类型,指明更新通知值的方法

枚举变量成员以及作用例如以下表所看到的。

枚举成员 描述
eNoAction 发送通知但不更新通知值,这以为这参数ulValue未使用。
eSetBits 被通知任务的通知值按位或上ulValue。使用这种方法可以默写场景下代替事件组,但执行速度更快。
eIncrement 被通知任务的通知值增加1,这种情况下,参数ulValue未使用,API函数xTaskNotifyGive()和这个等价。
eSetValueWithOverwrite 被通知任务的通知值设为ulValue。使用这种方法,可以在默写场景下代替xQueueOverwrite(),但执行速度更快。
eSetValueWithoutOverwrite 如果被通知任务当前没有通知,则被通知任务的通知值设为ulValue
如果被通知任务还没有取走上一个通知,又接受到了一个通知,则这次通知值丢弃,在这种情况下,函数xTaskNotify()调用失败并返回pdFALSE
使用这种方法可以在默写场景下替换长度为1的xQueueSend(),但速度更快。

返回值:

參数eActioneSetValueWithoutOverwrite时,假设被通知任务还没取走上一个通知。又接收到了一个通知,则这次通知值未能更新并返回pdFALSE,否则返回pdPASS

发送通知函数 - 形式2:
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify );

事实上这是一个宏。本质上相当于xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement )。能够使用该API函数取代二进制或计数信号量,但速度更快。在这样的情况下。应该使用API函数ulTaskNotifyTake()来等待通知。而不应该使用API函数xTaskNotifyWait()

此函数不能够在中断服务例程中调用,中断保护等价函数为vTaskNotifyGiveFromISR()

用法举例:

static void prvTask1( void *pvParameters );
static void prvTask2( void *pvParameters );

/* 保存任务句柄 */
static TaskHandle_t xTask1 = NULL, xTask2 = NULL;

/* 创建两个任务,它们之间重复发送通知,启动RTOS调度器*/
void main( void )
{
    xTaskCreate( prvTask1, "Task1",200, NULL, tskIDLE_PRIORITY, &xTask1 );
    xTaskCreate( prvTask2, "Task2",200, NULL, tskIDLE_PRIORITY, &xTask2 );
    vTaskStartScheduler();
}
/*-----------------------------------------------------------*/

static void prvTask1( void *pvParameters )
{
    for( ;; )
    {
        /*向prvTask2(),发送通知,使其解除堵塞状态 */
        xTaskNotifyGive( xTask2 );

        /* 等待prvTask2()的通知,进入堵塞 */
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY);
    }
}
/*-----------------------------------------------------------*/

static void prvTask2( void *pvParameters )
{
    for( ;; )
    {
        /* 等待prvTask1()的通知。进入堵塞 */
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY);

        /*向prvTask1(),发送通知,使其解除堵塞状态 */
        xTaskNotifyGive( xTask1 );
    }
}

获取通知:

函数原型:
uint32_t ulTaskNotifyTake( BaseType_txClearCountOnExit,
                           TickType_txTicksToWait );

ulTaskNotifyTake()是专门为使用更轻量级更快的方法来取代二进制或计数信号量而量身打造的。

FreeRTOS获取信号量的API函数为xSemaphoreTake(),能够使用ulTaskNotifyTake()函数等价取代。

当一个任务使用通知值来实现二进制或计数信号量时,其他任务或者中断要使用API函数xTaskNotifyGive()或者使用參数eActioneIncrement的API函数xTaskNotify()。推荐使用xTaskNotifyGive()函数(事实上是个宏,我们这里把它看作一个API函数)。另外须要注意的是,假设在中断中使用。要使用它们的中断保护等价函数:vTaskNotifyGiveFromISR()xTaskNotifyFromISR()

API函数xTaskNotifyTake()有两种方法处理任务的通知值。一种方法是在函数退出时将通知值清零,这样的方法适用于实现二进制信号量;第二种方法是在函数退出时将通知值减1。这样的方法适用于实现计数信号量。

假设RTOS任务的通知值为0,使用xTaskNotifyTake()能够可选的使任务进入堵塞状态,直到该任务的通知值不为0。进入堵塞的任务不消耗CPU时间。

参数说明:
  • xClearCountOnExit:假设该參数设置为pdFALSE,则API函数xTaskNotifyTake()退出前,将任务的通知值减1;假设该參数设置为pdTRUE。则API函数xTaskNotifyTake()退出前,将任务通知值清零。
  • xTicksToWait:因等待通知而进入堵塞状态的最大时间。时间单位为系统节拍周期。
返回值:

返回任务的当前通知值。为0或者为调用API函数xTaskNotifyTake()之前的通知值减1。

/* 中断处理程序。*/
void vANInterruptHandler( void )
{
    BaseType_txHigherPriorityTaskWoken;

    prvClearInterruptSource();

    /* xHigherPriorityTaskWoken必须被初始化为pdFALSE。假设调用vTaskNotifyGiveFromISR()会解除vHandlingTask任务的堵塞状态,而且vHandlingTask任务的优先级高于当前处于执行状态的任务,则xHigherPriorityTaskWoken将会自己主动被设置为pdTRUE。*/
    xHigherPriorityTaskWoken = pdFALSE;

    /*向一个任务发送通知,xHandlingTask是该任务的句柄。*/
    vTaskNotifyGiveFromISR( xHandlingTask,&xHigherPriorityTaskWoken );

    /* 假设xHigherPriorityTaskWoken为pdTRUE。则强制上下文切换。这个宏的实现取决于移植层。可能会调用portEND_SWITCHING_ISR */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken );
}
/*---------------------------------------------------------------------------------------------------*/

/* 一个由于等待通知而堵塞的任务。 */
voidvHandlingTask( void *pvParameters )
{
    BaseType_txEvent;

    for( ;; )
    {
        /*等待通知。无限期堵塞。參数pdTRUE表示函数退出前会清零通知值。这显然是用于替代二进制信号量的使用方法。须要注意的是,真实的程序一般不会无限期堵塞。*/
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY);

        /* 当处理全然部事件后,仍然等待下一个通知*/
        do
        {
            xEvent = xQueryPeripheral();

            if( xEvent != NO_MORE_EVENTS )
            {
                vProcessPeripheralEvent( xEvent);
            }

        } while( xEvent != NO_MORE_EVENTS );
    }
}

七、FreeRTOS事件标志:

八、系统裁剪:

九、参考文献:

回复

This is just a placeholder img.