简介:这份MATLAB仿真代码围绕CSMA/CA与CSMA/CD两种介质访问控制协议展开,面向通信工程、计算机网络课程学习者及需要复现协议行为的研发人员,帮助其在可运行脚本中理解信道监听、冲突避免、冲突检测与随机退避等关键机制。压缩包共16个文件,均为.m脚本与函数,整体约17KB,按协议实现、节点管理、帧处理、退避时间计算和结果显示等用途拆分,便于逐模块阅读与调试。已有6667人学习,说明其在协议仿真入门中具有一定参考价值。代码附带详细注释,读者可观察发送前信道感知、预约与确认流程,以及碰撞后停止发送、等待重试的过程,还能调整节点数量与参数,对比两种协议在不同网络负载下的效率变化,并借助图形化输出理解冲突处理逻辑。对课程实验、协议对比分析和仿真建模练习而言,这是一份轻量但完整的学习素材。
1. 两种接入协议在同一套 MATLAB 仿真里:csma/ca 与 csma/cd 的源代码到底把哪些抽象变成了变量
拿到一份带详细注释的 csma/ca 与 csma/cd matlab 仿真源代码,很多人的第一反应是直接 run 一遍看曲线。真跑过就知道,默认参数下两条曲线都「挺正常」,可一旦把节点数从 10 改到 50、把单跳传播时延从 1 μs 拉到 20 μs,曲线立刻出现反直觉的拐点,甚至吞吐冲过理论值。这套代码真正值钱的地方,是注释标出来的那些中间变量:退避计数器每一次递减、冲突窗口每一次翻倍、DIFS 期间信道由忙转闲的那一个时隙、ACK 超时之后重传次数怎么累加。它面向的是要交课程设计、要做协议对比实验、要在报告里放一张站得住脚的曲线的人,最好有 MATLAB 语法底子,并且知道以太网帧头、802.11 的 IFS 大概是干什么的。
2. 仿真骨架拆解:离散事件推进与四个核心函数
协议仿真写崩,八成不是协议逻辑错,而是时间推进的骨架搭歪了。拿到源码先别急着看退避公式,先把「时间是怎么往前走的」这件事弄清楚,后面所有参数的物理含义才站得住。这一章拆的是这套代码的骨架:为什么它用离散事件而不是等步长、文件之间怎么调用、以及第一次跑通要敲哪几条命令。
2.1 为什么这类协议必须用事件驱动而不是等步长
CSMA 家族的性能几乎全部由「微秒级的时间边界」决定。CSMA/CD 的争用期是往返传播时延,10 Mbps 以太网下只有 51.2 μs;CSMA/CA 的一个退避时隙是 9 μs 或 20 μs。如果按固定步长 Δt 推进,为了保证不把两个相邻时隙糊在一起,Δt 至少要取到时隙的十分之一,也就是 1 μs 量级。仿真时长 1 s 意味着 10^6 次循环,每次循环还要遍历所有节点判断信道状态,20 个节点就是 2×10^7 次判断,MATLAB 的 for 循环扛不住这个量级。
事件驱动的思路反过来:只在「状态可能发生改变的时刻」推进时间。帧到达、退避计数器归零、帧发送结束、冲突被检测到、ACK 返回、NAV 定时器到期,这几类事件之外的时间轴是空的,直接跳到下一个事件时刻。节点数在 200 以内时,用[~, idx] = min([events.t])每次线性找最小时间完全够用,不必上来就手写二叉堆,那样代码可读性掉得厉害,注释也不好写。
等步长也不是不能用。要统计「信道忙闲占比」这类连续量,等步长反而更容易写,采样点天然均匀。但只要目标是吞吐-负载曲线、冲突率、平均退避时延这些协议级指标,事件驱动的误差来源更少:不会有「事件落在两个采样点之间被吞掉」的问题。
2.2 文件清单与调用关系
这类包通常按「参数集 + 两个协议核 + 公共工具」分文件,我这边的叫法列在下面。你手上的包文件名可能不一样,按函数头注释里写的输入输出对应过去就行,重点是认清楚哪一个是唯一的参数入口。
| 文件/函数 | 职责 | 主要输入 | 主要输出 |
|---|---|---|---|
config_params.m | 参数集,按协议名返回结构体 | 'csma_cd'/'csma_ca' | cfg结构体 |
csma_cd_core.m | CSMA/CD 事件循环 | cfg | stat、log |
csma_ca_core.m | CSMA/CA 事件循环,含 IFS、NAV、ACK | cfg | stat、log |
backoff_exp.m | 二进制指数退避抽样 | 冲突次数、cfg | 等待时隙数、新冲突计数 |
channel_model.m | 传播时延与节点可见性矩阵 | cfg | vis矩阵 N×N |
plot_curve.m | 扫参数、画吞吐与时延曲线 | 结果矩阵 | figure 句柄 |
表里最容易被忽略的是channel_model.m。它产出的vis(i,j)表示节点 i 能不能听到节点 j,取 1 表示能听到。CSMA/CD 里这个矩阵基本是全 1(共享总线),CSMA/CA 里把某一对置 0 就构造出隐藏终端。很多现成的包把可见性写死在核函数里,一改拓扑就要动主循环,扩展性很差,注释也救不回来。
stat结构体建议核对自己手上那份是否至少包含这些字段:n_attempt(总发送尝试)、n_success、n_collision、n_drop、throughput、t_idle、t_busy、mean_backoff_slots、mean_delay。少一个,后面第 5 章的排查和第 6 章的守恒校验都做不了。
2.3 第一次跑通:两条无界面命令加一段最小脚本
先确认不报错,再谈结果。有 MATLAB 命令行的话,无界面跑一遍最快,报错信息直接打在终端里,比在编辑器里点运行更容易复制出来搜。
# 在源码根目录下无界面执行,先确认语法和路径没问题 matlab -batch "cd('csma_sim'); main_csma_cd" # 再跑无线侧,CSMA/CA 的事件更多,耗时通常是前者的两到三倍 matlab -batch "cd('csma_sim'); main_csma_ca"跑通之后换成自己的参数,用下面这段最小脚本,一次把三个自检量打出来。这三个数的量级对不对,基本决定了后面所有曲线可不可信。
% 清空工作区,避免上一次仿真的持久变量污染结果 clear; clc; close all; % 载入参数集:'csma_cd' 走有线,'csma_ca' 走无线 cfg = config_params('csma_cd'); cfg.N = 20; % 竞争节点数 cfg.L = 1024; % 帧长,单位 bit,别填字节 cfg.R = 10e6; % 信道速率,单位 bps cfg.tau = 5e-6; % 单跳传播时延,单位 s cfg.Tsim = 1.0; % 仿真总时长,单位 s % 跑一次,拿到统计结构和逐事件日志 [stat, log] = csma_cd_core(cfg); % 三个必须打印的自检量 fprintf('发送尝试 %d, 冲突 %d, 冲突率 %.3f\n', ... stat.n_attempt, stat.n_collision, ... stat.n_collision / max(stat.n_attempt, 1)); fprintf('有效吞吐 %.3f Mbps, 信道忙占比 %.3f\n', ... stat.throughput/1e6, stat.t_busy/cfg.Tsim); fprintf('平均退避时隙数 %.2f\n', stat.mean_backoff_slots);代码逻辑很直白:config_params只负责给默认值,所有可调量集中在cfg里,主循环不出现硬编码数字;csma_cd_core返回的stat是标量统计,log是逐事件的记录矩阵,排查时对着log看某一次冲突前后的时间戳,比在核函数里加disp高效得多。
参数上最需要留意的是Tsim和帧时间的关系。单帧传输时间约等于L/R,1024 bit 在 10 Mbps 下是 102.4 μs,Tsim = 1.0意味着信道满负荷能塞下近一万帧,20 个节点分下来每节点几百帧,统计量够了。若把Tsim压到 0.01 s,20 节点下总共只发出几十帧,吞吐的随机波动能到 30% 以上,这时候看到的曲线拐点大概率是噪声不是协议特性。
tau这个参数在 CSMA/CD 里不是「随便填个很小值」的装饰品,它直接决定争用期,下一章会专门算这笔账。
3. CSMA/CD 参数落地:时隙、退避上限与冲突检测窗口
有线侧的仿真看起来比无线简单,坑却更隐蔽:参数之间是有硬约束的,违反约束不会报错,只会让冲突率悄悄偏低、吞吐悄悄偏高,曲线漂亮得让人想直接截图。这一章把三个关键参数一个个拆开:最小时隙怎么由传播时延推出来、二进制指数退避的截断点在哪、扫节点数时怎么判断曲线是真是假。
3.1 最小时隙为什么取 2τ 而不是 τ
设想两个相距最远的站点 A 和 B。A 在 t=0 开始发送,信号要经过 τ 才到达 B。如果 B 在 t=τ-ε 时刻判断信道空闲并开始发送,此时 A 的信号还没到,B 认为没有冲突——冲突实际发生在 t≈τ。而 A 要等到自己发出的信号跑到 B 再跑回来,也就是 t≈2τ-ε,才能察觉 B 的干扰。所以一个站点必须持续监听满 2τ,才能保证「只要发生冲突,我一定检测得到」。这段窗口就是争用期,以太网把它取成 512 bit 时间。
| 标称速率 | 单跳 τ 量级 | 往返 2τ | 512 bit 时间 | 对应关系 |
|---|---|---|---|---|
| 10 Mbps | 25.6 μs | 51.2 μs | 51.2 μs | 时隙≈2τ,正好卡住 |
| 100 Mbps | 2.56 μs | 5.12 μs | 5.12 μs | 网段长度相应缩短 |
| 1 Gbps | 0.256 μs | 0.512 μs | 0.512 μs | 半双工需载波扩展补足 |
表里的对应关系说明一件事:slot_time不该在配置里独立写死,它应当由tau推出来。两处各写一个数字,改了一处忘了另一处,冲突检测窗口就比时隙短,出现「迟到冲突」检测不到的情况。表现是冲突率比预期低三到五成,吞吐曲线在高负载段不下降反而持平。
function cfg = set_slot_time(cfg) % 由传播时延推导最小时隙,避免配置里出现两个互相矛盾的常量 cfg.slot_time = 2 * cfg.tau; % 争用期 = 往返传播时延 cfg.slot_bits = cfg.slot_time * cfg.R; % 折算成比特时间,便于和标准值对照 if strcmp(cfg.protocol, 'csma_cd') && cfg.slot_bits < 512 warning('时隙仅 %d bit,小于 512 bit,冲突检测窗口不足', ... round(cfg.slot_bits)); end end这段函数在config_params末尾调用一次即可。参数含义:cfg.tau单位必须是秒,cfg.R单位必须是 bps,两者相乘得到的slot_bits是无量纲的比特数,可以直接和 512 这个标准值比。警告不是形式主义,tau填成 1e-6、R填成 100e6 的时候slot_bits只有 200,这个配置下的仿真结果不能用来支撑任何结论。
顺带说一句,真实以太网还有帧间间隔和阻塞信号(jam)的开销,仿真里通常简化为一个固定开销常数加在每帧上,取值在几十 bit 量级。这一项影响的是吞吐绝对值,不影响冲突率趋势,注释里一般会标出来。
3.2 二进制指数退避的截断点与重传上限
退避规则本身很短:第 n 次冲突后,从[0, 2^min(n,10) - 1]里均匀抽一个整数 k,等 k 个时隙再重试。截断在 10 是标准做法,意味着窗口最大 1023 个时隙,10 Mbps 下约 52 ms 的等待上限。抽到第 16 次还冲突,这一帧就丢掉交给上层,仿真里记为n_drop。
function [wait_time, n_new] = backoff_exp(n_collision, cfg) % 二进制指数退避:第 n 次冲突后从 [0, 2^min(n,10)-1] 均匀抽一个时隙 if n_collision > cfg.retry_limit wait_time = Inf; % 超过重传上限,交给上层,仿真里算丢帧 n_new = n_collision; return; end k = min(n_collision, cfg.backoff_limit); % backoff_limit 默认 10 r = randi(2^k) - 1; % randi 返回 1..2^k,减 1 才是 0..2^k-1 wait_time = r * cfg.slot_time; n_new = n_collision + 1; end逻辑上有两个细节值得盯。randi(2^k)返回的是 1 到 2^k 的整数,不减 1 的话整个分布右移一个时隙,平均退避时间比理论值大,吞吐曲线整体下移几个百分点,而且这种偏差在曲线上看不出来,只能靠mean_backoff_slots和理论均值(2^k-1)/2对。第二次冲突时 k=2,理论均值 1.5 个时隙,实测偏离超过 0.2 就该去查这一行。
参数怎么改:把backoff_limit从 10 调到 4,高负载下窗口封顶在 15 个时隙,冲突率会明显上升,吞吐-节点数曲线的下降段提前出现,这是观察退避截断影响的快捷实验。把retry_limit从 16 调到 8,丢帧率上升,n_drop不再为 0,此时要确认stat.throughput的统计口径是否已经把丢弃的帧排除,否则吞吐会被高估。
还有一处容易漏:成功发送一帧之后,n_collision必须清零。不清零的话,节点会把历史上的冲突次数累积下去,窗口越滚越大,表现为低负载区吞吐偏低、时延偏高,而高负载区反而「稳定」,这种不对称很容易被误读成协议特性。
3.3 扫节点数复现吞吐-负载曲线
单个点的数字说明不了什么,扫一遍节点数才能看出趋势。扫参数时每个点用独立但可控的随机流,保证点与点之间可比,又不至于所有点共用一条随机序列导致曲线过于平滑。
N_list = [2 5 10 20 30 50]; S = zeros(size(N_list)); D = zeros(size(N_list)); for i = 1:numel(N_list) cfg = config_params('csma_cd'); cfg.N = N_list(i); cfg.Tsim = 2.0; % 每个点跑长一点,压低随机波动 rng(20240501 + i, 'twister'); % 每个点一个可复现的种子 stat = csma_cd_core(cfg); S(i) = stat.throughput / cfg.R; % 归一化吞吐,落在 0..1 D(i) = stat.mean_delay; % 平均接入时延,单位 s end yyaxis left; plot(N_list, S, '-o'); ylabel('归一化吞吐 S'); yyaxis right; plot(N_list, D, '-s'); ylabel('平均时延 (s)'); xlabel('竞争节点数 N'); grid on;这段循环里cfg.R做分母是为了把吞吐归一化到 0 到 1,方便和教科书曲线对照。Tsim取 2.0 s 而不是 1.0 s,是因为 50 节点时单点耗时会到十几秒,但方差能压下来一半,算总账是划算的。
结果形态上,归一化吞吐一般先随 N 上升,在某个点到达峰值,然后缓降。峰值位置取决于帧长和tau的比值:帧越短、传播时延越大,峰值越靠左。如果曲线一路单调上升直到 50 节点还不见顶,基本可以断定冲突没有被正确计数——要么冲突判定写成了「同一时刻到达」而没有考虑传播时延窗口,要么n_collision只统计了第一次冲突。这时候别急着调参数,回到 3.1 检查slot_time,再回 3.2 检查退避函数是否真的被调用。
时延曲线的形态更敏感:低负载段几乎平坦(主要是固定开销),过峰值后陡升,斜率的变化点通常比吞吐峰值早出现一到两个节点。两条曲线放在同一张图上,交叉位置就是判断仿真是否合理的第二个参照。
4. CSMA/CA 关键机制:IFS 优先级、退避冻结与 RTS/CTS
无线侧比有线侧多出来的东西,几乎全部围绕一件事:节点不能边发边听,所以冲突只能事后由 ACK 缺失来推断。这套「事后推断」的机制带来三个必须写对的细节:不同帧之间要有优先级差(IFS)、退避计数器要被冻结而不是清零、隐藏终端要靠握手来缓解。参数表里任何一个数字改动,都会直接改变仿真结果。
4.1 三种 IFS 的时间关系与取值
IFS 的本质是给不同帧型排一个优先级队列:SIFS 最短,ACK 和 CTS 用它,保证一次数据交换不被别人插队;DIFS 最长,普通数据帧用它。PIFS 介于两者之间,仿真里用得少,做 PCQ 之类的扩展才需要。
| 参数 | 802.11b 取值 | 802.11a/g 取值 | 说明 |
|---|---|---|---|
| 时隙 slot | 20 μs | 9 μs | 退避计数的基本单位 |
| SIFS | 10 μs | 16 μs | ACK、CTS 使用,最短间隔 |
| DIFS | 50 μs | 34 μs | 数据帧竞争使用,等于 SIFS + 2×slot |
| CWmin | 31 | 15 | 初始竞争窗口 |
| CWmax | 1023 | 1023 | 窗口上限 |
竞争窗口的更新公式是CW = min(2^n × (CWmin+1) - 1, CWmax),n 是重传次数。注意这里有个 +1,初看很别扭,原因在于 802.11 的 CW 表示的是「窗口上界」,实际抽取范围是[0, CW]。写成2^n × CWmin会让窗口整体偏小,冲突率偏高,重传次数偏多,时延曲线整体抬升。
代码里这些值应当以符号常量出现在config_params里,而不是散落在核函数中。改 DIFS 是常见实验:把 DIFS 写成SIFS + slot(少一个时隙),ACK 与数据帧的优先级差被压缩,仿真会显示冲突率随负载「温和」上升,看起来更平滑好看,但与真实网络行为不符。判断依据是打印出cfg.difs - cfg.sifs,应当正好等于两个时隙。
4.2 退避冻结、NAV 与 ACK 超时
退避计数器只在信道空闲满一个完整时隙之后才减 1,信道忙的时候冻结,值保持不变。这一条写错,整篇结论都不可信。
% 时隙级循环:忙则冻结,闲满一个时隙才递减 while node(state).backoff > 0 if channel_busy(t, cfg) % 冻结:计数器值原样保留,时间照常前进 t = t + cfg.slot_time; continue; end node(state).backoff = node(state).backoff - 1; t = t + cfg.slot_time; end % 计数器归零,进入发送流程常见的错误写法是在信道忙的分支里把backoff重新抽一次,或者直接清零。清零的后果很直接:节点每次被抢占都从一个更小的窗口重新开始,等效于无限次抢占机会,高负载下吞吐虚高,而且吞吐-节点数曲线在高负载段不下降。冻结写对之后,mean_backoff_slots在高负载下应该随 N 上升而明显变大,这个数字是判断冻结是否生效的最快指标。
NAV 是虚拟载波监听:节点收到 Duration 字段就把 NAV 定时器设上,期间即使物理信道空闲也不参与竞争。仿真里要把 NAV 和物理载波监听合成一个channel_busy判断,两者取或。只做物理监听的话,隐藏终端场景的冲突率会被低估,因为 RTS/CTS 的握手保护完全没有起作用。
ACK 超时的判定窗口是SIFS + ACK 传输时间 + 一个时隙。窗口给宽了,真冲突被当成成功(因为下一次发送开始前才判定),冲突率偏低;给窄了,正常 ACK 来不及返回,误判冲突。ACK 帧只有十几个字节,ACK 传输时间 = ACK_bits / R,在 11 Mbps 下不到 20 μs,和 SIFS 同一量级,所以这个窗口对参数很敏感,注释里通常会把三项拆开写。
4.3 RTS/CTS 开关与隐藏终端对照实验
隐藏终端的构造方式就是改可见性矩阵,让两组节点互相听不到。这里最常见的翻车是只改了一半矩阵,结果非对称:A 听不到 B,但 B 听得到 A,冲突模式完全乱掉。
cfg = config_params('csma_ca'); cfg.N = 8; cfg.vis = ones(cfg.N); % 先全连通 cfg.vis(1:4, 5:8) = 0; % 前四听不到后四 cfg.vis(5:8, 1:4) = 0; % 对称置零,别只改一半 cfg.Tsim = 2.0; cfg.use_rts = false; stat_no_rts = csma_ca_core(cfg); cfg.use_rts = true; stat_rts = csma_ca_core(cfg); fprintf('无 RTS/CTS: 冲突率 %.3f, 吞吐 %.2f Mbps\n', ... stat_no_rts.n_collision/stat_no_rts.n_attempt, ... stat_no_rts.throughput/1e6); fprintf('有 RTS/CTS: 冲突率 %.3f, 吞吐 %.2f Mbps\n', ... stat_rts.n_collision/stat_rts.n_attempt, ... stat_rts.throughput/1e6);对照结果的方向是确定的:隐藏终端场景下不开 RTS/CTS,冲突率显著高于全连通场景,因为长数据帧直接对撞,代价是一整个帧的传输时间;开了之后冲突主要发生在短的控制帧之间,单次冲突代价从千字节级降到几十字节级,冲突率下降,吞吐回升。但这不是无条件的——帧长较小的时候,RTS/CTS 的握手开销占比过高,吞吐反而低于不开。真实设备里用一个 RTS 阈值来控制,帧长超过阈值才握手,仿真里对应cfg.rts_threshold,可以拿这个阈值做一组扫描,找出当前参数下的拐点。
需要提醒的是,RTS/CTS 只解决隐藏终端,不解决暴露终端。暴露终端场景下邻居的发送被误判为干扰,节点无谓地推迟发送,吞吐下降但冲突率不升。如果你的对照实验里冲突率几乎没变而吞吐掉了两成,八成是把暴露终端当成隐藏终端在分析。
5. 排查清单:曲线反直觉时的五个常见坑
仿真跑出来的曲线和课本对不上,先别怀疑协议理论,按下面五条顺序过一遍。这五条覆盖了我见过的大多数「结果离谱但代码不报错」的情况,每一条都能用打印出来的统计量定位。
5.1 坑一:时隙按 τ 取,冲突被漏检
现象:冲突率明显低于同参数下的参考值,高负载段吞吐不降反平,mean_backoff_slots几乎不随节点数增长。
原因:slot_time被写成了cfg.tau而不是2*cfg.tau。争用期只有实际需要的一半,晚到的冲突落在检测窗口之外,节点以为发送成功,实际已经撞了。
解决:把时隙推导收敛到一个函数里,cfg.slot_time = 2*cfg.tau,并加一条slot_bits >= 512的警告。改完再跑,冲突率应当明显上升,退避均值随负载增长。
5.2 坑二:随机种子没固定,两次结果差一个量级
现象:同一份cfg连跑两次,吞吐从 6.8 Mbps 跳到 8.1 Mbps,偏差超过 15%;扫参数时曲线锯齿严重。
原因:rand和randi默认吃全局随机流,MATLAB 每次启动的初始状态不同。扫参数循环里每个点吃到的随机序列还不一样,点与点之间不可比。
解决:主脚本开头rng(20240501, 'twister');扫参数时每个点用rng(base + i),保证可复现又不完全相关;关键结论跑 5 次取均值和标准差,标准差超过均值 10% 就说明Tsim还不够长。
5.3 坑三:事件时间相同时排序不稳定
现象:两个节点在同一微秒发起发送,冲突归属每次不同,冲突率在很大区间里跳。
原因:sortrows对相同时间键保持输入顺序,而输入顺序取决于上一轮的重排结果;冲突判定又写成「取前两个」,谁在前谁吃亏。
解决:事件结构里加一个自增的seq字段,排序键用[t, seq]双键;冲突判定改成「同一时刻到达的所有节点一起判」,而不是逐个两两判。
5.4 坑四:单位混用,吞吐恒差 8 倍
现象:曲线形状完全正确,数值正好是预期的 8 倍或 1/8。
原因:帧长按字节填(1024),速率按 bps 填,L/R少乘或多乘了 8;或者tau用微秒、slot_time用秒,两个量直接相加。
解决:cfg里所有时间统一用秒、所有长度统一用 bit,函数头注释强制写单位;再加两条断言卡住量级。
assert(cfg.L > 100 && cfg.L < 1e6, 'cfg.L 单位应为 bit,不是 byte'); assert(cfg.tau > 0 && cfg.tau < 1e-3, 'cfg.tau 单位应为秒,不是微秒');5.5 坑五:仿真时长太短,预热期没剔除
现象:2 个节点的吞吐反而低于 5 个节点;重复跑方差极大;低负载区出现吞吐为 0 的点。
原因:Tsim只有几十个帧时间,随机波动主导结果;另外所有节点在 t=0 同时启动,前若干帧处于同步抢占状态,不是稳态。
解决:Tsim至少取 500 到 1000 个帧时间;统计时只计入t > 0.1*Tsim的事件;节点初始相位用随机偏移打散,别让它们从同一时刻开始监听。
6. 可复现性收尾:固定种子与三个守恒量校验
改完参数就画图,是这套源码使用中最容易吃亏的习惯。我现在每改一次cfg,都先把三个守恒量过一遍,再决定这张图要不要留。
% 三个断言,任何一条挂了都说明统计口径或单位有问题 assert(stat.n_success + stat.n_collision + stat.n_drop == stat.n_attempt, ... '帧数不守恒,检查重传和丢帧的计数位置'); assert(abs(stat.t_idle + stat.t_busy - cfg.Tsim) < 1e-9, ... '时间轴不守恒,检查事件推进有没有重复计时'); S_max = cfg.L / (cfg.L + cfg.header_bits + ... cfg.R * (cfg.difs + cfg.sifs + 2*cfg.tau)); assert(stat.throughput/cfg.R <= S_max + 1e-6, ... '吞吐超过理论上限,八成是单位或统计窗口算错');第一条是帧守恒:每一次发送尝试最终只能落到成功、冲突、丢弃三者之一,加不起来就说明某个分支漏计了。第二条是时间守恒:空闲时间加忙时间应当精确等于仿真时长,差一点点都说明事件推进时存在重复计时或者跳过了某段。第三条是吞吐上限:S_max是「一帧数据除以一帧数据加上所有固定开销」得到的理论上界,实际吞吐超过它只可能是单位问题或者统计窗口把别的时间算进了忙时间。
这三条断言的价值在于,它们和协议逻辑无关,纯粹是账要对得上。协议写错了断言照样能过,但账对不上的时候,后面所有分析都是白费。
早些年我做一组 IFS 对比实验,把 DIFS 少写了一个时隙,曲线平滑得反常,当时还当成一个好结果写进了报告,直到重新核对cfg.difs - cfg.sifs才发现差了一个 slot。从那以后我每次改完cfg都强制走一遍这三个断言,跑通了再去动plot_curve。希望帮到你。
本文还有配套的精品资源,点击获取