ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CSMA/CA协议MATLAB仿真实现:从状态机到性能分析

2026/9/2 19:46:25 拓冰建站 浏览量
CSMA/CA协议MATLAB仿真实现:从状态机到性能分析 简介一套基于MATLAB的CSMA/CA载波监听多路访问/冲突避免协议仿真代码包面向无线通信课程学习者、网络协议研究者以及需要快速上手MAC层仿真的工程师。资源将载波监听、冲突避免、RTS/CTS握手等核心流程做了模块化实现并给出AP模式、AdHoc模式下的对比结果。压缩包共22个文件以12个m脚本为主涵盖节点生成、指数分布、队列管理、延迟缓冲等仿真模块另含3张fig结果图、6个txt统计输出和1份ReadMe说明文档便于读取实验数据与复现图形。整体仅35KB结构轻量、逻辑清晰。包含完整的CSMA/CA仿真源码与配套结果文件可帮助深入理解协议在吞吐量、平均延迟、数据包重发率等关键指标上的表现也能作为课程设计或小规模无线网络性能评估的参考基座。已有2750人学习下载适合作为初学入门和二次开发的起点。 做无线网络仿真这些年CSMA/CA协议算是绕不开的基础课。不管是写毕业设计、发论文还是做工程预研只要你涉及Wi-Fi、ZigBee、LoRa这类免授权频段的通信系统最终都会落到一个问题在这套“先听再说”的访问机制下系统到底能跑出多大的吞吐量、丢包率有多高、节点多了之后会怎么恶化。要回答这些最省事也最直观的手段就是用MATLAB搭一个事件驱动的模拟仿真环境。这篇东西我准备把一套可以直接跑的CSMA/CA协议MATLAB仿真代码完整拆开从协议状态机的设计思路到二进制指数退避的具体实现再到统计吞吐量、碰撞概率这些核心指标的计算方法全部过一遍。适合正在做网络仿真课设、准备通信方向复试、或者刚接触无线接入协议想找个切入点练手的读者。代码我会给关键片段完整的框架和参数配置也会一并说明保证你拿回去能跑出图、能改参数、能看懂每一个变量在干嘛。1. CSMA/CA协议先搞明白它在解决什么问题1.1 无线信道的“隐藏终端”困境有线和无线最大的区别在于有线网络里你发数据数据沿着线走基本不会跟别人的数据“撞车”但无线信道是开放的所有节点共享同一段频谱资源。两个节点同时发数据信号在空中叠加接收方就什么都解不出来这就是冲突。以太网用的CSMA/CD冲突检测方案里节点发数据的同时还能检测到冲突一旦发现就立刻停手退避。但无线环境做不了这件事因为无线收发普遍是半双工的——发送的时候天线在发射接收链路被自己发射的信号淹没了根本听不到别人有没有在同一时刻发数据所以只能退而求其次用“冲突避免”的思路把碰撞扼杀在发送之前。更麻烦的是隐藏终端问题。假设节点A在跟AP通信节点C也在跟同一个AP通信A和C离得比较远彼此听不到对方的信号。对A来说信道是空的对C来说信道也是空的两个人都以为可以放心发结果数据在AP那里撞了个正着。CSMA/CA机制里的RTS/CTS握手、随机退避这些设计从根上说都是为了在这种半双工、不可靠侦听的物理条件下尽量降低碰撞概率而不是彻底消灭碰撞。1.2 这套协议的三板斧先听后发、随机退避、确认重传CSMA/CA的核心动作可以拆成三个第一是载波侦听节点在发数据之前先监听信道判断介质是否空闲。判断方式可以是物理层的能量检测或载波检测也可以是MAC层的虚拟载波侦听也就是通过NAVNetwork Allocation Vector网络分配向量来模拟信道被占用的剩余时间。第二是帧间隔信道从忙变闲之后节点不能立刻发送必须等一段固定长度的时间这叫帧间间隔不同优先级的帧用不同的间隔来区分。第三是随机退避这也是整套机制里最有意思的部分每次发数据之前节点从一个竞争窗口里随机抽一个退避计数信道空闲一个时隙计数器减一减到零才允许发送。撞上了就翻倍扩大窗口再重抽这就是二进制指数退避。这三个动作配合起来效果就是大家都在等待但每个人等待的随机时间不一样因此再次同时抢到信道的概率被大幅摊薄。可以说退避算法是整个协议吞吐量性能的调节阀。1.3 为什么适合用MATLAB做这个仿真如果你问能不能用Python、NS-3、OMNeT做当然都能做而且NS-3这类专业网络仿真器能做更细的物理层建模。但MATLAB的优势在于一是上手门槛低语法直观不需要像NS-3那样先适应C和仿真框架的复杂度二是矩阵化思维天然适合做统计——你把节点状态全部定义成结构体数组或矩阵每个时隙统一更新代码量非常可控三是画图方便一个plot就能出曲线吞吐量、延迟、碰撞率的趋势一眼就能看到四是毕业论文和课程设计里MATLAB仿真是最常见的验收形式你不需要额外引入别的工具链。我个人的建议是如果研究目标是协议机制层面的对比比如不同退避参数下的性能差异、隐藏终端比例的影响MATLAB完全够用如果要做跨层交互、TCP/IP协议栈联合仿真再考虑NS-3不迟。2. 仿真整体架构从状态机到事件调度2.1 离散时间驱动还是事件驱动网络仿真只有两种主流模型一个是离散时间驱动的一个是离散事件驱动的。离散时间驱动最简单你把时间切成固定长度的时隙每个时隙内检查一次所有节点的状态更新它们的退避计数、发送状态。事件驱动则更精细系统里维护一个事件队列只有事件发生时才跳跃式推进时间比如“节点A在t0.5s开始发送”下一个事件是“t0.7s发送完成”。做CSMA/CA仿真我建议你直接上离散时间驱动原因很直接协议本身的信道访问粒度就是时隙退避计数按时隙递减时间驱动模型跟协议天然对齐实现简单代码清晰。事件驱动虽然执行效率更高但事件类型的管理要先设计清楚发送完成、退避结束、ACK超时、排队丢包这些事件都要考虑代码复杂度明显上一个台阶。对于节点数不超过50、仿真时长在几十秒级别的典型场景离散时间驱动在MATLAB里跑起来也就几分钟的事没必要为了性能牺牲代码可读性。我用的模型里一个时隙的时间设成固定值所有节点的动作都对齐到这个时隙边界上。这样做的代价是精度上有一点损失因为真实系统里节点不是全局同步的每个节点的退避起始时刻可能有亚时隙级别的差异但研究网络层性能趋势时这种简化完全可接受。2.2 节点状态机的设计每个节点在仿真里都是一个有限状态机状态分成四类IDLE是空闲态节点没有数据要发或者数据发完等待新的数据包到达。SENSING是载波侦听态节点有数据要发正在监测信道是否空闲。BACKOFF是退避态节点已经确认信道空闲开始随机退避倒计时。TRANSMITTING是发送态节点正在占用信道发送数据帧或控制帧。状态之间的切换逻辑是有新包产生或上个包发送失败需要重传时进入SENSINGSENSING期间如果检测到信道忙保持监听一旦信道空出来且持续空闲了DIFS的时间长度才转入BACKOFFBACKOFF里每个空闲时隙将退避计数器减一如果中途信道变忙立刻冻结计数挂在当前值上等信道重新空闲且过了DIFS之后继续倒数计数器减到零自动触发发送进入TRANSMITTING发送完成后等待ACK如果收到ACK就回到IDLE收不到就指数退避窗口翻倍重新走一轮。这个状态机的设计里有一个特别容易忽略的细节SENSING和BACKOFF的边界。很多人把信道空闲就直接跳去退避忽略了DIFS的等待过程。虽然DIFS在仿真里只是几个时隙但它是决定优先级的关键机制。你说MAC层怎么区分数据帧和控制帧就是靠不同的帧间隔。所以这个细节不能省否则你仿真的协议严格来说已经“不是”CSMA/CA了。2.3 停止等待ARQ与ACK超时数据帧发出去之后到底成没成功在仿真里你需要一个确认机制。现实里节点发完数据之后启动ACK计时器如果在规定时间内没收到接收方的ACK帧就判定发送失败。仿真里我没有精确到微秒去模拟ACK帧的解码过程而是用一个概率模型如果发送期间没有其他节点同时发送并且接收方处于可接收状态就认为接收成功节点在一个固定时间间隔后收到ACK如果发生了碰撞就认为ACK永远不会到达发送节点在ACK超时后进入重传。这个简化是否合理我觉得在MAC层的性能分析里是完全合理的因为你关心的核心指标——吞吐量、碰撞率、重传次数——都是由信道竞争行为决定的而信道竞争最直接的表现就是同时发送这一事件本身。至于物理层的调制方式、信噪比、误码率对接收成功率的影响那属于PHY层建模的范畴如果你要做跨层仿真可以再往这个模型里加一个误码率查表函数。3. 核心代码实现一步一步把框架搭起来3.1 参数定义与初始化建模第一步是把系统参数全部定义清楚。我把所有参数集中放在最前面方便后续修改。你需要关注的几个核心参数是节点数、信道速率、数据帧长度、控制帧长度、DIFS/SIFS长度用时隙数表示、最小竞争窗口、最大竞争窗口、仿真总时长。%% 系统参数 N 20; simTime 10; % 仿真时长单位秒 slotDuration 20e-6; % 一个时隙时长20us DIFS 2; % DIFS时长单位时隙 SIFS 1; % SIFS时长单位时隙 CWmin 32; % 最小竞争窗口 CWmax 1024; % 最大竞争窗口 dataRate 1e6; % 信道速率 1Mbps dataLen 1024 * 8; % 数据帧长度 1024字节 ackLen 14 * 8; % ACK帧长度 14字节 %% 节点状态结构体初始化 nodes(N) struct(state, 0, ... cw, CWmin, ... backoff, 0, ... hasPacket, 0, ... packetCount, 0, ... txCount, 0, ... collisionCount, 0, ... txStart, 0, ... txEnd, 0, ... ackTimeout, 0, ... queueLength, 0);这里的state我用数字代替枚举0是IDLE1是SENSING2是BACKOFF3是TRANSMITTING。MATLAB的struct数组访问字段比较耗时节点数多的时候可以考虑用普通的数值数组分别存每个属性速度会更快但可读性会下降。对于教学和常规实验结构体数组最直观。有一个参数我建议你重点理解CWmin和CWmax。IEEE 802.11b标准里CWmin是31CWmax是1023我代码里写32和1024是为了计算方便实际效果差别不大。二进制指数退避的意思就是每次碰撞后竞争窗口翻倍cw min(cw*2, CWmax)重置时回到CWmin。这个翻倍再封顶的逻辑是防止一个节点连续碰撞导致无限扩张也是保证信道公平性的关键——窗口太大发送延迟太久窗口太小碰撞概率飙升。3.2 主循环时隙推进与状态更新主循环是整个仿真的发动机。每迭代一次时间推进一个时隙然后遍历所有节点根据当前状态执行对应动作。这里直接上核心代码注释里我把每一步的逻辑都写清楚。totalSlots round(simTime / slotDuration); channelBusy false; % 当前信道是否有节点正在发送 successPackets 0; % 成功接收的数据帧数 collisionPackets 0; % 碰撞次数统计 for slot 1:totalSlots % 阶段1检查信道状态 % 是否有节点正在发送遍历所有发送态节点看发送是否完成 txNodes find([nodes.state] 3); channelBusy ~isempty(txNodes); % 如果正在发送的节点超过一个说明发生了碰撞 if length(txNodes) 1 collisionPackets collisionPackets 1; % 发送碰撞的节点都要重来 for idx 1:length(txNodes) nid txNodes(idx); nodes(nid).state 1; % 回到载波侦听 nodes(nid).cw min(nodes(nid).cw * 2, CWmax); % 指数退避 nodes(nid).backoff randi([0, nodes(nid).cw-1]); nodes(nid).collisionCount nodes(nid).collisionCount 1; end end % 阶段2更新各节点状态 for i 1:N switch nodes(i).state case 0 % IDLE % 随机产生新数据包 if rand arrivalRate nodes(i).hasPacket 1; nodes(i).state 1; end case 1 % SENSING % 信道空闲时计时DIFS if ~channelBusy nodes(i).difsCounter nodes(i).difsCounter 1; if nodes(i).difsCounter DIFS nodes(i).state 2; nodes(i).backoff randi([0, nodes(i).cw-1]); end else nodes(i).difsCounter 0; % 信道忙重置DIFS计时 end case 2 % BACKOFF if ~channelBusy % 空闲时隙退避计数器减一 nodes(i).backoff nodes(i).backoff - 1; if nodes(i).backoff 0 nodes(i).state 3; nodes(i).txStart slot; nodes(i).txEnd slot ceil((dataLen/dataRate)/slotDuration); end end % 信道忙则冻结退避不做任何操作 case 3 % TRANSMITTING % 发送完成后回到IDLE等待ACK if slot nodes(i).txEnd nodes(i).state 0; nodes(i).hasPacket 0; nodes(i).cw CWmin; successPackets successPackets 1; nodes(i).packetCount nodes(i).packetCount 1; end end end end这段代码是一个能跑通的最小骨干版本很多细节做了简化。比如ACK超时机制在主干代码里没有单独建模发送完成后默认都成功要模拟碰撞导致的丢包就要靠发送态节点同时存在这个判断。我建议你在自己复现的时候先跑通这个版本确认曲线形态合理再往里面加丢包重传、队列管理这些复杂功能。主循环里最大的坑是MATLAB的find([nodes.state] 3)这种向量化操作。node数少没事如果上了几百个节点每时隙做一次全量find性能会非常难看。优化方式是把节点状态单独存成一个1xN的数组每时隙用逻辑索引更新最后再同步回结构体。3.3 碰撞检测的边界细节碰撞检测这部分我认为是整个代码逻辑里最需要在脑子里面盘一遍的地方第一种情况两个节点在同一个时隙同时进入发送态它们从第一个时隙开始就重叠。这种情况在阶段1的检测里能抓出来因为同时有两个节点处于TRANSMITTING状态。第二种情况节点A从时隙100开始发送节点B的退避计数器减到0是在时隙105。真实的无线环境里B在中途接入肯定已经感知到信道忙退避本身应该被冻结。但我的简化代码里退避冻结只发生在阶段2检查当帧信道状态时如果B在时隙104检查时信道还是空闲的104结束时计数器减到0105这个时隙B就盲目地发出了数据跟A撞在一起。为了避免第二种误判我在实际用的代码里加了“未来信道占用表”的机制。每个时隙开始时先处理所有发送事件计算哪些时隙已经被标记为忙然后把忙信息的判断提前到退避递减之前。更优雅的做法是用一个二维数组维护每个时隙的发送节点ID列表这样既能判碰撞又能统计信道占用率。这里我建议你按自己的目标取舍如果只是验证协议原理简化版本完全够用如果你要把仿真结果跟论文里的理论曲线对比建议加上未来信道占用表否则重负载下仿真吞吐量会偏低撞得比理论模型更狠。4. 指标统计与结果可视化4.1 吞吐量怎么算才严谨仿真跑完统计是重头戏。网络仿真最核心的三个指标是吞吐量、碰撞率和平均访问延迟。吞吐量的定义是单位时间内成功传输的数据量throughput successPackets * dataLen / simTime;但注意这里的successPackets是仿真期间所有节点成功发送的数据帧总数。dataLen是数据部分长度不包含MAC头、PHY头和控制帧开销。如果你要算跟实际设备吞吐量更贴近的指标应该把MAC头通常24字节、PHY前导码通常192us占用时间都算进开销里得到的是“有效载荷吞吐量”和“信道占用吞吐量”两个口径。写论文的时候一定要在正文里说清楚你统计的是哪一个很多评审会盯这个细节。我在代码里把统计逻辑做成了每0.5秒一个统计窗口滑动输出当前窗口内的吞吐量和碰撞次数。这样你能看到系统从启动到稳定的过程而不是只拿到一个10秒的平均值。瞬态和稳态分开看很多性能问题才能暴露出来。比如你会发现启动阶段的第一秒吞吐量偏低因为节点刚开始排队、窗口还没收敛等系统进入稳态后吞吐量才代表协议的真实能力。4.2 不同节点数下的性能对比曲线拿这套仿真做了一组不同节点数的对比实验节点数从5到50步长5每个配置跑10次取平均。这个实验做出来的曲线能非常直观地展示协议的一个核心特征节点少的时候吞吐量随着节点数上升而上升因为更多节点意味着更多数据要传信道不容易空转但节点数超过20之后吞吐量增长放缓甚至掉头向下因为碰撞开始占用大量信道时间退避时间也在不断加长。这种“先升后降”的曲线形态是CSMA/CA协议性能的最典型特征。你拿这个趋势跟教材里的理论曲线对比会发现仿真值和理论推导的Bianchi模型曲线在重负载区间有偏差——主要原因是我这个简化模型里没有考虑物理层的前导码开销和MAC层重传上限。如果你仿真结果里下降斜率比理论曲线陡先别急着怀疑代码写错了先检查自己的模型里有没有把重传次数上限设成无限、有没有把每个节点默认只有一个包之后立刻可以产生新包这些假设说得清。画图方面我做了三张标准图第一张是吞吐量随节点数变化曲线第二张是碰撞率随节点数变化曲线第三张是单个节点发送机会的公平性指标——算了每个节点成功发送数量的标准差。第三张图往往是被忽略但很重要的因为很多接入协议在小规模下性能不错节点一多就出现“饿死”现象有的节点疯狂抢信道有的节点长期得不到发送机会。标准差就是衡量这个现象的直接数字。%% 多节点数批量仿真 nodeRange 5:5:50; avgThroughput zeros(size(nodeRange)); avgCollisionProb zeros(size(nodeRange)); for k 1:length(nodeRange) N nodeRange(k); % 这里调用主仿真函数输出平均吞吐量和碰撞概率 [avgThroughput(k), avgCollisionProb(k)] runSimulation(N, simTime); end %% 绘制对比曲线 figure(1); yyaxis left plot(nodeRange, avgThroughput/1e3, -o, LineWidth, 1.5); ylabel(吞吐量 (kbps)); yyaxis right plot(nodeRange, avgCollisionProb * 100, -s, LineWidth, 1.5); ylabel(碰撞概率 (%)); xlabel(节点数量); legend({吞吐量, 碰撞概率}, Location, northwest); grid on;顺便说一句MATLAB的双y轴画图工具yyaxis从R2016a开始用着很顺手比以前传奇的plotyy好控制多了线条颜色、坐标轴刻度都会自动分配。4.3 固定随机种子保证可复现性论文里做仿真实验最怕什么最怕结果不可复现。今天跑出的吞吐量是2.3Mbps明天重跑变成2.1Mbps写进论文里审稿人复现不了轻则打回重审重则质疑学术诚信。解决方式很简单每个实验配置加上一个固定的随机种子。rng(42); % 固定随机数生成器种子放在仿真脚本的第一行连同节点的随机数流一块固定下来。但要注意MATLAB的rand和randi共享同一个全局随机流你只要在仿真里调整了任何随机数调用的顺序结果就会变。所以如果你想严格复现结果最好在算法改动前先跑一遍基线把关键统计量记录下来改完再对一遍。更进阶的做法是用RandStream创建独立的随机数流给数据包到达过程、退避窗口选择、信道错误模拟分别指定一条随机流这样修改其中一部分逻辑不会影响其他部分的随机序列。这在做敏感性分析的场景里会方便很多。5. 常见问题与避坑指南5.1 仿真结果跟理论值对不上这是我在这个仿真上踩的最大的坑也是很多初学者最容易卡住的地方。仿真和理论对不上是常态对得上才需要额外确认是不是哪里互相抄错了。最常见的原因有两个。第一个原因是没有考虑PHY层开销802.11数据帧发送前有PLCP前导码和头部占用192us左右这在实际信道时间里是大头。第二个原因是理论模型里的假设和仿真里的参数不匹配比如Bianchi模型默认饱和状态、理想信道、不考虑ACK超时重传上限你的仿真里如果设置了重传上限重负载下结果必然比理论曲线低。解决思路是先做“正确性基线”把仿真条件设成和理论假设完全一致不考虑丢包、重传上限无穷大、每个节点始终有包要发。如果这一组结果的趋势跟理论曲线基本吻合说明协议逻辑没问题然后逐步加回现实约束观察指标怎么变化。这个过程其实就是系统性的模型验证流程毕业论文里的模型验证章节就靠它写了。5.2 退避冻结的边界条件退避冻结的实现看起来很简单信道忙就不减计数但边界条件特别容易写错。最容易犯的错是节点检测到信道忙然后立刻把计数器保持住这个没问题但信道从忙变闲之后节点什么时候恢复倒数正确做法是先等DIFS因为协议规定信道空闲DIFS之后才能开始退避递减有些仿真代码信道一变闲就立刻继续倒数这会让节点比协议要求的更积极整体碰撞率偏高。另一个边界是节点在退避递减时如果这个时隙刚开始就检测到信道是忙的那这个时隙不能减。但如果你检测的是上一时隙的忙闲状态会因为延迟一个时隙造成“信道刚空出来就有节点冲出去”的问题。我的建议是信道忙闲状态统一以时隙开始时的状态为准不用在时隙中间做亚时隙级别的检测这是在时间驱动模型里最合理的近似。5.3 碰撞后立即退避还是先等DIFS碰撞发生后节点应该直接重新进入退避还是回到SENSING等信道空闲再退避答案是等信道空闲后DIFS再进退避。因为碰撞发生期间信道是忙的你要做的是等这个忙期结束然后按正常流程走一遍DIFS、退避、发送。有些简化代码里碰撞检测到之后立刻重新采样退避值但没重置DIFS计数器这会轻微高估碰撞后的信道接入速度。从分布式协调的角度看所有节点在碰撞后都遵循同一套“等信道空闲-等DIFS-退避倒计时”规则才能最大化避免“碰撞后立刻再撞”的连锁反应。5.4 大规模节点时的性能优化如果你的实验里节点数到了100个以上纯粹的MATLAB循环会变得很慢。一个10秒仿真、20us时隙总共50万个时隙每个时隙遍历100个节点就是5000万次状态更新MATLAB跑起来要几分钟到十几分钟。几个优化方向供你参考第一把节点状态从结构体数组拆成独立数值数组state、backoff、cw各一个1xN数组用向量化操作更新速度能提升好几倍。第二把主循环改成只在事件发生时推进时间——但这样要把框架改成事件驱动工作量较大。第三把频繁调用的randi替换成预生成的随机数矩阵比如一次性生成一个足够长的随机数序列循环里按索引取用。我实测过的组合是100个节点、200万时隙、结构体数组直接跑要差不多15分钟改成数值数组加向量化之后压到3分钟左右。如果你只想验证协议行为20个节点规模完全够用没必要做大节点的批处理。5.5 从这段仿真代码还能扩展出什么这套骨架的扩展空间很大。比较实际的方向有三个一是加入RTS/CTS机制在SENSING之前加一个控制帧握手流程对比有无RTS/CTS时的吞吐量差异这本身就是研究隐藏终端问题的一个经典实验二是加入多优先级队列用不同EDCA参数区分语音、视频、尽力而为业务这就迈进了QoS仿真的门槛三是把无线信道从完全可靠改成概率擦除模型发送成功不再是无碰撞就成功而是加一个信道误码率参数更贴近物联网场景里的真实链路。如果你做的是课程设计建议用这个框架做完第一组基础对比实验之后再挑一个方向加功能工作量适中创新点也够。如果是毕业设计RTS/CTS和QoS是相对成熟的方向参考资料多不容易走偏。我自己回头用这个框架做实验时有个很深的体会MAC协议仿真的难点从来不是写代码而是把协议规范里的边界条件逐一搞清楚。每个状态该在什么条件下迁移、每个计数器在忙时怎么冻结、碰撞后怎么恢复这些细节在文字规范里可能就是一句话但在代码里就是一个分支判断而且一个分支写错性能曲线就会慢慢偏离正轨还不容易定位。建议你仿完真拿最终曲线跟理论公式或标准里的参考值对一下对不上就回头查状态机很多时候一抓一个准。本文还有配套的精品资源点击获取