Skip to content

FreeRTOS

任务管理

在任何时刻,只有一个任务可以处于运行状态!

FreeRTOS 是一个抢占式的实时多任务系统,其任务调度器也是抢占式的。高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权,这样就保证了那些紧急任务的运行,我们就可以为那些对实时性要求高的任务设置一个很高的优先级。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务。

任务函数一般包含一个死循环:

void TaskFunction(void *pvParameters) {
    int iVariableExample = 0;  // 任务实例独属的变量
    for(; ; ) {
        /* 完成任务功能 */
    }

    vTaskDelete(NULL);  // 跳出死循环,删除任务(传入NULL默认删除当前任务)
}

创建任务

xTaskCreate()函数

// task.h
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                        const char * const pcName,
                        const configSTACK_DEPTH_TYPE usStackDepth,
                        void * const pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t * const pxCreatedTask ) PRIVILEGED_FUNCTION;
/*
 * @return pdTRUE  任务创建成功
           errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 内存堆空间不足,无法创建任务
 */
  • pvTaskCode:指向任务实现函数的指针(函数名)

任务通常是一个永不退出的C语言函数,用死循环实现。

  • pcName:(FreeRTOS不使用)用于辅助调试
  • usStackDepth:任务创建时,内核为它分配的栈空间大小,单位为字(word)

在32位微型处理器中,一个字由4字节组成。比如32位宽的栈空间,usStackDepth=100,则分配\(400B\)的栈空间。

应用程序在FreeRTOSConfig.h文件中定义常量configMINIMAL_STACK_SIZE决定空闲任务的栈空间大小。

  • pvParameters:传递到任务中的值
  • uxPriority:决定执行任务的优先级,从最低0到最高configMAX_PRIORITIES-1

configMAX_PRIORITIESFreeRTOSConfig.h中定义,值越大,内核消耗的内存空间越多。

  • 如果uxPriority大于configPRIORITIES,则自动封顶为最大合法值。

  • 如果多个任务优先级相同,调度器会让这些任务轮流执行,每个任务执行一个时间片,在时间片起始时刻进入运行态,在时间片结束时刻退出运行态;在FreeRTOSConfig.h文件中定义的configTICK_RATE_HZ表示tick的频率,时间片长度为1/configTICK_RATE_HZ

  • pxCreatedTask:传出任务的句柄,在API调用中对创建出的任务进行引用(比如改变优先级、删除任务)

void vTaskStartScheduler( void ) PRIVILEGED_FUNCTION;  // 启动任务调度器,任务开始执行

代码示例见FreeRTOS实时内核使用指南P10~20.

任务状态

rtos_task_state_machine

Full task state machine

/*
 * Blocking API function e.g.
 * 代替(忙等待)的空循环,让任务在延迟期间进入阻塞态,可以去调度其它任务
 * @param xTicksToDelay 延迟tick周期数 = 延迟时间 / portTICK_RATE_MS
 */
void vTaskDelay( const TickType_t xTicksToDelay ) PRIVILEGED_FUNCTION;

void xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                                const TickType_t xTimeIncrement )

代码示例见FreeRTOS实时内核使用指南P24, 28, 29~30.

vApplacationIdleHook函数

FreeRTOSConfig.h中的常量configUSE_IDLE_HOOK定义为1时,Hook函数才会被调用。

void vApplacationIdleHook(void);  // 函数名固定,无参数,无返回值

vTaskPrioritySet函数

用于在调度器启动后改变任务的优先级。

void vTaskPrioritySet(xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority);
  • pxTask:任务句柄,pxTask=NULL表示自身
  • uxNewPriority:目标优先级

vTaskPriorityGet函数

/*
 * @return 返回被查询任务的优先级,
 */
void vTaskPriorityGet(xTaskHandle pxTask);

vTaskDelete函数

/*
 * 内核为任务分配的内存空间会在删除后自动回收,任务1自己占用的内存或资源需要应用程序显式释放
 * @param pxTaskDelete 待删除任务句柄
 */
void vTaskDelete(xTaskHandle pxTaskToDelete);

代码示例见FreeRTOS实时内核使用指南P36~37, 39~40.

队列管理

创建队列

创建队列时,RTOS从堆空间中分配内存空间,如果堆中空间不足,函数返回NULL

/*
 * @param  uxQueueLength 队列深度
 * @param  uxItemSize    队列中数据单元的长度
 * @return NULL 堆空间不足;非NULL,返回队列句柄
 */
xQueueHandle xQueueCreate(unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize);

写入函数

/*
 * @param  xQueue        目标队列句柄,xQueueCreate函数的返回值
 * @param  pvItemToQueue 发送数据的指针(按uxItemSize复制对应长度)
 * @param  xTicksToWait  阻塞超时时间,队列满时,任务处于阻塞态等待队列空间有效的最长等待时间
 *                       xTicksToWait = time / portTICK_RATE_MS
 * @return pdPass        超时前数据成功入队
 *         errQUEUE_FULL 队列已满                
 */
portBASE_TYPE xQueueSendToFront(xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait);
portBASE_TYPE xQueueSendToBack(xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait);

读出函数

/*
 * 出队
 * @return pdPASS         读取成功
 *         errQUEUE_EMPTY 队列已空,读取失败  
 */
portBASE_TYPE xQueueReceive(xQueueHandle xQueue,       // 被读队列的句柄
                            const void * pvBuffer,     // 接收缓存指针(指向的内存空间等于数据单元大小)
                            portTickType xTicksToWait  // 阻塞超时时间
                           );
// 与出队不同,不删除被接收的数据单元
BaseType_t xQueuePeek(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);
  • 如果设定xTicksToWait=portMAX_DELAY,并且在文件FreeRTOSConfig.h中设定INCLUDE_vTaskSuspend=1,则阻塞等待无时间限制。

查询函数

/*
 * @param  xQueue 被查询队列的句柄
 * @return 返回队列中保存的数据单元的个数
 */
unsigned portBASE_TYPE uxQueueMessageWaiting(xQueueHandle xQueue);

  • taskYIELD();函数用于立刻进行任务切换,不必等到时间片耗尽。

例如,两个任务的优先级相同,一个任务调用了taskYIELD();函数,立刻由运行态进入就绪态,另一个任务进入运行态。


代码示例见FreeRTOS实时内核使用指南P54~65

中断管理

二值信号量

rtos_intr_binary_semaphore

Using a binary semaphore to synchronize a task with an interrupt

创建二值信号量

/*
 * @param xSemaphore 创建的信号量,直接传入值,而不是传址
 */
void vSemaphoreCreateBinary(xSemaphoreHandle xSemaphore);

xSemaphoreTake函数

/*
 * @param  xSemaphore   获取的信号量
 * @param  xTicksToWait 阻塞超时时间(任务进入阻塞态等待信号量有效的最长时间)
 *                      = time / portTICK_RATE_MS
 * @return pdPASS       成功获得信号量
 *         pdFALSE      未获得信号量  
 */
portBASE_TYPE xSemaphoreTake(xSemaphoreHandle xSemaphore, portTickType xTickToWait);

xSemaphoreGiveFromISR函数

除了互斥信号量外,其它类型的信号量可通过调用xSemaphoreGiveFromISR给出

/*
 * @param xSemaphore                给出的信号量
 * @param pxHigherPriorityTaskWoken 调用本函数会使一个任务退出阻塞态,如果该任务优先级高于先前被中断的任务,pxHigherPriorityTaskWoken=pdTRUE,在中断退出前进行一次上下文切换
 * @return pdPASS 调用成功
 *         pdFALL 信号量已经有效,无法重复给出
 */
portBASE_TYPE xSemaphoreGiveFromISR(xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken);

代码示例见FreeRTOS实时内核使用指南P76~79.

计数信号量

一个二值信号量最多只可以锁存一个中断事件(0/1),在锁存的事件未被处理之前,如果还有中断发生,后续的中断事件将会丢失。采用计数的方式代替二值信号量,丢中断的情况可以避免。

rtos_intr_count_semaphore

Using a counting semaphore to 'count' events
  • 事件计数:如图,每次事件发生时,中断服务例程都会Give一次信号量,计数值加1.延迟处理任务每处理一个任务都会Take一次信号量,计数值减1,。最终信号量的计数值等于已发生事件与已处理事件数目的差值

  • 资源管理:用信号量的计数值表示可用资源的数目,可以实现资源管理(详见下节)。

创建计数信号量

/*
 * @param uxMaxCount     最大计数值(信号量队列的深度)
 * @param uxInitialCount 初始计数值
 *                       - 事件计数:0  资源管理:uxMaxCount
 * @return NULL  堆空间不足,信号量分配内存失败
 *         !NULL 信号量创建成功,返回信号量句柄
 */
xSemaphoreHandle xSemaphoreCreateCounting(
    unsigned portBASE_TYPE uxMaxCount,
    unsigned portBASE_TYPE uxInitialCount
);

代码示例见FreeRTOS实时内核使用指南P85~86

中断服务例程中使用队列

信号量用于事件通信,队列不仅可以事件通信,还可以传递数据。

xQueueSendToFrontFromISR, xQueueSendToBackFromISR, xQueueReceiveFromISR是中断安全版本,用于中断服务例程中,函数在ISR中被调用时,不会等待队列空间可用或处理队列满的情况,因此不需要指定阻塞超时时间xTicksToWait;相反,它会立即返回一个状态码,指示是否成功发送消息或是否需要进行上下文切换。

/*
 * @param xQueue        目标队列句柄,xQueueCreate函数的返回值
 * @param pvItemToQueue 发送数据的指针(按uxItemSize复制对应长度)
 * @param pxHigherPriorityTaskWoken 调用本函数会使一个任务退出阻塞态,如果该任务优先级高于先前被中断的任务,pxHigherPriorityTaskWoken=pdTRUE,在中断退出前进行一次上下文切换
 * @return pdPass        超时前数据成功入队
 *         errQUEUE_FULL 队列已满                
 */
portBASE_TYPE xQueueSendToFrontFromISR(xQueueHandle xQueue, void * pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken);
portBASE_TYPE xQueueSendToBackFromISE(xQueueHandle xQueue, void * pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken);

/*
 * @return pdPASS         读取成功
 *         errQUEUE_EMPTY 队列已空,读取失败  
 */
portBASE_TYPE xQueueReceiveFromISR(
    xQueueHandle xQueue,       // 被读队列的句柄
    const void * pvBuffer,     // 接收缓存指针(指向的内存空间等于数据单元大小)
    portTickType xTicksToWait  // 阻塞超时时间
);

代码示例见FreeRTOS实时内核使用指南P89

资源管理

临界区与挂起

基本临界区

临界区会关掉中断(关掉优先级低于configMAX_INTERRUPT_PRIORITY的中断)在ENTEREXIT之间执行时不会切换到其它任务

taskENTER_CRITICAL();
/* Critical Sections/Regions */

taskEXIT_CRITICAL();

挂起

如果一个临界区太长,不适合简单地关中断实现,可以考虑使用挂起调度器。

挂起可以停止上下文切换而不用关中断,如果某个中断在调度器挂起过程中要求上下文切换,则这个请求也会被挂起,直到调度器被重新唤醒。

void vTaskSuspendAll(void);  // 挂起调度器

/*
 * @return 如果一个挂起的上下文切换请求在xTaskResumeAll()返回前执行,返回pdTRUE,否则返回pdFALSE
 */
portBASE_TYPE xTaskResumeAll(void);

互斥量

感觉类似于多线程编程中的互斥锁

互斥量是一种特殊的二值信号量,单词MUTEX来源与"MUTual EXclusion"

  • 用于互斥:用后必须归还;
  • 用于同步:完成同步后丢弃;

rtos_mutex

Mutual exclusion implemented using a mutex

xSemaphoreCreateMutex函数

xSemaphoreHandle xSemaphoreCreateMutex(void); // 创建成功则返回互斥量的句柄,否则返回NULL

代码示例见FreeRTOS实时内核使用指南P108~111.

守护任务

优先级反转

低优先级任务拥有互斥量的时候,优先级会被临时提高,提高到和高优先级任务相同的优先级。如果另一个高优先级任务也想获取这个信号量,必须要等待互斥量被释放,否则高优先级任务将不能获取这个互斥量,并且那个拥有互斥量的低优先级任务也永远不会被剥夺。

为了避免优先级反转和死锁,规定守护任务是对某个资源具有唯一所有权的任务。

只有守护任务才可以直接访问其守护的资源,并且其它任务要访问时只能间接通过守护任务提供服务。

内存管理

标准的malloc()函数和free()函数可能会有以下问题:

  • 在小型嵌入式系统中不可用;
  • 具体实现较大,占用代码空间;
  • 不具备线程安全特性;
  • 每次调用的时间开销可能不同;
  • 可能会产生内存碎片;
  • 使链接器配置复杂;

当内核请求内存时,调用pvPortMalloc()函数而不是malloc()函数;当释放内存时,调用vPortFree()函数而不是直接调用free()函数。

代码详见FreeRTOS\Source\portable\MemMang\heap_1,2,3.c,FreeRTOS实时内核使用指南P123~126.

参考资料