一、环境介绍
MCU: STM32F103ZET6
代码开发工具: Keil5
TCP/IP协议栈: LWIP
网卡: DM9000
本篇文章主要讲解如何在STM32F103工程里添加移植LWIP协议,最终完成TCP服务器、TCP客户端的通信测试。 网卡采用的是DM9000,工程代码中,采用STM32的FSMC接口来驱动DM900网卡,DM9000是并口网卡,引脚多,但是速度快,也可以采用其他网卡,SPI协议的、UART协议的等。 比如:ENC28J60。 因为主要是讲LWIP协议栈的移植,所以网卡相关的代码就没有细说(需要准备一个网卡可以正常通信的工程,再移植)。
工程源码、LWIP资料包下载地址:https://download.csdn.net/download/xiaolong1126626497/19907087
资料包里的内容如下:
二、D9000网卡
2.1 DM9000简介
DM9000 是一款完全集成的、性价比高、引脚数少、带有通用处理器接口的单芯片快速以太网控制器。 自带一个 10/100M PHY 和 4K 双字的 SRAM ,DM9000A 为适应各种处理器提供了8位、16 位数据接口访问内部存储器,DM9000拥有自动协商功能,DM9000特性如下:
1、集成自适应10/100M收发器。
2、内置16k字节的SRAM。
3、支持硬件帧校验。
4、兼容3.3V和5.0V输入输出电压。
DM9000 有多种型号,有 100 引脚和 48 引脚的, 开发板选择的是 48 引脚的 DM9000,型号为 DM9000CEP。
2.2 DM9000 中断引脚电平设置
DM9000的34(INT)引脚为中断输出引脚,默认情况下该引脚高电平有效。可以通过设置DM9000 的 20(EECK)引脚来改变 INT 的有效电平,当 EECK 拉高以后, INT 低电平有效,否则的话 INT 是高电平有效的。开发板上 R66 电阻为 EECK 的上拉电阻,因此开发板上 DM9000 的 INT 引脚是低电平有效的。
2.3 DM9000 数据位宽设置
前面我们提了一下 DM9000 支持 8 位和 16 位两种数据位宽,可以通过 DM9000 的 21(EECS)引脚设置其数据位宽,当 EECS 上拉的时候 DM9000 选择 8 位数据位宽,否则的话选择 16 位数据位宽。开发板上的 R65 电阻为 EECS 的上拉电阻,但是此电阻并未焊接! DM9000 芯片的数据位宽为 16 位。
2.4 DM9000寄存器表
寄存器 | 描述 | 寄存器地址 | 默认值 | |
NCR | 网络控制寄存器。 | 00H | 00H | |
NSR | 网络状态寄存器。 | 01H | 00H | |
TCR | 发送控制寄存器。 | 02H | 00H | |
TSR I | 发送状态寄存器 I。 | 03H | 00H | |
TSR II | 发送状态寄存器 II。 | 04H | 00H | |
RCR | 接收控制寄存器。 | 05H | 00H | |
RSR | 接收状态寄存器。 | 06H | 00H | |
ROCR | 接收溢出计数寄存器。 | 07H | 00H | |
BPTR | 背压门限寄存器。 | 08H | 37H | |
FCTR | 溢出控制门限寄存器。 | 09H | 38H | |
FCR | TX/RX 流量控制寄存器。 | 0AH | 00H | |
EPCR | EEPROM/PHY 控制寄存器。 | 0BH | 00H | |
EPAR | EEPROM/PHY 地址寄存器。 | 0CH | 40H | |
EPDRL | EEPROM/PHY 数据寄存器低位。 | 0DH | XXH | |
EPDRH | EEPROM/PHY 数据寄存器高位。 | 0EH | XXH | |
WCR | 唤醒控制寄存器。 | 0FH | 00H | |
PAR | 物理地址寄存器。 | 10H~15H | 由 EEPROM 决定 | |
MAR | 广播地址寄存器。 | 16H~1DH | XXH | |
GPCR | 通用目的控制寄存器(8bit 模式)。 | 1EH | 01H | |
GPR | 通用目的寄存器。 | 1FH | XXH | |
TRPAL | TX SRAM 读指针地址低字节。 | 22H | 00H | |
TRPAH | TX SRAM 读指针地址高字节。 | 23H | 00H | |
RWPAL | RX SRAM 写指针地址低字节。 | 24H | 00H | |
RWRAH | RX SRAM 写指针地址高字节。 | 25H | 0CH | |
VID | 厂家 ID。 | 28H~29H | 0A46H | |
PID | 产品 ID。 | 2AH~2BH | 9000H | |
CHIPR | 芯片版本。 | 2CH | 18H | |
TCR2 | 发送控制寄存器 2。 | 2DH | 00H | |
OCR | 操作控制寄存器。 | 2EH | 00H | |
SMCR | 特殊模式控制寄存器。 | 2FH | 00H | |
ETXCSR | 即将发送控制/状态寄存器。 | 30H | 00H | |
TCSCR | 发送校验和控制寄存器。 | 31H | 00H | |
RCSCSR | 接收校验和控制状态寄存器。 | 32H | 00H | |
MRCMDX | 内存数据预取读命令寄存器(地址不加 1)。 | F0H | XXH | |
MRCMDX1 | 内存数据读命令寄存器(地址不加 1)。 | F1H | XXH | |
MRCMD | 内存数据读命令寄存器(地址加 1)。 | F2H | XXH | |
MRRL | 内存数据读地址寄存器低字节。 | F4H | 00H | |
MRRH | 内存数据读地址寄存器高字节。 | F5H | 00H | |
MWCMDX | 内存数据写命令寄存器(地址不加 1) | F6H | XXH | |
MWCMD | 内存数据写命令寄存器(地址加 1)。 | F8H | XXH | |
MWRL | 内存数据写地址寄存器低字节。 | FAH | 00H | |
MWRH | 内存数据写地址寄存器高字节。 | FBH | 00H | |
TXPLL | TX 数据包长度低字节寄存器。 | FCH | XXH | |
TXPLH | TX 数据包长度高字节寄存器。 | FDH | XXH | |
ISR | 中断状态寄存器。 | FEH | 00H | |
IMR | 中断屏蔽寄存器。 | FFH | 00H | |
2.5 DM9000常用寄存器介绍
NCR、 NSR、 TCR、 RCR、 FCTR、 BPTR、 TCR2、 ISR、 IMR。
NCR(网络控制寄存器)寄存器
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | RESERVED | WAKEEN | RESERVED | FCOL | FDX | LBK | RST |
FCOL:强制冲突模式,用于检测。
FDX:内部 PHY 全双工模式。
LBK:回环模式(LoopBack)
00 正常;
01 MAC 内部回环;
10 内部 PHY100M 模式数字回环;
11 保留;
RST:置 1 软件复位, 10us 后自动清零。
NSR 寄存器(网络状态寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名 称 | SPEED | LINKST | WAKEST | RESERVED | TX2END | TX1END | RXOV | RESERVED |
SPEED:网络速度,在使用内部 PHY 情况下,0 表示 100Mbps,1 表示 100Mbps,当 LINKST=0时,此位无意义。
LINKST:连接状态, 0 为连接失败, 1 位已连接。
TX2END: TX(发送)数据包 2 完成标志,读取或写 1 将清零该位。
TX1END: TX(发送)数据包 1 完成标志,读取或写 1 将清零该位。
RXOV: RX(接收)FIFO 溢出标志。
TCR 寄存器(发送控制寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名 称 | RESERVED | TJDIS | EXCECM | PAD_DIS2 | CRC_DIS2 | PAD_DIS1 | CRC_DIS1 | TXREQ |
TJDIS: Jabber 传输禁止。
1,禁止 Jabber 传输定时器(2048 字节)。
0,使能。
EXCECM:严重冲突模式控制
0,当冲突计数多于 15 则终止本次数据包。
1,始终尝试发送本次数据包。
PAD_DIS2:禁止为数据包 II 添加填充。
CRC_DIS2:禁止为数据包 II 添加 CRC 校验。
PAD_DIS1:禁止为数据包 I 添加填充。
CRC_DIS1:禁止为数据包 I 添加 CRC 校验。
TXREQ: TX(发送)请求,发送完成后自动清零该位
RCR 寄存器(发送控制寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | RESERVED | WTDIS | DIS_LONG | DIS_CRC | ALL | RUNT | PRMSC | RXEN |
WTDIS:看门狗定时器(2048 字节)禁止。
1,进制
0,使能
DIS_LONG:丢弃长数据包, 1,丢弃数据包长度超过 1522 字节的数据包。
DIS_CRC:丢弃 CRC 校验错误数据包。
ALL:允许广播。
RUNT:允许小于最小长度的数据包。
PRMSC:各种模式。
RXEN:接收使能。
FCTR 寄存器(流控制阈值寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | HWOT | HWOT | HWOT | HWOT | LWOT | LWOT | LWOT | LWOT |
HWOT:RX FIFO 缓存高位溢出门限
当 RX SRAM 空闲空间小于该门限值时则发送一个暂停时间为 FFFFH 的暂停包,若该值为 0,则无接收控件。 1=1k 字节,默认值为 3H,即 3K 字节空闲空间,不要超过 S RAM 大小。
LWOT:RX FIFO 缓存低位溢出门限当 RX SRAM 空闲空间大于该门限值时则发送一个暂停时间为 0000H 的暂停包。
当溢出门限最高值的暂停包发送之后,溢出门限最低值的暂停包才有效,默认值为 8K,不要超过 SRAM 大小。
BPTR 寄存器(背压阈值寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | BPHW | JPT |
BPHW:背压阈值最高值当接收 SRAM 空闲空间低于该阈值,则 MAC 将产生一个拥挤状态, 1=1k 字节。默认值为 3H,即 3K 字节空闲空间,不要超过 SRAM 大小。
JPT:拥挤状态时间,模式为 200us, JPT 值与其对应的拥挤状态时间表
JPT 值 | 拥挤状态时间(us) | JPT 值 | 拥挤状态时间(us) |
0000 | 5 | 1000 | 250 |
0001 | 10 | 1001 | 300 |
0010 | 15 | 1010 | 350 |
0011 | 25 | 1011 | 400 |
0100 | 50 | 1100 | 450 |
0101 | 100 | 1101 | 500 |
0110 | 150 | 1110 | 550 |
0111 | 200 | 1111 | 600 |
TCR2 寄存器(发送控制寄存器 2)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | LED | RLCP | DTU | ONEPM | IFGS |
LED: LED 模式
1,设置 LED 引脚为模式 1
0,设置 LED 引脚为模式 0 或根据 EEPROM 的设定。
RLCP:重试冲突延时数据包, 1 重新发送有冲突延迟的数据包。
DTU: 1 禁止重新发送“underruned”数据包。
ONEPM:单包模式。
1,发送完成前发送一个数据包的命令能被执行。
0,发送完成前发送最多两个数据包的命令能被执行。
IFGS:帧间间隔设置。
0XXX 为 96bit, 1000 为 64bit, 1001 为 72bit
1010 为 80bit, 1011 为 88bit, 1100 为 96bit
1101 为 104bit, 1110 位 112bit, 1111 为 120bit
ISR 寄存器(中断状态寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | IOMODE | RESERVED | LNKCHG | UDRUN | ROO | ROS | PT | PR |
IOMODE: 0,16 位模式; 1,8 位模式。
LNKCHG:连接状态改变。
UDRUN:发送“Underrun”
ROO:接收溢出计数器溢出
ROS:接收溢出。
PT:数据包发送。
PR:数据包接收。
IMR 寄存器(中断状态寄存器)
BIT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
名称 | PAR | RESERVED | LNKCHGI | UDRUNI | ROOI | ROSI | PTI | PRI |
PAR:使能 SRAM 的读/写指针在指针地址超过 SRAM 的大小时自动跳回起始位置。需要驱动程序设置该位,若设置该位, REG_F5 将自动置为 0XH。
LNKCHGI:使能连接状态改变中断。
UDRUNI:使能发送“Underrun”中断。
ROOI:使能接收溢出计数器溢出中断。
ROI:使能接收溢出中断。
PTI:使能数据包发送中断。
PRI:使能数据包接收中断。
2.6 DM9000 直接内存访问控制(DMAC)
DM9000 直接内存访问控制(DMAC)
DM9000 支持 DMA 方式简化对内部存储器的访问。在我们编程写好内部存储器地址后,就可以用一个读/写命令伪指令把当前数据加载到内部数据缓冲区,这样,内部存储器指定位置就可以被读/写命令寄存器访问。存储器地址将会自动增加,增加的大小与当前总线操作模式相同(比如:8-bit、 16-bit 或 32-bit),接着下一个地址数据将会自动加载到内部数据缓冲区。要注意的是在连续突发式第一次访问的数据应该被忽略,因为,这个数据是最后一次读写命令的内容。内部存储器空间大小 16K 字节。前 3K 字节单元用作发送包的缓冲区,其他 13K 字节用作接收包的缓冲区。所以在写存储器操作时,如果地址越界(即超出 3K 空间),在 IMR 寄存器 bit7 置位的情况下,地址指针将会返回到存储器 0 地址处。同样,在读存储器操作时,如果地址越界(即超出 16K 空间),在 IMR 寄存器 bit7 置位的情况下,地址指针将会返回到存储器 0x0C00 地址处。
DM9000 数据包发送
DM9000 有两个发送数据包: index1 和 index2,同时存储在 TX SRAM 中。发送控制寄存器(02h)控制循环冗余校验码(CRC)和填充(pads)的插入,其状态分别记录在发送状态寄存器I(03H)和发送状态寄存器 II(04H)中。发送器的起始地址为 0x00H,在软件或硬件复位后,默认的数据发送包为 index1。首先,使用 DMA 端口将数据写 TX SRAM 中,然后,在发送数据包长度寄存器中把数据字节数写入字节计数寄存器。置位发送控制寄存器的 bit0 位,则 DM9000 开始发送 index1 数据包。在 index1数据包发送结束之前,数据发送包 index2 被移入 TX SRAM 中。在 index1 数据包发送结束后,将 index2 数据字节数写入字节计数寄存器中,然后,置位发送控制寄存器的 bit0 位,则 index2数据包开始发送。以此类推,后面的数据包都以此方式进行发送。
DM9000 数据包接收
RX SRAM 是一个环形数据结构。在软件或硬件复位后, RX SRAM 的起始地址为 0X0C00。每个接收数据包都包含有 CRC 校验域,数据域,以及紧跟其后的 4 字节包头域。 4 字节包头格式为: 01h、状态、 BYTE_COUNT 低、 BYTE_COUNT 高。请注意:每个接收包的起始地址处在适当的地址边界,这取决于当前总线操作模式(8bit 或者 16bit)
2.7 DM9000原理图介绍
各信号线描述如下:
PWRST: DM9000 复位信号。
CS: DM9000 的片选信号。
WR(IOW): 处理器写命令。
RD(IOR): 处理器读命令。
CMD: 命令/数据标志, 0,读写命令; 1,读写数据。
SD0~SD15: 16 位双向数据线。
信号线对应的GPIO口对应关系
引脚名称 | GPIO口 | 功能说明 |
PWRST-->DM9000_RST | PD7 | 复位信号 |
CS-->FSMC_NE2 | PG9 | 片选信号 |
WR(IOW)-->FSMC_NWE | PD5 | 处理器写命令 |
RD(IOR) --->FSMC_NOE | PD4 | 处理器读命令 |
CMD—>FSMC-A7 | PF13 | 命令/数据标志, 0,读写命令; 1,读写数据 |
INT--->DM9000_INT | PG6 | 中断引脚 |
FSMC_D0 | PD14 | 数据线0 |
FSMC_D1 | PD15 | 数据线1 |
FSMC_D2 | PD0 | 数据线2 |
FSMC_D3 | PD1 | 数据线3 |
FSMC_D4 | PE7 | 数据线4 |
FSMC_D5 | PE8 | 数据线5 |
FSMC_D6 | PE9 | 数据线6 |
FSMC_D7 | PE10 | 数据线7 |
FSMC_D8 | PE11 | 数据线8 |
FSMC_D9 | PE12 | 数据线9 |
FSMC_D10 | PE13 | 数据线10 |
FSMC_D11 | PE14 | 数据线11 |
FSMC_D12 | PE15 | 数据线12 |
FSMC_D13 | PD8 | 数据线13 |
FSMC_D14 | PD9 | 数据线14 |
FSMC_D15 | PD10 | 数据线15 |
FSMC接口框图
DM9000网卡接在FSMC的第2块上,数据线地址: 0x64000000
PA7地址线作为命令与数据线切换引脚。
外接16位宽度存储器:HADDR[25:1] FSMC_A[24:0]
外接8位宽度存储器: HADDR[25:0] FSMC_A[25:0]
0x64000000基地址:01100100000000000000000000000000 0x64000000写数据:01100100000000000000000000000000 0x64000100写命令:01100100000000000000000100000000 |
2.8 DM9000时序图介绍
IOR和IOW是DM9000的读写选择引脚,低电平有效,即低电平时进行读(IOR)写(IOW)操作;AEN是芯片选通引脚,低电平有效,该引脚为低时才能进行读写操作;CMD的命令/数据切换引脚,低电平时读写命令操作,高电平时读写数据操作。
读时序:
写时序:
三、LWIP(TCP/IP)网络协议栈介绍
根据以太网帧头携带的上层协议类型值传递数据。
以太网帧格式定义:
目的MAC地址 源MAC地址 类型/长度 数据 校验
6字节 6字节 2字节 46-1500字节 4字节
ip:0x0800
ARP:0x0806
最大帧长1518字节 最小字节64字节
3.1 LWIP介绍
lwip是瑞典计算机科学院网络嵌入式系统小组(SICS)的Adam Dunkels(亚当·邓克尔) 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。
LwIP是Light Weight (轻型)IP协议,有无操作系统的支持都可以运行。LwIP实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用,它只需十几KB的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端的嵌入式系统中使用。lwip提供三种API:1)RAW API 2)(NETCONN)lwip API 3)BSD API。
RAW 编程接口使得程序效率高,但是需要对 LWIP 有深入的了解,而且不适合大数据量等场合。 NETCONN 编程接口,使用 NETCONN API 时需要有操作系统的支持。
RAW API把协议栈和应用程序放到一个进程里边,该接口基于函数回调技术,使用该接口的应用程序可以不用进行连续操作。不过,这会使应用程序编写难度加大且代 码不易被理解。为了接收数据,应用程序会向协议栈注册一个回调函数。该回调函数与特定的连接相关联,当该关联的连接到达一个信息包,该回调函数就会被协议 栈调用。这既有优点也有缺点。优点是既然应用程序和TCP/IP协议栈驻留在同一个进程中,那么发送和接收数据就不再产生进程切换。主要缺点是应用程序不 能使自己陷入长期的连续运算中,这样会导致通讯性能下降,原因是TCP/IP处理与连续运算是不能并行发生的。这个缺点可以通过把应用程序分为两部分来克 服,一部分处理通讯,一部分处理运算。
lwip API把接收与处理放在一个线程里面。这样只要处理流程稍微被延迟,接收就会被阻塞,直接造成频繁丢包、响应不及时等严重问题。因此,接收与协议处理必须 分开。LwIP的作者显然已经考虑到了这一点,他为我们提供了 tcpip_input() 函数来处理这个问题, 虽然他并没有在 rawapi 一文中说明。 讲到这里,读者应该知道tcpip_input()函数投递的消息从哪里来的答案了吧,没错,它们来自于由底层网络驱动组成的接收线程。我们在编写网络驱动时, 其接收部分以任务的形式创建。 数据包到达后, 去掉以太网包头得到IP包, 然后直接调用tcpip_input()函数将其 投递到mbox邮箱。投递结束,接收任务继续下一个数据包的接收,而被投递得IP包将由TCPIP线程继续处理。这样,即使某个IP包的处理时间过长也不 会造成频繁丢包现象的发生。这就是lwip API。
BSD API提供了基于open-read-write-close模型的UNIX标准API,它的最大特点是使应用程序移植到其它系统时比较容易,但用在嵌入式系统中效率比较低,占用资源多。这对于我们的嵌入式应用有时是不能容忍的
lwIP协议栈主要关注的是怎么样减少内存的使用和代码的大小,这样就可以让lwIP适用于资源有限的小型平台例如嵌入式系统。为了简化处理过程和内存要求,lwIP对API进行了裁减,可以不需要复制一些数据。
其主要特性如下:
(1)支持多网络接口下的IP转发;
(2)支持ICMP协议;
(3)包括实验性扩展的UDP(用户数据报协议);
(4)包括阻塞控制、RTT 估算、快速恢复和快速转发的TCP(传输控制协议);
(5)提供专门的内部回调接口(Raw API),用于提高应用程序性能;
(6)可选择的Berkeley接口API (在多线程情况下使用) 。
(7)在最新的版本中支持ppp
(8) 新版本中增加了的IP fragment(IP分片)的支持.
(9) 支持DHCP协议,动态分配ip地址.
3.2 几种开源TCPIP协议概述
1、BSD TCP/IP协议栈
BSD栈历史上是商业栈的起点,大多数专业TCP/IP栈(VxWorks内嵌的TCP/IP栈)是BSD栈派生的。这是因为BSD栈在BSD许可协议下提供了这些专业栈的雏形,BSD许用证允许BSD栈以修改或未修改的形式结合这些专业栈的代码而无须向创建者付版税。同时,BSD也是许多TCP/IP协议中的创新(如广域网中饿拥塞控制和避免)的点。
2、uC/IP
uC/IP是由Guy Lancaster编写的一套基于uC/OS且开放源码的TCP/IP协议栈,亦可移植到操作系统,是一套完全免费的、可供研究的TCP/IP协议栈,uC/IP大部分源码是从公开源码BSD发布站点和KA9Q(一个基于DOS单任务环境运行的TCP/IP协议栈)移植过来。uC/IP具有如下一些特点:带身份验证和报头压缩支持的PPP协议,优化的单一请求/回复交互过程,支持IP/TCP/UDP协议,可实现的网络功能较为强大,并可裁减。UCIP协议栈被为一个带最小化用户接口及可应用串行链路网络模块。根据采用CPU、编译器和系统所需实现协议的多少,协议栈需要的代码容量空间在30-60KB之间。http://ucip.sourceforge.net
3、LwIP
LwIP是瑞士计算机科学院(Swedish Institute of Computer Science)的Adam Dunkels等开发的一套用于嵌入式系统的开放源代码TCP/IP协议栈。LwIP的含义是Light Weight(轻型)IP协议,相对于uip。LwIP可以移植到操作系统上,也可以在无操作系统的情况下独立运行。LwIP TCP/IP实现的重点是在保持TCP协议主要功能的基础上减少对RAM的占用,一般它只需要几十K的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端嵌入式系统中使用。LwIP的特性如下:支持多网络接口下的IP转发,支持ICMP协议 ,包括实验性扩展的的UDP(用户数据报协议),包括阻塞控制,RTT估算和快速恢复和快速转发的TCP(传输控制协议),提供专门的内部回调接口(Raw API)用于提高应用程序性能,并提供了可选择的Berkeley接口API。http://www.sics.se/~adam/lwip/或http://savannah.nongnu.org/projects/lwip/
4、uIP
uIP是专门为8位和16位控制器设计的一个非常小的TCP/IP栈。完全用C编写,因此可移植到各种不同的结构和操作系统上,一个编译过的栈可以在几KB ROM或几百字节RAM中运行。uIP中还包括一个HTTP服务器作为服务内容。许可:BSD许用证http://www.sics.se/~adam/uip/
uIP是一个完全由C语言编写的开源软件, 它的文档和源代码可用于商业和非商业用途, 它已经移植到了大部分的8位微控制器, 而且已在很多的嵌入式产品和项目中使用.
5、TinyTcp
TinyTcp 栈是TCP/IP的一个非常小和简单的实现,它包括一个FTP客户。TinyTcp是为了烧入ROM设计的并且现在开始对大端结构似乎是有用的(初始目标是68000芯片)。TinyTcp也包括一个简单的以太网驱动器用于3COM多总线卡http://ftp.ecs.soton.ac.uk/pub/elks/utils/tiny-tcp.txt
选择一个开源协议栈可以从四个方面来考虑:
是否提供易用的底层硬件API,即与硬件平台的无关性;
协议栈需要调用的系统函数接口是否容易构造,另一个对于应用支持程度。
最关键的是占用的系统资源是否在可接受范围内,有裁减优化的空间否? 其中,
BSD 栈可完整实现TCP/IP协议,但代码庞大,70KB-150KB之间,裁减优化有难度,
uIP和TinyTcp代码容量小巧,实现功能精简,限制了在一些较高要求场合下的应用,如可靠性与大容量数据传输。
LwIP和uC/IP是同量级别的两个开源协议栈,两者代码容量和实现功能相似,LwIP没有操作系统针对性,它将协议栈与平台相关的代码抽象出来,用户如果要移植到自己的系统,需要完成该部分代码的封装,并为网络应用支持提供了API接口的可选性。
uC/IP协议最初是针对uC/OS设计,为方便用户移植实现,同样也抽象了协议栈与平台相关代码,但是协议栈所需调用的系统函数大多参照uC/OS内核函数原型设计,并提供了协议栈的函数,方便用户参考,其不足在于该协议栈对网络应用支持不足。
根据以上分析,从应用和开发的角度看,似乎LWIP更得到了网上很多朋友使用的青睐;uC/IP在文档支持与软件升级管理上有很多不足,但是它最初是针对UC/OS而设计,如果选用UC/OS作为软件基础的话,在系统函数构造方面有优势。当然你选择其他操作系统的话,可参照OS_NULL文件夹下的文件修改。 以上的这些开源协议栈也并非免费,拿来就可以用,据我所知,UC/OS的母公司推出UC/OS-TCP/IP花了6人*2年的工作量,国内某公司使用LWIP作为移植的参照,花了4-5人*2年的工作量来测试与优化协议,使用商用TCP/IP栈的高费用就不足为奇了。 作为广大的爱好者学习而言,如果只是跑跑原型,实验一下效果,以上的几种开源协议栈都提供了测试的例子,应该是不错的选择。
终上所述:LWIP可优先考虑,参考的资料较多
四、LWIP协议栈移植
4.1 LWIP源码下载
源码下载地址: http://ftp.yzu.edu.tw/nongnu/lwip/
下载LWIP1.4.1版本、并下载contrib-1.4.1版本。
4.2 将LWIP源码加入到工程目录
4.3 配置lwipopts.h文件
4.4 修改ethernetif.c文件
ethernetif.c文件默认是不编译的,该文件是网卡底层接口的模板文件,需要根据修改网卡发送接口和接收接口。
4.5 修改sys_arch.c文件
修改sys_arch.c只是留下sys_now()函数,其他代码全部删除掉。删除windows.h头文件。
sys_now()函数用于返回一个32位的系统时钟,单位是ms。没有操作系统的情况下,使用定时器提供时间即可。
4.6 新建lwip_config.c文件
在LWIP/app目录下新建一个lwip_config.c/lwip_config.h文件。用于编写动态IP地址分配处理代码,和LWIP事物轮询、初始化代码。
编写一个LWIP初始化配置函数,向LWIP协议栈添加一个新的网卡设备
/*
函数功能: LWIP协议栈初始化
*/
void lwip_config_init(void)
{ip_addr_t ipaddr; //IP地址ip_addr_t netmask; //子网掩码ip_addr_t gw; //网关//全部初始化为0 -因为使用了动态IP地址分配ipaddr.addr=0;netmask.addr=0;gw.addr=0;/*1. 初始化LWIP内核*/lwip_init();/*2. 向网卡列表中添加一个网络设备*/netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,ðernetif_init,ðernet_input);/*3. 开启DHCP服务 */dhcp_start(&lwip_netif);/*4. 设置netif为默认网口*/netif_set_default(&lwip_netif);/*5. 打开netif网口*/netif_set_up(&lwip_netif);
}
编写LWIP事物轮询函数与DHCP处理函数
u32 TCPTimer=0; //TCP查询计时器
u32 ARPTimer=0; //ARP查询计时器
u32 DHCPfineTimer=0; //DHCP精细处理计时器
u32 DHCPcoarseTimer=0; //DHCP粗糙处理计时器
u32 DHCP_State=1; //保存DHCP状态 1表示没有分配成功 0表示分配成功/*
函数功能: LWIP轮询任务
*/
void lwip_periodic_handle()
{//每250ms调用一次tcp_tmr()函数if(TCPTimer >= TCP_TMR_INTERVAL){TCPTimer=0;tcp_tmr(); //处理TCP协议请求}//ARP每5s周期性调用一次if(ARPTimer >= ARP_TMR_INTERVAL){ARPTimer=0;etharp_tmr();}//每500ms调用一次dhcp_fine_tmr()if(DHCPfineTimer >= DHCP_FINE_TIMER_MSECS){DHCPfineTimer=0;dhcp_fine_tmr(); //动态IP地址分配的事物处理if(DHCP_State)lwip_dhcp_process_handle(); //DHCP处理}//每60s执行一次DHCP粗糙处理if(DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS){DHCPcoarseTimer=0;dhcp_coarse_tmr();}
}//lwip控制结构体
typedef struct
{u8 remoteip[4]; //服务器主机IP地址 u8 ip[4]; //本机IP地址u8 netmask[4]; //子网掩码u8 gateway[4]; //默认网关的IP地址
}__lwip_dev;extern __lwip_dev lwipdev; //lwip信息结构体__lwip_dev lwipdev; //lwip信息结构体/*
函数功能: DHCP处理任务
*/
void lwip_dhcp_process_handle(void)
{u32 ip=0,netmask=0,gw=0;ip=lwip_netif.ip_addr.addr; //读取新IP地址netmask=lwip_netif.netmask.addr; //读取子网掩码gw=lwip_netif.gw.addr; //读取默认网关if(ip!=0) //正确获取到IP地址的时候{DHCP_State=0; //表示分配成功//解析出通过DHCP获取到的IP地址lwipdev.ip[3]=(uint8_t)(ip>>24); lwipdev.ip[2]=(uint8_t)(ip>>16);lwipdev.ip[1]=(uint8_t)(ip>>8);lwipdev.ip[0]=(uint8_t)(ip);printf("动态分配
IP:..............%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);//解析通过DHCP获取到的子网掩码地址lwipdev.netmask[3]=(uint8_t)(netmask>>24);lwipdev.netmask[2]=(uint8_t)(netmask>>16);lwipdev.netmask[1]=(uint8_t)(netmask>>8);lwipdev.netmask[0]=(uint8_t)(netmask);printf("子网掩
码............%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);//解析出通过DHCP获取到的默认网关lwipdev.gateway[3]=(uint8_t)(gw>>24);lwipdev.gateway[2]=(uint8_t)(gw>>16);lwipdev.gateway[1]=(uint8_t)(gw>>8);lwipdev.gateway[0]=(uint8_t)(gw);printf("网
关.........%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);}
}
4.7 配置一个定时器提供时间基准
4.8 初始化lwip动态获取IP地址
4.9 LWIP内存配置选择
LWIP可以选择使用系统库自带的函数malloc/free进行管理空间,也可以使用lwip自己的内存管理函数进行管理,源码默认就是使用lwip自己的内存管理方法,就是在初始化内存的时候定义一个数组,数组的大小在lwipopts.h文件MEM_SIZE宏定义的。
五、LWIP函数使用(RAW编程接口)
5.1 LWIP初始化配置
ip_addr_t ipaddr; //IP地址
ip_addr_t netmask; //子网掩码
ip_addr_t gw; //网关//全部初始化为0 -因为使用了动态IP地址分配
ipaddr.addr=0;
netmask.addr=0;
gw.addr=0;/*1. 初始化LWIP内核*/
lwip_init();
/*2. 向网卡列表中添加一个网络设备*/
netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,ðernetif_init,ðernet_input);
/*3. 开启DHCP服务 */
dhcp_start(&lwip_netif);
/*4. 设置netif为默认网口*/
netif_set_default(&lwip_netif);
/*5. 打开netif网口*/
netif_set_up(&lwip_netif);
5.2 LWIP轮询函数处理
LWIP轮询期间:
1. 推荐每250ms周期性调用一次tcp_tmr()函数,处理TCP协议请求。
超时时间LWIP使用TCP_TMR_INTERVAL宏进行了定义。
2. 推荐每5s周期性调用一次etharp_tmr()函数,清除ARP表中过期的数据。
超时时间LWIP使用ARP_TMR_INTERVAL宏进行了定义。
3. (如果开启了动态IP分配功能)推荐每500ms周期性调用一次dhcp_fine_tmr()函数,处理DHCP动态IP地址分配请求。 如果IP地址获取成功,将会放在初始化时注册的网络设备结构体里(struct netif)。
超时时间LWIP使用DHCP_FINE_TIMER_MSECS宏进行了定义。
4. (如果开启了动态IP分配功能)推荐每60s调用一次dhcp_coarse_tmr()函数,用于检查DHCP租约时间,并进行重新绑定。
超时时间LWIP使用DHCP_COARSE_TIMER_MSECS宏进行了定义。
5. 在LWIP运行期间,当网卡收到数据时,还需要调用ethernetif_input函数读取网卡数据。
在函数ethernetif_input()主要完成两个工作
1、调用low_level_input(); 读取网卡实际数据。
2、调用netif->input();
所以,为了能够实时的读取数据,需要最快的速度轮询调用ethernetif_input函数。
5.3 LWIP编程RAW接口函数
tcp_new() 创建一个 TCP 的 PCB 控制块
tcp_bind() 为 TCP 的 PCB 控制块绑定一个本地 IP 地址和端口号
tcp_listen() 开始 TCP 的 PCB 监听
tcp_accept() 控制块 accept字段注册的回调函数,侦听到连接时被调用
tcp_accepted() 通知 LWIP 协议栈一个 TCP 连接被接受了
tcp_conect() 连接远端主机
tcp_write() 构造一个报文并放到控制块的发送缓冲队列中
tcp_sent() 控制块 sent 字段注册的回调函数,数据发送成功后被回调
tcp_output() 将发送缓冲队列中的数据发送出去
tcp_recv()控制块 recv 字段注册的回调函数,当接收到新数据时被调用
tcp_recved()当程序处理完数据后一定要调用这个函数,通知内核更新接收窗口
tcp_poll() 控制块 poll 字段注册的回调函数,该函数周期性调用
tcp_close() 关闭一个 TCP 连接
tcp_err() 控制块 err 字段注册的回调函数,遇到错误时被调用
tcp_abort() 中断 TCP 连接
5.4 创建TCP服务器示例
下面演示了TCP服务器创建步骤,测试服务器是否正常。
u8 TCP_Create(u16_t port)
{struct tcp_pcb *pcb=NULL;pcb=tcp_new(); //创建套接字if(pcb==NULL)return 1;if(tcp_bind(pcb,IP_ADDR_ANY,port)!=ERR_OK)return 2; //绑定端口号pcb=tcp_listen(pcb); //开始监听tcp_accept(pcb,TCP_accept);//等待连接return 0;
}err_t TCP_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{u8 addr[4];//tcp_setprio(newpcb, TCP_PRIO_MIN); 设置优先级printf("有新的客户端连接!\n");addr[3]=(newpcb->remote_ip.addr>>24)&0xFF;addr[2]=(newpcb->remote_ip.addr>>16)&0xFF;addr[1]=(newpcb->remote_ip.addr>>8)&0xFF;addr[0]=(newpcb->remote_ip.addr>>0)&0xFF;printf("ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]);printf("端口号:%d\n",newpcb->remote_port);printf("当前队列剩余字节:%d\n",tcp_sndbuf(newpcb));tcp_write(newpcb,"1234567890",10,1); //将要发送的数据提交到发送队列(不会立即发送)tcp_output(newpcb); //提示系统现在,发送数据tcp_sent(newpcb,TCP_sent); //发送成功的回调函数tcp_recv(newpcb,TCP_recv);return ERR_OK;
}err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len)
{printf("成功发送:%d字节\n",len);//tcp_close(tpcb); //关闭客户端连接return ERR_OK;
}u8 rx_buff[1024];
err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err)
{u32 rx_cnt=0;struct pbuf *q;memset(rx_buff,0,sizeof(rx_buff));if(p==NULL){printf("客户端已经断开连接!\n");}else{for(q=p;q!=NULL;q=q->next){memcpy(rx_buff+rx_cnt,q->payload,q->len);rx_cnt+=q->len;}pbuf_free(p); //释放PUFFprintf("成功接收:%d字节\n",rx_cnt);printf("收到的数据=%s\n",rx_buff);}return ERR_OK;
}
5.5 创建TCP客户端示例
u8 TCP_Create(u16_t port)
{struct tcp_pcb *pcb=NULL;pcb=tcp_new(); //创建套接字ip_addr_t ipaddr;if(pcb==NULL)return 1;IP4_ADDR(&ipaddr,192,168,31,54); //在ip_addr.h里定义tcp_connect(pcb,&ipaddr,port,TCP_connected);return 0;
}err_t TCP_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{u8 addr[4];//tcp_setprio(newpcb, TCP_PRIO_MIN); 设置优先级printf("服务器连接成功!\n");addr[3]=(tpcb->remote_ip.addr>>24)&0xFF;addr[2]=(tpcb->remote_ip.addr>>16)&0xFF;addr[1]=(tpcb->remote_ip.addr>>8)&0xFF;addr[0]=(tpcb->remote_ip.addr>>0)&0xFF;printf("服务器ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]);printf("服务器端口号:%d\n",tpcb->remote_port);printf("当前队列剩余字节:%d\n",tcp_sndbuf(tpcb));tcp_write(tpcb,"1234567890",10,1); //将要发送的数据提交到发送队列(不会立即发送)tcp_output(tpcb); //提示系统现在,发送数据tcp_sent(tpcb,TCP_sent); //发送成功的回调函数tcp_recv(tpcb,TCP_recv);return ERR_OK;
}err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len)
{printf("成功发送:%d字节\n",len);//tcp_close(tpcb); //关闭客户端连接return ERR_OK;
}u8 rx_buff[1024];
err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err)
{u32 rx_cnt=0;struct pbuf *q;memset(rx_buff,0,sizeof(rx_buff));if(p==NULL){printf("服务器已经断开连接!\n");}else{for(q=p;q!=NULL;q=q->next){memcpy(rx_buff+rx_cnt,q->payload,q->len);rx_cnt+=q->len;}printf("成功接收:%d字节\n",rx_cnt);printf("收到的数据=%s\n",rx_buff);}return ERR_OK;
}