资讯动态

CSMA/CA与CSMA/CD协议MATLAB仿真源代码与性能曲线

发布时间:2026/10/26 2:06:26 来源:尧图企业网站定制
简介面向通信工程、计算机网络课程学习者以及需要开展协议仿真的读者这份 CSMA/CA 与 CSMA/CD 的 MATLAB 仿真源代码聚焦无线局域网中的冲突避免和有线以太网中的冲突检测可用于对照理解载波监听、退避重试、确认接收与碰撞检测等关键机制。压缩包内共 16 个文件全部为 .m 脚本或函数大小约 17KB覆盖主流程控制、节点增减、帧入队与弹出、退避时间设置、发送记录及图形化展示等模块。已有 6667 人学习下载。代码注释较为详细便于按函数调用关系梳理两种协议的仿真流程读者还可调整节点数量与相关参数观察信道占用、碰撞概率和网络效率的变化适合课程实验、自学复现与协议性能对比分析。1. 从一次两个节点同时发帧的仿真跑偏说起那天晚上调一段 CSMA/CA 的 MATLAB 仿真源代码两个节点同时发帧我盯着冲突率曲线看了十分钟发现它一直是 0。不是运气好是我把信道忙闲判定写成了只看当前时刻的静态查表节点自己发的帧根本没登记进去。这种错误在纸面推导里永远不会暴露只有真跑一遍才会翻车。这套 CSMA/CA 与 CSMA/CD 的 MATLAB 仿真源代码带详细注释做的就是这件事把有线以太网的冲突检测和无线局域网的冲突避免拆成载波监听、随机退避、冲突判定、ACK 确认四个可观察的动作用 MATLAB 脚本一步步跑出来再把吞吐量、冲突率、平均时延画成曲线。适合两类人正在学计算机网络、需要把协议流程从公式变成曲线的学生以及要给自己的调度算法做对照基线、不想从零写仿真框架的开发者。代码没有工具箱依赖一个能跑 MATLAB 的环境就够。2. 工程骨架与跑通路径从 cfg_default.m 到第一次出图拿到压缩包先别急着双击 .m 文件乱跑MATLAB 的当前文件夹和搜索路径是两个概念很多人第一次运行报未定义函数或变量八成是把脚本点开了但没把整个目录加进路径。正确的顺序是把整个文件夹设为当前工作目录确认cfg_default.m和主脚本在同一层然后运行主脚本。整个工程没有类、没有 App全是函数加脚本函数之间靠一个cfg结构体传参这点对新手很友好看不懂某个变量就去cfg_default.m里翻。2.1 目录与函数分工谁调用谁文件不多但分层很清楚脚本只负责流程编排真正干活的都在函数里。这样你改协议逻辑时不用碰绘图代码改图时也不会误伤仿真核心。路径类型作用cfg_default.m函数返回全部仿真参数的结构体CD 和 CA 共用一套字段csma_cd_main.m脚本CSMA/CD 单次演示入口跑完直接出图csma_ca_main.m脚本CSMA/CA 单次演示入口跑完直接出图csma_cd_once.m函数单节点一次完整发送监听、冲突检测、退避、重传csma_ca_once.m函数单节点一次完整发送DIFS、退避冻结、ACK 判定channel_busy.m函数判断某个时刻信道是否被占用事件表查表实现run_batch.m函数按节点数或到达率扫参数批量跑多组plot_result.m函数统一风格画吞吐量、冲突率、时延三张图调用关系是单向的*_main.m→*_once.m→channel_busy.mrun_batch.m只是把 main 里那套循环包了一层。这种结构的好处是可测你可以单独拿一个节点、一个时刻去调csma_cd_once看它到底在哪一步走岔了。2.2 参数集中配置把时隙、帧长、传播时延一次说清协议仿真里最容易出错的不是算法是单位。有人用微秒有人用秒混在一起算出来的退避时间能差六个数量级曲线看起来有点怪但说不上哪怪。所以这套代码要求所有时间一律用秒注释里再标一遍微秒换算。function cfg cfg_default(protocol) % CFG_DEFAULT 返回仿真参数结构体 % protocol: cd 有线以太网 | ca 无线局域网 % 注意所有时间字段单位统一为秒注释里给出常用值的微秒换算 if nargin 1, protocol cd; end cfg.protocol protocol; cfg.nNode 8; % 竞争节点数 cfg.nFrame 200; % 每节点待发帧数够统计即可别贪多 cfg.frameLen 1500 * 8; % 帧长bit按 1500 字节算 cfg.seed 2024; % 随机种子固定后结果可复现 if strcmpi(protocol, cd) cfg.rate 10e6; % 10 Mbit/s cfg.slotTime 51.2e-6; % 最小时隙 51.2 us cfg.propDelay 25.6e-6; % 单程传播时延 25.6 us冲突窗口 2*tau cfg.ifsTime 9.6e-6; % 帧间间隔 cfg.jamTime 3.2e-6; % 强化冲突的干扰信号时长 cfg.maxRetry 16; % 最大重传次数超了就丢帧 cfg.backoffCap 10; % 退避阶数截断k 最大取 10 else cfg.rate 54e6; % 54 Mbit/s cfg.slotTime 9e-6; % 时隙 9 us cfg.propDelay 1e-6; % 无线传播时延极短按 1 us 估 cfg.difs 34e-6; % DIFS cfg.sifs 16e-6; % SIFS cfg.ackTime 24e-6; % ACK 帧传输时间含前导 cfg.cwMin 15; % 最小竞争窗口 CWmin 31 - [0,15] cfg.cwMax 1023; % 最大竞争窗口 cfg.maxRetry 7; % 短重传计数超了丢帧 cfg.useRtsCts false; % RTS/CTS 开关验证隐藏终端时打开 end cfg.frameTime cfg.frameLen / cfg.rate; % 单帧传输时间两条协议都要用 endframeTime是后面所有统计的分母一帧 12000 bit 在 10 Mbit/s 下要 1.2 ms在 54 Mbit/s 下只要 222 us这个差距直接决定了两种协议的效率天花板不在一个量级。backoffCap对应标准里的退避阶数截断不截断的话第 16 次重传的随机窗口能到 65535 个时隙仿真会跑到你怀疑人生。seed这个字段看着不起眼但它是后面排查问题的后悔药结果对不上时第一件事就是确认两边种子一致。2.3 三条运行路径单次演示、批量扫描、出图跑通分三个层次建议按顺序来别一上来就跑批量。第一步验证单次发送逻辑。在命令行里直接调一次单节点发送看返回的成功标志和结束时刻是否合理cd /path/to/csma_sim % 先切到工程根目录 addpath(genpath(pwd)) % 把子目录也加进搜索路径 cfg cfg_default(cd); % 取 CD 参数 [succ, nRetry, tEnd] csma_cd_once(cfg, 0); fprintf(成功%d 重传%d 结束于 %.6f s\n, succ, nRetry, tEnd);如果tEnd小于cfg.frameTime说明你的时间推进有问题帧还没发完就返回了如果nRetry恒等于 0说明冲突判定根本没触发多半是channel_busy永远返回 false。第二步跑完整主脚本出图。csma_cd_main.m里是一个事件循环按泊松到达生成每个节点的首帧时刻然后逐事件推进仿真时钟run(csma_cd_main.m) % 出 CD 的三张图 run(csma_ca_main.m) % 出 CA 的三张图第三步扫参数。run_batch.m会把节点数从 2 扫到 32每个点跑若干次取均值适合做论文里的对比曲线rng(2024); % 批量跑之前锁种子否则每次曲线都在抖 res run_batch(cd, 2:2:32, 20); % 协议、节点数向量、每点重复次数 plot_result(res); % 横轴节点数纵轴吞吐量/冲突率/时延提示run_batch第二个参数是节点数向量第三个是重复次数。重复次数低于 10 时曲线毛刺明显别急着下结论说协议性能不稳定先加到 20 次以上再看。3. CSMA/CD 核心链路拆解监听、冲突检测、二进制指数退避有线侧的逻辑比无线简单因为它有一个物理前提边发边听冲突能在几微秒内被发现发现后立刻中止损失可控。整个流程就是听—发—听—退避—重发的循环难点全在时间边界上。下面按一次发送的生命周期拆。3.1 冲突窗口 2τ 与时隙怎么定以太网有个反直觉的结论帧发出去之后最坏情况下要等 2 倍端到端传播时延才能确定自己有没有撞车。原因是冲突信号从最远端的节点传回来需要 τ而对方检测到你也要 τ。所以冲突窗口就是 2τ帧长必须保证在 2τ 内发不完否则发送方会在冲突信号回来之前就把帧发完冲突检测失效。代码里这个约束体现为slotTime的取值。10 Mbit/s 以太网的最小帧 64 字节传输时间 51.2 us正好等于 2τ 的往返预算于是时隙就定成 51.2 us。写仿真时不用自己推但必须理解slotTime不是随便取的数它是冲突窗口的时间粒度退避抽到的每个槽都是它的整数倍。% 冲突窗口判定在 tStart 到 tStart2*tau 之间有没有别的节点也在发 window [tStart, tStart 2 * cfg.propDelay]; if channel_busy(cfg, window) % 撞了立刻停发改发 jam 信号把冲突坐实 endchannel_busy接收的是一个时间区间而不是单个时刻这个设计很关键。很多初版代码只判断tStart那一个点结果两个节点相差 1 us 发帧就被判成不冲突冲突率统计直接失真。3.2 二进制指数退避 BEB 的实现冲突之后的处理是整个 CD 的灵魂。第一次冲突从 {0,1} 里抽一个数当时隙数等待第二次从 {0,1,2,3} 里抽第 k 次冲突从 [0, 2^k - 1] 里抽。这样冲突越多等待范围越大把重负载下的碰撞概率压下去。k min(nRetry, cfg.backoffCap); % 阶数截断防止窗口爆炸 slots randi([0, 2^k - 1]); % 均匀抽一个退避时隙数 t t slots * cfg.slotTime; % 换算成真实等待时间 nRetry nRetry 1; % 重传计数加一min(nRetry, cfg.backoffCap)这一行是标准做法backoffCap取 10 时最大窗口 1023 个时隙10 Mbit/s 下大约 52 ms已经足够把冲突摊开。不截断的话第 16 次重传窗口能到 65535 槽约 3.3 秒仿真里会表现为个别节点长时间沉默吞吐量曲线出现莫名其妙的凹陷。我一般会把backoffCap做成可配参数方便对比截断和不截断的差别这个对比本身就是个不错的实验题。3.3 一次发送的完整代码走读把监听、发送、冲突检测、退避串起来就是下面这个函数。注释写得比较密改的时候不容易踩空。function [succ, nRetry, tEnd] csma_cd_once(cfg, tNow) % CSMA_CD_ONCE 单节点一次完整发送过程1-坚持式 二进制指数退避 % cfg : 参数结构体见 cfg_default.m % tNow : 该节点产生首帧的时刻单位秒 % succ : 是否最终发送成功nRetry : 实际重传次数tEnd : 结束时刻 nRetry 0; succ false; t tNow; while nRetry cfg.maxRetry % 1) 载波监听信道忙就一直听按最小时隙步进 while channel_busy(cfg, [t, t cfg.slotTime]) t t cfg.slotTime; end % 2) 空闲后等一个帧间间隔给上一帧留出恢复时间 t t cfg.ifsTime; tStart t; % 3) 边发边听2*tau 内没冲突才算真正抢到信道 tConf tStart 2 * cfg.propDelay; if channel_busy(cfg, [tStart, tConf]) % 4) 冲突发 jam 强化信号再抽随机退避 t tConf cfg.jamTime; k min(nRetry, cfg.backoffCap); slots randi([0, 2^k - 1]); t t slots * cfg.slotTime; nRetry nRetry 1; continue; % 回到监听重来一轮 end % 5) 无冲突把整帧发完 t tStart cfg.frameTime; succ true; break; end tEnd t; end三个参数值得单独盯一下。tConf是冲突判定的截止点它必须严格等于tStart 2*propDelay写成propDelay就等于把冲突窗口砍半冲突率会偏低。cfg.jamTime是强化冲突信号作用是让网络上所有节点都明确感知到撞了不加这一段远端节点可能以为信道一直空闲。continue那行返回循环顶部重新监听而不是直接 sleep 到t再发因为退避期间信道可能被别人占了必须重新判断。3.4 冲突域与信道利用率统计成功率不是唯一指标真正有价值的是信道利用率有效传输时间除以总仿真时长。统计的写法很直接把所有节点的有效帧时间累加再除以最后一个节点结束的时刻。totalBusy 0; for i 1:cfg.nNode t arrival(i, 1); % 该节点首帧到达时刻 for f 1:cfg.nFrame [ok, ~, tEnd] csma_cd_once(cfg, t); if ok, totalBusy totalBusy cfg.frameTime; end t max(tEnd, arrival(i, f1)); % 下一帧到达取较晚者 end end util totalBusy / maxT; % maxT 为全部节点结束的最大时刻max(tEnd, arrival(i, f1))这一句容易漏。节点发完上一帧后下一帧可能还没到直接拿tEnd当下一帧的起点会凭空多算负载节点数一多曲线就偏得离谱。4. CSMA/CA 核心链路拆解DIFS/SIFS、随机退避与 ACK 闭环无线侧没有边发边听这个能力天线收自己的发射信号会饱和根本听不出别人有没有在发。所以 CA 换了个思路发之前尽量避开发之后靠 ACK 确认没收到 ACK 就当撞了退避重来。代价是开销比 CD 大得多。4.1 DIFS/SIFS 与竞争窗口 CW 的配合802.11 用两档帧间间隔制造优先级SIFS 最短给 ACK、CTS 这类必须立刻回的控制帧DIFS 较长给普通数据帧竞争用。一个节点想发数据必须连续监听信道空闲 DIFS 时长才允许进入退避。退避计数器从 [0, CW] 里抽一个整数每过一个空闲时隙减一减到 0 才能发。backoff randi([0, cfg.cw]); % 初始退避计数cw 初值等于 cwMin while backoff 0 if channel_busy(cfg, [t, t cfg.slotTime]) t wait_idle(cfg, t, cfg.difs); % 遇忙冻结等下一次空闲 DIFS end t t cfg.slotTime; % 空闲一个时隙 backoff backoff - 1; % 计数器减一 endcwMin设成 15 意味着初始窗口 [0,15] 共 16 个槽这个值不是随便定的窗口太小撞车多窗口太大空等久。wait_idle里那个 DIFS 参数也不能省冻结之后重新竞争需要再次确认信道持续空闲否则会出现忙完立刻发的抢跑行为冲突率会异常升高。4.2 退避计数器冻结CA 和 CD 最大的结构差异CD 在退避期间如果信道变忙它只是继续等待之前抽的槽数并不保留一旦空闲重新开始监听。CA 不一样退避计数器是冻结的信道忙的时候计数器停住等信道再次空闲 DIFS 之后从上次停下的地方接着往下减不重新抽。这个差异在代码里体现为循环结构。上面的while backoff 0里遇忙只是把时间推进到下一个空闲点backoff的值不动下一轮循环继续减。如果写成了backoff randi(...)重新抽那就是每次遇忙都重置退避实际效果是重负载下节点几乎永远发不出去吞吐量断崖式下跌。我第一次手写这版的时候就栽在这跑出来的曲线比理论值低一个数量级查了两个小时才发现是重新抽取。4.3 ACK 闭环与重传上限数据帧发完之后接收端等 SIFS 回一个 ACK。发送端如果在超时时间内没收到就判定失败把竞争窗口翻倍再重试。function cfg csma_ca_once(cfg, tNow) % 返回值改成 cfg是因为竞争窗口 cw 需要跨帧保持 t wait_idle(cfg, tNow, cfg.difs); backoff randi([0, cfg.cw]); while backoff 0 if channel_busy(cfg, [t, t cfg.slotTime]) t wait_idle(cfg, t, cfg.difs); end t t cfg.slotTime; backoff backoff - 1; end tStart t; t tStart cfg.frameTime cfg.sifs cfg.ackTime; if ack_received(cfg, tStart, t) % 模拟 ACK 是否按时到达 cfg.cw cfg.cwMin; % 成功窗口复位 cfg.succCount cfg.succCount 1; else cfg.cw min(2 * cfg.cw 1, cfg.cwMax); % 失败窗口翻倍 cfg.failCount cfg.failCount 1; end end注意函数签名返回的是cfg而不是单个状态。cw必须挂在这个结构体上跨帧传递否则每帧都从cwMin开始退避机制等于没生效。这是 MATLAB 值传递带来的典型坑后面避坑那章还会再提一次。窗口翻倍用的是2*cw 1而不是2*cw因为窗口大小是槽数加一这个细节对不上重传几次之后窗口值就和标准差了 1长时间仿真下会累积成可见的偏差。4.4 RTS/CTS 开关与隐藏终端验证cfg.useRtsCts默认关着打开之后每次发数据前先发一个 RTS接收端回 CTS周边节点听到 CTS 就设置 NAV虚拟载波监听在这段时间内不发起竞争。这一套是给隐藏终端准备的A 和 C 互相听不到但都能和 B 通信不加 RTS/CTS 的话两边同时发给 B在 B 处撞车而 A、C 自己浑然不觉。验证方法很直接构造三个节点的场景把 A 和 C 的传播关系在channel_busy里掐断跑两组对比cfg cfg_default(ca); cfg.nNode 3; cfg.hiddenPair [1 3]; % 标记 1 和 3 互为隐藏终端 r1 run_batch(cfg, 3, 20); % useRtsCts false cfg.useRtsCts true; r2 run_batch(cfg, 3, 20); % useRtsCts true正常情况下打开 RTS/CTS 后冲突率应该明显下降代价是吞吐量因为多了两次握手而略降。如果两组结果一模一样说明hiddenPair没被channel_busy真正读取或者 NAV 的生效时间没算进信道占用表。5. 跑仿真时最容易翻车的几个地方排查清单下面五条都是我或者身边人真实踩过的按出现频率排遇到问题先对着扫一遍比盲改代码快得多。5.1 冲突率恒为 0所有节点都运气很好现象不管节点数调到 32 还是 64failCount一直是 0吞吐量曲线平滑得像理论值。原因channel_busy判断的是历史事件表但主循环里没有把当前节点正在发送这件事写回表里导致每个节点看信道都是空的自然不会冲突。解决在每个节点开始发送前把[tStart, tStartframeTime]这段占用登记到全局占用表接收端回 ACK 的时间和 jam 信号时间也要登记。登记完再跑一次节点数超过 8 之后冲突率应该开始爬升。5.2 仿真跑了几分钟还没结束现象命令行光标一直闪run_batch卡在第 3 个参数点上不动。原因while循环里某条分支没有推进时间。最常见的写法是检测到信道忙之后只做了continuet一点没变循环原地打转。解决所有while循环体里必须保证t严格递增最省事的做法是在循环末尾加一句断言assert(tNext t, time not advancing)。另外检查单位slotTime写成 51.2 而不是 51.2e-6 的话时间会被推进到几万秒表现也是跑不完。5.3 CSMA/CD 的吞吐量反而低于 CSMA/CA现象两组曲线放在一张图上有线那条明显更低和直觉相反。原因两条协议用了不同的速率和帧长CD 是 10 Mbit/s、CA 是 54 Mbit/s直接比绝对吞吐量没有意义。另外 CD 侧如果忘了给冲突加jamTime冲突处理的开销被低估也会让数字对不上。解决统一口径要么都算归一化吞吐量有效传输时间 / 总时间要么把速率对齐。对比图里我一般用归一化值横轴节点数、纵轴利用率这样才看得出CD 在高负载下效率下滑更陡这个结论。5.4 竞争窗口 CW 不增长重传次数打满现象CA 场景里cfg.cw始终等于cwMin失败帧全部撞到maxRetry上限被丢弃。原因MATLAB 函数默认值传递在csma_ca_once里改了cfg.cw但调用方拿到的还是旧结构体。解决函数签名改成function cfg csma_ca_once(cfg, tNow)调用处写成cfg csma_ca_once(cfg, t)。这个坑特别隐蔽因为函数内部打印出来的cw是对的只有跨帧看才露馅。5.5 每次运行曲线都不一样图对不上现象同一份代码跑两遍同一参数点差 5% 以上。原因没有固定随机种子randi和exprnd每次都走不同的随机流。解决在*_main.m和run_batch.m的开头各加一句rng(cfg.seed)。注意run_batch内部如果并行开了parfor每个 worker 的随机流是独立的得用parfor配合RandStream单独设种子否则复现性还是保不住。注意这五条里前两条属于逻辑没跑通后三条属于跑通了但结果不可信。前者看输出报错和死循环后者只能靠对照理论值建议每改一版都拿两节点的最简单场景验一次看看冲突率和理论值是否在同一量级。6. 二次开发把两种协议塞进同一个坐标系代码跑通之后多数人下一步想做的是对比。但两个主脚本各跑各的参数、统计口径、绘图样式都不统一直接拼图会很别扭。我的做法是抽一个统一的驱动器把协议名当参数传进去其他全走同一套统计。对比维度CSMA/CDCSMA/CA冲突处理时机发送中检测立即中止发送前避免靠 ACK 事后判定退避触发条件检测到冲突未按时收到 ACK固定开销2τ 冲突窗口DIFS 退避 SIFS ACK隐藏终端不存在该问题需 RTS/CTS 缓解高负载表现利用率下滑相对平缓碰撞加剧时延上升更快统一驱动的写法不复杂核心是把csma_cd_once和csma_ca_once包成同一个接口function res run_unified(protocol, nNodeList, repeat) % RUN_UNIFIED 两种协议共用一套统计与出图流程 res struct(nNode, [], util, [], collision, [], delay, []); for n nNodeList utilAcc 0; colAcc 0; dlyAcc 0; for r 1:repeat rng(2024 r); % 每个重复点独立种子保证可比 cfg cfg_default(protocol); cfg.nNode n; m run_core(cfg); % 内部按 protocol 分派 utilAcc utilAcc m.util; colAcc colAcc m.collision; dlyAcc dlyAcc m.delay; end res.nNode(end1) n; res.util(end1) utilAcc / repeat; res.collision(end1) colAcc / repeat; res.delay(end1) dlyAcc / repeat; end endrun_core里按cfg.protocol分派到两个*_once函数统计字段名保持一致这样plot_result一行都不用改就能画两组曲线。种子用2024 r而不是固定值是为了让重复实验之间独立但对两种协议是同一组种子配对比较更公平。改完之后一定要验证把nNode设成 1 跑一次两种协议的冲突率都应该是 0利用率应该接近frameTime / (frameTime 固定开销)。这个单节点基线如果对不上说明统计公式写错了后面多节点的曲线全都不作数。另一个值得试的改造是把cfg从结构体换成containers.Map或者句柄类彻底绕开值传递的问题。不过说实话结构体返回这种写法虽然土但改起来最省事我一般不折腾。从那以后我每次加新参数都强制先跑一遍单节点基线再动多节点这个习惯帮我省掉了至少三次通宵找 bug 的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑