1、 LWIP简介
TCP/IP协议栈Lwip为瑞典人Adam Dunkels所著,其为一个协议栈的实现。它的目的是减少内存使用率和代码大小,使Lwip适用于资源受限系统比如嵌入式系统。Lwip既可以移植到操作系统上,又可以在无操作系统的情况下独立运行。
为了减少处理和内存需求,lwip使用不需要任何数据复制的经过裁剪的API。其中提供了
*low-level "core" / "callback" or "raw" API.
*higher-level "sequential" API.
RawAPI在不带有操作系统的移植中应用,sequential" API在多进程的操作系统中应用,本产品用到的是raw API函数。
LWIP的特性如下:
1) 支持多网络接口下的IP转发
2) 支持ICMP协议
3) 包括实验性扩展的UDP(用户数据报协议)
4) 包括阻塞控制,RTT估算和快速恢复和快速转发的TCP(传输控制协议)
5) 提供专门的内部回调接口(Raw API)用于提高应用程序性能
6) 可选择的Berkeley接口API(多线程情况下)
7) 在最新的版本中支持PPP
8) 最新版本中增加了的IP fragment的支持
9) 支持DHCP协议,动态分配ip地址。
10) 支持IPv6
正如其他TCP/IP协议的实现,分层协议的设计为LWIP的设计与实现提供一向导。每一个协议都作为一个模块来实现,提供一些与其他协议的接口函数。尽管各层分开实现,但正如上面所讨论的,为了同时提高处理速度和内存利用两方面的性能,一些层在设计时违背这一原则。
例如: 当检验一接收到的TCP段(segment) 的校验和(checksum) 和分解TCP段时, 源和目的IP地址必须被告知TCP模块。LWIP实现时不是通过函数调用把IP地址传递给TCP, 而是TCP模块通过获取IP报头的结构进而自己提取这一信息。
LWIP有几个模块组成, 除了实现TCP/IP协议的各个模块(IP、 ICMP、 UDP、 和 TCP), 同时设计了许多支持模块。 这些支持模块组成了操作系统模拟层、 缓冲和存储管理子系统、 网络接口函数和一些处理因特网校验和的函数。LWIP还包括关于API的摘要。
2、raw API介绍
程序的执行是靠回调函数来驱动的。每一个回调函数也只不过是一个能够直接被 TCP/IP 代码调用的普通的 C 语言函数。 每一个回调函数的调用都是传递一个当前连接 UDP 或 TCP 的状态。另外,为了使应用程序有一个明确的执行状态,回调函数的指定是可编程的,并且是独立于 TCP/IP 状态之外的。 剩下的部分我们介绍 LwIP 提供的几个用到的 RAW API 函数。
2.1建立TCP连接函数
建立连接的函数同 sequential API 以及BSD 标准的 socket API 都非常相似。一个新的TCP 连接的标志(实质上是一个协议控制块-PCB)由 tcp_ new () 函数来创建。连接创建后,该 PCB可以进 入监听状态,等待数据接收的连接信号,也可以直接连接另外一个主机。
1) tcp_new()
该函数在定义一个tcp_pcb控制块后应该首先被调用,以建立该控制块的连接标志。 该函数的详细描述请见表2.1 。
功能 | 建立一个新的连接标志(pcb) |
原型 | Struct tcp_pcb *tcp_new(void) |
参数 | 无 |
返回 | Pcb:正常建立了连接标志,返回建立的pcb NULL:新的pcb内存不可用 |
表2.1 函数tcp_new()
2) tcp_bind()
该函数用户绑定本地的IP地址和端口号,用户可以将其绑定在一个任意
的本地IP地址上,它也只能在函数tcp_new(请见表2.1)调用之后才能调用。
功能 | 绑定本地IP地址和端口号 |
原型 |
err_t tcp_bind (struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port)
|
参数 | pcb : 准备绑定的连接,类似于 BSD 标准中的 Sockets |
返回 | ERR_OK : 正确地绑定了指定的连接
|
表2.2 函数tcp_bind()
3)、tcp_listen()
当一个正在请求的连接被接收时, 由 tcp_accept() 函数指定的回调函数将会被调用。当然,在调用本函数前,必须首先调用函tcp_bind()来绑定一个本地的 IP 地址和端口号。该函数的详细描述请见表 2.3 。
表2.3 函数tcp_listen()
功能 | 使指定的连接开始进入监听状态 |
原型 | struct tcp_pcb *tcp_listen (struct tcp_pcb *pcb) |
参数 | Pcb:指定将要进入监听状态的连接 |
返回 | pcb: 返回一个新的连接标志 pcb , 它作为一个参数传递给将要被分派的函数。 这样做的原因是处于监听状态的连接一般只需要较小的内存,于是函数 tcp_listen() 就会收回原始连接的
|
4)、tcp_accept()
当处于监听的连接与一个新来的连接连上后,该函数指定的回调函数将被调用。通常在tcp_listen()函数调用之后调用。该函数的详细描述请见下表。
表2.7 函数tcp_accept()
功能 | 指定处于监听状态的连接接通后将要调用的回调函数 |
原型 | void tcp_accept(struct tcp_pcb *pcb, err_t (* accept)(void *arg, struct tcp_pcb *newpcb, |
参数 | pcb : 指定一个处于监听状态的连接 |
返回 | 无 |
5)、tcp_connect()
请求参数pcb指定的连接连接到远程主机,并发送打开连接的最初的SYN段。函数tcp_connect()调用后立即返回,它并不会等待连接一定要正确建立。如果当连接正确建立,那么它会直接调用第四个参数指定的函数(connected参数)。相反地,如果连接不能够被正确建立,这原因可能是远程主机拒绝连接,也可能是远程主机不应答,无论是什么原因,都会调用connected函数来设置相应的参数err。该函数的详细描述见下表:
功能 | 请求指定的连接连接到远程主机,并发送打开连接的最初的SYN |
原型 | err_t tcp_connect(struct tcp_pcb *pcb, struct ip_addr *ipaddr, err_t (* connected)(void *arg, struct tcp_pcb *tpcb,
|
参数 | pcb : 指定一个连接 (pcb) |
返回 | ERR_MEM :当访问 SYN 段的内存不可用时,即连接没有成功建立 |
6)、tcp_write()
该函数功能是发送TCP数据,但是并不是一经调用,就立即发送数据,而是将指定的数据放入到发送队列,由协议内核来决定发送。发送队列中可用字节的大小可以通过函数tcp_sndbuf()来重新获得。使用这个函数的一个比较恰当的方法是以函数tcp_sndbuf()返回的字节大小来发送数据。如果函数返回ERR_MEM,则应用程序就等待一会,直到当前发送队列中的数据被远程主机成功地接收,然后在尝试发送下一个数据。该函数的详细描述请见下表:
功能 | 发送TCP数据 |
原型 | err_t tcp_write(struct tcp_pcb *pcb, void *dataptr, |
参数 | pcb : 指定所要发送的连接 (pcb) 制进去。 如果该参数为 0 , 则不会为发送的数据分配新的内存空间, 因而对发送数据的访 |
返回 | ERR_MEM : 如果数据的长度超过了当前发送数据缓冲区的大小或者将要发送的段队列的长度超过了文件 lwipopts.h 中定义的上限 ( 即最大值 ) , 则函数 tcp_write() 调用失败, 返回 ERR_MEM ERR_OK :数据被正确地放入到发送队列中,返回 ERR_OK |
7)、tcp_recv()
该函数用于指定当有新的数据接收到时调用的回调函数,通常在函数tcp_accept()指定的回调函数中调用。该函数的详细描述请见下表:
功能 | 指定当新的数据接收到时调用的回调函数 |
原型 | void tcp_recv (struct tcp_pcb *pcb, struct tcp_pcb *tpcb, err_t err)) |
参数 | pcb : 指定一个与远程主机相连接的连接 (pcb) |
返回 | 无 |
8)、tcp_recved()
当应用程序接收到数据的时候该函数必须被调用,用于获取接收到的数据的长度,即该函数应该在函数tcp_recv()指定的回调函数中调用。该函数的详细描述请见下表:
功能 | 获取接收到的数据的长度 |
原型 | void tcp_recved(struct tcp_pcb *pcb, u16_t len) |
参数 | pcb : 指定一个与远程主机相连接的连接 (pcb) |
返回 | 无 |
9)、tcp_poll()
当使用LwIP的轮询功能时必须调用该函数,用于指定轮询的时间间隔及轮询时应该调用的回调函数。该函数的详细描述见下表:
功能 | 指定轮询的时间间隔以及轮询应用程序时应该调用的回调函数 |
原型 | void tcp_poll(struct tcp_pcb *pcb, u8_t interval) |
参数 | pcb : 指定一个连接 (pcb) |
返回 | 无 |
10)、tcp_close()
功能 | 关闭一个指定的TCP连接,调用该函数后,TCP代码将会释放(删除)pcb结构 |
原型 | err_t tcp_close(struct tcp_pcb *pcb) |
参数 | Pcb:指定一个需要关闭的连接(pcb) |
返回 | ERR_MEM : 当需要关闭的连接没有可用的内存时, 该函数返回 ERR_MEM 。 如果这样的话, 应用程序将通过事先确立的回调函数或者是轮询功能来等待及重新关闭连接 |
3、开发与应用平台
以太网硬件开发平台为arm(型号为2292),网卡芯片为RTL8019as。RTL8019as支持10M的以太网传输速度,双口ram的设计结构支持全双工的数据收发方式,可以并行的处理数据的收发。
软件开发平台是keil4.0.2,仿真器型号为ulink2。
3.1 RTL8019as简介
10M以太网芯片RTL8019as采用寄存器分页设计的理念,支持以太网II和IEEE802.3 10Base5,10Base2,10BaswT。由于其寄存器进行分页设计,所以在访问寄存器时,同一个地址可能访问不同的寄存器。
3.2 RTL8019as引脚配置方式
(1)工作方式
RTL8019AS 有 3 种工作方式 , 由管脚第 65 脚 JP 决定
第一种为跳线方式 : 网卡的 I/O 和中断由跳线决定
第二种为即插即用方式 : 由软件进行自动配置 Plug and Play
第三种为免跳线方式 : 网卡的 I/O 和中断由外接的 93C46 里的内容 决定。
第 65 脚 JP 是输入引脚
当 65 脚为高电平时 , RTL8019as 工作在第 1 种方式 , 本设计使用第 1 种工作方式
当 65 脚为低电平时 , RTL8019as 工作在第 2 或第 3 种方式 , 具体由 93C46 决定。
RTL8019AS 悬空时 , 引脚的输入状态为低电平 ( 内部 100K 下拉 )
这里用到的是跳线方式。
(2)I/O地址
由于芯片工作在跳线方式
管脚85、84、82、81(IOS3…IOS0)决定芯片的I/O地址,RTL8019as是在arm外扩的,利用的是arm外扩的地址空间。RTL8019as地址空间是由IOS[3-0]决定的,电路图中没有接任何信号,代表默认值0000,那么芯片本身的地址空间是从0H-300H。
(3)中断引脚
芯片的中断线由以下引脚80、79、78(IRQ2…IRQ0)决定,这里用到的是INT0中断。
(4)RTL8019as寻址
RTL8019as引脚A9接上拉的5伏电源,另一个是RTL8019as的A8接到arm芯片中的A22脚,为了保证RTL8019as的地址空间为300H,那么arm输出的A22脚必须为1。电路中ARM带有两个网卡芯片,分别通过CS1、CS2做片选。ARM外扩的地址空间有四个分别为0x81000000,0x82000000,0x83000000,0x84000000。当向不同地址空间写入时,相应的片选信号自动变为零。电路连接时,由于arm地址线A0没接,只是A1接到了RTL8019as的A0管教,以此类推接下去的,由以上这些确定了RTL8019as的实际地址。(就是基本地址+偏移地址,这里偏移地址为300H)。
(5)读写寄存器
RTL8019as的寄存器地址是按页存储的,它每个地址都分为四页。比如平时我们说的寄存器地址为0x8888888,往这个寄存器里写值时,那么这个寄存器里的值就确定了。但是RTL8019as不是这样,如果往那个地址里写值,就不一定写到哪个寄存器了。它是分页的,这个地址分了四页,一页中有一个寄存器,也就是说这个地址代表四个寄存器。如果要向地址中这四个寄存器中的某一个写入值时,要先向指令寄存器CR写值(指令寄存器CR的第七和第六位选择页位的可选择四页),选中其中的某一页后再向地址写值就OK了。
RTL8019AS内部有两块RAM区。一块16K字节,地方为0x4000~0x7fff;一块32字节,地方为0x0000~0x001f。RAM按页存储,每256字节为一页。平常将RAM的前12页(即0x4000~0x4bff)存储区作为发送缓冲区;后52页(即0x4c00~0x7fff)存储区作为接管缓冲区。第0页叫Prom页,只有32字节,地方为0x0000~0x001f,用于存储以太网物理地方。
要接管和发送数据包就务必议定DMA读写RTL8019AS内部的16KB RAM。它现实上是双端口的RAM,是指有两套总线连结到该RAM,一套总线RTL8019AS读或写该RAM,即当地DMA;另一套总线是单片机读或写该RAM,即长途DMA。
数据发送或者接收时,要使远端DMA首地址指向数据的发送或者接收缓冲区,然后再执行读写指令。
(6) RTL8019as的数据发送
RTL8019as发送驱动编写的步骤很好理解分为以下步骤:
a) 等待上次发送结束
b) 清除DMA中断标志位
c) 配置远端DMA发送首地址
d) 配置发送远端DMA的字节长度
e) 发写远端dma指令
f) 配置传输地址
g) 配置传输长度
h) 执行发送指令
(7)RTL8019as的数据接收
接收缓存区大小的配置:分为两种情况:当8位模式时,网卡含有8k字节的RAM,地址为:0x4000-0x5fff ;当为16位模式时,网卡含有16K字节的RAM,地址为:0x4000-0x7fff。这里用到的是8位数字模式,配置的是接收缓存区为0x4c00-0x5fff,发送缓存区配置为0x4000-0x4dff。
RTL8019as的数据接收有两种方式,一种是查询式的接收方式(用的比较多),一种是自动接收方式。
查询式接收方式(Remote DMA READ)步骤如下:
a.查看bnry是否=CURR-1,不等则说明有数据包读
b.用bnry初始化DMA起址控制器RSAR0,1
c 用18初始化DMA长度控制器RBCR0,1
d.执行Remote DMA READ命令
e.读出以太网包头(18字节),从包头中读出包长度
f.同b-e读出所有数据
g.调整bnry指针=CURR-1
自动接收方式(send command 指令)
初始化 BNRY=CURR+PSTART
有数据包到来时:
a. 向RBCR1寄存器写入0x0FH(见8390技术手册)
b. 执行send command指令
c. 等待DMA传输结束
d. 从0x10端口读数据…
在执行自动接收指令时要注意接收缓存区大小的配置,这里用到的是8位数字模式,所以接收缓存区设置为0x4C00-0x4dff。
3.3 arm2292简介
Arm2292是基于一个支持实时仿真和跟踪的 16/32 位 ARM7TDMI-S CPU 的微控制器,并带有 128/256k 字节 (kB) 嵌入的高速 Flash 存储器。 128 位宽度的存储器接口和独特的加速结构使 32 位代码能够在最大时钟速率下运行。对代码规模有严格控制的应用可使用 16 位 Thumb 模式将代码规模降低超过 30% ,而性能的损失却很小。
Arm2292较小的 64 和 144 脚封装、 极低的功耗、 多个 32 位定时器、 4 路 10 位 ADC 、 2/4 路 CAN 或 8 路 10 位 ADC 、 2/4 路 CAN ( 64 脚和 144 脚封装) 以及多达 9 个外部中断使它们特别适用于工业控制、医疗系统、访问控制和 POS 机。
在 64 脚的封装中,最多可使用 46 个 GPIO 。在 144 脚的封装中,可使用的 GPIO 高达 76 (使用了外部存储器)~ 112 个(单片应用)。 由于内置了宽范围的串行通信接口,它们也非常适合于通信网关、协议转换器、嵌入式软 modern 以及其它各种类型的应用。
3.4 arm与RTL8019as接口
RTL8019as是通过arm的外扩地址空间相连的,数据线16根,arm端的地址线A1接到RTL8019as地址线的a0,所以RTL8019as只读取偶地址中的数据,RTL8019as设置为字节传输,Arm配置为16位传输。
RTL8019as的总线是按照ISA总线的模式设计的,对于普通的51单片机来说不用考虑单片机与RTL8019as之间的传输时序问题,但对于arm就要考虑和RTL8019as在传输中的时间配合问题,因为arm中有调节传输速度寄存器BCFG,通过配置可使其正常通讯。此处配置为0x10001460 。
RTL8019as的驱动放在arm的中断服务程序里,RTL8019as在无数据传输时,中断管脚传输的是低电平,在有数据传输时会变为高电平,当清除RTL8019as中断标志寄存器后才会将高电平变低。因此在中断配置时arm端配置为上升沿触发,在进入arm中断服务程序时先清除arm中断标志位,运行中断服务程序后再清除RTL8019as。
4、lwip工程介绍
4.1 文件功能
整个工程分为三个文件夹:lwip内核文件夹,项目主函数文件夹、驱动文件夹。
Lwip内核文件夹:放有关于lwip协议栈相关的所有内核代码,其中opt.h、lwiplib文件,为协议栈的配置文件,通过配置可以实现相应的功能。
主函数文件件:放有主函数及其相关的文件;
驱动文件夹:有RTL8019as的驱动文件及底层的相关内容
4.2 驱动文件夹中主要函数介绍
lwIPServiceTimers():在lwiplib.c文件中,用于lwip定时器的服务。该函数用于lwip所有的周期性事件的定时器,包括TCP和主机的定时器。它应该在LWIP相 关的上下文中(lwIP context)被调用,在无RTOS支持的情况下在以太网中断服务程序中被调用,而在有操作系统的支持下,直接在LWIP的线程里调用即可。
lwIPEthernetIntHandler():在lwiplib.c文件中,TCP/IP协议栈——LwIP的以太网中断服务程序。该函数必须设置为一个最低的优先级,所有接收的数据包都被放入 到数据包队列中等待一个高平台的任务处理。同时,它还将检查发送数据包队列,并根据需要通过以太网MAC发送 数据。如果系统被配置为没有使用RTOS,那么额外的处理将会在中断中。数据包队列是被TCP/IP源码处理的,并且需要周期性的定时服务事件来处理。
InitNic()在lwip.c文件中,配置IP地址,子网掩码,网关等
4.3 主函文件中的主要函数
http_recv():在LcmMain.c函数中,其中接收到的数据放在pbuf变量指针p->payload指针所指向的地址空间里,数据长度放在p->len中,应用时将数据取出即可。
http_init():在LcmMain.c文件中,建立通信的tcb块,以及要绑定的端口。
4.4 lwip的接口函数
etharp_arp_input ():驱动层与协议栈的接口函数,当要接收arp报文时候用到。
etharp_ip_input():驱动层与协议栈的接口函数,当要接收ip报文时被用到。
4.5 TCP处理整体框图:
当应用程序想要发送TCP 数据, 函数tcp_write()将被调用。函数tcp_write() 将控制权交给tcp_enqueue(),该函数将数据分成合适大小的TCP段(如果必要), 并放进发送队列。 接下来函数tcp_output()将检查数据是否可以发送。也就是说,如果接收器的窗口有足够的空间并且 拥塞窗口足够大,则使用ip_route()和ip_output_if()两个函数发送数据 。
当ip_input()对IP报头进行检验且把TCP段移交给tcp_input()函数后,输入处理开始。 在该函数中将进行初始检验(也就是 ,checksumming和TCP 剖析析)并决定该段属于哪个TCP连接。该段于是由tcp_process()处理,它实现TCP状态机和其他任何必须的状态转换。 。如果一个连接处于从网络接收数据的状态,函数tcp_receive() 将被调用。 如果那样, tcp_receive() 将把段上传给应用程序。如果段构成未应答数据(先前放入缓冲区的)的ACK, 数据将从缓冲被移走并且收回该存储区。 同样, 如果接收到请求数据的ACK, 接收者可能希望接收更多的数据,这时tcp_output() 将被调用。