内容参考并引用了野火FreeRTOS内核实现与应用开发实战指南

一、FreeRTOS的移植

其实任何开源代码的移植都不外乎总结成三个步骤:

1.整理要移植的代码;(要移植的源文件)

2.将要移植代码需要的硬件资源或软件接口进行绑定;

3.在工程中包含移植代码的h文件和c文件;

FreeRTOS也是一样分为这三步:

1.整理要移植的文件;源文件、头文件、接口文件、

具体移植移步至我的FreeRTOS移植教程。


二、任务的创建

任务的三要素分别是:1.任务函数主体 .任务栈;3.任务控制块;

  1. 任务函数主体:其实就是任务的主要逻辑,执行的函数。
  2. 任务栈:在不同的任务中切换的时候,需要保存自己程序运行的上下文。
  3. 任务控制块:其实是用于系统与任务的链接结构,存储着任务的属性信息。系统通过任务控制块的信息来控制调用任务函数和栈(在工程中,任务控制块会最终赋值给任务句柄)。

于是任务的创建也就是完成任务要素的创建,将任务要素联系到任务控制块中,再将任务控制块与系统联系。其实我们操作的步骤并不会复杂,这些操作均有系统函数接口已经实现了。但是静态内存和动态内存会有些许区别。


在freertos中任务的创建分为两个类型:1.静态内存;2.动态内存;

其本质的区别在于使用的缓存区变量存储的地址不一致。

动态内存的任务创建使用了堆,而静态内存的任务创建则是在类似于全局变量在静态数据区。

一个是在运行的过程中创建,另一个则是在程序开始前则创建好了。

1.静态内存的任务创建

静态内存的任务的数据变量空间是存放在静态数据区的,于是我们得自己像创建全局变量一样自己编写。这一点非常重要,因为操作系统本身也有其基础类似定时器任务和空任务的存在。只要是任务都需要任务三元素。于是在选择静态内存任务创建的同时。我们也要为操作系统来创建系统任务的三要素并将其与系统联系在一起(任务函数主体不需要,因为操作系统已经编写)。

 /* 空闲任务任务堆栈 */ 
 static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE]; 
 /* 定时器任务堆栈 */ 
 static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH]; 
 
 /* 空闲任务控制块 */ 
 static StaticTask_t Idle_Task_TCB; 
 /* 定时器任务控制块 */ 
 static StaticTask_t Timer_Task_TCB;

/**
 *******************************************************************
 * @brief 获取空闲任务的任务堆栈和任务控制块内存
 * ppxTimerTaskTCBBuffer : 任务控制块内存
 * ppxTimerTaskStackBuffer : 任务堆栈内存
 * pulTimerTaskStackSize : 任务堆栈大小
 * @author fire
 * @version V1.0
 * @date 2018-xx-xx
 **********************************************************************
 */
 void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, 
 StackType_t **ppxIdleTaskStackBuffer, 
 uint32_t *pulIdleTaskStackSize) 
 { 
 *ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */ 
 *ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
 *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */ 
 } 
 
 /**
 *********************************************************************
 * @brief 获取定时器任务的任务堆栈和任务控制块内存
 * ppxTimerTaskTCBBuffer : 任务控制块内存
 * ppxTimerTaskStackBuffer : 任务堆栈内存
 * pulTimerTaskStackSize : 任务堆栈大小
 * @author fire
 * @version V1.0
 * @date 2018-xx-xx
 **********************************************************************
 */
 void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, 
 StackType_t **ppxTimerTaskStackBuffer, 
 uint32_t *pulTimerTaskStackSize) 
 { 
 *ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */ 
 *ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */ 
 *pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */ 
 }

完成了系统任务的布置,这样操作系统才能正常运行。接下去就是用户任务的创建了:

/* LED 任务句柄 */
static TaskHandle_t LED_Task_Handle;

/* LED 任务堆栈 */
static StackType_t LED_Task_Stack[128];

/* AppTaskCreate 任务控制块 */
static StaticTask_t LED_Task_TCB;

static void LED_Task (void* parameter)
{
    while(1)
    {
    }
}

LED_Task_Handle = xTaskCreateStatic((TaskFunction_t )LED_Task, //任务函数
                                     (const char*)"LED_Task",//任务名称
                                     (uint32_t)128, //任务堆栈大小
                                     (void* )NULL, //传递给任务函数的参数
                                     (UBaseType_t)4, //任务优先级
                                     (StackType_t*)LED_Task_Stack,//任务堆栈
                                     (StaticTask_t*)&LED_Task_TCB);//任务控制块

这里的代码能看到,最后的函数是将三要素联系在一起的功能接口。但是我们创建的变量中多了一个任务句柄。那为什么会多一个任务句柄的变量呢?因为FreeRTOS不想将真实的任务控制块暴露给用户,这涉及到任务运行内核,任意修改会导致系统出现问题。于是将任务控制块再次"封装",以任务句柄的方式开发给用户。

2.动态内存的任务创建

动态内存的任务创建就不需要我们来完成系统任务的初始化了。系统会自动完成这些三要素的创建以及联系。同样的对于我们创建用户任务也是,调用动态任务创建的接口,系统也会帮我们完成这个过程,当然任务函数主体还是需要用户自己完成的。

/* LED 任务句柄 */
static TaskHandle_t LED_Task_Handle;

static void LED_Task (void* parameter)
{
    while(1)
    {
    }
}

/* 创建 AppTaskCreate 任务 */
 xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */(1)
                        (const char* )"AppTaskCreate",/* 任务名字 */(2)
                        (uint16_t )512, /* 任务栈大小 */ (3)
                        (void* )NULL,/* 任务入口函数参数 */ (4) 
                        (UBaseType_t )1, /* 任务的优先级 */ (5)
                        (TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */(6)

三、任务管理

任务状态通常分为以下四种:

就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等 待调度器进行调度,新创建的任务会初始化为就绪态。

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

阻塞(Blocked):如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。

挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而把一个挂起状态的FreeRTOS内核任务恢复的唯一途径 就是调用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到 我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

任务的设计要点

1.中断服务函数:中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发,然后通知任务,让对应任务去执行相关处理。

2.用户任务:如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行。所以对于每个任务适量添加一定的堵塞。

3.空闲函数:空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。 因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。

对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:
  •  永远不会挂起空闲任务;
  •  不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。

4.任务的执行时间:如果任务a的执行时间为20ms,且优先级大于任务b,任务b要求响应事件b在10ms之内,这样就会导致在事件b触发时,任务b无法在10ms内响应(任务a优先级高,正在运行)。所以要调节好不同执行事件任务的优先级,一般来说执行事件短的任务优先级可以相对高一些。

四、消息队列

队列又称消息队列,是一种常用于任务间通信的数据结构。其实就类似一个全局变量数组,创建好后,每个任务(只要能够接触到消息队列的句柄)都能够使用其来进行存储或获取数据。
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
 #define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */ 
 #define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */

Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */ 
                            (UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */ 
if (NULL != Test_Queue) 
printf("创建 Test_Queue 消息队列成功!\r\n");

xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */ 
                       &send_data1,/* 发送的消息内容 */ 
                       0 ); /* 等待时间 0 */

xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */ 
                         &r_queue, /* 发送的消息内容 */ 
                         portMAX_DELAY); /* 等待时间 一直等 */

消息队列中,传输的数据其实是不定长的数据,但是有最大的数据长度。那任务和任务之间要如何定义传输信息的消息格式呢?是在设计程序的时候就规划好的。例如a与b任务的 消息队列1 用于传输1类型的数据,消息队列则是用于传输2类型的数据。

五、信号量

信号量分为三个类型:1.二值信号量;2.计数信号量;3.互斥量--普通互斥量和递归互斥量

二值信号量和普通互斥信号量非常相似,有些微的差别互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。

其实如果溯源代码,可以发现,信号量和消息队列的定义一致,只是信号量是特殊的消息队列。单元大小为1,长度为1的消息队列。这也代表它具有消息队列的特点。只要任务具有其句柄,都能够对其进行操作(除了互斥量外)。即是a拿了二值互斥量,但是可以b返还。

1.二值信号量:

二值信号量的存储的数据只有0和1。

二值信号量既可以用于临界资源访问也可以用于同步功能。用作同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞,任务 2 在 某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态,如果任务 1 的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量,任务 1 也会得到信号量,从而达到任务与中断间的同步。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

#define xSemaphoreCreateBinary() \
        xQueueGenericCreate( \
                        ( UBaseType_t ) 1, \
                        semSEMAPHORE_QUEUE_ITEM_LENGTH, \
                        queueQUEUE_TYPE_BINARY_SEMAPHORE )

#endif

你可以看到,二值信号量的函数定义是由消息队列的内层函数宏定义而来。
2.计数信号量:
其实就是二值互斥量的升级版。。
#if( ( configUSE_COUNTING_SEMAPHORES == 1 )
&& ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
 
 QueueHandle_t xQueueCreateCountingSemaphore( 
 const UBaseType_t uxMaxCount, 
 const UBaseType_t uxInitialCount ) 
 {
 QueueHandle_t xHandle;
 
 configASSERT( uxMaxCount != 0 );
 configASSERT( uxInitialCount <= uxMaxCount );
 
 xHandle = xQueueGenericCreate( uxMaxCount, 
 queueSEMAPHORE_QUEUE_ITEM_LENGTH, 
 queueQUEUE_TYPE_COUNTING_SEMAPHORE ); 
 
 if ( xHandle != NULL ) { 
 ( ( Queue_t * ) xHandle )->uxMessagesWaiting = 
 uxInitialCount; 
 
 traceCREATE_COUNTING_SEMAPHORE();
 } else {
 traceCREATE_COUNTING_SEMAPHORE_FAILED();
 }
 
 return xHandle;
 }
 
 #endif
 /*-------

可以看到同样使用队列创建函数来进行对信号量的创建。

3.互斥量:

互斥量分为普通互斥量和递归互斥量。他们和其他的信号量不同,他们对信号量的获取必须由他们本身释放,即对谁拿走了钥匙有记忆,只有本人才能返还。
同时互斥信号量有一个非常重要的特点,就是优先级继承。在两个任务a,b。b优先级较低,在b使用互斥信号量锁内的资源时,同时a任务也需要调用锁内资源,此时b的任务优先级会提到高与a一致来保证a任务的不会被低级任务长时间堵塞。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐