FreeRTOS使用总结

来源:互联网 发布:推广淘宝产品 编辑:程序博客网 时间:2024/06/06 10:44

序言

      在实际开发中,如果程序等待一个事件发生,传统的无RTOS情况下,要么在原地一直等待而不能执行其它任务,要么使用状态机机制处理。而RTOS提供事件驱动型设计方式,只是在处理实际任务时才会运行,这能够更合理的利用CPU,也可以很方便的将当前任务阻塞在该事件下,然后自动去执行别的任务,这显然更方便,并且可以高效的利用CPU。处理这类事件,是我使用RTOS的最大动力。

    (1)大多数RTOS代码都具有一定规模,任何代码都可能带来BUG,何况是代码具有一定规模的RTOS,因此引入RTOS的同时也可能会引入该RTOS的BUG,这些RTOS本身的BUG一旦被触发,影响可能是是灾难性的。(2)不将RTOS分析透彻,很容易为项目埋下错误。典型的,像中断优先级、任务堆栈分配、可重入等,都是更容易出错的地方。(3)RTOS的优先级嵌套使得任务执行顺序、执行时序更难分析,甚至变成不可能。任务嵌套对所需的最大堆栈RAM大小估计也变得困难。这对很多对安全有严格要求的场合是不可想象的。(4)RTOS应该用于任务复杂的场合,以至于对任务调度的需求可以抵消RTOS所带来的稳定性影响,但大部分的应用并非复杂到需要RTOS。

 

一、内核配置说明

1、 时钟配置

configCPU_CLOCK_HZ    (/*你的硬件平台CPU系统时钟,Fcclk*/)

configTICK_RATE_HZ      ((portTickType)100)         

      第一个宏定义CPU系统时钟,也就是CPU执行时的频率。第二个宏定义FreeRTOS的时间片频率,这里定义为100,表明RTOS一秒钟可以切换100次任务,也就是每个时间片为10ms。

      prot.c中,函数vPortSetupTimerInterrupt()设置节拍时钟。该函数根据上面的两个宏定义的参数,计算SysTick定时器的重装载数值寄存器,然后设置SysTick定时器的控制及状态寄存器,设置如下:使用内核时钟源、使能中断、使能SysTick定时器。另外,函数vPortSetupTimerInterrupt()由函数vTaskStartScheduler()调用,这个函数用于启动调度器。

系统节拍中断用来测量时间,因此,越高的测量频率意味着可测到越高的分辨率时间。但是,高的系统节拍中断频率也意味着RTOS内核占用更多的CPU时间,因此会降低效率。多个任务可以共享一个优先级,RTOS调度器为相同优先级的任务分享CPU时间,在每一个RTOS 系统节拍中断到来时进行任务切换。

2、configMAX_PRIORITIES

    在RTOS内核中,每个有效优先级都会消耗一定量的RAM,因此这个值不要超过你的应用实际需要的优先级数目。每一个任务都会被分配一个优先级,优先级值从0~ (configMAX_PRIORITIES- 1)之间。

3、configMINIMAL_STACK_SIZE

       定义空闲任务使用的堆栈大小。通常此值不应小于对应处理器演示例程文件FreeRTOSConfig.h中定义的数值。

       就像xTaskCreate()函数的堆栈大小参数一样,堆栈大小不是以字节为单位而是以字为单位的,比如在32位架构下,栈大小为100表示栈内存占用400字节的空间

4、configMAX_TASK_NAME_LEN

       调用任务函数时,需要设置描述任务信息的字符串,这个宏用来定义该字符串的最大长度。这里定义的长度包括字符串结束符’\0’。

5、configUSE_TRACE_FACILITY

       设置成1表示启动可视化跟踪调试,会激活一些附加的结构体成员和函数xTASK_STATUS。

6、configUSE_16_BIT_TICKS

       定义系统节拍计数器的变量类型,即定义portTickType是表示16位变量还是32位变量。

       定义configUSE_16_BIT_TICKS为1意味着portTickType代表16位无符号整形,定义configUSE_16_BIT_TICKS为0意味着portTickType代表32位无符号整形。

       使用16位类型可以大大提高8位和16位架构微处理器的性能,但这也限制了最大时钟计数为65535个’Tick’。因此,如果Tick频率为100HZ10MS中断一次),对于任务最大延时或阻塞时间,16位计数器是655

7、configIDLE_SHOULD_YIELD

       这个参数控制任务在空闲优先级中的行为。仅在满足下列条件后,才会起作用:

    (1)使用抢占式内核调度

    (2)用户任务使用空闲优先级。

        通过时间片共享同一个优先级的多个任务,如果共享的优先级大于空闲优先级,并假设没有更高优先级任务,这些任务应该获得相同的处理器时间。(即优先级相同的任务共享时间片)

但如果共享空闲优先级时,情况会稍微有些不同。当此值为1时,其它共享空闲优先级的用户任务就绪时,空闲任务立刻让出CPU,用户任务运行,这样确保了能最快响应用户任务。处于这种模式下也会有不良效果(取决于你的程序需要),描述如下:

       图中描述了四个处于空闲优先级的任务,任务ABC是用户任务,任务I是空闲任务。上下文切换周期性的发生在T0、T1…T6时刻。当用户任务运行时,空闲任务立刻让出CPU,但是,空闲任务已经消耗了当前时间片中的一定时间。这样的结果就是空闲任务I和用户任务A共享一个时间片。用户任务B和用户任务C因此获得了比用户任务A更多的处理器时间。

可以通过下面方法避免:

(1)如果合适的话,将处于空闲优先级的各单独的任务放置到空闲钩子函数中;

(2)创建的用户任务优先级大于空闲优先级;

(3)设置IDLE_SHOULD_YIELD为0;

       设置configIDLE_SHOULD_YIELD为0将阻止空闲任务为用户任务让出CPU,直到空闲任务的时间片结束。这确保所有处在空闲优先级的任务分配到相同多的处理器时间,但是,这是以分配给空闲任务更高比例的处理器时间为代价的

8、configUSE_MUTEXES

    设置为1表示使用互斥量,设置成0表示忽略互斥量。

 互斥量和二值信号量区别:

互斥信号量必须是同一个任务申请,同一个任务释放,其它任务释放无效;

二值信号量,一个任务申请成功后,可以由另一个任务释放;

互斥信号量是二进制信号量的子集。

9、configUSE_QUEUE_SETS

       设置成1使能队列集功能(可以阻塞、挂起到多个队列和信号量),设置成0取消队列集功能

10、中断优先级设置

configKERNEL_INTERRUPT_PRIORITY

configMAX_SYSCALL_INTERRUPT_PRIORITY都需要设置的硬件设备:configKERNEL_INTERRUPT_PRIORITY用来设置RTOS内核自己的中断优先级。因为RTOS内核中断不允许抢占用户使用的中断,因此这个宏一般定义为硬件最低优先级。

    configMAX_SYSCALL_INTERRUPT_PRIORITY用来设置可以在中断服务程序中安全调用FreeRTOS API函数的最高中断优先级。优先级小于等于这个宏所代表的优先级时,程序可以在中断服务程序中安全的调用FreeRTOS API函数;如果优先级大于这个宏所代表的优先级,表示FreeRTOS无法禁止这个中断,在这个中断服务程序中绝不可以调用任何API函数。

    假如一个微控制器有8个中断优先级别:0表示最低优先级,7表示最高优先级(Cortex-M3Cortex-M4内核优先数和优先级别正好与之相反,后续文章会专门介绍它们)。当两个配置选项分别为40时,下图描述了每一个优先级别可以和不可做的事件:

configMAX_SYSCALL_INTERRUPT_PRIORITY=4

configKERNEL_INTERRUPT_PRIORITY=0


二、任务概述

通常任务函数都是一个死循环,任务由xTaskCreate()函数创建,由vTaskDelete()函数删除。删除任务后,空闲任务用来释放RTOS分配给被删除任务的内存。因此,在应用中使用vTaskDelete()函数后确保空闲任务能获得处理器时间。除此之外,空闲任务没有其它有效功能,所以可以被合理的剥夺处理器时间,并且它的优先级也是最低的。

vTaskDelay( 500 / portTICK_RATE_MS ),portTICK_RATE_MS将系统时钟节拍周期转变为ms。

在任务进行过程中可以获取、改变任务优先级,挂起、恢复和删除等操作。

三、队列管理

队列的基本用法:

1、定义一个队列句柄变量,用于保存创建的队列:xQueueHandle xQueue1;

2、使用API函数xQueueCreate()创建一个队列。

3、如果希望使用先进先出队列,使用API函数xQueueSend()或xQueueSendToBack()向队列投递队列项。如果希望使用后进先出队列,使用API函数xQueueSendToFront()向队列投递队列项。如果在中断服务程序中,切记使用它们的带中断保护版本。

4、使用API函数xQueueReceive()从队列读取队列项,如果在中断服务程序中,切记使用它们的带中断保护版本。

目前串口收发数据使用队列进行传输,串口接收数据时,将收到的数据放入队列,开启uart_rx_hook()函数,超时之后发送一个二值信号量激活对应任务进行执行。

四、信号量管理

    FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。我们可以把互斥量和递归互斥量看成特殊的信号量。互斥量和信号量在用法上不同:

(1)信号量用于同步,任务间或者任务和中断间同步;互斥量用于互锁,用于保护同时只能有一个任务访问的资源,为资源上一把锁。

(2)信号量用于同步时,一般是一个任务(或中断)给出信号,另一个任务获取信号;互斥量必须在同一个任务中获取信号、同一个任务给出信号。

(3)互斥量具有优先级继承,信号量没有。

(4)互斥量不能用在中断服务程序中,信号量可以。

(5)创建互斥量和创建信号量的API函数不同,但是共用获取和给出信号API函数;

    可以将二进制信号量看作只有一个项目(item)的队列,因此这个队列只能为空或满(因此称为二进制)。任务和中断使用信号量无需关注谁控制---只需要知道二值信号量是空还是满。利用这个机制可以在任务和中断之间同步。

互斥资源的使用方式:

当一个任务在使用某个资源的过程中,即还没有完全结束对资源的访问时,便被切出运行态,使得资源处于非一致,不完整的状态。如果这个时候有另一个任务或者中断来访问这个资源,则会导致数据损坏或是其它相似的错误。

例子:访问外设考虑如下情形,有两个任务都试图往一个 LCD 中写数据:
 任务 A 运行,并往 LCD 写字符串”Hello world”。
 任务 A 被任务 B 抢占,但此时字符串才输出到”Hello w”。
 任务 B 往 LCD 写”Abort, Retry, Fail?”,然后进入阻塞态。
 任务 A 从被抢占处继续执行,完成剩余的字符输出——“orld”。 现在 LCD 显示的是被破坏了的字符串”HellowAbort, Retry, Fail?orld”。

1、基本临界区是指宏 taskENTER_CRITICAL()与 taskEXIT_CRITICAL()之间的代码区间。临界区的工作仅仅是简单地把中断全部关掉,抢占式上下文切换只可能在某个中断中完成,所以调用 taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。

坏处:临界区之间的代码必须简短,像上面例子这种中断输出是不行的。长时间没有任务切换影响系统的实时性。

2、挂起调度器。

通过调用 vTaskSuspendAll()来挂起调度器。挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则这个请求也会被挂起,直到调度器被唤醒后才会得到执行。xTaskResumeAll()解除调度器的挂起状态。

3、互斥量

使用xSemaphoreCreateMutex()API函数创建互斥量,xSemaphoreTake(xMutex, portMAX_DELAY )拿走信号量,xSemaphoreGive( xMutex )归还信号量。

使用互斥量需要避免进入死锁:

例子:任务A与任务B都需要获得互斥量X与互斥量Y以完成各自的工作:

1. 任务A执行,并成功获得了互斥量 X。

2. 任务 A 被任务 B 抢占。

3. 任务 B 成功获得了互斥量 Y,之后又试图获取互斥量 X——但互斥量 X 已 经被任务 A 持有,所以对任务 B 无效。任务 B 选择进入阻塞态以等待互斥量 X 被释放。

4. 任务A得以继续执行。其试图获取互斥量Y——但互斥量 Y 已经被任务B持有而对任务A无效。任务A也选择进入阻塞态以等待互斥量 Y 被释放。

这种情形的最终结局是,任务A在等待一个被任务B持有的互斥量,而任务B也在等待一个被任务A持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下去了。

五、链表管理

    从整段代码中可以看出,初始化后的链表是一个循环链表,以xListEnd为尾。整个链表只有一个节点(就是xListEnd)。而头节点则是xListEnd的下一个节点。另外,尾节点的节点值为portMAX_DELAY。上图描述:可以说,整条链表是按以下方式进行管理的。首先是初始化状态,如下图所示:


    P指针为前一节点指针,N为下一节点指针。接下来是插入操作。插入节点有两种类型:(1)是添加新节点到尾部。根据代码描述,这种类型的插入结果如下图所示。可以看出整条链表是一条循环链表,用链表中的xListEnd来指示链表结尾,并用来给出链表头入口。

          

上图的添加节点New4是属于第(2)种节点插入的情况,根据代码描述,是按值的顺序来决定插入位置的,只要遇到链表中的其中一个节点B的值比新节点A的值要大,那么A就插入到B的前面。例如,节点值排序为new1<new2<new4<new3,new4为新节点,那么new4就插入到new3的前面。

至于移除的话就是很普通的链表操作,注意一下链表指针没有指错,以及保持链表为循环链表就行了。

六、内存管理

    目前官方提供了5种内存管理策略:对于heap_1.c、heap_2.c和heap_4.c这三种内存管理策略,内存堆实际上是一个很大的数组,定义为:
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

Heap_1只能简单的申请内存,不能进行释放,主要适用于不需要动态删除任务、信号量、队列等,而是在初始化的时候一次性创建好,便一直使用,永远不用删除的程序。

   Heap_2.c这个方案使用一个匹配算法,它允许释放之前分配的内存块,但不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。

heap_3.c,这种策略只是简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。因此,内存堆需要通过编译器或者启动文件设置堆空间。

    heap_4.c与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。

    heap_5.c比较有趣,它允许程序设置多个非连续内存堆,比如需要快速访问的内存堆设置在片内RAM,稍微慢速访问的内存堆设置在外部RAM。每个内存堆的起始地址和大小由应用程序设计者定义。

     API函数xPortGetFreeHeapSize()返回剩下的未分配堆栈空间的大小(可用于优化设置configTOTAL_HEAP_SIZE宏的值),但是不能提供未分配内存的碎片细节信息。

      heap_2功能简介:

    目前我们使用第二种策略,可以用于重复的分配和删除具有相同堆栈空间的任务、队列、信号量、互斥量等等,并且不考虑内存碎片的应用程序。不能用在分配和释放随机字节堆栈空间的应用程序。

如果一个应用程序动态的创建和删除任务,并且分配给任务的堆栈空间总是同样大小,那么大多数情况下heap_2.c是可以使用的。但是,如果分配给任务的堆栈不总是相等,那么释放的有效内存可能碎片化,形成很多小的内存块。最后会因为没有足够大的连续堆栈空间而造成内存分配失败。在这种情况下,heap_4.c是一个很好的选择。虽然不具有确定性,但是它比标准库中的malloc函数具有高得多的效率。

代码分析示意图:

    假如内存堆数组ucHeap从RAM地址0x10002003处开始,系统按照8字节对齐,则对齐后的内存堆如图所示:

                                      内存堆示大小与地址对齐示意图

内存初始化函数:

                                                                           内存堆初始化示意图



内存申请函数:

                                                                 经过两次内存分配后的内存堆示意图

                                                                                                                                                                                

                                                                             释放内存后,内存堆示意图


 

 

1 0
原创粉丝点击