简介:面向无线局域网协议研究与OPNET仿真实践的802.11-MAC源代码资料包,适合通信、计算机网络方向的高年级学生、研究生及需要复现WLAN信道接入实验的工程人员。压缩包为rar格式,共356个文件、约1.31MB,以ov、os模型与状态机文件,m脚本,c源码,以及obj、lib、exp、dll等仿真工程与库文件为主,并配有prj工程和说明文本,便于在OPNET中加载、编译与调试。已有876人学习下载,说明其在MAC层仿真学习中具有一定参考面。代码围绕802.11的帧结构、CSMA/CA信道访问、DCF公平共享等机制展开,也可据此考察QoS与安全相关实现;从管理帧与控制帧构造到优先级队列、加密认证交互,均可作为课程设计、论文实验或工程验证的参考,帮助读者理解无线信道竞争、退避与帧同步的落地方式,并对照仿真结果分析性能、积累协议建模与排错经验。
1. 从一条贴着零的吞吐曲线说起
第一次用 opnet 跑 802.11 仿真,很多人会拿到一条紧贴横轴、几乎没有起伏的吞吐曲线:节点明明在发包,结果图里什么都没动。问题通常不在场景搭建上,而在 MAC 层那套进程模型:信道状态没刷新、退避定时器的时间单位写错、包在进程里被处理掉却没人销毁。这份 802.11 MAC 协议源代码的价值,就在于它把 DCF 的载波侦听、DIFS 判定、随机退避、RTS/CTS 握手和重传计数,全部摊开成可读可改的状态机加函数块。它适合两类人:一类要在课程或论文里复现 802.11 性能曲线、必须搞清楚每个参数最终落到哪行代码;另一类嫌内置模型不够用,想加 EDCA、加统计量、换自己的调度策略。下面按“结构—链路—代码—跑通—改造”的顺序拆。
2. 模型分层:从网络域到进程域的四个层级
2.1 四层建模各自管什么
opnet 的建模是分层的,这个分层不是形式主义,它决定了你改一个参数之后要多走几步才能让它真正生效。网络域放子网、移动节点、应用配置;节点域放模块和包流线,决定一个工作站有几根天线、缓存在哪;进程域放状态机,802.11 的协议逻辑 90% 都在这里;最下面是包格式和 ICI,决定帧头有哪些字段、层与层之间怎么传控制信息。
| 层级 | 典型文件 | 802.11 中对应什么 | 实际最常改的地方 |
|---|---|---|---|
| 网络域 | .nt.m | 子网、移动节点、应用业务 | 节点数、初始坐标、移动轨迹 |
| 节点域 | .nd.m | 工作站、接入点、收发信机 | 天线增益、包流线、缓存位置 |
| 进程域 | .pr.m | wlan_mac状态机 | DCF 参数、重传策略、统计量 |
| 包格式 | .pk.m | wlan_mac_frame | 帧头字段增删、地址位 |
把这张表记住有一个好处:当你发现“改了 CWmin 但曲线一动不动”,就知道该往上一层查节点模型有没有指向你改过的那个进程模型,而不是继续在状态机里找 bug。
2.2 文件清单:哪些必须一起带走
这套模型不是一个单文件脚本,而是一组互相引用的模型文件。少一个 pipeline,包照样能发出去,但收端永远判成错误帧,曲线会变成一条零线,而且不报错——这种问题很难排查。一个能直接跑起来的最小集合,大致是这样:
wlan_project/ ├── wlan_project.ef # 工程文件,双击进入 Modeler 即可加载 ├── wlan_scenario/ │ ├── wlan_scenario.nt.m # 网络域:子网与移动节点布置 │ ├── wlan_station.nd.m # 节点域:普通工作站 │ ├── wlan_ap.nd.m # 节点域:接入点,多一个分发模块 │ ├── wlan_mac.pr.m # 进程域:MAC 状态机,全部协议逻辑 │ ├── wlan_mac.h # 宏定义:DIFS / SIFS / SLOT / CW 上下限 │ ├── wlan_mac_frame.pk.m # 包格式:帧头的字段布局 │ └── wlan_radio_tx.ps.m # 发信机 pipeline:传播时延、误码、信噪比 └── results/ # 仿真输出目录,可以整个清空重跑.pr.m是 Proto-C 写的,本质是 C 语言加一层状态机语法扩展;.h里那几个宏决定了时间尺度,比如 SIFS 常见取 10 微秒、SLOT 取 20 微秒、DIFS 等于 SIFS 加两倍 SLOT,也就是 50 微秒。这几个数字一旦写错单位(比如把微秒当成秒),退避会瞬间完成,所有节点在同一时刻发包,冲突率直接拉满。
2.3 进程模型的六个块,读代码从这里入手
Proto-C 的进程模型从上到下是固定几块:头文件块、状态变量块、临时变量块、函数块、状态块、状态转移。读别人写的模型,我的习惯是先跳到状态变量块——作者把什么东西当成“协议状态”,一眼就能看出来他理解到哪一层。
/* ---- 状态变量块:进程创建时分配一次,仿真全程常驻 ---- */ typedef struct { /* 信道与定时器:统一存绝对仿真时间,不存剩余时间 */ int channel_state; /* IDLE / BUSY / CONFLICT 三态 */ double backoff_timer; /* 退避计时器到期时刻 */ double defer_timer; /* DIFS 到期时刻 */ double nav_timer; /* 虚拟载波侦听,收到 Duration 就往后推 */ double last_busy_time; /* 最后一次信道由忙转闲的时刻 */ int backoff_slots; /* 本次抽到的时隙数,调试打印用 */ /* 重传与队列 */ int tx_attempts; /* 连续重传次数,收 ACK 后清零 */ int retry_limit; /* 短 / 长重传上限,从属性读入 */ int queue_len; /* 缓存中排队等待发送的帧数 */ int seq_num; /* 12 位序列号,回绕 */ /* 统计量句柄:取到 OPC_INVALID 说明根本没注册上 */ Stathandle_t mac_delay_hndl; Stathandle_t drop_hndl; } WlanT_Mac_State_Vars;这里有个设计细节值得注意:所有定时器存的都是绝对时间,而不是“还剩多少”。原因是op_sim_time()返回的是全局仿真钟,两个绝对时间一减就得到间隔,而剩余时间的写法在包到达打断退避、之后又要恢复的时候,必须自己记住已经走了多少,容易算错。血泪经验是:一旦你开始在一个定时器变量上做加减法维护剩余量,这个模型迟早会在某个时序上出问题。
2.4 属性到变量的映射链路
模型对外暴露的参数不是直接写死在代码里的,而是通过节点域或进程域的属性(attribute)声明,运行时用op_ima_obj_attr_get读出来。链路是:网络域节点实例上的属性值 → 节点域模块属性 → 进程域op_ima_obj_attr_get。这条链每一段都能断,最常见的是你在网络域改了某个节点的“RTS Threshold”,但进程域读的是模块级属性,节点实例上的覆盖值压根没传下来。
/* INIT 状态里读属性,读不到就用默认值兜底 */ if (op_ima_obj_attr_get (op_id_self (), "Short Retry Limit", &mac_sv_ptr->retry_limit) == OPC_ERR) { mac_sv_ptr->retry_limit = 7; /* 802.11 常见默认值 */ }参数说明:op_id_self()拿到当前进程所属对象的 ID,op_ima_obj_attr_get的第二个参数必须和属性声明里的名字一字不差,包括空格和大小写。返回值判 OPC_ERR 是必须的,否则属性缺失时这个变量就是个未初始化的随机值,仿真结果会变得不可预测。这套代码的好处是每个属性读取都带了兜底,你从别人机器上拷过来的工程即使属性没同步,也不至于跑出一堆随机数。
3. 状态机拆解:一次 DCF 竞争要经过哪几个状态
3.1 状态迁移总览
整套 MAC 的核心是一个有限状态机,迁移路径大致是:INIT 完成属性读取和统计量注册,转到 BSS_INIT 判断自己是 AP 还是终端、要不要发 Beacon,然后落到 IDLE 等业务;有帧要发时进 DEFER 等信道空闲够 DIFS;DIFS 到了进退避阶段,每个空闲时隙把计数器减一;减到零进 TRANSMIT 真正把帧推给物理层;发完进 FRAME_END 等 ACK 或者超时;收到 ACK 就回 IDLE 继续发下一个,超时就回退到 DEFER 重来一次。另外还有一条并行支路 RECEIVE,处理从物理层送上来的帧,它会在任意状态打断当前流程。
理解这个结构的关键是:DEFER 和退避其实是两个阶段,很多人把它们合并成一个“等信道空闲”的状态,结果就是无法正确实现“退避计数器在信道变忙时挂起、变闲后继续”的行为,隐藏终端场景下的曲线会明显偏高。
3.2 INIT:读属性、注册统计量、把状态清干净
INIT 里干的活很杂但很关键:读属性、注册统计量、初始化序列号、把信道状态假设成空闲。这一段写漏了,后面所有逻辑都建立在不牢靠的基础上。
/* INIT 状态进入代码:一次性的初始化,必须全部走完 */ mac_sv_ptr->channel_state = WLANC_CHANNEL_IDLE; mac_sv_ptr->backoff_timer = 0.0; mac_sv_ptr->nav_timer = 0.0; mac_sv_ptr->last_busy_time = op_sim_time (); mac_sv_ptr->tx_attempts = 0; mac_sv_ptr->seq_num = (int) op_dist_uniform (4096.0); /* 随机起点,避免同步 */ /* 统计量注册,名字要和结果查看器里的一致 */ mac_sv_ptr->mac_delay_hndl = op_stat_reg ("MAC Delay (sec)", OPC_STAT_INDEX_NONE, OPC_STAT_LOCAL); mac_sv_ptr->drop_hndl = op_stat_reg ("MAC Drops", OPC_STAT_INDEX_NONE, OPC_STAT_LOCAL);参数说明:op_dist_uniform(4096.0)给序列号一个随机起点,这不是可有可无的细节——如果所有节点的初始序列号都是 0,在广播场景下会出现大量重复帧被上层丢弃,你会误以为是 MAC 逻辑错了。op_stat_reg的第三个参数选OPC_STAT_LOCAL表示统计量按节点分别记录,想看全网汇总再用OPC_STAT_GLOBAL。
3.3 IDLE 到 DEFER:载波侦听与 DIFS 判定
物理载波侦听看的是channel_state,虚拟载波侦听看的是nav_timer,两者只要有一个说忙,信道就是忙的。这是 DCF 里最容易被简化掉的一步,一旦简化,RTS/CTS 就基本失效了。
/* IDLE 状态的转移条件:信道空闲且虚拟载波侦听也已过期 */ (mac_sv_ptr->channel_state == WLANC_CHANNEL_IDLE) && (op_sim_time () >= mac_sv_ptr->nav_timer) && (op_sim_time () - mac_sv_ptr->last_busy_time >= WLANC_DIFS) /* 满足后进入 DEFER,同时做两件事:抽退避、起 DIFS 定时器 */ wlan_mac_backoff (); op_intrpt_schedule_self (op_sim_time () + WLANC_DIFS, WLANC_DEFER_DONE);逻辑说明:last_busy_time在每次信道由闲转忙时更新,这样“空闲持续了多久”就是一个减法。DIFS 判定必须放在这里而不是放进退避函数里,因为退避函数会被重传路径反复调用,混在一起之后你根本分不清某次退避是因为新帧还是因为重传。WLANC_DEFER_DONE是自定义的中断码,建议在头文件里定义成常量,不要直接用数字,否则多个自中断混在一起时排查起来很痛苦。
3.4 退避计时器的挂起与恢复
退避的本质是一个倒计时器,每过一个空闲时隙减一,期间只要信道变忙就冻住,等再次空闲够 DIFS 后从冻住的值继续。所以它不能简单地用一个“到期时间”实现,得记录剩余时隙。
/* 函数块:抽退避窗口并挂定时器 */ static void wlan_mac_backoff (void) { double cw; int slots; /* 第 n 次重传,窗口翻倍,封顶 CWmax */ cw = WLANC_MIN_BACKOFF * pow (2.0, (double) mac_sv_ptr->tx_attempts); if (cw > WLANC_MAX_BACKOFF) cw = WLANC_MAX_BACKOFF; /* 在 [0, cw) 内取整,抽到的就是本次要等的空闲时隙数 */ slots = (int) op_dist_uniform (cw); if (slots < 0) slots = 0; mac_sv_ptr->backoff_slots = slots; mac_sv_ptr->backoff_timer = op_sim_time () + slots * WLANC_SLOT_TIME; /* 只有确实还要等,才注册自中断,避免无效事件堆积 */ if (slots > 0) op_intrpt_schedule_self (mac_sv_ptr->backoff_timer, WLANC_BACKOFF_DONE); }参数说明:WLANC_MIN_BACKOFF对应 CWmin,常见取 31,WLANC_MAX_BACKOFF对应 CWmax,常见取 1023,两个值都是“窗口大小减一”,所以实际抽的是 0 到 cw 之间的整数。tx_attempts参与指数运算,意味着重传次数越多窗口越大,这是二元指数退避的标准做法。
注意:op_intrpt_schedule_self每调用一次就多一个待处理事件。如果信道反复变忙变闲而你不取消旧事件,队列里会积一堆到期时刻各不相同的自中断,退避就会莫名其妙地提前结束。稳妥的写法是在注册新事件前用op_intrpt_clear_self()清掉同一中断码的旧事件。
3.5 RTS/CTS 与 NAV 的配合
当帧长超过 RTS 门限,发送方要先发一个短 RTS,收到 CTS 之后才发数据。这一段的作用是把隐藏终端的问题从“数据帧冲突”降级成“控制帧冲突”,代价是多两次握手开销。代码上它体现在两处:一是 TRANSMIT 之前插入一个等待 CTS 的状态,二是收到任何帧时都要读它的 Duration 字段去更新nav_timer。
/* 收到帧的第一件事:不管是不是发给自己的,先按 Duration 设 NAV */ nav_value = op_pk_fd_get_int (pkptr, WLANC_FD_DURATION); if (nav_value > 0) { mac_sv_ptr->nav_timer = op_sim_time () + nav_value * WLANC_TIME_UNIT; }参数说明:Duration字段的单位是微秒,而仿真时间单位可能是秒或毫秒,取决于工程设置,WLANC_TIME_UNIT就是干这个换算的。这个换算漏掉,NAV 要么短到没作用,要么长到把信道锁死——两种表现都很像“协议不工作”,但原因完全不同。判定的写法是把 NAV 更新放在地址过滤之前,因为 802.11 规定即使帧不是发给自己的,也要按 Duration 更新 NAV。
4. 收发与重传:MAC 帧结构、队列和下层接口
4.1 包格式与帧头字段
wlan_mac_frame.pk.m定义了帧头的字段布局,标准结构是帧控制 2 字节、Duration 2 字节、三个地址各 6 字节、序列控制 2 字节,加上可选的第四地址和 4 字节 FCS。在 opnet 里这些字段可以用op_pk_fd_set逐个写,也可以把整个头映射成一个 C 结构体指针一次写进去,后者代码更短,但要求包格式里的字段顺序和结构体严格对齐。
| 字段 | 长度 | 这份代码里怎么用 |
|---|---|---|
| Frame Control | 2 B | 高 2 位是协议版本,中间几位标类型(管理/控制/数据) |
| Duration | 2 B | 供其他节点设置 NAV,RTS/CTS 里尤其重要 |
| Address 1 | 6 B | 直接接收方 |
| Address 2 | 6 B | 发送方 |
| Address 3 | 6 B | 按 ToDS/FromDS 组合决定是源地址还是目的地址 |
| Sequence Control | 2 B | 高 4 位分片号,低 12 位序列号 |
实际改代码时最容易出错的是 Address 3。在一个只做终端到终端的场景里它往往被忽略,一旦加上 AP,ToDS 和 FromDS 两个标志位不同,Address 3 的含义就完全不同,地址填错的表现是 AP 收到了帧但不知道往哪转发,最后统计里的吞吐只有单向。
4.2 发送路径:从队列到物理层
发送是一个“取队头、填头、推流”的三步动作。这份代码里把它收在一个函数里,好处是重传路径可以直接复用,不用把填头逻辑写两遍。
/* 从发送缓存取一帧,填好帧头后推给物理层 */ static void wlan_mac_transmit_frame (void) { Packet *pkptr; WlanT_Mac_Hdr *hdr; pkptr = wlan_mac_queue_dequeue (); if (pkptr == OPC_NIL) return; /* 队列空,直接返回,别继续往下走 */ hdr = (WlanT_Mac_Hdr *) op_pk_fd_get_ptr (pkptr, 0); hdr->frame_control = wlan_mac_build_fc (WLANC_DATA); hdr->duration = wlan_mac_calc_duration (pkptr); /* 给别人设 NAV */ hdr->addr1 = wlan_mac_next_hop (pkptr); /* 下一跳 */ hdr->addr2 = mac_sv_ptr->my_addr; hdr->addr3 = wlan_mac_src_or_dst (pkptr); hdr->seq_control = (mac_sv_ptr->seq_num++ & 0x0FFF); op_pk_send (pkptr, TX_STRM); /* 交给发信机 pipeline */ }参数说明:op_pk_fd_get_ptr的第二个参数是字段索引,0 表示第一个字段,用指针方式取要求该字段在包格式里被声明成结构体类型。wlan_mac_calc_duration按当前数据速率和帧长估算还要占用信道多久,这个值不需要非常精确,但必须大于零,否则 NAV 形同虚设。TX_STRM是包流线的索引,必须和节点域里连到wlan_port_tx的那条线一致,写错了包会流到一个不存在的输出流上,然后被内核静默丢弃。
4.3 接收路径:地址过滤与应答
接收路径做三件事:判断这帧是不是给自己的、检查有没有错、决定要不要回 ACK。地址过滤的顺序有讲究:先看 Address 1,如果是广播或多播地址就收下,如果是自己的单播地址也收下,如果都不是,还需要判断自己是不是 AP 并且这帧需要转发。顺序写反会导致 AP 场景下大量帧被丢弃。
/* 接收判定:返回 OPC_TRUE 表示要往上送 */ boolean wlan_mac_accept_frame (Packet *pkptr) { WlanT_Mac_Hdr *hdr; hdr = (WlanT_Mac_Hdr *) op_pk_fd_get_ptr (pkptr, 0); if (wlan_mac_is_broadcast (hdr->addr1)) return OPC_TRUE; if (wlan_mac_addr_equal (hdr->addr1, mac_sv_ptr->my_addr)) return OPC_TRUE; /* 既不是广播也不是发给自己,再看是不是 AP 需要转发 */ if (mac_sv_ptr->is_ap && wlan_mac_need_forward (pkptr)) return OPC_TRUE; return OPC_FALSE; }逻辑说明:wlan_mac_need_forward内部会检查 ToDS/FromDS 和 Address 3,只有确认自己是路径上的一跳才返回真。这个函数里没有做 FCS 校验,是因为误码在收信机 pipeline 阶段已经处理掉了,错误帧压根不会送到进程层——这也是为什么前面强调.ps.m文件不能少。如果 pipeline 缺失,进程层收到的全是“正确”的帧,你的误帧率统计会永远是零,看起来像是链路质量特别好。
4.4 重传计数与短/长重传限制
802.11 定义了短重传限制和长重传限制两个值,前者用于小于 RTS 门限的帧,后者用于大帧。区别在于大帧重传的代价更高,所以允许的次数更少。这份代码里在发送前先按帧长选一个上限,然后每次超时把tx_attempts加一,超过上限就丢帧并记一次 drop。
/* 超时未收到 ACK,进入重传判定 */ mac_sv_ptr->tx_attempts++; limit = (op_pk_total_size_get (pkptr) < mac_sv_ptr->rts_threshold) ? mac_sv_ptr->short_retry_limit : mac_sv_ptr->long_retry_limit; if (mac_sv_ptr->tx_attempts > limit) { op_stat_write (mac_sv_ptr->drop_hndl, 1.0); /* 记一次丢包 */ op_pk_destroy (pkptr); /* 必须销毁,否则内存泄漏 */ mac_sv_ptr->tx_attempts = 0; /* 回到 IDLE,继续下一个 */ } else { wlan_mac_backoff (); /* 窗口翻倍后重新竞争 */ }参数说明:op_pk_total_size_get返回的是比特数,和rts_threshold的单位必须一致,很多工程里阈值写的是字节,这里忘了乘 8,结果所有帧都被判定成小帧,长重传限制永远用不上。op_pk_destroy这一步不能省,opnet 的包是引用计数的,收包方不销毁、发包方又已经交出去,这个包就永远留在内存里,长时间仿真跑到后面会明显变慢,最后卡在某个事件上不动。
5. 避坑与排查:五个让仿真跑不出结果的常见问题
5.1 模型加载与环境类
现象:仿真能启动,跑完之后所有节点的吞吐都是零,也没有报错。原因大概率是收信机 pipeline 文件没加载或者路径不对,包发出去之后在物理层被判成无效直接丢弃,进程层根本收不到东西,自然也不会有任何统计。解决办法是先把仿真设成“收集所有包的轨迹”,在结果里看发信机的吞吐计数,如果发包计数正常而收包计数为零,就一定卡在 pipeline 上;然后在节点域里双击发信机,确认 pipeline 模型名和目录里实际存在的文件名一致。
现象:打开工程时提示某个模型找不到,但文件明明在目录里。原因是 opnet 的模型搜索路径是按“模型目录”注册的,文件放在磁盘上不等于已经注册进当前工程。解决方式是在模型库管理器里把该目录加进搜索路径,或者把文件拷进工程默认的模型目录再重启环境。这一步在换机器、换环境的时候几乎必踩。
5.2 协议逻辑类
现象:所有节点几乎在同一时刻发包,冲突率接近百分之百。原因通常是退避窗口初始化为零,或者WLANC_SLOT_TIME的单位写错,把微秒当成了秒。检查办法是在 INIT 里打印一次WLANC_SLOT_TIME和第一次抽到的backoff_slots,如果 slots 恒等于零,就去看WLANC_MIN_BACKOFF是不是被设成了 0。另一个隐蔽原因是随机数种子,如果所有节点的种子相同,抽出来的退避值会完全一样,冲突反而比不做退避更严重。
现象:改了重传次数上限,但丢包统计毫无变化。先确认你改的是不是节点实例上的属性——进程域读的是模块级属性,实例上的覆盖值需要显式传下来。再确认丢包统计的写入点:如果 ACK 超时那条分支压根没被走到,说明问题在上层,帧根本没发出去。我的习惯是在 TRANSMIT 状态的进入代码里加一句轻量的调试输出,先确认“到底发出去了多少帧”,再往下查。
5.3 结果与统计类
现象:统计量曲线一直是零,但仿真日志里数据看着正常。原因是op_stat_reg返回了OPC_INVALID,也就是这个统计量没注册成功,后续所有op_stat_write都写到了空句柄上。常见触发条件是统计量名字重复注册,或者注册代码放在了会被反复执行的函数块里而不是 INIT。解决方式很简单:注册之后立刻判断句柄是否有效,无效就打印一行警告,别等到看结果的时候才发现。
现象:仿真跑到某个时刻就卡住不动,进度条长时间不变,也没报错。这种多半是包泄漏或者自中断死循环。包泄漏的排查方式是看进程里每一处op_pk_create是不是都有对应的op_pk_destroy,尤其是错误分支——正常的收包路径往往会销毁,异常路径经常忘。自中断死循环更隐蔽:两个状态互相用零延迟事件来回触发,仿真时间不推进,看起来就是卡住。把自中断的调度时刻打印出来,如果连续多个事件的时刻完全相同,基本可以确定是这个问题。
6. 二次开发:从改参数到加自己的统计维度
参数级别的改造最省事:CWmin、CWmax、RTS 门限、重传上限这些都在属性里,改完直接跑对比实验就行。但如果要做点有意思的,比如把二元指数退避换成自适应窗口,改动点就在wlan_mac_backoff()里那两行pow和op_dist_uniform上。我的做法是把窗口计算单独抽成一个函数,参数传进去是重传次数和当前信道冲突率,返回值是窗口大小,这样原来那条标准路径还能保留,用来做基线对比。
再往上一层是加统计量。想观察每个节点的接入时延分布,最直接的方式是在帧入队时打一个时间戳,在收到 ACK 时算差值再op_stat_write。时间戳不要存在包字段里,包格式一改所有引用它的地方都得跟着改;我一般存在一个按序列号索引的小数组里,出队时按序列号回查。要注意序列号只有 12 位,会回绕,数组得做成环形缓冲,否则跑久了会拿到别人的时间戳。
/* 入队时记时间戳,环形缓冲,容量要大于在途帧数 */ mac_sv_ptr->enq_time[mac_sv_ptr->seq_num & 0x0FFF] = op_sim_time (); /* 收到 ACK 时算时延,写统计量 */ delay = op_sim_time () - mac_sv_ptr->enq_time[hdr->seq_control & 0x0FFF]; if (delay > 0.0) op_stat_write (mac_sv_ptr->mac_delay_hndl, delay);验证改造是否正确的顺序,我建议固定成三步:先跑单节点对单节点、无移动、固定速率的基线,确认吞吐接近理论值;再跑两个节点加隐藏终端,看 RTS/CTS 打开和关闭的差异是否符合预期;最后才上 AP 加多站点。直接上复杂场景,出了问题你根本分不清是协议逻辑错了还是场景配错了。
从那以后我每次动 MAC 层的代码,都会先把基线场景单独存一份、跑一遍记下三个数(吞吐、平均时延、丢包率),改完再跑一遍对着看。这三个数只要有一个偏离量级,就一定是引入 bug 了,不用等曲线画出来才发现。希望帮到你。
本文还有配套的精品资源,点击获取