把万兆链路的物理层眼图调开、64b/66b 编码对齐稳定、MAC 收发通道跑通之后,很多人的第一反应是立刻挂上 UDP 或者 TCP 的业务流,结果遇到一个很别扭的局面:链路指示灯是绿的,收发统计计数器在涨,可业务就是不通。这时候把抓包工具挂上去,往往只能看到一个设备在反复广播"谁是 192.168.1.10,请告诉 192.168.1.20",却始终等不到答复。原因很朴素——ARP 协议报文格式这一层没做,或者做了但字段填错了。在万兆以太网MAC的设计里,MAC 层只认 48 位物理地址,它根本不知道什么叫 IP 地址,把 IP 包封装成以太网帧这件事,必须有人在中间完成地址翻译。这个中间人就是ARP层模块。
这篇内容聚焦的就是这个中间人:从 28 字节 ARP PDU 的每一个比特怎么摆,到接收解析状态机怎么切拍、应答怎么组帧、表项怎么老化,再到联调阶段抓包抓出来的几类典型异常。整套逻辑只涉及以太网和 IPv4 的最基础约定,不依赖任何私有协议。适合已经能把万兆 MAC 收发跑通、正要往上接网络层的中级开发者,也适合刚接触 FPGA 网络加速、想搞清楚"为什么 ping 不通"的初学者。下面按设计顺序一层层展开。
1. 万兆MAC之上为什么还要单独做一层ARP
1.1 链路通但业务不通的典型现象
先描述一个几乎每个做网络加速的人都会遇到的场景。板子上电,自协商完成,光模块的 LOS 灯灭掉,MAC 的 rx_valid 开始有翻转,统计寄存器里收到的帧计数缓慢增长。心里一喜,觉得大功告成。接着把 PC 的网线插到对端,配置好同一网段的 IP,敲一个 ping 命令,返回的是"目标主机不可达"或者干脆超时。此时在 PC 上执行arp -a,会发现目标 IP 对应的 MAC 地址一栏是空的,说明 ARP 请求发出去之后没有收到应答。
这个现象背后的机制非常直接:ping 走的是 ICMP,ICMP 要装进 IP 包,IP 包要装进以太网帧,而以太网帧头部的目的 MAC 地址从哪来?只能从 ARP 表里查。表里没有,就要先发一个 ARP 请求去问,问不到,IP 包就压根发不出去。所以万兆以太网MAC设计得再漂亮,只要 ARP 层缺位,上层任何基于 IP 的业务都启动不了。这个因果关系不是可选项,而是硬约束。
更麻烦的是万兆场景下的一些特殊性。10Gbps 的线速意味着最小 64 字节的帧间隔只有 67.2 纳秒,ARP 请求本身是广播帧,很多为了省资源而裁剪过的接收通路只做了单播地址过滤,广播帧直接被丢弃。所以第一件要检查的事往往不是状态机写没写对,而是 MAC 的接收过滤配置里,广播地址有没有被放行。这一点在后面第 6 章还会展开说。
1.2 硬件自动应答与软件参与的边界划分
确定要做 ARP 之后,下一个问题是在哪儿做。常见的有三种思路,各有适用面。
第一种是纯软件方案:MAC 收到 ARP 帧不做任何解析,原样通过 DMA 送进主机内存,由协议栈解析后生成应答再下发。这种方案实现门槛最低,逻辑侧几乎不用改,代价是延迟大、占用主机 CPU、而且必须有主机参与。对于纯逻辑的数据通路,比如一个只有 FPGA 没有处理器的采集卡,这条路直接堵死。
第二种是硬件全自动方案:ARP 层模块内部维护一张小表,收到请求后自己判断目标 IP 是不是本机,是就立刻组应答帧从发送通道打回去,全程不惊动主机。这条路的延迟可以压到微秒级甚至更低,资源开销也不大,一张 32 项的表加上解析状态机,在主流 FPGA 上占用的逻辑资源相当有限。
第三种是混合方案:常规请求由硬件自动应答,同时把学习到的 IP-MAC 映射通过一个旁路通道上送软件,软件可以下发静态表项、调整老化时间、或者做更复杂的策略。实际项目里用得最多的是这一种。
我在一个纯逻辑的采集项目里选的是第二种,理由是链路上只有一个固定的对端,地址映射不超过四条,硬件自动应答足够,还省掉了软硬件交互的握手协议和随之而来的一堆时序问题。选型的判断标准其实就一条:地址映射是否需要动态变化、是否需要策略控制。固定拓扑就硬件全自动,动态拓扑就上混合方案,不要为了"通用"在一开始就把复杂度拉满。
2. 28字节ARP PDU的逐字段拆解
2.1 以太网封装与长度账的完整算法
先把帧结构摆清楚。ARP 报文并不是独立存在的,它作为以太网帧的载荷出现,前面套了一层 14 字节的以太网帧头。完整结构从线序来看是这样的:
| 层次 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 以太网头 | 目的 MAC | 6 | 广播时为 FF:FF:FF:FF:FF:FF |
| 以太网头 | 源 MAC | 6 | 请求方的物理地址 |
| 以太网头 | 类型 | 2 | 固定 0x0806,标识上层是 ARP |
| ARP PDU | 硬件类型 | 2 | 以太网填 0x0001 |
| ARP PDU | 协议类型 | 2 | IPv4 填 0x0800 |
| ARP PDU | 硬件地址长度 | 1 | 填 6 |
| ARP PDU | 协议地址长度 | 1 | 填 4 |
| ARP PDU | 操作码 | 2 | 1 为请求,2 为应答 |
| ARP PDU | 发送方硬件地址 | 6 | 请求方的 MAC |
| ARP PDU | 发送方协议地址 | 4 | 请求方的 IP |
| ARP PDU | 目标硬件地址 | 6 | 请求时全 0,应答时填对端 MAC |
| ARP PDU | 目标协议地址 | 4 | 被询问或被回复的 IP |
| 填充 | Padding | 18 | 补齐到最小帧长 |
| 尾部 | FCS | 4 | CRC32 校验 |
长度账要算两遍。第一遍算有效载荷:以太网头 14 字节加上 ARP PDU 28 字节,合计 42 字节。第二遍对照最小帧长要求:以太网规定从目的 MAC 到 FCS 之前的最小长度是 60 字节,含 FCS 是 64 字节。42 字节明显不够,需要补 18 字节的填充,补完之后 42 加 18 正好 60,再加上 4 字节 FCS 就是 64。
这个 18 字节的填充量是固定的,因为 ARP PDU 的长度是固定的 28 字节,不像 IP 那样长度可变。很多实现里图省事直接不补,短帧发出去之后,对端的 MAC 可能因为长度不足直接丢帧,或者在交换机侧被当成残帧处理。这个坑我踩过一次,现象是抓包工具能看到请求,但应答帧在交换机端口就被丢了,因为交换机对 42 字节的帧判定为 runt。
2.2 字段取值与字节序的双重陷阱
字段本身的取值不难,难的是字节序。ARP 协议规定所有多字节字段采用网络字节序,也就是大端,高字节在前。而我们在 FPGA 内部处理 IP 地址时,习惯性地会把它当成一个 32 位无符号数存在寄存器里,比如 192.168.1.10 存成32'hC0A8010A。这个寄存器在线序上第一个字节应该发0xC0,也就是取[31:24]。
最容易出错的地方是 6 字节的 MAC 地址。在 Verilog 里通常写成48'h02_00_00_00_00_01,这个写法里[47:40]是0x02,是线序上的第一个字节。但如果你在做比较的时候误用了[7:0]那一端,比较逻辑写反了,功能仿真可能因为两边的 MAC 正好对称而通过,上板就完蛋。写代码时我养成一个习惯,凡是 6 字节地址,统一在命名上标注清楚,比如local_mac_hi对应[47:40],避免后面接手的人误读。
还有两个容易翻车的取值细节。一是硬件类型字段,虽然以太网填 1,但有些老实现会填 6(对应 IEEE 802 网络),接收侧如果判等只认 1,遇到填 6 的请求就不应答。稳妥的做法是接收侧不强校验硬件类型和长度字段,只校验协议类型和操作码,发送侧老老实实填 1、0x0800、6、4。二是操作码,值是 1 和 2,占 2 字节,但实际只有低字节有效,高字节为 0。如果按字节流逐字节比较,需要比较两个字节;如果按 16 位字比较,直接比16'h0001和16'h0002。
2.3 请求与应答的字段镜像关系
请求和应答的字段差异是设计应答组装逻辑的直接依据,这张对照表建议直接对着写代码:
| 字段 | ARP 请求 | ARP 应答 |
|---|---|---|
| 目的 MAC | FF:FF:FF:FF:FF:FF | 请求方的源 MAC |
| 源 MAC | 本机 MAC | 本机 MAC |
| 类型 | 0x0806 | 0x0806 |
| 硬件类型 | 1 | 1 |
| 协议类型 | 0x0800 | 0x0800 |
| 硬件地址长度 | 6 | 6 |
| 协议地址长度 | 4 | 4 |
| 操作码 | 1 | 2 |
| 发送方硬件地址 | 本机 MAC | 本机 MAC |
| 发送方协议地址 | 本机 IP | 本机 IP |
| 目标硬件地址 | 全 0(未知) | 请求方的源 MAC |
| 目标协议地址 | 被询问的 IP | 请求方的源 IP |
看出规律了吗?应答帧就是把请求帧里的两对地址做了一次交换,同时把操作码改成 2,把目的 MAC 从广播改成请求方的单播地址。所以组帧逻辑可以写得非常简洁:把接收到的 28 字节 PDU 缓存下来,应答时按偏移取字段,SHA 改成自己的,SPA 改成自己的,THA 取缓存里的 SHA,TPA 取缓存里的 SPA,操作码覆写成 2。
这里有个小细节值得注意:请求帧里目标硬件地址是全 0,如果直接原样交换,应答帧的 THA 会变成 0,那就错了。所以交换的对象不是"目标硬件地址"字段,而是"发送方硬件地址"字段,这两个不一样。写代码时如果图省事做了字段平移,很容易把这个坑埋进去,而且仿真用例如果只测字段全 0 的场景,根本发现不了。
3. ARP层模块的接口约定与状态机组织
3.1 与MAC通道的握手约定与跨拍分割
10G MAC 的数据通路典型配置是 64 位数据宽度配 156.25MHz 时钟,一拍承载 8 个字节,线速正好对得上。接口用标准的流式握手,四个信号:数据tdata、字节有效掩码tkeep、帧尾标志tlast、数据有效tvalid,配一个反压tready。这套约定在业界是通用的,不同 IP 的差异主要在字节序映射上。
关键问题是 42 字节怎么落在 64 位总线上。42 除以 8 等于 5 余 2,也就是需要 6 拍,前 5 拍满,第 6 拍只有 2 个字节有效。逐拍映射关系如下,这张表是整个接收状态机的基础:
| 拍号 | 字节范围 | 承载内容 | tkeep |
|---|---|---|---|
| 0 | 0-7 | 目的 MAC + 源 MAC 前 2 字节 | 0xFF |
| 1 | 8-15 | 源 MAC 后 4 字节 + 类型 + 硬件类型 | 0xFF |
| 2 | 16-23 | 协议类型 + 两个长度 + 操作码 + 发送方 MAC 前 2 字节 | 0xFF |
| 3 | 24-31 | 发送方 MAC 后 4 字节 + 发送方 IP | 0xFF |
| 4 | 32-39 | 目标 MAC + 目标 IP 前 2 字节 | 0xFF |
| 5 | 40-47 | 目标 IP 后 2 字节 + 填充起始 | 视情况 |
第 5 拍比较特殊。如果对端发来的是标准 60 字节最小帧,那么第 5 拍之后还有 2 拍填充数据;如果对端直接发了 42 字节的短帧(不规范但确实存在),那么第 5 拍的tkeep就是0x03,而且tlast会在这一拍拉高。这两种情况都要能处理,不能假定填充一定存在。
注意:不要把"以太网帧一定是 64 字节"当成硬假设写进状态机。虽然标准要求如此,但实际抓到过的短帧不少,处理不了短帧的实现会在联调时莫名其妙地卡在某个状态出不来。
3.2 接收解析状态机的状态划分
状态机建议按"帧头识别—载荷缓存—判定—触发"四段来切,不要试图一拍做完所有事。万兆下时序很紧,组合逻辑链路一长,时序收敛就是噩梦。
先定义状态:
localparam S_IDLE = 3'd0; // 等待帧起始 localparam S_HEAD = 3'd1; // 收以太网头,判地址与类型 localparam S_PAYLOAD= 3'd2; // 缓存 28 字节 ARP PDU localparam S_JUDGE = 3'd3; // 判操作码与目标 IP localparam S_HIT = 3'd4; // 命中,置应答请求 localparam S_DROP = 3'd5; // 丢弃,等帧尾S_IDLE里等tvalid && tready同时有效,进来之后进S_HEAD。S_HEAD处理前两拍,第一拍一次性拿到目的 MAC 和源 MAC 的前两个字节,此时就能完成地址过滤:目的 MAC 是全 F 的广播地址,或者是本机 MAC,才继续往下走,否则直接跳S_DROP。第二拍拿到类型字段,判断是不是0x0806,不是也跳S_DROP。
S_PAYLOAD处理第 2 到第 5 拍,把 28 字节 PDU 的字段按偏移拆出来存到寄存器里。这里不需要真的存 28 字节,只存后面会用到的:发送方 MAC 6 字节、发送方 IP 4 字节、目标 IP 4 字节、操作码 2 字节,总共 16 字节,省资源。拆分的时候按上表的偏移去截取对应的位段,注意每一拍里字段是跨字节段的,比如第 2 拍里操作码在[39:24]这个位置,具体取决于你的字节序映射约定,写之前一定要拿 MAC IP 的文档核对清楚第一拍里tdata[7:0]到底对应线序的哪个字节。
S_JUDGE只做两件事:操作码是不是 1,目标 IP 是不是本机 IP。两个条件同时满足才进S_HIT,否则跳S_DROP。这个状态独立出来是有讲究的,如果把判断和缓存揉在一个状态里,组合逻辑会拉得很长,在 156.25MHz 下大概率时序不过。
S_DROP的作用是一直等到tlast拉高,把整个帧消化完再回S_IDLE。少了这个状态,帧还没结束就回空闲态,后面的填充数据会被误当成新帧的开头,直接导致状态机错乱。
3.3 应答组装流水线与发送仲裁
应答帧的组帧相对简单,因为所有字段都是已知的或者从缓存里取。但要注意组帧和发送之间需要一个 FIFO 或者至少一个深度可控的缓冲,原因是发送通道可能正被业务数据占用,ARP 应答不能等。
组帧的顺序按线序走:先填第 0 拍的目的 MAC(来自缓存的发送方 MAC)和源 MAC 前 2 字节,第 1 拍填源 MAC 后 4 字节加类型和硬件类型,第 2 拍填协议类型、两个长度、操作码 2、本机 MAC 前 2 字节,第 3 拍填本机 MAC 后 4 字节和本机 IP,第 4 拍填请求方 MAC 和目标 IP 的前 2 字节,第 5 拍填目标 IP 后 2 字节和 6 个字节的零填充。第 6、7 拍补 12 个零字节,凑满 60 字节,tlast在第 7 拍拉高。
发送仲裁的策略要提前想清楚。最简单的是 ARP 应答优先,业务数据让路,实现方式是给 ARP 应答一个高优先级请求,业务侧看到这个请求就暂停一两个拍。这种策略的代价是业务流会有一个微小的抖动,但 ARP 帧极其稀疏,实际影响可以忽略。另一种是完全插空发送,等业务帧之间的间隙,实现复杂但业务无损。我在项目里用的是前一种,因为在万兆下 ARP 帧的出现频率低到可以忽略不计,为它设计复杂的调度器不划算。
3.4 ARP缓存表的组织方式与查找时序
表的核心是 IP 到 MAC 的映射,外加有效位和老化信息。表项结构大概是这样:
| 字段 | 位宽 | 说明 |
|---|---|---|
| ip_addr | 32 | 协议地址 |
| mac_addr | 48 | 硬件地址 |
| valid | 1 | 表项有效标志 |
| age_cnt | 16 | 老化计数,递减到 0 失效 |
加起来 97 位,一张 32 项的表大约 3104 位,用一块小容量的块存储或者分布式存储都能装下,资源压力很小。
查找方式有两种,选哪种取决于表的大小和查找频率。表项少于 16 条时,直接用并行比较,把所有表项的 IP 同时和待查 IP 比对,一拍出结果,逻辑资源大概几百个查找表,完全可以接受。表项超过 32 条时,并行比较的资源开销开始变得不划算,改用哈希加链式查找,或者干脆用内容寻址存储器的思路。
有个时序上的坑需要提醒:ARP 表的查找通常发生在发送 IP 包之前,而万兆下帧与帧之间的间隔可能只有几个拍。如果查找需要多个周期,就必须在帧开始发送之前预查到结果,或者接受一个小的发送延迟。我的做法是在业务侧维护一个小的待发队列,在出队之前就完成地址查找,把查找延迟藏在排队时间里。这个设计变更看起来不起眼,但它是把发送通路时序从"查找拖慢发送"变成"查找与发送并行"的关键。
老化机制相对简单,一个自由运行的定时器每秒钟产生一个脉冲,所有表项的age_cnt递减,减到 0 就把valid清零。老化时间一般设 60 到 300 秒,具体看网络规模。太短会导致频繁重新请求,太长会导致地址变更后长时间不通。
4. 组帧、FCS与最小帧长的实操处理
4.1 FCS由谁负责这一层责任划分
FCS 是 4 字节的 CRC32 校验,覆盖范围从目的 MAC 第一个字节到载荷最后一个字节,不含前导码、帧起始定界符和 FCS 本身。这里有个责任划分问题:FCS 到底由 ARP 层算,还是由 MAC 层算?
绝大多数 10G MAC IP 都提供 FCS 自动插入功能,配置寄存器里有一个使能位,打开之后 MAC 自己算 CRC 并附加,上层只需要把帧的载荷送到tlast拉高就行。这种情况下 ARP 层完全不用管 FCS,省事很多。但也有一部分精简实现不提供这个功能,或者提供了但需要软件在发送前写入一个长度字段,那就得自己算。
自己算 CRC32 的话,多项式是0x04C11DB7,反射形式是0xEDB88320,初值全 1,最后结果取反。硬件实现用 8 位并行的查表或者直接展开的异或树,一个 64 位数据通路的 CRC 单元大概需要几十个异或门,资源不大。关键的坑在于计算范围必须包含填充字节,很多人算到 42 字节就截止了,补的 18 个零没有参与计算,结果对端校验失败,帧被静默丢弃,抓包工具上连个影子都看不到,因为坏帧在物理层就被扔了。
提示:判断 FCS 该不该自己算,最快的办法是看 MAC IP 的发送接口有没有
tx_fcs_en之类的控制位,有就交给 MAC,没有就自己算。不要在文档不全的情况下猜。
4.2 填充字节填什么与长度字段的关系
填充的 18 个字节填什么值,标准没有硬性规定,填 0 是最常见的做法,也有些实现对填充区填入随机数据,目的是防止某些场景下的数据压缩侧信道问题,那是安全领域的考量,跟常规网络设备无关。做 FPGA 实现时一律填 0 就行。
但有一个关联问题必须说清楚:以太网帧头里的类型字段是0x0806,这是一个大于0x0600的值,按照标准,大于这个值表示"上层协议类型",小于这个值才表示"载荷长度"。所以 ARP 帧走的是类型字段这条路,接收方靠类型字段判断载荷是 ARP,而不是靠长度字段推算载荷在哪结束。这意味着填充字节的内容不会被误解析,因为解析器在读完 28 字节 PDU 之后就不再看了。
如果哪天要做的是 802.3 长度字段格式的帧,那填充字节的边界就必须精确计算,因为接收方会用长度字段去界定载荷范围,填充区一旦填了非零值又落在长度范围里,就会被当成有效载荷的一部分。这两种封装格式的差别在实现时要区分清楚,别混用。
4.3 组帧时序与反压处理
组帧的时序要跟发送通道的反压配合好。发送通道tready拉低时,组帧状态机必须停住不动,tdata、tkeep、tlast保持上一次的值,不能因为内部计数器还在跑就把数据丢了。这一点在流式接口里是硬性要求,违反了会导致对端收到的数据和tkeep不匹配。
一个常见的写法错误是把组帧和数据发送写成一个状态机,用计数器推进。计数器在反压期间如果没被冻结,就会继续加,等tready恢复时数据已经错位了。正确做法是让计数器只在tvalid && tready同时为真的那一拍才递增,或者把组帧和数据发送拆成两个阶段,中间用一个小 FIFO 解耦。
拆成两阶段的好处还有一个:组帧阶段可以做得慢一点,只要平均吞吐跟得上就行,而发送阶段可以专门优化时序。ARP 应答的组帧频率极低,完全没必要为了它把时序做到极限。
5. 联调阶段的问题定位
5.1 抓包工具能给出的有效信息
联调时最有效的手段是抓包。主流协议分析软件对 ARP 的解码非常完善,会直接把字段翻译成人话,比如"Who has 192.168.1.10? Tell 192.168.1.20"。看到这一行,至少说明三层信息:请求发出去了、类型字段对、发送方 IP 填对了。
抓包时要重点看几个地方。一是应答帧有没有出现。如果只有请求没有应答,问题在接收侧或者判定条件。二是应答帧的字段。软件会把应答显示成"192.168.1.10 is at xx:xx:xx:xx:xx:xx",如果这个 MAC 显示出来跟你板子上的不一致,说明组帧时字段取错了位置。三是帧长度。软件会标出帧的总长度,如果显示 42 字节而不是 60 字节,说明填充没做。
还有一类问题抓包工具帮不上忙,就是帧压根没发到线上。这时候要看 MAC 的发送统计寄存器,有没有丢帧计数在涨,有没有 undersize 错误。有些 MAC 会把小于最小帧长的帧直接丢弃并计数,这个计数器是定位填充问题的最快入口。
5.2 仿真环境里绕不过去的测试用例
上板之前,仿真里至少要把这几类用例跑一遍,能省掉大量来回折腾的时间。
第一类是标准请求应答:对端发广播请求,目标 IP 命中本机,检查应答帧每个字段是否与第 2.3 节的对照表一致。这个用例要逐字节比对,不能只看"有应答输出"。
第二类是目标 IP 不命中:发一个询问别人 IP 的请求,检查模块没有产生应答。这个用例专门抓"忘了判断目标 IP"这个经典错误,很多实现收到任何 ARP 请求都回,看上去能用,实际上会在网络上制造混乱。
第三类是短帧:强制发 42 字节不填充的请求,检查模块能正常解析并产生应答,状态机不卡死。
第四类是背靠背请求:连续发三个请求,间隔很小,检查应答是否全部正确产生,缓冲有没有溢出。
第五类是操作码异常:发一个操作码为 3 的帧,检查被正确忽略。
第六类是类型字段异常:发一个0x0800的 IP 帧,检查不会被误判成 ARP。
我在实际项目里发现最有价值的是第二类和第六类,因为这两类错误在功能演示时完全看不出来,只有多设备共存或者混合流量时才会暴露,而那时候排查成本已经很高了。
5.3 上板之后几类异常的分层排查思路
上板之后如果还是不通,按层次往下找,别一上来就怀疑状态机。
第一步看物理层和链路层,MAC 的接收计数器有没有在涨。不涨,问题在物理层或者 MAC 的接收使能配置。涨了但 ARP 没反应,进第二步。
第二步看接收过滤。很多 MAC IP 默认只接收目的地址等于本机 MAC 的帧,广播帧需要单独使能。如果广播过滤没打开,ARP 请求压根进不了 ARP 层,这时候在 ARP 模块的输入端口上加一个计数器,看有没有数据进来,一目了然。
第三步看判定条件。把目标 IP 比较的结果、操作码比较的结果各拉一个调试信号出来,看看命中的时候这两个信号是不是同时为高。很多时候是 IP 地址的字节序搞反了,明明是本机 IP,比较却不等。
第四步看发送通路。把应答帧的组帧完成信号和实际发送完成信号都拉出来,看组帧完成了但发送没发出去,还是压根没组帧。如果是前者,去看发送仲裁和 FIFO 状态。
这套分层排查的顺序不是拍脑袋定的,它是按"信号从远端到近端"的顺序排的,能保证每次只往下推一层,不会在多处同时改东西导致问题定位失真。
6. 容易被忽略的几个工程细节
6.1 广播与组播过滤的配置差异
前面提过广播帧被过滤的问题,这里展开说清楚。MAC 层的地址过滤通常有几档:单播命中本机地址、广播全 F、组播地址范围。ARP 请求走的是广播,所以广播过滤必须打开。有些 IP 的过滤配置是分层的,先过一遍哈希过滤再过精确匹配,广播地址能不能通过哈希那一关取决于具体实现,需要看手册确认。
还有一个容易忽略的地方是混杂模式。调试阶段把 MAC 配成混杂模式能收到所有帧,方便定位,但正式运行时不能开,否则所有经过交换机的帧都会被当成自己的,ARP 层会被大量无关请求淹没,应答风暴随之而来。
6.2 复位后的初始状态与表项清空
复位之后 ARP 表必须是全无效状态,不能有残留。这在仿真里很少出问题,因为仿真初始值通常是 X 或者 0,但上板后如果用的是块存储,上电初值是不确定的,如果valid位恰好是 1,就会拿着垃圾地址去应答或者去封装。
稳妥的做法是在复位释放之后用一个短状态机把所有表项的valid位逐条清一遍,几十个周期的事,代价极小。这一步在项目里千万别省,我见过因为省这一步导致上电后前几百毫秒网络行为诡异的案例,排查了大半天才发现是存储初值问题。
6.3 应答风暴与请求限速
最后说一个理论上不常见但实际会遇到的场景:如果网络上有个设备在疯狂发 ARP 请求,或者有人用工具做地址扫描,ARP 层的应答频率会突然升高。如果应答通路没有限速,大量应答帧会挤占业务带宽,严重的会导致发送 FIFO 溢出、业务帧丢失。
处理办法很简单,在应答触发路径上加一个简单的漏桶:每 N 个微秒最多产生一次应答,超出的请求直接丢弃。ARP 协议本身有重传机制,丢一两个请求不会导致通信失败,对端会重发。这个限速阈值设多少合适?按经验,正常网络里 ARP 请求的频率极低,每秒几十个已经算多了,把限速设成每毫秒一次就足够覆盖正常场景,同时把异常流量压制住。
再补充一个跟表项有关的经验:如果项目里对端地址是固定的,与其让模块动态学习,不如在复位之后直接把固定映射写进表里,并把这些表项的valid位锁死,不参与老化。这样一来,即使链路上出现过短暂的 ARP 风暴或者异常帧,固定映射也不会被冲掉,通信的确定性会好很多。我在一个需要长时间无人值守运行的项目里就是这么做的,运行几个月没有出现过地址失效导致的通信中断。