LV030-ARP与MAC地址
一、 MAC 地址是什么
1. 48 位与厂商编号
MAC 地址是48 位,写成六组十六进制,中间用冒号或短横线分隔:
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 1 | 0 = 全球唯一(厂商分配),1 = 本地管理(软件自定义) |
3. 本工程 MAC 的来历
本工程的 MAC 是 02:00:00:XX:XX:XX:首字节 0x02 的二进制是 00000010,最高位(bit 1)为 1 → 本地管理地址,最低位为 0 → 单播。
为什么这么做?因为手头这块板子的 DM9000 里没有烧 MAC,硬编码一个全球唯一的地址反而可能与别的设备冲突。取"本地管理"段,再从芯片的 UID 里取三个字节拼上是更稳妥的做法——前三个字节固定,后三个字节随机,同一批板子里不会重复。
具体生成代码在驱动里:
// 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. 层与层的地址矛盾
- 网络层寻址用的是 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" |
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 = 应答 |
| 发送方 MAC | 6 字节 | 提问者/应答者的 MAC |
| 发送方 IP | 4 字节 | 提问者/应答者的 IP |
| 目标 MAC | 6 字节 | 请求时全 0(还不知道),应答时填对方 |
| 目标 IP | 4 字节 | 要解析的那个 IP |
合计 28 字节。这也解释了为什么抓包时 ARP 帧看起来特别短——以太网要求数据部分至少 46 字节,不足的部分靠填充凑到 60 字节(不含前导码),细节见 LV005-数据封装与以太网帧 第二节。
ARP 报文的长度固定、字段固定,这意味着它不是"可变长协议",没有长度字段可以解析。所以协议栈识别它的依据只能是"以太网类型 = 0x0806"。
五、 ARP 缓存与本项目的落地
1. 缓存与老化
如果每发一个包都要先广播一次 ARP,网络会被广播刷满。所以每台设备都维护一张 ARP 缓存表(IP → MAC 的映射),带老化时间,过期就重新解析。
查表的命令:
| 系统 | 命令 |
|---|---|
| Windows | arp -a |
| Linux | ip 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 节拍必须稳定 |
| 发送时解析目的 MAC | etharp_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. 参考资料
- lwIP 的 ARP 实现:
etharp.c - 网卡接口层:
ethernetif.c - 平台时基:
sys_arch.c - 手册:
STM32F1 LWIP开发手册(DM9000版)_V1.1.pdf第三章
本文档由 markdowncli 技能辅助生成