首页资源分类其它科学普及 > 以太网ENC28J60教程

以太网ENC28J60教程

已有 445125个资源

下载专区

文档信息举报收藏

标    签:ENC28J60

分    享:

文档简介

关于在stm32下配置ENC28J60的技术教程

文档预览

第 1 章 以太网-Lwip 火哥注:该章教程是以野火 M3-V3 为蓝本来讲解的,其中以太网模块用的是 SPI1 接 口,现在 ISO/ISO-MINI 这两个开发板提供的程序和视频是基于 SPI2 接口的,只是 IO 不一 样,其中教程里面分析的原理和协议等都是完全一样的,可参考学习。对于教程中涉及到 的操作过程,可参考野火的视频教程,更直观明了。 ISO / ISO-MINI 的用户,程序以光盘提供的为准,不要完全复制教程里面的代码,以 免出现实验不成功的现象。 互联网技术对人类社会的影响不言而喻。当今大部分电子设备都能以不同的方式接入 互联网(Internet),在家庭中 PC 常见的互联网接入方式是使用路由器(Router)组建小型局域 网(LAN),利用调制调解器(modem)经过电话线网络,连接到互联网服务提供商(ISP),由互 联网服务提供商把用户的局域网接入互联网。而企业或学校的局域网规模较大,常使用交 换机组成局域网,经过路由以不同的方式接入到互联网中。 1.1 互联网模型 互联网技术的发展过程中,曾出现过 OSI 七层模型和 TCP/IP 四层模型,两种模型分别 有对应的协议,这两种模型和协议都有各自的优缺点。而最终 TCP/IP 改进模型(混合模型) 及其协议被广泛使用。 设计网络时,为了降低网络设计的复杂性,对组成网络的硬件、软件进行封装、分 层,这些分层即构成了网络体系模型。在两个设备相同层之间的对话、通信约定,构成了 层级协议。设备中使用的所有协议加起来统称协议栈。这三个概念 是互联网技术的核心。 虽然当前互联网的模型没有被明确规定,但我们一般可以使用五层的混合参考模型来 理解。见图 26- 1。 应用层 传输层 网络层 数据链路层 物理层 DNS、HTTP、 SMTP 邮件协议 主要使用 TCP、 UDP 协议 主要为 IP、 ICMP、ARP 协议 又分为 LLC 层 和 MAC 层 主要定义物理 传输介质 图 26- 1 混合参考模型 本书开篇便强调了分层在解决计算机科学问题的强大作用,在各个章节中都有不同程 度的体现,网络体系模型对网络传输问题的分层解决则再次印证了分层思想的作用。在这 个网络模型中,每一层完成不同的任务,都提供接口供上一层访问。而在每层的内部,可 以使用不同的方式来实现接口, 因而内部的改变不会影响其它层。 在本参考模型中,数据链路层又被分为 LLC 层(逻辑链路层)和 MAC 层(媒体介质访问 层)。目前,对于普通的接入网络终端的设备, LLC 层和 MAC 层是软、硬件的分界线。如 PC 的网卡主要负责实现参考模型中的 MAC 子层和物理层,在 PC 的软件系统中则有一套 庞大程序实现了 LLC 层及以上的所有网络层次的协议。 由硬件实现的物理层和 MAC 子层在不同的网络形式有很大的区别,如以太网和 WiFi,这是由物理传输方式决定的。但由软件实现的其它网络层次通常不会有太大区别,在 PC 上也许能实现完整的功能,支持所有协议,而在嵌入式领域则按需要进行裁剪。本章的 网络模型的硬件部分用以太网为例讲解,软件部分以 LwIP 协议栈讲解。 1.2 以太网 以太网(Ethernet)是互联网技术的一种,由于它是在组网技术中占的比例最高,很多人 直接把以太网理解为互联网。 以太网是指遵守 IEEE 802.3 标准组成的局域网,由 IEEE 802.3 标准规定的主要是位于 参考模型的物理层(PHY)和数据链路层中的媒体接入控制子层(MAC)。在家庭、企业和学 校所组建的 PC 局域网形式一般也是以太网,其标志是使用水晶头网线来连接(当然还有其 它形式)。IEEE 还有其它局域网标准,如 IEEE 802.11 是无线局域网,俗称 Wi-Fi 。IEEE 802.15 是个人域网,即蓝牙技术,其中的 802.15.4 标准则是当前非常热门的 ZigBee 技术。 渗透到工业控制、环境监测、智能家居的嵌入式设备产生了接入互联网的需求,利用 以太网技术,嵌入式设备可以非常容易地接入到现有的计算机网络中。 1.2.1 PHY 层 在物理层,由 IEEE 802.3 标准规定了以太网使用的传输介质、传输速度、数据编码方 式和冲突检测机制。 1. 传输介质 传输介质包括同轴电缆、双绞线(水晶头网线是一种双绞线)、光纤。根据不同的传输 速度和距离要求,基于这三类介质的信号线又衍生出很多不同的种类。最常用的是“五类 线 ” 适 用 于 100BASE-T 和 10BASE-T 的 网 络 , 它 们 的 网 络 速 率 分 别 为 100Mbps 和 10Mbps。 2. 编码 为了让接收方在没有外部时钟参考的情况也能确定每一位的起始、结束和中间位置, 在传输信号时不直接采用二进制编码。在 10BASE-T 的传输方式中采用曼彻斯特编码,在 100BASE-T 中则采用 4B/5B 编码。 曼彻斯特编码把每一个二进制位的周期分为两个间隔,在表示“1”时,以前半个周期 为高电平,后半个周期为低电平。表示“0”时则相反,见图 26- 2。 图 26- 2 曼彻斯特编码 采用曼彻斯特码在每个位周期都有电压变化,便于同步。但这样的编码方式效率太 低,只有 50%。 在 100BASE-T 采用的 4B/5B 编码是把待发送数据位流的每 4 位分为一组,以特定的 5 位编码来表示,这些特定的 5 位编码能使数据流有足够多的跳变,达到同步的目的,而且 效率也从曼彻斯特编码的 50%提高到了 80%。 3. CSMA/CD 冲突检测 早期的以太网大多是多个节点连接到同一条网络总线上(总线型网络),存在信道竞争 问题,因而每个连接到以太网上的节点都必须具备冲突检测功能。以太网具备 CSMA/CD 冲突检测机制,如果多个节点同时利用同一条总线发送数据,则会产生冲突,总线上的节 点可通过接收到的信号与原始发送的信号的比较检测是否存在冲突,若存在冲突则停止发 送数据,随机等待一段时间再重传。 现在大多数局域网组建的时候很少采用总线型网络,大多是一个设备接入到一个独立 的路由或交换机接口,组成星型网络,不会产生冲突。但为了兼容,新出的产品还是带有 冲突检测机制。 1.2.2 MAC 子层 1. MAC 的功能 MAC 子层是属于数据链路层的下半部分,它主要负责与物理层进行数据交接,如是否 可以发送数据,发送的数据是否正确,对数据流进行控制等。它自动对来自上层的数据包 加上一些控制信号,交给物理层。接收方得到正常数据时,自动去除 MAC 控制信号,把 该数据包交给上层。 2. MAC 数据包 IEEE 对以太网上传输的数据包格式也进行了统一规定,见图 26- 3。该数据包被称为 MAC 数据包。 图 26- 3 MAC 数据包格式 MAC 数据包由前导字段、帧起始定界符、目标地址、源地址、数据包类型、数据域、 填充域、校验和域组成。  前导字段,这是一段方波,用于使收发节点的时钟同步。而帧起始定界符则用于区分 前导段与数据段的。字段和帧起始定界符在 MAC 收到数据包后会自动过滤掉。  MAC 地址: MAC 地址由 48 位数字组成,它是网卡的物理地址,在以太网传输的最 底层,就是根据 MAC 地址来收发数据的。部分 MAC 地址用于广播和多播,在同一个 网络里不能有两个相同的 MAC 地址。PC 的网卡在出厂时已经设置好了 MAC 地址, 但也可以通过一些软件来进行修改,在嵌入式的以太网控制器中可由程序进行配置。 数据包中的 DA 是目标地址,SA 是源地址。  数据包类型:本区域可以用来描述本 MAC 数据包是属于 TCP/IP 协议层的 IP 包、ARP 包还是 SNMP 包,也可以用来描述本 MAC 数据包数据段的长度。  数据段:数据段是 MAC 包的核心内容,它包含的数据来自 MAC 的上层。其长度可以 从 0~1500 字节间变化。  填充域:由于协议要求整个 MAC 数据包的长度至少为 64 字节(接收到的数据包如果少 于 64 字节会被认为发生冲突,数据包被自动丢弃),当数据段的字节少于 46 字节时, 在填充域会自动填上无效数据,以使数据包符合长度要求。  校验和域:MAC 数据包的尾部是校验和域,它保存了 CRC 校验序列,用于检错。 1.2.3 以太网控制器 1. 集成 MAC 控制器的 MCU 接入方案 PHY 层和 MAC 层的实现是由以太网控制器实现的。部分 MCU 如 STM32F107 型号的 芯片,集成了 MAC 控制器外设,在芯片外再外接一个 PHY 控制器 和以太网变压器接口即 可实现以太网功能。它们的关系可以类比 STM32 的 CAN 通信模型,STM32 集成了 CAN 控制器,但要实现 CAN 通信还需要外接 CAN 收发器芯片。以太网变压器的功能是增强、 隔离信号及与 RJ45 水晶头进行连接。 STM32 的 MAC 控制器外设可通过 IEEE 协议规定的 MII 或 RMII、SMI 与 PHY 芯片 进行通讯。STM32F107 型号芯片使用 MII 接口与 PHY 构成的以太网接口见图 26- 4。(省略 了以太网变压器)。 图 26- 4 STM32F107 与 PHY 芯片构成以太网接口 2. 外接以太网控制器方案 对于没有集成以太网控制器的 MCU,可通过外接以太网控制器芯片接入以太网。本书 使用的 STM32F103VET6 型号的 MCU 没有集成以太网控制器,所以采用 STM32 外接常用 的嵌入式以太网控制器 ENC28J60 接入网络。 ENC28J60 芯片是兼容 IEEE 802.3 的以太网控制器,集成 MAC 控制器和 10 BASE-T PHY 控制器,自带缓冲区、DMA,使用 SPI 接口与 MCU 进行通讯。MCU 使用 SPI 对 ENC28J60 芯片的寄存器写入控制参数和接收数据,实现以太网的功能。它与 MCU 连接组 成的以太网接口见图 26- 5。 图 26- 5 MCU 与 ENC28J60 组成以太网接口 使用以太网控制器,实现了混合模型的 MAC 子层和物理层,就可以利用它进行基本 的收发 MAC 数据包了。 1.3 MAC 之上的网络层 1.3.1 为什么在 MAC 之上还有分层? 如果两个 STM32 开发板,都实现了以太网接口的 MAC 子层和物理层,我们使用网线 把这两个板子连接起来,针对接收端和发送端写两个代码,使得在 MAC 控制器收取到 MAC 数据包时,把位于数据段的内容提取出来,毫无疑问这是可以实现数据传输的,如果 读者想尝试这样的功能,可以参考配本书的 NRF24L01 无线传输例程。 但是如果使用以太网仅仅是为了实现这样的功能真是大材小用了,而且还是有线传 输,实在是鸡肋。想实现用 STM32 开发板与 PC 通讯,如果不同的 PC 使用不同的系统, 或者设备使用嵌入式系统,针对每种差异,都需要分别写一套软件,这样会过于复杂。 使用以太网接口的目的就是为了方便与其它设备互联,如果所有设备都约定使用一种 互联方式,在软件上加一些层次来封装,这样不同系统、不同的设备通讯就变得相对容易 了。而且只要新加入的设备也使用同一种方式,就可以直接与之前存在于网络上的其它设 备通讯。这就是为什么产生了在 MAC 之上的其它层次的网络协议及为什么要使用协议栈 的原因。又由于在各种协议栈中 TCP/IP 协议栈 得到了最广泛使用,所有接入互联网的设 备都遵守 TCP/IP 协议。所以,想方便地与其它设备互联通信,需要提供对 TCP/IP 协议的 支持。 1.3.2 各网络层的功能 用以太网和 Wi-Fi 作例子,它们的 MAC 子层和物理层有较大的区别,但在 MAC 之上 的 LLC 层、网络层、传输层和应用层的协议,是基本上同的,这几层协议由软件实现,并 对各层进行封装。根据 TCP/IP 协议,各层的要实现的功能如下: LLC 层:处理传输错误;调节数据流,协调收发数据双方速度,防止发送方发送得太 快而接收方丢失数据。主要使用数据链路协议。 网络层:本层也被称为 IP 层。LLC 层负责把数据从线的一端传输到另一端,但很多时 候不同的设备位于不同的网络中(并不是简单的网线的两头)。此时就需要网络层来解决子 网路由拓扑问题、路径选择问题。在这一层主要有 IP 协议、ICMP 协议。 传输层:由网络层处理好了网络传输的路径问题后,端到端的路径就建立起来了。传 输层就负责处理端到端的通讯。在这一层中主要有 TCP、UDP 协议 应用层:经过前面三层的处理,通讯完全建立。应用层可以通过调用传输层的接口来 编写特定的应用程序。而 TCP/IP 协议一般也会包含一些简单的应用程序如 Telnet 远程登 录、FTP 文件传输、SMTP 邮件传输协议。 实际上,在发送数据时,经过网络协议栈的每一层,都会给来自上层的数据添加上一 个数据包的头,再传递给下一层。在接收方收到数据时,一层层地把所在层的数据包的头 去掉,向上层递交数据。见图 26- 6。 图 26- 6 数据经过每一层的封装和还原 1.3.3 LwIP 协议栈 1. LwIP 协议栈简介 要实现 TCP/IP 协议栈的,把每一层都处理好,这样的代码当然可以自己亲自写,但我 们一般会移植更加稳定和性能优良的代码。最著名的实现了 TCP/IP 协议栈的代码是 BSD TCP/IP 协议栈,大多数专业的协议栈都是由它派生出来的,由它提供的接口 socket 连接模 式也几乎成为了通用标准。但 BSD 协议栈比庞大,大多数小型的嵌入式设备不宜使用,所 以人们还开发出了 uc/IP、LwIP、uIP 等适用于嵌入式设备使用的协议栈。 LwIP 是 Light Weight Internet Protocol 的缩写,是由瑞士计算机科学院 Adam Dunkels 等开发的适用于嵌入式领域的轻量级 TCP/IP 协议栈。它可以移植到含有操作系统的平台 中,也可以在无操作系统的平台下运行。由于它开源、占用的 RAM 和 ROM 比较少、支持 较为完整的 TCP/IP 协议、且十分便于裁剪、调试,被广泛应用在中低端的 32 位控制器平 台。 2. 获取 LwIP 协议栈 LwIP 协议栈是开源的,可以在网址 http://download.savannah.gnu.org/releases/lwip/ 下 载得到,目前最新版本 1.4.0 版刚推出不久,尚不清楚其稳定性如何,本章采用 LwIP 的 1.3.2 版 本 进 行 讲 解 。 从 该 网 站 中 可 以 下 载 到 lwip-1.3.2.zip 文 件 和 相 应 版 本 的 contrib1.3.0.zip 文件,它们包含了 LwIP 协议栈的核心源代码及应用、移植例程。 解压 lwip-1.3.2.zip 文件后,其目录\lwip-1.3.2\src 的内容如图 26- 7。 图 26- 7 lwip 的 src 目录 在 src 目录下的 api 文件夹保存的文件中,包含了适用于具有操作系统平台调用的应用 层接口函数。core 文件夹下的文件内容为 LwIP 协议栈对于各种协议的实现。netif 下的文 件则保存了与硬件底层关系比较紧密的函数。这三个文件夹下的都是 c 源文件,它们的头 文件都被保存到 include 文件夹中。在实际应用中,可根据需要进行裁剪。 在移植的时候,我们也常常需要利用 contrib-1.3.0.zip 中的文件。解压后,在\contrib1.3.0_\contrib\ports 目录下有一些针对特定平台移植时使用的文件,选择进入其中一个目 录,如\contrib-1.3.0_\contrib\ports old\6502\include\arch,其内容如图 26- 8。 图 26- 8 特定头文件 我们需要用到的是 cc.h、perf.h 和 sys_arch.h 文件,通常把它复制出来存放到自己工程 中的 arch 文件夹中。cc.h 包含了 LwIP 对于基本数据类型的定义。sys_arch.h 定义了与系统 有关的信号量、邮箱及线程。 1.4 ENC28J60+LwIP 以太网实验 1.4.1 实验描述及工程文件清单 实验描述: 使用野火开发板利用 ENC28J60 以太网控制器接入局域网,移植 LwIP 协议栈,并利用 协议栈提供的函数在 STM32 上建立服务器和创建 Telnet 应用。 建立服务器后可以使用 PC 的浏览器访问网页,通过鼠标点击网页按钮,控制开发板 上的 LED 灯。Telnet 应用,利用 PC 上的 Telnet 程序向 STM32 输入命令,控制 LED 灯。 本实验分析十分复杂,对网络应用不熟悉的读者,务必先阅读本章的实验步骤和实验 现象,这样既能有个大概了解,也能激起学习的兴趣。 硬件连接: PB13 :ENC28J60-INT PA6-SPI1-MISO :ENC28J60-SO PA7-SPI1-MOSI :ENC28J60-SI PA5-SPI1-SCK :ENC28J60-SCK PA4-SPI1-NSS :ENC28J60-CS PE1 :ENC28J60-RST 库文件: 使用 3.5 版本固件库 startup/start_stm32f10x_hd.c CMSIS/core_cm3.c CMSIS/system_stm32f10x.c FWlib/stm32f10x_spi.c FWlib/stm32f10x_rcc.c FWlib/stm32f10x_gpio.c FWlib/stm32f10x_usart.c 用户文件: USER/main.c USER/stm32f10x_it.c USER/SysTick.c USER/usart1.c USER/led.c USER/ENC28J60.c USER/SPI.c USER/httpd.c USER/netconfig.c USER/cmd.c LwIP 文件: 使用 1.3.2 版本 LwIP 协议栈 lwip-1.3.2.zip 文件解压后 src 中的源文件,具体见工程环境配置。 图 26- 9 ENC28J60 与 STM32 硬件连接图 1.4.2 配置工程环境 以太网实验中我们用到了 GPIO、RCC、SPI、USART 外设,所以我们先要把以下库文 件添加到工程:stm32f10x_gpio.c 、stm32f10x_rcc.c、stm32f10x_spi.c、stm32f10x_usart.c。 实验中还使用了旧工程中的 led.c 和 systick.c 文件用于网络控制 LED 和给 LwIP 协议栈提供 定时功能。 接下来就要新建文件编写 SPI 初始化函数和编写 ENC28J60 的驱动文件了,由于本次 实验使用的文件十分多,所以把 SPI.C 文件和 ENC28J60.C 文件放到新的分组中管理。 在应用层则创建了三个文件用来配置网卡、服务器和 telnet 应用程序,这三个文件分 别为 httpd.c、netconfig.c 和 cmd.c。 最后,LwIP 协议栈整个文件夹 lwip-1.3.2 复制到目录下,从 contrib-1.3.0.zip 中复制 cc.h 等头文件保存到新建的 ARCH 文件夹下。把使用到的 LwIP 协议栈的源文件都按它在 scr 的分组添加到工程,添加后的结果见图 26- 12。 图 26- 10 添加工程文件 其中带有“+”号的 LwIP 组下,都包含 LwIP 协议文件包中相应文件夹的所有源文件 (文件太多,无法一一列出,请参照例程),没有“+”号的 ipv6 和 ppp 工程组相应的源文件 则因为没有被使用到而未被添加进工程。 添加完工程后,还要在 c/c++编译器选项中添加头文件路径,主要为 LwIP 的头文件, 见图 26- 11。 1.4.3 main 文件 图 26- 11 添加头文件路径 无论工程多么复杂,坚持从 main 文件理解不动摇。本工程中的 main 文件主要内容见 代码清单 26- 1。 代码清单 26- 1:以太网例程的 main 函数 1. /********************************************************************* ********************************************************/ 2. 3. __IO uint32_t LocalTime = 0; 4. /* 该变量每 10ms 改变一次,增量为 10*/ 5. 6. /* 7. * 函数名:main 8. * 描述 :主函数 9. * 输入 :无 10. * 输出 : 无 11. * 调用 : 12. */ 13. int main(void) 14. { 15. /*初始化串口*/ 16. USART1_Config(); 17. 18. /*初始化 以太网 SPI 接口*/ 19. ENC_SPI_Init(); 20. 21. /*初始化 systick,用于定时轮询输入或给 LWIP 提供定时*/ 22. SysTick_Init(); 23. 24. /*初始化 LED 使用的端口*/ 25. LED_GPIO_Config(); 26. 27. printf("\r\n*****野火 STM32_enc8j60+lwip 移植实验******\r\n"); 28. 29. /* 初始化 LWIP 协议栈*/ 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. } 45. LwIP_Init(); /*初始化 web server 显示网页程序*/ httpd_init(); /* 初始化 telnet 远程控制 程序 */ CMD_init(); /* Infinite loop */ while ( 1 ) { /*轮询*/ LwIP_Periodic_Handle(LocalTime); } 在 main 函数中,使用 USART1_Config()配置了串口驱动;用 ENC_SPI_Init()配置好了 与 ENC28J60 进 行 通 讯 的 SPI 接 口 ; SysTick_Init() 配 置 了 用 于 给 LwIP 定 时 的 systick ; LED_GPIO_Config()则对控制 LED 的 GPIO 进行了初始化。 LwIP_Init()初始化了 LwIP 协议栈,它设置了网络接口的 IP、子网掩码、网关,并使 能了 ENC28J60。httpd_init()完成了建立服务器所需的状态,如监听网络端口,配置连接后 调用的各种函数,以完成网页控制 LED 的功能。类似地,CMD_init()完成了 telnet 控制 LED 的准备。 最后,就 在 while 死循环中 调用 LwIP_Periodic_Handle()根据 LocalTime 变量的计 时 值,轮询网络接口和给 LwIP 协议栈系统提供定时。 1.4.4 LwIP 对底层数据结构的封装 了解了 main 函数的执行流程后,读者可能会觉得奇怪为何没有关于 ENC28J60 接口驱 动的初始化呢 ?(ENC_SPI_Init()函数仅仅完成了 SPI 接口的 GPIO、SPI 模式配置) ,实际 上它是在 LwIP_Init()函数完成的,但由于封装的层次比较多,所以不仔细分析很难了解到 它是如何完成对底层的 ENC28J60 进行初始化的。 由于硬件底层使用的设备十分容易发生变化,同时也为了使 LwIP 的适用范围更广, 就像文件系统的底层驱动是留给移植者完成一样,LwIP 协议栈给底层的驱动提供了接口, 如初 始化 网 卡、 发 送数 据 包、 接 收数 据 包 等 最底 层 的操 作 。这 些 接口 定 义位 于\lwip1.3.2\src\netif 目录下的 ethernetif.c 文件,该文件已经被添加进了工程中,接口的具体实现 因使用的网络接口不同而有所区别,需要我们移植者来完成。 在编 写这 些接口 相关 的内 容时, 涉及 到了两 个关 系密 切的 LwIP 的底 层数 据结 构 pbuf、netif 结构体。 1. pbuf 结构体 LwIP 的底层接口涉及到了大量对 pbuf、netif 结构体赋值的内容,我们先来分析它们的 结构体成员。 当 MAC 层收到 MAC 数据包的时候,它会把 MAC 包中的前导字段和帧起始定界符过 滤掉,把剩余的数据段都保存起来,交给 LwIP 的 pbuf 结构体类型的变量,由 LwIP 协议 栈进行处理。pbuf 结构体在 pbuf.h 文件中定义见代码清单 26- 2。 代码清单 26- 2:pbuf 结构体定义 1. struct pbuf { 2. /** next pbuf in singly linked pbuf chain */ 3. struct pbuf *next; 4. 5. /** pointer to the actual data in the buffer */ 6. void *payload; 7. 8. /** 9. * total length of this buffer and all next buffers in chain 10. * belonging to the same packet. */ 11. u16_t tot_len; 12. 13. /** length of this buffer */ 14. u16_t len; 15. 16. /** pbuf_type as u8_t instead of enum to save space */ 17. u8_t type; /*pbuf_type*/ 18. 19. /** misc flags */ 20. u8_t flags; 21. 22. /** 23. * the reference count always equals the number of pointers 24. * that refer to this pbuf. This can be pointers from an application , * the stack itself, or pbuf->next pointers from a chain. 25. */ 26. u16_t ref; 27. 28. }; 其成员作用如下: (1) next 这是一个指向 pbuf 类型的指针,它用于指向下一个 pbuf 结构体。由于每个 MAC 包的 数据段最大只是 1500 字节,而来自上层的数据包往往很大,LwIP 把大数据包分装到多个 pbuf 结构体,并把这些 pbuf 组成链表,通过 next 指针来索引。 (2) payload 这是一个指向实际数据的指针。当 MAC 控制器接收到 MAC 数据包时,整个 MAC 包 (包括 MAC 地址)直接被存储到 payload 指向的存储区。 当需要发送数据时,LwIP 一层层地给原始数据加上数据头,最终组装成 MAC 包的格 式,也保存到 payload 指向的存储区,然后把这些数据交给底层的 MAC 控制器发送出去。 (3) tot_len 和 len len 成员表示本 pbuf 结构体的长度,tot_len 表示本 pbuf 及下一个 pbuf (next 指向的 pbuf)长度的和。 例如 pbuf A 的 next 成员指向 pbuf B,若 pbuf A 的 len 值为 500,pbuf B 的 len 值为 600,那么 pbuf A 的 tot_len 长度就等于 500+600,而 pbuf B 的 tot_len 长度就等于 600+下一 个 pbuf 的 len 。 若 pbuf C 是 pbuf 链 表 中 的 最 后 一 个 , 那 么 pbuf C 的 len 就 等 于 它 的 tot_len,从而就可以判断链表的结束。 (4) type 和 flags type 表 示 pbuf 的 存 储 类 型 , 共 有 四 种 , 分 别 为 PBUF_RAM 、 PBUF_ROM 、 PBUF_REF 和 PBUF_POOL,针对不同的应用而使用不同的存储方式。flags 在存储分配时 被赋值为 0,作用不明,可能是作者为了兼容而保留的。 (5) ref ref 用于记录 pbuf 的访问次数,在 LwIP 中存在一根据访问次数来决定内存释放的机制 从上面的分析可以了解到 LwIP 利用 pbuf 结构体来进行与底层硬件的交接,用 payload 存储数据包的指针,为了便于管理,使用 next、len、tot_len、type、ref 成员来定义 pbuf 的 属性。 最后,参考图 26- 12 可以让我们更加清晰地了解 pbuf 的结构,该图是存储形式为 PBUF_POOL 类型的结构体存储分布,其它类型的 pbuf 也是类似的。 2. netif 结构体 图 26- 12 PBUF_POOL 类型的存储方式 LwIP 通过 pbuf 建立了与底层硬件收发数据包的数据结构,可以实现数据的管理。但 很多时候,收发数据还需要清楚网卡的状态,特别是网卡地址、IP 地址、网关等设置,有 的设备上还可能有多个网卡,这就需要一个结构体来保存这些信息,让 LwIP 进行管理和 使用。这些信息被保存在 netif 结构体中,它的定义在 netif.h 文件定义见代码清单 26- 3。 (为了便于讲解,在初学时,只分析重点代码,野火只保留了重点与网卡相关的结构体成 员,条件编译部分都被删除了,想了解完整版的读者可参考源码) 。 代码清单 26- 3:netif 结构体 1. struct netif { 2. /** pointer to next in linked list */ 3. struct netif *next; 4. 5. /** IP address configuration in network byte order */ 6. struct ip_addr ip_addr; 7. struct ip_addr netmask; 8. struct ip_addr gw; 9. 10. /** This function is called by the network device driver 11. * to pass a packet up the TCP/IP stack. */ 12. err_t (* input)(struct pbuf *p, struct netif *inp); 13. 14. /** This function is called by the IP module when it wants 15. * to send a packet on the interface. This function typically 16. * first resolves the hardware address, then sends the packet. */ 17. 18. err_t (* output)(struct netif *netif, struct pbuf *p, 19. struct ip_addr *ipaddr); 20. 21. /** This function is called by the ARP module when it wants 22. * to send a packet on the interface. This function outputs 23. * the pbuf as-is on the link medium. */ 24. err_t (* linkoutput)(struct netif *netif, struct pbuf *p); 25. 26. /** This field can be set by the device driver and could point 27. * to state information for the device. */ 28. 29. void *state; 30. 31. /** maximum transfer unit (in bytes) */ 32. u16_t mtu; 33. 34. /** number of bytes used in hwaddr */ 35. u8_t hwaddr_len; 36. 37. /** link level hardware address of this interface */ 38. 39. u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; 40. /** flags (see NETIF_FLAG_ above) */ 41. 42. u8_t flags; 43. /** descriptive abbreviation */ 44. 45. char name[2]; 46. /** number of this interface */ 47. 48. u8_t num; 49. 50. }; 各成员的作用如下: (1) next 这个 next 指针是 netif 结构体类型,它与 pbuf 的 next 指针作用类似,也是用作链表。 netif 结构体是用来存储网卡属性的,由 netif 结构体构成的链表即表示同一个设备上不同网 卡的属性,LwIP 就通过本 next 指针访问这个链表。 (2) ip_addr、netmask 和 gw 这三个结构体成员分别用来存储本网卡的 IP 地址、子网掩码和网关。 (3) err_t (* input)(struct pbuf *p, struct netif *inp) 这个结构体成员第一眼看上去有点奇怪,与以往碰到的都不一样,实际上这是一个函 数指针。这个函数是用来接收底层硬件输入的数据包的。函数的返回值是 err_t 类型,它是 由 LwIP 定义的一种用于表示操作成功、失败等参数的变量。函数的输入参数为 pbuf 类型 的 p 指针和 netif 类型的 inp 指针,表示我们要接收 inp 网卡通过底层硬件接收到的数据 包,并把数据包保存到结构体 p 中。 (4) err_t (* output)(struct netif *netif, struct pbuf *p, struct ip_addr *ipaddr) 本成员也是一个函数指针,它指向一个用于 IP 层输出的函数。在 IP 层有数据包需要 发送时,就通过该指针调用 output 函数。本函数输入参数有 netif、pbuf 和 ip_addr 三个参 数,表示使用 netif 网卡、把 pbuf 结构体的内容发送到 IP 地址为 ip_addr 的设备中。 但由 于在硬件底层收发数据时是使用 MAC 地址的,所以本函数把 IP 地址经过转化得到 MAC 地址后,调用下一个成员——linkoutput 函数。 (5) err_t (* linkoutput)(struct netif *netif, struct pbuf *p) 这个函数指针是指向最底层的网卡发送数据包函数。它是被上面的 output 函数调用 的。在 output 函数中的 pbuf 数据包实际上缺少了 MAC 包中的 DA 段(目标 MAC 地址), output 函数把它的输入参数 IP 地址转化成 MAC 地址后,添加到 pbuf 的 payload 成员,组 成完整 MAC 包,再由本函数 linkoutput 输出到网络中。 (6) state state 指针指向一用户感兴趣的信息,这部分可以由用户自行配置,也可以不使用。 (7) mtu 本成员记录了最大传输单元,我们知道 MAC 数据段的最大长度为 1500 字节,所以本 成员我们一般会直接赋值为 1500。 (8) hwaddr_len 和 hwaddr[NETIF_MAX_HWADDR_LEN] hwaddr_len 存储的是 MAC 地长度,hwaddr[]数组则存储了本网卡的 MAC 完整地址。 (9) flags 本成员保存了网卡允许使用的功能,如使用广播地址、ARP 功能等。 (10) name 和 num name 用于保存网络接口的名字,可以自由设置,名字是两个字节的 ASCII 编码。当网 络接口名字出现相同的时候,使用 num 的数值进行区分。 1.4.5 初始化协议栈 了解了 pbuf、netif 结构体后,我们进入被 mian 函数调用的 LwIP_Init()的具体代码中 (删减了部分扩展功能的代码),该函数位于自定义的 netconfig.c 文件,见代码清单 26- 4。 代码清单 26- 4:LwIP_Init()函数 1. /* 2. * 函数名:LWIP_Init 3. * 描述 :初始化 LWIP 协议栈,主要是把 enc28j60 与 LWIP 连接起来。 4. 包括 IP、MAC 地址,接口函数 5. * 输入 :无 6. * 输出 : 无 7. * 调用 :外部调用 8. */ 9. void LwIP_Init( void ) 10. { 11. struct ip_addr ipaddr; 12. struct ip_addr netmask; 13. struct ip_addr gw; 14. 15. /*调用 LWIP 初始化函数, 16. 初始化网络接口结构体链表、内存池、pbuf 结构体*/ 17. lwip_init(); 18. 19. IP4_ADDR(&ipaddr, 192, 168, 1, 18); //设置网络接口的 ip 地址 20. IP4_ADDR(&netmask, 255, 255, 255, 0); //子网掩码 21. IP4_ADDR(&gw, 192, 168, 1, 1); //网关 22. 23. /*初始化 enc28j60 与 LWIP 的接口,参数为网络接口结构体、ip 地址、 24. 子网掩码、网关、网卡信息指针、初始化函数、输入函数*/ 25. netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input); 26. 27. /*把 enc28j60 设置为默认网卡 .*/ 28. netif_set_default(&enc28j60); 29. 30. /* When the netif is fully configured this function must be called. */ 31. netif_set_up(&enc28j60); //使能 enc28j60 接口 32. } 函数执行流程如下: (1) 第 11~13 行,定义了三个 ip_addr 类型的变量用于存储 IP 地址、子网掩码、网关, 这些变量在第 19~21 行被赋值,本实验的网卡这三个属性就是由它们配置的。 (2) 第 17 行,调用 lwip_init()函数(名字跟 LwIP_Init()很相似,注意区分),lwip_init()是 由 LwIP 协议栈定义的函数,在使用 LwIP 协议栈相关的内容前,要先调用它对网 卡结构体链表、内存池、pbuf 进行初始化。 (3) 第 25 行,调用 netif_add()函数,它把我们配置好的网卡属性赋值到 netif 类型的 enc28j60 变量中,它是在本文件的开头定义的(被省略了)。 如 ip 地址、子网掩 码、网关地址、网卡初始化函数 ethernetif_init()、网卡输入函数 ethernet_input()。 在本函数之后的 LwIP 协议栈操作,只要使用到了相关的底层服务,都通过这个 netif 类型的 enc28j60 变量来访问。代码中的 28~31 行调用两个函数的作用分别是 设置 enc28j60 结构体指向的网卡为默认网卡,并使能。 1. netif_add()函数 netif_add()是上面提到的重点函数,它是由 LwIP 协议栈定义的,它的使用方法和我们 使用 STM32 固件库初始化外设时类似,先把初始化结构体配置好,然后调用 xxx_init 函数 把这些配置写入到寄存器。netif_add()函数则把我们对网卡属性的配置写入到该函数的输入 参数 netif 中,本实验调用时该参数是变量 enc28j60。netif_add()在 netif.c 文件的定义见代 码清单 26- 5(有删减)。 代码清单 26- 5:netif_add()函数 1. struct netif * 2. netif_add(struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask, 3. struct ip_addr *gw, 4. void *state, 5. err_t (* init)(struct netif *netif), 6. err_t (* input)(struct pbuf *p, struct netif *netif)) 7. { 8. static u8_t netifnum = 0; 9. 10. /* reset new interface configuration state */ 11. netif->ip_addr.addr = 0; //复位结构体成员 12. netif->netmask.addr = 0; 13. netif->gw.addr = 0; 14. netif->flags = 0; 15. 16. /* remember netif specific state information data */ 17. netif->state = state; 18. netif->num = netifnum++; 19. netif->input = input; //输入函数 20. 21. netif_set_addr(netif, ipaddr, netmask, gw); //设置三个地址 22. 23. /* call user specified initialization function for netif */ 24. if (init(netif) != ERR_OK) { 25. return NULL; 26. } //初始化函数 27. 28. /* add this netif to the list */ 29. netif->next = netif_list; //加入链表 30. netif_list = netif; // 31. snmp_inc_iflist(); 32. 33. return netif; 34. } 从它的源码中可以看出这个函数就是完成了对 netif 结构体的赋值,在这里有一个问 题 , 调 用 netif_add() 函 数 时 ( 见 LwIP_Init() 中 的 调 用 ) , 输 入 的 参 数 ethernetif_init 和 ethernet_input 分别是网卡初始化函数和输入函数的指针,这两个指针指向的函数具体是如 何定义的呢? 1.4.6 LwIP 对底层操作的封装 要解决上一小节的问题,需要对了解 LwIP 对底层操作的封装。对这些操作封装后给 上层提供的接口呈现在 LwIP 协议栈中就是上述的函数指针形式,LwIP 协议栈内部的函数 通过这些指针来调用函数。这些接口都保存在\lwip-1.3.2\src\netif 文件夹下的 ethernetif.c 文 件中。它已被添加至本实验工程的 lwip-netif 组中。 1. ethernetif_init 指针 首先看看网卡初始化函数 ethernetif_init(),它并不是最底层的,该函数的具体定义都 是由 LwIP 协议栈已经完全编写好。其定义见代码清单 26- 6。 代码清单 26- 6: ethernetif_init()函数 1. err_t ethernetif_init( struct netif *netif ) 2. { 3. struct ethernetif *ethernetif; 4. 5. ethernetif = mem_malloc( sizeof(struct ethernetif) ); //分配内存 6. 7. if( ethernetif == NULL ) //debug 时使用的输出 8. { 9. LWIP_DEBUGF( NETIF_DEBUG, ("ethernetif_init: out of memory\n\r ") ); 10. return ERR_MEM; 11. } 12. 13. netif->state = ethernetif; 14. netif->name[0] = IFNAME0; //网卡名字 15. netif->name[1] = IFNAME1; 16. netif->output = etharp_output; //IP 层输出函数 17. netif->linkoutput = low_level_output; //底层硬件输出函数 18. 19. ethernetif->ethaddr = ( struct eth_addr * ) &( netif- >hwaddr[0] ); //MAC 地址 20. 21. low_level_init( netif ); //最底层初始化函数 22. 23. return ERR_OK; 24. } 在上一小节的 netif_add()只是对 netif 部分的结构体成员进行了赋值,还有一些成员如 state、name、output 函数指针、linkoutput 函数指针及 mac 地址成员,就是在本函数中进行 赋 值 的 , 见 第 13~21 行 。 来 到 这 个 函 数 , 我 们 终 于 发 现 了 最 底 层 的 输 出 函 数 指 针 low_level_output 和最底层的初始化函数指针 low_level_init。 2. 底层输出函数 LwIP 协议栈的原文件只是提供了 low_level_output 函数的模型,野火对该函数的具体 实现见代码清单 26- 7。 代码清单 26- 7:low_level_output()函数 1. /* 2. * low_level_output(): Should do the actual transmission of the packet. The 3. * packet is contained in the pbuf that is passed to the function. Thi s pbuf 4. * might be chained. 5. */ 6. static err_t low_level_output( struct netif *netif, struct pbuf *p ) /*底层发送数据函数*/ 7. { 8. struct pbuf *q; 9. int i = 0; 10. 11. err_t xReturn = ERR_OK; 12. 13. /* Parameter not used. */ 14. for(q = p; q != NULL; q = q->next) 15. { 16. //复制 pbuf 内容 17. memcpy(&Tx_Data_Buf[i], (u8_t*)q->payload, q->len); 18. i = i + q->len; 19. } 20. enc28j60PacketSend(i,Tx_Data_Buf); //发送数据包 21. 22. return xReturn; 23. } 本函数看上去十分简单,在上层需要发送数据的时候,以 pbuf 作为输入参数调用 low_level_output()函数。而 pbuf 链表的所有 payload 成员就是需要发送的 MAC 数据包,每 个数据包的长度为 len 成员的值。函数中利用 for 循环调用 memcpy()把 q->payload 链表的 数 据 都 取 出 来 存 放 在 Tx_Data_Buf 数 组 中 , 最 后 调 用 enc28j60PacketSend() 函 数 把 Tx_Data_Buf 数组发送出去。enc28j60PacketSend()是在 ENC28J60.C 文件编写好的 enc28j60 驱动,它使用 SPI 接口控制 enc28j60 芯片发送数据。 3. 底层初始化函数 最底层的网卡初始化函数 low_level_init()的定义见代码清单 26- 8。 代码清单 26- 8: low_level_init()函数 1. static void low_level_init( struct netif *netif ) 2. { 3. /* set MAC hardware address length */ 4. netif->hwaddr_len = 6; 5. 6. /* set MAC hardware address */ 7. /* MAC 地址 */ 8. netif->hwaddr[0] = macaddress[0]; 9. netif->hwaddr[1] = macaddress[1]; 10. netif->hwaddr[2] = macaddress[2]; 11. netif->hwaddr[3] = macaddress[3]; 12. netif->hwaddr[4] = macaddress[4]; 13. netif->hwaddr[5] = macaddress[5]; 14. 15. /* maximum transfer unit */ 16. /* 最大传输单元 */ 17. netif->mtu = netifMTU; 18. 19. /* broadcast capability */ 20. netif->flags = 21. NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; 22. 23. enc28j60Init(netif->hwaddr); //初始化 enc28j60 24. enc28j60clkout(1); // change clkout from 6.25MHz to 12.5MHz 25. 26. } 本初始化函数中先对 netif 结构体的 MAC 地址成员 netif->hwaddr 赋值为 macaddress 数 组的值,macaddress 数组是在 netconfig.c 文件中定义的,定义的时候已经设置了它的初始 值,在本函数中复制到 netif->hwaddr 后作为网卡的 MAC 地址。本函数中对最大传输单元 结构体成员 netif->mtu 赋值为 1500,对 netif->flags 赋值使其和允许使用广播地址、使能 ARP 功能。函数的最后,像上面的 low_level_output 函数一样,调用了在 ENC28J60.C 文件 中编写的驱动。分别是初始化函数 enc28j60Init()和时钟输出函数 enc28j60clkout()。 4. ethernet_input 指针 分析完 netif_add()输入参数 ehternetif_init 指针,再来看看另一个作为它的输入参数的 函数指针 ethernet_input,它是由 LwIP 协议栈自带的完整函数,一般不需要我们修改,见 代码清单 26- 9。 代码清单 26- 9:ethernet_input()函数 1. /* 2. * ethernetif_input(): This function should be called when a packet is ready to 3. * be read from the interface. It uses the function low_level_input() that 4. * should handle the actual reception of bytes from the network interf ace. 5. */ 6. err_t ethernetif_input(struct netif *netif) 7. { 8. err_t err = ERR_OK; 9. struct pbuf *p = NULL; 10. 11. /* move received packet into a new pbuf */ 12. p = low_level_input(netif); 13. 14. if (p == NULL) return ERR_MEM; 15. 16. err = netif->input(p, netif); 17. if (err != ERR_OK) 18. { 19. LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n")); 20. pbuf_free(p); 21. p = NULL; 22. } 23. 24. return err; 25. } 由于本实验中的 enc28j60 驱动没有使用中断接收数据包,所以 ethernetif_input()需要被 main 函数中 while 循环的 LwIP_Periodic_Handle()函数不停地调用,通过 ethernetif_input()函 数的返回值来判断是否接收到数据包。本函数执行时,先调用了最底层的输入函数 low_level_input(),它在底层又调用了 enc28j60 接收数据包的驱动,若接收到数据包,返回 值赋值到 pbuf 类型指针 pbuf 非空,然后在本函数的第 16 行,把 pbuf 输入到 netif 结构体 的 input 函数指针,通过 input 指针把数据包传递给上层的 LwIP 协议栈代码进行处理,完 成数据包的收取。(读者也可以修改 enc28j60 的驱动,使用中断方式接收数据包,就不需要 采用轮询的方式了) 5. 底层输入函数 上面的 ethernetif_input()调用了最底层的输入函数 low_level_input(),我们需要在这个 函数中实验 LwIP 接收数据包的功能,主要是通过调用 enc28j60 来实现的。见代码清单 26- 10。 代码清单 26- 10: low_level_input()函数 1. /* 2. * low_level_input(): Should allocate a pbuf and transfer the bytes of the 3. * incoming packet from the interface into the pbuf. 4. */ 5. 6. static struct pbuf *low_level_input( struct netif *netif ) 7. { 8. 9. struct pbuf *q,*p = NULL; 10. u16 Len = 0; 11. 12. int i =0; 13. 14. //接收数据包,并返回接收到的数据包长度 15. Len = enc28j60PacketReceive(1520 *4, Data_Buf); 16. 17. if ( Len == 0 ) return 0; 18. 19. p = pbuf_alloc(PBUF_RAW, Len, PBUF_POOL); 20. 21. if (p != NULL) 22. { 23. 24. for (q = p; q != NULL; q = q->next) 25. { 26. memcpy((u8_t*)q->payload, (u8_t*)&Data_Buf[i], q->len); 27. 28. i = i + q->len; 29. } 30. if( i != p->tot_len ){ return 0;} //相等时表明到了数据尾 31. } 32. 33. return p; 34. } 在本函数的第 15 行,就是调用 ENC28J60.C 文件中的 enc28j60 接收数据包的驱动, enc28j60PacketReceive()函数,它把接收到的数据存放在它的输入参数 Data_Buf 数组中, 并把该数据包的长度以返回值的形式赋值给变量 Len,若 Len 的长度非 0 时,即接收到数 据包,就调用 LwIP 的内存管理函数 pbuf_alloc 申请一个长度为 Len 的 pbuf 存储空间,在 for 循环中把数组 Data_Buf 的内容转移到 pbuf 的 q->payload 成员中,最后以返回值的形式 把 pbuf 交给调用它的函数。 对于最底层 enc28j60 驱动函数的具体实现,请读者利用源码进行分析,它们都是在 ENC28J60.C 文件中定义的。编写的思想就是利用 SPI 接口根据 datasheet 发送不同的命 令,控制 enc28j60 芯片,收发数据包时则往它的缓冲区读取或写入数据。 1.4.7 轮询和计时 确保底层驱动正确,并且把 LwIP 接口与驱动关联起来后,还需要在循环中轮询输入 函数,给 LwIP 协议栈提供计时。这是在 main 函数中循环调用的 LwIP_Periodic_Handle()函 数实现的。调用它时,它的输入参数 LocalTime 变量由 systick 定时器每 10ms 更新一次, 每次加 10。其具体定义见代码清单 26- 11。 代码清单 26- 11:LwIP_Periodic_Handle()函数 1. /* 2. * 函数名:LwIP_Periodic_Handle 3. * 描述 :lwip 协议栈要求周期调用一些函数 4. tcp_tmr etharp_tmr dhcp_fine_tmr dhcp_coarse_tmr 5. * 输入 :无 6. * 输出 : 无 7. * 调用 :外部调用 8. */ 9. void LwIP_Periodic_Handle(__IO uint32_t localtime) 10. { 11. //err_t err; 12. 13. /* 接收数据包 */ 14. ethernetif_input(&enc28j60); //轮询是否接收到数据 15. 16. 17. /* TCP periodic process every 250 ms */ 18. if (localtime - TCPTimer >= TCP_TMR_INTERVAL) 19. { 20. TCPTimer = localtime; 21. tcp_tmr(); //每 250ms 调用一次 22. } 23. /* ARP periodic process every 5s */ 24. if (localtime - ARPTimer >= ARP_TMR_INTERVAL) 25. { 26. ARPTimer = localtime; 27. etharp_tmr(); //每 5s 调用一次 28. } 29. 30. } 本函数在第 14 行调用了 ethernetif_input()函数,用于查询是否接收到数据包,若接收 到数据包则通过内部的机制把它传递给 LwIP 的上层。接下来检查当前 LocalTime 变量与上 一次调用了 tcp_tmr()或 etharp_tmr()的差值,若时间差达到 250ms 或 5s 时,则调用一次 tcp_tmr()或 etharp_tmr()一次。这两个函数分别用于 TCP 协议层和 ARP 模块,这些模块涉 及到一些超时重传或其它与时间有关的操作。 1.4.8 opt.h 文件和 debug 1. 裁剪和配置 LwIP 正确完成了 LwIP 与底层的接口,轮询也配置完毕,还需要配置 LwIP 的 opt.h 文件, 它如同 fatfs 文件系统中的 ffconf.h 文件。opt.h 用于裁剪、配置 LwIP 协议栈,如是否使用 操作系统,上层的应用是使用 API 或是 RAW 函数等等。部分关于 TCP 配置的一些宏见代 码清单 26- 12。 代码清单 26- 12:TCP 配置宏 1. /* 2. --------------------------------- 3. ---------- TCP options ---------- 4. --------------------------------- 5. */ 6. /** 7. * LWIP_TCP==1: Turn on TCP. 8. */ 9. #ifndef LWIP_TCP 10. #define LWIP_TCP 1 11. #endif 12. 13. /** 14. * TCP_TTL: Default Time-To-Live value. 15. */ 16. #ifndef TCP_TTL 17. #define TCP_TTL (IP_DEFAULT_TTL) 18. #endif 19. 20. /** 21. * TCP_WND: The size of a TCP window. This must be at least 22. * (2 * TCP_MSS) for things to work well 23. */ 24. #ifndef TCP_WND 25. #define TCP_WND (4 * TCP_MSS) 26. #endif 由于 opt.h 文件中这些宏已经配置了默认值,所以大部分是不需要修改的。又由于这里 的每个宏都包含条件编译的设置,所以我们在移植 LwIP 协议栈时,通常新建一个名为 lwipopts.h 的头文件,在该文件中修改的需要的配置。 如上面 的 LWIP_TCP 宏用于使能 TCP 协议或关闭 TCP 协议,它的默认值为 1,表示 使能。若我们不需要使用 LwIP 的 TCP 协议时,我们就在 lwipopts.h 文件中添加一个宏, 把 LWIP_TCP 宏定义为 0。 由于 opt.h 文件中的 LWIP_TCP 宏是在条件编译#ifndef 中,若在其它地方定义了相同 的宏,如 lwipopts.h 文件中定义了 LWIP_TCP 宏,则该宏的参数以 lwipopts.h 文件的定义为 准,若在其它地方没有定义相同名字的宏,则以 opt.h 文件的为准。野火在本工程实验中的 lwipopts.h 文件对 LwIP 配置见代码清单 26- 13。 代码清单 26- 13:lwipopts.h 文件配置 1. /******************************************************** 2. **------------Socket 参数配置:Socket options-------------- 3. ****************************************************************/ 4. #define LWIP_SOCKET 0 5. //#define LWIP_COMPAT_SOCKETS 0 6. #define NO_SYS 1 //不使用操作系统 7. 8. /************* 9. -----------------Sequential layer options---------------------------- 10. *******************************/ 11. #define LWIP_NETCONN 0 12. 13. #define NO_SYS_NO_TIMERS 1 14. 15. #define LWIP_DHCP 0 16. 17. /*************************************************************/ 18. #define ETHARP_TMR_INTERVAL 5000 /* Time in milliseconds to perform ARP processing */ 19. 20. /****************************************************************/ 21. 22. /* ---------- Memory options ---------- */ 23. /* MEM_ALIGNMENT: should be set to the alignment of the CPU for which 24. lwIP is compiled. 4 byte alignment - > define MEM_ALIGNMENT to 4, 2 25. byte alignment -> define MEM_ALIGNMENT to 2. */ 26. 27. #define MEM_ALIGNMENT 4 28. /* Align memory on 4 byte boundery (32-bit) */ 29. 30. /* MEM_SIZE: the size of the heap memory. If the application will send 31. a lot of data that needs to be copied, this should be set high. */ 32. 33. #define MEM_SIZE (4*1024) 34. 35. /* MEMP_NUM_PBUF: the number of memp struct pbufs. If the application 36. sends a lot of data out of ROM (or other static memory), this 37. should be set high. */ 38. 39. #define MEMP_NUM_PBUF 10 40. 41. /* MEMP_NUM_UDP_PCB: the number of UDP protocol control blocks. One 42. per active UDP "connection". */ 43. 44. #define MEMP_NUM_UDP_PCB 6 45. 46. /* MEMP_NUM_TCP_PCB: the number of simulatenously active TCP 47. connections. */ 48. 49. #define MEMP_NUM_TCP_PCB 10 50. /* MEMP_NUM_TCP_PCB_LISTEN: the number of listening TCP 51. connections. */ 52. 53. #define MEMP_NUM_TCP_PCB_LISTEN 6 54. 55. /* MEMP_NUM_TCP_SEG: the number of simultaneously queued TCP 56. segments. */ 57. 58. #define MEMP_NUM_TCP_SEG 12 59. 60. /* MEMP_NUM_SYS_TIMEOUT: the number of simulateously active 61. timeouts. */ 62. 63. #define MEMP_NUM_SYS_TIMEOUT 3 64. 65. 66. /* ---------- Pbuf options ---------- */ 67. /* PBUF_POOL_SIZE: the number of buffers in the pbuf pool. */ 68. 69. #define PBUF_POOL_SIZE 10 70. 71. /* PBUF_POOL_BUFSIZE: the size of each pbuf in the pbuf pool. */ 72. 73. #define PBUF_POOL_BUFSIZE 1500 74. /********************************* 75. **----------TCP 参数配置:TCP options---------------------------- 76. ********************************/ 77. 78. #define MEMP_NUM_REASSDATA 5 79. /* ---------- TCP options ---------- */ 80. #define LWIP_TCP 1 81. #define TCP_TTL 255 82. 83. /* Controls if TCP should queue segments that arrive out of 84. order. Define to 0 if your device is low on memory. */ 85. 86. #define TCP_QUEUE_OOSEQ 0 87. 88. /* TCP Maximum segment size. */ 89. 90. #define TCP_MSS (1500 - 40) /* TCP_MSS = (Ethernet MTU - IP header size - TCP header size) */ 91. 92. /* TCP sender buffer space (bytes). */ 93. 94. #define TCP_SND_BUF (2*TCP_MSS) 95. 96. /* TCP sender buffer space (pbufs). This must be at least = 2 * 97. TCP_SND_BUF/TCP_MSS for things to work. */ 98. 99. #define TCP_SND_QUEUELEN (6 * TCP_SND_BUF)/TCP_MSS 100. 101. /* TCP receive window. */ 102. 103. #define TCP_WND (2*TCP_MSS) 104. 105. 106. /* ---------- ICMP options ---------- */ 107. #define LWIP_ICMP 1 108. 完成了这部分后(不需要例程 main 中的 httpd_init()和 CMD_init()函数),下载程序,接 上网线就可以 ping 通了,表示 LwIP 移植初步成功。但由于没有建立服务器和 telnet 响应 的程序,这两个功能还不能实现。 2. LwIP 的 debug 功能 大部分初始学者,不熟悉 TCP/IP 协议,移植 LwIP 十分不容易,而 LwIP 协议栈有一 个十分贴心的输出调试信息的功能。 如在前面提到的 ethernet_input()函数的第 19 行有一句宏: 1. LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n")); 如果我们设置了输出调试信息使能,若 ethernet_input()中出现错误时就会输出调试信 息。这样类似的宏由 LwIP 的设计者精心安排在各种容易出错的场合,以便于移植和调 试。宏 LWIP_DEBUGF 是在 LwIP 的 debug.h 文件定义的,见代码清单 26- 14。 代码清单 26- 14: LWIP_DEBUGF 宏 1. #define LWIP_DEBUGF(debug, message) do { \ 2. if ( \ 3. ((debug) & LWIP_DBG_ON) && \ 4. ((debug) & LWIP_DBG_TYPES_ON) && \ 5. ((s16_t)((debug) & LWIP_DBG_MASK_LEVEL) >= LWIP_DBG_MIN_LEVEL)) { \ 6. LWIP_PLATFORM_DIAG(message); \ 7. if ((debug) & LWIP_DBG_HALT) { \ 8. while(1); \ 9. }\ 10. }\ 11. } while(0) 这个宏看似十分复杂,其功能就是当符合某些条件的时候,调用第 6 行中的宏 LWIP_PLATFORM_DIAG 输出信息。具体如何输出信息需要我们去实现,本工程在 cc.h 文件对该宏设置如下: 1. #define LWIP_PLATFORM_DIAG(x) do {printf x;} while(0) 即调用 printf 函数来输出这些调试信息,而 printf 函数已经经被我们重定义到串口,所 以 PC 的终端就可以接收调试信息了。 在默认的情况下,是不输出调试信息的,详见 opt.h 文件相关的 debug 宏设置,可以配 置不输出调试信息,输出部分调试信息。野火的本实验工程已调试完毕,所以野火把大部 分输出调试信息的功能都关了。本例程中在 lwipopts.h 文件中设置的与调试信息相关的 宏,见代码清单 26- 15。 代码清单 26- 15:与调试信息相关的宏 1. /************************************************************/ 2. #define LWIP_DEBUG 1 3. #define LWIP_DBG_TYPES_ON LWIP_DBG_OFF 4. 5. /** 6. * ETHARP_DEBUG: Enable debugging in etharp.c. 7. */ 8. #define ETHARP_DEBUG LWIP_DBG_OFF 9. 10. /** 11. * NETIF_DEBUG: Enable debugging in netif.c. 12. */ 13. #define NETIF_DEBUG LWIP_DBG_OFF 14. /** * PBUF_DEBUG: Enable debugging in pbuf.c. */ #define PBUF_DEBUG LWIP_DBG_OFF 若 需 要 输 出 调 试 信 息 , 必 须 把 宏 LWIP_DEBUG 设 置 为 1 , 其 它 的 如 NETIF_DEBUG、ETHARP_DEBUG 可根据调试的情况决定是否开启。 1.4.9 LwIP 应用 1. 网络应用软件架构 构成网络应用的软件有不同的结构,有 B/S 结构(浏览器/服务器) 和 C/S 结构(客户端/ 服务器)。 基于 B/S 结构的应用程序以网页形式存放于用服务器上,用户运行应用程序时通过 web 调用服务器的应用程序,并通过浏览器把结果显示给用户,用户只需要浏览器即可运 行所有服务器中提供的应用模块。 而 C/S 结构的软件需要针对不同的应用而对客户端进行更改。本实验工程中的 http 服 务器是基于 B/S 结构的,而 telnet 程序是基于 C/S 结构的。 2. TCP 网络应用 基于 TCP 协议的网络应用十分常见。TCP 应用的服务器流程如图 26- 13,例如要把 STM32 作为网页服务器,其程序就是根据该流程编写的。 创建连接 绑定 IP 和端口 监听连接 回调函数响应 接收数据报 通知接收到连接请求 图 26- 13 建立 TCP 应用流程 当服务器开始监听连接时,客户端或浏览器就可以向服务器提出连接请求,然后服务 器作出响应。 3. LwIP 的应用函数 移植好 LwIP 后,就可以利用它来编写应用层的程序了。LwIP 协议栈支持类似 BSD 协 议栈的 socket 应用程序接口,这种被称为 LwIP 的 API 函数,API 函数更适合用在具有操作 系统的平台上。 另一种方式是直接调用 LwIP 协议栈内部的函数,这些函数被称为 RAW 函数,RAW 函数适合于用在没有操作系统的平台,效率高,但开发起来较为复杂。由于本例程中没有 使用操作系统,所以我们利用 LwIP 的 RAW 函数进行应用层编程。 利用 LwIP 编写的 TCP 类型应用程序,它在各个层次的处理过程如图 26- 14。 应用层 传输层 网络层(IP 层) netif->input() 网络接口层 图 26- 14 TCP 处理过程 在移植的时候,我们主要完成的就是图中网络接口层的 netif->output 和 netif->input 函 数。移植完成后,即可利用 LwIP 的应用层接口编写应用程序,而网络层和传输层则由 LwIP 协议栈完成了。不过,大部分 RAW 函数都是位于传输层的。 1.4.10 网页服务器 利用 LwIP 编写网页服务器是在 main 中调用的 httpd_init()函数实现的。该函数的源码 见代码清单 26- 16。 代码清单 26- 16:httpd_init()函数 1. /* 2. * 函数名:httpd_init 3. * 描述 :初始化 web server,初始化后才能显示网页 4. * 输入 :无 5. * 输出 :无 6. * 调用 :外部调用 7. */ 8. void httpd_init(void) 9. { 10. struct tcp_pcb *pcb; 11. 12. pcb = tcp_new(); /* 建立通信的 TCP 控制块(pcb) */ 13. 14. tcp_bind(pcb,IP_ADDR_ANY,80); /* 绑定本地 IP 地址和端口号 */ 15. 16. pcb = tcp_listen(pcb); /* 进入监听状态 */ 17. 18. tcp_accept(pcb,http_accept); /* 设置有连接请求时的回调函数 */ 19. 20. } httpd_init()函数只有几行,它每调用一个函数,就完成了 TCP 服务器流程图中的一个 步骤。分析如下: (1) tcp_new() 本函数的原型为: 1. struct tcp_pcb * tcp_new(void) 它用于创建一个 TCP 连接。它向 LwIP 协议栈申请分配一个 TCP 控制块,若分配成 功,则返回控制块的指针,否则返回 NULL。TCP 控制块结构体用 tcp_pcb 进行定义,它 是用于描述 TCP 连接的,包含了非常丰富的信息。 本实验中调用 tcp_new()后,把它的返回值赋给 TCP 控制块类型的指针 pcb。 (2) tcp_bind() 本函数原型为: 1. err_t 2. tcp_bind(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 它用于把一个本地的 IP 和端口进行绑定,它的输入参数分别为要进行绑定的连接、绑 定的 IP 地址、端口号。 本实验中把 tcp_new()新建的连接 pcb 进行绑定,绑定的 IP 地址为 IP_ADDR_ANY, 这个宏展开为 0x00000000,绑定的端口为 80,表示把本地的任意 IP 地址都与 80 端口绑 定。网页浏览器默认使用 80 端口。 (3) tcp_listen() 它实际是一个宏,宏展开是一个函数 1. #define tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_ LISTEN_BACKLOG) 函数的原型如下: 1. struct tcp_pcb * 2. tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog) 它用于监听端口,若接收到连接,则会调用下面 tcp_accept()函数中指定中的回调函数 进行响应,返回值为新的 TCP 控制块,用于回调函数的输入参数。 本实验调用 tcp_listen()对以上创建的 80 端口进行监听。 (4) tcp_accept() 函数的原型为: 1. void 2. tcp_accept(struct tcp_pcb *pcb, err_t (* accept)(void *arg, struct tcp _pcb *newpcb, err_t err)) 它的作用是用于指定连接建立后调用的回调函数。第一个输入参数为 TCP 控制块,用 于指定是哪个连接,第二个参数是回调函数的指针。 本实验中调用 tcp_accept()指定回调函数为 http_accept。 当我们在 PC 的浏览器的地址栏输入: http://192.168.1.18:80 ( :80 表示 80 端口,可省 略) ,由于服务器 STM32 的监听设置,连接成功,就会调用 tcp_accept()指定的 http_accept 函数。http_accept()函数定义见代码清单 26- 17。 代码清单 26- 17:http_accept()函数 1. /* 2. * 函数名:http_accept 3. * 描述 :http web server 的回调函数,建立连接后调用 4. * 输入 :tcp_arg 设置的参数、pcb、err 5. * 输出 :err 6. * 调用 :内部调用 7. */ 8. static err_t http_accept(void *arg,struct tcp_pcb *pcb,err_t err) 9. { 10. /* 设置回调函数优先级,当存在几个连接时特别重要,此函数必须调用*/ 11. tcp_setprio(pcb, TCP_PRIO_MIN); 12. 13. tcp_recv(pcb,http_recv); /* 设置 TCP 段到时的回调函数 */ 14. 15. return ERR_OK; 16. } http_accept()函数又调用了 tcp_setprio 进行优先级设置,并调用 tcp_recv 指定当接收到 新的数据的时候调用 http_recv 函数。在本实验中我们使用 http_recv()函数向浏览器输出网 页,它的定义见代码清单 26- 18。 代码清单 26- 18:http_recv()函数 1. /* 2. * 函数名:http_recv 3. * 描述 :http 接收到数据后的回调函数 4. * 输入 :tcp_arg 设置的参数、tcp_pcb、pcb、err 5. * 输出 :err 6. * 调用 :内部调用 7. */ 8. 9. static err_t http_recv(void *arg, struct tcp_pcb *pcb,struct pbuf *p,e rr_t err) 10. { 11. char * data = NULL; 12. char *UserName =NULL; 13. char *PassWord =NULL; 14. char *LED_CMD =NULL; 15. char *ch =NULL; 16. 17. data = p->payload; //把接收到的数据指针交给 data 18. 19. if (err == ERR_OK && p != NULL) //判断是否非空 20. { 21. if(strncmp(data,"GET",3)==0) 22. { 23. 24. /*第一次与服务器连接,打开登录页面,未输入登录信息*/ 25. tcp_write(pcb,http_html_hdr,sizeof(http_html_hdr),0); /* 发送 http 协议头部信息 */ 26. tcp_write(pcb,login,sizeof(login),0); /* 发送登录网页信息 */ 27. } 28. 29. else if(strncmp(data,"POST",4)==0) 30. { 31. LED_CMD = strstr(data,"LED_CTRL="); 32. UserName = strstr(data,"UserName="); 33. //截取 UserName= 34. PassWord = strstr(UserName,"PassWord="); 35. //截取 PassWord= 36. /*输入了用户名和密码*/ 37. if(UserName!=NULL && PassWord!=NULL) 38. { 39. ch = strchr(UserName,'&'); 40. //把&替换为‘\0’ 41. *ch = '\0'; 42. 43. UserName +=sizeof("UserName=")-1; 44. //提取用户名。sizeof 字符串包括终止符‘\0’ 45. PassWord += sizeof("PassWord=")-1; 46. //提取密码 47. 48. if(strcmp(UserName,"wildfire")==0 && strncmp(P assWord,"123456",6)==0) /*输入的用户名和密码正确*/ 49. { 50. LED1(ON); 51. // printf("\r\n 提取出的用户名 =%s\r\n 提取出的密 =%s",UserName,PassWord); 52. 53. tcp_write(pcb,http_html_hdr,sizeof(http_html_hdr),0); /* 发送 http 协议头部信息 */ 54. tcp_write(pcb,led_ctrl_on,sizeof(led_ctrl_on),0); /* 发送 led 控制网页信息 */ 55. 56. } 57. else /*输入的用户名和密码错误*/ 58. { 59. tcp_write(pcb,http_html_hdr,sizeof(http_ht ml_hdr),0); /* 发送 http 协议头部信息 */ 60. tcp_write(pcb,login,sizeof(login),0); /* 发送登录网页信息 */ 61. } 62. } 63. else if(LED_CMD !=NULL) /*接收到 LED 控制命令*/ 64. { 65. if(strstr(LED_CMD,"LED_CTRL=ON")) /*检测是哪 个命令:开\关*/ 66. { 67. LED1(ON); 68. tcp_write(pcb,http_html_hdr,sizeof(http_ht ml_hdr),0); /* 发送 http 协议头部信息 */ 69. tcp_write(pcb,led_ctrl_on,sizeof(led_ctrl_ on),0); /* 发送 led 控制网页信息 */ 70. } 71. else if(strstr(LED_CMD,"LED_CTRL=OFF")) 72. { 73. LED1(OFF); 74. tcp_write(pcb,http_html_hdr,sizeof(http_ht ml_hdr),0); /* 发送 http 协议头部信息 */ 75. tcp_write(pcb,led_ctrl_off,sizeof(led_ctrl _off),0); /* 发送 led 控制网页信息 */ 76. } 77. } 78. } 79. 80. pbuf_free(p); 81. } 82. tcp_close(pcb); /* 关闭这个连接 */ 83. err = ERR_OK; 84. 85. return err; 86. } /* 释放该 pbuf 段 */ 该函数看似十分复杂,其实只是重复判断接收到什么数据,然后根据数据的内容调用 tcp_write()函数发送不同的网页。其流程如下: (1) 第 17 行,把接收到的数据包 p->payload 的指针赋值给 data,在后面使用 data 来判断接 收到的数据。 (2) 第 19 行,若数据包非空,则开始判断数据包的内容。 (3) 当我们在浏览器输入服务器的 IP 地址并访问时,浏览器会向服务器发送一些数据,我 们可以使用一些网络抓包工具查看这些数据包。其数据内容见图 26- 15 中的箭头所指 的区域。数据包中开头的内容为“GET ”。在服务器中我们以这三个字符作为第一次 与服务器连接的标志,当确定为第一次连接时,调用 LwIP 的 tcp_write()函数输出网 页。 图 26- 15 浏览器发送的数据 (4) 第 25 行,调用 tcp_write()函数,输出参数 http_html_hdr 数组的内容,这部分内容的大 小为 sizeof(http_html_hdr)。http_html_hdr 数组是 http 网页的文件头部信息,在输出网 页的时候我们需要先发送这部分内容。见代码清单 26- 19。 代码清单 26- 19:http 文件头 /*http 文件头 */ #define HTTP_HEAD "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n" (5) 第 26 行,调用 tcp_write()函数,输出 login 数组内容 login 数组存储的就是登录页面。 由于没有使用文件系统,所以在本实验中 html 格式的网页文件都是以 c 语言的数组存 储起来的。 (6) 登录页面后的代码作用是类似的,只是浏览器向服务器发送的数据不同,而服务器响 应的网页也有所区别而已。服务器接收到特定的 LED 控制的信息后,STM32 对 LED 进行控制。建议读者了解一下 html 语言,本实验中重点用到的是 html 语言中用于提 交表单的 GET 方法和 POST 方法。 经过以上步骤,我们就成功建立了一个网页服务器,实验中还有一个针对 telnet 程序 的服务,它是在 CMD_init()函数中完成的,编写的思路和搭建网页服务器十分相似。 1.4.11 实验现象 将野火 STM32 开发板供电(DC5V),插上 JLINK,插上串口线(两头都是母的交叉线), 利用网线把 STM32 开发板接入与 PC 相同的路由,也可以直接利用网线把开发板和 PC 相 连,其实验的操作是相同的(这样可以排除路由的问题),但在进行浏览网页实验时,图片 可能无法正常显示。把本工程文件编译后烧录到开发板上,在程序运行框输入 cmd 命令进 入 dos 模式。 (1) ping 实验,见图 26- 16 ping 192.168.1.18。 在命令提示符窗口输入命令并回车: ping 192.168.1.18 输入 ping 命令 成功后的现象 图 26- 16 ping 192.168.1.18 (2) telnet 实验。 1) 如果使用 windows 7 系统,系统没有 telnet 程序,需要自行下载安装。使用 xp 系统的用户,在命令提示符窗口输入命令并回车: telnet 192.168.1.18 输入命令后弹出如下窗口: 图 26- 17 进入 telnet 程序 2) 见图 26- 18,在弹出的窗口下输入用户名并回车: wildfire 3) 若用户名正确,程序提示输入密码,键入密码并回车:123456 4) 若密码正确,提示输入命令,本工程只允许两条命令,分别为 LED1_ON 和 LED1_OFF,用于控制 LED1 的亮和灭。 输入命令:LED1_ON 板上的 LED1 灯会被点亮,窗口会弹出控制成功的信息,并且提示输入命令。 输入命令:LED1_OFF 板上的 LED1 会被关灭,窗口弹出控制成功信息,再次提示输入命令。 输入用户名 输入密码 输入命令,点亮 led 输入命令,关闭 led 输入命令 图 26- 18 telnet 控制流程 若用户输入的用户名、密码不正确或不存在的命令,会出现各种提示,并可以重新输 入。 (3) 网页浏览实验。(若 PC 没有接入互联网,图片可能没法正常显示。) 1) 打开浏览器,在地址栏输入 IP 并回车:192.168.1.18 在弹出的网页中输入用户名和密码:wildfire 123456 输入地址并回车 输入用户名和 密码点击登录 图 26- 19 网页登录 2) 点击登录后,出现如下界面,且开发板上的 LED 被点亮。 点选操作,点 击控制按钮 图 26- 20 登录后的页面 3) 点选关闭 LED1,并点击控制按钮,网页显示的 LED 状态改变,板上的 LED1 也被关灭。 图 26- 21 关闭 LED

Top_arrow
回到顶部
EEWORLD下载中心所有资源均来自网友分享,如有侵权,请发送举报邮件到客服邮箱bbs_service@eeworld.com.cn 或通过站内短信息或QQ:273568022联系管理员 高进,我们会尽快处理。