Ethernet
目录
Ethernet(以太网)
互联网模型
简介
通信至少是两个设备的事,需要相互兼容的硬件和软件支持,我们称之为通信协议。以太网通信在结构比较复杂,国际标准组织将整个以太网通信结构制定了OSI模型, 总共分层七个层,分别为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层,每个层功能不同,通信中各司其职,整个模型包括硬件和软件定义。 OSI模型是理想分层,一般的网络系统只是涉及其中几层。
TCP/IP模型
互联网模型(Internet Model)通常指TCP/IP模型,它是互联网通信的事实标准。与OSI七层模型不同,TCP/IP模型更简洁实用,分为四层,由网络层的IP协议和传输层的TCP协议组成。TCP/IP只有四个分层, 分别为应用层、传输层、网络层以及网络访问层。虽然TCP/IP分层少了,但与OSI模型是不冲突的,它把OSI模型一些层次整合一起的,本质上可以实现相同功能。

实际上,还有一个TCP/IP混合模型,分为五个层,它实际与TCP/IP四层模型是相通的,只是把网络访问层拆成数据链路层和物理层。 这种分层方法对我们学习理解更容易。
设计网络时,为了降低网络设计的复杂性,对组成网络的硬件、软件进行封装、分层,这些分层即构成了网络体系模型。在两个设备相同层之间的对话、 通信约定,构成了层级协议。设备中使用的所有协议加起来统称协议栈。在这个网络模型中,每一层完成不同的任务,都提供接口供上一层访问。 而在每层的内部,可以使用不同的方式来实现接口,因而内部的改变不会影响其它层。

应用层(Application Layer)
- 功能:提供用户接口和网络服务
- 协议示例:
- HTTP/HTTPS(网页浏览)
- FTP(文件传输)
- SMTP/POP3(电子邮件)
- DNS(域名解析)
- DHCP(动态主机配置)
传输层(Transport Layer)
- 功能:提供端到端的数据传输服务
- 主要协议:
- TCP(传输控制协议)
- 面向连接
- 可靠传输
- 流量控制
- 拥塞控制
- UDP(用户数据报协议)
- 无连接
- 不可靠但快速
- 适用于实时应用
网络层(Internet Layer)
- 功能:负责数据包的路由和转发
- 核心协议:IP协议
- IPv4:32位地址
- IPv6:128位地址
- 其他协议:
- ICMP(网络控制消息)
- ARP(地址解析)
- IGMP(组播管理)
网络接口层(Network Interface Layer)
- 功能:处理物理网络连接
- 包含内容:
- 物理层(电缆、光纤、无线)
- 数据链路层(MAC地址、帧结构)
- 技术示例:以太网、Wi-Fi、PPP
在TCP/IP混合参考模型中,数据链路层又被分为LLC层(逻辑链路层)和MAC层(媒体介质访问层)。目前,对于普通的接入网络终端的设备, LLC层和MAC层是软、硬件的分界线。如PC的网卡主要负责实现参考模型中的MAC子层和物理层,在PC的软件系统中则有一套庞大程序实现了LLC层及以上的所有网络层次的协议。
由硬件实现的物理层和MAC子层在不同的网络形式有很大的区别,如以太网和Wi-Fi,这是由物理传输方式决定的。 但由软件实现的其它网络层次通常不会有太大区别,在PC上也许能实现完整的功能,一般支持所有协议,而在嵌入式领域则按需要进行裁剪。
以太网
以太网(Ethernet)是互联网技术的一种,由于它是在组网技术中占的比例最高,很多人直接把以太网理解为互联网。
以太网是指遵守IEEE 802.3标准(互联网标准)组成的局域网,由IEEE802.3标准规定的主要是位于参考模型的物理层(PHY)和数据链路层中的介质访问控制子层(MAC)。 在家庭、企业和学校所组建的PC局域网形式一般也是以太网,其标志是使用水晶头网线来连接(当然还有其它形式)。IEEE还有其它局域网标准, 如IEEE 802.11是无线局域网,俗称Wi-Fi。IEEE802.15是个人域网,即蓝牙技术,其中的802.15.4标准则是ZigBee技术。
现阶段,工业控制、环境监测、智能家居的嵌入式设备产生了接入互联网的需求,利用以太网技术,嵌入式设备可以非常容易地接入到现有的计算机网络中。
以太网标准发展
经典以太网
- 10BASE5:粗缆以太网,500米段长
- 10BASE2:细缆以太网,185米段长
- 10BASE-T:双绞线以太网,100米段长(革命性改进)
现代以太网
- 快速以太网:100BASE-TX(100Mbps)
- 千兆以太网:1000BASE-T(1Gbps)
- 万兆以太网:10GBASE-T(10Gbps)
- 更高速率:25G、40G、100G以太网
PHY层
在物理层,由IEEE 802.3标准规定了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,物理层一般是通过一个PHY芯片实现其功能的。
传输介质
传输介质包括同轴电缆、双绞线(水晶头网线是一种双绞线)、光纤。根据不同的传输速度和距离要求, 基于这三类介质的信号线又衍生出很多不同的种类。最常用的是“五类线”适用于100BASE-T和10BASE-T的网络,它们的网络速率分别为100Mbps和10Mbps。
作用
这些特性通常通过SMI配置PHY的内部寄存器来实现:
- 速度与双工模式
- 自动协商(Auto-Negotiation):PHY默认功能。通过发送快速链路脉冲(FLP)与对端设备(如交换机)协商,自动选择双方均支持的最高性能模式(10/100Mbps,半/全双工)。
- 强制模式:可关闭自动协商,手动指定速度和双工模式。适用于固定网络环境。
- 自动翻转(Auto-MDIX)
- 功能:自动识别所用网线是直连线还是交叉线,并内部切换发送和接收线对。
- 原理:通过检测线对上的信号特性,智能地交换TX和RX通道。
- 链路状态检测
- PHY持续监测链路质量,并提供“链路建立(Link Up)”和“链路断开(Link Down)”状态给MAC/驱动。LwIP的网络接口状态依赖于此。
- 节能特性
- 如EEE(Energy Efficient Ethernet),在无数据收发时降低功耗。
编码
为了让接收方在没有外部时钟参考的情况也能确定每一位的起始、结束和中间位置,在传输信号时不直接采用二进制编码。 在10BASE-T的传输方式中采用曼彻斯特编码,在100BASE-T中则采用4B/5B编码。
曼彻斯特编码把每一个二进制位的周期分为两个间隔,在表示“1”时,以前半个周期为高电平,后半个周期为低电平。表示“0”时则相反

采用曼彻斯特码在每个位周期都有电压变化,便于同步。但这样的编码方式效率太低,只有50%。
在100BASE-T 采用的4B/5B编码是把待发送数据位流的每4位分为一组,以特定的5位编码来表示,这些特定的5位编码能使数据流有足够多的跳变, 达到同步的目的,而且效率也从曼彻斯特编码的50%提高到了80%。
CSMA/CD冲突检测
早期的以太网大多是多个节点连接到同一条网络总线上(总线型网络),存在信道竞争问题,因而每个连接到以太网上的节点都必须具备冲突检测功能。 以太网具备CSMA/CD冲突检测机制,如果多个节点同时利用同一条总线发送数据,则会产生冲突,总线上的节点可通过接收到的信号与原始发送的信号的比较检测是否存在冲突, 若存在冲突则停止发送数据,随机等待一段时间再重传。
现在大多数局域网组建的时候很少采用总线型网络,大多是一个设备接入到一个独立的路由或交换机接口,组成星型网络,不会产生冲突。但为了兼容,新出的产品还是带有冲突检测机制。
MAC层
MAC(Media Access Control,媒体访问控制)层是数据链路层的两个子层之一(另一子层是LLC)。它负责控制设备如何访问共享的传输介质,并处理物理寻址。
功能
MAC子层是属于数据链路层的下半部分,它主要负责与物理层进行数据交接,如是否可以发送数据,发送的数据是否正确, 对数据流进行控制等。它自动对来自上层的数据包加上一些控制信号,交给物理层。接收方得到正常数据时,自动去除MAC控制信号,把该数据包交给上层。
职责
- 帧封装/解封装:将网络层数据包封装成帧
- 物理寻址:使用MAC地址标识设备
- 介质访问控制:协调多设备共享信道
- 错误检测:通过FCS校验帧完整性
在OSI/TCP/IP模型中的位置
- OSI模型:数据链路层的下半部分
- TCP/IP模型:网络接口层的一部分
MAC地址
地址格式
- 48位(6字节) 全球唯一标识
- 十六进制表示:
00:1A:2B:3C:4D:5E - 前24位:OUI(组织唯一标识符),由IEEE分配
- 后24位:设备制造商分配
地址类型
| 类型 | 特征 | 示例 | 用途 |
|---|---|---|---|
| 单播地址 | 首字节最低位=0 | 00:1A:2B:xx:xx:xx |
点对点通信 |
| 组播地址 | 首字节最低位=1 | 01:00:5E:xx:xx:xx |
组播通信 |
| 广播地址 | 全1地址 | FF:FF:FF:FF:FF:FF |
广播到所有设备 |
特殊地址范围
- 本地管理地址:次低位=1(
xx:xx:xx:xx:xx:xx中第二字符为2,3,6,7,A,B,E,F) - 全局管理地址:次低位=0(由IEEE分配)
数据包

各字段说明
- 前导码:连续7字节
10101010(0x55反过来读),用于时钟同步 - SFD:1字节
10101011(0xD5反过来读),帧开始定界符 - 目的地址:接收方MAC地址
- 源地址:发送方MAC地址
- 长度/类型:
- ≤1500:长度字段(IEEE 802.3)
- ≥1536(0x0600):类型字段(Ethernet II),描述是IP包、ARP包还是SNMP包
- 数据:来自上层协议的数据单元
- FCS:32位CRC校验
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到开发板上运行。
为什么需要协议栈
物理层主要定义物理介质性质,MAC子层负责与物理层进行数据交接,这两部分是与硬件紧密联系的,就嵌入式控制芯片来说,很多都内部集成了MAC控制器, 完成MAC子层功能,所以依靠这部分功能是可以实现两个设备数据交换,而实际传输的数据就是MAC数据包,发送端封装好数据包,接收端则解封数据包得到可用数据, 这样的一个模型与使用USART控制器实现数据传输是非常类似的。但如果将以太网运用在如此基础的功能上,完全是大材小用,因为以太网具有传输速度快、 可传输距离远、支持星型拓扑设备连接等等强大功能。功能强大的东西一般都会用高级的应用,这也是设计者的初衷。
使用以太网接口的目的就是为了方便与其它设备互联,如果所有设备都约定使用一种互联方式,在软件上加一些层次来封装,这样不同系统、 不同的设备通讯就变得相对容易了。而且只要新加入的设备也使用同一种方式,就可以直接与之前存在于网络上的其它设备通讯。 这就是为什么产生了在MAC之上的其它层次的网络协议及为什么要使用协议栈的原因。又由于在各种协议栈中TCP/IP协议栈得到了最广泛使用, 所有接入互联网的设备都遵守TCP/IP协议。所以,想方便地与其它设备互联通信,需要提供对TCP/IP协议的支持。
各网络层的功能要求
用以太网和Wi-Fi作例子,它们的MAC子层和物理层有较大的区别,但在MAC之上的LLC层、网络层、传输层和应用层的协议,是基本相同的, 这几层协议由软件实现,并对各层进行封装。
这里补充之前没有的LLC层
LLC层:处理传输错误;调节数据流,协调收发数据双方速度,防止发送方发送得太快而接收方丢失数据。主要使用数据链路协议。
各网络层关系
在发送数据时,经过网络协议栈的每一层,都会给来自上层的数据添加上一个数据包的头,再传递给下一层。
在接收方收到数据时, 一层层地把所在层的数据包的头去掉,向上层递交数据。

以太网外设(ETH)
一些单片机控制器内部集成了一个以太网外设(ETH),我们以STM32F4xx系列为例,它实际是一个通过DMA控制器进行介质访问控制(MAC),它的功能就是实现MAC层的任务。 借助以太网外设,STM32F4xx控制器可以通过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系统功能框图(stm32f407)

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根通信, 在功能上是相同的。
介质独立接口 (MII) 定义了 10 Mbit/s 和 100 Mbit/s 的数据传输速率下 MAC 子层与 PHY 之间的互连。

精简介质独立接口 (RMII) 规范降低了 10/100 Mbit/s 下微控制器以太网外设与外部 PHY 间的引脚数。根据 IEEE 802.3u 标准,MII 包括 16 个数据和控制信号的引脚。RMII 规范将引脚数减少为 7 个(引脚数减少 62.5%)。 RMII 接口是 MAC 和 PHY 之间的实例化对象。这有助于将 MAC 的 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。
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 控制以太网帧的发送操作。它执行下列功能以满足 IEEE 802.3/802.3z 规范。包括:
- 生成报头和 SFD以(格式上文有说到)及发送帧状态返回给DMA
- 在半双工模式下生成阻塞信号
- 在 MII 模式下,如果在开始传输帧到 CRC 字段结束之间的任何时间发生冲突,MAC 将在MII 上发送 0x5555 5555 的 32 位阻塞信号,通知所有其它站已发生冲突。如果在报头发送阶段发生冲突,MAC 将完成报头和 SFD 的发送,然后发送阻塞信号。
- 控制 Jabber 超时(MAC看门狗),在传输字节超过2048字节时切断数据包发送
- 控制半双工模式下的流量(背压)。使用延迟机制进行流量控制, 程序通过将ETH_MACFCR寄存器的BPA位置1来请求流量控制
- 包含符合 IEEE 1588 的时间戳快照逻辑


MAC数据包接收
MAC 接收的帧将推入 Rx FIFO。此 FIFO 的状态(填充级别)一旦超过配置的接收阈值(ETH_DMAOMR 寄存器中的 RTC),就会将其指示给 DMA,这样 DMA 可向 AHB 接口发起预配置的突发传输。
在默认直通模式下,当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帧。它将检查所有接收到的数据帧的目标地址和源地址,根据过滤选择设定情况, 检测后报告过滤状态。针对目标地址过滤可以有三种,分别是单播、多播和广播目标地址过滤;针对源地址过滤就只有单播源地址过滤。
单播目标地址过滤是将接收的相应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结构

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/REFCLK引脚用于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个。

序号与SMI数据帧中的RADDR是对应的,这在编写驱动时非常重要,本文将它们标记为R0~R31。
寄存器可规划为三个组:Basic、Extended和Vendor-specific。
- Basic是IEEE 802.3要求的,R0是基本控制寄存器,其位15为SoftReset位,向该位写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实现的一些应用实例测试。
Note
在某个版本后(我没有具体看), lwip-x.x.x.zip里面就包含了contrib不过,由于LwIP不一定由有全部平台的移植,因此,我们下面基于HAL库在STM32F407ZGTb上从头移植。(可以在MX上直接勾选的LwIP)
无操作系统LwIP移植
需要移植的文件

文件树

Core/
MX没有生成ETH相关的配置文件,也就是说,ETH相关初始化(包括MAC配置、DMA配置)融合到了其他地方。不过我在其他文件里面没有找到ETH对应引脚配置,说明这部分需要我们自己写(我感觉应该是CubeMX的失误?)
Middlewares/Third_Party/LwIP/
这是未经修改的LwIP官方源码包。STM32CubeMX在创建项目时,会将这个完整的、与硬件无关的TCP/IP协议栈复制到我们的工程中。它提供所有网络协议的核心实现(IP、TCP、UDP、DHCP、DNS等)。这部分代码通常不应被用户直接修改,因为它是一个标准的、经过验证的库。修改它可能导致兼容性问题,且更新CubeMX或LwIP版本时会覆盖你的更改。
LWIP/
这个文件夹分为2部分
Target:硬件和Lwip的适配
- eth_custom_phy_interface
顾名思义,这个文件用于用户自定义phy接口,用于配置PHY。我们使用的LAN8720A没有ST官方提供的接口,所以需要自己写。不过MX还是给我们生成了模板(因为大多都是通用的)
这个模板包含:
- 基本PHY寄存器地址(USER_PHY_Registers_Mapping)
- 相应寄存器的位定义(USER_PHY_XXX_Bit_Definition)
- PHY状态(USER_PHY_Status)
- PHY连接模式(USER_PHY_LINK_MODE_Definition),包括自动协商和强制链路。
- 用于供netif注册的功能函数接口结构体(user_phy_IOCtx_t),包含:
Init; DeInit; WriteReg; ReadReg; GetTick;。使用的是函数指针typedef,能够实现适配。这四个函数执行PHY的(去)初始化、写寄存器、读寄存器、获取当前时间戳。 - phy对象结构体(user_phy_Object_t)
其中,寄存器相关设定一般遵循IEEE 802.3规定,因此一般不用修改,除非对应PHY的数据手册特别写明。其它的内容也基本不用修改。
还可以实现
-
完成一些其他用户功能。比如启用和关闭回环模式(自发自收)等等。可以由用户自己编写,这些函数的操作就是根据手册的寄存器位来配置PHY相应模式。
-
ethernetif(ethernet interface)
‘’This file provides initialization code for LWIP middleWare.‘’ MX生成了此文件模板,用来根据LwIP协议配置PHY。
模板包含以下函数
c
/**
* @brief Should be called at the beginning of the program to set up the
* network interface. It calls the function low_level_init() to do the
* actual setup of the hardware.
* This function should be passed as a parameter to netif_add().
*/
err_t ethernetif_init(struct netif *netif);
-
以太网接口初始化(ethernetif_init)。由简介可以知道,它会在添加网络接口的时候被调用,用于配置对应的物理层PHY。
实际的调用
low_level_init包含了如下步骤:配置MAC、MII、DMA;初始化ETH外设(不过HAL_ETH_MspInit需要我们重写,即上文说的引脚映射);分配发送包的内存;中间需要我们补充用户自定义PHY相关函数的注册。注意,两个结构体对象在全局变量处实例化了。
c /* USER CODE BEGIN low_level_init Code 1 for User BSP */ USER_PHY_RegisterBusIO(&USER_PHY, &USER_PHY_IOCtx); /* Initialize the USER PHY */ if(USER_PHY_Init(&USER_PHY) != USER_PHY_STATUS_OK) { netif_set_link_down(netif); netif_set_down(netif); return; } printf("PHY Initialized.\r\n"); /* USER CODE END low_level_init Code 1 for User BSP */最后是检测链路连接状态
ethernet_link_check_state,下面会讲。
c
/**
* @brief This function should do the actual transmission of the packet. The packet is
* contained in the pbuf that is passed to the function. This pbuf
* might be chained.
*/
static err_t low_level_output(struct netif *netif, struct pbuf *p);
- 发送包函数,供上层接口调用,实现物理层发送。
c
/**
* @brief Should allocate a pbuf and transfer the bytes of the incoming
* packet from the interface into the pbuf.
*/
static struct pbuf * low_level_input(struct netif *netif)
- 接收包函数,供上层接口调用,实现物理层接收。
c
/**
* @brief This function should be called when a packet is ready to be read
* from the interface. It uses the function low_level_input() that
* should handle the actual reception of bytes from the network
* interface. Then the type of the received packet is determined and
* the appropriate input function is called.
*/
void ethernetif_input(struct netif *netif);
- 由于给LwIP上层调用,函数内部调用low_level_input实现物理层接收。
c
/**
* @brief Custom Rx pbuf free callback
*/
void pbuf_free_custom(struct pbuf *p);
-
供我们用户定义的回调函数,由 LwIP 在释放
pbuf时内部调用。LwIP 协议栈中,
pbuf是用于管理网络数据包的核心数据结构。pbuf_free_custom并不是一个独立的函数,而是指具有自定义释放pbuf的回调函数。通常,释放一个
pbuf链使用函数pbuf_free()。该函数: -
递减
pbuf的引用计数。 -
当引用计数为 0 时,将
pbuf占用的内存归还给内存池(MEMP_PBUF_POOL)或堆(PBUF_RAM类型)。有时,
pbuf所承载的数据并非来自 LwIP 内部的内存池或堆,而是来自其他内存区域(例如:静态数组、DMA 缓冲区、或其他自定义分配的内存)。此时,需要一种机制,在释放pbuf时,能够以用户自定义的方式处理底层内存。
c
/**
* @brief Check the ETH link state then update ETH driver and netif link accordingly.
*/
void ethernet_link_check_state(struct netif *netif);
- 检查链路状态,包括网络链路和物理链路。这个函数MX生成为空,需要我们自己重写。(感觉也是BUG,因为它生成的函数里面没有沙箱给我们写),参考
```c
/**
* @brief Check the ETH link state then update ETH driver and netif link accordingly.
* @retval None
*/
void ethernet_link_check_state(struct netif *netif)
{
ETH_MACConfigTypeDef MACConf = {0};
int32_t PHYLinkState = 0;
uint32_t linkchanged = 0U, speed = 0U, duplex = 0U;
PHYLinkState = USER_PHY_GetLinkState(&USER_PHY);
if(netif_is_link_up(netif) && (PHYLinkState <= USER_PHY_STATUS_LINK_DOWN))
{
printf("netif_is_link_up, PHYLinkState=%ld\r\n", PHYLinkState);
HAL_ETH_Stop(&heth);
netif_set_down(netif);
netif_set_link_down(netif);
}
else if(!netif_is_link_up(netif) && (PHYLinkState > USER_PHY_STATUS_LINK_DOWN))
{
printf("netif_is_link_down, PHYLinkState=%ld\r\n", PHYLinkState);
switch (PHYLinkState)
{
case USER_PHY_STATUS_100MBITS_FULLDUPLEX:
duplex = ETH_FULLDUPLEX_MODE;
speed = ETH_SPEED_100M;
linkchanged = 1;
break;
case USER_PHY_STATUS_100MBITS_HALFDUPLEX:
duplex = ETH_HALFDUPLEX_MODE;
speed = ETH_SPEED_100M;
linkchanged = 1;
break;
case USER_PHY_STATUS_10MBITS_FULLDUPLEX:
duplex = ETH_FULLDUPLEX_MODE;
speed = ETH_SPEED_10M;
linkchanged = 1;
break;
case USER_PHY_STATUS_10MBITS_HALFDUPLEX:
duplex = ETH_HALFDUPLEX_MODE;
speed = ETH_SPEED_10M;
linkchanged = 1;
break;
default:
break;
}
if(linkchanged)
{
/* Get MAC Config MAC */
HAL_ETH_GetMACConfig(&heth, &MACConf);
MACConf.DuplexMode = duplex;
MACConf.Speed = speed;
HAL_ETH_SetMACConfig(&heth, &MACConf);
HAL_ETH_Start(&heth);
netif_set_up(netif);
netif_set_link_up(netif);
printf("linkchanged, DuplexMode:%ld, Speed:%ld \r\n", duplex, speed);
}
}
}
```
c
void Error_Handler(void);
- 经典的错误挂起函数。
c
u32_t sys_jiffies(void);
- LwIP 协议栈内部计时系统的核心函数,用于获取当前的系统时间计数。
c
u32_t sys_now(void);
- 获取系统时间
需要我们实现的函数
c
void HAL_ETH_MspInit(ETH_HandleTypeDef* ethHandle);
void HAL_ETH_MspDeInit(ETH_HandleTypeDef* ethHandle);
- IO口的映射,用于
HAL_ETH_Init
c
int32_t ETH_PHY_IO_Init(void);
int32_t ETH_PHY_IO_DeInit (void);
- PHY接口(去)初始化。用于功能函数结构体注册。
c
int32_t ETH_PHY_IO_ReadReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t *pRegVal);
int32_t ETH_PHY_IO_WriteReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t RegVal);
- 读写PHY寄存器。内部使用
HAL_ETH_ReadPHYRegister和ETH_PHY_IO_WriteReg实现即可(ETH外设->PHY)。用于功能函数结构体注册。
c
int32_t ETH_PHY_IO_GetTick(void)
- 获取时间戳。用于功能函数结构体注册。
c
void HAL_ETH_RxAllocateCallback(uint8_t **buff);
void HAL_ETH_RxLinkCallback(void **pStart, void **pEnd, uint8_t *buff, uint16_t Length);
void HAL_ETH_TxFreeCallback(uint32_t * buff);
App/lwip
MX生成的和Lwip的适配的应用层模板文件,类似于main.c的作用。
void MX_LWIP_Init(void);
- 配置DHCP或者静态IP地址
- 申请空间
- 添加(创建)网口(
netif_add) - 初始化网口(
netif_set_default) - 激活网口(
netif_set_up) - 设置链接状态改变回调函数(
netif_set_link_callback,this function is called on change of link status)
static void Ethernet_Link_Periodic_Handle(struct netif *netif)
{
/* USER CODE BEGIN 4_4_1 */
/* USER CODE END 4_4_1 */
/* Ethernet Link every 100ms */
if (HAL_GetTick() - EthernetLinkTimer >= 100)
{
EthernetLinkTimer = HAL_GetTick();
ethernet_link_check_state(netif);
}
/* USER CODE BEGIN 4_4 */
/* USER CODE END 4_4 */
}
- 周期性链路状态处理函数,主要负责:
- 轮询 PHY 芯片状态:读取 PHY 寄存器,获取当前物理链路状态。
ethernet_link_check_state
/**
* ----------------------------------------------------------------------
* Function given to help user to continue LwIP Initialization
* Up to user to complete or change this function ...
* Up to user to call this function in main.c in while (1) of main(void)
*-----------------------------------------------------------------------
* Read a received packet from the Ethernet buffers
* Send it to the lwIP stack for handling
* Handle timeouts if LWIP_TIMERS is set and without RTOS
* Handle the llink status if LWIP_NETIF_LINK_CALLBACK is set and without RTOS
*/
void MX_LWIP_Process(void){
ethernetif_input(&gnetif);
sys_check_timeouts();
Ethernet_Link_Periodic_Handle(&gnetif);
}
- 其中
sys_check_timeouts()是 LwIP 协议栈中核心的定时事件处理函数,负责执行所有已到期的定时任务。
可见,这个函数是主入口函数。
/**
* @brief Notify the User about the network interface config status
*/
static void ethernet_link_status_updated(struct netif *netif)
- 给用户实现(里面空),用来处理当链接状态发送改变时的任务。
IP配置
IP配置有两种方法,一种是静态IP,一种是设置DHCP由路由器分配。
静态IP
需要保证和IP在统一子网下。子网范围由IP和子网掩码(mask)确定。子网掩码可以自行设定
- 例如,子网掩码255.255.255.0,即11111111.11111111.11111111.00000000,表示只要求前3个数一样。
若单片机IP为192.167.46.28,则可以连主机IP为192.167.46.0~192.167.46.254(注意192.167.46.255为广播地址)
网关(Gateway) 是连接不同网络的关键节点,负责在不同网络之间转发数据包。在TCP/IP网络中,网关通常是路由器的一个接口。当设备需要与不同子网的设备通信时,必须通过网关。网关IP通常是子网下的第一个可用IP,比如192.167.46.1,或者你可以用电脑打开路由器的管理页面查看路由器IP。
DHCP
DHCP(Dynamic Host Configuration Protocol)是网络设备自动获取IP地址的协议。它让设备无需手动配置就能接入网络。
DHCP服务器提供:
- IP地址:设备在网络中的标识
- 子网掩码:定义网络范围
- 默认网关:访问其他网络的出口
- DNS服务器:域名解析服务
- 租约时间:IP地址的有效期
由路由器提供DHCP服务。
开启DHCP后,void MX_LWIP_Init(void)最后会多出:
/* Start DHCP negotiation for a network interface (IPv4) */
dhcp_start(&gnetif);
为我们开启DHCP服务,关键:dhcp_discover(netif);,与服务器协商。
还有一行代码值得我们注意,在 dhcp_start()中
if (!netif_is_link_up(netif)) {
/* set state INIT and wait for dhcp_network_changed() to call dhcp_discover() */
dhcp_set_state(dhcp, DHCP_STATE_INIT);
return ERR_OK;
}
表示,当物理链路未建立的时候,DHCP会挂起,仅当dhcp_network_changed()(链路改变)时,才会调用dhcp_discover()。
void
netif_set_link_up(struct netif *netif)
{
LWIP_ASSERT_CORE_LOCKED();
...
#if LWIP_DHCP
dhcp_network_changed(netif);
#endif /* LWIP_DHCP */
因此,我们在启动业务逻辑之前,需要等待DHCP完成。参考:
uint32_t wait_time = 0;
while (wait_time < 30000) { // 30 秒超时
MX_LWIP_Process();
if (gnetif.ip_addr.addr != 0) {
printf("DHCP 成功!\n");
break;
}
if (wait_time % 5000 == 0) {
printf("等待 DHCP... %ld ms\n", wait_time);
}
HAL_Delay(100);
wait_time += 100;
}
完成
移植完成后,在主循环里面循环调用 MX_LWIP_Process(),单片机就成功接上局域网了。在电脑打开命令行,输入
ping 192.167.46.28
就能够接收到回复了。
LwIP使用
LwIP 提供的三种编程接口
RAW/Callback API (最原始)
- 特点:基于回调函数,无阻塞,性能最高
- 适用:对实时性要求极高,资源极度受限
- 复杂度:高,需要手动管理连接状态
Netconn API (中间层)
- 特点:面向连接的API,支持阻塞操作,更易用
- 适用:需要多任务协作,中等复杂度应用
- 注意:需在LwIP线程中运行
Socket API (推荐)
- 特点:标准BSD Socket接口,移植性好,最简单
- 适用:大多数应用场景,特别是初学者
- 配置:需在
lwipopts.h中启用LWIP_SOCKET=1
LwIP RAW/Callback API
RAW/Callback API(有时简称RAW API)是LwIP提供的最底层、最高效的编程接口。它直接操作协议栈内部结构,通过回调函数(Callback)机制处理网络事件,完全无阻塞,能实现最高的性能和最小的内存开销。
回调驱动模型
与传统“请求-响应”模式不同,RAW API采用事件驱动: - 应用层注册回调函数到协议栈 - 协议栈在特定事件发生时主动调用这些回调函数 - 应用在回调函数中处理数据或状态变化
事件发生 → LwIP调用回调函数 → 你的代码被执行
(网络数据到达、连接建立、定时器超时等)
核心数据结构:Protocol Control Block (PCB)
每种协议都有对应的PCB结构,用于维护连接/通信状态:
- TCP:struct tcp_pcb
- UDP:struct udp_pcb
- RAW IP:struct raw_pcb
应用直接创建和操作PCB,绕过Socket层的抽象。
服务器端典型流程
步骤1:创建TCP PCB
struct tcp_pcb *tcp_server_pcb;
// 创建新的TCP PCB(控制块)
tcp_server_pcb = tcp_new(); // 相当于socket()创建
if (tcp_server_pcb == NULL) {
// 内存不足,创建失败
}
步骤2:绑定本地地址
err_t err;
// 绑定IP和端口
err = tcp_bind(tcp_server_pcb, IP_ADDR_ANY, 8080);
if (err != ERR_OK) {
// 绑定失败(如端口被占用)
tcp_abort(tcp_server_pcb);
return;
}
步骤3:开始监听并设置回调
// 进入监听状态,指定最大待处理连接数
tcp_server_pcb = tcp_listen(tcp_server_pcb);
if (tcp_server_pcb == NULL) {
// 监听失败
return;
}
// 设置连接建立回调函数
tcp_accept(tcp_server_pcb, tcp_server_accept_callback);
步骤4:实现连接接受回调
// 当客户端连接时,LwIP自动调用此函数
static err_t tcp_server_accept_callback(
void *arg,
struct tcp_pcb *newpcb, // 为新连接创建的PCB
err_t err
) {
if (err != ERR_OK || newpcb == NULL) {
return ERR_VAL;
}
// 为新连接设置数据接收回调
tcp_recv(newpcb, tcp_server_recv_callback);
// 设置错误回调
tcp_err(newpcb, tcp_server_error_callback);
// 设置发送完成回调
tcp_sent(newpcb, tcp_server_sent_callback);
// 可在此保存连接上下文(arg参数)
return ERR_OK;
}
步骤5:实现数据接收回调
static err_t tcp_server_recv_callback(
void *arg,
struct tcp_pcb *tpcb,
struct pbuf *p, // 接收到的数据包(pbuf链)
err_t err
) {
if (p == NULL) {
// p为NULL表示连接关闭
tcp_close(tpcb);
return ERR_OK;
}
if (err != ERR_OK) {
// 接收错误,释放pbuf
if (p != NULL) pbuf_free(p);
return err;
}
// 处理接收到的数据
// p->payload指向数据,p->len是数据长度
// 告诉TCP已经处理完这些数据(更新接收窗口)
tcp_recved(tpcb, p->tot_len);
// 释放pbuf(重要!)
pbuf_free(p);
return ERR_OK;
}
客户端流程
步骤1:创建PCB并连接
struct tcp_pcb *tcp_client_pcb;
ip_addr_t server_ip;
// 解析服务器IP
IP4_ADDR(&server_ip, 192, 168, 1, 100);
// 创建PCB
tcp_client_pcb = tcp_new();
// 设置回调函数
tcp_recv(tcp_client_pcb, tcp_client_recv_callback);
tcp_err(tcp_client_pcb, tcp_client_error_callback);
// 发起连接(非阻塞,立即返回)
tcp_connect(tcp_client_pcb, &server_ip, 8080, tcp_client_connected_callback);
步骤2:连接完成回调
static void tcp_client_connected_callback(
void *arg,
struct tcp_pcb *tpcb,
err_t err
) {
if (err != ERR_OK) {
// 连接失败
return;
}
// 连接成功,可以开始发送数据
const char *data = "Hello Server";
tcp_write(tpcb, data, strlen(data), TCP_WRITE_FLAG_COPY);
tcp_output(tpcb); // 立即发送
}
UDP RAW API 核心函数
UDP通信流程更简单:
// 1. 创建UDP PCB
struct udp_pcb *udp_pcb = udp_new();
// 2. 绑定本地端口(可选)
udp_bind(udp_pcb, IP_ADDR_ANY, 8080);
// 3. 设置接收回调
udp_recv(udp_pcb, udp_recv_callback, NULL);
// 4. 发送数据
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, data_len, PBUF_RAM);
memcpy(p->payload, data, data_len);
// 设置目标地址
ip_addr_t dest_ip;
IP4_ADDR(&dest_ip, 192, 168, 1, 100);
// 发送(无连接,每次指定目标)
udp_sendto(udp_pcb, p, &dest_ip, 8080);
pbuf_free(p); // 释放pbuf
UDP接收回调示例:
void udp_recv_callback(
void *arg,
struct udp_pcb *pcb,
struct pbuf *p,
const ip_addr_t *addr,
u16_t port
) {
// addr和port是发送方的地址和端口
// p是接收到的数据
// 处理数据...
pbuf_free(p); // 必须释放
}
特性
-
无阻塞设计
-
所有函数调用立即返回
- 长时间操作通过回调异步通知
-
适合单线程或简单RTOS环境
-
零拷贝支持
-
接收回调直接获得
pbuf指针 - 发送时可以直接使用应用缓冲区(指定
TCP_WRITE_FLAG_COPY标志) -
减少内存复制,提高性能
-
精细控制
-
直接操作MSS、窗口大小等TCP参数
- 手动控制ACK发送时机
-
完全控制连接生命周期
-
资源高效
-
不需要Socket层的额外内存开销
- 回调机制减少上下文切换
注意事项
-
回调函数约束
-
执行时间必须尽量短
- 避免在回调中进行复杂计算
-
不能调用可能阻塞的函数
-
内存管理责任
-
接收回调中必须处理
pbuf(使用或释放) - 注意
tcp_write()的flags参数: TCP_WRITE_FLAG_COPY:LwIP复制数据(安全)-
无标志:直接使用应用缓冲区(需保证生命周期)
-
错误处理
-
每个回调都有
err_t返回值 - 必须正确处理各种错误码
-
连接异常通过错误回调通知
-
多任务同步
-
RAW API本身非线程安全
- 如果多任务访问同一PCB,需要同步机制
- 建议每个PCB只由一个任务操作
配置
在lwipopts.h中需要启用(在MX配置即可):
#define LWIP_TCP 1 // 启用TCP
#define LWIP_UDP 1 // 启用UDP
#define LWIP_RAW 1 // 启用RAW API
#define TCP_LISTEN_BACKLOG 5 // 监听队列长度
#define LWIP_CALLBACK_API 1 // 启用回调API
LwIP Netconn API
Netconn API 是 LwIP 提供的面向连接的中间层抽象接口。它在 RAW/Callback API 之上构建,提供了更易于使用的同步编程模型,同时保持了较高的效率和较小的开销。
特点
- 面向连接:以
struct netconn对象为中心管理连接 - 同步操作:提供阻塞式函数调用,简化编程逻辑
- 线程安全:内置信号量保护,支持多线程环境
- 内存管理:自动处理
pbuf链的分配和释放
核心数据结构
头文件c
#include "lwip/api.h" // Netconn API 主要函数声明
struct netconn - 连接控制块
struct netconn {
enum netconn_type type; // 连接类型:TCP/UDP/RAW
enum netconn_state state; // 连接状态
union {
struct tcp_pcb *tcp; // TCP协议控制块
struct udp_pcb *udp; // UDP协议控制块
} pcb;
sys_sem_t op_completed; // 操作完成信号量
sys_mbox_t recvmbox; // 接收数据邮箱
// ... 其他内部字段
};
关键字段说明:
- type:定义连接协议类型
- pcb:指向底层RAW API的协议控制块
- op_completed:用于同步操作的信号量
- recvmbox:接收数据的消息队列
TCP服务器端编程
- 创建和绑定监听连接
// 创建TCP类型的netconn
struct netconn *server_conn = netconn_new(NETCONN_TCP);
// 绑定到本地地址和端口
netconn_bind(server_conn, IP_ADDR_ANY, 8080);
// 开始监听,指定最大等待连接数
netconn_listen(server_conn);
- 接受客户端连接
struct netconn *new_conn;
err_t err;
// 阻塞等待客户端连接
err = netconn_accept(server_conn, &new_conn);
if (err == ERR_OK) {
// new_conn是与客户端通信的专用连接
// 可在此创建线程处理该连接
}
- 接收和发送数据
struct netbuf *buf;
char *data;
u16_t len;
// 阻塞接收数据
netconn_recv(new_conn, &buf);
// 从netbuf中提取数据
netbuf_data(buf, (void **)&data, &len);
// 处理数据...
// 发送响应(自动复制数据)
netconn_write(new_conn, "ACK", 3, NETCONN_COPY);
// 释放netbuf
netbuf_delete(buf);
- 关闭连接
// 关闭连接(发送FIN)
netconn_close(new_conn);
// 释放netconn结构
netconn_delete(new_conn);
TCP客户端编程
- 建立连接
struct netconn *client_conn;
ip_addr_t server_addr;
// 创建连接对象
client_conn = netconn_new(NETCONN_TCP);
// 设置服务器地址
IP4_ADDR(&server_addr, 192, 168, 1, 100);
// 阻塞连接服务器
netconn_connect(client_conn, &server_addr, 8080);
- 非阻塞操作选项
// 设置非阻塞模式
netconn_set_nonblocking(client_conn, 1);
// 非阻塞接收(立即返回)
err = netconn_recv(client_conn, &buf);
if (err == ERR_WOULDBLOCK) {
// 没有数据可用
}
UDP通信编程
- UDP服务器
struct netconn *udp_conn;
struct netbuf *buf;
struct ip_addr *addr;
u16_t port;
// 创建UDP连接
udp_conn = netconn_new(NETCONN_UDP);
// 绑定本地端口
netconn_bind(udp_conn, IP_ADDR_ANY, 8080);
// 接收数据(包含源地址信息)
netconn_recv(udp_conn, &buf);
// 获取发送方地址
addr = netbuf_fromaddr(buf);
port = netbuf_fromport(buf);
// 发送回复到原地址
netconn_send(udp_conn, buf);
- UDP客户端
// 创建UDP连接(无需绑定)
struct netconn *udp_client = netconn_new(NETCONN_UDP);
// 创建发送缓冲区
struct netbuf *send_buf = netbuf_new();
// 向缓冲区填充数据
char *payload = "Hello UDP";
netbuf_alloc(send_buf, strlen(payload));
memcpy(netbuf_data(send_buf), payload, strlen(payload));
// 发送到指定地址
netconn_sendto(udp_client, send_buf, &server_addr, 8080);
// 清理
netbuf_delete(send_buf);
回调机制
事件回调注册
// 设置接收回调(替代阻塞接收)
netconn_recv_callback(conn, my_recv_callback, my_arg);
// 回调函数原型
void my_recv_callback(struct netconn *conn,
enum netconn_evt evt,
u16_t len)
{
switch(evt) {
case NETCONN_EVT_RCVPLUS: // 有数据到达
// 可以调用netconn_recv_nonblock接收
break;
case NETCONN_EVT_SENDPLUS: // 发送缓冲区可用
break;
case NETCONN_EVT_ERROR: // 发生错误
break;
}
}
内存管理函数
Netbuf操作函数族
// 创建和释放netbuf
struct netbuf *netbuf_new(void);
void netbuf_delete(struct netbuf *buf);
// 数据操作
void *netbuf_alloc(struct netbuf *buf, u16_t size);
err_t netbuf_ref(struct netbuf *buf, const void *dataptr, u16_t size);
void netbuf_free(struct netbuf *buf);
// 数据访问
err_t netbuf_data(struct netbuf *buf, void **dataptr, u16_t *len);
struct ip_addr *netbuf_fromaddr(struct netbuf *buf);
u16_t netbuf_fromport(struct netbuf *buf);
错误处理
错误码类型
// Netconn专用错误码
#define NETCONN_EAGAIN -1 // 重试(非阻塞模式)
#define NETCONN_INPROGRESS -2 // 操作进行中
#define NETCONN_ABORTED -3 // 操作被中止
#define NETCONN_CLOSED -4 // 连接已关闭
#define NETCONN_RESET -5 // 连接被重置
错误检查模式
err_t err;
// 标准错误检查
err = netconn_write(conn, data, len, flags);
if (err != ERR_OK) {
switch(err) {
case ERR_MEM: // 内存不足
case ERR_TIMEOUT: // 操作超时
case ERR_CLSD: // 连接关闭
// ... 其他错误处理
}
}
配置
lwipopts.h配置(在MX无法勾选)
#define LWIP_NETCONN 1 // 启用Netconn API
#define TCPIP_MBOX_SIZE 8 // TCPIP线程邮箱大小
#define DEFAULT_THREAD_STACKSIZE 1024 // 默认线程栈大小
#define DEFAULT_RAW_RECVMBOX_SIZE 8 // RAW PCB接收邮箱大小
#define DEFAULT_UDP_RECVMBOX_SIZE 8 // UDP接收邮箱大小
#define DEFAULT_TCP_RECVMBOX_SIZE 8 // TCP接收邮箱大小
线程模型与执行上下文
TCPIP线程要求
// Netconn API必须在TCPIP线程上下文中执行
// 典型的主线程初始化:
void tcpip_init_done_fn(void *arg) {
// LwIP初始化完成后执行
sys_sem_signal(init_sem);
}
int main() {
sys_sem_t init_sem;
sys_sem_new(&init_sem, 0);
// 初始化LwIP,指定回调
tcpip_init(tcpip_init_done_fn, &init_sem);
// 等待初始化完成
sys_sem_wait(&init_sem);
// 现在可以在主线程中使用Netconn API
// 或创建专用线程处理网络连接
}
多线程访问保护
// Netconn对象不是完全线程安全的
// 需要额外的保护机制:
// 方案1:每个线程使用独立的netconn
struct netconn *thread_local_conn = netconn_new(NETCONN_TCP);
// 方案2:使用互斥锁保护共享netconn
sys_mutex_t conn_mutex;
sys_mutex_new(&conn_mutex);
sys_mutex_lock(&conn_mutex);
// 操作共享的netconn
netconn_write(shared_conn, data, len, flags);
sys_mutex_unlock(&conn_mutex);
性能优化函数
批量发送接口
// 零拷贝发送(需保证数据在回调期间有效)
netconn_write_vectors(conn, iovec, count, flags);
// 异步发送(立即返回)
netconn_write_async(conn, data, len, flags, callback, arg);
缓冲区管理
// 设置发送缓冲区大小
netconn_set_sendbuf(conn, size);
// 设置接收缓冲区大小
netconn_set_recvbuf(conn, size);
// 获取当前缓冲区使用情况
u16_t sendbuf_used = netconn_get_sendbuf_used(conn);
u16_t recvbuf_used = netconn_get_recvbuf_used(conn);
连接状态查询
状态检查函数
// 检查连接是否有效
int netconn_is_valid(struct netconn *conn);
// 获取连接状态
enum netconn_state state = netconn_get_state(conn);
// 检查是否可写(发送缓冲区可用)
int writable = netconn_is_writable(conn);
// 检查是否有待读数据
int readable = netconn_is_readable(conn);
超时控制
// 设置接收超时(毫秒)
netconn_set_recvtimeout(conn, timeout_ms);
// 设置发送超时
netconn_set_sendtimeout(conn, timeout_ms);
// 设置连接超时
netconn_set_connecttimeout(conn, timeout_ms);
调试支持
调试信息输出
// 启用Netconn调试
#define NETCONN_DEBUG LWIP_DBG_ON
// 获取连接调试信息
void netconn_debug_print(struct netconn *conn) {
LWIP_DEBUGF(NETCONN_DEBUG,
"Netconn %p: type=%d, state=%d\n",
conn, conn->type, conn->state);
}
// 跟踪API调用
#define NETCONN_TRACE 1
统计信息
// 获取Netconn内存使用统计
void netconn_memp_stats(void);
// 获取活动连接数
int active_connections = netconn_get_active_count();
// 获取各类型连接统计
struct netconn_stats stats;
netconn_get_stats(&stats);
LwIP Socket API
Note
Socket一般需要OS的支持,比如RTOS。当然,理论上裸机也可以运行。Socket API 是 LwIP 提供的最高层、最标准的编程接口,实现了 POSIX/BSD Socket API 的子集。它为嵌入式开发者提供了与桌面/服务器开发完全相同的网络编程接口,最大程度保证了代码的可移植性。
特点
- 标准兼容:遵循 POSIX 标准,与 Linux/Windows Socket API 兼容
- 易于移植:桌面网络代码几乎可直接移植到嵌入式环境
- 阻塞/非阻塞:完整支持两种操作模式
- 线程安全:内置同步机制,支持多线程并发访问
- 内存透明:自动处理数据缓冲和转换
核心数据结构与头文件
头文件
#include "lwip/sockets.h" // Socket API 主要函数声明
#include "lwip/netdb.h" // 网络数据库函数(gethostbyname等)
#include <errno.h> // 错误码定义
地址结构体
// IPv4 地址结构(主要使用)
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址结构
unsigned char sin_zero[8]; // 填充字节
};
// 通用地址结构(用于类型转换)
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址数据
};
TCP Socket 核心函数
socket()- 创建套接字
int socket(int domain, int type, int protocol);
参数详解:
- domain:协议域
- AF_INET:IPv4 协议(唯一支持)
- type:套接字类型
- SOCK_STREAM:流式套接字(TCP)
- SOCK_DGRAM:数据报套接字(UDP)
- protocol:协议类型
- 0:根据 type 自动选择
返回值:
- 成功:返回套接字描述符sockfd(非负整数)
- 失败:返回 -1,设置 errno
bind()- 绑定本地地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
地址设置示例:
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(8080); // 主机序转网络序
local_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有接口
bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));
listen()- 进入监听状态
int listen(int sockfd, int backlog);
参数:
- backlog:等待连接队列的最大长度
- 建议值:5-10
- 受 MEM_SIZE 限制,内存不足会失败
完整服务器初始化:
// 创建、绑定、监听三步曲
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, ...);
listen(server_fd, 5);
accept()- 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
阻塞特性: - 默认阻塞,直到有客户端连接 - 返回新套接字用于客户端通信 - 原监听套接字继续接受新连接
获取客户端信息:
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr*)&client_addr,
&client_len);
// 获取客户端IP和端口
char *client_ip = inet_ntoa(client_addr.sin_addr);
uint16_t client_port = ntohs(client_addr.sin_port);
connect()- 建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端连接流程:
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(80);
inet_aton("192.168.1.100", &server_addr.sin_addr);
connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
send()- 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
关键特性:
- 用于已连接的 TCP 套接字
- 实际发送字节数可能小于请求的 len
- 需要循环发送确保所有数据发出
可靠发送实现:
int send_all(int sockfd, const void *buf, size_t len) {
size_t total_sent = 0;
while (total_sent < len) {
ssize_t sent = send(sockfd,
(char*)buf + total_sent,
len - total_sent,
0);
if (sent <= 0) return -1; // 出错或连接关闭
total_sent += sent;
}
return total_sent;
}
recv()- 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
返回值语义:
- >0:接收到的字节数
- =0:对方已关闭连接
- -1:出错,检查 errno
常见错误码:
if (recv_len < 0) {
switch (errno) {
case EWOULDBLOCK: // 非阻塞模式下无数据
// 可稍后重试
break;
case ECONNRESET: // 连接被对端重置
// 需要关闭套接字
close(sockfd);
break;
// ... 其他错误
}
}
close()- 关闭连接
int close(int sockfd);
关闭行为: - TCP:发送 FIN 包,进入 TIME_WAIT - 释放套接字占用的所有资源 - 多次关闭同一套接字是未定义行为
UDP Socket 核心函数
sendto()- 发送数据报
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
无需连接的发送:
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8080);
inet_aton("192.168.1.100", &dest_addr.sin_addr);
sendto(udp_fd, data, data_len, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
recvfrom()- 接收数据报
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
获取发送方信息:
struct sockaddr_in sender_addr;
socklen_t addrlen = sizeof(sender_addr);
ssize_t recv_len = recvfrom(udp_fd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&sender_addr, &addrlen);
if (recv_len > 0) {
// 获取发送方IP和端口
char *sender_ip = inet_ntoa(sender_addr.sin_addr);
uint16_t sender_port = ntohs(sender_addr.sin_port);
}
套接字选项控制函数
setsockopt()- 设置选项
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
- 常用选项设置:
设置接收超时:
struct timeval timeout;
timeout.tv_sec = 5; // 5秒
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,
&timeout, sizeof(timeout));
设置发送超时:
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO,
&timeout, sizeof(timeout));
启用地址重用:
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse));
设置TCP保活:
int keepalive = 1;
int keepidle = 60; // 空闲60秒后开始发送保活探测
int keepinterval = 5; // 探测间隔5秒
int keepcount = 3; // 最多探测3次
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepinterval, sizeof(keepinterval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepcount, sizeof(keepcount));
getsockopt()- 获取选项
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
获取错误状态:
int error = 0;
socklen_t len = sizeof(error);
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
非阻塞I/O控制
- 设置非阻塞模式
#include <fcntl.h>
int set_nonblocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags < 0) return -1;
return fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
}
- 非阻塞接收模式
// 设置非阻塞
set_nonblocking(sockfd);
// 接收数据(立即返回)
ssize_t recv_len = recv(sockfd, buffer, sizeof(buffer), 0);
if (recv_len < 0) {
if (errno == EWOULDBLOCK) {
// 没有数据可读,正常情况
} else {
// 真实错误
}
}
select()- I/O多路复用
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 监控多个套接字:
fd_set readfds;
struct timeval timeout;
while (1) {
FD_ZERO(&readfds);
FD_SET(sockfd1, &readfds);
FD_SET(sockfd2, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int ret = select(sockfd2 + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(sockfd1, &readfds)) {
// sockfd1 有数据可读
}
if (FD_ISSET(sockfd2, &readfds)) {
// sockfd2 有数据可读
}
} else if (ret == 0) {
// 超时
} else {
// 错误
}
}
地址转换函数
- 字节序转换
#include "lwip/def.h"
uint16_t htons(uint16_t hostshort); // 主机序 -> 网络序(16位)
uint16_t ntohs(uint16_t netshort); // 网络序 -> 主机序(16位)
uint32_t htonl(uint32_t hostlong); // 主机序 -> 网络序(32位)
uint32_t ntohl(uint32_t netlong); // 网络序 -> 主机序(32位)
- 字符串与地址转换
// 字符串转IP地址(推荐)
int inet_aton(const char *cp, struct in_addr *inp);
// IP地址转字符串
char *inet_ntoa(struct in_addr in);
// 现代版本(支持IPv6)
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);
- 主机名解析
#include "lwip/netdb.h"
struct hostent *gethostbyname(const char *name);
// 使用示例
struct hostent *host = gethostbyname("www.example.com");
if (host) {
struct in_addr **addr_list = (struct in_addr**)host->h_addr_list;
for (int i = 0; addr_list[i] != NULL; i++) {
printf("IP: %s\n", inet_ntoa(*addr_list[i]));
}
}
辅助函数
- 获取对端地址
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 获取本地地址
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 关闭部分连接
int shutdown(int sockfd, int how);
关闭模式:
- SHUT_RD:停止接收
- SHUT_WR:停止发送(发送FIN)
- SHUT_RDWR:双向关闭
配置
lwipopts.h 配置(MX无法勾选)
#define LWIP_SOCKET 1 // 启用Socket API
#define LWIP_COMPAT_SOCKETS 1 // 兼容标准Socket
#define LWIP_SO_RCVTIMEO 1 // 启用接收超时
#define LWIP_SO_SNDTIMEO 1 // 启用发送超时
#define LWIP_SO_RCVBUF 1 // 启用接收缓冲区
#define SO_REUSE 1 // 启用地址重用
// 内存配置(根据应用调整)
#define MEM_SIZE (16*1024)
#define MEMP_NUM_NETBUF 8
#define MEMP_NUM_NETCONN 8
#define MEMP_NUM_TCP_PCB 5
#define MEMP_NUM_TCP_PCB_LISTEN 5
错误处理模式
标准错误处理模板
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("socket failed: errno=%d\n", errno);
return -1;
}
struct sockaddr_in addr;
// ... 设置地址
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
printf("connect failed: errno=%d\n", errno);
close(sockfd);
return -1;
}
// 数据收发
ssize_t len = send(sockfd, data, data_len, 0);
if (len < 0) {
printf("send failed: errno=%d\n", errno);
}
// 清理
close(sockfd);
错误码对照表
| errno 值 | 宏定义 | 含义 |
|---|---|---|
| 11 | EAGAIN | 资源暂时不可用(非阻塞) |
| 104 | ECONNRESET | 连接被对端重置 |
| 110 | ETIMEDOUT | 操作超时 |
| 111 | ECONNREFUSED | 连接被拒绝 |
| 113 | EHOSTUNREACH | 主机不可达 |
性能优化函数
零拷贝发送选项
// 使用 MSG_DONTWAIT 进行非阻塞发送
ssize_t len = send(sockfd, data, data_len, MSG_DONTWAIT);
// 使用 MSG_MORE 提示更多数据将发送
send(sockfd, data1, len1, MSG_MORE);
send(sockfd, data2, len2, 0); // 实际发送
缓冲区大小调整
// 设置发送缓冲区大小
int sndbuf_size = 8192;
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size));
// 设置接收缓冲区大小
int rcvbuf_size = 8192;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));
调试与统计
调试输出控制
// 在 lwipopts.h 中启用调试
#define SOCKETS_DEBUG LWIP_DBG_ON
// 调试函数
void socket_debug_print(int sockfd) {
struct sockaddr_in local_addr, remote_addr;
socklen_t len = sizeof(local_addr);
getsockname(sockfd, (struct sockaddr*)&local_addr, &len);
getpeername(sockfd, (struct sockaddr*)&remote_addr, &len);
LWIP_DEBUGF(SOCKETS_DEBUG,
"Socket %d: Local %s:%d, Remote %s:%d\n",
sockfd,
inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port),
inet_ntoa(remote_addr.sin_addr), ntohs(remote_addr.sin_port));
}
资源统计
// 获取当前打开的套接字数
extern int lwip_socket_counter;
// 打印统计信息
void print_socket_stats(void) {
printf("Active sockets: %d\n", lwip_socket_counter);
}
平台适配注意
与标准Socket的差异
// 1. 部分函数可能未实现
// 如:getaddrinfo(), sendmsg(), recvmsg()
// 2. 选项支持可能有限
// 某些SO_*选项可能不可用
// 3. 文件描述符与套接字描述符分开
// LwIP中socket返回的描述符与文件系统无关
线程安全实现
// LwIP的Socket API本身是线程安全的
// 但同一个套接字在多个线程中操作需要额外保护:
// 方案1:每个线程使用独立套接字
int thread1_sock = socket(...);
int thread2_sock = socket(...);
// 方案2:使用互斥锁保护共享套接字
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t sock_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(sock_mutex, portMAX_DELAY);
send(shared_sock, data, len, 0);
xSemaphoreGive(sock_mutex);
Important
启用DHCP时,不知为什么,在单片机做客户端的情况下,需要主机先ping一下单片机,单片机才能连上。可能时由于防火墙限制。