42. ETH—Lwip以太网通信

互联网技术对人类社会的影响不言而喻。当今大部分电子设备都能以不同的方式接入互联网(Internet),在家庭中PC常见的互联网接入方式是使用路由器(Router)组建小型局域网(LAN), 利用互联网专线或者调制调解器(modem)经过电话线网络,连接到互联网服务提供商(ISP),由互联网服务提供商把用户的局域网接入互联网。 而企业或学校的局域网规模较大,常使用交换机组成局域网,经过路由以不同的方式接入到互联网中。

42.1. 互联网模型

通信至少是两个设备的事,需要相互兼容的硬件和软件支持,我们称之为通信协议。以太网通信在结构比较复杂,国际标准组织将整个以太网通信结构制定了OSI模型, 总共分层七个层,分别为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层,每个层功能不同,通信中各司其职,整个模型包括硬件和软件定义。 OSI模型是理想分层,一般的网络系统只是涉及其中几层。

TCP/IP是互联网最基本的协议,是互联网通信使用的网络协议,由网络层的IP协议和传输层的TCP协议组成。TCP/IP只有四个分层, 分别为应用层、传输层、网络层以及网络访问层。虽然TCP/IP分层少了,但与OSI模型是不冲突的,它把OSI模型一些层次整合一起的,本质上可以实现相同功能。

实际上,还有一个TCP/IP混合模型,分为五个层,参考图 TCP_IP混合参考模型 ,它实际与TCP/IP四层模型是相通的,只是把网络访问层拆成数据链路层和物理层。 这种分层方法对我们学习理解更容易。

TCP/IP混合参考模型

设计网络时,为了降低网络设计的复杂性,对组成网络的硬件、软件进行封装、分层,这些分层即构成了网络体系模型。在两个设备相同层之间的对话、 通信约定,构成了层级协议。设备中使用的所有协议加起来统称协议栈。在这个网络模型中,每一层完成不同的任务,都提供接口供上一层访问。 而在每层的内部,可以使用不同的方式来实现接口,因而内部的改变不会影响其它层。

在TCP/IP混合参考模型中,数据链路层又被分为LLC层(逻辑链路层)和MAC层(媒体介质访问层)。目前,对于普通的接入网络终端的设备, LLC层和MAC层是软、硬件的分界线。如PC的网卡主要负责实现参考模型中的MAC子层和物理层,在PC的软件系统中则有一套庞大程序实现了LLC层及以上的所有网络层次的协议。

由硬件实现的物理层和MAC子层在不同的网络形式有很大的区别,如以太网和Wi-Fi,这是由物理传输方式决定的。 但由软件实现的其它网络层次通常不会有太大区别,在PC上也许能实现完整的功能,一般支持所有协议,而在嵌入式领域则按需要进行裁剪。

42.2. 以太网

以太网(Ethernet)是互联网技术的一种,由于它是在组网技术中占的比例最高,很多人直接把以太网理解为互联网。

以太网是指遵守IEEE 802.3标准组成的局域网,由IEEE802.3标准规定的主要是位于参考模型的物理层(PHY)和数据链路层中的介质访问控制子层(MAC)。 在家庭、企业和学校所组建的PC局域网形式一般也是以太网,其标志是使用水晶头网线来连接(当然还有其它形式)。IEEE还有其它局域网标准, 如IEEE 802.11是无线局域网,俗称Wi-Fi。IEEE802.15是个人域网,即蓝牙技术,其中的802.15.4标准则是ZigBee技术。

现阶段,工业控制、环境监测、智能家居的嵌入式设备产生了接入互联网的需求,利用以太网技术,嵌入式设备可以非常容易地接入到现有的计算机网络中。

42.2.1. PHY层

在物理层,由IEEE 802.3标准规定了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,物理层一般是通过一个PHY芯片实现其功能的。

42.2.1.1. 传输介质

传输介质包括同轴电缆、双绞线(水晶头网线是一种双绞线)、光纤。根据不同的传输速度和距离要求, 基于这三类介质的信号线又衍生出很多不同的种类。最常用的是“五类线”适用于100BASE-T和10BASE-T的网络,它们的网络速率分别为100Mbps和10Mbps。

42.2.1.2. 编码

为了让接收方在没有外部时钟参考的情况也能确定每一位的起始、结束和中间位置,在传输信号时不直接采用二进制编码。 在10BASE-T的传输方式中采用曼彻斯特编码,在100BASE-T中则采用4B/5B编码

曼彻斯特编码把每一个二进制位的周期分为两个间隔,在表示“1”时,以前半个周期为高电平,后半个周期为低电平。表示“0”时则相反,见图 曼彻斯特编码

曼彻斯特编码

采用曼彻斯特码在每个位周期都有电压变化,便于同步。但这样的编码方式效率太低,只有50%。

在100BASE-T 采用的4B/5B编码是把待发送数据位流的每4位分为一组,以特定的5位编码来表示,这些特定的5位编码能使数据流有足够多的跳变, 达到同步的目的,而且效率也从曼彻斯特编码的50%提高到了80%。

42.2.1.3. CSMA/CD冲突检测

早期的以太网大多是多个节点连接到同一条网络总线上(总线型网络),存在信道竞争问题,因而每个连接到以太网上的节点都必须具备冲突检测功能。 以太网具备CSMA/CD冲突检测机制,如果多个节点同时利用同一条总线发送数据,则会产生冲突,总线上的节点可通过接收到的信号与原始发送的信号的比较检测是否存在冲突, 若存在冲突则停止发送数据,随机等待一段时间再重传。

现在大多数局域网组建的时候很少采用总线型网络,大多是一个设备接入到一个独立的路由或交换机接口,组成星型网络,不会产生冲突。但为了兼容,新出的产品还是带有冲突检测机制。

42.3. MAC子层

42.3.1. MAC的功能

MAC子层是属于数据链路层的下半部分,它主要负责与物理层进行数据交接,如是否可以发送数据,发送的数据是否正确, 对数据流进行控制等。它自动对来自上层的数据包加上一些控制信号,交给物理层。接收方得到正常数据时,自动去除MAC控制信号,把该数据包交给上层。

42.3.1.1. MAC数据包

IEEE对以太网上传输的数据包格式也进行了统一规定,见图 MAC数据包格式 。该数据包被称为MAC数据包。

MAC数据包格式

MAC数据包由前导字段、帧起始定界符、目标地址、源地址、数据包类型、数据域、填充域、校验和域组成。

  • 前导字段,也称报头,这是一段方波,用于使收发节点的时钟同步。内容为连续7个字节的0x55。字段和帧起始定界符在MAC收到数据包后会自动过滤掉。

  • 帧起始定界符(SFD):用于区分前导段与数据段的,内容为0xD5。

  • MAC地址: MAC地址由48位数字组成,它是网卡的物理地址,在以太网传输的最底层,就是根据MAC地址来收发数据的。部分MAC地址用于广播和多播, 在同一个网络里不能有两个相同的MAC地址。PC的网卡在出厂时已经设置好了MAC地址,但也可以通过一些软件来进行修改, 在嵌入式的以太网控制器中可由程序进行配置。数据包中的DA是目标地址,SA是源地址。

  • 数据包类型:本区域可以用来描述本MAC数据包是属于TCP/IP协议层的IP包、ARP包还是SNMP包,也可以用来描述本MAC数据包数据段的长度。 如果该值被设置大于0x0600,不用于长度描述,而是用于类型描述功能,表示与以太网帧相关的MAC客户端协议的种类。

  • 数据段:数据段是MAC包的核心内容,它包含的数据来自MAC的上层。其长度可以从0~1500字节间变化。

  • 填充域:由于协议要求整个MAC数据包的长度至少为64字节(接收到的数据包如果少于64字节会被认为发生冲突,数据包被自动丢弃), 当数据段的字节少于46字节时,在填充域会自动填上无效数据,以使数据包符合长度要求。

  • 校验和域:MAC数据包的尾部是校验和域,它保存了CRC校验序列,用于检错。

以上是标准的MAC数据包,IEEE 802.3同时还规定了扩展的MAC数据包,它是在标准的MAC数据包的SA和数据包类型之间添加4个字节的QTag前缀字段, 用于获取标志的MAC帧。前2个字节固定为0x8100,用于识别QTag前缀的存在;后两个字节内容分别为3个位的用户优先级、1个位的标准格式指示符(CFI)和一个12位的VLAN标识符。

42.4. TCP/IP协议栈

标准TCP/IP协议是用于计算机通信的一组协议,通常称为TCP/IP协议栈,通俗讲就是符合以太网通信要求的代码集合, 一般要求它可以实现图 TCP_IP混合参考模型 中每个层对应的协议,比如应用层的HTTP、FTP、DNS、SMTP协议, 传输层的TCP、UDP协议、网络层的IP、ICMP协议等等。关于TCP/IP协议详细内容推荐阅读《TCP-IP详解》和《用TCP/IP进行网际互连》理解。

Windows操作系统、UNIX类操作系统都有自己的一套方法来实现TCP/IP通信协议,它们都提供非常完整的TCP/IP协议。对于一般的嵌入式设备, 受制于硬件条件没办法支持使用在Window或UNIX类操作系统的运行的TCP/IP协议栈,一般只能使用简化版本的TCP/IP协议栈, 目前开源的适合嵌入式的有uIP、TinyTCP、uC/TCP-IP、LwIP等等。其中LwIP是目前在嵌入式网络领域被讨论和使用广泛的协议栈。本章内容其中一个目的就是移植LwIP到开发板上运行。

42.4.1. 为什么需要协议栈

物理层主要定义物理介质性质,MAC子层负责与物理层进行数据交接,这两部分是与硬件紧密联系的,就嵌入式控制芯片来说,很多都内部集成了MAC控制器, 完成MAC子层功能,所以依靠这部分功能是可以实现两个设备数据交换,而时间传输的数据就是MAC数据包,发送端封装好数据包,接收端则解封数据包得到可用数据, 这样的一个模型与使用USART控制器实现数据传输是非常类似的。但如果将以太网运用在如此基础的功能上,完全是大材小用,因为以太网具有传输速度快、 可传输距离远、支持星型拓扑设备连接等等强大功能。功能强大的东西一般都会用高级的应用,这也是设计者的初衷。

使用以太网接口的目的就是为了方便与其它设备互联,如果所有设备都约定使用一种互联方式,在软件上加一些层次来封装,这样不同系统、 不同的设备通讯就变得相对容易了。而且只要新加入的设备也使用同一种方式,就可以直接与之前存在于网络上的其它设备通讯。 这就是为什么产生了在MAC之上的其它层次的网络协议及为什么要使用协议栈的原因。又由于在各种协议栈中TCP/IP协议栈得到了最广泛使用, 所有接入互联网的设备都遵守TCP/IP协议。所以,想方便地与其它设备互联通信,需要提供对TCP/IP协议的支持。

42.4.2. 各网络层的功能

用以太网和Wi-Fi作例子,它们的MAC子层和物理层有较大的区别,但在MAC之上的LLC层、网络层、传输层和应用层的协议,是基本相同的, 这几层协议由软件实现,并对各层进行封装。根据TCP/IP协议,各层的要实现的功能如下:

LLC层:处理传输错误;调节数据流,协调收发数据双方速度,防止发送方发送得太快而接收方丢失数据。主要使用数据链路协议。

网络层:本层也被称为IP层。LLC层负责把数据从线的一端传输到另一端,但很多时候不同的设备位于不同的网络中(并不是简单的网线的两头)。此时就需要网络层来解决子网路由拓扑问题、路径选择问题。在这一层主要有IP协议、ICMP协议。

传输层:由网络层处理好了网络传输的路径问题后,端到端的路径就建立起来了。传输层就负责处理端到端的通讯。在这一层中主要有TCP、UDP协议

应用层:经过前面三层的处理,通讯完全建立。应用层可以通过调用传输层的接口来编写特定的应用程序。而TCP/IP协议一般也会包含一些简单的应用程序如Telnet远程登录、FTP文件传输、SMTP邮件传输协议。

实际上,在发送数据时,经过网络协议栈的每一层,都会给来自上层的数据添加上一个数据包的头,再传递给下一层。在接收方收到数据时, 一层层地把所在层的数据包的头去掉,向上层递交数据,参考图 数据经过每一层的封装和还原

数据经过每一层的封装和还原

42.5. 以太网外设(ETH)

STM32F42x系列控制器内部集成了一个以太网外设,它实际是一个通过DMA控制器进行介质访问控制(MAC),它的功能就是实现MAC层的任务。 借助以太网外设,STM32F42x控制器可以通过ETH外设按照IEEE 802.3-2002标准发送和接收MAC数据包。ETH内部自带专用的DMA控制器用于MAC, ETH支持两个工业标准接口介质独立接口(MII)和简化介质独立接口(RMII)用于与外部PHY芯片连接。MII和RMII接口用于MAC数据包传输, ETH还集成了站管理接口(SMI)接口专门用于与外部PHY通信,用于访问PHY芯片寄存器。

物理层定义了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,PHY芯片是物理层功能实现的实体,生活中常用水晶头网线+水晶头插座+PHY组合构成了物理层。

ETH有专用的DMA控制器,它通过AHB主从接口与内核和存储器相连,AHB主接口用于控制数据传输,而AHB从接口用于访问“控制与状态寄存器”(CSR)空间。 在进行数据发送是,先将数据有存储器以DMA传输到发送TX FIFO进行缓冲,然后由MAC内核发送;接收数据时,RXFIFO先接收以太网数据帧, 再由DMA传输至存储器。ETH系统功能框图见图 ETH功能框图

ETH功能框图

42.5.1. SMI接口

SMI是MAC内核访问PHY寄存器标志接口,它由两根线组成,数据线MDIO和时钟线MDC。SMI支持访问32个PHY,这在设备需要多个网口时非常有用, 不过一般设备都只使用一个PHY。PHY芯片内部一般都有32个16位的寄存器,用于配置PHY芯片属性、工作环境、状态指示等等, 当然很多PHY芯片并没有使用到所有寄存器位。MAC内核就是通过SMI向PHY的寄存器写入数据或从PHY寄存器读取PHY状态, 一次只能对一个PHY的其中一个寄存器进行访问。SMI最大通信频率为2.5MHz,通过控制以太网MAC MII地址寄存器 (ETH_MACMIIAR)的CR位可选择时钟频率。

42.5.1.1. SMI帧格式

SMI是通过数据帧方式与PHY通信的,帧格式如表 SMI帧格式 ,数据位传输顺序从左到右。

SMI帧格式

PADDR用于指定PHY地址,每个PHY都有一个地址,一般由PHY硬件设计决定,所以是固定不变的。RADDR用于指定PHY寄存器地址。 TA为状态转换域,若为读操作,MAC输出两个位高阻态,而PHY芯片则在第一位时输出高阻态,第二位时输出“0”。若为写操作,MAC输出“10”,PHY芯片则输出高阻态。 数据段有16位,对应PHY寄存器每个位,先发送或接收到的位对应以太网 MAC MII 数据寄存器(ETH_MACMIIDR)寄存器的位15。

42.5.1.2. SMI读写操作

当以太网MAC MII地址寄存器 (ETH_MACMIIAR)的写入位和繁忙位被置1时,SMI将向指定的PHY芯片指定寄存器写入ETH_MACMIIDR中的数据。写操作时序见图 SMI写操作

SMI写操作

当以太网MAC MII地址寄存器 (ETH_MACMIIAR)的写入位为0并且繁忙位被置1时,SMI将从向指定的PHY芯片指定寄存器读取数据到ETH_MACMIIDR内。读操作时序见图 SMI读操作

SMI读操作

42.5.2. MII和RMII接口

介质独立接口(MII)用于连接MAC控制器和PHY芯片,提供数据传输路径。RMII接口是MII接口的简化版本,MII需要16根通信线,RMII只需7根通信, 在功能上是相同的。图 MII接口连接 为MII接口连接示意图,图 RMII接口连接 为RMII接口连接示意图。

MII接口连接 RMII接口连接
  • TX_CLK:数据发送时钟线。标称速率为10Mbit/s时为2.5MHz;速率为100Mbit/s时为25MHz。RMII接口没有该线。

  • RX_CLK:数据接收时钟线。标称速率为10Mbit/s时为2.5MHz;速率为100Mbit/s时为25MHz。RMII接口没有该线。

  • TX_EN:数据发送使能。在整个数据发送过程保存有效电平。

  • TXD[3:0]或TXD[1:0]:数据发送数据线。对于MII有4位,RMII只有2位。只有在TX_EN处于有效电平数据线才有效。

  • CRS:载波侦听信号,由PHY芯片负责驱动,当发送或接收介质处于非空闲状态时使能该信号。在全双工模式该信号线无效。

  • COL:冲突检测信号,由PHY芯片负责驱动,检测到介质上存在冲突后该线被使能,并且保持至冲突解除。在全双工模式该信号线无效。

  • RXD[3:0]或RXD[1:0]:数据接收数据线,由PHY芯片负责驱动。对于MII有4位,RMII只有2位。在MII模式,当RX_DV禁止、RX_ER使能时,特定的RXD[3:0]值用于传输来自PHY的特定信息。

  • RX_DV:接收数据有效信号,功能类似TX_EN,只不过用于数据接收,由PHY芯片负责驱动。对于RMII接口,是把CRS和RX_DV整合成CRS_DV信号线,当介质处于不同状态时会自切换该信号状态。

  • RX_ER:接收错误信号线,由PHY驱动,向MAC控制器报告在帧某处检测到错误。

  • REF_CLK:仅用于RMII接口,由外部时钟源提供50MHz参考时钟。

因为要达到100Mbit/s传输速度,MII和RMII数据线数量不同,使用MII和RMII在时钟线的设计是完全不同的。对于MII接口, 一般是外部为PHY提供25MHz时钟源,再由PHY提供TX_CLK和RX_CLK时钟。对于RMII接口,一般需要外部直接提供50MHz时钟源,同时接入MAC和PHY。

开发板板载的PHY芯片型号为LAN8720A,该芯片只支持RMII接口,电路设计时参考图 RMII接口连接

ETH相关硬件在STM32F42x控制器分布参考表 ETH复用引脚

ETH复用引脚

其中,PPS_OUT是IEEE 1588定义的一个时钟同步机制。

42.5.3. MAC数据包发送和接收

ETH外设负责MAC数据包发送和接收。利用DMA从系统寄存器得到数据包数据内容,ETH外设自动填充完成MAC数据包封装,然后通过PHY发送出去。 在检测到有MAC数据包需要接收时,ETH外设控制数据接收,并解封MAC数据包得到解封后数据通过DMA传输到系统寄存器内。

42.5.3.1. MAC数据包发送

MAC数据帧发送全部由DMA控制,从系统存储器读取的以太网帧由DMA推入FIFO,然后将帧弹出并传输到MAC内核。帧传输结束后, 从MAC内核获取发送状态并传回DMA。在检测到SOF(Start Of Frame)时,MAC接收数据并开始MII发送。在EOF(End Of Frame)传输到MAC内核后, 内核将完成正常的发送,然后将发送状态返回给DMA。如果在发送过程中发送常规冲突,MAC内核将使发送状态有效,然后接受并丢弃所有后续数据, 直至收到下一SOF。检测到来自MAC的重试请求时,应从SOF重新发送同一帧。如果发送期间未连续提供数据,MAC将发出下溢状态。在帧的正常传输期间, 如果MAC在未获得前一帧的EOF的情况下接收到SOF,则将忽略该SOF并将新的帧视为前一帧的延续。

MAC控制MAC数据包的发送操作,它会自动生成前导字段和SFD以及发送帧状态返回给DMA,在半双工模式下自动生成阻塞信号, 控制jabber(MAC看门狗)定时器用于在传输字节超过2048字节时切断数据包发送。在半双工模式下,MAC使用延迟机制进行流量控制, 程序通过将ETH_MACFCR寄存器的BPA位置1来请求流量控制。MAC包含符合IEEE 1588的时间戳快照逻辑。 MAC数据包发送时序参考图 MAC数据包发送时序

MAC数据包发送时序

42.5.3.2. MAC数据包接收

MAC接收到的数据包填充RX FIFO,达到FIFO设定阈值后请求DMA传输。在默认直通模式下,当FIFO接收到64个字节(使用ETH_DMAOMR寄存器中的RTC位配置)或完整的数据包时, 数据将弹出,其可用性将通知给DMA。DMA向AHB接口发起传输后,数据传输将从FIFO持续进行,直到传输完整个数据包。完成EOF帧的传输后, 状态字将弹出并发送到DMA控制器。在Rx FIFO存储转发模式(通过ETH_DMAOMR寄存器中的RSF位配置)下,仅在帧完全写入Rx FIFO后才可读出帧。

当MAC在MII上检测到SFD时,将启动接收操作。MAC内核将去除报头和SFD,然后再继续处理帧。检查报头字段以进行过滤, FCS字段用于验证帧的CRC如果帧未通过地址滤波器,则在内核中丢弃该帧。MAC数据包接收时序参考图 MAC数据包接收时序

MAC数据包接收时序

42.5.4. MAC过滤

MAC过滤功能可以选择性的过滤设定目标地址或源地址的MAC帧。它将检查所有接收到的数据帧的目标地址和源地址,根据过滤选择设定情况, 检测后报告过滤状态。针对目标地址过滤可以有三种,分别是单播、多播和广播目标地址过滤;针对源地址过滤就只有单播源地址过滤。

单播目标地址过滤是将接收的相应DA字段与预设的以太网MAC地址寄存器内容比较,最高可预设4个过滤MAC地址。 多播目标地址过滤是根据帧过滤寄存器中的HM位执行对多播地址的过滤,是对MAC地址寄存器进行比较来实现的。 单播和多播目标地址过滤都还支持Hash过滤模式。广播目标地址过滤通过将帧过滤寄存器的BFD位置1使能,这使得MAC丢弃所有广播帧。

单播源地址过滤是将接收的SA字段与SA寄存器内容进行比较过滤。

MAC过滤还具备反向过滤操作功能,即让过滤结构求补集。

42.6. PHY:LAN8720A

LAN8720A是SMSC公司(已被Microchip公司收购)设计的一个体积小、功耗低、全能型10/100Mbps的以太网物理层收发器。 它是针对消费类电子和企业应用而设计的。LAN8720A总共只有24Pin,仅支持RMII接口。由它组成的网络结构见图 由LAN8720A组成的网络系统结构

由LAN8720A组成的网络系统结构

LAN8720A通过RMII与MAC连接。RJ45是网络插座,在与LAN8720A连接之间还需要一个变压器,所以一般使用带电压转换和LED指示灯的HY911105A型号的插座。 一般来说,必须为使用RMII接口的PHY提供50MHz的时钟源输入到REF_CLK引脚,不过LAN8720A内部集成PLL, 可以将25MHz的时钟源陪频到50MHz并在指定引脚输出该时钟,所以我们可以直接使其与REF_CLK连接达到提供50MHz时钟的效果。

LAN8720A内部系统结构见图 LAN8720A内部系统结构

LAN8720A内部系统结构

LAN8720A有各个不同功能模块组成,最重要的要数接收控制器和发送控制器,其它的基本上都是与外部引脚挂钩,实现信号传输。部分引脚是具有双重功能的, 比如PHYAD0与RXER引脚是共用的,在系统上电后LAN8720A会马上读取这部分共用引脚的电平,以确定系统的状态并保存在相关寄存器内,之后则自动转入作为另一功能引脚。

PHYAD[0]引脚用于配置SMI通信的LAN8720A地址,在芯片内部该引脚已经自带下拉电阻,默认认为0(即使外部悬空不接),在系统上电时会检测该引脚获取得到LAN8720A的地址为0或者1, 并保存在特殊模式寄存器(R18)的PHYAD位中,该寄存器的PHYAD有5个位,在需要超过2个LAN8720A时可以通过软件设置不同SMI通信地址。PHYAD[0]是与RXER引脚共用。

MODE[2:0]引脚用于选择LAN8720A网络通信速率和工作模式,可选10Mbps或100Mbps通信速度,半双工或全双工工作模式,另外LAN8720A支持HP Auto-MDIX自动翻转功能, 即可自动识别直连或交叉网线并自适应。一般将MODE引脚都设置为1,可以让LAN8720A启动自适应功能,它会自动寻找最优工作方式。MODE[0]与RXD0引脚共用、 MODE[1]与RXD1引脚共用、MODE[2]与CRS_DV引脚共用。

nINT/REFCLKO引脚用于RMII接口中REF_CLK信号线,当nINTSEL引脚为低电平是,它也可以被设置成50MHz时钟输出, 这样可以直接与STM32F42x的REF_CLK引脚连接为其提供50MHz时钟源,这种模式要求为XTAL1与XTAL2之间或为XTAL1/CLKIN提供25MHz时钟, 由LAN8720A内部PLL电路陪频得到50MHz时钟,此时nIN/REFCLKO引脚的中断功能不可用,用于50MHz时钟输出。当nINTSEL引脚为高电平时, LAN8720A被设置为时钟输入,即外部时钟源直接提供50MHz时钟接入STM32F42x的REF_CLK引脚和LAN8720A的XTAL1/CLKIN引脚, 此时nINT/REFCLKO可用于中断功能。nINTSEL与LED2引脚共用,一般使用下拉

REGOFF引脚用于配置内部+1.2V电压源,LAN8720A内部需要+1.2V电压,可以通过VDDCR引脚输入+1.2V电压提供,也可以直接利用LAN8720A内部+1.2V稳压器提供。 当REGOFF引脚为低电平时选择内部+1.2V稳压器。REGOFF与LED1引脚共用。

SMI支持寻址32个寄存器,LAN8720A只用到其中14个,参考表 LAN8720A寄存器列表

LAN8720A寄存器列表

序号与SMI数据帧中的RADDR是对应的,这在编写驱动时非常重要,本文将它们标记为R0~R31。寄存器可规划为三个组:Basic、Extended和Vendor-specific。 Basic是IEEE 802.3要求的,R0是基本控制寄存器,其位15为SoftReset位,向该位写1启动LAN8720A软件复位,还包括速度、自适应、低功耗等等功能设置。 R1是基本状态寄存器。Extended是扩展寄存器,包括LAN8720A的ID号、制造商、版本号等等信息。Vendor-specific是供应商自定义寄存器, R31是特殊控制/状态寄存器,指示速度类型和自适应功能。

42.7. LwIP:轻型TCP/IP协议栈

LwIP是Light Weight Internet Protocol 的缩写,是由瑞士计算机科学院Adam Dunkels等开发的适用于嵌入式领域的开源轻量级TCP/IP协议栈。它可以移植到含有操作系统的平台中,也可以在无操作系统的平台下运行。由于它开源、 占用的RAM和ROM比较少、支持较为完整的TCP/IP协议、且十分便于裁剪、调试,被广泛应用在中低端的32位控制器平台。 可以访问网站:http://savannah.nongnu.org/projects/lwip/ 获取更多LwIP信息。

目前,LwIP最新更新到1.4.1版本,我们在上述网站可找到相应的LwIP源码下载通道。我们下载两个压缩包:lwip-1.4.1.zip和contrib-1.4.1.zip, lwip-1.4.1.zip包括了LwIP的实现代码,contrib-1.4.1.zip包含了不同平台移植LwIP的驱动代码和使用LwIP实现的一些应用实例测试。

但是,遗憾的是contrib-1.4.1.zip并没有为STM32平台提供实例,这对于初学者想要移植LwIP来说难度还是非常大的。ST公司也是认识到LwIP在嵌入式领域的重要性, 所以他们针对LwIP应用开发了测试平台,其中有一个是在STM32F4x7系列控制器运行的 (文件编号为:STSW-STM32070),虽然我们的开发板平台是STM32F429控制器,但经测试发现关于ETH驱动部分以及LwIP接口函数部分是可以通用的。 为减少移植工作量,我们选择使用ST官方例程相关文件,特别是ETH底层驱动部分函数,这样我们也可以花更多精力在理解代码实现方法上。

本章的一个重点内容就是介绍LwIP移植至我们的开发平台,详细的操作步骤参考下文介绍。

42.8. ETH初始化结构体详解

一般情况下,标准库都会为外设建立一个外设对应的文件存放外设相关库函数的实现,比如stm32f4xx_adc.c、stm32f4xx_can.c等等,然而标准库并没有为ETH外设建立相关的文件, 这样我们根本没有标准库函数可以使用,究其原因是ETH驱动函数与PHY芯片连续较为紧密,很难使用一套通用的代码实现兼容。难道要我们自己写寄存器实现? 实际情况还没有这么糟糕,正如上文所说的ST官方有提供LwIP方面的测试平台,特别是基于STM32F4x7控制器的测试平台是非常合适我们参考的。 我们在解压stsw-stm32070.rar压缩包之后,在其文件目录(…STM32F4x7_ETH_LwIP_V1.1.1LibrariesSTM32F4x7_ETH_Driver)下可找到stm32f4x7_eth.c、 stm32f4x7_eth.h和stm32f4x7_eth_conf_template.h三个文件,其中的stm32f4x7_eth.c和stm32f4x7_eth.h就是类似stm32f4xx_adc.c是关于ETH外设的驱动, 我们在以太网通信实现实验中会使用到这三个文件,stm32f4x7_eth.c和stm32f4x7_eth.h两个文件内容不用修改。(不过修改了文件名称)

stm32f4x7_eth.h有定义了一个ETH外设初始化结构体ETH_InitTypeDef,理解结构体成员可以帮助我们使用ETH功能。初始化结构体成员用于设置ETH工作环境参数, 并由ETH相应初始化配置函数或功能函数调用,这些设定参数将会设置ETH相应的寄存器,达到配置ETH工作环境的目的。

代码清单:ETH-1 ETH_InitTypeDef
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
typedef struct {
    /**
    * @brief / * MAC
    */
    uint32_t   ETH_AutoNegotiation;             // 自适应功能
    uint32_t   ETH_Watchdog;                    // 以太网看门狗
    uint32_t   ETH_Jabber;                      // jabber定时器功能
    uint32_t   ETH_InterFrameGap;               // 发送帧间间隙
    uint32_t   ETH_CarrierSense;                // 载波侦听
    uint32_t   ETH_Speed;                       // 以太网速度
    uint32_t   ETH_ReceiveOwn;                  // 接收自身
    uint32_t   ETH_LoopbackMode;                // 回送模式
    uint32_t   ETH_Mode;                        // 模式
    uint32_t   ETH_ChecksumOffload;             // 校验和减荷
    uint32_t   ETH_RetryTransmission;           // 传输重试
    uint32_t   ETH_AutomaticPadCRCStrip;        // 自动去除PAD和FCS字段
    uint32_t   ETH_BackOffLimit;                // 后退限制
    uint32_t   ETH_DeferralCheck;               // 检查延迟
    uint32_t   ETH_ReceiveAll;                  // 接收所有MAC帧
    uint32_t   ETH_SourceAddrFilter;            // 源地址过滤
    uint32_t   ETH_PassControlFrames;           // 传送控制帧
    uint32_t   ETH_BroadcastFramesReception;    // 广播帧接收
    uint32_t   ETH_DestinationAddrFilter;       // 目标地址过滤
    uint32_t   ETH_PromiscuousMode;             // 混合模式
    uint32_t   ETH_MulticastFramesFilter;       // 多播源地址过滤
    uint32_t   ETH_UnicastFramesFilter;         // 单播源地址过滤
    uint32_t   ETH_HashTableHigh;               // 散列表高位
    uint32_t   ETH_HashTableLow;                // 散列表低位
    uint32_t   ETH_PauseTime;                   // 暂停时间
    uint32_t   ETH_ZeroQuantaPause;             // 零时间片暂停
    uint32_t   ETH_PauseLowThreshold;           // 暂停阈值下限
    uint32_t   ETH_UnicastPauseFrameDetect;     // 单播暂停帧检测
    uint32_t   ETH_ReceiveFlowControl;          // 接收流控制
    uint32_t   ETH_TransmitFlowControl;         // 发送流控制
    uint32_t   ETH_VLANTagComparison;           // VLAN标记比较
    uint32_t   ETH_VLANTagIdentifier;           // VLAN标记标识符
    /**
    * @brief / * DMA
    */
    uint32_t   ETH_DropTCPIPChecksumErrorFrame; // 丢弃TCP/IP校验错误帧
    uint32_t   ETH_ReceiveStoreForward;         // 接收存储并转发
    uint32_t   ETH_FlushReceivedFrame;          // 刷新接收帧
    uint32_t   ETH_TransmitStoreForward;        // 发送存储并并转发
    uint32_t   ETH_TransmitThresholdControl;    // 发送阈值控制
    uint32_t   ETH_ForwardErrorFrames;          // 转发错误帧
    uint32_t   ETH_ForwardUndersizedGoodFrames; // 转发过小的好帧
    uint32_t   ETH_ReceiveThresholdControl;     // 接收阈值控制
    uint32_t   ETH_SecondFrameOperate;          // 处理第二个帧
    uint32_t   ETH_AddressAlignedBeats;         // 地址对齐节拍
    uint32_t   ETH_FixedBurst;                  // 固定突发
    uint32_t   ETH_RxDMABurstLength;            // DMA突发接收长度
    uint32_t   ETH_TxDMABurstLength;            // DMA突发发送长度
    uint32_t   ETH_DescriptorSkipLength;        // 描述符跳过长度
    uint32_t   ETH_DMAArbitration;              // DMA仲裁
} ETH_InitTypeDef;
  • ETH_AutoNegotiation:自适应功能选择,可选使能或禁止,一般选择使能自适应功能, 系统会自动寻找最优工作方式,包括选择10Mbps或者100Mbps的以太网速度以及全双工模式或半双工模式。

  • ETH_Watchdog:以太网看门狗功能选择,可选使能或禁止,它设定以太网MAC配置寄存器(ETH_MACCR)的WD位的值。 如果设置为1,使能看门狗,在接收MAC帧超过2048字节时自动切断后面数据,一般选择使能看门狗。如果设置为0,禁用看门狗,最长可接收16384字节的帧。

  • ETH_Jabber:jabber定时器功能选择,可选使能或禁止,与看门狗功能类似,只是看门狗用于接收MAC帧,jabber定时器用于发送MAC帧, 它设定ETH_MACCR寄存器的JD位的值。如果设置为1,使能jabber定时器,在发送MAC帧超过2048字节时自动切断后面数据,一般选择使能jabber定时器。

  • ETH_InterFrameGap:控制发送帧间的最小间隙,可选96bit时间、88bit时间、…、40bit时间, 他设定ETH_MACCR寄存器的IFG[2:0]位的值,一般设置96bit时间。

  • ETH_CarrierSense:载波侦听功能选择,可选使能或禁止,它设定ETH_MACCR寄存器的CSD位的值。 当被设置为低电平时,MAC发送器会生成载波侦听错误,一般使能载波侦听功能。

  • ETH_Speed:以太网速度选择,可选10Mbps或100Mbit/s,它设定ETH_MACCR寄存器的FES位的值, 一般设置100Mbit/s,但在使能自适应功能之后该位设置无效。

  • ETH_ReceiveOwn:接收自身帧功能选择,可选使能或禁止,它设定ETH_MACCR寄存器的ROD位的值, 当设置为0时,MAC接收发送时PHY提供的所有MAC包,如果设置为1,MAC禁止在半双工模式下接收帧。一般使能接收。

  • ETH_LoopbackMode:回送模式选择,可选使能或禁止,它设定ETH_MACCR寄存器的LM位的值, 当设置为1时,使能MAC在MII回送模式下工作。

  • ETH_Mode:以太网工作模式选择,可选全双工模式或半双工模式,它设定ETH_MACCR寄存器DM位的值。 一般选择全双工模式,在使能了自适应功能后该成员设置无效。

  • ETH_ChecksumOffload:IPv4校验和减荷功能选择,可选使能或禁止,它设定ETH_MACCR寄存器IPCO位的值, 当该位被置1时使能接收的帧有效载荷的TCP/UDP/ICMP标头的IPv4校验和检查。一般选择禁用,此时PCE和IP HCE状态位总是为0。

  • ETH_RetryTransmission:传输重试功能,可选使能或禁止,它设定ETH_MACCR寄存器RD位的值,当被设置为1时, MAC仅尝试发送一次,设置为0时,MAC会尝试根据BL的设置进行重试。一般选择使能重试。

  • ETH_AutomaticPadCRCStrip:自动去除PAD和FCS字段功能,可选使能或禁用,它设定ETH_MACCR寄存器APCS位的值。 当设置为1时,MAC在长度字段值小于或等于1500自己是去除传入帧上的PAD和FCS字段。一般禁止自动去除PAD和FCS字段功能。

  • ETH_BackOffLimit:后退限制,在发送冲突后重新安排发送的延迟时间,可选10、8、4、1, 它设定ETH_MACCR寄存器BL位的值。一般设置为10。

  • ETH_DeferralCheck:检查延迟,可选使能或禁止,它设定ETH_MACCR寄存器DC位的值,当设置为0时, 禁止延迟检查功能,MAC发送延迟,直到CRS信号变成无效信号。

  • ETH_ReceiveAll:接收所有MAC帧,可选使能或禁用,它设定以太网MAC帧过滤寄存器(ETH_MACFFR)RA位的值。 当设置为1时,MAC接收器将所有接收的帧传送到应用程序,不过滤地址。当设置为0是,MAC接收会自动过滤不与SA/DA匹配的帧。一般选择不接收所有。

  • ETH_SourceAddrFilter:源地址过滤,可选源地址过滤、源地址反向过滤或禁用源地址过滤, 它设定ETH_MACFFR寄存器SAF位和SAIF位的值。一般选择禁用源地址过滤。

  • ETH_PassControlFrames:传送控制帧,控制所有控制帧的转发,可选阻止所有控制帧到达应用程序、转发所有控制帧、 转发通过地址过滤的控制帧,它设定ETH_MACFFR寄存器PCF位的值。一般选择禁止转发控制帧。

  • ETH_BroadcastFramesReception:广播帧接收,可选使能或禁止,它设定ETH_MACFFR寄存器BFD位的值。 当设置为0时,使能广播帧接收,一般设置接收广播帧。

  • ETH_DestinationAddrFilter:目标地址过滤功能选择,可选正常过滤或目标地址反向过滤, 它设定ETH_MACFFR寄存器DAIF位的值。一般设置为正常过滤。

  • ETH_PromiscuousMode:混合模式,可选使能或禁用,它设定ETH_MACFFR寄存器PM位的值。当设置为1时, 不论目标或源地址,地址过滤器都传送所有传入的帧。一般禁用混合模式。

  • ETH_MulticastFramesFilter:多播源地址过滤,可选完美散列表过滤、散列表过滤、完美过滤或禁用过滤, 它设定ETH_MACFFR寄存器HPF位、PAM位和HM位的值。一般选择完美过滤。

  • ETH_UnicastFramesFilter:单播源地址过滤,可选完美散列表过滤、散列表过滤或完美过滤,它设定ETH_MACFFR寄存器HPF位和HU位的值。一般选择完美过滤。

  • ETH_HashTableHigh:散列表高位,和ETH_HashTableLow组成64位散列表用于组地址过滤,它设定以太网MAC散列表高位寄存器(ETH_MACHTHR)的值。

  • ETH_HashTableLow:散列表低位,和ETH_ HashTableHigh组成64位散列表用于组地址过滤,它设定以太网MAC散列表低位寄存器(ETH_MACHTLR)的值。

  • ETH_PauseTime:暂停时间,保留发送控制帧中暂停时间字段要使用的值,可设置0至65535,它设定以太网MAC流控制寄存器(ETH_MACFCR)PT位的值。

  • ETH_ZeroQuantaPause:零时间片暂停,可选使用或禁止,它设定ETH_MACFCR寄存器ZQPD位的值。当设置为1时,当来自FIFO层的流控制信号去断言后, 此位会禁止自动生成零时间片暂停控制帧。一般选择禁止。

  • ETH_PauseLowThreshold:暂停阈值下限,配置暂停定时器的阈值,达到该值值时,会自动程序传输暂停帧,可选暂停时间减去4个间隙、28个间隙、 144个间隙或256个间隙,它设定ETH_MACFCR寄存器PLT位的值。一般选择暂停时间减去4个间隙。

  • ETH_UnicastPauseFrameDetect:单播暂停帧检测,可选使能或禁止,它设定ETH_MACFCR寄存器UPFD位的值。当设置为1时,MAC除了检测具有唯一多播地址的暂停帧外, 还会检测具有ETH_MACA0HR和ETH_MACA0LR寄存器所指定的站单播地址的暂停帧。一般设置为禁止。

  • ETH_ReceiveFlowControl:接收流控制,可选使能或禁止,它设定ETH_MACFCR寄存器RFCE位的值。当设定为1时,MAC对接收到的暂停帧进行解码, 并禁止其在指定时间(暂停时间)内发送;当设置为0时,将禁止暂停帧的解码功能,一般设置为禁止。

  • ETH_TransmitFlowControl:发送流控制,可选使能或禁止,它设定ETH_MACFCR寄存器TFCE位的值。在全双工模式下,当设置为1时, MAC将使能流控制操作来发送暂停帧;为0时,将禁止MAC中的流控制操作,MAC不会传送任何暂停帧。在半双工模式下,当设置为1时,MAC将使能背压操作;为0时,将禁止背压功能。

  • ETH_VLANTagComparison:VLAN标记比较,可选12位或16位,它设定以太网MACVLAN标记寄存器(ETH_MACVLANTR)VLANTC位的值。 当设置为1时,使用12位VLAN标识符而不是完整的16位VLAN标记进行比较和过滤;为0时,使用全部16位进行比较,一般选择16位。

  • ETH_VLANTagIdentifier:VLAN标记标识符,包含用于标识VLAN帧的802.1QVLAN标记,并与正在接收的VLAN帧的第十五和第十六字节进行比较。 位[15:13]是用户优先级,位[12]是标准格式指示符(CFI),位[11:0]是VLAN标记的VLAN标识符(VID)字段。VLANTC位置1时,仅使用VID(位[11:0])进行比较。

  • ETH_DropTCPIPChecksumErrorFrame:丢弃TCP/IP校验错误帧,可选使能或禁止,它设定以太网DMA工作模式寄存器(ETH_DMAOMR)DTCEFD位的值, 当设置为1时,如果帧中仅存在由接收校验和减荷引擎检测出来的错误,则内核不会丢弃它;为0时,如果FEF为进行了复位,则会丢弃所有错误帧。

  • ETH_ReceiveStoreForward:接收存储并转发,可选使能或禁止,它设定以太网DMA工作模式寄存器(ETH_DMAOMR)RSF位的值,当设置为1时, 向RX FIFO写入完整帧后可以从中读取一帧,同时忽略接收阈值控制(RTC)位;当设置为0时,RXFIFO在直通模式下工作,取决于RTC位的阈值。一般选择使能。

  • ETH_FlushReceivedFrame:刷新接收帧,可选使能或禁止,它设定ETH_DMAOMR寄存器FTF位的值,当设置为1时,发送FIFO控制器逻辑会恢复到缺省值, TX FIFO中的所有数据均会丢失/刷新,刷新结束后改为自动清零。

  • ETH_TransmitStoreForward:发送存储并并转发,可选使能或禁止,它设定ETH_DMAOMR寄存器TSF位的值,当设置为1时,如果TX FIFO有一个完整的帧则发送会启动, 会忽略TTC值;为0时,TTC值才会有效。一般选择使能。

  • ETH_TransmitThresholdControl:发送阈值控制,有多个阈值可选,它设定ETH_DMAOMR寄存器TTC位的值,当TX FIFO中帧大小大于该阈值时发送会自动,对于小于阈值的全帧也会发送。

  • ETH_ForwardErrorFrames:转发错误帧,可选使能或禁止,它设定ETH_DMAOMR寄存器FEF位的值,当设置为1时,除了段错误帧之外所有帧都会转发到DMA; 为0时,RX FIFO会丢弃滴啊有错误状态的帧。一般选择禁止。

  • ETH_ForwardUndersizedGoodFrames:转发过小的好帧,可选使能或禁止,它设定ETH_DMAOMR寄存器FUGF位的值,当设置为1时,RX FIFO会转发包括PAD和FCS字段的过小帧; 为0时,会丢弃小于64字节的帧,除非接收阈值被设置为更低。

  • ETH_ReceiveThresholdControl:接收阈值控制,当RX FIFO中的帧大小大于阈值时启动DMA传输请求,可选64字节、32字节、96字节或128字节,它设定ETH_DMAOMR寄存器RTC位的值。

  • ETH_SecondFrameOperate:处理第二个帧,可选使能或禁止,它设定ETH_DMAOMR寄存器OSF位的值,当设置为1时会命令DMA处理第二个发送数据帧。

  • ETH_AddressAlignedBeats:地址对齐节拍,可选使能或禁止,它设定以太网DMA总线模式寄存器(ETH_DMABMR)AAB位的值,当设置为1并且固定突发位(FB)也为1时, AHB接口会生成与起始地址LS位对齐的所有突发;如果FB位为0,则第一个突发不对齐,但后续的突发与地址对齐。一般选择使能。

  • ETH_FixedBurst:固定突发,控制AHB主接口是否执行固定突发传输,可选使能或禁止,它设定ETH_DMABMR寄存器FB位的值,当设置为1时, AHB在正常突发传输开始期间使用SINGLE、INCR4、INCR8或INCR16;为0时,AHB使用SINGLE和INCR突发传输操作。

  • ETH_RxDMABurstLength:DMA突发接收长度,有多个值可选,一般选择32Beat,可实现32*32bits突发长度,它设定ETH_DMABMR寄存器FPM位和RDP位的值。

  • ETH_TxDMABurstLength:DMA突发发送长度,有多个值可选,一般选择32Beat,可实现32*32bits突发长度,它设定ETH_DMABMR寄存器FPM位和PBL位的值。

  • ETH_DescriptorSkipLength:描述符跳过长度,指定两个未链接描述符之间跳过的字数,地址从当前描述符结束处开始跳到下一个描述符起始处,可选0~7, 它设定ETH_DMABMR寄存器DSL位的值。

  • ETH_DMAArbitration:DMA仲裁,控制RX和TX优先级,可选RX TX优先级比为1:1、2:1、3:1、4:1或者RX优先于TX,它设定ETH_DMABMR寄存器PM位和DA位的值, 当设置为1时,RX优先于TX;为0时,循环调度,RX TX优先级比由PM位给出。

42.9. 以太网通信实验:无操作系统LwIP移植

LwIP可以在带操作系统上运行,亦可在无操作系统上运行,这一实验我们讲解在无操作系统的移植步骤,并实现简单的传输代码, 后续章节会讲解在带操作系统移植过程,一般都是在无操作系统基础上修改而来的。

42.9.1. 硬件设计

在讲解移植步骤之前,有必须先介绍我们的实验硬件设计,主要是LAN8720A通过RMII和SMI接口与STM32F42x控制器连接,见图 PHY硬件设计

PHY硬件设计

电路设计时,将NINTSEL引脚通过下拉电阻拉低,设置NINT/FEFCLKO为输出50MHz时钟,当然前提是在XTAL1和XTAL2接入了25MHz的时钟源。 另外也把REGOFF引脚通过下拉电阻拉低,使能使用内部+1.2V稳压器。

42.9.2. 移植步骤

之前已经介绍了LwIP源代码(lwip-1.4.1.zip)和ST官方LwIP测试平台资料(stsw-stm32070.zip)下载,我们移植步骤是基于这两份资料进行的。

无操作系统移植LwIP需要的文件参考图 LwIP移植实验文件结构 ,图中只显示了*.c文件,还需要用到对应的*.h文件。

LwIP移植实验文件结构

接下来,我们就根据图中文件结构详解移植过程。实验例程有需要用到系统滴答定时器systick、调试串口USART、独立按键KEY、LED灯功能, 对这些功能实现不做具体介绍,可以参考相关章节理解。

第一步:相关文件拷贝

首先,解压lwip-1.4.1.zip和stsw-stm32070.zip两个压缩包,把整个lwip-1.4.1文件夹拷贝到USER文件夹下,特别说明,在整个移植过程中, 不会对lwip-1.4.1.zip文件下的文件内容进行修改。然后,在stsw-stm32070文件夹找到port文件夹(路径:…\Utilities\Third_Party\lwip-1.4.1\port), 把整个port文件夹拷贝lwip-1.4.1文件夹中,在port文件夹下的STM32F4x7文件中把arch和Standalone两个文件夹直接剪切到port文件夹中, 即此时port文件夹有三个STM32F4x7、arch和Standalone文件夹,最后把STM32F4x7文件夹删除,最终的文件结构见图 LwIP相关文件拷贝 , arch存放与开发平台相关头文件,Standalone文件夹是无操作系统移植时ETH外设与LwIP连接的底层驱动函数。

LwIP相关文件拷贝

lwip-1.4.1文件夹下的doc文件夹存放LwIP版权、移植、使用等等说明文件,移植之前有必须认真浏览一遍;src文件夹存放LwIP的实现代码, 也是我们工程代码真正需要的文件;test文件夹存放LwIP部分功能测试例程;另外,还有一些无后缀名的文件,都是一些说明性文件,可用记事本直接打开浏览。 port文件夹存放LwIP与STM32平台连接的相关文件,正如上面所说contrib-1.4.1.zip包含了不同平台移植代码,不过遗憾地是没有STM32平台的, 所以我们需要从ST官方提供的测试平台找到这部分连接代码,也就是port文件夹的内容。

接下来,在Bsp文件下新建一个ETH文件夹,用于存放与ETH相关驱动文件,包括两个部分文件,其中一个是ETH外设驱动文件, 在stsw-stm32070文件夹中找到stm32f4x7_eth.h和stm32f4x7_eth.c两个文件(路径:…\Libraries\STM32F4x7_ETH_Driver\), 将这两个文件拷贝到ETH文件夹中,对应改名为stm32f429_eth.h和stm32f429_eth.c,这两个文件是ETH驱动文件,类似标准库中外设驱动代码实现文件,在移植过程中我们几乎不过文件的内容。 这部分函数由port文件夹相关代码调用。另外一部分是相关GPIO初始化、ETH外设初始化、PHY状态获取等等函数的实现, 在stsw-stm32070文件夹中找到stm32f4x7_eth_bsp.c、stm32f4x7_eth_bsp.h和stm32f4x7_eth_conf.h三个文件(路径:…\Project\Standalone\tcp_echo_client\), 将这三个文件拷贝到ETH文件夹中。,对应改名为stm32f429_phy.c、stm32f429_phy.h和stm32f429_eth_conf.h。因为,ST官方LwIP测试平台使用的PHY型号不是使用LAN8720A,所以这三个文件需要我们进行修改。

最后,是LwIP测试代码实现,为测试LwIP移植是否成功和检查LwIP功能,我们编写TCP通信实现代码,设置开发板为TCP从机,电脑端为TCP主机。 在stsw-stm32070文件夹中找到netconf.c、tcp_echoclient.c、lwipopts.h、netconf.h和tcp_echoclient.h五个文件(路径:…\Project\Standalone\tcp_echo_client\), 直接拷贝到App文件夹(自己新建)中,netconf.c文件代码实现LwIP初始化函数、周期调用函数、DHCP功能函数等等,tcp_echoclient.c文件实现TCP通信参数代码, lwipopts.h包含LwIP功能选项。

第二部:为工程添加文件

第一步已经把相关的文件拷贝到对应的文件夹中,接下来就可以把需要用到的文件添加到工程中。 图 LwIP移植实验文件结构 已经指示出来工程需要用到的*.c文件,所以最终工程文件结构见 图工程文件结构 , 图中api、ipv4和core都包含了对应文件夹下的所有*.c文件。

图工程文件结构

接下来,还需要在工程选择中添加相关头文件路径,参考图 添加相关头文件路径

添加相关头文件路径

第三步:文件修改

ethernetif.c文件是无操作系统时网络接口函数,该文件在移植是只需修改相关头文件名,函数实现部分无需修改。该文件主要有三个部分函数, 一个是low_level_init,用于初始化MAC相关工作环境、初始化DMA描述符链表,并使能MAC和DMA;一个是low_level_output, 它是最底层发送一帧数据函数;最后一个是low_level_input,它是最底层接收一帧数据函数。

stm32f429_eth.c和stm32f429_eth.h两个文件用于ETH驱动函数实现,它是通过直接操作寄存器方式实现,这两个文件我们无需修改。 stm32f429_eth_conf.h文件包含了一些功能选项的宏定义,我们对部分内容进行了修改。

代码清单:ETH-2 stm32f429_eth_conf.h文件宏定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifdef USE_Delay
#include "Bsp/systick/bsp_SysTick.h"
#define _eth_delay_    Delay_10ms
#else
#define _eth_delay_    ETH_Delay
#endif

#ifdef USE_Delay
/* LAN8742A Reset delay */
#define LAN8742A_RESET_DELAY    ((uint32_t)0x00000005)
#else
/* LAN8742A Reset delay */
#define LAN8742A_RESET_DELAY    ((uint32_t)0x00FFFFFF)
#endif

/* The LAN8742A PHY status register  */
/* PHY status register Offset */
#define PHY_SR                 ((uint16_t)0x001F)
/* PHY Speed mask  1:10Mb/s       0:100Mb/s*/
#define PHY_SPEED_STATUS       ((uint16_t)0x0004)
/* PHY Duplex mask 1:Full duplex  0:Half duplex*/
#define PHY_DUPLEX_STATUS      ((uint16_t)0x0010)

通过宏定义USE_Delay可选是否使用自定义的延时函数,Delay_10ms函数是通过系统滴答定时器实现的延时函数,ETH_Delay函数是ETH驱动自带的简单循环延时函数, 延时函数实现方法不同,对形参要求不同。因为ST官方例程是基于DP83848型号的PHY,而开发板的PHY型号是LAN8720A。LAN8720A复位时需要一段延时时间, 这里需要定义延时时间长度,大约50ms。驱动代码中需要获取PHY的速度和工作模式,LAN8720A的R31是特殊控制/状态寄存器,包括指示以太网速度和工作模式的状态位。

stm32f429_phy.c和stm32f429_phy.h两个文件是ETH外设相关的底层配置,包括RMII接口GPIO初始化、SMI接口GPIO初始化、MAC控制器工作环境配置, 还有一些PHY的状态获取和控制修改函数。ST官方例程文件包含了中断引脚的相关配置,主要用于指示接收到以太网帧,我们这里不需要使用, 采用无限轮询方法检测接收状态。stm32f429_phy.h文件存放相关宏定义,包含RMII和SMI引脚信息等宏定义,其中要特别说明的有一个宏, 定义了PHY地址:ETHERNET_PHY_ADDRESS,这里根据硬件设计设置为0x00,这在SMI通信是非常重要的。

代码清单:ETH-3 ETH_GPIO_Config函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
void ETH_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* Enable GPIOs clocks */
    RCC_AHB1PeriphClockCmd(ETH_MDIO_GPIO_CLK | ETH_MDC_GPIO_CLK |
            ETH_RMII_REF_CLK_GPIO_CLK|ETH_RMII_CRS_DV_GPIO_CLK|
            ETH_RMII_RXD0_GPIO_CLK | ETH_RMII_RXD1_GPIO_CLK |
            ETH_RMII_TX_EN_GPIO_CLK | ETH_RMII_TXD0_GPIO_CLK |
            ETH_RMII_TXD1_GPIO_CLK ,| ETH_NRST_GPIO_CLK,  ENABLE);

    /* Enable SYSCFG clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

    /* MII/RMII Media interface selection ------------------------------

#ifdef MII_MODE /* Mode MII with STM324xx-EVAL  */
#ifdef PHY_CLOCK_MCO
    /* Output HSE clock (25MHz) on MCO pin (PA8) to clock the PHY */
    RCC_MCO1Config(RCC_MCO1Source_HSE, RCC_MCO1Div_1);
#endif /* PHY_CLOCK_MCO */

    SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_MII);
#elif defined RMII_MODE  /* Mode RMII with STM324xx-EVAL */

    SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_RMII);
#endif

    /* Ethernet pins configuration *************************************/
    /*
        ETH_MDIO -------------------------> PA2
        ETH_MDC --------------------------> PC1
        ETH_MII_RX_CLK/ETH_RMII_REF_CLK---> PA1
        ETH_MII_RX_DV/ETH_RMII_CRS_DV ----> PA7
        ETH_MII_RXD0/ETH_RMII_RXD0 -------> PC4
        ETH_MII_RXD1/ETH_RMII_RXD1 -------> PC5
        ETH_MII_TX_EN/ETH_RMII_TX_EN -----> PB11
        ETH_MII_TXD0/ETH_RMII_TXD0 -------> PG13
        ETH_MII_TXD1/ETH_RMII_TXD1 -------> PG14
        ETH_NRST -------------------------> PI1
                                                */
    GPIO_InitStructure.GPIO_Pin = ETH_NRST_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
    GPIO_Init(ETH_NRST_PORT, &GPIO_InitStructure);

    ETH_NRST_PIN_LOW();
    _eth_delay_(LAN8742A_RESET_DELAY);
    ETH_NRST_PIN_HIGH();
    _eth_delay_(LAN8742A_RESET_DELAY);

    /* Configure ETH_MDIO */
    GPIO_InitStructure.GPIO_Pin = ETH_MDIO_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(ETH_MDIO_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(ETH_MDIO_PORT, ETH_MDIO_SOURCE, ETH_MDIO_AF);

    /* Configure ETH_MDC */
    GPIO_InitStructure.GPIO_Pin = ETH_MDC_PIN;
    GPIO_Init(ETH_MDC_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(ETH_MDC_PORT, ETH_MDC_SOURCE, ETH_MDC_AF);

    /**************************************/
    /**      省略部分引脚初始化         ***/
    /**************************************/

    /* Configure ETH_RMII_TXD1 */
    GPIO_InitStructure.GPIO_Pin = ETH_RMII_TXD1_PIN;
    GPIO_Init(ETH_RMII_TXD1_PORT, &GPIO_InitStructure);
    GPIO_PinAFConfig(ETH_RMII_TXD1_PORT, ETH_RMII_TXD1_SOURCE,
                                        ETH_RMII_TXD1_AF);
}

STM32f42x控制器支持MII和RMII接口,通过程序控制使用RMII接口,同时需要使能SYSYCFG时钟,函数后部分就是接口GPIO初始化实现, 这里我们还连接了LAN8720A的复位引脚,通过拉低一段时间让芯片硬件复位。

代码清单:ETH-4 ETH_MACDMA_Config函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
static void ETH_MACDMA_Config(void)
{
    /* Enable ETHERNET clock  */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC |
    RCC_AHB1Periph_ETH_MAC_Tx|RCC_AHB1Periph_ETH_MAC_Rx,ENABLE);

    /* Reset ETHERNET on AHB Bus */
    ETH_DeInit();
    /* Software reset */
    ETH_SoftwareReset();
    /* Wait for software reset */
    while (ETH_GetSoftwareResetStatus() == SET);

    /* ETHERNET Configuration ------------------------------*/
    /* 缺省配置ETH_InitStructure */
    ETH_StructInit(&ETH_InitStructure);

    /* Fill ETH_InitStructure parametrs */
    /*--------------------   MAC   ----------------------------*/
    /* 开启网络自适应功能,速度和工作模式无需配置 */
    ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Enable;
//  ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Disable;
//  ETH_InitStructure.ETH_Speed = ETH_Speed_10M;
//  ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;
    /* 关闭反馈 */
    ETH_InitStructure.ETH_LoopbackMode = ETH_LoopbackMode_Disable;
    /* 关闭重传功能 */
ETH_InitStructure.ETH_RetryTransmission=ETH_RetryTransmission_Disable;
    /* 关闭自动去除PDA/CRC功能  */
ETH_InitStructure.ETH_AutomaticPadCRCStrip =
                            ETH_AutomaticPadCRCStrip_Disable;
    /* 关闭接收所有的帧 */
    ETH_InitStructure.ETH_ReceiveAll = ETH_ReceiveAll_Disable;
    /* 允许接收所有广播帧 */
    ETH_InitStructure.ETH_BroadcastFramesReception =
                            ETH_BroadcastFramesReception_Enable;
    /* 关闭混合模式的地址过滤  */
    ETH_InitStructure.ETH_PromiscuousMode = ETH_PromiscuousMode_Disable;
    /* 对于组播地址使用完美地址过滤    */
    ETH_InitStructure.ETH_MulticastFramesFilter =
                                ETH_MulticastFramesFilter_Perfect;
    /* 对单播地址使用完美地址过滤  */
    ETH_InitStructure.ETH_UnicastFramesFilter =
                            ETH_UnicastFramesFilter_Perfect;
#ifdef CHECKSUM_BY_HARDWARE
    /* 开启ipv4和TCP/UDP/ICMP的帧校验和卸载   */
    ETH_InitStructure.ETH_ChecksumOffload = ETH_ChecksumOffload_Enable;
#endif

    /*------------------------   DMA   -------------------------------*/
    /*当我们使用帧校验和卸载功能的时候,一定要使能存储转发模式,存储
    转发模式中要保证整个帧存储在FIFO中,  这样MAC能插入/识别出帧校验
    值,当真校验正确的时候DMA就可以处理帧,否则就丢弃掉该帧*/
    /* 开启丢弃TCP/IP错误帧 */
    ETH_InitStructure.ETH_DropTCPIPChecksumErrorFrame =
                ETH_DropTCPIPChecksumErrorFrame_Enable;
    /* 开启接收数据的存储转发模式  */
    ETH_InitStructure.ETH_ReceiveStoreForward =
                            ETH_ReceiveStoreForward_Enable;
    /* 开启发送数据的存储转发模式   */
    ETH_InitStructure.ETH_TransmitStoreForward =
                                ETH_TransmitStoreForward_Enable;

    /* 禁止转发错误帧 */
    ETH_InitStructure.ETH_ForwardErrorFrames =
                                ETH_ForwardErrorFrames_Disable;
    /* 不转发过小的好帧 */
    ETH_InitStructure.ETH_ForwardUndersizedGoodFrames =
                    ETH_ForwardUndersizedGoodFrames_Disable;
    /* 打开处理第二帧功能 */
    ETH_InitStructure.ETH_SecondFrameOperate =
                                ETH_SecondFrameOperate_Enable;
    /* 开启DMA传输的地址对齐功能 */
    ETH_InitStructure.ETH_AddressAlignedBeats =
                                ETH_AddressAlignedBeats_Enable;
    /* 开启固定突发功能 */
    ETH_InitStructure.ETH_FixedBurst = ETH_FixedBurst_Enable;
    /* DMA发送的最大突发长度为32个节拍 */
    ETH_InitStructure.ETH_RxDMABurstLength = ETH_RxDMABurstLength_32Beat;
    /*DMA接收的最大突发长度为32个节拍 */
    ETH_InitStructure.ETH_TxDMABurstLength = ETH_TxDMABurstLength_32Beat;
    ETH_InitStructure.ETH_DMAArbitration =
                        ETH_DMAArbitration_RoundRobin_RxTx_2_1;

    /* 配置ETH */
    EthStatus = ETH_Init(&ETH_InitStructure, ETHERNET_PHY_ADDRESS);
}

首先是使能ETH时钟,复位ETH配置。ETH_StructInit函数用于初始化ETH_InitTypeDef结构体变量,会给每个成员赋予缺省值。 接下来就是根据需要配置ETH_InitTypeDef结构体变量,关于结构体各个成员意义已在“ETH初始化结构体详解”作了分析。 最后调用ETH_Init函数完成配置,ETH_Init函数有两个形参,一个是ETH_InitTypeDef结构体变量指针,第二个是PHY地址,函数还有一个返回值,用于指示初始化配置是否成功。

代码清单:ETH-5 ETH_BSP_Config函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#define GET_PHY_LINK_STATUS()
    (ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS,PHY_BSR)&0x00000004)

void ETH_BSP_Config(void)
{
    /* Configure the GPIO ports for ethernet pins */
    ETH_GPIO_Config();

    /* Configure the Ethernet MAC/DMA */
    ETH_MACDMA_Config();

    /* Get Ethernet link status*/
    if (GET_PHY_LINK_STATUS()) {
        EthStatus |= ETH_LINK_FLAG;
    }
}

GET_PHY_LINK_STATUS()是定义获取PHY链路状态的宏,如果PHY连接正常那么整个宏定义为1,如果不正常则为0, 它是通过ETH_ReadPHYRegister函数读取PHY的基本状态寄存器(PHY_BSR)并检测其Link Status位得到的。

ETH_BSP_Config函数分别调用ETH_GPIO_Config和ETH_MACDMA_Config函数完成ETH初始化配置,最后调用GET_PHY_LINK_STATUS()来判断PHY状态, 并保存在EthStatus变量中。ETH_BSP_Config函数一般在main函数中优先LwIP_Init函数调用。

代码清单:ETH-6 ETH_CheckLinkStatus函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void ETH_CheckLinkStatus(uint16_t PHYAddress)
{
    static uint8_t status = 0;
    uint32_t t = GET_PHY_LINK_STATUS();

    /* If we have link and previous check was not yet */
    if (t && !status) {
        /* Set link up */
        netif_set_link_up(&gnetif);

        status = 1;
    }
    /* If we don't have link and it was on previous check */
    if (!t && status) {
        EthLinkStatus = 1;
        /* Set link down */
        netif_set_link_down(&gnetif);

        status = 0;
    }
}

ETH_CheckLinkStatus函数用于获取PHY状态,实际上也是通过宏定义GET_PHY_LINK_STATUS()获取得到的,函数还根据PHY状态通知LwIP当前链路状态, gnetif是一个netif结构体类型变量,LwIP定义了netif结构体类型,用于指示某一网卡相关信息,LwIP是支持多个网卡设备, 使用时需要为每个网卡设备定义一个netif类型变量。无操作系统时ETH_CheckLinkStatus函数被无限循环调用。

代码清单:ETH-7 ETH_link_callback函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
void ETH_link_callback(struct netif *netif)
{
    __IO uint32_t timeout = 0;
    uint32_t tmpreg;
    uint16_t RegValue;
    struct ip_addr ipaddr;
    struct ip_addr netmask;
    struct ip_addr gw;

    if (netif_is_link_up(netif)) {
        /* Restart the auto-negotiation */
        if (ETH_InitStructure.ETH_AutoNegotiation !=
                                ETH_AutoNegotiation_Disable) {
            /* Reset Timeout counter */
            timeout = 0;
            /* Enable auto-negotiation */
            ETH_WritePHYRegister(ETHERNET_PHY_ADDRESS, PHY_BCR,
                                        PHY_AutoNegotiation);
            /* Wait until the auto-negotiation will be completed */
            do {
                timeout++;
            } while (!(ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_BSR)
            &PHY_AutoNego_Complete)&&(timeout<(uint32_t)PHY_READ_TO));

            /* Reset Timeout counter */
            timeout = 0;
            /* Read the result of the auto-negotiation */
            RegValue = ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_SR);

            if ((RegValue & PHY_DUPLEX_STATUS) != (uint16_t)RESET) {
                ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;
            } else {
                ETH_InitStructure.ETH_Mode = ETH_Mode_HalfDuplex;
            }
            if (RegValue & PHY_SPEED_STATUS) {
    /* Set Ethernet speed to 10M following the auto-negotiation */
                ETH_InitStructure.ETH_Speed = ETH_Speed_10M;
            } else {
    /* Set Ethernet speed to 100M following the auto-negotiation */
                ETH_InitStructure.ETH_Speed = ETH_Speed_100M;
            }

            /*------------ ETHERNET MACCR Re-Configuration -------------

            /* Get the ETHERNET MACCR value */
            tmpreg = ETH->MACCR;

            /* Set the FES bit according to ETH_Speed value */
            /* Set the DM bit according to ETH_Mode value */
            tmpreg |= (uint32_t)(ETH_InitStructure.ETH_Speed |
                                    ETH_InitStructure.ETH_Mode);

            /* Write to ETHERNET MACCR */
            ETH->MACCR = (uint32_t)tmpreg;

            _eth_delay_(ETH_REG_WRITE_DELAY);
            tmpreg = ETH->MACCR;
            ETH->MACCR = tmpreg;
        }

        /* Restart MAC interface */
        ETH_Start();

#ifdef USE_DHCP
        ipaddr.addr = 0;
        netmask.addr = 0;
        gw.addr = 0;
        DHCP_state = DHCP_START;
#else
        IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
        IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,
                                    NETMASK_ADDR2, NETMASK_ADDR3);
        IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
#endif /* USE_DHCP */

        netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);

/* When the netif is fully configured this function must be called.*/
        netif_set_up(&gnetif);

        EthLinkStatus = 0;
    } else {
        ETH_Stop();
#ifdef USE_DHCP
        DHCP_state = DHCP_LINK_DOWN;
        dhcp_stop(netif);
#endif /* USE_DHCP */

        /*  When the netif link is down this function must be called.*/
        netif_set_down(&gnetif);
    }
}

ETH_link_callback函数被LwIP调用,当链路状态发送改变时该函数就被调用,用于状态改变后处理相关事务。首先调用netif_is_link_up函数判断新状态是否是链路启动状态, 如果是启动状态就进入if语句,接下来会判断ETH是否被设置为自适应模式,如果不是自适应模式需要使用ETH_WritePHYRegister函数使能PHY工作为自适应模式, 然后ETH_ReadPHYRegister函数读取PHY相关寄存器,获取PHY当前支持的以太网速度和工作模式,并保存到ETH_InitStructure结构体变量中。 ETH_Start函数用于使能ETH外设,之后就是配置ETH的IP地址、 子网掩码、网关,如果是定义了DHCP (动态主机配置协议)功能则启动DHCP。最后就是调用netif_set_up函数在LwIP层次配置启动ETH功能。

如果检测到是链路关闭状态,调用ETH_Stop函数关闭ETH,如果定义了DHCP功能则需关闭DHCP,最后调用netif_set_down函数在LwIP层次关闭ETH功能。

以上对文件修改部分更多涉及到ETH硬件底层驱动,一些是PHY芯片驱动函数、一些是ETH外设与LwIP连接函数。接下来要讲解的文件代码更多是与LwIP应用相关的。

netconf.c和netconf.h文件用于存放LwIP配置相关代码。netcon.h定义了相关宏。

代码清单:ETH-8 LwIP配置相关宏定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/* DHCP状态 */
#define DHCP_START                   1
#define DHCP_WAIT_ADDRESS          2
#define DHCP_ADDRESS_ASSIGNED      3
#define DHCP_TIMEOUT                4
#define DHCP_LINK_DOWN              5

//#define USE_DHCP       /* enable DHCP, if disabled static address is d */

/* 调试信息输出 */
#define SERIAL_DEBUG
/* 远端IP地址和端口 */
#define DEST_IP_ADDR0               192
#define DEST_IP_ADDR1               168
#define DEST_IP_ADDR2                 1
#define DEST_IP_ADDR3               105
#define DEST_PORT                  6000

/* MAC地址:网卡地址 */
#define MAC_ADDR0                     2
#define MAC_ADDR1                     0
#define MAC_ADDR2                     0
#define MAC_ADDR3                     0
#define MAC_ADDR4                     0
#define MAC_ADDR5                     0

/*静态IP地址 */
#define IP_ADDR0                    192
#define IP_ADDR1                    168
#define IP_ADDR2                      1
#define IP_ADDR3                    122

/* 子网掩码 */
#define NETMASK_ADDR0               255
#define NETMASK_ADDR1               255
#define NETMASK_ADDR2               255
#define NETMASK_ADDR3                 0

/* 网关 */
#define GW_ADDR0                    192
#define GW_ADDR1                    168
#define GW_ADDR2                      1
#define GW_ADDR3                      1

/* 检测PHY链路状态的实际间隔(单位:ms) */
#ifndef LINK_TIMER_INTERVAL
#define LINK_TIMER_INTERVAL        1000
#endif

/* MII and RMII mode selection ***********/
#define RMII_MODE
//#define MII_MODE

/* 在MII模式时,使能MCO引脚输出25MHz脉冲 */
#ifdef  MII_MODE
#define PHY_CLOCK_MCO
#endif

USE_DHCP宏用于定义是否使用DHCP功能,如果不定义该宏,直接使用静态的IP地址,如果定义该宏,则使用DHCP功能,获取动态的IP地址, 这里有个需要注意的地方,电脑是没办法提供DHCP服务功能的,路由器才有DHCP服务功能,使用当开发板直连电脑时不能定义该宏。

SERIAL_DEBUG宏是定义是否使能串口定义相关调试信息功能,一般选择使能,所以在main函数中需要添加串口初始化函数。

接下来,定义了远端IP和端口、MAC地址、静态IP地址、子网掩码、网关相关宏,可以根据实际情况修改。

LAN8720A仅支持RMII接口,根据硬件设计这里定义使用RMII_MODE。

代码清单:ETH-9 LwIP_Init函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void LwIP_Init(void)
{
    struct ip_addr ipaddr;
    struct ip_addr netmask;
    struct ip_addr gw;

    /* Initializes the dynamic memory heap defined by MEM_SIZE.*/
    mem_init();
    /* Initializes the memory pools defined by MEMP_NUM_x.*/
    memp_init();

#ifdef USE_DHCP
    ipaddr.addr = 0;
    netmask.addr = 0;
    gw.addr = 0;
#else
    IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
    IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,
            NETMASK_ADDR2,NETMASK_ADDR3);
    IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
#endif
    /* 添加以太网设备 */
    netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL,
            &ethernetif_init, &ethernet_input);

    /*  设置以太网设备为默认网卡 */
    netif_set_default(&gnetif);

    if (EthStatus == (ETH_INIT_FLAG | ETH_LINK_FLAG)) {
        gnetif.flags |= NETIF_FLAG_LINK_UP;
        /* 配置完成网卡后启动网卡*/
        netif_set_up(&gnetif);
#ifdef USE_DHCP
        DHCP_state = DHCP_START;
#else
#ifdef SERIAL_DEBUG
        printf("\n  Static IP address   \n");
        printf("IP: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);
        printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
                                        NETMASK_ADDR2,NETMASK_ADDR3);
printf("Gateway:%d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,GW_ADDR2,GW_ADDR3);
#endif /* SERIAL_DEBUG */
#endif /* USE_DHCP */
    } else {
        /*  当网络链路关闭时关闭网卡设备 */
        netif_set_down(&gnetif);
#ifdef USE_DHCP
        DHCP_state = DHCP_LINK_DOWN;
#endif /* USE_DHCP */
#ifdef SERIAL_DEBUG
        printf("\n  Network Cable is  \n");
        printf("    not connected   \n");
#endif /* SERIAL_DEBUG */
    }
    /* 设置链路回调函数,用于获取链路状态 */
    netif_set_link_callback(&gnetif, ETH_link_callback);
}

LwIP_Init函数用于初始化LwIP协议栈,一般在main函数中调用。首先是内存相关初始化,mem_init函数是动态内存堆初始化,memp_init函数是存储池初始化, LwIP是实现内存的高效利用,内部需要不同形式的内存管理模式。

接下来为ipaddr、netmask和gw结构体变量赋值,设置本地IP地址、子网掩码和网关,如果使用DHCP功能直接赋值为0即可。netif_add是以太网设备添加函数, 即向LwIP协议栈申请添加一个网卡设备,函数有7个形参,第一个为netif结构体类型变量指针,这里赋值为gnetif地址,该网卡设备属性就存放在gnetif变量中; 第二个为ip_addr结构体类型变量指针,用于设置网卡IP地址;第三个ip_addr结构体类型变量指针,用于设置子网掩码;第四个为ip_addr结构体类型变量指针, 用于设置网关;第五个为void变量,用户自定义字段,一般不用直接赋值NULL;第六个为netif_init_fn类型函数指针,用于指向网卡设备初始化函数, 这里赋值为指向ethernetif_init函数,该函数在ethernetif.c文件定义,初始化LwIP与ETH外设连接函数;最后一个参数为netif_input_fn类型函数指针, 用于指向以太网帧接收函数,这里赋值为指向ethernet_input函数,该函数定义在etharp.c文件中。

netif_set_default函数用于设置指定网卡为默认的网络通信设备。

在无硬件连接错误时,调用ETH_BSP_Config(优先LwIP_Init函数被调用)时会将EthStatus变量对应的ETH_LINK_FLAG位使能, 所以在LwIP_INIT函数中会执行if判断语句代码,置位网卡设备标志位以及运行netif_set_up函数启动网卡设备。否则执行netif_set_down函数停止网卡设备。

最后,根据需要调用netif_set_link_callback函数实在当链路状态发生改变时需要调用的回调函数配置。

代码清单:ETH-10 LwIP_Pkt_Handle函数
1
2
3
4
5
void LwIP_Pkt_Handle(void)
{
    /* 从以太网存储器读取一个以太网帧并将其发送给LwIP */
    ethernetif_input(&gnetif);
}

LwIP_Pkt_Handle函数用于从以太网存储器读取一个以太网帧并将其发送给LwIP,它在接收到以太网帧时被调用, 它是直接调用ethernetif_input函数实现的,该函数定义在ethernetif.c文件中。

代码清单:ETH-11 LwIP_Periodic_Handle函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void LwIP_Periodic_Handle(__IO uint32_t localtime)
{
#if LWIP_TCP
    /* TCP periodic process every 250 ms */
    if (localtime - TCPTimer >= TCP_TMR_INTERVAL) {
        TCPTimer =  localtime;
        tcp_tmr();
    }
#endif

    /* ARP periodic process every 5s */
    if ((localtime - ARPTimer) >= ARP_TMR_INTERVAL) {
        ARPTimer =  localtime;
        etharp_tmr();
    }

    /* Check link status periodically */
    if ((localtime - LinkTimer) >= LINK_TIMER_INTERVAL) {
        ETH_CheckLinkStatus(ETHERNET_PHY_ADDRESS);
        LinkTimer=localtime;
    }

#ifdef USE_DHCP
    /* Fine DHCP periodic process every 500ms */
    if (localtime - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS) {
        DHCPfineTimer =  localtime;
        dhcp_fine_tmr();
        if ((DHCP_state != DHCP_ADDRESS_ASSIGNED) &&
            (DHCP_state != DHCP_TIMEOUT) &&
            (DHCP_state != DHCP_LINK_DOWN)) {
#ifdef SERIAL_DEBUG
            LED1_TOGGLE;
            printf("\nFine DHCP periodic process every 500ms\n");
#endif /* SERIAL_DEBUG */

            /* process DHCP state machine */
            LwIP_DHCP_Process_Handle();
        }
    }

    /* DHCP Coarse periodic process every 60s */
    if (localtime - DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS) {
        DHCPcoarseTimer =  localtime;
        dhcp_coarse_tmr();
    }

#endif
}

LwIP_Periodic_Handle函数是一个必须被无限循环调用的LwIP支持函数,一般在main函数的无限循环中调用,主要功能是为LwIP各个模块提供时间并查询链路状态, 该函数有一个形参,用于指示当前时间,单位为ms。

对于TCP功能,每250ms执行一次tcp_tmr函数;对于ARP(地址解析协议),每5s执行一次etharp_tmr函数;对于链路状态检测, 每1s执行一次ETH_CheckLinkStatus函数;对于DHCP功能,每500ms执行一次dhcp_fine_tmr函数, 如果DHCP处于DHCP_START或DHCP_WAIT_ADDRESS状态就执行LwIP_DHCP_Process_Handle函数,对于DHCP功能,还有每60s执行一次dhcp_coarse_tmr函数。

代码清单:ETH-12 LwIP_DHCP_Process_Handle函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void LwIP_DHCP_Process_Handle(void)
{
    struct ip_addr ipaddr;
    struct ip_addr netmask;
    struct ip_addr gw;

    switch (DHCP_state) {
    case DHCP_START: {
        DHCP_state = DHCP_WAIT_ADDRESS;
        dhcp_start(&gnetif);
        /* IP address should be set to 0
        every time we want to assign a new DHCP address */
        IPaddress = 0;
#ifdef SERIAL_DEBUG
        printf("\n     Looking for    \n");
        printf("     DHCP server    \n");
        printf("     please wait... \n");
#endif /* SERIAL_DEBUG */
    }
    break;

    case DHCP_WAIT_ADDRESS: {
        /* Read the new IP address */
        IPaddress = gnetif.ip_addr.addr;

        if (IPaddress!=0) {
            DHCP_state = DHCP_ADDRESS_ASSIGNED;
            /* Stop DHCP */
            dhcp_stop(&gnetif);
#ifdef SERIAL_DEBUG
            printf("\n  IP address assigned \n");
            printf("    by a DHCP server   \n");
            printf("IP: %d.%d.%d.%d\n",(uint8_t)(IPaddress),
                (uint8_t)(IPaddress >> 8),(uint8_t)(IPaddress >> 16),
                            (uint8_t)(IPaddress >> 24));
            printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
                                    NETMASK_ADDR2,NETMASK_ADDR3);
            printf("Gateway: %d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,
                                                GW_ADDR2,GW_ADDR3);
            LED1_ON;
#endif /* SERIAL_DEBUG */
        } else {
            /* DHCP timeout */
            if (gnetif.dhcp->tries > MAX_DHCP_TRIES) {
                DHCP_state = DHCP_TIMEOUT;
                /* Stop DHCP */
                dhcp_stop(&gnetif);
                /* Static address used */
        IP4_ADDR(&ipaddr, IP_ADDR0 ,IP_ADDR1 , IP_ADDR2 , IP_ADDR3 );
                IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1,
                                    NETMASK_ADDR2, NETMASK_ADDR3);
                IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
                netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);
#ifdef SERIAL_DEBUG
                printf("\n    DHCP timeout    \n");
                printf("  Static IP address   \n");
                printf("IP: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,
                                                IP_ADDR2,IP_ADDR3);
    printf("NETMASK: %d.%d.%d.%d\n",NETMASK_ADDR0,NETMASK_ADDR1,
                                        NETMASK_ADDR2,NETMASK_ADDR3);
                printf("Gateway: %d.%d.%d.%d\n",GW_ADDR0,GW_ADDR1,
                                                    GW_ADDR2,GW_ADDR3);
                LED1_ON;
#endif /* SERIAL_DEBUG */
            }
        }
    }
    break;
    default:
        break;
    }
}

LwIP_DHCP_Process_Handle函数用于执行DHCP功能,当DHCP状态为DHCP_START时,执行dhcp_start函数启动DHCP功能,LwIP会向DHCP服务器申请分配IP请求, 并进入等待分配状态。当DHCP状态为DHCP_WAIT_ADDRESS时,先判断IP地址是否为0,如果不为0说明已经有IP地址,DHCP功能已经完成可以停止它; 如果IP地址总是为0,就需要判断是否超过最大等待时间,并提示出错。

lwipopts.h文件存放一些宏定义,用于剪切LwIP功能,比如有无操作系统、内存空间分配、存储池分配、TCP功能、DHCP功能、UDP功能选择等等。这里使用与ST官方例程相同配置即可。

LwIP为使用者提供了两种应用程序接口(API函数)来实现TCP/IP协议栈,一种是低水平、基于回调函数的API,称为RAW API,另外一种是高水平、连续的API, 称为sequential API,sequentialAPI又有两种函数结构,一种是Netconn,一种是Socket,它与在电脑端使用的BSD标准的Socket API结构和原理是非常相似的。

接下来内容我们使用RAW API实现一个简单的TCP通信测试,ST官方有提供相关的例程,我们对其内容稍作调整。代码内容存放在tcp_echoclient.c文件中。 TCP在各个层次处理过程见图 TCP处理过程

TCP处理过程

网络接口层的netif->output和netif->input是在ethernetif.c文件中实现的,网络层和传输层有LwIP协议栈实现,应用层代码就是用户使用LwIP函数实现网络功能。

代码清单:ETH-13 tcp_echoclient_connect函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void tcp_echoclient_connect(void)
{
    struct ip_addr DestIPaddr;

    /* create new tcp pcb */
    echoclient_pcb = tcp_new();

    if (echoclient_pcb != NULL) {
        IP4_ADDR( &DestIPaddr, DEST_IP_ADDR0, DEST_IP_ADDR1,
                DEST_IP_ADDR2, DEST_IP_ADDR3 );

        /* connect to destination address/port */
        tcp_connect(echoclient_pcb,&DestIPaddr,
                    DEST_PORT,tcp_echoclient_connected);
    } else {
        /* deallocate the pcb */
        memp_free(MEMP_TCP_PCB, echoclient_pcb);
#ifdef SERIAL_DEBUG
        printf("\n\r can not create tcp pcb");
#endif
    }
}

tcp_echoclient_connect函数用于创建TCP从设备并启动与TCP服务器连接。tcp_new函数创建一个新TCP协议控制块,主要是必要的内存申请, 返回一个未初始化的TCP协议控制块指针。如果返回值不了0就可以使用tcp_connect函数连接到TCP服务器,tcp_connect函数用于TCP从设备连接至指定IP地址和端口的TCP服务器, 它有四个形参,第一个为TCP协议控制块指针,第二个为服务器IP地址,第三个为服务器端口,第四个为函数指针,当连接正常建立时或连接错误时函数被调用, 这里赋值tcp_echoclient_connected函数名。如果tcp_new返回值为0说明创建TCP协议控制块失败,调用memp_free函数释放相关内容。

代码清单:ETH-14 tcp_echoclient_disconnect函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct echoclient {
    enum echoclient_states state; /* connection status */
    struct tcp_pcb *pcb;          /* pointer on the current tcp_pcb */
    struct pbuf *p_tx;            /* pointer on pbuf to be transmitted */
};

void tcp_echoclient_disconnect(void)
{
    /* close connection */
    tcp_echoclient_connection_close(echoclient_pcb,echoclient_es);
#ifdef SERIAL_DEBUG
    printf("\n\r close TCP connection");
#endif
}

echoclient是自定义的一个结构体类型,包含了TCP从设备的状态、TCP协议控制块指针和发送数据指针。tcp_echoclient_disconnect函数用于断开TCP连接, 通过调用tcp_echoclient_connection_close函数实现,它有两个形参,一个是TCP协议控制块,一个是echoclient类型指针。

代码清单:ETH-15 tcp_echoclient_connected函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static err_t tcp_echoclient_connected(void *arg, struct tcp_pcb *tpcb,
                                                            err_t err)
{
    struct echoclient *es = NULL;

    if (err == ERR_OK) {
        /* allocate structure es to maintain tcp connection informations

        es = (struct echoclient *)mem_malloc(sizeof(struct echoclient));
        echoclient_es=es;
        if (es != NULL) {
            es->state = ES_CONNECTED;
            es->pcb = tpcb;
            sprintf((char*)data, "sending tcp client message %d",
                                                message_count);
            /* allocate pbuf */
            es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data),
                                                            PBUF_POOL);
            if (es->p_tx) {
                /* copy data to pbuf */
                pbuf_take(es->p_tx, (char*)data, strlen((char*)data));
            /* pass newly allocated es structure as argument to tpcb */
                tcp_arg(tpcb, es);
                /* initialize LwIP tcp_recv callback function */
                tcp_recv(tpcb, tcp_echoclient_recv);
                /* initialize LwIP tcp_sent callback function */
                tcp_sent(tpcb, tcp_echoclient_sent);
                /* initialize LwIP tcp_poll callback function */
                tcp_poll(tpcb, tcp_echoclient_poll, 1);
                /* send data */
                tcp_echoclient_send(tpcb,es);
                return ERR_OK;
            }
        } else {
            /* close connection */
            tcp_echoclient_connection_close(tpcb, es);
            /* return memory allocation error */
            return ERR_MEM;
        }
    } else {
        /* close connection */
        tcp_echoclient_connection_close(tpcb, es);
    }
    return err;
}

tcp_echoclient_connected函数作为tcp_connect函数设置的回调函数,在TCP建立连接时被调用,这里实现的功能是向TCP服务器发送一段数据。 使用mem_malloc函数申请内存空间存放echoclient结构体类型数据,并赋值给es指针变量。如果内存申请失败调用tcp_echoclient_connection_close函数关闭TCP连接; 确保内存申请成功后为es成员赋值,p_tx成员是发送数据指针,这里使用pbuf_alloc函数向内存池申请存放发送数据的存储空间,即数据发送缓冲区。 确保发送数据存储空间申请成功后使用pbuf_take函数将待发送数据data拷贝到数据发送存储器。tcp_arg函数用于设置用户自定义参数, 使得该参数可在相关回调函数被重新使用。tcp_recv、tcp_sent和tcp_poll函数分别设置TCP协议控制块对应的接收、发送和轮询回调函数。最后调用tcp_echoclient_send函数发送数据。

代码清单:ETH-16 tcp_echoclient_recv函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static err_t tcp_echoclient_recv(void *arg, struct tcp_pcb *tpcb,
                                            struct pbuf *p, err_t err)
{
    char *recdata=0;
    struct echoclient *es;
    err_t ret_err;

    LWIP_ASSERT("arg != NULL",arg != NULL);
    es = (struct echoclient *)arg;
    /* if we receive an empty tcp frame from server => close connection

    if (p == NULL) {
        /* remote host closed connection */
        es->state = ES_CLOSING;
        if (es->p_tx == NULL) {
            /* we're done sending, close connection */
            tcp_echoclient_connection_close(tpcb, es);
        } else {
            /* send remaining data*/
            tcp_echoclient_send(tpcb, es);
        }
        ret_err = ERR_OK;
    }
    /* else : a non empty frame was received from echo server
    but for some reason err != ERR_OK */
    else if (err != ERR_OK) {
        /* free received pbuf*/
        pbuf_free(p);
        ret_err = err;
    } else if (es->state == ES_CONNECTED) {
        /* increment message count */
        message_count++;
        /* Acknowledge data reception */
        tcp_recved(tpcb, p->tot_len);
#ifdef SERIAL_DEBUG
        recdata=(char *)malloc(p->len*sizeof(char));
        if (recdata!=NULL) {
            memcpy(recdata,p->payload,p->len);
            printf("upd_rec<<%s",recdata);
        }
        free(recdata);
#endif
        /* free received pbuf*/
        pbuf_free(p);
        ret_err = ERR_OK;
    }
    /* data received when connection already closed */
    else {
        /* Acknowledge data reception */
        tcp_recved(tpcb, p->tot_len);

        /* free pbuf and do nothing */
        pbuf_free(p);
        ret_err = ERR_OK;
    }
    return ret_err;
}

tcp_echoclient_recv函数是TCP接收回调函数,TCP从设备接收到数据时该函数就被运行一次,我们可以提取数据帧内容。函数先检测是否为空帧, 如果为空帧则关闭TCP连接,然后检测是否发生传输错误,如果发送错误执行pbuf_free函数释放内存。检查无错误就可以调用tcp_recved函数接收数据, 这样就可以提取接收到信息。最后调用pbuf_free函数释放相关内存。

代码清单:ETH-17 tcp_echoclient_send函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void tcp_echoclient_send(struct tcp_pcb *tpcb, struct echoclient * es)
{
    struct pbuf *ptr;
    err_t wr_err = ERR_OK;

    while ((wr_err == ERR_OK) &&
        (es->p_tx != NULL) &&
        (es->p_tx->len <= tcp_sndbuf(tpcb))) {

        /* get pointer on pbuf from es structure */
        ptr = es->p_tx;

        /* enqueue data for transmission */
        wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);

        if (wr_err == ERR_OK) {
            /* continue with next pbuf in chain (if any) */
            es->p_tx = ptr->next;

            if (es->p_tx != NULL) {
                /* increment reference count for es->p */
                pbuf_ref(es->p_tx);
            }

            /* free pbuf: will free pbufs up to es->p
            (because es->p has a reference count > 0) */
            pbuf_free(ptr);
        } else if (wr_err == ERR_MEM) {
            /* we are low on memory, try later, defer to poll */
            es->p_tx = ptr;
        } else {
            /* other problem ?? */
        }
    }
}

tcp_echoclient_send函数用于TCP数据发送,它有两个形参,一个是TCP协议控制块结构体指针,一个是echoclient结构体指针。 在判断待发送数据存在并不超过最大可用发送队列数据数后,执行tcp_write函数将待发送数据写入发送队列,由协议内核决定发送时机。

代码清单:ETH-18 tcp_echoclient_poll函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static err_t tcp_echoclient_poll(void *arg, struct tcp_pcb *tpcb)
{
    err_t ret_err;
    struct echoclient *es;

    es = (struct echoclient*)arg;
    if (es != NULL) {
        if (es->p_tx != NULL) {
            /* there is a remaining pbuf (chain) , try to send data */
            tcp_echoclient_send(tpcb, es);
        } else {
            /* no remaining pbuf (chain)  */
            if (es->state == ES_CLOSING) {
                /* close tcp connection */
                tcp_echoclient_connection_close(tpcb, es);
            }
        }
        ret_err = ERR_OK;
    } else {
        /* nothing to be done */
        tcp_abort(tpcb);
        ret_err = ERR_ABRT;
    }
    return ret_err;
}

tcp_echoclient_poll函数是由tcp_poll函数指定的回调函数,它每500ms执行一次,函数检测是否有待发送数据,如果有就执行tcp_echoclient_send函数发送数据。

代码清单:ETH-19 tcp_echoclient_sent函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static err_t tcp_echoclient_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
    struct echoclient *es;

    LWIP_UNUSED_ARG(len);

    es = (struct echoclient *)arg;

    if (es->p_tx != NULL) {
        /* still got pbufs to send */
        tcp_echoclient_send(tpcb, es);
    }

    return ERR_OK;
}

tcp_echoclient_sent函数是有tcp_sent函数指定的回调函数,当接收到远端设备发送应答信号时被调用,它实际是通过调用tcp_echoclient_send函数发送数据实现的。

代码清单:ETH-20 tcp_echoclient_connection_close函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static void tcp_echoclient_connection_close(struct tcp_pcb *tpcb,
                                            struct echoclient * es )
{
    /* remove callbacks */
    tcp_recv(tpcb, NULL);
    tcp_sent(tpcb, NULL);
    tcp_poll(tpcb, NULL,0);

    if (es != NULL) {
        mem_free(es);
    }
    /* close tcp connection */
    tcp_close(tpcb);
}

tcp_echoclient_connection_close函数用于关闭TCP连接,将相关的回调函数解除,释放es变量内存,最后调用tcp_close函数关闭TCP连接,释放TCP协议控制块内存。

代码清单:ETH-21 定时器初始化配置及中断服务函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 初始化配置TIM3,使能每10ms发生一次中断 */
static void TIM3_Config(uint16_t period,uint16_t prescaler)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);  ///使能TIM3时钟

    TIM_TimeBaseInitStructure.TIM_Prescaler=prescaler;  //定时器分频
    TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period=period;   //自动重装载值
    TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;

    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);

    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
    TIM_Cmd(TIM3,ENABLE); //使能定时器3

    NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03;
    NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/* TIM3中断服务函数 */
void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) { //溢出中断
        LocalTime+=10;//10ms增量
    }
    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);  //清除中断标志位
}

LwIP_Periodic_Handle函数执行LwIP需要周期性执行函数,该所以我们需要为该函数提高一个时间基准,这里使用TIM3产生这个基准, 初始化配置TIM3每10ms中断一次,在其中断服务函数中递增LocalTime变量值。

代码清单:ETH-22 main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int main(void)
{
    uint8_t flag=0;
    /* 初始化LED */
    LED_GPIO_Config();

    /* 初始化按键 */
    Key_GPIO_Config();

    /* 初始化调试串口,一般为串口1 */
    Debug_USART_Config();

    /* 初始化系统滴答定时器 */
    SysTick_Init();

    TIM3_Config(999,899);//10ms定时器
    printf("以太网通信实现例程\n");

    /* Configure ethernet (GPIOs, clocks, MAC, DMA) */
    ETH_BSP_Config();
    printf("PHY初始化结束\n");

    /* Initilaize the LwIP stack */
    LwIP_Init();

    printf("    KEY1: 启动TCP连接\n");
    printf("    KEY2: 断开TCP连接\n");

    /* IP地址和端口可在netconf.h文件修改,或者使用DHCP服务自动获取IP
    (需要路由器支持)*/
    printf("本地IP和端口: %d.%d.%d.%d\n",IP_ADDR0,IP_ADDR1,
        IP_ADDR2,IP_ADDR3);
    printf("远端IP和端口: %d.%d.%d.%d:%d\n",DEST_IP_ADDR0, DEST_IP_ADDR1,
        DEST_IP_ADDR2, DEST_IP_ADDR3,DEST_PORT);

    while (1) {
        if ((Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)==KEY_ON) && (flag==0)) {
            LED2_ON;
            if (EthLinkStatus == 0) {
                printf("connect to tcp server\n");
                /*connect to tcp server */
                tcp_echoclient_connect();
                flag=1;
            }
        }
        if ((Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) && flag) {
            LED2_OFF;
            tcp_echoclient_disconnect();
            flag=0;
        }
        /* check if any packet received */
        if (ETH_CheckFrameReceived()) {
            /* process received ethernet packet */
            LwIP_Pkt_Handle();
        }
        /* handle periodic timers for LwIP */
        LwIP_Periodic_Handle(LocalTime);
    }
}

首先是初始化LED指示灯、按键、调试串口、系统滴答定时器,TIM3_Config函数配置10ms定时并启动定时器,ETH_BSP_Config函数初始化ETH相关GPIO、 配置MAC和DMA并获取PHY状态,LwIP_Init函数初始化LwIP协议栈。进入无限循环函数,不断检测按键状态,如果KEY1被按下则调用tcp_echoclient_connect函数启动TCP连接, 如果KEY2被按下则调用tcp_echoclient_disconnect关闭TCP连接。ETH_CheckFrameReceived函数用于检测是否接收到数据帧, 如果接收到数据帧则调用LwIP_Pkt_Handle函数将数据帧从缓冲区传入LwIP。LwIP_Periodic_Handle函执行必须被周期调用的函数。

下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手并配置好相关参数; 使用网线连接开发板网口跟路由器,这里要求电脑连接在同一个路由器上,之所以使用路由器是这样连接方便,电脑端无需更多操作步骤, 并且路由器可以提供DHCP服务器功能,而电脑是不行的,最后在电脑端打开网络调试助手软件,并设置相关参数, 见图 调试助手设置界面 ,调试助手的设置与netconf.h文件中相关宏定义是对应的, 不同电脑设置情况可能不同。把编译好的程序下载到开发板。

调试助手设置界面

在系统硬件初始化时串口调试助手会打印相关提示信息,等待初始化完成后可打开电脑端CMD窗口,输入ping命令测试开发板链路, 图 ping窗口 为链路正常情况,如果出现ping不同情况,检查网线连接。

ping窗口

ping状态正常后,可按下开发板KEY1按键,使能开发板连接电脑端的TCP服务器,之后就可以进行数据传输, 需要接收传输时可以按下开发板KEY2按键,实际操作调试助手界面见图 调试助手接发通信效果

调试助手接发通信效果

42.10. 基于uCOS-III移植LwIP实验

上面的实验是无操作系统移植LwIP,LwIP也确实是支持无操作系统移植运行,这对于芯片资源紧张、不合适运行操作系统环境还是有很大用处的。 不过在很多应用中会采用操作系统上运行LwIP,这有利于提高整体性能。这个实验我们主要讲解移植操作步骤,过程中直接使用上个实验LwIP底层驱动, 除非有需要修改地方才指出,同时这里假设已有移植好的uCOS-III工程可参考使用,关于uCOS-III移植部分可参考我们相关文档, 这里主要介绍LwIP使用uCOS-III信号量、消息队列、定时器函数等等函数接口。

这个实验最终实现在uCOS-III操作系统基础上移植LwIP,使能DHCP功能,在动态获取IP之后即可ping通。运行uCOS-III操作系统之后一般会使用Netconn或Socket方法使用LwIP, 关于这两个的应用方法限于篇幅问题这里不做深入探究。

UCOS-III和LwIP都是属于软件编程层次,所以硬件设计部分并不需要做更改,直接使用上个实验的硬件设计即可。

接下来开始介绍移植步骤,为简化移植步骤,我们的思路是直接使用uCOS-III例程,在其基础上移植LwIP部分。

第一步:文件拷贝

拷贝整个uCOS-III工程,修改文件夹名称为“ETH—基于uCOS-III的LwIP移植”,作为我们这个实验工程基础,我们在此基础上添加功能。拷贝上个实验工程中的lwip-1.4.1整个文件夹到USER文件夹(路径:…\ETH—基于uCOS-III的LwIP移植\USER)中。

LwIP源码部分,即src文件夹,内容是不用修改的,只有port文件夹内容需要修改。在stsw-stm32070文件夹找到FreeRTOS文件夹(路径:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS),该文件夹内容是LwIP与FreeRTOS操作系统连接的相关接口函数,虽然我们选择使用uCOS- III操作系统,当还是有很多可以借鉴的地方,移植过程我们采用修改这些文件方法实现而不是完全自己新建文件,把FreeRTOS整个文件夹拷贝到port文件夹(路径:…\ETH—基于uCOS- III的LwIP移植\USER\lwip-1.4.1\port)内,并改名为UCOS305,此时port文件夹内有三个文件夹,分别为:arch、Standalone、UCOS305,其中Standalone在本实验是不被使用的。

把上个实验工程中的App文件夹拷贝到本实验相同位置,其中tcp_echoclient.c和tcp_echoclient.h文件不是本实验需要的,将其删除。netconf.c、netconf.h和lwipopts.h三个文件是必需的,但因为如果在本实验直接使用lwipopts.h文件需要修改较多地方, 我们先将该文件删除,然后在stsw-stm32070文件夹找到httpserver_socket文件夹。(路径:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS\httpserver_socket),在该文件夹下inc文件夹中的lwipopts.h文件是更方便我们移植的文件,我们拷贝它到App文件夹中。

最后,把上个实验工程中的ETH文件夹拷贝到本实验相同位置,这个文件夹内容都是必需的,但我们不用进行修改。

第二步:为工程添加文件

与上个工程相比,LwIP部分文件只有port文件夹文件有所修改,其他使用与上个实验相同文件结构皆可,最终工程文件结构参考图 工程文件结构

工程文件结构

添加完源文件后,还需要在工程选项中设置添加头文件路径,参考图 添加头文件路径

添加头文件路径

第三步:文件修改

ETH文件夹内文件,stm32f429_eth.c、stm32f429_eth.h、stm32f429_phy.c和stm32f429_phy.h四个文件是ETH外部和PHY相关驱动, 本实验并无需修改硬件,所以这四个文件内容不用修改,stm32f429_eth_conf.h文件是与ETH外设相关硬件宏定义, 因为本实验使用操作系统,对延时函数定义与上个实验工程有所不同,需要稍作修改。

代码清单:ETH-23 延时函数定义
1
2
3
4
5
6
#ifdef USE_Delay
#include "Bsp/bsp.h"
#define _eth_delay_    Delay_10ms
#else
#define _eth_delay_
#endif

这里使用在bsp.h文件中定义的Delay_10ms延时函数。

sys_arch.h和sys_arch.c两个文件是LwIP与uCOS-III连接的实现代码。sys_arch.h存放相关宏定义和类型定义。

代码清单:ETH-24 宏定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#define LWIP_STK_SIZE         512
#define LWIP_TASK_MAX         8

#define LWIP_TSK_PRIO         3
#define LWIP_TASK_START_PRIO  LWIP_TSK_PRIO
#define LWIP_TASK_END_PRIO    LWIP_TSK_PRIO +LWIP_TASK_MAX

#define MAX_QUEUES            10  // 消息邮箱的数量
#define MAX_QUEUE_ENTRIES     20  // 每个邮箱的大小

#define SYS_MBOX_NULL         (void *)0
#define SYS_SEM_NULL          (void *)0

#define sys_arch_mbox_tryfetch(mbox,msg)   sys_arch_mbox_fetch(mbox,msg,1)

宏LWIP_STK_SIZE定义LwIP任务栈空间大小,实际空间是4*LWIP_STK_SIZE个字节。宏LWIP_TASK_MAX定义预留给LwIP使用的最大任务数量。 LWIP_TSK_PRIO、LWIP_TASK_START_PRIO和LWIP_TASK_END_PRIO三个宏指定LwIP任务的优先级范围。宏MAX_QUEUES定义LwIP可以使用的最大邮箱数量, 宏MAX_QUEUE_ENTRIES定义每个邮箱的大小。宏SYS_MBOX_NULL和SYS_SEM_NULL分别定义邮箱和信号量NULL对于的值。 sys_arch_mbox_tryfetch函数是尝试获取邮箱内容,这里直接调用sys_arch_mbox_fetch函数实现。

代码清单:ETH-25 类型定义
1
2
3
4
5
6
typedef OS_SEM     sys_sem_t; // type of semiphores
typedef OS_MUTEX   sys_mutex_t; // type of mutex
typedef OS_Q       sys_mbox_t; // type of mailboxes
typedef CPU_INT08U sys_thread_t; // type of id of the new thread

typedef CPU_INT08U sys_prot_t;

不同操作系统有不同名称定义信号量、复合信号、邮箱、任务ID等等,这里使用uCOS-III操作系统需要使用对应的名称。

实际上,除了需要定于与操作系统对应的名称之外,还需要定于与编译器相关的名称,在sys_arch.h文件中有引用了cc.h头文件, 因为我们使用Windows操作系统的Keil开发工具,需要对cc.h文件进行必须修改。

代码清单:ETH-26 编译器相关类型定于和宏定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
typedef u32_t mem_ptr_t;
//typedef int sys_prot_t;


//#define U16_F "hu"
//#define S16_F "d"
//#define X16_F "hx"
//#define U32_F "u"
//#define S32_F "d"
//#define X32_F "x"
//#define SZT_F "uz"

#define U16_F "4d"
#define S16_F "4d"
#define X16_F "4x"
#define U32_F "8ld"
#define S32_F "8ld"
#define X32_F "8lx"

sys_prot_t类型已在sys_arch.h文件中定于,在cc.h文件必须注释掉不被使用。U16_F、S16_F、X16_F等等一系列名称用于LwIP的调试函数,这一系列宏定于用于调试信息输出格式化。

代码清单:ETH-27 调试信息输出定于
1
2
3
4
5
6
7
8
#define LWIP_PLATFORM_DIAG(x)  {printf x;}

#define LWIP_PLATFORM_ASSERT(x) do { printf("Assertion \"%s\" failed at  \
    line %d in %s\n",x, __LINE__, __FILE__);} while(0)

#define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \
printf("Assertion \"%s\" failed at line %d in %s\n", message, \
__LINE__, __FILE__); fflush(NULL);handler;} } while(0)

LwIP实现代码已经添加了调试信息功能,我们只需要定于信息输出途径即可,这里直接使用printf函数,将调试信息打印到串口调试助手。

sys_arch.c文件存放uCOS-III与LwIP连接函数,LwIP为实现在操作系统上运行,预留了相关接口函数,不同操作系统使用不同方法实现要求的功能。该文件存放在UCOS305文件夹内。

代码清单:ETH-28 sys_now函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
u32_t sys_now()
{
    OS_TICK os_tick_ctr;
    CPU_SR_ALLOC();

    CPU_CRITICAL_ENTER();
    os_tick_ctr = OSTickCtr;
    CPU_CRITICAL_EXIT();

    return os_tick_ctr;
}

sys_now函数用于为LwIP提供系统时钟,这里直接的读取OSTickCtr变量值。CPU_CRITICAL_ENTER和CPU_CRITICAL_EXIT分别是关闭总中断和开启总中断。

LwIP的邮箱用于缓存和传递数据包。

代码清单:ETH-29 邮箱创建与删除
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
err_t sys_mbox_new(sys_mbox_t *mbox, int size)
{
    OS_ERR       ucErr;

    OSQCreate(mbox,"LWIP quiue", size, &ucErr);
    LWIP_ASSERT( "OSQCreate ", ucErr == OS_ERR_NONE );

    if ( ucErr == OS_ERR_NONE) {
        return 0;
    }
    return -1;
}

void sys_mbox_free(sys_mbox_t *mbox)
{
    OS_ERR     ucErr;
    LWIP_ASSERT( "sys_mbox_free ", mbox != SYS_MBOX_NULL );

    OSQFlush(mbox,& ucErr);

    OSQDel(mbox, OS_OPT_DEL_ALWAYS, &ucErr);
    LWIP_ASSERT( "OSQDel ", ucErr == OS_ERR_NONE );
}

sys_mbox_new函数要求实现的功能是创建一个邮箱,这里使用OSQCreate函数创建一个队列。sys_mbox_free函数要求实现的功能是释放一个邮箱, 如果邮箱存在内容,会发生错误,这里先使用OSQFlush函数清除队列内容,然后再使用OSQDel函数删除队列。LWIP_ASSERT函数是由LwIP定义的断言,用于调试错误。

代码清单:ETH-30 邮箱发送和获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void sys_mbox_post(sys_mbox_t *mbox, void *data)
{
    OS_ERR     ucErr;
    CPU_INT08U  i=0;
    if ( data == NULL ) data = (void*)&pvNullPointer;
    /* try 10 times */
    while (i<10) {
        OSQPost(mbox, data,0,OS_OPT_POST_ALL,&ucErr);
        if (ucErr == OS_ERR_NONE)
            break;
        i++;
        OSTimeDly(5,OS_OPT_TIME_DLY,&ucErr);
    }
    LWIP_ASSERT( "sys_mbox_post error!\n", i !=10 );
}

err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
{
    OS_ERR     ucErr;
    if (msg == NULL ) msg = (void*)&pvNullPointer;
    OSQPost(mbox, msg,0,OS_OPT_POST_ALL,&ucErr);
    if (ucErr != OS_ERR_NONE) {
        return ERR_MEM;
    }
    return ERR_OK;
}

u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout)
{
    OS_ERR  ucErr;
    OS_MSG_SIZE   msg_size;
    CPU_TS        ucos_timeout;
    CPU_TS        in_timeout = timeout/LWIP_ARCH_TICK_PER_MS;
    if (timeout && in_timeout == 0)
        in_timeout = 1;
    *msg  = OSQPend (mbox,in_timeout,OS_OPT_PEND_BLOCKING,&msg_size,
                    &ucos_timeout,&ucErr);

    if ( ucErr == OS_ERR_TIMEOUT )
        ucos_timeout = SYS_ARCH_TIMEOUT;
    return ucos_timeout;
}

sys_mbox_post函数要求实现的功能是发送一个邮箱,这里主要调用OSQPost函数实现队列发送,为保证发送成功,最多尝试10次队列发送。 sys_mbox_trypost函数是尝试发送一个邮箱,这里我们直接使用OSQPost函数发送一次信号量,而不像sys_mbox_post函数在发送失败时可能尝试发送多次。 sys_arch_mbox_fetch函数用于获取邮箱内容,并指定等待超时时间,这里主要通过调用OSQPend函数实现队列获取。

代码清单:ETH-31 邮箱可用性检查和不可用设置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int sys_mbox_valid(sys_mbox_t *mbox)
{
    if (mbox->NamePtr)
        return (strcmp(mbox->NamePtr,"?Q"))? 1:0;
    else
        return 0;
}

void sys_mbox_set_invalid(sys_mbox_t *mbox)
{
    if (sys_mbox_valid(mbox))
        sys_mbox_free(mbox);
}

sys_mbox_valid函数要求实现的功能是检查指定的邮箱是否可用,对于uCOSIII,直接调用strcmp函数检查队列名称是否存在“Q”字段, 如果存在说明该邮箱可用,否则不可用。sys_mbox_set_invalid函数要求实现的功能是将指定的邮箱设置为不可用(无效), 这里先调用sys_mbox_valid函数判断邮箱是可用的,如果本身不可用就无需操作,确定邮箱可用后调用sys_mbox_free函数删除邮箱。

LwIP的信号量用于进程间的通信。

代码清单:ETH-32 新建信号量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
err_t sys_sem_new(sys_sem_t *sem, u8_t count)
{
    OS_ERR  ucErr;
    OSSemCreate (sem,"LWIP Sem",count,&ucErr);
    if (ucErr != OS_ERR_NONE ) {
        LWIP_ASSERT("OSSemCreate ",ucErr == OS_ERR_NONE );
        return -1;
    }
    return 0;
}

sys_sem_new函数要求实现的功能是新建一个信号量,这里直接调用OSSemCreate函数新建一个信号量,count参数用于指定信号量初始值。

代码清单:ETH-33 信号量相关函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)
{
    OS_ERR  ucErr;
    CPU_TS        ucos_timeout;
    CPU_TS        in_timeout = timeout/LWIP_ARCH_TICK_PER_MS;
    if (timeout && in_timeout == 0)
        in_timeout = 1;
    OSSemPend (sem,in_timeout,OS_OPT_PEND_BLOCKING,&ucos_timeout,&ucErr);
    /*  only when timeout! */
    if (ucErr == OS_ERR_TIMEOUT)
        ucos_timeout = SYS_ARCH_TIMEOUT;
    return ucos_timeout;
}

void sys_sem_signal(sys_sem_t *sem)
{
    OS_ERR  ucErr;
    OSSemPost(sem,OS_OPT_POST_ALL,&ucErr);
    LWIP_ASSERT("OSSemPost ",ucErr == OS_ERR_NONE );
}

void sys_sem_free(sys_sem_t *sem)
{
    OS_ERR     ucErr;
    OSSemDel(sem, OS_OPT_DEL_ALWAYS, &ucErr );
    LWIP_ASSERT( "OSSemDel ", ucErr == OS_ERR_NONE );
}

int sys_sem_valid(sys_sem_t *sem)
{
    if (sem->NamePtr)
        return (strcmp(sem->NamePtr,"?SEM"))? 1:0;
    else
        return 0;
}

void sys_sem_set_invalid(sys_sem_t *sem)
{
    if (sys_sem_valid(sem))
        sys_sem_free(sem);
}

sys_arch_sem_wait函数要求实现的功能是等待获取一个信号量,并具有超时等待检查功能,这里直接调用OSSemPend函数实现信号量获取。 sys_sem_signal函数要求实现的功能是发送一个信号量,这里直接调用OSSemPost函数发送一个信号量。sys_sem_free函数要求实现的功能是释放一个信号量, 这里直接调用OSSemDel函数删除信号量。sys_sem_valid函数要求实现的功能是检查指定的信号量是否可用,这里调用strcmp函数判断信号量名称中是否存在“SEM”字段, 如果存在说明是信号量,否则不是信号量。sys_sem_set_invalid函数要求实现的功能是使指定的信号量不可用, 这里先调用sys_sem_valid确保信号量可用,再调用sys_sem_free函数释放该信号量。

代码清单:ETH-34 系统初始化
1
2
3
4
5
6
7
8
9
void sys_init(void)
{
    OS_ERR ucErr;
    memset(LwIP_task_priority_stask,0,sizeof(LwIP_task_priority_stask));
    /* init mem used by sys_mbox_t, use ucosIII functions */
    OSMemCreate(&StackMem,"LWIP TASK STK",(void*)LwIP_Task_Stk,
                LWIP_TASK_MAX,LWIP_STK_SIZE*sizeof(CPU_STK),&ucErr);
    LWIP_ASSERT( "sys_init: failed OSMemCreate STK", ucErr == OS_ERR_NONE );
}

sys_init函数在系统启动时被调用,可以用于初始化工作环境,这里先调用memset函数将LwIP_task_priority_stask数组内容清空, 该数值用于存放任务优先级,接下来调用OSMemCreate函数初始化申请内存空间,用于LwIP任务栈空间。

代码清单:ETH-35 任务创建
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
sys_thread_t sys_thread_new(const char *name, lwip_thread_fn thread ,
                                void *arg, int stacksize, int prio)
{
    CPU_INT08U  ubPrio = LWIP_TASK_START_PRIO;
    OS_ERR      ucErr;
    int i;
    int tsk_prio;
    CPU_STK * task_stk;
    if (prio) {
        ubPrio +=(prio-1);
        for (i=0; i<LWIP_TASK_MAX; ++i)
            if (LwIP_task_priority_stask[i] == ubPrio)
                break;
        if (i == LWIP_TASK_MAX) {
            for (i=0; i<LWIP_TASK_MAX; ++i)
                if (LwIP_task_priority_stask[i]==0) {
                    LwIP_task_priority_stask[i] = ubPrio;
                    break;
                }
            if (i == LWIP_TASK_MAX) {
                LWIP_ASSERT("sys_thread_new: there is no space for priority",0);
                return (-1);
            }
        } else
            prio = 0;
    }
    /* Search for a suitable priority */
    if (!prio) {
        ubPrio = LWIP_TASK_START_PRIO;
        while (ubPrio < (LWIP_TASK_START_PRIO+LWIP_TASK_MAX)) {
            for (i=0; i<LWIP_TASK_MAX; ++i)
                if (LwIP_task_priority_stask[i] == ubPrio) {
                    ++ubPrio;
                    break;
                }
            if (i == LWIP_TASK_MAX)
                break;
        }
        if (ubPrio < (LWIP_TASK_START_PRIO+LWIP_TASK_MAX))
            for (i=0; i<LWIP_TASK_MAX; ++i)
                if (LwIP_task_priority_stask[i]==0) {
                    LwIP_task_priority_stask[i] = ubPrio;
                    break;
                }
        if(ubPrio>=(LWIP_TASK_START_PRIO+LWIP_TASK_MAX)||i==LWIP_TASK_MAX){
        LWIP_ASSERT( "sys_thread_new: there is no free priority", 0 );
            return (-1);
        }
    }
    if (stacksize > LWIP_STK_SIZE || !stacksize)
        stacksize = LWIP_STK_SIZE;
    /* get Stack from pool */
    task_stk = OSMemGet( &StackMem, &ucErr );
    if (ucErr != OS_ERR_NONE) {
        LWIP_ASSERT( "sys_thread_new: impossible to get a stack", 0 );
        return (-1);
    }
    tsk_prio = ubPrio-LWIP_TASK_START_PRIO;
    OSTaskCreate(&LwIP_task_TCB[tsk_prio],
                (CPU_CHAR  *)name,
                (OS_TASK_PTR)thread,
                (void      *)0,
                (OS_PRIO    )ubPrio,
                (CPU_STK   *)&task_stk[0],
                (CPU_STK_SIZE)stacksize/10,
                (CPU_STK_SIZE)stacksize,
                (OS_MSG_QTY )0,
                (OS_TICK    )0,
                (void      *)0,
            (OS_OPT     )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                (OS_ERR    *)&ucErr);

    return ubPrio;
}

sys_thread_new函数要求实现的功能是新建一个任务,函数有五个形参,分别指定任务名称、任何函数、任务自定义参数、任务栈空间大小、任务优先级。 对于LwIP,系统有限制其最多可用任务数,对于优先级也指定一定的范围,sys_thread_new函数先对优先级参数进行处理,获取合适的优先级。 OSMemGet函数用于从分配给LwIP任务栈使用的内存空间中申请一块空间用于本任务。OSTaskCreate函数用于创建一个任务。

代码清单:ETH-36 临界区域保护
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sys_prot_t sys_arch_protect(void)
{
    CPU_SR_ALLOC();

    CPU_CRITICAL_ENTER();
    return 1;
}

void sys_arch_unprotect(sys_prot_t pval)
{
    CPU_SR_ALLOC();

    LWIP_UNUSED_ARG(pval);
    CPU_CRITICAL_EXIT();
}

sys_arch_protecth函数要求实现的功能是完成临界区域保护并保存当前内容,这里调用CPU_CRITICAL_ENTER函数进入临界区域保护。 sys_arch_unprotect函数要求实现的功能是恢复受保护区域的先前状态,与sys_arch_protecth函数配套使用,这里直接调用CP U_CRITICAL_EXIT函数完成退出临界区域保护。

ethernetif.c文件存放LwIP与ETH外设连接函数(网络接口函数),属于最底层驱动函数,与上个实验无操作系统移植的文件内容有所不同,这里在函数内部会调用相关系统操作函数。

代码清单:ETH-37 low_level_init函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static void low_level_init(struct netif *netif)
{
    uint32_t i;

    /* set netif MAC hardware address length */
    netif->hwaddr_len = ETHARP_HWADDR_LEN;
    /* set netif MAC hardware address */
    netif->hwaddr[0] =  MAC_ADDR0;
    netif->hwaddr[1] =  MAC_ADDR1;
    netif->hwaddr[2] =  MAC_ADDR2;
    netif->hwaddr[3] =  MAC_ADDR3;
    netif->hwaddr[4] =  MAC_ADDR4;
    netif->hwaddr[5] =  MAC_ADDR5;
    /* set netif maximum transfer unit */
    netif->mtu = 1500;
    /* Accept broadcast address and ARP traffic */
    netif->flags=NETIF_FLAG_BROADCAST|NETIF_FLAG_ETHARP|NETIF_FLAG_LINK_UP;
    s_pxNetIf =netif;

    /* initialize MAC address in ethernet MAC */
    ETH_MACAddressConfig(ETH_MAC_Address0, netif->hwaddr);
    /* Initialize Tx Descriptors list: Chain Mode */
    ETH_DMATxDescChainInit(DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
    /* Initialize Rx Descriptors list: Chain Mode  */
    ETH_DMARxDescChainInit(DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);

    /* Enable Ethernet Rx interrrupt */
    for (i=0; i<ETH_RXBUFNB; i++) {
        ETH_DMARxDescReceiveITConfig(&DMARxDscrTab[i], ENABLE);
    }

#ifdef CHECKSUM_BY_HARDWARE
    /* Enable the checksum insertion for the Tx frames */
    {
        for (i=0; i<ETH_TXBUFNB; i++) {
            ETH_DMATxDescChecksumInsertionConfig(&DMATxDscrTab[i],
                        ETH_DMATxDesc_ChecksumTCPUDPICMPFull);
        }
    }
#endif

    /* create the task that handles the ETH_MAC */
    sys_thread_new((const char*)"Eth_if",ethernetif_input,netif,
        netifINTERFACE_TASK_STACK_SIZE,netifINTERFACE_TASK_PRIORITY);
    /* Enable MAC and DMA transmission and reception */
    ETH_Start();
}

low_level_init函数是网络接口初始化函数,在ethernetif_init函数被调用,在系统启动时被运行一次,用于初始化与网络接口相关硬件。 函数先是给netif结构体成员赋值配置网卡参数,调用ETH_MACAddressConfig函数绑定网卡MAC地址, ETH_DMATxDescChainInit和ETH_DMARxDescChainInit初始化网络数据帧发送和接收描述符,设置为链模式。 调用ETH_DMARxDescReceiveITConfig函数使能DMA数据接收相关中断。通过定义宏CHECKSUM_BY_HARDWARE,可以使能发送数据硬件校验和, 这个需要硬件支持,STM32F42x控制器是支持的。调用sys_thread_new函数创建一个任务,设置任务函数是ethernetif_input, 该函数用于讲接收到数据包转入到LwIP内部缓存区,这里还传递了netif结构体变量。最后,调用ETH_Start函数使能ETH。

low_level_output和low_level_input两个函数内容与上个实验工程同名函数几乎相同,这里不再讲解。

代码清单:ETH-38 ethernetif_input函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ethernetif_input( void * pvParameters )
{
    struct pbuf *p;
    OS_ERR os_err;
    err_t err;

    /* move received packet into a new pbuf */
    while (1) {
        SYS_ARCH_DECL_PROTECT(sr);

        SYS_ARCH_PROTECT(sr);
        p = low_level_input(s_pxNetIf);
        SYS_ARCH_UNPROTECT(sr);
        if (p == NULL)
            continue;
        err = s_pxNetIf->input(p, s_pxNetIf);
        if (err != ERR_OK) {
            LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
            pbuf_free(p);
            p = NULL;
        }
        /*sleep 5 ms*/
        OSTimeDlyHMSM(0, 0, 0, 5, OS_OPT_TIME_DLY, (OS_ERR *)&os_err);
    }
}

ethernetif_input函数作为sys_thread_new指定的任务函数,用于接收网络数据包并将其转入到LwIP内核。SYS_ARCH_DECL_PROTECT、 SYS_ARCH_PROTECT和SYS_ARCH_UNPROTECT三个函数是与临界区域保护相关代码,可用在lwipopts.h文件中的SYS_LIGHTWEIGHT_PROT配置相关功能。 调用low_level_input函数获取接收到的数据包,如果判断没有接收到数据包则不执行本来循环后面内容。在判断接收到数据包后,将数据包内容转入LwIP内核。

相对与上个实验工程,那个时候我们需要在main函数中的无限循环中调用数据包接收查询,现在在这里我们创建一个这个数据包接收任务,让它执行数据包接收并将数据转入到LwIP内核。

代码清单:ETH-39 ethernetif_init函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
err_t ethernetif_init(struct netif *netif)
{
    LWIP_ASSERT("netif != NULL", (netif != NULL));

#if LWIP_NETIF_HOSTNAME
    /* Initialize interface hostname */
    netif->hostname = "lwip";
#endif /* LWIP_NETIF_HOSTNAME */

    netif->name[0] = IFNAME0;
    netif->name[1] = IFNAME1;

    netif->output = etharp_output;
    netif->linkoutput = low_level_output;


    /* initialize the hardware */
    low_level_init(netif);

    etharp_init();
    sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);

    return ERR_OK;
}

ethernetif_init函数用于初始化网卡,在系统启动时必须被运行一次,该函数在LwIP_Init函数中被调用。函数首先为netif结构体成员赋值, 然后调用low_level_init函数完成ETH外设初始化,etharp_init函数完成ARP协议初始化,sys_timeout函数启动ARP超时并注册一个ARP超时回调函数。

netconf.c和netconf.h用于存放LwIP配置相关代码。netconf.h是相关的宏定义,具体参考 代码清单:ETH-8 , 不过本实验定义了USE_DHCP宏,使能DHCP功能。不同于上个实验,现在netconf.c文件只要求实现两个函数,LwIP_Init和LwIP_DHCP_task函数, 把其他函数删除。其中LwIP_Init函数与上个实验工程使用相同配置即可,参考 代码清单:ETH-9

代码清单:ETH-40 LwIP_DHCP_task函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#ifdef USE_DHCP
void LwIP_DHCP_task(void * pvParameters)
{
    struct ip_addr ipaddr;
    struct ip_addr netmask;
    struct ip_addr gw;
    OS_ERR  os_err;

    while (1) {
        switch (DHCP_state) {
            /*************************************************/
            /*   与上个实验工程代码相同,参考代码清单 43 12          */
            /*************************************************/
        }
        OSTimeDlyHMSM( 0u, 0u, 0u, 250u,OS_OPT_TIME_HMSM_STRICT,&os_err);
    }
}
#endif

在netconf.h文件中定义了USE_DHCP宏,即开启了DHCP功能,LwIP_DHCP_task函数才有效。在上个实验工程中, 我们使用LwIP_DHCP_Process_Handle函数(参考 代码清单:ETH-12 )完成DHCP功能实现,该函数是被周期调用执行的。 现在,既然我们使用了操作系统,就可以直接创建一个任务执行DHCP功能,LwIP_DHCP_task函数就是DHCP任务函数, 函数需要实现的内容与 代码清单:ETH-12 相同,在函数最后调用OSTimeDlyHMSM函数延时250ms。

lwipopts.h文件存放一些宏定义,用于剪切LwIP功能,该文件拷贝自ST官方带操作系统的工程文件,方便我们移植, 该文件同时使能了Netconn和Socket编程支持。这里我们还需要对该文件一个宏定义进行修改,直接把TCPIP_THREAD_PRIO宏定义为6,该宏定义了TCPIP任务的优先级。

至此,有关LwIP函数文件修改已经全部完成,接下来还需要实现就是调用相关初始化函数完成LwIP初始化,然后就可以直接使用LwIP函数完成用户任务。

首先是ETH外设硬件相关初始化函数调用,bsp.c文件中BSP_Init函数用于放置系统启动时各模块硬件初始化函数。

代码清单:ETH-41 BSP_Init函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void  BSP_Init (void)
{
    /* 设置NVIC优先级分组为Group2:0-3抢占式优先级,0-3的响应式优先级 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    /* 初始化LED */
    LED_GPIO_Config();

    Key_GPIO_Config();
    /* 初始化调试串口,一般为串口1 */
    Debug_USART_Config();

    printf("基于uCOS-III的LwIP网络通信测试\n");

    BSP_Tick_Init();

    /* Configure ethernet (GPIOs, clocks, MAC, DMA) */
    ETH_BSP_Config();
    printf("LAN8720A BSP INIT AND COMFIGURE SUCCESS\n");
}

在BSP_Init函数最后调用ETH_BSP_Config函数完成ETH外设相关硬件初始化,包含了RMII和SMI相关GPIO初始化,ETH外设时钟使能、MAC和DMA配置并获取PHY的状态。

代码清单:ETH-42 开始任务函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static  void  AppTaskStart (void *p_arg)
{
    OS_ERR      err;
    (void)p_arg;

    BSP_Init();                    /* Initialize BSP functions*/
    CPU_Init();                  /* Initialize the uC/CPU services*/

#if OS_CFG_STAT_TASK_EN > 0u
    OSStatTaskCPUUsageInit(&err); /*Compute CPU capacity with no task running*/
#endif

#ifdef CPU_CFG_INT_DIS_MEAS_EN
    CPU_IntDisMeasMaxCurReset();
#endif

#if (APP_CFG_SERIAL_EN == DEF_ENABLED)
    APP_TRACE_DBG(("Creating Application kernel objects\n\r"));
#endif
    AppObjCreate();
    /* Create Applicaiton kernel objects                    */
#if (APP_CFG_SERIAL_EN == DEF_ENABLED)
    APP_TRACE_DBG(("Creating Application Tasks\n\r"));
#endif
    AppTaskCreate();                /* Create Application tasks*/

    /* Initilaize the LwIP stack */
    LwIP_Init();

#ifdef USE_DHCP
    /* Start DHCPClient */
    OSTaskCreate(&AppTaskDHCPTCB,"DHCP",
                LwIP_DHCP_task,
                &gnetif,
                APP_CFG_TASK_DHCP_PRIO,
                &AppTaskDHCPStk[0],
                AppTaskDHCPStk[APP_CFG_TASK_DHCP_STK_SIZE / 10u],
                APP_CFG_TASK_DHCP_STK_SIZE,
                0u,
                0u,
                0,
                (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                &err);
#endif  //#ifdef USE_DHCP

    while (DEF_TRUE) {
        OSTimeDlyHMSM(0u, 0u, 1u, 0u,
                    OS_OPT_TIME_HMSM_STRICT,
                    &err);
    }
}

AppTaskStart函数是系统运行的启动任务函数,先执行BSP_Init函数完成各个模块硬件初始化,接下来几个函数用于uCOS- III初始化,接下来调用LwIP_Init函数完成LwIP协议栈初始化。如果使能了DHCP功能,就创建DHCP任务,指定LwIP_DHCP_task函数为DHCP任务函数。

这个实验只是简单实现LwIP在uCOS-III操作系统基础上移植,并没有过多实现应用层方面代码,最后通过开发板是否ping通检验。

下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手并配置好相关参数; 使用网线连接开发板网口跟路由器,这里要求电脑连接在同一个路由器上,这里要求使用路由器,可以提供DHCP服务器功能,而电脑不行的。 编译工程文件下载到开发板上。在串口调试助手可以看到相关信息,参考图 串口调试助手窗口 ,可以看到在使能DHCP功能之后, 开发板动态获取IP地址为:192.168.1.124,这与我们在netconf.h文件中设置的静态地址是不同的(当然,也有可能刚好相同)。

串口调试助手窗口

串口调试助手显示如图 串口调试助手窗口 信息是代码移植成功的最基本保证。如果没有代码没有移植成功或者网线没有接好, 是无法通过DHCP获取动态IP的。保证移植成功之后,为进一步验证程序,我们可以在电脑端ping开发板网络。打开电脑端的DOC命令输入窗口, 输入“ping 192.168.1.124”,就可以测试网络链路,链路正常时DOC窗口截图如图 ping命令窗口

ping命令窗口