PHY 是物理层芯片,MAC 是介质访问控制层,两者不是一回事。
1. 一句话分清:MAC vs PHY
- MAC:管 “数据怎么打包、发给谁”(数字逻辑,纯数字电路)
- PHY:管 “怎么把数据变成网线上的电信号”(模拟 + 数字,必须外接芯片)
它们的关系:
MCU / CPU → MAC → MII/RMII/RGMII → PHY → 网口变压器 → 网线
2. 你最关心的:
MAC 在哪里?
MAC 一般是集成在 MCU / 处理器内部的,不是独立芯片!
-
STM32F4/F7/H7、IMX6UL、IMX8、ESP32、GD32、AT32 等→ 内部自带 MAC→ 必须外接 PHY(比如 RTL8211E)
-
只有极少数老芯片 / 专用芯片会把 MAC+PHY 做在一起比如:ENC28J60、W5500(这种叫硬件 TCP/IP 芯片)
3. 所以你会遇到三种以太网结构
① 最常见:MCU 内置 MAC + 外置 PHY
- MAC:MCU 内部
- PHY:RTL8211E / KSZ9031 / YT8522 等→ 你现在用的就是这种!
② 老方案:外置 MAC + 外置 PHY
- MAC:独立芯片(如 ENC28J60 里的 MAC)
- PHY:也在里面→ 现在很少用
③ 全集成网卡芯片(MAC+PHY 一体)
- W5500、CH395
- 内部 = MAC + PHY + 协议栈→ 适合没有 MAC 的低端 MCU
4. 直接回答你的问题
问:MAC 也是像 PHY 那样的独立芯片吗?答:现代系统里基本不是。
- MAC 大多在 MCU/CPU 内部
- PHY 一定是独立芯片(Realtek、Microchip、裕太微这些)
5. 超简短总结(方便记)
- MAC = 内置在 MCU
- PHY = 必须外接芯片
- 以太网 = MAC + PHY 才能工作
“配置好 MAC 和 PHY 通信” 只是以太网开发的基础第一步,完整的以太网功能开发还需要覆盖协议栈、数据收发等环节。我用新手能听懂的方式,把代码开发的核心步骤拆解清楚:
一、先明确核心结论
你写代码时,必须先完成 MAC 与 PHY 的通信配置(这是以太网能工作的前提),但仅这一步还不够,还需要配置 MAC 本身、挂载协议栈、实现数据收发逻辑,才能真正用上网线传输数据。
二、代码开发的完整步骤(以 STM32+RTL8211E 为例)
1. 第一步:核心前提 —— 配置 MAC 与 PHY 的通信(你关注的重点)
这一步的目标是让 MCU 内置的 MAC 能 “指挥” 外置的 PHY 芯片(比如 RTL8211E),核心是通过 MDIO/MDC 总线读写 PHY 的寄存器:
- 初始化 MDIO/MDC 引脚(MAC 和 PHY 的 “控制总线”);
- 读取 PHY 的 ID 寄存器,验证 MAC 能和 PHY 通信(比如 RTL8211E 的 ID 是 0x001CC916);
- 配置 PHY 的工作模式:比如千兆全双工、自动协商、使能自协商、关闭低功耗等;
- 等待 PHY 链路状态(Link Up),确认网线插好且协商成功。
示例代码片段(STM32 HAL 库):
#include "stm32f4xx_hal.h"
#include "lan8742.h" // 通用PHY驱动,适配RTL8211E
// PHY地址(RTL8211E默认是0x01)
#define PHY_ADDR 0x01
// 初始化MAC与PHY通信
HAL_StatusTypeDef eth_phy_init(void)
{
uint16_t phy_id = 0;
// 1. 初始化MDIO/MDC总线(MAC底层驱动)
HAL_ETH_MDIO_Init(&heth);
// 2. 读取PHY ID,验证通信是否正常
phy_id = ETH_ReadPHYRegister(&heth, PHY_ADDR, PHY_REG_ID1);
phy_id = (phy_id << 16) | ETH_ReadPHYRegister(&heth, PHY_ADDR, PHY_REG_ID2);
// 检查RTL8211E的ID(高位0x001C,低位0xC916)
if ((phy_id & 0xFFFF0000) != 0x001C0000) {
return HAL_ERROR; // MAC和PHY通信失败
}
// 3. 配置PHY:自动协商、千兆全双工
ETH_WritePHYRegister(&heth, PHY_ADDR, PHY_REG_CTRL, PHY_CTRL_AUTONEG);
// 4. 等待链路建立(Link Up)
uint32_t timeout = 1000;
while (timeout–) {
if (ETH_ReadPHYRegister(&heth, PHY_ADDR, PHY_REG_STATUS) & PHY_STATUS_LINK) {
return HAL_OK; // 链路正常
}
HAL_Delay(1);
}
return HAL_TIMEOUT; // 链路超时
}
2. 第二步:配置 MCU 内置的 MAC
PHY 配置好后,需要初始化 MAC 本身的工作模式:
- 选择 MAC 的接口类型(比如 RGMII,RTL8211E 常用);
- 配置 MAC 的工作模式(全双工 / 半双工、速率匹配 PHY 协商结果);
- 使能 MAC 的接收 / 发送功能;
- 配置中断(比如接收数据中断、链路状态变化中断)。
3. 第三步:挂载网络协议栈(关键!否则只能传裸数据)
如果只配置 MAC 和 PHY,只能收发 “裸的以太网帧”,无法实现 ping、TCP/UDP 通信,必须挂载协议栈:
- 轻量级协议栈:LWIP(最常用,STM32 官方支持)、uIP;
- 配置步骤:初始化 LWIP→设置 IP / 子网掩码 / 网关→初始化 ARP/TCP/UDP→开启协议栈任务。
示例:LWIP 初始化片段
#include "lwip/netif.h"
#include "lwip/tcpip.h"
#include "netif/ethernetif.h"
struct netif gnetif; // 网络接口结构体
// 初始化LWIP协议栈
void lwip_init(void)
{
ip4_addr_t ipaddr, netmask, gw;
// 1. 设置IP地址(根据你的网段改)
IP4_ADDR(&ipaddr, 192, 168, 1, 100);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);
// 2. 初始化TCP/IP内核
tcpip_init(NULL, NULL);
// 3. 将MAC/PHY绑定到LWIP网络接口
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input);
netif_set_default(&gnetif);
netif_set_up(&gnetif); // 启用网络接口
}
4. 第四步:实现业务逻辑(收发数据)
协议栈配置好后,就可以写具体功能:
- 实现 ping 响应(LWIP 自带,只需开启);
- 实现 TCP 服务器 / 客户端(比如监听端口、收发数据);
- 实现 UDP 广播 / 单播;
- 处理链路中断(比如网线断开时的异常处理)。
三、新手最容易踩的坑(完整版)
这些都是你写 MAC + PHY(RTL8211E) 代码时,100% 会遇到的问题:
1. MDIO/MDC 没配置对 → 根本读不到 PHY
- 现象:读 PHY ID 读不出来、一直超时
- 原因:
- GPIO 模式错了(应该开复用功能 / 推挽)
- MDC 时钟太快或太慢
- PHY 地址错了(RTL8211E 常用 0 或 1,看硬件走线)
- 结果:MAC 根本管不了 PHY
2. 只初始化 PHY,没初始化 MAC
- 现象:PHY 链路 UP 了,但发不出、收不到
- 原因:
- MAC 没开发送 / 接收
- MAC 没配置 RGMII / RMII 模式
- 没配置速率、双工(要和 PHY 协商结果一致)
3. MAC 和 PHY 接口模式不匹配
- RGMII / RMII / MII 必须硬件 + 软件一致
- 你用 RTL8211E 一般是 RGMII
- 错了直接不通
4. 自协商开了,但软件没读协商结果
- PHY 自动协商出:1000M 全双工
- 但你 MAC 还固定在:100M 半双工
- 结果:能 Link 但不通 / 丢包严重
正确做法:
5. LWIP 底层接口没对接好
- 你配置完 MAC+PHY 只是硬件通了
- 但 LWIP 不知道怎么发、怎么收
- 必须实现两个函数:
- low_level_output() // 协议栈 → MAC 发帧
- low_level_input() // MAC → 协议栈收帧
6. 时序 / 电平问题(RGMII 特别常见)
- RGMII 需要 内部延迟或外部延迟
- 有些 MCU 要开 MAC 内部延迟
- 有些靠 PCB 上的电阻 / 走线补偿
- 错了:千兆不通,百兆能通
7. 中断或 DMA 没开
- 以太网一般用 DMA + 描述符
- 你不初始化描述符、不开 DMA
- 数据根本不会自动收发
四、给你一句最实用的总结
写以太网代码 = 做 3 件事
协议栈的本质就是帮你完成 “数据解析 / 打包” 的核心工作,不用你手动处理复杂的网络协议格式,我再把这个过程拆得更细,让你写代码时完全清楚每一步该做什么:
一、先确认你的理解(完全正确)
协议栈对数据的处理,核心就是两件事:✅ 发数据时:把你要发给 MAC 的 “裸数据”(比如一串字符、传感器数值),按 TCP/IP 规则打包成以太网帧,再交给 MAC 发送;✅ 收数据时:把 MAC 接收到的 “以太网帧”,按 TCP/IP 规则反向解析,还原成你能直接用的 “裸数据”。
你不用关心 “以太网帧格式”“IP 头怎么加”“UDP 校验和怎么算”—— 这些全由协议栈搞定。
二、用具体例子看协议栈的 “打包 / 解析” 过程
假设你要给 192.168.1.200 发一串字符 "hello"(UDP 通信),全程不用你碰底层格式:
1. 你只需要写的业务代码(极简)
// 1. 准备要发的裸数据
char send_data[] = "hello";
// 2. 调用协议栈 API 发送(不用管格式)
udp_sendto(udp_pcb, send_data, strlen(send_data), &dest_ip, dest_port);
2. 协议栈自动完成的 “打包” 工作(你不用写)
| 1 | 把 "hello" 包装成 UDP 包(加 UDP 头:源端口、目的端口、长度、校验和) | UDP 数据包 |
| 2 | 把 UDP 包包装成 IP 包(加 IP 头:源 IP、目的 IP、协议类型、TTL) | IP 数据包 |
| 3 | 把 IP 包包装成以太网帧(加以太网头:源 MAC、目的 MAC、帧类型) | 以太网帧 |
3. 交给 MAC/PHY 发送(你只需对接协议栈和 MAC)
协议栈把打包好的以太网帧传给 MAC,MAC 再通过 RGMII 发给 PHY,PHY 转成电信号发出去。
4. 接收数据时的 “解析” 过程(反向)
当 MAC 从 PHY 收到以太网帧后:
- 协议栈先解析以太网帧,去掉以太网头,取出里面的 IP 包;
- 再解析 IP 包,去掉 IP 头,取出里面的 UDP 包;
- 最后解析 UDP 包,去掉 UDP 头,还原出 "hello" 这串裸数据;
- 调用你写的回调函数,把 "hello" 传给你的业务代码。
三、新手写代码时的核心工作(不用碰协议细节)
// 函数1:协议栈要发数据时,调用这个函数把数据给 MAC
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
// 把协议栈的 pbuf 数据传给 MAC 的发送缓冲区
HAL_ETH_Transmit(&heth, p->payload, p->len);
return ERR_OK;
}
// 函数2:MAC 收到数据时,调用这个函数把数据给协议栈
static struct pbuf *low_level_input(struct netif *netif)
{
uint8_t recv_buf[1500];
uint32_t len = HAL_ETH_Receive(&heth, recv_buf, 1500);
// 把 MAC 收到的数据封装成 pbuf,传给协议栈解析
struct pbuf *p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
pbuf_take(p, recv_buf, len);
return p;
}
总结
简单说:你管 “发什么数据、收了数据做什么”,协议栈管 “数据怎么打包 / 解析”,MAC/PHY 管 “数据怎么在网线上传”。
MDIO/MDC 是什么?
一句话:MDIO/MDC 是 MAC 用来 “配置、控制、看状态” 外部 PHY 芯片的两根专用线。
1. 先记死:只有 2 根线
- MDC:时钟线(MAC 发 → PHY 收)
- MDIO:数据线(双向,MAC ↔ PHY)
它俩只干一件事:让 MAC 读写 PHY 内部寄存器。
2. 形象比喻(你马上懂)
- MAC = 老板(在 MCU 里)
- PHY = 员工(外面独立芯片)
- RGMII/RMII = 员工干活的通道(传大数据)
- MDIO/MDC = 老板给员工下命令的对讲机
老板(MAC)要知道:
- 网线插没插?
- 现在是千兆还是百兆?
- 全双工还是半双工?
就通过 MDIO/MDC 问 PHY。
3. 它到底用来干嘛?(你写代码一定会用到)
通过 MDIO/MDC,MAC 可以对 PHY 做 4 件事:
4. 和 RGMII 的区别(超级重要)
- RGMII/RX/TX 那一组线:传以太网数据(大包)
- MDIO/MDC:传配置命令(小包)
没有 MDIO/MDC → PHY 不会工作。没有 RGMII → 不能传网络数据。
5. 你写代码时看到的就是这个
// 通过 MDIO/MDC 读 PHY 寄存器
HAL_ETH_ReadPHYRegister(…)
// 通过 MDIO/MDC 写 PHY 寄存器
HAL_ETH_WritePHYRegister(…)
这两个函数,底层就是在翻转 MDC 和 MDIO。
最终极简总结
MDIO/MDC = MAC 对 PHY 的管理总线。只负责配置 + 看状态,不负责传用户数据。
你现在这个理解层级,已经完全能上手写以太网驱动了。
MCU / CPU → MAC → MII/RMII/RGMII → PHY → 网口变压器 → 网线
举例子:GD32F407VET6单片机以太网内部结构

一、完整以太网链路总图(从上到下就是真实硬件位置)
【MCU 芯片】
├─ CPU / 内核
├─ Flash / RAM
└─ 【MAC 模块】(以太网控制器,集成在 MCU 内部)
├─ ① MDIO / MDC (2 根线:管理总线)
│ ↓
└─ ② RGMII / RMII / MII (4~16 根线:数据总线)
↓
【外部 PHY 芯片】(例如 RTL8211E)
├─ 接收 MDIO/MDC:配置、读状态
├─ 接收 RGMII/RMII:数据包
└─ 把数字信号 → 转为网口差分信号
↓
【网口变压器】(隔离、防雷、滤波)
↓
【RJ45 网口】(插网线的接口)
↓
【网线】→ 电脑 / 交换机 / 路由器
二、每个部分在哪里 + 干什么 + 关系
1. MCU 芯片
- 位置:你板子上的主控芯片
- 内部包含:
- CPU:运行代码
- MAC:以太网数据帧处理模块
- 重要:MAC 在 MCU 内部,不是独立芯片!
2. MAC(介质访问控制)
- 位置:MCU 内部硬件模块
- 作用:
- 打包 / 解析以太网帧
- 控制发送、接收
- 对外有两组接口:
- MDIO/MDC(管理口)
- RGMII/RMII/MII(数据口)
3. MDIO / MDC(你重点问的)
- 位置:MCU 两个引脚 → PHY 两个引脚
- 线数:2 根
- 方向:
- MDC:时钟,MCU → PHY
- MDIO:数据,双向
- 作用:
- 配置 PHY(速率、自协商、复位)
- 读 PHY 状态(网线是否插入、link 状态)
- 读 PHY 型号 ID(识别 RTL8211E)
- 总结:管理总线 = 配置、看状态,不传用户数据
4. RGMII / RMII / MII
- 位置:MCU 引脚 → PHY 引脚的一组高速线
- 作用:专门传以太网数据包
- 区别:
- MII:老款、线多、百兆
- RMII:简化版、线少、百兆
- RGMII:千兆、线少、你现在用的就是这个
- 总结:数据总线 = 传真正的网络数据
5. PHY 芯片(例如 RTL8211E)
- 位置:MCU 外部,独立芯片
- 作用:
- 把 MAC 发来的数字信号
- 转成网口能用的电信号
- 必须同时接:
- MDIO/MDC(配置)
- RGMII/RMII(数据)
6. 网口变压器
- 位置:PHY 和 RJ45 之间
- 作用:隔离、防雷、防烧、信号整形
7. RJ45 网口 → 网线
- 最终信号通过网线出去
三、最精简一句话总关系
- MAC 在 MCU 里
- PHY 是外部独立芯片
- MDIO/MDC:2 根线,用来配置 PHY
- RGMII/RMII:一组高速线,用来传数据
- 两条路必须同时有,以太网才能工作
你问的这个问题特别关键,能帮你跳出 “只能用 LWIP” 的固定思维!先给核心结论,再讲清楚不同场景该选什么:
一、核心结论
LWIP 不是必须的!它只是嵌入式领域最常用的选择,是否用它、用什么替代,完全取决于你的功能需求和MCU 资源:
- 若需要 TCP/UDP/IP/ICMP(ping、联网通信)→ 必须用协议栈(LWIP 或其他);
- 若只需要 “裸以太网帧收发”(比如自定义协议、简单数据传输)→ 可以不用任何协议栈,直接操作 MAC/PHY。
二、什么时候可以不用 LWIP(不用任何协议栈)
如果你的需求只是 “把数据从 MCU 发到另一台设备,且两台设备都由你控制”,比如:
- 仅需要收发自定义格式的以太网帧;
- 不需要联网、不需要 ping、不需要和电脑 / 服务器通信;
- 追求极致精简(MCU 资源极少,比如只有几 KB RAM)。
✅ 做法:直接操作 MAC 硬件,收发 “裸以太网帧”,示例代码(STM32):
// 发送裸以太网帧(不用协议栈)
void send_raw_eth_frame(uint8_t *data, uint16_t len)
{
uint8_t eth_frame[1500];
// 手动填充以太网帧头(目的MAC、源MAC、帧类型)
eth_frame[0] = 0x00; eth_frame[1] = 0x11; eth_frame[2] = 0x22; // 目的MAC前3字节
eth_frame[3] = 0x33; eth_frame[4] = 0x44; eth_frame[5] = 0x55; // 目的MAC后3字节
eth_frame[6] = 0xAA; eth_frame[7] = 0xBB; eth_frame[8] = 0xCC; // 源MAC前3字节
eth_frame[9] = 0xDD; eth_frame[10] = 0xEE; eth_frame[11] = 0xFF; // 源MAC后3字节
eth_frame[12] = 0x08; eth_frame[13] = 0x00; // 帧类型(0x0800=IP,自定义则用0x88B5等)
// 填充业务数据
memcpy(ð_frame[14], data, len);
// 直接通过MAC发送
HAL_ETH_Transmit(&heth, eth_frame, 14 + len);
}
// 接收裸以太网帧(不用协议栈)
void recv_raw_eth_frame(void)
{
uint8_t recv_buf[1500];
uint32_t len = HAL_ETH_Receive(&heth, recv_buf, 1500);
if (len > 14)
{
// recv_buf[14:] 就是裸数据,自己解析即可
}
}
三、除了 LWIP,还有哪些协议栈可选
如果需要 TCP/IP 功能(必须用协议栈),除了 LWIP,还有这些主流选择,按 “易用性 / 资源占用” 分类:
| LWIP | 轻量(几十 KB RAM)、开源、适配性强 | 90% 嵌入式场景(STM32/MCU/ 无 OS) |
| uIP | 超轻量(几 KB RAM)、极简 | 资源极省的 MCU(如 51/STM32F103) |
| FreeRTOS+TCP | 和 FreeRTOS 深度集成、稳定 | 用 FreeRTOS 的项目 |
| NetX/NetX Duo | 商用、高可靠、支持 IPv6 | 工业 / 车载(需要认证、稳定性优先) |
| Zephyr Net | 嵌入式 OS Zephyr 内置、模块化 | 基于 Zephyr 的物联网项目 |
| uC/IP | 和 uC/OS-II/III 配套、商用 | 用 uC/OS 的工业项目 |
重点对比(新手关注):
LWIP vs uIP:
- LWIP:功能全(TCP/UDP/IP/DHCP/DNS)、易移植,适合大多数场景;
- uIP:只有核心 TCP/IP,代码量极小,但功能弱(无 DHCP、无多连接),适合极低端 MCU。
LWIP vs FreeRTOS+TCP:
- 若你的项目已经用 FreeRTOS,优先选 FreeRTOS+TCP(无缝集成);
- 若不用 RTOS(裸机),优先选 LWIP。
四、新手选型建议(不用纠结)
总结
简单说:能裸帧解决的就不用协议栈,要用协议栈的话,新手直接选 LWIP 准没错。
你想知道功能完整的 LWIP 协议栈实际工程里长什么样,我直接给你展示 “标准 LWIP 工程结构 + 核心配置 / 代码示例”,新手一看就懂,还能直接参考移植:
一、先看 LWIP 在工程里的 “物理形态”(文件结构)
这是 STM32 + LWIP 工程中,LWIP 相关的核心文件布局(功能全版本),你移植时只需要把这些文件加入工程,再配置少量参数即可:
你的工程/
├─ LWIP/ // LWIP 核心目录(不用改)
│ ├─ src/
│ │ ├─ core/ // TCP/IP 核心(IP/TCP/UDP/ICMP 等)
│ │ │ ├─ ip.c/ip.h // IP 协议(地址、路由、分片)
│ │ │ ├─ tcp.c/tcp.h // TCP 协议(连接、重传、拥塞控制)
│ │ │ ├─ udp.c/udp.h // UDP 协议(无连接传输)
│ │ │ ├─ icmp.c/icmp.h // ICMP(ping 响应)
│ │ │ └─ netif/ // 网络接口(对接 MAC/PHY)
│ │ ├─ apps/ // 上层应用(可选,功能全时加)
│ │ │ ├─ dhcp.c/dhcp.h // DHCP(自动获取 IP)
│ │ │ ├─ dns.c/dns.h // DNS(域名解析,比如解析 www.baidu.com)
│ │ │ └─ httpd.c // 轻量 HTTP 服务器(可选)
│ │ └─ include/ // 所有头文件(不用改)
│ └─ lwipopts.h // LWIP 核心配置文件(你需要改的唯一核心文件)
├─ Drivers/
│ └─ ethernetif.c/ethernetif.h // 你需要写的:LWIP ↔ MAC 对接代码
└─ User/
└─ lwip_app.c // 你的业务代码(配置 IP、写 TCP/UDP 逻辑)
二、核心配置文件 lwipopts.h(功能全版本示例)
这是 “开启 TCP/UDP/IP/DHCP/DNS” 的标准配置,新手只需改内存大小、开关功能,不用改核心逻辑:
#ifndef LWIPOPTS_H
#define LWIPOPTS_H
// 1. 基础配置(适配 MCU 资源)
#define MEM_SIZE 32*1024 // 内存池大小(STM32F4 配 32KB 足够)
#define MEMP_NUM_TCP_PCB 8 // 最大 TCP 连接数(比如支持 8 个客户端)
#define MEMP_NUM_UDP_PCB 4 // 最大 UDP 连接数
#define PBUF_POOL_SIZE 16 // 数据包缓冲区数量
// 2. 开启核心协议(功能全)
#define LWIP_IPV4 1 // 启用 IPv4
#define LWIP_TCP 1 // 启用 TCP
#define LWIP_UDP 1 // 启用 UDP
#define LWIP_ICMP 1 // 启用 ICMP(ping)
#define LWIP_DHCP 1 // 启用 DHCP(自动获取 IP)
#define LWIP_DNS 1 // 启用 DNS(域名解析)
#define LWIP_NETIF_LINK_CALLBACK 1 // 链路状态回调(网线插拔检测)
// 3. 优化配置(新手不用改)
#define LWIP_TCP_KEEPALIVE 1 // TCP 保活(防止连接断开)
#define LWIP_TCP_TIMESTAMPS 1 // TCP 时间戳(优化重传)
#define LWIP_ARP 1 // 启用 ARP(MAC/IP 映射)
#define LWIP_IGMP 0 // 不用组播就关了
#define LWIP_STATS 0 // 关闭统计(节省资源)
// 4. 操作系统适配(裸机/RTOS 可选)
#define NO_SYS 0 // 0=用 RTOS(FreeRTOS),1=裸机
#define SYS_LIGHTWEIGHT_PROT 1 // 轻量级保护
#endif /* LWIPOPTS_H */
三、功能全的业务代码示例(TCP+UDP+DHCP+DNS)
这是基于 LWIP 实现 “自动获取 IP + TCP 服务器 + UDP 客户端 + 域名解析” 的核心代码,直接体现 LWIP 的 “功能全”:
#include "lwip/netif.h"
#include "lwip/tcpip.h"
#include "lwip/dhcp.h"
#include "lwip/dns.h"
#include "lwip/tcp.h"
#include "lwip/udp.h"
struct netif gnetif;
// ———————- 1. 初始化 LWIP 核心 ———————-
void lwip_init_all(void)
{
// 初始化 TCP/IP 内核
tcpip_init(NULL, NULL);
// 添加网络接口(绑定 MAC/PHY)
netif_add(&gnetif, NULL, NULL, NULL, NULL, ðernetif_init, &tcpip_input);
netif_set_default(&gnetif);
netif_set_up(&gnetif);
// 开启 DHCP(自动获取 IP,替代静态 IP)
dhcp_start(&gnetif);
// 初始化 TCP 服务器 + UDP 客户端
tcp_server_init(8080); // TCP 监听 8080 端口
udp_client_init(); // UDP 客户端初始化
}
// ———————- 2. TCP 服务器(监听 8080 端口) ———————-
err_t tcp_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
if (p != NULL)
{
// 收到 TCP 数据:p->payload 是数据内容,p->len 是长度
tcp_write(pcb, "收到数据", 6, TCP_WRITE_FLAG_COPY); // 回复客户端
tcp_output(pcb);
pbuf_free(p);
}
return ERR_OK;
}
void tcp_server_init(uint16_t port)
{
struct tcp_pcb *pcb = tcp_new();
tcp_bind(pcb, IP_ADDR_ANY, port);
pcb = tcp_listen(pcb);
tcp_accept(pcb, tcp_accept_cb); // 注册连接回调
}
// ———————- 3. UDP 客户端(发数据到 192.168.1.10) ———————-
void udp_client_init(void)
{
struct udp_pcb *pcb = udp_new();
ip4_addr_t dest_ip;
IP4_ADDR(&dest_ip, 192, 168, 1, 10); // 目标 IP
udp_connect(pcb, &dest_ip, 9090); // 目标端口 9090
udp_send(pcb, "UDP 测试数据", 8); // 发送 UDP 数据
}
// ———————- 4. DNS 域名解析(解析百度) ———————-
void dns_test(void)
{
ip4_addr_t ipaddr;
// 解析 www.baidu.com,回调函数获取 IP
dns_gethostbyname("www.baidu.com", &ipaddr, dns_found_cb, NULL);
}
// DNS 解析完成回调
void dns_found_cb(const char *name, const ip_addr_t *ipaddr, void *arg)
{
if (ipaddr != NULL)
{
// ipaddr 就是百度的 IP 地址,可用于 TCP/UDP 连接
}
}
四、LWIP “易移植” 的核心体现
总结
简单说:LWIP 把复杂的 TCP/IP 协议封装成了简单的 API,你不用懂协议细节,调用接口就能实现联网、通信、域名解析等功能。
你问的这个问题,是从 “实操层” 深入到了 “协议原理层”,我先给核心结论,再用新手能懂的方式讲透:
一、核心结论
✅ 3 次握手、4 次挥手是 TCP 协议的核心机制(UDP 没有);✅ 这些机制不是你写代码实现,而是LWIP 协议栈内部已经封装好的,你调用 LWIP 的 TCP API 时,它会自动完成。
简单说:你只需要调用 tcp_connect()/tcp_listen() 等 API,LWIP 会在底层自动处理 3 次握手建立连接、4 次挥手断开连接,你完全不用管细节。
二、先搞懂:3 次握手 / 4 次挥手到底是什么?
用 “打电话” 的比喻,让你一眼看懂(TCP 是 “面向连接” 的通信,像打电话;UDP 是 “发短信”,无连接):
1. 3 次握手(建立 TCP 连接)
目的:确保双方都能 “听得到、说得出”,建立可靠的通信通道。
你(客户端) 服务器
│ │
│ “喂,能听到吗?”(SYN)│
│───────────────────────>│
│ │ “能听到!你能听到我吗?”(SYN+ACK)
│<───────────────────────│
│ │
│ “能听到!开始聊吧”(ACK)│
│───────────────────────>│
│ │
│ 连接建立,开始传数据 │
2. 4 次挥手(断开 TCP 连接)
目的:确保双方都把数据发完,优雅断开连接(TCP 要保证数据不丢)。
你(客户端) 服务器
│ │
│ “我说完了,要挂了”(FIN)│
│───────────────────────>│
│ │ “好,我知道了”(ACK)
│<───────────────────────│
│ │ (服务器把剩下的数据发完)
│ │ “我也说完了,你挂吧”(FIN)
│<───────────────────────│
│ │
│ “好,那我挂了”(ACK)│
│───────────────────────>│
│ │
│ 连接断开 │
三、和 LWIP 的关系:你不用写,LWIP 帮你做
LWIP 的 tcp.c 源码里,已经内置了 3 次握手 / 4 次挥手的完整逻辑,你写代码时只需要调用 API,剩下的全由 LWIP 处理:
1. 你写的代码(极简)
// 客户端建立 TCP 连接(你只需要写这一行)
tcp_connect(pcb, &dest_ip, 8080, tcp_connect_cb);
// 服务器断开连接(你只需要写这一行)
tcp_close(pcb);
2. LWIP 底层自动做的事(你完全不用管)
- 调用 tcp_connect() 后:LWIP 自动发送 SYN 包 → 等待服务器的 SYN+ACK → 发送 ACK,完成 3 次握手;
- 调用 tcp_close() 后:LWIP 自动发送 FIN 包 → 等待服务器的 ACK → 接收服务器的 FIN → 发送 ACK,完成 4 次挥手;
- 甚至连 “重传、超时、拥塞控制” 这些细节,LWIP 也会自动处理。
3. 看 LWIP 源码里的核心逻辑(不用改,了解即可)
在 LWIP 的 tcp.c 中,有专门的状态机处理握手 / 挥手:
// LWIP 内部 TCP 状态机(简化版)
switch (pcb->state) {
case CLOSED:
// 初始状态
break;
case SYN_SENT:
// 发送 SYN 后,等待 SYN+ACK(握手第二步)
break;
case ESTABLISHED:
// 连接已建立,可收发数据
break;
case FIN_WAIT_1:
// 发送 FIN 后,等待 ACK(挥手第一步)
break;
// 其他状态…
}
四、新手关键认知
总结
简单说:3 次握手 / 4 次挥手是 TCP 的 “规矩”,LWIP 是帮你按规矩办事的 “助手”,你只需要告诉助手 “我要连谁”“我要断开”,剩下的规矩它来守。
你这个问题正好能帮你打通 “网络协议分层” 的核心逻辑,彻底搞懂 TCP 所处的位置和整个网络协议的体系!先给核心结论:
一、核心结论
✅ TCP 绝对不是最底层!它属于 “传输层”,在整个网络协议栈中处于中间层;✅ 除了 TCP,还有 UDP(同属传输层),以及更底层的 IP、以太网帧、物理层,和更上层的 HTTP、MQTT 等应用层协议。
二、用 “快递” 比喻,看懂完整协议分层(从上层到下层)
嵌入式以太网常用的是 TCP/IP 五层模型(比七层模型更易理解),TCP 只在其中一层,每一层都有明确分工:
| 应用层 | HTTP、MQTT、Modbus-TCP、自定义协议 | 你要寄的 “货物”(业务数据) | LWIP/apps(可选) |
| 传输层 | TCP、UDP | 快递单(标注收件人、保价等) | LWIP/src/core/tcp.c/udp.c |
| 网络层 | IP、ICMP(ping) | 物流路线(从北京到上海) | LWIP/src/core/ip.c/icmp.c |
| 数据链路层 | 以太网帧(MAC 地址) | 快递货车(同城运输) | MCU 内置 MAC 硬件 |
| 物理层 | 电信号 / 光信号(网线 / 光纤) | 公路 / 光纤线路 | PHY 芯片 + 网线 |
关键说明:
三、完整数据打包 / 解析流程(以 TCP 发送 “hello” 为例)
你写的业务数据会从上层到下层逐层打包,接收方再反向解析,TCP 只负责其中一步:
你的业务代码:"hello"(应用层)
↓
TCP 层:给"hello"加 TCP 头(源端口、目的端口、序列号)→ TCP 包
↓
IP 层:给 TCP 包加 IP 头(源 IP、目的 IP)→ IP 包
↓
数据链路层(MAC):给 IP 包加以太网帧头(源 MAC、目的 MAC)→ 以太网帧
↓
物理层(PHY):把以太网帧转换成网线上的电信号 → 发送
四、除了 TCP,常用的网络协议(按分层整理)
1. 同层(传输层):UDP
- 特点:无连接、不可靠、速度快、开销小;
- 适用场景:传感器数据上报、广播 / 组播、实时控制(如机器人遥控);
- LWIP 支持:只需在 lwipopts.h 中开 LWIP_UDP=1 即可调用 API。
2. 下层(网络层):IP、ICMP
- IP:所有 TCP/UDP 数据都必须通过 IP 传输,负责 “跨网段寻址”(比如从 192.168.1.100 到 192.168.2.200);
- ICMP:ping 基于 ICMP,用于检测网络连通性,LWIP 开 LWIP_ICMP=1 就能响应 ping。
3. 下层(数据链路层):ARP
- 作用:把 IP 地址转换成 MAC 地址(比如知道 192.168.1.1 的 IP,通过 ARP 查它的 MAC 地址);
- LWIP 自动处理:你不用写代码,LWIP 会维护 ARP 缓存表。
4. 上层(应用层):常用协议(基于 TCP/UDP)
| HTTP | TCP | 网页访问、物联网设备配网 | 单片机做 HTTP 服务器,手机访问 |
| MQTT | TCP | 物联网数据上报(低功耗) | 传感器数据发阿里云 / 腾讯云 |
| Modbus-TCP | TCP | 工业控制(PLC / 仪表通信) | 读取变频器参数 |
| DNS | UDP | 域名解析(比如解析百度 IP) | LWIP 开 LWIP_DNS=1 即可用 |
| DHCP | UDP | 自动获取 IP 地址 | 不用手动配 IP,插上网线自动获取 |
五、新手关键认知
- 要可靠(数据不丢、有序)→ 选 TCP(如文件传输、控制指令);
- 要快 / 实时(允许少量丢包)→ 选 UDP(如传感器数据、音视频);
总结
简单说:TCP 是 “可靠快递员”,UDP 是 “快速快递员”,IP 是 “物流规划师”,MAC/PHY 是 “货车和公路”,你只需要告诉 “快递员” 要寄什么货(应用层数据),剩下的运输环节由底层协议 / 硬件搞定。
给你一张极简清晰表格,一眼看懂它们的关系、用途、底层协议:
| HTTP | 网页 / 接口通信,一问一答 | 浏览器、APP、设备配网、网页控制 | TCP | 应用层 |
| MQTT | 物联网低功耗数据上报(发布 / 订阅) | 传感器上云、远程控制 | TCP | 应用层 |
| Modbus-TCP | 工业设备通信(PLC、仪表、变频器) | 工业控制、单片机连 PLC | TCP | 应用层 |
| DNS | 域名 → IP 解析 | 输入网址自动找到 IP | UDP/TCP | 应用层 |
| DHCP | 自动获取 IP 地址 | 插上网线自动有 IP | UDP | 应用层 |
一句话总关系
HTTP / MQTT / Modbus-TCP / DNS / DHCP都属于应用层,都跑在 TCP/UDP 之上,都由 LWIP 支持。
最终包含关系(从上到下:大 → 小)
【1. 整个网络通信】
↓ 包含
【2. TCP/IP 协议栈】(LWIP 就是实现它)
↓ 包含
【3. TCP 协议】(可靠传输、3次握手/4次挥手)
↓ 依赖(不是包含)
【4. IP 协议】(负责找设备:IP 地址)
↓ 依赖(不是包含)
【5. 以太网】(最底层硬件通路)
包含:MAC、PHY、RGMII、MDIO/MDC、网线、网口
网硕互联帮助中心





评论前必须登录!
注册