FreeRTOS+LWIP.docx
《FreeRTOS+LWIP.docx》由会员分享,可在线阅读,更多相关《FreeRTOS+LWIP.docx(43页珍藏版)》请在冰豆网上搜索。
FreeRTOS+LWIP
FreeRTOS与LWIP的移植
1FreeRTOS任务管理
1-1任务函数
任务是由C语言函数实现的。
唯一特别的只是任务的函数原型,其必须返回void,而且带有一个void指针参数(voidATaskFunction(void*pvParameters);)。
每个任务都是在自己权限范围内的一个小程序。
其具有程序入口,通常会运行在一个死循环中,也不会退出。
FreeRTOS任务不允许以任何方式从实现函数中返回——它们绝不能有一
条”return”语句,也不能执行到函数末尾。
如果一个任务不再需要,可以显式地将其删除。
一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。
例:
voidATaskFunction(void*pvParameters)
{
/*可以像普通函数一样定义变量。
用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变量。
但如果iVariableExample被定义为static,这一点则不成立–这种情况下只存在一个变量,所有的任务实例将会共享这个变量。
*/
intiVariableExample=0;
/*任务通常实现在一个死循环中。
*/
for(;;)
{
/*完成任务功能的代码将放在这里。
*/
}
/*如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。
传入NULL参数表示删除
的是当前任务*/
vTaskDelete(NULL);
}
1-2创建任务
创建任务使用FreeRTOS的API函数xTaskCreate()。
接下来描述用到的数据类型和命名约定。
xTaskCreate()API函数原型如下:
portBASE_TYPExTaskCreate(pdTASK_CODEpvTaskCode,
constsignedportCHAR*constpcName,
unsignedportSHORTusStackDepth,
void*pvParameters,
unsignedportBASE_TYPEuxPriority,
xTaskHandle*pxCreatedTask);
表1xTaskCreate()参数与返回值
参数名
含义描述
pdTASK_CODE
任务只是永不退出的C函数,实现常通常是一个死循环。
参数pvTaskCode只一个指向任务的实现函数的指针(效果上仅仅是函数名)。
pcName
具有描述性的任务名。
这个参数不会被FreeRTOS使用。
其只是单纯地用于辅助调试。
识别一个具有可读性的名字总是比通过句柄来识别容易得多。
usStackDepth
当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态。
usStackDepth值用于告诉内核为它分配多大的栈空间。
这个值指定的是栈空间可以保存多少个字(word),而不是多少个字节(byte)。
比如说,如果是32位宽的栈空间,传入的usStackDepth值为100,则将会分配400字节的栈空间(100*4bytes)。
栈深度乘以栈宽度的结果千万不能超过一个size_t类型变量所能表达的最大值。
应用程序通过定义常量configMINIMAL_STACK_SIZE来决定空闲任务任用的栈空间大小。
在FreeRTOS为微控制器架构提供的Demo应用程序中,赋予此常量的值是对所有任务的最小建议值。
如果你的任务会使用大量栈空间,那么你应当赋予一个更大的值。
没有任何简单的方法可以决定一个任务到底需要多大的栈空间。
计算出来虽然是可能的,但大多数用户会先简单地赋予一个自认为合理的值,然后利用FreeRTOS提供的特性来确证分配的空间既不欠缺也不浪费。
pvParameters
任务函数接受一个指向void的指针(void*)。
pvParameters的值即是传递到任务中的值。
这篇文档中的一些范例程序将会示范这个参数可以如何使用。
uxPriority
指定任务执行的优先级。
优先级的取值范围可以从最低优先级0到最高优先级(configMAX_PRIORITIES–1)。
pxCreatedTask
用于传出任务的句柄。
这个句柄将在API调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。
如果应用程序中不会用到这个任务的句柄,则pxCreatedTask可以被设为NULL。
返回值
有两个可能的返回值:
1.pdTRUE表明任务创建成功。
2.errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY由于内存堆空间不足,FreeRTOS无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
1-3任务优先级
xTaskCreate()API函数的参数uxPriority为创建的任务赋予了一个初始优先级。
优先级可以在调度器启动后调用vTaskPrioritySet()API函数进行修改。
任意数量的任务可以共享同一个优先级——以保证最大设计弹性。
低优先级号表示任务的优先级低,优先级号0表示最低优先级。
调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运行态。
如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。
两个测试任务被创建在同一个优先级上,并且一直是可运行的。
所以每个任务都执行一个“时间片”,任务在时间片起始时刻进入运行态,在时间片结束时刻又退出运行态。
调度器总是在可运行的任务中,选择具有最高优级的任务,并使其进入运行态。
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。
一个称为心跳(tick,有些地方被称为时钟滴答,本文中一律称为时钟心跳)中断的周期性中断用于此目的。
时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h中的编译时配置常configTICK_RATE_HZ进行配置。
比如说,如果configTICK_RATE_HZ设为100(HZ),则时间片长度为10ms。
API函数vTaskPriofitySet()可以用于在调度器启动后改变任何任务的优先级。
表2vTaskPrioritySet()参数
参数
含义描述
pxTask
被修改优先级的任务句柄(即目标任务)——参考xTaskCreate()API函数的参数pxCreatedTask以了解如何得到任务句柄方面的信息。
任务可以通过传入NULL值来修改自己的优先级。
uxNewPriority
目标任务将被设置到哪个优先级上。
如果设置的值超过了最大可用优先级(configMAX_PRIORITIES–1),则会被自动封顶为最大值。
常量configMAX_PRIORITIES是FreeRTOSConfig.h头文件中设置的一个编译时选项。
1-4调度算法
优先级抢占式调度
●每个任务都赋予了一个优先级。
●每个任务都可以存在于一个或多个状态。
●在任何时候都只有一个任务可以处于运行状态。
●调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行。
这种方法被称为“固定优先级抢占式调度”,所谓“固定优先级”是指每个任务都被赋予了一个优先级,这个优先级不能被内核本身改变(只能被任务修改)。
”抢占式”是指当任务进入就绪态或是优先级被改变时,如果处于运行态的任务优先级更低,则该任务总是抢占当前运行的任务。
任务可以在阻塞状态等待一个事件,当事件发生时其将自动回到就绪态。
时间事件发生在某个特定的时刻,比如阻塞超时。
时间事件通常用于周期性或超时行为。
任务或中断服务例程往队列发送消息或发送任务一种信号量,都将触发同步事件。
同步事件通常用于触发同步行为,比如某个外围的数据到达了。
下图为某个应用程序的执行流程展现了抢占式调度的行为方式。
图1执行流程中的主要抢占点
1.空闲任务
空闲任务具有最低优先级,所以每当有更高优先级任务处于就绪态是,空闲任务就会被抢占——如图中t3,t5和t9时刻。
2.任务3
任务3是一个事件驱动任务。
其工作在一个相对较低的优先级,但优先级高于空闲任务。
其大部份时间都在阻塞态等待其关心的事件。
每当事件发生时其就
从阻塞态转移到就绪态。
FreeRTOS中所有的任务间通信机制(队列,信号量等)
都可以通过这种方式用于发送事件以及让任务解除阻塞。
事件在t3,t5以及t9至t12之间的某个时刻发生。
发生在t3和t5时刻的事件可以立即被处理,因为这些时刻任务3在所有可运行任务中优先级最高。
发生在t9至t12之间某个时刻的事件不会得到立即处理,需要一直等到t12时刻。
因为具有更高优先级的任务1和任务2尚在运行中,只有到了t12时刻,这两个任务进入阻塞态,使得任务3成为具有最高优先级的就绪态任务。
3.任务2
任务2是一个周期性任务,其优先级高于任务3并低于任务1。
根据周期间隔,任务2期望在t1,t6和t9时刻执行。
在t6时刻任务3处于运行态,但是任务2相对具有更高的优先级,所以会抢占任务3,并立即得到执行。
任务2完成处理后,在t7时刻返回阻塞态。
同时,任务3得以重新进入运行态,继续完成处理。
任务3在t8时刻进入阻塞状态。
4.任务1
任务1也是一个事件驱动任务。
任务1在所有任务中具有最高优先级,因此可以抢占系统中的任何其它任务。
在图中看到,任务1的事件只是发生在在t10时刻,此时任务1抢占了任务2。
只有当任务1在t11时刻再次进入阻塞态之后,任务2才得以机会继续完成处理。
选择任务优先级
单调速率调度(RateMonotonicScheduling,RMS)是一种常用的优先级分配技术。
其根据任务周期性执行的速率来分配一个唯一的优先级。
具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。
这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。
协作式调度
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用taskYIELD()时,才会进行上下文切换。
任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。
协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。
实现混合调度方案也是可行的,这需要在中断服务例程中显式地进行上下文切换,从而允许同步事件产生抢占行为,但时间事件却不行。
这样做的结果是得到了一个没有时间片机制的抢占式系统。
或许这正是所期望的,因为获得了效率,并且这也是一种常用的调度器配置。
2FreeRTOS队列管理
2-1队列的特性
1、数据存储
队列可以保存有限个具有确定长度的数据单元。
队列可以保存的最大单元数目被称为队列的“深度”。
在队列创建时需要设定其深度和每个单元的大小。
通常情况下,队列被作为FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出。
当然,由队列首写入也是可能的。
往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
2、可被多任务存取
所有任务都可以向同一队列写入和读出。
3、读队列时阻塞
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。
在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。
当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。
当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。
这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。
而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
4、写队列时阻塞
同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。
这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
2-2队列的使用
队列由声明为xQueueHandle的变量进行引用。
xQueueCreate()用于创建一个队列,并返回一个xQueueHandle句柄以便于对其创建的队列进行引用。
当创建队列时,FreeRTOS从堆空间中分配内存空间。
分配的空间用于存储队列数据结构本身以及队列中包含的数据单元。
如果内存堆中没有足够的空间来创建队列,xQueueCreate()将返回NULL。
xQueueSendToBack()与xQueueSendToFront()API函数
xQueueSendToBack()用于将数据发送到队列尾;而xQueueSendToFront()用于将数据发送到队列首。
切记不要在中断服务例程中调用xQueueSendToFront()或xQueueSendToBack()。
系统提供中断安全版本xQueueSendToFrontFromISR()与xQueueSendToBackFromISR()用于在中断服务中实现相同的功能。
xQueueReceive()与xQueuePeek()API函数
xQueueReceive()用于从队列中接收(读取)数据单元。
接收到的单元同时会从队列中删除。
xQueuePeek()也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。
xQueuePeek()从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储顺序。
uxQueueMessagesWaiting()API函数
uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。
使用队列传递复合数据类型
一个任务从单个队列中接收来自多个发送源的数据是经常的事。
通常接收方收到数据后,需要知道数据的来源,并根据数据的来源决定下一步如何处理。
一个简单的方式就是利用队列传递结构体,结构体成员中就包含了数据信息和来源信息。
图2对这一方案进行了展现。
图2结构体被用于队列传递的一种情形
从图2中可以看出:
•创建一个队列用于保存类型为xData的结构体数据单元。
结构体成员包括了一个数据值和表示数据含义的编码,两者合为一个消息可以一次性发送到队列。
•中央控制任务用于完成主要的系统功能。
其必须对队列中传来的输入和其它系统状态的改变作出响应。
•CAN总线任务用于封装CAN总线的接口功能。
当CAN总线任务收到并解码一个消息后,其将把解码后的消息放到xData结构体中发往控制任务。
结构体的iMeaning成员用于让中央控制任务知道这个数据是用来干什么的—从图中的描述可以看出,这个数据表示电机速度。
结构体的iValue成员可以让中央控制任务知道电机的实际速度值。
•人机接口(HMI)任务用于对所有的人机接口功能进行封装。
设备操作员可能通过各种方式进行命令输入和参数查询,人机接口任务需要对这些操作进行检测并解析。
当接收到一个新的命令后,人机接口任务通过xData结构将命令发送到中央控制任务。
结构体的iMeaning成员用于让中央控制任务知道这个数据是用来干什么的—从图中的描述可以看出,这个数据表示一个新的参数设置。
结构体的iValue成员可以让中央控制任务知道具体的设置值。
3FreeRTOS中断管理
3-1延迟中断处理(采用二值信号量同步)
二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断同步。
这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务例程(ISR)中只是快速处理少部份工作。
中断处理可以说是被“推迟(deferred)”到一个“处理(handler)”任务。
如果某个中断处理要求特别紧急,其延迟处理任务的优先级可以设为最高,以保证延迟处理任务随时都抢占系统中的其它任务。
这样,延迟处理任务就成为其对应的ISR退出后第一个执行的任务,在时间上紧接着ISR执行,相当于所有的处理都在ISR中完成一样。
延迟处理任务对一个信号量进行带阻塞性质的“take”调用,意思是进入阻塞态以等待事件发生。
当事件发生后,ISR对同一个信号量进行“give”操作,使得延迟处理任务解除阻塞,从而事件在延迟处理任务中得到相应的处理。
在这种中断同步的情形下,信号量可以看作是一个深度为1的队列。
这个队列由于最多只能保存一个数据单元,所以其不为空则为满(所谓“二值”)。
延迟处理任务调用xSemaphoreTake()时,等效于带阻塞时间地读取队列,如果队列为空的话任务则进入阻塞态。
当事件发生后,ISR简单地通过调xSemaphoreGiveFromISR()放置一个令牌(信号量)到队列中,使得队列成为满状态。
这也使得延迟处理任务切出阻塞态,并移除令牌,使得队列再次成为空。
当任务完成处理后,再次读取队列,发现队列为空,又进入阻塞态,等待下一次事件发生。
4FreeRTOS时间管理
FreeRTOS提供的典型时间管理函数是vTaskDelay(),调用此函数可以实现将任务延时一段特定时间的功能。
在FreeRT0S中,若一个任务要延时xTicksToDelay个时钟节拍,系统内核会把当前系统已运行的时钟节拍总数(定义为xTickCount,32位长度)加上xTicksToDelay得到任务下次唤醒时的时钟节拍数xTimeToWake。
然后,内核把此任务的任务控制块从就绪链表中删除,把xTimeToWake作为结点值赋予任务的xItemValue,再根据xTimeToWake的值把任务控制块按照顺序插入不同的链表。
若xTimeToWake>xTickCount,即计算中没有出现溢出,内核把任务控制块插入到pxDelayedTaskList链表;若xTimeToWake每发生一个时钟节拍,内核就会把当前的xTick-Count加1。
若xTickCount的结果为0,即发生溢出,内核会把pxOverflowDelayedTaskList作为当前链表;否则,内核把pxDelaycdTaskList作为当前链表。
内核依次比较xTickCotlrtt和链表各个结点的xTimcToWake。
若xTick-Count等于或大于xTimeToWake,说明延时时间已到,应该把任务从等待链表中删除,加入就绪链表。
由此可见,不同于μC/OS—II,FreeRTOS采用“加”的方式实现时间管理。
其优点是时间节拍函数的执行时间与任务数量基本无关,而μC/OS—II的OSTimcTick()的执行时间正比于应用程序中建立的任务数。
因此当任务较多时,FreeRTOS采用的时间管理方式能有效加快时钟节拍中断程序的执行速度。
5FreeRTOS内存分配
每当任务、队列和信号量创建的时候,FreeRTOS要求分配一定的RAM。
虽然采用malloc()和free()函数可以实现申请和释放内存的功能,但这两个函数存在以下缺点:
并不是在所有的嵌入式系统中都可用,要占用不定的程序空间,可重人性欠缺以及执行时间具有不可确定性。
为此,除了可采用malloc()和free()函数外,FreeRTOS还提供了另外两种内存分配的策略,用户可以根据实际需要选择不同的内存分配策略。
第1种方法是,按照需求内存的大小简单地把一大块内存分割为若干小块,每个小块的大小对应于所需求内存的大小。
这样做的好处是比较简单,执行时间可严格确定,适用于任务和队列全部创建完毕后再进行内核调度的系统;这样做的缺点是,由于内存不能有效释放,系统运行时应用程序并不能实现删除任务或队列。
第2种方法是,采用链表分配内存,可实现动态的创建、删除任务或队列。
系统根据空闲内存块的大小按从小到大的顺序组织空闲内存链表。
当应用程序申请一块内存时,系统根据申请内存的大小按顺序搜索空闲内存链表,找到满足申请内存要求的最小空闲内存块。
为了提高内存的使用效率,在空闲内存块比申请内存大的情况下,系统会把此空闲内存块一分为二。
一块用于满足申请内存的要求,一块作为新的空闲内存块插入到链表中。
下面以图3为例介绍方法2的实现。
假定用于动态分配的RAM共有8KB,系统首先初始化空闲内存块链表,把8KBRAM全部作为一个空闲内存块。
当应用程序分别申请1KB和2KB内存后,空闲内存块的大小变为5KB。
2KB的内存使用完毕后,系统需要把2KB插入到现有的空闲内存块链表。
由于2KB<5KB,所以把这2KB插入5KB的内存块之前。
若应用程序又需要申请3KB的内存,而在空闲内存块链表中能满足申请内存要求的最小空闲内存块为5KB,因此把5KB内存拆分为2部分,3KB部分用于满足申请内存的需要,2KB部分作为新的空闲内存块插入链表。
随后1KB的内存使用完毕需要释放,系统会按顺序把1KB内存插入到空闲内存链表中。
图3采用空闲内存块链表进行内存管理
方法2的优点是,能根据任务需要高效率地使用内存,尤其是当不同的任务需要不同大小的内存的时候。
方法二的缺点是,不能把应用程序释放的内存和原有的空闲内存混合为一体,因此,若应用程序频繁申请与释放“随机”大小的内存,就可能造成大量的内存碎片。
这就要求应用程序申请与释放内存的大小为“有限个”固定的值(如图3中申请与释放内存的大小固定为lKB、2KB或3KB)。
方法2的另一个缺点是,程序执行时间具有一定的不确定性。
μC/OS—II提供的内存管理机制是把连续的大块内存按分区来管理,每个分区中包含整数个大小相同的内存块。
由于每个分区的大小相同,即使频繁地申请和释放内存也不会产生内存碎片问题,但其缺点是内存的利用率相对不高。
当申请和释放的内存大小均为一个固定值时(如均为2KB),FreeRTOS的方法2内存分配策略就可以实现类似μC/OS—Ⅱ的内存管理效果。
6STM32中FreeRTOS的移植
FreeRTOS源码包结构
FreeRTOS的实现主要由list.c、queue.c、croutine.c和tasks.c4个文件组成。
list.c是一个链表的实现,主要供给内核调度器使用;queue.c是一个队列的实现,支持中断环境和信号量控制;croutine.c和task.c是两种任务的组织实现。
对于croutine,各任务共享同一个堆栈,使RAM的需求进一步缩小,但也正因如此,他的使用受到相对严格的限制。
而task则是传统的实现,各任务使用各自的堆栈,支持完全的抢占式调度。
1)与FreeRTOS内核有关的文件数量仅为5个,分别是list.cqueue.ctasks.ccroutine.ctimers.c
这些文件位于FreeRTOS\Source
2)与内存分配有关的文件共有4个,分别是heap_1.c,heap_2.c,heap_3.c,heap_4.c。
4个文件只需选择其中的1个,STM32选择heap_2.c。
该文件位于FreeRTOS\Source\portable\MemMang
3)与移植相关的代码包括port.c,portmacro.h。
这些代码不但和编译器有关还和平台(MCU)有关。
FreeRTOS先以编译器为大类,然后再以平台(MCU)为小类。
在这里选择KEIL编译器,平台为ARM_CM3。
该