ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OPNET 802.11 MAC源码解析:DCF退避、帧交互与二次开发

OPNET 802.11 MAC源码解析:DCF退避、帧交互与二次开发

简介:这份压缩包面向无线网络协议学习与OPNET仿真实践者,提供802.11 MAC层在OPNET环境中的源代码与工程文件,可用于理解CSMA/CA、DCF、帧结构及信道接入等机制,并借助仿真观察不同场景下的性能表现。包内共356个文件,覆盖ov、os、m等模型与脚本文件,c、pr.c等源码,以及obj、lib、dll、exp、log等工程编译与运行产物,另有少量ac、prj等配置与说明文件,整体约1.31MB,目录按仿真工程组织,便于按模块查阅。已有877人学习,适合作为无线局域网课程实验、协议分析与二次开发的参考资料。读者可结合源码梳理MAC层实现细节,对照仿真模型调试参数,积累OPNET建模与协议排错经验。

1. 从一条不降反升的吞吐曲线说起:这套 OPNET 802.11 MAC 源代码到底能干什么

去年帮人看一个无线组网场景,节点从 5 个加到 20 个,吞吐先升后断崖,重跑十几次结果还不一样。路由、干扰、天线增益全查过,最后把 MAC 层的退避窗口打出来才发现,竞争窗口上限被写死在了一个很小的值上。这件事让我形成一个习惯:做 802.11 仿真,MAC 层必须拿到源代码,黑盒模型只能看结果不能解释结果。这套 OPNET 802.11 MAC 协议源代码给的就是可直接编译运行的进程模型、状态机、DCF 帧交换逻辑和统计量采集代码,节点模型、包流接口、属性表都是完整的一套。适合三类人:要拿标准 DCF 做 baseline 对比的、想把退避算法换掉验证自己思路的、以及被仿真结果反复打脸想搞清楚每一帧到底怎么走的人。下面按“这堆文件是什么 → 一帧怎么走完 → 怎么跑起来 → 哪里容易翻车 → 怎么改”的顺序拆。

2. 模型骨架拆解:一套 OPNET 无线 MAC 源码由哪些文件拼起来

拿到一个 OPNET 工程,第一件事不是点运行,而是先搞清楚哪些文件是“模型定义”、哪些是“模型实现”、哪些是“场景数据”。这三类东西混在一起看,改错一个地方编译能过但结果全错,这种坑很难回头查。

2.1 工程目录与文件类型的职责划分

OPNET 的模型文件基本都是文本(少数是二进制导出格式),这点很关键——意味着可以塞进版本库、可以 diff、可以两个人同时改不同模块。典型的无线 MAC 模型目录长这样:

wlan_mac_project/ ├── wlan_mac.prj # 工程文件,记录模型库引用与编译配置 ├── station_node.nd.m # 节点模型:MAC/队列/发信机的连接拓扑 ├── mac_process.pr.m # 进程模型:状态机定义 + 状态变量声明 ├── mac_support.c # 进程代码:退避、帧构造、队列管理 ├── mac_defs.h # 常量与结构体:帧格式、时序常量、状态枚举 ├── wireless_scene.scenario # 场景:拓扑、业务、统计量开关 └── results/ # 仿真输出(.os 标量 / .ov 矢量)

各文件职责对应得很清楚,改代码之前先记住这条边界:

文件类型管什么改它的典型场景
.pr.m状态机的状态、转移条件、进入/离开动作新增一个等待状态、加一个中断分支
.c真正的逻辑:算退避、判信道、组帧换退避算法、改重传策略
.h帧结构、常量、枚举加字段、改时序常量
.nd.m模块之间怎么连、属性怎么提升加一条统计线、改包流方向
.scenario参数取值、业务分布、统计量开关改节点数、改发包速率

提示:先把工程复制一份再动手。OPNET 的模型库引用是相对路径,原地改完再想回退很麻烦。

2.2 节点模型里的包流:MAC 模块的五个接口

MAC 进程不是孤立的,它在节点模型里被四五个模块夹着。理解包流方向比读代码更快建立直觉。常见连接方式是:

  • 从高层(IP/ARP/应用)下来一条包流,进 MAC 的FROM_HIGHER流索引;
  • MAC 往高层送一条,走TO_HIGHER;
  • 从无线发信机上来一条,走FROM_PHY;
  • 往发信机下去一条,走TO_PHY;
  • 如果做了独立队列模块,还会多一条FROM_QUEUE/TO_QUEUE。

流索引在代码里是硬编码的数字,op_pk_get(0)、op_pk_send(pkptr, 1)这种写法到处都是。新手最容易犯的错是节点模型里加了一条线,代码里的流索引没跟着改,编译不报错,运行起来包从错误的端口出去了,表现为“高层收不到包”或者“PHY 收到一个空包”。

/* MAC 进程的流索引定义,放在 mac_defs.h 里统一管理 */ #define MAC_STRM_FROM_HIGHER 0 /* 高层下来的数据帧 */ #define MAC_STRM_FROM_PHY 1 /* 空口收到的帧 */ #define MAC_STRM_TO_HIGHER 0 /* 上交高层的帧 */ #define MAC_STRM_TO_PHY 1 /* 发往空口的帧 */

用宏代替裸数字,改接口时只改一处。参数说明上,op_pk_get()的参数就是流索引,op_pk_send()的第二个参数是输出流索引——注意输入和输出的索引是两套独立的编号,别混用。

2.3 进程模型的状态机骨架

802.11 MAC 的进程模型通常围绕一个显式的状态机展开,状态数量在六到十个之间。典型状态划分如下:

状态含义主要出口
IDLE空闲,等中断收到包 → 判信道;超时 → 退避
DEFER信道忙,挂起等待信道空闲中断 → 进入退避
BACKOFF倒计时退避中退避到 0 → 发送;信道忙 → 冻结
WAIT_ACK已发送,等 ACKACK 到达 → 成功;超时 → 重传
WAIT_CTSRTS 已发,等 CTSCTS 到达 → 发数据;超时 → 退避
RECEIVE正在收一个帧收完 → 校验 → 上交或丢弃

状态之间的转移条件写在.pr.m里,而“到了这个状态具体干什么”写在.c的进入函数和中断处理函数里。这套源码的组织方式就是这种分离式——状态机只负责“在哪”,实现函数负责“做什么”。

/* 退避计时器的状态变量,声明在进程模型的状态变量区 */ static double mac_backoff_slots; /* 剩余退避时隙数 */ static double mac_slot_time; /* 单个时隙时长,从属性读 */ static int mac_backoff_frozen; /* 是否处于冻结状态 */ static int mac_retry_count; /* 当前帧重传次数 */

参数说明:mac_backoff_slots用 double 而不是 int,因为 OPNET 的中断时间是浮点仿真时间,退避剩余量经常要按小数时隙反算回时间去调度中断;mac_slot_time必须从模型属性里读,不要写死 20us,否则换了速率集之后时序全崩。

2.4 属性表与属性提升

OPNET 的参数体系是三层:模型属性(定义在进程/节点模型上)、提升属性(promote 到节点层)、场景属性(在场景里按节点或全局赋值)。这套源码把关键参数都提升出来了,包括:

  • 物理层速率(决定帧传输时长)
  • 时隙时长、SIFS、DIFS
  • CWmin / CWmax
  • 重传次数上限
  • 是否开启 RTS/CTS 及其门限

排查问题时经常需要临时改某一个节点的参数,如果属性没提升,就只能去改进程模型,改一次编译一次。属性提升做全了的模型,在场景里右键节点就能改,这是省时间的地方。

3. 核心链路拆解:一个数据帧从进 MAC 到发上信道的全过程

这一章是整套源码里最值得逐行看的部分。搞清楚一帧走完的路径,后面所有“结果不对”的问题都能顺着链路往回倒推。

3.1 接收路径:从空口上来先判什么

从 PHY 上来的中断,第一件事不是解析帧头,而是判当前 MAC 处于什么状态。如果正在发送(WAIT_ACK或WAIT_CTS),这个帧要么是给自己的 ACK/CTS,要么是干扰,处理分支完全不同。

/* PHY 中断处理:空口上来的每一帧都从这进 */ static void mac_phy_intrpt_handler(void) { Packet *pkptr; int fcf_type; /* 帧控制字段里的 type/subtype */ double rx_start_time; /* 取包。流索引 1 = 从 PHY 上来,节点模型里必须对上 */ pkptr = op_pk_get(MAC_STRM_FROM_PHY); if (pkptr == OPC_NIL) { return; } /* 物理层已经算好的接收起始时刻,用于算信道占用时长 */ op_pk_nfd_get_dbl(pkptr, "rx_start_time", &rx_start_time); /* 只有数据帧和 RTS 需要判忙;ACK/CTS 直接进控制帧分支 */ fcf_type = mac_frame_type_get(pkptr); switch (fcf_type) { case MAC_FRAME_ACK: mac_handle_ack(pkptr); /* 可能是自己等的,也可能是别人的 */ break; case MAC_FRAME_CTS: mac_handle_cts(pkptr); break; case MAC_FRAME_RTS: mac_handle_rts(pkptr); /* 需要回 CTS + 置 NAV */ break; case MAC_FRAME_DATA: mac_handle_data(pkptr); break; default: op_pk_destroy(pkptr); break; } }

逻辑说明:这段函数是最外层的分发器,不做业务判断。有两个细节容易忽略。一是op_pk_get()返回空指针必须立刻 return,否则后面op_pk_nfd_get_dbl直接崩;二是rx_start_time这个字段是从物理层带过来的,用来计算 NAV 更新值——如果物理层模型没设这个字段,op_pk_nfd_get_dbl会报 field not found,这是典型的模型不匹配问题。

参数说明:op_pk_nfd_get_dbl(pkptr, "name", &var)是按名字取包格式字段(formatted field),字段名必须在包格式定义里存在,拼错会运行时报错。帧类型判断走的是自定义函数mac_frame_type_get(),内部读"fcf"字段然后拆位,不要在这里直接写位运算,后面加新帧型会很难改。

3.2 发送路径与 DCF 退避:整个 MAC 的心脏

发送路径的核心是“什么时候可以发”。DCF 的规则可以压缩成一句话:信道空闲持续 DIFS 之后,再随机退避若干个时隙,退避到 0 才允许发送;退避期间信道变忙就冻结计数,等下次空闲 DIFS 后继续。

/* 尝试发送队首帧:判断能否立即发,不能就启动退避 */ static void mac_tx_attempt(void) { double now, difs_end; int cw; if (mac_queue_empty()) { return; } if (mac_state != MAC_STATE_IDLE) { return; /* 已经在流程里,不重入 */ } now = op_sim_time(); /* 信道忙:进入 DEFER,等下一次空闲中断再回来 */ if (mac_channel_busy_get()) { mac_state = MAC_STATE_DEFER; return; } /* 信道空闲但未满 DIFS:调度一个 DIFS 结束的中断 */ if (now - mac_last_busy_end < mac_difs) { difs_end = mac_last_busy_end + mac_difs; op_intrpt_schedule_self(difs_end, MAC_INTRPT_DIFS_EXPIRE); return; } /* 退避还没启动过,就先抽一个随机数 */ if (!mac_backoff_active) { cw = mac_cw_current_get(); /* 当前竞争窗口 */ mac_backoff_slots = op_dist_uniform(cw); /* 取 [0, cw) 的整数时隙 */ mac_backoff_active = 1; mac_backoff_frozen = 0; } /* 退避计数为 0,可以发了 */ if (mac_backoff_slots <= 0.0) { mac_transmit_frame(mac_queue_head()); return; } /* 否则调度一个时隙后的中断,逐时隙递减 */ op_intrpt_schedule_self(now + mac_slot_time, MAC_INTRPT_SLOT_TICK); }

逻辑说明:这段函数是全篇最该读懂的地方。它把“判断能否发”拆成四道闸:队列非空、状态空闲、信道不忙、DIFS 已过。任何一道没过就提前 return,靠中断重新进来,而不是死循环等待——仿真里绝对不能写 while 等时间,会把事件队列卡死。

参数说明:mac_cw_current_get()返回当前 CW,初值等于 CWmin,每次重传翻倍直到 CWmax;op_dist_uniform(cw)这里取的是 [0, cw) 区间的浮点数,实际标准里退避时隙数是 [0, CW] 的整数,很多实现会写成op_dist_uniform(cw + 1)或者(int)(op_dist_uniform(cw)),两种写法的平均退避差半个时隙,做精确对比实验时要统一,否则两套模型的差异会掩盖掉你真正想验证的算法差异——这是我踩过的一个很隐蔽的坑。

3.3 退避槽的冻结与恢复

冻结逻辑是 DCF 里最容易写错的地方。标准规定的行为是:退避过程中如果检测到信道忙,就把剩余时隙数存起来(冻结),等信道再次空闲 DIFS 后从冻结值继续递减,不是重新抽随机数。

/* 时隙中断:每个时隙扣一次退避计数 */ static void mac_slot_tick_handler(void) { if (mac_channel_busy_get()) { /* 信道变忙:冻结,保留剩余时隙数 */ mac_backoff_frozen = 1; op_intrpt_schedule_self(op_sim_time(), MAC_INTRPT_CHANNEL_IDLE); return; } if (mac_backoff_frozen) { /* 信道恢复空闲,解冻后继续本次时隙 */ mac_backoff_frozen = 0; mac_last_busy_end = op_sim_time(); op_intrpt_schedule_self(op_sim_time() + mac_difs, MAC_INTRPT_DIFS_EXPIRE); return; } mac_backoff_slots -= 1.0; if (mac_backoff_slots <= 0.0) { mac_transmit_frame(mac_queue_head()); } else { op_intrpt_schedule_self(op_sim_time() + mac_slot_time, MAC_INTRPT_SLOT_TICK); } }

逻辑说明:注意解冻后要先等一个 DIFS 才能继续递减,这个 DIFS 等待是通过调度一个新中断实现的,不能直接减。如果这里图省事直接mac_backoff_slots -= 1,模型会偏向于更激进的接入,节点数一多吞吐虚高,和实测对不上。

参数说明:mac_last_busy_end记录信道最后一次变忙结束的时刻,mac_difs是 DIFS 时长。这两个变量在多个函数间共享,属于典型的“隐性耦合”——改 DIFS 的时候要确认所有用到它的地方,包括接收路径里更新 NAV 的分支。

3.4 RTS/CTS 与 ACK:时序常量必须对齐

开启 RTS/CTS 之后,一次成功的数据交换变成四帧握手。这里每一段间隔都是固定常量,写错一个,整套时序就错位。

间隔位置典型取值来源
SIFS数据帧与 ACK 之间、CTS 与数据帧之间属性表读取
DIFS竞争窗口起点SIFS + 2×时隙
ACK Timeout等 ACK 的超时上限SIFS + ACK 传输时长 + 余量
CTS Timeout等 CTS 的超时上限SIFS + CTS 传输时长 + 余量

超时时长的算法有个细节:ACK 传输时长依赖当前速率,而速率可能随节点变化。常见做法是用当前帧的速率反算,而不是用全局默认速率。这套源码里mac_ack_timeout_get()会先读队首帧的速率字段再算,改速率集的时候不用改超时逻辑。

/* 计算等待 ACK 的超时时刻 */ static double mac_ack_timeout_get(Packet *data_pkptr) { double rate, ack_tx_time; /* 从数据帧的字段里拿发送速率,避免用全局默认值 */ op_pk_nfd_get_dbl(data_pkptr, "tx_rate", &rate); if (rate <= 0.0) { rate = MAC_DEFAULT_RATE; } ack_tx_time = (MAC_ACK_BITS + MAC_PHY_HDR_BITS) / rate; /* ACK 实际传输时长 + SIFS,作为超时上限 */ return op_sim_time() + mac_sifs + ack_tx_time; }

逻辑说明:超时一定是在“发送完成之后”开始算的,如果写成op_sim_time() + ...而当前时刻还是发送开始时刻,超时会提前触发,表现是重传次数异常偏高、吞吐腰斩。正确做法是在发送完成中断里调用这个函数。

参数说明:tx_rate字段要求发送前由 MAC 写入包格式,如果上游没写就取默认值兜底。MAC_ACK_BITS是 ACK 帧长度(通常 14 字节量级),MAC_PHY_HDR_BITS是物理层前导和头部开销——这部分经常被漏算,短帧场景下误差能到 20% 以上。

3.5 队列与重传统计

MAC 层通常自带一个发送队列,队列满就丢包,这个丢弃计数是判断“模型是不是被配置压垮”的第一手指标。

/* 入队:满了就丢,并记一次统计 */ static void mac_enqueue(Packet *pkptr) { if (op_subq_stat_get(mac_queue_id, OPC_QSTAT_PKSIZE) >= mac_queue_max) { op_pk_destroy(pkptr); op_stat_write(mac_stat_q_drop, 1.0); /* 队列溢出计数 */ return; } op_subq_pk_insert(mac_queue_id, pkptr, OPC_QPOS_TAIL); op_stat_write(mac_stat_q_len, 1.0); /* 队列长度采样 */ }

逻辑说明:op_stat_write的第二个参数是本次采样的增量或瞬时值,具体语义取决于统计量是设为“累加”还是“采样”。队列丢弃用累加,队列长度用采样,混了以后出来的曲线会莫名其妙。统计量句柄在进程初始化时用op_stat_reg()注册,名字要和场景里勾选的统计量对得上,否则结果里看不到。

参数说明:op_subq_stat_get的第三个参数可以是包数OPC_QSTAT_PKSIZE或字节数OPC_QSTAT_BYTESIZE,按包数限队列会让大小帧混合场景下的实际占用差异很大,做吞吐对比实验建议用字节数。

4. 把模型跑起来:从编译到出图的实操路径

代码看懂了,下一步是让它在自己的场景里跑起来。这一步的坑主要不在代码,而在环境和配置。

4.1 编译前的三项检查

OPNET 的编译错误信息经常指向奇怪的位置,多半是环境问题而不是代码问题。开跑之前先确认三件事。

第一,模型库引用路径。工程里引用的标准模型库必须指向本机实际安装位置,换机器之后路径会断。表现是打开工程弹一堆“model not found”,或者节点模型里的标准模块变成灰块。

第二,C 编译器配置。OPNET 进程代码最终是编译成动态库加载的,编译器路径、include 路径、链接选项都要在环境配置里对。常见的错误是“编译成功但运行时找不到符号”,本质是链接阶段没把用到的库带进去。

第三,文件编码和换行符。模型文件是文本,从别的地方拷过来如果编码被改成带 BOM 的格式,解析器可能读不出第一个关键字。这个错很难看出来,报错信息往往只说“解析失败”。

# 典型的编译流程(命令行方式,图形界面等价) op_mksim -project wlan_mac_project -model mac_process # 若报 model not found,先检查模型库引用 op_mklib -list

命令的含义:op_mksim负责把进程模型编译成可加载的仿真库,op_mklib用来查看当前模型库列表。如果编译通过但运行时报找不到进程模型,一般是库路径没进环境变量。

4.2 场景参数的三类改法

同一套源码,换三种场景配置就能验证三种不同的行为,这是它适合做 baseline 的原因。

第一种,改节点数和业务强度。在场景里复制节点或者用应用配置对象批量生成,业务速率按指数分布给,这样能画出标准的吞吐-负载曲线。节点数从 5 加到 30 是常用的扫描范围。

第二种,改 MAC 参数。CWmin/CWmax、重传上限、RTS 门限这三组在属性表里都能改,不用重新编译。想做“小窗口 vs 大窗口”的对比,直接复制两个场景改属性即可。

第三种,改统计量开关。默认只开了全局吞吐和时延,如果要做逐帧分析,需要把 MAC 层的发送次数、重传次数、退避次数、队列丢弃全部打开,同时开矢量采集(.ov),否则只能看标量平均值,看不到随时间的变化。

注意:矢量采集会显著增加结果文件大小和仿真时间。做长时仿真之前先估一下事件数,几百万事件配上全量矢量,跑一晚上很正常。

4.3 结果采集与基线验证

跑完之后先别急着看图,做一次基线核对。常用的核对方法是单节点、饱和业务、无隐藏终端,这个时候理论上吞吐应该接近信道容量的一个稳定比例,重传率应该很低。如果重传率超过 10%,说明时序常量或者超时算法有问题,别急着往下做实验。

再看三个关键曲线:吞吐随负载的变化、平均时延随负载的变化、队列丢弃率随节点数的变化。三条线如果出现“吞吐先升后平但时延指数上升”,说明已经进入饱和区,这是 DCF 的正常行为,不是 bug。真正值得注意的是吞吐在饱和区之前就掉头向下,那基本可以判定是退避或者超时出了问题。

5. 避坑与排查:五条从现场捞回来的经验

5.1 仿真能跑但结果每次都不一样

现象:同一个场景连跑三次,吞吐差异超过 15%,节点数越多差异越大。

原因:随机数种子没固定。OPNET 默认每次运行使用不同的种子,退避的随机取值、业务到达间隔都依赖随机数,多节点场景下这种差异会被放大。

解决:在场景的仿真配置里把随机种子固定下来,做对比实验时所有场景用同一组种子,至少跑 5 次取均值再比较。注意不要只固定一个种子就下结论,单种子的结果本身也有偶然性。

5.2 高层收到的包全是重复的

现象:接收端统计的应用层包数约为发送端的两倍,且时延分布出现两个峰。

原因:WAIT_ACK状态下收到 ACK 后没有清理重传定时器,超时中断随后又触发了一次重传,接收端对同一帧处理了两次。ACK 和自己的数据帧是同一个包的副本,如果接收侧按序列号去重没做,就会重复上交。

解决:处理 ACK 时先取消挂起的超时中断(op_intrpt_clear_self()或记录标志位),再更新状态。同时在接收侧加序列号缓存做去重,两层保险。

5.3 信道利用率算出来超过 100%

现象:把各节点的发送时长加起来除以仿真时长,得到 1.2 甚至更高。

原因:统计时把失败重传的时长也算进了“有效发送”,或者节点模型的发送时长是按包大小估算而不是按物理层实际占用时间累计。

解决:区分“尝试发送时长”和“成功发送时长”两个统计量,信道利用率只用成功部分。物理层占用时间要包含前导、头部和帧间间隔,不能只算载荷比特数除以速率。

5.4 一开 RTS/CTS 吞吐反而暴跌

现象:门限设得很低,本意是消除隐藏终端,结果吞吐掉了一半。

原因:门限按字节数比较时单位搞错了,把门限值当成包数,导致所有小包都走了四帧握手,控制帧开销吃掉大部分信道时间。另一个常见原因是 CTS 超时算得太短,握手频繁失败。

解决:打印一下触发 RTS 的帧比例,如果接近 100% 就说明门限设错。门限应该设在“帧长超过某个字节数才值得握手”的位置,一般从几百字节起调。CTS 超时按 CTS 实际传输时长加 SIFS 再加余量计算,余量别省。

5.5 编译通过但运行时报 field not found

现象:仿真开始几秒后进程崩溃,日志显示某个包格式字段读不到。

原因:发送端和接收端用的包格式版本不一致,或者物理层模型没往包里写 MAC 期望的字段(比如前面提到的rx_start_time、tx_rate)。这在混用不同来源的模型时特别常见。

解决:把包格式定义收到一个公共头文件里,收发双方都引用同一份。跨模块传字段的地方加空值检查,读不到就用默认值兜底并且记一次统计,这样即使字段缺失也能跑完,从统计量里能看出是哪一段出的问题。

6. 二次开发:把 MAC 层改成你自己的试验台

源码最大的价值不在“能跑”,在“能改”。这套模型的结构决定了改动成本很低:状态机在.pr.m,逻辑在.c,常量在.h,三者分离得很干净。下面是我实际改过的三个方向,按改动量从小到大排。

改造方向要动的文件改动量验证方法
换退避算法(如指数退避改线性)mac_support.c里的一两个函数小对比重传率与吞吐曲线
加一个 EDCA 式的优先级队列.h加结构、.c加调度、.pr.m加状态中两类业务时延分布分离度
加一个新帧型做握手扩展三处都要改大抓逐帧时序验证

先说要领。改退避算法时,不要直接动mac_backoff_slots的递减逻辑,而是把“抽随机数”这一步抽成独立函数,让 CW 的更新策略也走同一个入口。

/* 把竞争窗口的更新策略抽出来,换算法只改这里 */ static int mac_cw_current_get(void) { int cw; /* 原始 DCF:每次重传翻倍,上限 CWmax */ cw = mac_cw_min << mac_retry_count; if (cw > mac_cw_max) { cw = mac_cw_max; } /* 想验证线性退避,就把上面三行换成: cw = mac_cw_min + mac_retry_count * MAC_CW_STEP; 并保证 cw 不超过 mac_cw_max */ return cw; }

这样改的好处是回归验证简单:只改这一个函数,其他逻辑不动,跑出来的差异可以直接归因到算法本身,而不是“顺手改的其他地方”。我一般会保留原始实现作为宏开关,用属性控制走哪条分支,一个场景文件就能跑两组对比。

第二个技巧是验证方法。改完 MAC 之后,别直接上看吞吐曲线,先做单节点、单业务的逐帧时序核对:打印每一帧的发送时刻、退避时隙数、等待 ACK 的时长,和手算的值对一遍。手算只算前几帧就够了,能对上说明时序逻辑没错,再去跑多节点统计才有意义。这一步花二十分钟,能省掉后面几小时的“结果不对但不知道哪不对”。

第三个是保持可回退。所有改动先在工程副本里做,每改一个函数就在注释里写清楚“原始行为是什么、改成了什么、预期差异方向”。做多了会发现,最有用的不是新算法,而是那句“原始行为”的注释——半年后回来看,只有它能告诉你当初为什么这么改。

从那以后我每次动 MAC 层代码,都会先把原始模型的单节点基线跑一遍存下来,改完再跑一遍对比,中间任何一步数字对不上就停下来查。这个习惯救过我很多次,也希望帮到你。

本文还有配套的精品资源,点击获取

返回列表