TCP/IP五层混合模型

数据链路层又被分为LLC层(逻辑链路层)和MAC层(媒体介质访问层)。
对于接入网络终端的设备, LLC层和MAC层是软、硬件的分界线。如PC的网卡主要负责实现参考模型中的MAC子层和物理层,在PC的软件系统中则有一套庞大程序实现了LLC层及以上的所有网络层次的协议。
由硬件实现的物理层和MAC子层在不同的网络形式有很大的区别,如以太网和Wi-Fi,这是由物理传输方式决定的。但由软件实现的其它网络层次通常不会有太大区别。
以太网外设(ETH)
STM32F4xx系列控制器内部集成了一个以太网外设,它的功能就是实现MAC层的任务。借助以太网外设,STM32F4xx控制器可以通过ETH外设按照IEEE 802.3-2002标准发送和接收MAC数据包。ETH内部自带专用的DMA控制器用于MAC,ETH支持MII或RMII与PHY芯片传输数据包。ETH还集成了站管理接口(SMI)与外部PHY通信,用于访问PHY芯片寄存器。
物理层定义了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,PHY芯片是物理层功能实现的实体,生活中常用水晶头网线+水晶头插座+PHY组合构成了物理层。

ETH有专用的DMA控制器,它通过AHB与内核/存储器相连,AHB主接口用于控制数据传输,而AHB从接口用于访问“控制与状态寄存器”(CSR)空间。
在进行数据发送时,先将数据由存储器以DMA传输到发送TX FIFO进行缓冲,然后由MAC内核发送;接收数据时,RX FIFO先接收以太网数据帧,再由DMA传输至存储器。
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位可选择时钟频率。
SMI帧格式
SMI是通过数据帧方式与PHY通信的,帧格式如下,数据位传输顺序从左到右。

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

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

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

-
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。
ETH相关硬件在STM32F4xx控制器分布参考表

PPS_OUT是IEEE 1588定义的一个时钟同步机制。
MAC数据包发送和接收
ETH外设负责MAC数据包发送和接收。
利用DMA从系统寄存器得到数据包数据内容,ETH外设自动填充完成MAC数据包封装,然后通过PHY发送出去。
在检测到有MAC数据包需要接收时,ETH外设控制数据接收,并解封MAC数据包得到解封后数据通过DMA传输到系统寄存器内。
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接收到的数据包填充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过滤功能可以选择性的过滤设定目标地址或源地址的MAC帧。它将检查所有接收到的数据帧的目标地址和源地址,根据过滤选择设定情况,检测后报告过滤状态。针对目标地址过滤可以有三种,分别是单播、多播和广播目标地址过滤;针对源地址过滤就只有单播源地址过滤。
单播目标地址过滤是将接收的相应DA字段与预设的以太网MAC地址寄存器内容比较,最高可预设4个过滤MAC地址。多播目标地址过滤是根据帧过滤寄存器中的HM位执行对多播地址的过滤,是对MAC地址寄存器进行比较来实现的。单播和多播目标地址过滤都还支持Hash过滤模式。广播目标地址过滤通过将帧过滤寄存器的BFD位置1使能,这使得MAC丢弃所有广播帧。
单播源地址过滤是将接收的SA字段与SA寄存器内容进行比较过滤。
MAC过滤还具备反向过滤操作功能,即让过滤结构求补集。
PHY:LAN8720A
LAN8720A是SMSC公司(已被Microchip公司收购)设计的一个体积小、功耗低、全能型10/100Mbps的以太网物理层收发器。它是针对消费类电子和企业应用而设计的。LAN8720A总共只有24Pin,仅支持RMII接口。

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

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时钟输出,这样可以直接与STM32F4xx的REF_CLK引脚连接为其提供50MHz时钟源,这种模式要求为XTAL1与XTAL2之间或为XTAL1/CLKIN提供25MHz时钟,由LAN8720A内部PLL电路倍频得到50MHz时钟,此时nIN/REFCLKO引脚的中断功能不可用,用于50MHz时钟输出。当nINTSEL引脚为高电平时,LAN8720A被设置为时钟输入,即外部时钟源直接提供50MHz时钟接入STM32F4xx的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个。
| 序号 | 寄存器名称 | 分组 |
|---|---|---|
| 0 | Basic Control Register | Basic |
| 1 | Basic Status Register | Basic |
| 2 | PHY Identifier 1 | Extended |
| 3 | PHY Identifier 2 | Extended |
| 4 | Auto-Negotiation Advertisement Register | Extended |
| 5 | Auto-Negotiation Link Partner Ability Register | Extended |
| 6 | Auto-Negotiation Expansion Register | Extended |
| 17 | Mode Control/Status Register | Vendor-specific |
| 18 | Special Modes | Vendor-specific |
| 26 | Symbol Error Counter Register | Vendor-specific |
| 27 | Control / Status Indication Register | Vendor-specific |
| 29 | Interrupt Source Register | Vendor-specific |
| 30 | Interrupt Mask Register | Vendor-specific |
| 31 | PHY Special Control/Status Register | Vendor-specific |
序号与SMI数据帧中的RADDR是对应的,这在编写驱动时非常重要,本文将它们标记为R0~R31。
寄存器可规划为三个组:Basic、Extended和Vendor-specific。Basic是IEEE 802.3要求的,R0是基本控制寄存器,其位15为Soft Reset位,向该位写1启动LAN8720A软件复位,还包括速度、自适应、低功耗等等功能设置。R1是基本状态寄存器。Extended是扩展寄存器,包括LAN8720A的ID号、制造商、版本号等等信息。Vendor-specific是供应商自定义寄存器,R31是特殊控制/状态寄存器,指示速度类型和自适应功能。
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)。为减少移植工作量,我们选择使用ST官方例程相关文件,特别是ETH底层驱动部分函数,这样我们也可以花更多精力在理解代码实现方法上。
ETH初始化结构体详解
STM32 HAL库为ETH外设提供了专用的初始化结构体,通过配置结构体成员,由HAL库函数自动设置ETH相关寄存器,简化外设配置流程。相关结构体定义在stm32f4xx_hal_eth.h和stm32f4xx_hal_eth.c中。
ETH_InitTypeDef(ETH核心初始化结构体)
typedef struct{
uint32_t AutoNegotiation; // 自适应功能
uint32_t Speed; // 以太网速度
uint32_t DuplexMode; // 以太网工作模式选择
uint16_t PhyAddress; // 以太网PHY地址
uint8_t * MACAddr; // MAC地址指针
uint32_t RxMode; // 以太网接收模式
uint32_t ChecksumMode; // 检查校验和模式
uint32_t MediaInterface; // 以太网介质接口
}ETH_InitTypeDef;
-
AutoNegotiation:使能/禁止自适应,一般使能,自动匹配10/100Mbps速率和半/全双工模式;
-
Speed:以太网速度选择,可选10Mbps或100Mbit/s,一般设置100Mbit/s。配置ETH_MACCR寄存器FES位。自适应使能后该配置无效。
-
DuplexMode:以太网工作模式选择,可选全双工模式或半双工模式,一般选择全双工模式。配置ETH_MACCR寄存器DM位。自适应使能后该配置无效。
-
PhyAddress:以太网PHY地址,取值范围为0~32。该字段指示正在访问 32 个可能的 PHY 器件中的哪一个。LAN8720A默认0。
-
MACAddr:6字节MAC地址数组的指针,需自定义唯一地址;
-
RxMode:以太网接收接收模式,可以是轮询模式或者中断模式。
-
ChecksumMode:检查校验和模式,可以是硬件校验或者软件校验。
-
MediaInterface:MII/RMII接口,LAN8720A选择RMII。
ETH_MACInitTypeDef(MAC层初始化结构体)
typedef struct{
uint32_t Watchdog; // 以太网看门狗
uint32_t Jabber; // jabber定时器功能
uint32_t InterFrameGap; // 发送帧间间隙
uint32_t CarrierSense; // 载波侦听
uint32_t ReceiveOwn; // 接收自身
uint32_t LoopbackMode; // 回送模式
uint32_t ChecksumOffload; // 校验和减荷
uint32_t RetryTransmission; // 传输重试
uint32_t AutomaticPadCRCStrip; // 自动去除PAD和FCS字段
uint32_t BackOffLimit; // 后退限制
uint32_t DeferralCheck; // 检查延迟
uint32_t ReceiveAll; // 接收所有MAC帧
uint32_t SourceAddrFilter; // 源地址过滤
uint32_t PassControlFrames; // 传送控制帧
uint32_t BroadcastFramesReception;// 广播帧接收
uint32_t DestinationAddrFilter; // 目标地址过滤
uint32_t PromiscuousMode; // 混合模式
uint32_t MulticastFramesFilter; // 多播源地址过滤
uint32_t UnicastFramesFilter; // 单播源地址过滤
uint32_t HashTableHigh; // 散列表高位
uint32_t HashTableLow; // 散列表低位
uint32_t PauseTime; // 暂停时间
uint32_t ZeroQuantaPause; // 零时间片暂停
uint32_t PauseLowThreshold; // 暂停阈值下限
uint32_t UnicastPauseFrameDetect; // 单播暂停帧检测
uint32_t ReceiveFlowControl; // 接收流控制
uint32_t TransmitFlowControl; // 发送流控制
uint32_t VLANTagComparison; // VLAN标记比较
uint32_t VLANTagIdentifier; // VLAN标记标识符
}ETH_MACInitTypeDef;
- Watchdog:以太网看门狗功能选择,可选使能或禁止,它设定以太网MAC配置寄存器(ETH_MACCR)的WD位的值。 如果设置为1,使能看门狗,在接收MAC帧超过2048字节时自动切断后面数据,一般选择使能看门狗。 如果设置为0,禁用看门狗,最长可接收16384字节的帧。
- Jabber:jabber定时器功能选择,可选使能或禁止,与看门狗功能类似,只是看门狗用于接收MAC帧, jabber定时器用于发送MAC帧,它设定ETH_MACCR寄存器的JD位的值。如果设置为1,使能jabber定时器, 在发送MAC帧超过2048字节时自动切断后面数据,一般选择使能jabber定时器。
- InterFrameGap:控制发送帧间的最小间隙,可选96bit时间、88bit时间、…、40bit时间, 他设定ETH_MACCR寄存器的IFG[2:0]位的值,一般设置96bit时间。
- CarrierSense:载波侦听功能选择,可选使能或禁止,它设定ETH_MACCR寄存器的CSD位的值。 当被设置为低电平时,MAC发送器会生成载波侦听错误,一般使能载波侦听功能。
- ReceiveOwn:接收自身帧功能选择,可选使能或禁止,它设定ETH_MACCR寄存器的ROD位的值, 当设置为0时,MAC接收发送时PHY提供的所有MAC包,如果设置为1,MAC禁止在半双工模式下接收帧。一般使能接收。
- LoopbackMode:回送模式选择,可选使能或禁止,它设定ETH_MACCR寄存器的LM位的值,当设置为1时, 使能MAC在MII回送模式下工作。
- ChecksumOffload:IPv4校验和减荷功能选择,可选使能或禁止,它设定ETH_MACCR寄存器IPCO位的值, 当该位被置1时使能接收的帧有效载荷的TCP/UDP/ICMP标头的IPv4校验和检查。一般选择禁用,此时PCE和IPHCE状态位总是为0。
- RetryTransmission:传输重试功能,可选使能或禁止,它设定ETH_MACCR寄存器RD位的值, 当被设置为1时,MAC仅尝试发送一次,设置为0时,MAC会尝试根据BL的设置进行重试。一般选择使能重试。
- AutomaticPadCRCStrip:自动去除PAD和FCS字段功能,可选使能或禁用,它设定ETH_MACCR寄存器APCS位的值。 当设置为1时,MAC在长度字段值小于或等于1500自己是去除传入帧上的PAD和FCS字段。一般禁止自动去除PAD和FCS字段功能。
- BackOffLimit:后退限制,在发送冲突后重新安排发送的延迟时间,可选10、8、4、1, 它设定ETH_MACCR寄存器BL位的值。一般设置为10。
- DeferralCheck:检查延迟,可选使能或禁止,它设定ETH_MACCR寄存器DC位的值,当设置为0时, 禁止延迟检查功能,MAC发送延迟,直到CRS信号变成无效信号。
- ReceiveAll:接收所有MAC帧,可选使能或禁用,它设定以太网MAC帧过滤寄存器(ETH_MACFFR)RA位的值。 当设置为1时,MAC接收器将所有接收的帧传送到应用程序,不过滤地址。当设置为0是,MAC接收会自动过滤不与SA/DA匹配的帧。一般选择不接收所有。
- SourceAddrFilter:源地址过滤,可选源地址过滤、源地址反向过滤或禁用源地址过滤, 它设定ETH_MACFFR寄存器SAF位和SAIF位的值。一般选择禁用源地址过滤。
- PassControlFrames:传送控制帧,控制所有控制帧的转发,可选阻止所有控制帧到达应用程序、 转发所有控制帧、转发通过地址过滤的控制帧,它设定ETH_MACFFR寄存器PCF位的值。一般选择禁止转发控制帧。
- BroadcastFramesReception:广播帧接收,可选使能或禁止,它设定ETH_MACFFR寄存器BFD位的值。 当设置为0时,使能广播帧接收,一般设置接收广播帧。
- DestinationAddrFilter:目标地址过滤功能选择,可选正常过滤或目标地址反向过滤, 它设定ETH_MACFFR寄存器DAIF位的值。一般设置为正常过滤。
- PromiscuousMode:混合模式,可选使能或禁用,它设定ETH_MACFFR寄存器PM位的值。 当设置为1时,不论目标或源地址,地址过滤器都传送所有传入的帧。一般禁用混合模式。
- MulticastFramesFilter:多播源地址过滤,可选完美散列表过滤、散列表过滤、完美过滤或禁用过滤, 它设定ETH_MACFFR寄存器HPF位、PAM位和HM位的值。一般选择完美过滤。
- UnicastFramesFilter:单播源地址过滤,可选完美散列表过滤、散列表过滤或完美过滤, 它设定ETH_MACFFR寄存器HPF位和HU位的值。一般选择完美过滤。
- HashTableHigh:散列表高位,和HashTableLow组成64位散列表用于组地址过滤, 它设定以太网MAC散列表高位寄存器(ETH_MACHTHR)的值。
- HashTableLow:散列表低位,和HashTableHigh组成64位散列表用于组地址过滤, 它设定以太网MAC散列表低位寄存器(ETH_MACHTLR)的值。
- PauseTime:暂停时间,保留发送控制帧中暂停时间字段要使用的值,可设置0至65535, 它设定以太网MAC流控制寄存器(ETH_MACFCR)PT位的值。
- ZeroQuantaPause:零时间片暂停,可选使用或禁止,它设定ETH_MACFCR寄存器ZQPD位的值。 当设置为1时,当来自FIFO层的流控制信号去断言后,此位会禁止自动生成零时间片暂停控制帧。一般选择禁止。
- PauseLowThreshold:暂停阈值下限,配置暂停定时器的阈值,达到该值值时, 会自动程序传输暂停帧,可选暂停时间减去4个间隙、28个间隙、144个间隙或256个间隙, 它设定ETH_MACFCR寄存器PLT位的值。一般选择暂停时间减去4个间隙。
- UnicastPauseFrameDetect:单播暂停帧检测,可选使能或禁止,它设定ETH_MACFCR寄存器UPFD位的值。 当设置为1时,MAC除了检测具有唯一多播地址的暂停帧外,还会检测具有ETH_MACA0HR和ETH_MACA0LR寄存器所指定的站单播地址的暂停帧。一般设置为禁止。
- ReceiveFlowControl:接收流控制,可选使能或禁止,它设定ETH_MACFCR寄存器RFCE位的值。 当设定为1时,MAC对接收到的暂停帧进行解码,并禁止其在指定时间(暂停时间)内发送;当设置为0时, 将禁止暂停帧的解码功能,一般设置为禁止。
- TransmitFlowControl:发送流控制,可选使能或禁止,它设定ETH_MACFCR寄存器TFCE位的值。 在全双工模式下,当设置为1时,MAC将使能流控制操作来发送暂停帧;为0时,将禁止MAC中的流控制操作, MAC不会传送任何暂停帧。在半双工模式下,当设置为1时,MAC将使能背压操作;为0时,将禁止背压功能。
- VLANTagComparison:VLAN标记比较,可选12位或16位,它设定以太网MAC VLAN标记寄存器(ETH_MACVLANTR)VLANTC位的值。当设置为1时,使用12位VLAN标识符而不是完整的16位VLAN标记进行比较和过滤;为0时,使用全部16位进行比较,一般选择16位。
- VLANTagIdentifier:VLAN标记标识符,包含用于标识VLAN帧的802.1Q VLAN标记,并与正在接收的VLAN帧的第十五和第十六字节进行比较。位[15:13]是用户优先级,位[12]是标准格式指示符(CFI),位[11:0]是VLAN标记的VLAN标识符(VID)字段。VLANTC位置1时,仅使用VID(位[11:0])进行比较。
ETH_DMAInitTypeDef(DMA层初始化结构体)
typedef struct{
uint32_t DropTCPIPChecksumErrorFrame; //丢弃TCP/IP校验错误帧
uint32_t ReceiveStoreForward; // 接收存储并转发
uint32_t FlushReceivedFrame; // 刷新接收帧
uint32_t TransmitStoreForward; // 发送存储并转发
uint32_t TransmitThresholdControl; // 发送阈值控制
uint32_t ForwardErrorFrames; // 转发错误帧
uint32_t ForwardUndersizedGoodFrames; // 转发过小的好帧
uint32_t ReceiveThresholdControl; // 接收阈值控制
uint32_t SecondFrameOperate; // 处理第二个帧
uint32_t AddressAlignedBeats; // 地址对齐节拍
uint32_t FixedBurst; // 固定突发
uint32_t RxDMABurstLength; // DMA突发接收长度
uint32_t TxDMABurstLength; // DMA突发发送长度
uint32_t EnhancedDescriptorFormat; // 增强描述符格式
uint32_t DescriptorSkipLength; // 描述符跳过长度
uint32_t DMAArbitration; // DMA仲裁
} ETH_DMAInitTypeDef;
- DropTCPIPChecksumErrorFrame:丢弃TCP/IP校验错误帧,可选使能或禁止, 它设定以太网DMA工作模式寄存器(ETH_DMAOMR)DTCEFD位的值,当设置为 1时,如果帧中仅存在由接收校验和减荷引擎检测出来的错误,则内核不会丢弃它;为0时,如果FEF为进行了复位,则会丢弃所有错误帧。
- ReceiveStoreForward:接收存储并转发,可选使能或禁止, 它设定以太网DMA工作模式寄存器(ETH_DMAOMR)RSF位的值,当设置为1时,向RX FIFO写入完整帧后可以从中读取一帧,同时忽略接收阈值控制(RTC)位;当设置为0时,RX FIFO在直通模式下工作,取决于RTC位的阈值。一般选择使能。
- FlushReceivedFrame:刷新接收帧,可选使能或禁止,它设定ETH_DMAOMR寄存器FTF位的值, 当设置为1时,发送FIFO控制器逻辑会恢复到缺省值,TX FIFO中的所有数据均会丢失/刷新,刷新结束后改为自动清零。
- TransmitStoreForward:发送存储并并转发,可选使能或禁止, 它设定ETH_DMAOMR寄存器TSF位的值,当设置为1时,如果TX FIFO有一个完整的帧则发送会启动,会忽略TTC值;为0时,TTC值才会有效。一般选择使能。
- TransmitThresholdControl:发送阈值控制,有多个阈值可选,它设定ETH_DMAOMR寄存器TTC位的值,当TX FIFO中帧大小大于该阈值时发送会自动,对于小于阈值的全帧也会发送。
- ForwardErrorFrames:转发错误帧,可选使能或禁止,它设定ETH_DMAOMR寄存器FEF位的值, 当设置为1时,除了段错误帧之外所有帧都会转发到DMA;为0时,RX FIFO会丢弃滴啊有错误状态的帧。一般选择禁止。
- ForwardUndersizedGoodFrames:转发过小的好帧,可选使能或禁止, 它设定ETH_DMAOMR寄存器FUGF位的值,当设置为1时,RX FIFO会转发包括PAD和FCS字段的过小帧;为0时,会丢弃小于64字节的帧,除非接收阈值被设置为更低。
- ReceiveThresholdControl:接收阈值控制,当RX FIFO中的帧大小大于阈值时启动DMA传输请求,可选64字节、32字节、96字节或128字节, 它设定ETH_DMAOMR寄存器RTC位的值。
- SecondFrameOperate:处理第二个帧,可选使能或禁止,它设定ETH_DMAOMR寄存器OSF位的值, 当设置为1时会命令DMA处理第二个发送数据帧。
- AddressAlignedBeats:地址对齐节拍,可选使能或禁止,它设定以太网DMA总线模式寄存器(ETH_DMABMR)AAB位的值, 当设置为1并且固定突发位(FB)也为1时,AHB接口会生成与起始地址LS位对齐的所有突发; 如果FB位为0,则第一个突发不对齐,但后续的突发与地址对齐。一般选择使能。
- FixedBurst:固定突发,控制AHB主接口是否执行固定突发传输,可选使能或禁止, 它设定ETH_DMABMR寄存器FB位的值,当设置为1时,AHB在正常突发传输开始期间使用SINGLE、 INCR4、INCR8或INCR16;为0时,AHB使用SINGLE和INCR突发传输操作。
- RxDMABurstLength:DMA突发接收长度,有多个值可选,一般选择32Beat, 可实现32*32bits突发长度,它设定ETH_DMABMR寄存器FPM位和RDP位的值。
- TxDMABurstLength:DMA突发发送长度,有多个值可选,一般选择32Beat, 可实现32*32bits突发长度,它设定ETH_DMABMR寄存器FPM位和PBL位的值。
- EnhancedDescriptorFormat:增强描述符格式,可以使能或者禁止。该位置 1 时,使能增强描述符格式,并将描述符大小增加至 32 字节(8 个 DWORD)。如果已激活时间戳功能(ETH_PTPTSCR 位 0 TSE=1)或 IPv4 校验和减荷(ETH_MACCR 位10 IPCO=1),则必须使用此增强描述符。
- DescriptorSkipLength:描述符跳过长度,指定两个未链接描述符之间跳过的字数, 地址从当前描述符结束处开始跳到下一个描述符起始处,可选0~7,它设定ETH_DMABMR寄存器DSL位的值。
- 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位给出。
以太网通信实验:无操作系统LwIP移植
硬件设计

-
nINTSEL引脚下拉:配置nINT/REFCLKO为50MHz时钟输出,为STM32提供REF_CLK;
-
REGOFF引脚下拉:启用LAN8720A内部+1.2V稳压器,无需外部供电;
-
XTAL1/XTAL2接入25MHz晶振:经LAN8720A内部PLL倍频为50MHz,供RMII接口使用;
-
RJ45使用HY911105A:带变压器和LED指示灯,指示链路和速度状态;
-
ETH引脚配置:严格遵循STM32F4xx的RMII复用引脚定义(PA1/PA2/PA7/PC1/PC4/PC5/PG11/PG13/PG14)。
移植步骤
移植基于lwip-1.4.1.zip和STSW-STM32070.zip两份资料,核心是拷贝源码、添加工程文件、修改硬件适配代码,实现LwIP与STM32 ETH外设的对接。

第一步:相关文件拷贝
解压lwip-1.4.1.zip和stsw-stm32070.zip两个压缩包,把整个lwip-1.4.1文件夹拷贝到USER文件夹下
在stsw-stm32070文件夹找到port文件夹(路径:… \UtilitiesThird_Partylwip-1.4.1port),把整个port文件夹拷贝lwip-1.4.1文件夹中,在port文件夹下的STM32F4x7文件中把arch和Standalone两个文件夹直接剪切到port文件夹中,即此时port文件夹有三个STM32F4x7、arch和Standalone文件夹,最后把STM32F4x7文件夹删除,最终的文件结构见 图36_1_16,arch存放与开发平台相关头文件,Standalone文件夹是无操作系统移植时ETH外设与LwIP连接的底层驱动函数。

lwip-1.4.1文件夹下的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两个文件(路径:…LibrariesSTM32F4x7_ETH_Driver),将这两个文件拷贝到ETH文件夹中,这两个文件是ETH驱动文件,类似HAL库中外设驱动代码实现文件,在移植过程中我们几乎不过文件的内容。这部分函数由port文件夹相关代码调用。另外一部分是相关GPIO初始化、ETH外设初始化、PHY状态获取等等函数的实现,在stsw-stm32070文件夹中找到stm32f4x7_eth_bsp.c、stm32f4x7_eth_bsp.h和stm32f4x7_eth_conf.h三个文件(路径:…ProjectStandalonetcp_echo_client),将这三个文件拷贝到ETH文件夹中。因为ST官方LwIP测试平台使用的PHY型号不是使用LAN8720A,所以这三个文件需要我们进行修改。
最后,是LwIP测试代码实现,为测试LwIP移植是否成功和检查LwIP功能,我们编写TCP通信实现代码,设置开发板为TCP从机,电脑端为TCP主机。在stsw-stm32070文件夹中找到netconf.c、tcp_echoclient.c、lwipopts.h、netconf.h和tcp_echoclient.h五个文件(路径:…ProjectStandalonetcp_echo_client),直接拷贝到App文件夹(自己新建)中,netconf.c文件代码实现LwIP初始化函数、周期调用函数、DHCP功能函数等等,tcp_echoclient.c文件实现TCP通信参数代码,lwipopts.h包含LwIP功能选项。
第二步:为工程添加文件
第一步已经把相关的文件拷贝到对应的文件夹中,接下来就可以把需要用到的文件添加到工程中。 图36_1_15 已经指示出来工程需要用到的*.c文件,所以最终工程文件结构见 图36_1_17,图中api、ipv4和core都包含了对应文件夹下的所有*.c文件。

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

第三步:文件修改
ethernetif.c文件是无操作系统时网络接口函数,该文件在移植时需要根据实际硬件初始化网络相关IO口,以及需要指定的SRAM空间作为缓存。该文件主要有三个部分函数,HAL_ETH_MspInit函数用于初始化系统硬件接口; low_level_init函数用于初始化MAC相关工作环境、初始化DMA描述符链表,并使能MAC和DMA; low_level_output函数是最底层发送一帧数据函数; low_level_input函数是最底层接收一帧数据函数。sys_now函数获取当前时间的一个函数;ethernetif_init函数初始化网络接口结构 (netif)并调用 low_level_init 以初始化以太网外设;ethernet_input函数调用 low_level_input 接收包,然后将其提供给 LwIP 栈。
app_ethernet.c文件主要是实际的网络初始化应用程序,这里包含两个函数,Netif_Config函数是创建一个网络接口;User_notification函数是指示当前网络连接的状态。
LAN8720A.h和LAN8720A.c两个文件是ETH外设相关的底层配置,主要是 GPIO初始化即相关时钟使能。
void ETH_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能端口时钟 */
ETH_MDIO_GPIO_CLK_ENABLE();
ETH_MDC_GPIO_CLK_ENABLE();
ETH_RMII_REF_CLK_GPIO_CLK_ENABLE();
ETH_RMII_CRS_DV_GPIO_CLK_ENABLE();
ETH_RMII_RXD0_GPIO_CLK_ENABLE();
ETH_RMII_RXD1_GPIO_CLK_ENABLE();
ETH_RMII_TX_EN_GPIO_CLK_ENABLE();
ETH_RMII_TXD0_GPIO_CLK_ENABLE();
ETH_RMII_TXD1_GPIO_CLK_ENABLE();
/* 配置以太网引脚*/
/*
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_MDIO引脚 */
GPIO_InitStructure.Pin = ETH_MDIO_PIN;
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Alternate = ETH_MDIO_AF;
HAL_GPIO_Init(ETH_MDIO_PORT, &GPIO_InitStructure);
/* 配置ETH_MDC引脚 */
GPIO_InitStructure.Pin = ETH_MDC_PIN;
GPIO_InitStructure.Alternate = ETH_MDC_AF;
HAL_GPIO_Init(ETH_MDC_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_REF_CLK引脚 */
GPIO_InitStructure.Pin = ETH_RMII_REF_CLK_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_REF_CLK_AF;
HAL_GPIO_Init(ETH_RMII_REF_CLK_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_CRS_DV引脚 */
GPIO_InitStructure.Pin = ETH_RMII_CRS_DV_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_CRS_DV_AF;
HAL_GPIO_Init(ETH_RMII_CRS_DV_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_RXD0引脚 */
GPIO_InitStructure.Pin = ETH_RMII_RXD0_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_RXD0_AF;
HAL_GPIO_Init(ETH_RMII_RXD0_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_RXD1引脚 */
GPIO_InitStructure.Pin = ETH_RMII_RXD1_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_RXD1_AF;
HAL_GPIO_Init(ETH_RMII_RXD1_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_TX_EN引脚 */
GPIO_InitStructure.Pin = ETH_RMII_TX_EN_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_TX_EN_AF;
HAL_GPIO_Init(ETH_RMII_TX_EN_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_TXD0引脚 */
GPIO_InitStructure.Pin = ETH_RMII_TXD0_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_TXD0_AF;
HAL_GPIO_Init(ETH_RMII_TXD0_PORT, &GPIO_InitStructure);
/* 配置ETH_RMII_TXD1引脚 */
GPIO_InitStructure.Pin = ETH_RMII_TXD1_PIN;
GPIO_InitStructure.Alternate = ETH_RMII_TXD1_AF;
HAL_GPIO_Init(ETH_RMII_TXD1_PORT, &GPIO_InitStructure);
}
HAL_ETH_MspInit函数调用ETH_GPIO_Config进行硬件初始化,并使能以太网时钟。
/**
* @brief 以太网硬件底层驱动
* @param heth: 以太网句柄
* @retval None
*/
void HAL_ETH_MspInit(ETH_HandleTypeDef *heth)
{
ETH_GPIO_Config();
/* 使能以太网时钟 */
__HAL_RCC_ETH_CLK_ENABLE();
}
low_level_init主要是初始化硬件外设,最终被 ethernetif_init函数调用。
/**
* @brief 在这个函数中初始化硬件
* 最终被ethernetif_init函数调用
*
* @param netif已经初始化了这个以太网的lwip网络接口结构
*/
static void low_level_init(struct netif *netif)
{
uint8_t macaddress[6]= { MAC_ADDR0, MAC_ADDR1, MAC_ADDR2,
MAC_ADDR3, MAC_ADDR4, MAC_ADDR5 };
EthHandle.Instance = ETH;
EthHandle.Init.MACAddr = macaddress;
EthHandle.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE;//使能自协商模式
EthHandle.Init.Speed = ETH_SPEED_100M;//网络速率100M
EthHandle.Init.DuplexMode = ETH_MODE_FULLDUPLEX;//全双工模式
EthHandle.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;//RMII接口
EthHandle.Init.RxMode = ETH_RXPOLLING_MODE;//轮询接收模式
EthHandle.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;//硬件帧校验
EthHandle.Init.PhyAddress = LAN8720A_PHY_ADDRESS;//PHY地址
/* 配置以太网外设 (GPIOs, clocks, MAC, DMA) */
if (HAL_ETH_Init(&EthHandle) == HAL_OK) {
/* 设置netif链接标志 */
netif->flags |= NETIF_FLAG_LINK_UP;
}
/* 初始化 Tx 描述符列表:链接模式 */
HAL_ETH_DMATxDescListInit(&EthHandle, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
/* 初始化 Rx 描述符列表:链接模式 */
HAL_ETH_DMARxDescListInit(&EthHandle, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);
/* 设置netif MAC 硬件地址长度 */
netif->hwaddr_len = ETHARP_HWADDR_LEN;
/* 设置netif MAC 硬件地址 */
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;
/* 设置netif最大传输单位 */
netif->mtu = 1500;
/* 接收广播地址和ARP流量 */
netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;
/* 使能 MAC 和 DMA 发送和接收 */
HAL_ETH_Start(&EthHandle);
}
首先是ETH_HandleTypeDef结构体填充,关于结构体各个成员意义已在“ETH初始化结构体详解”作了分析。然后调用系统函数HAL_ETH_Init初始化以太网外设。初始化相关描述符的列表,设置MAC地址,使能MAC和DMA发送和接收。
Netif_Config函数一般在main函数中在LwIP_Init函数初始化完成后调用。
#define DEST_IP_ADDR0 (uint8_t)192
#define DEST_IP_ADDR1 (uint8_t)168
#define DEST_IP_ADDR2 (uint8_t)31
#define DEST_IP_ADDR3 (uint8_t)198
#define DEST_PORT (uint8_t)7
/*Static IP ADDRESS: IP_ADDR0.IP_ADDR1.IP_ADDR2.IP_ADDR3 */
#define IP_ADDR0 (uint8_t) 192
#define IP_ADDR1 (uint8_t) 168
#define IP_ADDR2 (uint8_t) 31
#define IP_ADDR3 (uint8_t) 122
/*NETMASK*/
#define NETMASK_ADDR0 (uint8_t) 255
#define NETMASK_ADDR1 (uint8_t) 255
#define NETMASK_ADDR2 (uint8_t) 255
#define NETMASK_ADDR3 (uint8_t) 0
/*Gateway Address*/
#define GW_ADDR0 (uint8_t) 192
#define GW_ADDR1 (uint8_t) 168
#define GW_ADDR2 (uint8_t) 31
#define GW_ADDR3 (uint8_t) 1
/**
* @brief 建立网络接口
* @param None
* @retval None
*/
void Netif_Config(void)
{
ip_addr_t ipaddr;
ip_addr_t netmask;
ip_addr_t gw;
IP_ADDR4(&ipaddr,IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);
IP_ADDR4(&netmask,NETMASK_ADDR0,NETMASK_ADDR1,NETMASK_ADDR2,NETMASK_ADDR3);
IP_ADDR4(&gw,GW_ADDR0,GW_ADDR1,GW_ADDR2,GW_ADDR3);
/* 添加网络接口 */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input);
/* 注册默认网络接口 */
netif_set_default(&gnetif);
if (netif_is_link_up(&gnetif)) {
/* 当netif完全配置时,必须调用此函数 */
netif_set_up(&gnetif);
} else {
/* 当netif链接断开时,必须调用此函数 */
netif_set_down(&gnetif);
}
}
通过宏定义了远端IP和端口、MAC地址、静态IP地址、子网掩码、网关相关宏,可以根据实际情况修改。netif_add函数添加网络接口;netif_set_default注册默认网络接口。
/**
* @brief 通知用户有关网络接口配置状态
* @param netif: 网络接口
* @retval None
*/
void User_notification(struct netif *netif)
{
if (netif_is_up(netif)) {
printf("Static 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);
LED_GREEN;
} else {
printf ("The network cable is not connected \n");
LED_RED;
}
}
User_notification函数在网络接口配置完成后调用,通知用户有关网络接口配置状态。打印接口连接状态,LED指示连接状态。
/**
* @ingroup lwip_nosys
* 初始化所有模块.
* 在NO_SYS模式下使用,否则使用tcpip_init()。
*/
void
lwip_init(void)
{
#ifndef LWIP_SKIP_CONST_CHECK
int a = 0;
LWIP_UNUSED_ARG(a);
LWIP_ASSERT("LWIP_CONST_CAST not implemented correctly.
Check your lwIP port.", LWIP_CONST_CAST(void*, &a) == &a);
#endif
#ifndef LWIP_SKIP_PACKING_CHECK
LWIP_ASSERT("Struct packing not implemented correctly.
Check your lwIP port.", sizeof(struct packed_struct_test) ==
PACKED_STRUCT_TEST_EXPECTED_SIZE);
#endif
/* 模块初始化 */
stats_init();
#if !NO_SYS
sys_init();
#endif /* !NO_SYS */
mem_init();
memp_init();
pbuf_init();
netif_init();
#if LWIP_IPV4
ip_init();
#if LWIP_ARP
etharp_init();
#endif /* LWIP_ARP */
#endif /* LWIP_IPV4 */
#if LWIP_RAW
raw_init();
#endif /* LWIP_RAW */
#if LWIP_UDP
udp_init();
#endif /* LWIP_UDP */
#if LWIP_TCP
tcp_init();
#endif /* LWIP_TCP */
#if LWIP_IGMP
igmp_init();
#endif /* LWIP_IGMP */
#if LWIP_DNS
dns_init();
#endif /* LWIP_DNS */
#if PPP_SUPPORT
ppp_init();
#endif
#if LWIP_TIMERS
sys_timeouts_init();
#endif /* LWIP_TIMERS */
}
lwip_Init函数用于初始化LwIP协议栈,一般在main函数中调用。首先是内存相关初始化,mem_init函数是动态内存堆初始化,memp_init函数是存储池初始化,LwIP是实现内存的高效利用,内部需要不同形式的内存管理模式。
pbuf 函数为预留的函数,目前是一个空操作。netif_init函数多播的时候用到,本例没有用到。后面的功能都是通过lwipopts.h进行裁剪。
/**
* @brief 当数据包准备好从接口读取时,应该调用此函数。
*它使用应该处理来自网络接口的字节的实际接收的函数low_level_input。
*然后确定接收到的分组的类型,并调用适当的输入功能。
*
* @param netif 以太网的lwip网络接口结构
*/
void ethernetif_input(struct netif *netif)
{
err_t err;
struct pbuf *p;
/* 将接收到的数据包移动到新的pbuf中 */
p = low_level_input(netif);
/* 没有数据包可以读取,直接返回 */
if (p == NULL) return;
/* 到LwIP堆栈入口 */
err = netif->input(p, netif);
if (err != ERR_OK) {
LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
pbuf_free(p);
p = NULL;
}
}
ethernetif_input函数用于从以太网存储器读取一个以太网帧并将其发送给LwIP,它在接收到以太网帧时被调用,它是直接调用low_level_input函数实现的,该函数定义在ethernetif.c文件中。
/**
* @ingroup lwip_nosys
* 处理NO_SYS==1超时 (即不使用tcpip_thread/sys_timeouts_mbox_fetch())
* 使用sys_now()函数,当超时到期时调用超时处理函数。
* 必须定期从主循环中调用。
*/
#if !NO_SYS && !defined __DOXYGEN__
static
#endif /* !NO_SYS */
void
sys_check_timeouts(void)
{
if (next_timeout) {
struct sys_timeo *tmptimeout;
u32_t diff;
sys_timeout_handler handler;
void *arg;
u8_t had_one;
u32_t now;
now = sys_now();
/* this cares for wraparounds */
diff = now - timeouts_last_time;
do {
PBUF_CHECK_FREE_OOSEQ();
had_one = 0;
tmptimeout = next_timeout;
if (tmptimeout && (tmptimeout->time <= diff)) {
/* timeout has expired */
had_one = 1;
timeouts_last_time += tmptimeout->time;
diff -= tmptimeout->time;
next_timeout = tmptimeout->next;
handler = tmptimeout->h;
arg = tmptimeout->arg;
#if LWIP_DEBUG_TIMERNAMES
if (handler != NULL) {
LWIP_DEBUGF(TIMERS_DEBUG, ("sct calling h=%s arg=%p\n",
tmptimeout->handler_name, arg));
}
#endif /* LWIP_DEBUG_TIMERNAMES */
memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
if (handler != NULL) {
#if !NO_SYS
/* For LWIP_TCPIP_CORE_LOCKING, lock the core before calling the
timeout handler function. */
LOCK_TCPIP_CORE();
#endif /* !NO_SYS */
handler(arg);
#if !NO_SYS
UNLOCK_TCPIP_CORE();
#endif /* !NO_SYS */
}
LWIP_TCPIP_THREAD_ALIVE();
}
/* repeat until all expired timers have been called */
} while (had_one);
}
}
sys_check_timeouts函数是一个必须被无限循环调用的LwIP支持函数,一般在main函数的无限循环中调用,使用sys_now()函数,当超时到期时调用超时处理函数。
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官方例程相同配置即可。
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* 配置系统时钟为168 MHz */
SystemClock_Config();
/* 初始化RGB彩灯 */
LED_GPIO_Config();
/* 初始化USART1 配置模式为 115200 8-N-1 */
UARTx_Config();
/* 初始化LwIP协议栈*/
lwip_init();
printf("LAN8720A Ethernet Demo\n");
printf("LwIP版本:%s\n",LWIP_VERSION_STRING);
printf("ping实验例程\n");
printf("使用同一个局域网中的电脑ping开发板的地址,可进行测试\n");
//IP地址和端口可在main.h文件修改
printf("本地IP和端口: %d.%d.%d.%d\n",
IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);
/* 网络接口配置 */
Netif_Config();
/* 报告用户网络连接状态 */
User_notification(&gnetif);
while (1) {
/* 从以太网缓冲区中读取数据包,交给LwIP 处理 */
ethernetif_input(&gnetif);
/* 处理 LwIP 超时 */
sys_check_timeouts();
}
}
首先是使能指令缓存、数据缓存,初始化系统时钟、LED指示灯、按键、调试串口,lwip_init 函数初始化LwIP协议栈。通过Netif_Config函数配置网络接口;通过User_notification函数报告用户网络连接状态。进入无限循环函数,调用ethernetif_input 函数从以太网缓存中读取数据包并交给LwIP处理;调用sys_check_timeouts 函数处理LwIP超时。这两个函数必须在大循环中调用。
下载验证
保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手并配置好相关参数;使用网线连接开发板网口跟路由器,这里要求电脑连接在同一个路由器上,之所以使用路由器是这样连接方便,电脑端无需更多操作步骤,并且路由器可以提供DHCP服务器功能,而电脑是不行的,最后在电脑端打开网络调试助手软件,并设置相关参数,见 图36_1_19,调试助手的设置与netconf.h文件中相关宏定义是对应的, 不同电脑设置情况可能不同。把编译好的程序下载到开发板。

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

ping状态正常后,可按下开发板KEY1按键,使能开发板连接电脑端的TCP服务器, 之后就可以进行数据传输,需要接收传输时可以按下开发板KEY2按键


1777

被折叠的 条评论
为什么被折叠?



