LWIP+UCOSIII学习笔记
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
1.网络芯片比较
目前使用的网络芯片一般有以下几种:DP83848、DM9000、enc28j60、RLD8019、w5100
网卡工作在osi的最后两层,物理层(PHY)和数据链路层(MAC)。
物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。
物理层的芯片称之为PHY。
数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能。
以太网卡中数据链路层的芯片称之为MAC 控制器。
1.DP83848:物理层(PHY),跟MII接口。
2. DM9000:物理层(PHY)和数据链路层(MAC)(10/100M)。
跟8/16/32总线接口
3. enc28j60:MAC+PHY(10M Base T)。
spi接口
4. w5100:硬件TCP/IP协议栈+MAC+PHY(10/100M Base T)。
并行总线接口
5. RLD8019:和w5100类似,比较老。
举个例子:
W5100里面用硬件逻辑电路实现了TCP/IP的协议栈结构,不需要向ENC28J60这样的网络控制器那样还需要一个资源较大的MCU跑软件协议栈。
你直接把W5100当外部RAM使用,MCU初始化一下I/O,寄存器等就能使用了。
2.TCP/IP协议族的四个层次
网络协议通常分不同层次进行开发,每一层分别负责不同的通信功能。
一个协议族,比如TCP/IP,是一组不同层次上的多个协议的组合。
TCP/IP通常被认为是一个四层协议系统,如下图所示:
每一层负责不同的功能:
1) 链路层,有时也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。
它们一起处理与电缆(或其他任何
传输媒介)的物理接口细节。
2) 网络层,有时也称作互联网层,处理分组在网络中的活动,例如分组的选路。
在TCP/IP协议族中,网络层协议包括I P协议(网际协议),ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)。
3 ) 运输层主要为两台主机上的应用程序提供端到端的通信。
在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。
TCP为两台主机提供高可靠性的数据通信。
它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。
由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。
而另一方面,UDP则为应用层提供一种非常简单的服务。
它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。
任何必需的可靠性必须由应用层来提供。
这两种运输层协议分别在不同的应用程序中有不同的用途,这一点将在后面看到。
4 ) 应用层负责处理特定的应用程序细节。
几乎各种不同的TCP/IP实现都会提供下面这些通用的应用程序:
• Telnet 远程登录。
• FTP 文件传输协议。
• SMTP 简单邮件传送协议。
• SNMP 简单网络管理协议。
另外还有许多其他应用,在后面章节中将介绍其中的一部分。
假设在一个局域网(LAN)如以太网中有两台主机,二者都运行FTP协议,如下图列出了该过程所涉及到的所有协议。
3.LWIP协议
随着嵌入式系统功能的多样化以及网络在各个领域的中的广泛应用,具备网络功能的嵌入式设备拥有更高的使用价值和更强的通用性。
然而大部分嵌入式设备使用经济型处理器,受内存和速度限制,资源有限,不需要也不可能完整实现所有的TCP/IP协议,有时只需要满足实际需求就行。
LwIP是由瑞典计算机科学研究院开发的轻量型TCP/IP协议栈,其特点是保持了以太网的基本功能,通过优化减少了对存储资源的占用。
LwIP是免费、开源的,任何人可以使用,能够在裸机的环境下运行,当然设计的时候也考虑了将来的移植问题,可以很容易移植到多任务操作系统中。
LwIP含义是light weight(轻型)IP协议,在实现时保持了TCP协议的主要功能基础上减少对RAM的占用,一般它只需要几十K的RAM和40K左右的ROM
就可以运行,这使LwIP协议栈很适合在低端嵌入式系统中使用。
LwIP协议栈的设计才用分层结构的思想,每一个协议都作为一个模块来实现,提供一些与其它协议的接口函数。
所有的TCP/IP协议栈都在一个进程当中,这样TCP/IP协议栈就和操作系统内核分开了。
而应用程序既可以是单独的进程也可以驻留在TCP/IP进程中,它们之间利用ICP机制进行通讯。
如果应用程序是单独的线程可以通过操作系统的邮箱、消息队列等,与协议栈进程通讯。
如果应用程序驻留在协议栈进程中,则应用程序可以通过内部回调函数和协议栈进程通讯。
3.1 LwIP源代码文件目录
3.1.1目录
root@motadou:/home/motadou/lwip/lwip-1.4.1# tree . ├── CHANGELOG
├── COPYING
├── doc
│├── contrib.txt
│├── FILES
│├── rawapi.txt
│├── savannah.txt
│├── snmp_agent.txt
│└── sys_arch.txt
├── FILES
├── README
├── src
│├── api
││├── api_lib.c
││├── api_msg.c
││├── err.c
││├── netbuf.c
││├── netdb.c
││├── netifapi.c
││├── sockets.c
││└── tcpip.c
│├── core
││├── def.c
││├── dhcp.c
││├── dns.c
││├── init.c
││├── ipv4
│││├── autoip.c
│││├── icmp.c
│││├── igmp.c
│││├── inet.c
│││├── inet_chksum.c │││├── ip_addr.c
│││├── ip.c
│││└── ip_frag.c
││├── ipv6
│││├── icmp6.c
│││├── inet6.c
│││├── ip6_addr.c
│││├── ip6.c
│││└── README
││├── mem.c
││├── memp.c
││├── netif.c
││├── pbuf.c
││├── raw.c
││├── snmp
│││├── asn1_dec.c
│││├── asn1_enc.c
│││├── mib2.c
│││├── mib_structs.c
│││├── msg_in.c
│││└── msg_out.c
││├── stats.c
││├── sys.c
││├── tcp.c
││├── tcp_in.c
││├── tcp_out.c
││├── timers.c
││└── udp.c
│├── FILES
│├── include
││├── ipv4
│││└── lwip
│││├── autoip.h
│││├── icmp.h
│││├── igmp.h
│││├── inet_chksum.h │││├── inet.h
│││├── ip_addr.h
│││├── ip_frag.h
│││└── ip.h
││├── ipv6
│││└── lwip
│││├── icmp.h
│││├── inet.h
│││├── ip_addr.h
│││└── ip.h
││├── lwip
│││├── api.h
│││├── api_msg.h
│││├── arch.h
│││├── debug.h
│││├── def.h
│││├── dhcp.h
│││├── dns.h
│││├── init.h
│││├── mem.h
│││├── memp.h
│││├── memp_std.h
│││├── netbuf.h
│││├── netdb.h
│││├── netifapi.h
│││├── netif.h
│││├── opt.h
│││├── pbuf.h
│││├── raw.h
│││├── sio.h
│││├── snmp_asn1.h
│││├── snmp.h
│││├── snmp_msg.h
│││├── snmp_structs.h │││├── sockets.h
│││├── stats.h
│││├── sys.h
│││├── tcp.h
│││├── tcp_impl.h
│││├── tcpip.h
│││├── timers.h
│││└── udp.h
││├── netif
│││├── etharp.h
│││├── ppp_oe.h
│││└── slipif.h
││└── posix
││├── netdb.h
││└── sys
││└── socket.h
│└── netif
│├── etharp.c
│├── ethernetif.c
│├── FILES
│├── ppp
││├── auth.h
││├── chap.c
││├── chap.h
││├── chpms.c
││├── chpms.h
││├── fsm.c
││├── fsm.h
││├── ipcp.c
││├── ipcp.h
││├── lcp.c
││├── lcp.h
││├── magic.c
││├── magic.h
││├── md5.c
││├── md5.h
││├── pap.c
││├── pap.h
││├── ppp.c
││├── pppdebug.h
││├── ppp.h
││├── ppp_impl.h
││├── ppp_oe.c
││├── randm.c
││├── randm.h
││├── vj.c
││└── vj.h
│└── slipif.c
├── test
│└── unit
│├── core
││├── test_mem.c
││└── test_mem.h
│├── etharp
││├── test_etharp.c ││└── test_etharp.h │├── lwip_check.h
│├── lwipopts.h
│├── lwip_unittests.c
│├── tcp
││├── tcp_helper.c
││├── tcp_helper.h
││├── test_tcp.c
││├── test_tcp.h
││├── test_tcp_oos.c
││└── test_tcp_oos.h
│└── udp
│├── test_udp.c
│└── test_udp.h
├── tt.xtx
└── UPGRADING
24 directories, 151 files
3.1.2 api文件夹源代码结构
为方便用户编程,LwIP为用户提供两种简单的高级API接口:协议栈
sequentialAPI和socket API。
这两种API实现的原理都是通过引进邮箱和信号
量等通信与同步机制,来实现对内核中raw/callback API函数的封装和调用。
也就是说,要使用这两种API,必须基于底层操作系统提供的邮箱和信号量机制,
必须要在开发板上移植好操作系统。
文件说明
api_lib.c 包含sequential API函数的实现,主要包含预留给用户的编程接口
api_msg.c 包含sequential API函数的实现,主要包含API消息的封装和处理函数netbuf.c 包含上层数据包管理函数的实现
netdb.c 包含与主机名字转换相关的函数,主要在socket中被使用到
netifapi.c 包含上层网络接口管理函数的实现
sockets.c 包含socket API函数的实现
tcpip.c 提供了上层API与协议栈内核交互的函数,它是整个上层API功能得以实现的一个枢纽,其实现的功能可以理解为:从API函数处接收消息,然后将消息递交给内核函数,内核函数根据消息做出相应的处理。
3.1.3netif文件夹源代码结构
3.1.4 core文件夹源代码结构
3.2 LwIP协议栈开发嵌入式网络的三种方法分析
轻量级的TCP/IP协议栈LwIP,提供了三种应用程序设计方法,且很容易被移植到多任务的操作系统中。
LwIP协议栈在设计的时候就考虑到了将来的移植问题,因此把所有与硬件、操作系统、编译器有关的部分都全部独立起来,形成了一个操作系统模拟层。
操作系统模拟层用进程间的信号量、邮箱机制处理通信
问题,而μC/OS是一个基于任务调度的嵌入式实时操作系统,因此移植LwIP
协议栈到μC/OS,是很容易实现的。
LwIP提供了三种应用程序接口:
(1)低水平的,基于内核/回调函数的API(后面称 RAW API)
(2)高水平的,连续的API(后面称LwIP API)
(3) BSD风格的套接字API(后面称BSD socket)
可以在协议栈中通过对宏定义的不同配置,来决定使用哪种方式。
其中BSD socket方式不是很成熟,RAW API需要编写回调函数,协议栈推荐使用LwIP API 这种方式,但是三种方式到了底层都是通过回调函数实现的。
本文直接从RAW API 入手,以建立TCP服务器端通信为例,详述底层的调用,然后再讲述后面的两种是如何封装而成的。
3.2.1基于RAW API的应用程序设计步骤
使用RAW API进行TCP/IP编程,可以使应用程序的代码和协议栈的代码很好地结合起来。
程序的执行机制是以回调函数为基础的事件驱动的,同时回调函数也是被TCP/IP代码直接调用的,回调函数、数据发送函数都需要自己编写。
这种方式是唯一的一种支持设备裸机运行,又可以完成网络通信完成系统功能。
裸机运行实际相当于是一个线程,而协议栈代码和应用程序代码通过先后次序处理,完成数据流转。
图1是使用RAW API方式,多任务系统实现TCP服务器端通信的步骤。
图1 RAW API方式应用程序设计
LwIP协议栈中的tcp块结构有两种TCP_PCB和TCP_PCB_LISTEN,前者在内
存池中的默认个数是5,后者是8,其中listen型的结构占用少量的内存,专门用于处理在侦听状态的tcp块结构。
tcp_listen函数中,释放tcp_new创建的块结构,而是返回一个listen型的tcp块结构。
客户端连接,到达TCP层,在tcp_listen_input函数中,重新创建一个TCP_PCB块结构,专门用于和客户端通信。
侦听到客户端连接,完成三次握手后,回调自己编写的接收函数,然后将全局的指针指向与客户端通信的块结构,在数据发送时,使用这个指针,就是在用这个块结构与客户端通信。
由上面看出,这种方式最大的特点是减少了任务之间的切换,只要数据来到协议栈线程,通过回调的方式就可以完成数据的处理。
3.2.2 基于LwIP API的应用程序设计
LwIP API方式的编程,是基于上面的RAW API的,封装了一个netconn的结构,所有操作不在针对TCP块结构,而变成了netconn型的结构变量。
操作都需要协议栈去处理,应用程序与协议栈通信,通过发送消息方式进行,因此这种方式会造成频繁的任务切换,速度相比RAW API慢了许多,使用步骤如图2所示。
图2 LwIP API方式应用程序设计
3.2.3基于BSD socket的应用程序设计
BSD socket相当于对LwIP API做了一层封装,而netconn结构有一个变量是socket,这样两者很容易结合起来。
Socket方式很容易被理解,编写应用程序也较为容易,但是效率低,消耗的资源更多,使用步骤如图3所示。
图3 BSD socket方式应用程序设计3.3 LwIP-API函数
3.3.1 Raw API functions
3.3.2 UDP Raw API functions
API function Description
udp_new Creates a new UDP PCB.
udp_remove Removes and de-allocates a UDP PCB.
udp_bind Binds a UDP PCB with a local IP address and port.
udp_connect Sets up a UDP PCB remote IP address and port.
udp_disconnect Removes a UDP PCB remote IP and port.
udp_send Sends UDP data.
udp_recv Specifies a callback function which is called when a datagram is received.
3.3.3 Netconn API functions
3.3.4 Socket API functions
3.3.5 Pbuf API functions
API function Description
pbuf_alloc Allocates a new pbuf.
pbuf_realloc Resizes a pbuf (shrink size only).
pbuf_ref Increments the reference count field of a pbuf.
pbuf_free Decrements the pbuf reference count. If it reaches zero, the pbuf is deallocated.
pbuf_clen Returns the count number of pbufs in a pbuf chain.
pbuf_cat Chains two pbufs together (but does not change the reference count of the tail pbuf chain). pbuf_chain Chains two pbufs together (tail chain reference count is incremented).
pbuf_dechain Unchains the first pbuf from its succeeding pbufs in the chain.
pbuf_copy_partial Copies (part of) the contents of a packet buffer to an application supplied buffer.
pbuf_take Copies application supplied data into a pbuf.
pbuf_coalesce Creates a single pbuf out of a queue of pbufs.
4.ModbusTCP协议4.1 TCP与RTU数据帧的区别
4.2 ModbusTCP报文传输服务结构
5.裸机运行LWIP
参考《零死角玩转STM32-V2.pdf》的29章。
网站中可以下载到lwip-1.3.2.zip 文件和相应版本的contrib-1.3.0.zip 文件,它们包含了LwIP 协议栈的核心源代码及应用、移植例程。
在src 目录下的
api 文件夹保存的文件中,包含了适用于具有操作系统平台调用的应用层接口函数。
core文件夹下的文件内容为LwIP 协议栈对于各种协议的实现。
netif 下的文件则保存了与硬件底层关系比较紧密的函数。
这三个文件夹下的都是c 源文件,它们的头文件都被保存到include 文件夹中。
在实际应用中,可根据需要进行裁剪。
如下图所示,与LWIP原文件lwip-1.3.2.zip的src比较,只有ethernetif.c 文件被修改,该文件包含了与以太网网卡密切相关的初始化、发送、接收等函数的实现。
这个文件夹中的函数并不能使用,它们都是一个框架性的结构,移植者
需要根据自己使用的网卡特性来完成这些函数。
在移植的时候,我们也常常需要利用contrib-1.3.0.zip 中的文件。
解压后,在\contrib-1.3.0_\contrib\ports 目录下有一些针对特定平台移植时使用的文件。
我们需要用到的是cc.h、perf.h 和sys_arch.h 文件,通常把它复制出来存放到自己工程中的arch 文件夹中。
cc.h 包含了LwIP 对于基本数据类型的定义。
sys_arch.h 定义了与系统有关的信号量、邮箱及线程。
6.UCOSIII+LWIP 1.4.0
6.1 LwIP的移植过程
LwIP的移植主要涉及两个方面:操作系统模拟层和硬件驱动层。
LwIP在设计时已考虑到在不同操作系统中的可移植性,其内部使用的函数和数据结构均为抽象定义。
开发者可根据不同的操作系统要求来具体实现相关的函数和数据结构。
同时,硬件相关的驱动同样预留了接口,开发者可针对实际使用情况编写网络控制芯片驱动函数。
另外,对不同的编译环境,开发者还需要编写部分头文件定义相关数据结构和宏。
LwIP在μC/OS-Ⅲ嵌入式系统中的结构如图1所示,其中的箭头框为移植工作需要实现的模块。
6.1.1 操作系统模拟层的编写
6.1.1.1编写头文件cc.h
cc.h文件中包含处理器相关的变量类型、数据结构及字节对齐的相关宏。
LwIP中使用的基本变量类型均以位数进行命名,为抽象的变量定义,开发者需要根据所用处理器具体定义。
基本变量的定义有两种方法:一种是将变量直接定义为C语言的基本类型,如unsigned char、int等;另一种是将变量定义为操作系统内对应的抽象变量。
当使用操作系统时,应采用第二种方法。
该方法的优点是变量对于处理器是“透明”的,应用程序更换硬件平台时无需修改操作系统模拟层内的定义。
μC/OS-Ⅲ中对基本变量的定义在cpu.h文件中,均以CPU为命名前缀。
对于这些变量在μC/OS-Ⅲ中具体如何定义本文不做讨论。
LwIP 要求定义8 bit、16 bit、32 bit和内存指针型变量:
typedef CPU_INT08U u8_t;
typedef CPU_INT08S s8_t;
typedef CPU_INT16U u16_t;
typedef CPU_INT16S s16_t;
typedef CPU_INT32U u32_t;
typedef CPU_INT32S s32_t;
typedef CPU_INT32U mem_ptr_t;
由于ARM处理器的编译环境默认对变量存储采取4 B对齐方式,而以太网数据包等结构体要求处理器按照变量的实际大小存储和访问,因此,需要定义相关的结构封装宏,使得结构体内的成员变量不以4 B对齐的方式进行存储。
6.1.1.2编写头文件sys_arch.h
sys_arch.h文件要求定义操作系统相关的数据结构和宏。
LwIP多线程功能需要信号量和邮箱等结构体,用于多个任务的同步和消息的传递。
μC/OS-Ⅲ中的信号量OS_SEM和消息队列OS_Q可实现相应的功能。
LwIP 1.4.0版本中使用了互斥信号量管理共享的资源,而有些嵌入式操作系统中不包含互斥信号量的变量类型。
为了适应不同的操作系统,LwIP定义了宏LWIP_COMPAT_MUTEX。
LWIP_COMPAT_MUTEX的值定义为1,则LwIP使用二值信号量代替互斥信号量以及相关的功能函数。
虽然μC/OS-Ⅲ包含了互斥信号量
OS_MUTEX,但LwIP中两种数据结构可相互替换,选择使用二值信号量可以减少一定的移植工作。
#define LWIP_COMPAT_MUTEX 1
typedef OS_SEM sys_sem_t;
typedef OS_Q sys_m box_t;
LwIP中包含有必须完整执行而不可被打断的代码,因此需要使用临界段代码保护的功能。
μC/OS-Ⅲ中提供了关闭中断和锁定调度器两种临界段代码保护方法。
LwIP中的临界段代码保护宏可直接定义为μC/OS-Ⅲ关闭中断的对应临界段代码保护宏。
#define SYS_ARCH_DECL_PROTECT() CPU_SR_ALLOC()
#define SYS_ARCH_PROTECT() OS_CRITICAL_ENTER()
#define SYS_ARCH_UNPROTECT() OS_CRITICAL_EXIT()
6.1.1.3编写源文件sys_arch.c
sys_arch.c文件要求实现操作系统模拟层的接口函数,主要包括对信号量和邮箱等数据结构的操作以及LwIP线程的操作。
LwIP的信号量用于进程间的通信,相关操作主要包括以下几个函数:
sys_sem_new() //新建信号量
sys_sem_free() //释放信号量
sys_sem_signal() //发送信号量
sys_arch_sem_wait() //阻塞进程,等待指定信号量
sys_sem_valid() //检查信号量可用性
sys_sem_set_invalid() //设置信号量不可用
LwIP的邮箱用于缓存和传递数据报文,相关操作主要包括以下几个函数:
sys_mbox_new() //新建邮箱
sys_mbox_free() //删除邮箱
sys_mbox_post() //阻塞进程,投递消息至邮箱
sys_mbox_trypost() //投递消息至邮箱,仅一次操作
sys_arch_mbox_fetch() //阻塞进程,从邮箱中提取消息
sys_arch_mbox_tryfetch() //从邮箱中提取消息,仅一次操作
sys_mbox_valid() //检查邮箱可用性
sys_mbox_set_invalid() //设置邮箱不可用
LwIP使用了μC/OS-Ⅲ中的信号量OS_SEM和消息队列OS_Q结构,以上函数的实现调用了μC/OS-Ⅲ的操作函数,包括OS?Create()、OS?Del()、OS?Post()和OS?Pend()。
在实现sys_?_new()和sys_?_free()函数时,需加入临界段代码保护以确保OS?Create()和OS?Del()在执行时不被打断,可避免出现系统资源管理错误。
err_t sys_?_new(……)
{
OS_ERR err;
CPU_SR_ALLOC(); //临界代码保护开始
CPU_CRITICAL_ENTER();
OS?Create(……,&err);
CPU_CRITICAL_EXIT(); //临界代码保护结束
return err;
}
void sys_?_free(……)
{
OS_ERR err;
CPU_SR_ALLOC(); //临界代码保护开始
CPU_CRITICAL_ENTER();
OS?Del(……, &err );
CPU_CRITICAL_EXIT(); //临界代码保护结束 }
LwIP 1.4.0版本新添加了sys_?_valid()和sys_?_set_invalid(),这两个函数的实现无需调用操作系统内部的函数,可由开发者根据实际需求实现。
另外,二值信号量替换了互斥信号量,相关的操作函数也无需在此文件内实现。
在LwIP内核中的sys.h文件给出了详细的宏定义:
#if LWIP_COMPAT_MUTEX
#define sys_mutex_t sys_sem_t
#define sys_mutex_new(mutex) sys_sem_new(mutex, 1)
#define sys_mutex_lock(mutex) sys_sem_wait(mutex)
#define sys_mutex_unlock(mutex) sys_sem_signal(mutex)
#define sys_mutex_free(mutex) sys_sem_free(mutex)
#define sys_mutex_valid(mutex) sys_sem_valid(mutex)
#define sys_mutex_set_invalid(mutex) \
sys_sem_set_invalid(mutex)
LwIP建立新进程的接口函数sys_thread_new()要求成功建立一个任务并返回任务优先级。
μC/OS-Ⅲ中加入了时间片轮转调度功能,使得同一优先级可建立多个任务,避免了优先级重复导致任务建立失败的情况。
相比于使用
μC/OS-Ⅱ或其他不支持同级任务的操作系统,sys_thread_new()的实现仅调用OSTaskCreate()即可,省略了一些查找可用优先级的容错操作。
操作系统模拟层的初始化函数sys_init()由开发者根据实际情况进行
编写,没有固定的规范要求。
该函数可不执行任何操作,但必须在文件内实现。
6.1.2 硬件驱动层的编写
LwIP内核文件中给出了驱动文件的参考模板ethernetif.c,开发者可根据其模板的架构结合实际使用的网络控制芯片来编写驱动。
以low_level为前缀的函数均为网络控制芯片相关的接口函数,主要包含初始化、接收、发送等操作。
以太网控制器模块包含了MAC层和物理层,有别
于传统的MCU+PHY芯片的结构。
因此,实现驱动函数时可直接对相应的寄存器进行操作,无需再次封装PHY芯片的操作函数。
以ethernetif为前缀的函数要求开发者实现底层硬件与上层协议间的接口函数,包括底层设备描述结构体的相关操作、LwIP主线程和以太网中断服务函数等。
static void low_level_init(struct netif *netif) //底层硬件初始化
static err_t low_level_output(struct netif *netif, struct pbuf *p) //底层硬件发送数据函数
static struct pbuf * low_level_input(struct netif *netif) //底层硬件接收数据函数
err_t ethernetif_input(struct pbuf *pp,struct netif *netif) //硬件抽象层接收数据
err_t ethernetif_init(struct netif *netif) //硬件抽象层初始化
6.1.3 LWIP功能裁剪和定制
opt.h文件里面包含了LWIP的模块选项,可以在这里选择哪些模块需要编译,那些模块不编译,分成几个部分,mem,arp,icmp,igmp,ppp,dhcp等,这里可以根据自己的需要修改编译选项,如果是带操作系统的,还要修改栈空间,优先级之类的选项。
LwIP为开发者提供了一个功能定制的接口文件lwipopt.h,可根据系统实际需求定义宏的值、裁剪功能和配置参数。
例如使用TCP和UDP功能,则需添加下列定义:
#define LWIP_TCP 1
#define LWIP_UDP 1
内核文件opt.h是lwipopt.h的设计模板,包含了所有LwIP功能配置的宏。
opt.h文件对宏定义均采用了#ifndef预编译判断,当开发者在lwipopt.h 中没有对某个宏给出定义时,该文件会定义一个默认值。
虽然修改opt.h中的宏定义和在lwipopt.h中编写宏定义均可实现剪裁和定制LwIP的功能,但由于修改内核文件会破坏协议栈的封装性,为今后的应用程序移植和维护造成隐患,所以开发者不应直接修改opt.h内的宏定义。
开发者在编写lwipopt.h时,由于每个宏的默认值并不能保证LwIP的正确运行,所以应对opt.h中给出的所有宏进行定义。
例如opt.h中对于TCP的一
些宏定义如下:
#ifndef TCPIP_THREAD_STACKSIZE
#define TCPIP_THREAD_STACKSIZE 0
#endif
#ifndef TCPIP_MBOX_SIZE
#define TCPIP_MBOX_SIZE
#endif
LwIP默认的TCPIP进程堆栈空间为0,TCPIP使用的邮箱空间为0。
若开发者在lwipopt.h中不对这些宏进行定义,当tcpip_init()对LwIP进行初始化时,就会出现错误致使LwIP无法正确运行。
6.2 测试
上面几个文件都实现完了,那基本上操作系统部分就差不多了,我的建议是,在opt.h和cc.h修改完以后,你就开始编译,如果有错误几乎都应该是没有include某个文件造成的,及时修改错误,然后直到编译得没有错误了,再来添加sys_arch.c,这样可以减少一些麻烦。
总得来说就是,首先,你要修改opt.h,把你需要的模块添加上去,把栈空间,mailbox大小之类的定义好,然后编译,因为LWIP是标准的C写的,理论上应该很容易编译成功,我只是有两个地方没有.h文件,添加一下就编译过了。
当然,编译过了只是第一步,完了以后就要手写sys_arch.c了,sys.h里面的所有函数都要实现,当然,你要偷懒的话有些也可以直接返回。
这个文件是和你系统API相关的,写好以后记得加入到Makefile里面,然后再编译,找错误,编译,直到成功。
其实还有临界保护,主要是
#define SYS_ARCH_DECL_PROTECT(lev)
#define SYS_ARCH_PROTECT(lev)
#define SYS_ARCH_UNPROTECT(lev)
这三个宏,定义成你的系统的临界保护调用就行了,我偷懒定义的空的,暂时没发现问题。
测试任务首先初始化底层硬件和协议栈,包括使能以太网硬件模块和中断、调用协议栈内核初始化函数tcpip_init()、初始化网络接口的结构体。
void My_LwIP_Init(void)
{
/* 调用StellarisWare库函数进行硬件初始化 */
……
/* 调用内核初始化函数 */
tcpip_init();
/* 初始化netif,设置本机的IP、子网掩码、网关,绑定netif的回调函数 */ netif_add(……);
netif_set_default(……);
netif_set_up(……);
}
第二步是初始化客户端。
首先创建一个网络连接结构体,再将其绑定至端口并连接到指定的服务器。
void TCP_Client_Init(void)
{
pstNetconn = netconn_new(NETCONN_TCP);//新建连接
netconn_bind(……);//绑定端口
netconn_connect(……);//连接主机
}
任务的主循环中调用了LwIP具有进程阻塞功能的函数netconn_recv()以接收来自主机的数据。
若数据接收正确,则将数据发送回主机端的PC;若接收不正确,则删除当前的连接,重新连接到主机。
while(1)
{
err = netconn_recv(……); // 接收数据
if(err == ERR_OK) // 数据正确
{
netconn_write(……); // 发送数据
netbuf_delete(……); // 删除数据缓冲区
}
else
{
netconn_delete(……); // 删除当前连接
/* 重新连接 */
……
}
}
6.3 执行过程
从上面的图中可以清晰的看出两部分API函数之间的交互过程,以及应用程序和内核函数之间的交互过程。
API函数netconn_xxx在文件api_lib.c中,而API实现的另一部分函数do_xxx在api_msg.c中。
接下来的一节我们将精力集中在api_lib.c中的各个函数,而不去过多关心api_msg.c中的各个do_xxx是怎样与内核函数交互完成相关工作的。
netconn_xxx函数在LwIP说明文档16小节也有相关的描述,这里打算再重复的描述下各个函数的功能以及它们的实现过程。
6.3.1 ethernetif_input()函数
通过定时查询或网卡的接收中断调用ethernetif_input(struct netif *netif),推荐使用网卡接收中断的方法,定时查询可能会有漏帧的可能;
6.3.2 tcpip_input ()函数
ethernetif_input(struct netif *netif)函数判断有数据,则调用netif->input(p, netif)函数,该函数在执行netif_add函数时已关联到tcpip_input(struct pbuf *p, struct netif *inp)函数;
6.3.3 tcpip_thread ()函数
tcpip_thread进程在进行tcpip_init(NULL,NULL)时创建,创建后一直等待消息;
static void tcpip_thread(void *arg)
{
struct tcpip_msg *msg;
sys_timeout(IP_TMR_INTERVAL, ip_reass_timer, NULL);//创建IP分片重装超时事件 sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);// 创建ARP超时事件
while (1) { // 进程循环
sys_mbox_fetch(mbox, (void *)&msg); // 阻塞在邮箱上接收要处理的消息
switch (msg->type) { // 判断消息类型
case TCPIP_MSG_API: // 若是API消息,调用消息内部的function函数
msg->msg.apimsg->function(&(msg->msg.apimsg->msg));
break;
case TCPIP_MSG_INPKT:// 若是接收到IP层递交的数据包
if (msg->if->flags & NETIF_FLAG_ETHARP) // 支持ARP
ethernet_input(msg->msg.inp.p, msg->if);//先进行ARP 处
//理,再判断是否递交IP层处理
else //否则直接递交给IP层
ip_input(msg->msg.inp.p, msg->if);
memp_free(MEMP_TCPIP_MSG_INPKT, msg);
break;。
default: break;
}// switch
} // while
}//
0] = 0xD3
7.lwip-内存管理
7.1内存管理函数说明
1、mem_init(): 内存堆初始化函数,主要设置内存堆的起始地址,以及初始化空闲列表,lwip 初始化时调用,内部接口。
2、void *mem_malloc(mem_size_t size) : 申请分配内存,size为需要申请的内存字节数,返回值为最新分配的内存块的数据地址,注意不是内存块的地址。
如果为分配好内存,返回NULL。
申请的内存堆是全局变量。
3、*mem_calloc(mem_size_t count, mem_size_t size) : 是对mem_malloc()函数的简单包装,两个入口参数,count为每个元素大小,size为元素的总个数,两个参数的乘积就是实际要分配的内存空间的大小,与mem_malloc()不同的是它会把动态分配的内存清零。
所以很多人会选择调用mem_calloc(),这样肯定会清0,并且可以避免调用memset()。
与mem_malloc()返回值一样。
4、void mem_free(void *rmem): 内存释放函数,rmem为待释放的内存块首地址。
5、mem_realloc(void *rmem, mem_size_t newsize) : 重新分配内存,指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)。
6、static void plug_holes(struct mem *mem) :对相邻且未用的内存块进行合并。
在mem_free中调用
7.2 lwip—mem_init和mem_malloc详解
1.<pre name="code"class="cpp">#define MEM_ALIGNMENT 4
2.//对齐方式为4字节对齐
3.#ifndef LWIP_MEM_ALIGN_SIZE
4.#define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIG
NMENT-1))
5.//实现待分配数据空间的内存对齐
6.#endif
7.
8.#ifndef LWIP_MEM_ALIGN
9.//地址对齐,对齐方式也为4字节对齐
10.#define LWIP_MEM_ALIGN(addr) ((void *)(((mem_ptr_t)(addr) + MEM_ALIGNMENT -
1) & ~(mem_ptr_t)(MEM_ALIGNMENT-1)))
11.#endif
12.
13.
14./* MEM_SIZE: the size of the heap memory. If the application will send
15.a lot of data that needs to be copied, this should be set high. */
16.#define MEM_SIZE (8*1024)
17.//堆的总空间大小,此后在这个基础上划分堆,将在这个空间进行内存分配,内存块结构体和数
据都是在这个空间上的
18.
19.//mem为内存块的结构体,next;,prev都为内存块索引
20.struct mem {
21./** index (-> ram[next]) of the next struct *///ram为堆的首地址,相当于数组
的首地址,索引基地址。