当前位置:文档之家› FreeRTOS+LWIP

FreeRTOS+LWIP

FreeRTOS+LWIP
FreeRTOS+LWIP

FreeRTOS与LWIP的移植

1 FreeRTOS任务管理

1-1任务函数

任务是由C 语言函数实现的。唯一特别的只是任务的函数原型,其必须返回void,而且带有一个void 指针参数(void ATaskFunction( void

*pvParameters );)。每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一个死循环中,也不会退出。

FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条”return”语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。

一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量。

例:

void ATaskFunction( void *pvParameters )

{

/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变量。但如果iVariableExample被定义为static,这一点则不成立–这种情况下只存在一个变量,所有的任务实例将会共享这个变量。*/

int iVariableExample = 0;

/* 任务通常实现在一个死循环中。*/

for( ;; )

{

/* 完成任务功能的代码将放在这里。*/

}

/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除

的是当前任务*/

vTaskDelete( NULL );

}

1-2创建任务

创建任务使用FreeRTOS 的API 函数xTaskCreate()。

接下来描述用到的数据类型和命名约定。

xTaskCreate() API 函数原型如下:

portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,

const signed portCHAR * const pcName,

unsigned portSHORT usStackDepth,

void *pvParameters,

unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask );

表1 xTaskCreate()参数与返回值

1-3任务优先级

xTaskCreate()API函数的参数uxPriority为创建的任务赋予了一个初始优先级。优先级可以在调度器启动后调用vTaskPrioritySet()API函数进行修改。任意数量的任务可以共享同一个优先级——以保证最大设计弹性。低优先级号表

示任务的优先级低,优先级号0表示最低优先级。调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。两个测试任务被创建在同一个优先级上,并且一直是可运行的。所以每个任务都执行一个“时间片”,任务在时间片起始时刻进入运行态,在时间片结束时刻又退出运行态。

调度器总是在可运行的任务中,选择具有最高优级的任务,并使其进入运行态。要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳(tick,有些地方被称为时钟滴答,本文中一律称为时钟心跳)中断的周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h 中的编译时配置常configTICK_RATE_HZ 进行配置。比如说,如果configTICK_RATE_HZ设为100(HZ),则时间片长度为10ms。

API 函数vTaskPriofitySet()可以用于在调度器启动后改变任何任务的优先级。

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 才得以机会继续完成处理。

选择任务优先级

单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配

技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。

协作式调度

采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。

实现混合调度方案也是可行的,这需要在中断服务例程中显式地进行上下文切换,从而允许同步事件产生抢占行为,但时间事件却不行。这样做的结果是得到了一个没有时间片机制的抢占式系统。或许这正是所期望的,因为获得了效率,并且这也是一种常用的调度器配置。

2 FreeRTOS队列管理

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 成员可以让中央控制任务知道具体的设置值。

3 FreeRTOS中断管理

3-1延迟中断处理(采用二值信号量同步)

二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断同步。这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务例程(ISR)中只是快速处理少部份工作。中断处理可以说是被“推迟(deferred)”到一个“处理(handler)”任务。

如果某个中断处理要求特别紧急,其延迟处理任务的优先级可以设为最高,以保证延迟处理任务随时都抢占系统中的其它任务。这样,延迟处理任务就成为其对应的ISR退出后第一个执行的任务,在时间上紧接着ISR 执行,相当于所有的处理都在ISR 中完成一样。

延迟处理任务对一个信号量进行带阻塞性质的“take”调用,意思是进入阻塞态以等待事件发生。当事件发生后,ISR 对同一个信号量进行“give”操作,使得延迟处理任务解除阻塞,从而事件在延迟处理任务中得到相应的处理。

在这种中断同步的情形下,信号量可以看作是一个深度为1 的队列。这个队列由于最多只能保存一个数据单元,所以其不为空则为满(所谓“二值”)。延迟处理任务调用xSemaphoreTake()时,等效于带阻塞时间地读取队列,如果队列为空的话任务则进入阻塞态。当事件发生后,ISR 简单地通过调xSemaphoreGiveFromISR()放置一个令牌(信号量)到队列中,使得队列成为满状态。这也使得延迟处理任务切出阻塞态,并移除令牌,使得队列再次成为空。当任务完成处理后,再次读取队列,发现队列为空,又进入阻塞态,等待下一次事件发生。

4 FreeRTOS时间管理

FreeRTOS提供的典型时间管理函数是vTaskDelay(),调用此函数可以实现将任务延时一段特定时间的功能。在FreeRT0S中,若一个任务要延时xTicksToDelay个时钟节拍,系统内核会把当前系统已运行的时钟节拍总数(定义为xTickCount,32位长度)加上xTicksToDelay得到任务下次唤醒时的时钟节拍数xTimeToWake。然后,内核把此任务的任务控制块从就绪链表中删除,把xTimeToWake作为结点值赋予任务的xItemValue,再根据xTimeToWake的值把任务控制块按照顺序插入不同的链表。若 xTimeToWake>xTickCount,即计算中没有出现溢出,内核把任务控制块插入到pxDelayedTaskList链表;若xTimeToWak e

每发生一个时钟节拍,内核就会把当前的xTick-Count加1。若xTickCount 的结果为0,即发生溢出,内核会把pxOverflowDelayedTaskList作为当前链表;否则,内核把pxDelaycdTaskList作为当前链表。内核依次比较 xTickCotlrtt 和链表各个结点的xTimcToWake。若xTick-Count等于或大于xTimeToWake,说明延时时间已到,应该把任务从等待链表中删除,加入就绪链表。

由此可见,不同于μC/OS—II,FreeRTOS采用“加”的方式实现时间管理。其优点是时间节拍函数的执行时间与任务数量基本无关,而μC/OS— II的OSTimcTick()的执行时间正比于应用程序中建立的任务数。因此当任务较多时,FreeRTOS采用的时间管理方式能有效加快时钟节拍中断程序的执行速度。

5 FreeRTOS内存分配

每当任务、队列和信号量创建的时候,FreeRTOS要求分配一定的RAM。虽然采用malloc()和free()函数可以实现申请和释放内存的功能,但这两个函数存在以下缺点:并不是在所有的嵌入式系统中都可用,要占用不定的程序空间,可重人性欠缺以及执行时间具有不可确定性。为此,除了可采用 malloc()和free()函数外,FreeRTOS还提供了另外两种内存分配的策略,用户可以根据实际需要选择不同的内存分配策略。

第1种方法是,按照需求内存的大小简单地把一大块内存分割为若干小块,每个小块的大小对应于所需求内存的大小。这样做的好处是比较简单,执行时间可严格确定,适用于任务和队列全部创建完毕后再进行内核调度的系统;这样做的缺点是,由于内存不能有效释放,系统运行时应用程序并不能实现删除任务或队列。

第2种方法是,采用链表分配内存,可实现动态的创建、删除任务或队列。系统根据空闲内存块的大小按从小到大的顺序组织空闲内存链表。当应用程序申请一块内存时,系统根据申请内存的大小按顺序搜索空闲内存链表,找到满足

申请内存要求的最小空闲内存块。为了提高内存的使用效率,在空闲内存块比申请内存大的情况下,系统会把此空闲内存块一分为二。一块用于满足申请内存的要求,一块作为新的空闲内存块插入到链表中。

下面以图3为例介绍方法2的实现。假定用于动态分配的RAM共有8KB,系统首先初始化空闲内存块链表,把8KB RAM全部作为一个空闲内存块。当应用程序分别申请1KB和2KB内存后,空闲内存块的大小变为5KB。2KB的内存使用完毕后,系统需要把2KB插入到现有的空闲内存块链表。由于2 KB<5KB,所以把这2 KB插入5KB的内存块之前。若应用程序又需要申请3 KB的内存,而在空闲内存块链表中能满足申请内存要求的最小空闲内存块为5KB,因此把5KB内存拆分为2部分,3KB部分用于满足申请内存的需要,2KB部分作为新的空闲内存块插入链表。随后1KB的内存使用完毕需要释放,系统会按顺序把1KB内存插入到空闲内存链表中。

图3 采用空闲内存块链表进行内存管理

方法2的优点是,能根据任务需要高效率地使用内存,尤其是当不同的任务需要不同大小的内存的时候。方法二的缺点是,不能把应用程序释放的内存和原有的空闲内存混合为一体,因此,若应用程序频繁申请与释放“随机”大小的内存,就可能造成大量的内存碎片。这就要求应用程序申请与释放内存的大小为“有限个”固定的值(如图3中申请与释放内存的大小固定为l KB、2 KB或3 KB)。方法2的另一个缺点是,程序执行时间具有一定的不确定性。

μC/OS—II提供的内存管理机制是把连续的大块内存按分区来管理,每个分区中包含整数个大小相同的内存块。由于每个分区的大小相同,即使频繁地申请和释放内存也不会产生内存碎片问题,但其缺点是内存的利用率相对不高。当申请和释放的内存大小均为一个固定值时(如均为2 KB),FreeRTOS的方法2内存分配策略就可以实现类似μC/OS—Ⅱ的内存管理效果。

6 STM32中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.c queue.c https://www.doczj.com/doc/ca17134672.html,routine.c timers.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。

该文件位于FreeRTOS\Source\portable\RVDS\ARM_CM3(KEIL与RVDS用同一个源码,所以KEIL里没有提供源码,直接从RVDS里取)

4)除了上述内容之外,还包括FreeRTOS内核相关的头文件。

该文件FreeRTOS\Source\include

FreeRTOS添加到KEIL

1、在KEIL工程目录下添加一个FreeRTOS的目录文件并添加上述列举STM32需要的.C文件具体如下图:

2、将源码包FreeRTOS\Source下的include文件复制到KEIL的工程目录下,再此之前要将portmacro.hFreeRTOSConfig.h复制到该文件(include)下。portmacro.h在FreeRTOS\Source\portable\RVDS\ARM_CM3下FreeRTOSConfig.h在FreeRTOSV7.2.0\FreeRTOS\Demo\CORTEX_STM32F103_Keil下FreeRTOSConfig.h是一个STM32对应的DEMO已经配置好了,如需修改请参照《配置FreeRTOS》,《配置FreeRTOS》详细的说明了FreeRTOSConfig.h里每个宏定义的意义,根据自己需要进行配置。

找到Project->options for target->C/C++->Include Paths

将头文件文件include包含到工程里面去。

3、修改启动文件startup_stm32f10x_hd.s(32内核中)

在050行在__heap_limit 下面添加:

PRESERVE8

THUMB

IMPORT xPortPendSVHandler

IMPORT xPortSysTickHandler

IMPORT vPortSVCHandler

跳到 076 行 DCD SVC_Handler 改成 DCD vPortSVCHandler

跳到079行 DCD PendSV_Handler成 DCD xPortPendSVHandler 跳到080行DCD SysTick_Handler 成DCD xPortSysTickHandler 4、在main.c的头部里#include一下头文件:FreeRTOS.h task.h queue.h

这样就移植完毕了。移植过程参考《在STM32中移植FreeRTOS》文档。

7 LWIP的移植

LWIP是TCP/IP协议栈的一个开放源代码实现,它由瑞士计算机科学院的Adam Dunkels 等开发,目的是减少内存使用率和代码空间大小,因此LWIP适用于运行在资源受限的嵌入式系统环境中。LWIP可以在几百字节或几十KB的RAM空间运行。LWIP既可以移植到操作系统上运行,也可以在无操作系统下独立运行。

LWIP具有如下特性:

LWIP在STM32上的底层驱动的移植

LWIP在STM32上的底层驱动的移植主要包括两方面:一、修改文件ethernetif.c,该文件

是连接LWIP协议栈和STM32网络驱动程序的桥梁,LWIP协议栈为开发者提供了ethernetif.c 的程序模板;二、编写STM32的网络驱动程序,实现网络底层的初始化、收发报文功能。

1、e thernetif.c文件的移植

ethernetif.c是LWIP协议栈和STM32网络驱动程序之间的接口,它主要包含ethernetif_init、ethernetif_input、low_level_init、low_level_input、low_level_output等函数。

1.1ethernetif_init函数

该函数是LWIP底层网络接口的初始化函数,指定了网络接口netif对应的主机名及网卡的描述,并指定了网卡的mac地址。同时,该函数还指定了netif的发送数据报文函数,并调用了网络底层驱动初始化函数low_level_init对网络底层进行初始化。(要根据自己选用的网卡调用网卡驱动)

1.2low_level_init函数

这个函数是网卡的初始化函数。虽然该函数代码量不大,但是它完成的工作量却不少。代码如下:

low_level_init(struct netif *netif)

{

// struct ethernetif *ethernetif = netif->state;

/* set MAC hardware address length */

netif->hwaddr_len = ETHARP_HWADDR_LEN;

/* set MAC hardware address */

netif->hwaddr[0] = mymac[0];

netif->hwaddr[1] = mymac[1];

netif->hwaddr[2] = mymac[2];

netif->hwaddr[3] = mymac[3];

netif->hwaddr[4] = mymac[4];

netif->hwaddr[5] = mymac[5];

/* maximum transfer unit */

netif->mtu = MAX_FRAMELEN;

if(ENC28J60_Init((u8*)mymac)) //初始化ENC28J60

{

return ERR_IF; //底层网络接口错误

}

//指示灯状态:0x476 is PHLCON LEDA(绿)=links status, LEDB(红)=receive/transmit

//PHLCON:PHY 模块LED 控制寄存器

ENC28J60_PHY_Write(PHLCON,0x0476);

/* device capabilities */

/* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */

netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;

return ERR_OK;

/* Do whatever else is needed to initialize interface. */

}

Low_level_init函数设定了网卡的物理地址和每帧最大传输数据字节数。

1.3ethernetif_input函数

该函数用于从底层物理网卡读取报文,并将该报文向上传递给LWIP协议栈函数ethernet_input进行处理。源代码如下:

void ethernetif_input(struct netif *netif)

{

// struct ethernetif *ethernetif;

struct eth_hdr *ethhdr;

struct pbuf *p;

// ethernetif = netif->state;

/* move received packet into a new pbuf */

p = low_level_input(netif);

/* no packet could be read, silently ignore this */

if (p == NULL) return;

/* points to packet payload, which starts with an Ethernet header */

ethhdr = p->payload;

switch (htons(ethhdr->type)) {

/* IP or ARP packet? */

case ETHTYPE_IP:

case ETHTYPE_ARP:

#if PPPOE_SUPPORT

/* PPPoE packet? */

case ETHTYPE_PPPOEDISC:

case ETHTYPE_PPPOE:

#endif /* PPPOE_SUPPORT */

/* full packet send to tcpip_thread to process */

if (netif->input(p, netif)!=ERR_OK)

{ LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));

pbuf_free(p);

p = NULL;

}

break;

default:

pbuf_free(p);

p = NULL;

break;

}

}

1.4low_level_input函数

该函数用于从内存中申请一个新的pbuf,并把接收到的数据报文内容拷贝至该pbuf 中。源代码如下:

static struct pbuf *

low_level_input(struct netif *netif)

{

// struct ethernetif *ethernetif = netif->state;

struct pbuf *p, *q;

u16_t len;

int rev_len=0;

/* Obtain the size of the packet and put it into the "len"

variable. */

len = ENC28J60_Packet_Receive(MAX_FRAMELEN,lwip_buf);

#if ETH_PAD_SIZE

len += ETH_PAD_SIZE; /* allow room for Ethernet padding */

#endif

/* We allocate a pbuf chain of pbufs from the pool. */

p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);

if (p != NULL) {

#if ETH_PAD_SIZE

pbuf_header(p, -ETH_PAD_SIZE); /* drop the padding word */

#endif

/* We iterate over the pbuf chain until we have read the entire

* packet into the pbuf. */

for(q = p; q != NULL; q = q->next) {

/* Read enough bytes to fill this pbuf in the chain. The

* available data in the pbuf is given by the q->len

* variable.

* This does not necessarily have to be a memcpy, you can also preallocate

* pbufs for a DMA-enabled MAC and after receiving truncate it to the

* actually received size. In this case, ensure the tot_len member of the

* pbuf is the sum of the chained pbuf len members.

*/

//read data into(q->payload, q->len);

memcpy((u8_t*)q->payload, (u8_t*)&lwip_buf[rev_len],q->len);

rev_len +=q->len;

}

// acknowledge that packet has been read();

#if ETH_PAD_SIZE

pbuf_header(p, ETH_PAD_SIZE); /* reclaim the padding word */

#endif

LINK_STATS_INC(link.recv);

} else {

//drop packet();

LINK_STATS_INC(link.memerr);

LINK_STATS_INC(link.drop);

}

return p;

}

1.5 low_level_output函数

该函数的功能是将pbuf中的数据帧通过底层发送函数ENC28J60_Packet_Send发送出去,由于要发送的数据可能被分割成多个pbuf,而这些pbuf通过pbuf->next指针连接起来,因此low_level_output函数需要使用foe循环将这些pbuf中的数据拷贝至当前的发送缓冲区中。源代码如下:

static err_t

low_level_output(struct netif *netif, struct pbuf *p)

{

// struct ethernetif *ethernetif = netif->state;

struct pbuf *q;

int send_len=0;

// initiate transfer();

#if ETH_PAD_SIZE

pbuf_header(p, -ETH_PAD_SIZE); /* drop the padding word */

#endif

for(q = p; q != NULL; q = q->next) {

/* Send the data from the pbuf to the interface, one pbuf at a

time. The size of the data in each pbuf is kept in the ->len

variable. */

//send data from(q->payload, q->len);

memcpy((u8_t*)&lwip_buf[send_len], (u8_t*)q->payload, q->len);

send_len +=q->len;

}

// signal that packet should be sent();

ENC28J60_Packet_Send(send_len,lwip_buf);

#if ETH_PAD_SIZE

pbuf_header(p, ETH_PAD_SIZE); /* reclaim the padding word */

#endif

LINK_STATS_INC(link.xmit);

return ERR_OK;

}

2、网卡驱动

本次用的是ENC28J60网卡模块。ENC28J60 是带有行业标准串行外设接口( Serial Peripheral Interface,SPI)的独立以太网控制器。它可作为任何配备有SPI 的控制器的以太网接口。ENC28J60 符合IEEE 802.3的全部规范,采用了一系列包过滤机制以对传入数据包进行限制。它还提供了一个内部 DMA 模块,以实现快速数据吞吐和硬件支持的IP校验和计算。与主控制器的通信通过两个中断引脚和SPI 实现,数据传输速率高达10Mb/s。

ENC28J60由七个主要功能模块组成:

1、SPI接口——充当主控制器和ENC28J60之间通信通道。

2、控制寄存器|——用于控制和监视ENC28J60.

3、双端口RAM缓冲器——用于接收和发送数据包。

4、判优器——当DMA、发送和接收模块发出请求是对RAM缓冲器的访问进行控制。

5、总线接口——对通过SPI接收的数据和命令进行解析。

6、MAC(Medium Access control)模块——实现符合IEEE802.3标准的MAC逻

辑。

7、PHY(物理层)模块——对双绞线上的模拟数据进行编码和译码。该期间还包

括其他支持模块,诸如振荡器、片内稳压器、电平变换器(提供可以接受5V

电压的I/O引脚)和系统控制逻辑。

enc28j60.c:ENC28J60(以太网芯片)SPI的接口应用函数库。

ENC28J60_Reset(void)这个函数里SPI的硬件初始化、设置SPI的时

钟SCK的频率和复位ENC28J60.

ENC28J60_Read_Op(u8 op,u8 addr)读取ENC28J60寄存器(带操作

码)op:操作码addr:寄存器地址/参数返回值:读到的数据。

ENC28J60_Write_Op(u8 op,u8 addr,u8 data)读取ENC28J60寄存器

(带操作码)op:操作码addr:寄存器地址data:参数

返回值:无返回值(函数为Void类型的)。

ENC28J60_Read_Buf(u32 len,u8* data)读取ENC28J60接收缓存数据

len:要读取的数据长度data:输出数据缓存区(末尾自动添加结束符)

返回值:无返回值(函数为Void类型的)。

ENC28J60_Write_Buf(u32 len,u8* data)向ENC28J60写发送缓存数

据len:要写入的数据长度data:数据缓存区

返回值:无返回值(函数为Void类型的)。

ENC28J60_Set_Bank(u8 bank)设置ENC28J60寄存器Bank

bank:要设置的bank 返回值:无返回值(函数为Void类型的)。

ENC28J60_Read(u8 addr)读取ENC28J60指定寄存器

addr:寄存器地址返回值:读到的数据

ENC28J60_Write(u8 addr,u8 data)向ENC28J60指定寄存器写数据

addr:寄存器地址data:要写入的数据

返回值:无返回值(函数为Void类型的)。

ENC28J60_PHY_Write(u8 addr,u32 data)向ENC28J60的PHY寄存器

写入数据addr:寄存器地址data:要写入的数据

返回值:无返回值(函数为Void类型的)。

ENC28J60_Init(u8* macaddr)初始化ENC28J60

macaddr:MAC地址返回值:0,初始化成功;1,初始化失败;

ENC28J60_Get_EREVID(void)在EREVID 内也存储了版本信息。

EREVID 是一个只读控制寄存器,包含一个5 位标识符,用来标识器

件特定硅片的版本号

ENC28J60_Packet_Send(u32 len,u8* packet)通过ENC28J60发送数

据包到网络len:数据包大小packet:数据包

返回值:无返回值(函数为Void类型的)。

ENC28J60_Packet_Receive(u32 maxlen,u8* packet)从网络获取一个

数据包内容maxlen:数据包最大允许接收长度

packet:数据包缓存区返回值:收到的数据包长度(字节)

基于FreeRTOS的LWIP协议栈移植

在FreeRTOS操作系统下的LWIP任务模型:

1 操作系统模拟层文件sys_arch.c的移植

在LWIP中,操作系统模拟层是LWIP协议栈的一部分,它存在的目的是方便将LWIP移植到各种不同的操作系统上,它为操作系统和LWIP协议栈之间提供一个接口桥梁,当用户移植LWIP到一个新的操作系统的时候,只需要修改操作系统模拟层内的各函数即可。Sys_arch.txt文件给出了详细说明。总的来说,操作系统模拟层主要完成了与信号量、消息邮箱机制、线程相关的功能。

看如下三个类型定义:

typedef xSemaphoreHandle sys_sem_t;//在LWIP中信号量使用这个类型定义

typedef xQueueHandle sys_mbox_t;//在LWIP中队列消息使用这个类型定义

typedef xTaskHandle sys_thread_t;//在LWIP中线程使用这个类型定义

1.sys_mbox_new函数

该函数的功能是使用FreeRTOS提供的消息队列机制创建一个空的消息队列。在FreeRTOS中,消息队列创建函数是xQueueCreate。创建的邮箱大小由sys_arch.h中的宏定义archMESG_QUEUE_LENGTH实现。具体代码如下:

err_t sys_mbox_new(sys_mbox_t *mbox, int size)

{

(void ) size;

*mbox = xQueueCreate( archMESG_QUEUE_LENGTH, sizeof( void * ) );

#if SYS_STATS

++lwip_https://www.doczj.com/doc/ca17134672.html,ed;

if (lwip_stats.sys.mbox.max < lwip_https://www.doczj.com/doc/ca17134672.html,ed) {

lwip_stats.sys.mbox.max = lwip_https://www.doczj.com/doc/ca17134672.html,ed;

}

#endif /* SYS_STATS */

if (*mbox == NULL)

return ERR_MEM;

return ERR_OK;

}

2.sys_mbox_free函数

该函数功能与sys_mbox_new相反,它用于删除一个队列。该队列中还有未被取出的消息时,该函数应当报错,并通知应用程序。代码如下:

void sys_mbox_free(sys_mbox_t *mbox)

{

if( uxQueueMessagesWaiting( *mbox ) )

{

/* Line for breakpoint. Should never break here! */

portNOP();

#if SYS_STATS

lwip_stats.sys.mbox.err++;

#endif /* SYS_STATS */

// TODO notify the user of failure.

}

vQueueDelete( *mbox );

#if SYS_STATS

--lwip_https://www.doczj.com/doc/ca17134672.html,ed;

#endif /* SYS_STATS */

}

3.sys_mbox_post函数

该函数用于将消息发送至消息队列中。该函数是一个阻塞函数。当消息被发送至队列后,该函数才退出阻塞状态。代码如下:

void sys_mbox_post(sys_mbox_t *mbox, void *data)

{

while ( xQueueSendToBack(*mbox, &data, portMAX_DELAY ) != pdTRUE ){} }

4.sys_mbox_trypost函数

该函数用于尝试将某个消息发送至消息队列中,当消息被成功投递后,则返回成功,否则返回失败。代码如下:

err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)

{

err_t result;

if ( xQueueSend( *mbox, &msg, 0 ) == pdPASS )

{

result = ERR_OK;

}

else {

// could not post, queue must be full

result = ERR_MEM;

#if SYS_STATS

lwip_stats.sys.mbox.err++;

#endif /* SYS_STATS */

}

return result;

}

5.sys_arch_mbox_fetch函数

相关主题
文本预览
相关文档 最新文档