Skip to content

LV030-ARP与MAC地址 ​

一、 MAC 地址是什么 ​

1. 48 位与厂商编号 ​

MAC 地址是48 位,写成六组十六进制,中间用冒号或短横线分隔:

text
02 : 00 : 00 : A3 : 5C : 1F
└─────────┬─────────┘
   前 3 字节:OUI(厂商编号)

它和 IP 地址的分工完全不同:

MAC 地址IP 地址
长度48 位32 位(IPv4)
属于哪一层数据链路层网络层
谁分配的厂商(烧在芯片里)或软件设置网络管理员 / DHCP
作用范围一跳之内(每过一个路由器就换)端到端(全程不变)

2. 第一字节里藏着的两个标志位 ​

第一字节最低两位有特殊含义,理解它可以解释本项目的 MAC 为什么长这样:

位含义
bit 0(最低位)0 = 单播,1 = 组播(广播地址 FF:FF:FF:FF:FF:FF 就是全 1)
bit 10 = 全球唯一(厂商分配),1 = 本地管理(软件自定义)

3. 本工程 MAC 的来历 ​

本工程的 MAC 是 02:00:00:XX:XX:XX:首字节 0x02 的二进制是 00000010,最高位(bit 1)为 1 → 本地管理地址,最低位为 0 → 单播。

为什么这么做?因为手头这块板子的 DM9000 里没有烧 MAC,硬编码一个全球唯一的地址反而可能与别的设备冲突。取"本地管理"段,再从芯片的 UID 里取三个字节拼上是更稳妥的做法——前三个字节固定,后三个字节随机,同一批板子里不会重复。

具体生成代码在驱动里:

c
// Drivers/BSP/Ethernet/dm9000.c
uid = *(volatile uint32_t *)0x1FFFF7E8U;   /* 芯片 96 位 UID 的前 32 位 */
mac_addr[0] = 2; mac_addr[1] = 0; mac_addr[2] = 0;   /* 本地管理地址块 */
mac_addr[3] = (uid >> 16) & 0xFF;
mac_addr[4] = (uid >> 8)  & 0xFF;
mac_addr[5] = (uid)       & 0xFF;

串口上那句 lwIP: MAC 02-00-00-XX-XX-XX 打的就是这个地址。

二、 为什么需要 ARP ​

1. 层与层的地址矛盾 ​

把 LV010 与 LV020 连起来看,矛盾就出现了:

  • 网络层寻址用的是 IP 地址(目的 IP 是 192.168.16.30);
  • 链路层组帧用的是 MAC 地址(以太网头里必须填目的 MAC)。

那"已知目的 IP,怎么填目的 MAC?"——这就是 ARP(Address Resolution Protocol,地址解析协议) 要解决的事。

用一个比喻说明它们的关系:IP 像是门牌号(能定位到哪栋楼),MAC 像是这个路口该往哪走(只对这一跳有效)。 每到一个路口都要重新问一次"下一步往哪走",这就是 ARP 在每一跳上要做的事。

2. ARP 的作用范围 ​

ARP 只在同一网段内有意义。跨网段时:

  • 目的 IP 不在本网段,包要交给网关;
  • 于是 ARP 解析的是网关的 IP,而不是最终目的 IP。

所以"ARP 解析出来的一定是目的主机的 MAC"这句话只在同网段时成立,跨网段时解析到的是网关的 MAC。这一点的完整推演见 LV015-网关与跨网段通信 第三节。

三、 请求与应答:一问一答 ​

1. 请求广播、应答单播 ​

过程只有一问一答,但方向不同:

步骤谁发发给谁内容
请求想通信的一方广播(目的 MAC = FF:FF:FF:FF:FF:FF)"谁是 192.168.16.30?请把你的 MAC 告诉 192.168.16.100"
应答被问的那台单播(直接发给提问者)"我是,我的 MAC 是 02:00:00:A3:5C:1F"
ARP 请求与应答,以及 ARP 缓存

2. 两个容易忽略的细节 ​

(1)请求必须广播,因为提问时还不知道对方 MAC —— 这正是"先有鸡还是先有蛋"的那一步,只能靠泛洪解决。

(2)应答是单播。既然请求里已经带了提问者的 MAC,就没必要再广播一次。

(3)网段内所有设备都会收到 ARP 请求,但只有 IP 匹配的那台会回应答,其余设备顺手把请求里的"发送方 IP → 发送方 MAC"记进自己的缓存——这又是"顺便学习"的一次体现。

四、 ARP 报文格式 ​

ARP 报文直接封在以太网帧里,帧类型字段是 0x0806(IP 是 0x0800)。IPv4 over 以太网时,各字段固定:

字段长度取值
硬件类型2 字节1(以太网)
协议类型2 字节0x0800(IPv4)
硬件地址长度1 字节6
协议地址长度1 字节4
操作码2 字节1 = 请求,2 = 应答
发送方 MAC6 字节提问者/应答者的 MAC
发送方 IP4 字节提问者/应答者的 IP
目标 MAC6 字节请求时全 0(还不知道),应答时填对方
目标 IP4 字节要解析的那个 IP

合计 28 字节。这也解释了为什么抓包时 ARP 帧看起来特别短——以太网要求数据部分至少 46 字节,不足的部分靠填充凑到 60 字节(不含前导码),细节见 LV005-数据封装与以太网帧 第二节。

ARP 报文的长度固定、字段固定,这意味着它不是"可变长协议",没有长度字段可以解析。所以协议栈识别它的依据只能是"以太网类型 = 0x0806"。

五、 ARP 缓存与本项目的落地 ​

1. 缓存与老化 ​

如果每发一个包都要先广播一次 ARP,网络会被广播刷满。所以每台设备都维护一张 ARP 缓存表(IP → MAC 的映射),带老化时间,过期就重新解析。

查表的命令:

系统命令
Windowsarp -a
Linuxip neigh
删掉某条(Windows)arp -d 192.168.16.30

排错时这条命令很有用:能看到"是不是学到了错误的 MAC",以及"到底有没有学到"。

2. 在本项目里对应哪些代码 ​

环节位置
lwIP 的 ARP 实现Components/lwip-2.1.3/src/core/ipv4/etharp.c
收包后按帧类型分流(0x0806 给 ARP、0x0800 给 IP)src/core/ipv4/ethernet.c → ethernet_input()
网卡收到帧后交给协议栈port/ethernetif.c 的 ethernetif_input()
ARP 表的老化由 sys_check_timeouts() 驱动,所以 1 ms 节拍必须稳定
发送时解析目的 MACetharp_output()(在 ethernetif_init() 里挂成 netif->output)

有一处排错经验值得记:ARP 表老化是靠 sys_check_timeouts() 推进的。如果主循环某处堵住几百毫秒(比如在 TCP 回调里打印日志、算 CRC、写 Flash),ARP 表老化与 TCP 重传都会跟着抖——这也是 移植那篇 强调"回调里只做 O(1) 搬运"的原因。

3. 两种常见变体 ​

名称做什么用在哪
免费 ARP(Gratuitous ARP)主动广播自己的"IP → MAC"映射设备刚上线时通告自己;也可用来检测 IP 冲突
代理 ARP(Proxy ARP)路由器替另一个网段的设备回答 ARP让本网段设备误以为对方就在本地,由路由器代收转发

第二种在嵌入式调试里偶尔会碰到:某些网关出厂就开了代理 ARP,于是"跨网段的设备看起来像在同一网段",抓包时容易看不懂。遇到这种情况对着 arp -a 的输出看一眼就能确认。

六、 小结与参考资料 ​

1. 本篇小结 ​

  • MAC 是链路层的地址,只在一跳内有效;首字节最低两位分别表示"单播/组播"与"全球唯一/本地管理"。
  • 本工程的 MAC 取本地管理段 02:00:00,后三字节来自芯片 UID,由驱动运行时生成并写入 PAR。
  • ARP 解决的是"已知 IP,求 MAC",只在同一网段内有意义;跨网段时解析的是网关的 MAC。
  • 请求用广播、应答用单播;结果是"谁看到谁记得",双方都会更新缓存。
  • ARP 报文固定 28 字节,没有长度字段,靠以太网类型 0x0806 识别。
  • 本工程的 ARP 由 etharp.c 实现,老化依赖 sys_check_timeouts() 的稳定节拍。

2. 参考资料 ​


本文档由 markdowncli 技能辅助生成