用Matlab实现多址接入协议仿真:从ALOHA到CSMA/TDMA
2026/9/15 21:35:04 网站建设 项目流程

简介:基于Matlab实现的多址接入协议仿真源码包,面向通信工程、电子信息、计算机及相关专业的学习者,适合作为课程设计、实验报告或毕业设计的辅助参考资料。资源围绕随机多址接入中的ALOHA、时隙ALOHA、非坚持CSMA等典型协议展开,通过源码演示协议流程、参数配置和结果对比,帮助理解信道争用、冲突避免与吞吐量分析等核心概念。压缩包内共6个文件,全部为.m格式的Matlab脚本,总大小仅7KB,便于快速下载和按需修改;目前已有141人学习下载。源码中既包含基础协议实现,也有改进版本,可对照观察不同访问策略对网络性能的影响,读者在掌握一定Matlab基础后,可自行调整参数、叠加噪声或增加节点数,进一步验证协议特性。使用时需具备调试和扩展代码的能力,并注意将这份源码作为思路参考,而非直接照搬全部实现。

1. 多址接入协议仿真到底在仿什么:先想清楚再动手

做无线通信系统仿真的人,大概率会遇到这样一个痛点:信道模型建得再精细,调制解调性能再漂亮,一旦把多个用户丢进同一个小区、同一条链路上,系统吞吐量立刻崩掉。问题往往不出在物理层,而是出在媒体接入控制(MAC)层的多址接入协议上。标题里这个“基于Matlab实现多址接入协议仿真(源码)”的项目,本质上是想搭一条从协议逻辑到性能指标的标准化验证链路:在同一个仿真平台上,比较TDMA、FDMA、CDMA、ALOHA、CSMA这类接入机制在不同用户数、不同业务负载下的吞吐量、时延、丢包率和公平性。

多址接入协议不是一套算法,而是一族“多个节点如何共享同一份信道资源”的规则集合。写这类仿真,最忌讳的就是上来就贴代码先跑通。你需要做的第一件事,是把“节点数量—到达间隔—退避策略—信道状态”这四个要素抽象成可度量的数学模型,再落成Matlab里的离散事件循环。本文会从协议建模切入,带你在Matlab中实现一个可扩展的多址接入仿真框架,并给出一个可以直接改参数复现的最小代码骨架。适合已经有Matlab基础、想从“单用户链路仿真”转向“多用户接入仿真”的通信方向工程师和学生。这套框架跑通之后,换协议只是换一个函数的事情。

2. 多址接入协议的建模粒度:连续时间、离散事件与状态机

2.1 为什么不能用“逐时隙扫描”替代事件驱动

很多初学者写第一个多址仿真,习惯做法是:把时间切成极小的时间片,比如1微秒一个步长,在每个时间片上依次让所有节点执行“是否有新包到达、信道是否忙、是否发送”的判断。这种做法在业务量极低时勉强能跑,但只要节点数超过几十个、业务负载超过信道容量的20%,仿真时间就会呈指数级膨胀,因为绝大部分时间片里没有任何事件需要处理。

在Matlab里实现高效的协议仿真,核心思想是离散事件驱动:仿真时钟只在“有事情发生”的时刻向前跳跃,这些时刻包括新包到达、发送开始、发送结束、超时重传决定、退避计时器到期。每个事件按时间顺序放入事件队列,主循环每次取出队首事件,调用对应的事件处理函数,函数执行过程中可能生成新事件并插入队列。这样仿真的时间开销只与事件总数成正比,而与仿真时长无关。

2.2 协议动作的公共抽象:节点状态与信道状态

把多址协议拆开看,每个节点在最小粒度上都只有四种状态:空闲(Idle)、退避(Backoff)、发送(Transmit)、等待确认(WaitACK)。信道状态只有两种:忙(Busy)、空闲(Idle)。所有多址协议之间的差异,本质上就是“状态转移的条件”和“转移后等待的时长”不同。

以纯ALOHA为例,状态转移极其简单:节点有包就直接发,发完立刻监听信道是否冲突;CSMA/CA则引入载波监听和随机退避,节点不能在信道忙时发送,退避计时器挂起;TDMA则是把“能否发送”这个条件改成了“当前时隙是否属于本节点编号”。在Matlab里建模时,比较推荐的做法是定义一个MACNode结构体数组,每个节点记录当前状态、剩余退避时间、等待确认的定时器、已发送包数、已冲突包数。协议切换时,只改事件触发逻辑,不改数据结构和主循环框架。

这里有一个经常被忽略的建模细节:ACK与超时定时器。真实系统中,接收端收到正确数据帧后会回复确认帧,发送端只有在收到ACK后才认为发送成功。但很多课程设计的仿真为了省事,默认信道没有噪声、冲突是唯一的丢包原因,于是直接省略了ACK,把“发送结束未冲突”等价于“发送成功”。这种简化对于纯ALOHA这种无监听协议还能接受,但对于CSMA这类依赖ACK触发退避的协议,省略ACK会让退避算法完全失真。

2.2.1 仿真参数表的预先定义

无论你打算实现哪几种多址协议,下面这张参数表建议在写代码之前先固定下来,所有协议共用同一份配置,保证对比时只有接入机制这一个变量:

参数名符号默认值说明
仿真时长simTime10单位秒,配合数据速率使用
节点数量numNodes30接入同一信道的竞争节点数
数据速率dataRate1e6单位bps,决定帧传输时间
帧长度frameSize1000单位bit,固定帧长便于算吞吐
到达间隔均值lambda0.005单位秒,泊松到达的平均间隔
传播时延propDelay1e-6单位秒,用于CSMA的监听判断
重传次数上限maxRetry5超过后丢弃该帧
运行次数numRuns30每次独立种子取平均

固定帧长的好处在于传输时间可以直接计算:txTime = frameSize / dataRate,这就避免了在事件队列里维护一个“发送剩余时间”的变量,事情会简单很多。真实系统中帧长是变化的,仿真阶段固定帧长不影响协议对比的相对结论。

3. 用Matlab实现最小可运行的多址仿真框架

3.1 事件队列与仿真时钟的数据结构

在Matlab中实现离散事件仿真,可以不用面向对象的classdef,但那会让你在协议切换时付出沉重的维护代价。这里给一个折中方案:用结构化数组实现事件队列,用一个struct承载全局状态,协议差异通过函数句柄注入。这样代码量不大,可读性和扩展性都有保证。

事件队列的定义和操作函数如下:

function events = eventPush(events, time, type, nodeId, extra) % events: 结构体数组,含字段 t(时间), type(事件类型), nodeId(节点编号), extra(附加数据) % 事件类型: 1=包到达, 2=发送开始, 3=发送结束, 4=退避结束 ev = struct('t', time, 'type', type, 'nodeId', nodeId, 'extra', extra); % 插入排序,保证队首始终是最早发生的事件 if isempty(events) events = ev; else idx = find([events.t] > time, 1, 'first'); if isempty(idx) events(end+1) = ev; else events = [events(1:idx-1), ev, events(idx:end)]; end end end

主循环从事件队列里取出events(1)执行,然后删除该元素。因为每次插入都排序,取事件是O(1)操作,插入是O(n)操作,节点数几百个以内性能完全够用。这套结构比用timer对象或for循环扫时间轴要快一个数量级以上,也更容易加断点观察某个具体时刻的信道状态。

3.2 主循环框架与统计计数器

全局状态我们用state结构体承载:state.channelBusy标记信道忙闲,state.nodeStatus是一个numNodes x 1的数组记录每个节点当前所处状态码,state.collisionFlag表示当前正在发送的帧是否已经冲突。统计量分四类:发送帧数txCount、冲突帧数collisionCount、成功帧数successCount、平均时延delaySum

function [stats, events] = runSimulation(params, protocolFunc) % 初始化状态 state = struct('channelBusy', false, 'collisionFlag', false, ... 'nodeStatus', zeros(1, params.numNodes)); stats = struct('txCount', 0, 'collisionCount', 0, ... 'successCount', 0, 'delaySum', 0); events = eventInit(); % 为每个节点生成首个包到达事件 for n = 1:params.numNodes tArrival = exprnd(params.lambda); if tArrival < params.simTime events = eventPush(events, tArrival, 1, n, struct('retry', 0)); end end % 主循环 while ~isempty(events) ev = events(1); events(1) = []; if ev.t > params.simTime break; end % 时钟推进 state.curTime = ev.t; % 分派事件到协议处理函数 [stats, state, events] = protocolFunc(ev, stats, state, events, params); end end

这段代码的逻辑要点在“生成首事件”和“时钟推进”两处。首事件生成用exprnd(params.lambda)而不是均匀分布,是为了让包到达过程符合泊松过程假设——这是通信系统性能分析的标准前提,也是理论公式对比时的统一假设。时钟推进不做任何物理时间运算,只跟随事件时间戳,所以即使simTime设成10秒,实际CPU运行时间也只有几毫秒到几秒。

3.3 纯ALOHA协议的最小实现

用上面框架写纯ALOHA,只需要处理两类事件:包到达、发送结束。

function [stats, state, events] = alohaProtocol(ev, stats, state, events, params) txTime = params.frameSize / params.dataRate; switch ev.type case 1 % 包到达:直接发送,检查信道冲突 if state.channelBusy % 已有别的节点在发,冲突 stats.collisionCount = stats.collisionCount + 1; state.collisionFlag = true; else state.channelBusy = true; end state.nodeStatus(ev.nodeId) = 1; % 发送中 stats.txCount = stats.txCount + 1; events = eventPush(events, ev.t + txTime, 3, ev.nodeId, ev.extra); case 3 % 发送结束:释放信道,记录结果 state.channelBusy = false; state.nodeStatus(ev.nodeId) = 0; if state.collisionFlag stats.collisionCount = stats.collisionCount + 1; % 重传处理:若未超重试上限,安排一个随机退避后的重传 if ev.extra.retry < params.maxRetry backoffTime = rand() * 2 * params.lambda; events = eventPush(events, ev.t + backoffTime, 4, ev.nodeId, ... struct('retry', ev.extra.retry + 1)); end else stats.successCount = stats.successCount + 1; end state.collisionFlag = false; case 4 % 退避结束:重新发送 state.channelBusy = true; stats.txCount = stats.txCount + 1; state.nodeStatus(ev.nodeId) = 1; events = eventPush(events, ev.t + txTime, 3, ev.nodeId, ev.extra); end end

注意看这个实现里的一个隐蔽问题:当两个节点的包到达事件发生在同一时刻,第一个包的发送事件会立即把channelBusy置为true,第二个包到达时会发现信道忙,这种“同时刻事件”处理是合理的,它模拟了物理层冲突检测。但如果在同一时刻有两个节点同时启动发送,且都设置了collisionFlag = true,发送结束后计数会重复计算——这就是离散事件仿真里经典的“同时刻事件顺序”问题,后面排错章节会专门讲。

纯ALOHA跑完后,吞吐量用successCount * frameSize / simTime计算,单位是bps。理论上纯ALOHA最大吞吐在负载G=0.5时达到18.4%的信道利用率,你用上面代码跑出来应该在0.15到0.2之间,偏差太大就要回去检查事件处理是否有逻辑漏洞。

4. 从ALOHA到CSMA/TDMA:协议扩展与参数标定

4.1 Slotted ALOHA与时隙对齐的实现技巧

纯ALOHA改成Slotted ALOHA,关键区别是发送必须对齐到固定时隙边界。实现上有两种常见做法:一种是在事件处理函数里计算nextSlot = ceil(ev.t / slotTime) * slotTime,把发送事件钉在下一个时隙开始;另一种更省事——在初始化时就生成一个全局的时隙边界数组,所有发送事件都找最近的边界对齐。这里推荐第一种,因为时隙长度如果定义为帧传输时间的整数倍,边界计算就是纯算术运算,不会引入额外事件。

slotTime = params.frameSize / params.dataRate; % 在包到达处理分支中: startTime = ceil(ev.t / slotTime) * slotTime; events = eventPush(events, startTime, 2, ev.nodeId, ev.extra);

这个改动对性能影响极大:纯ALOHA冲突窗口是2倍帧传输时间,而Slotted ALOHA冲突窗口缩减到1倍帧传输时间,理论最大吞吐翻倍到36.8%。你在Matlab中跑对比实验时,最直观的验证方式就是固定lambda不变,看吞吐量是否接近翻倍。

4.2 非坚持CSMA的载波监听与退避设计

CSMA类协议比ALOHA多了一个动作:发送前先监听信道。在离散事件仿真中,这个“监听”不是一个持续的过程,而是在发送事件执行的那个时刻查看state.channelBusy。非坚持CSMA的规则是:信道忙就等待一个随机时间后重试,信道空闲就立即发送。看到这里你应该意识到,这个逻辑和ALOHA的退避重传几乎一样,区别只在“是否先看信道状态再决定发不发”。

case 1 % 包到达 if state.channelBusy % 信道忙,非坚持:随机延迟后重听 waitTime = rand() * params.lambda * 2; events = eventPush(events, ev.t + waitTime, 4, ev.nodeId, ev.extra); else % 信道空闲,直接发送 state.channelBusy = true; stats.txCount = stats.txCount + 1; events = eventPush(events, ev.t + txTime, 3, ev.nodeId, ev.extra); end case 4 % 重监听 if state.channelBusy waitTime = rand() * params.lambda * 2; events = eventPush(events, ev.t + waitTime, 4, ev.nodeId, ev.extra); else state.channelBusy = true; stats.txCount = stats.txCount + 1; events = eventPush(events, ev.t + txTime, 3, ev.nodeId, ev.extra); end

这段代码看起来有重复,但它恰恰反映了非坚持CSMA的建模核心:退避完成后不需要检查重试次数是否超限,因为监听不产生冲突,只是延迟发送。真正可能产生冲突的情况,是传播时延大于0时两个节点同时监听到“空闲”——仿真中把传播时延设成一个大于0的数可以复现这个场景,这也是CSMA吞吐量永远达不到100%的根本原因。

4.2.1 CSMA的参数敏感性实验设计

CSMA协议对传播时延极度敏感。建议做一组对比实验:固定其他参数,把propDelay从0扫到5e-6(对应1.5km距离的传播时延),步长1e-6,观察吞吐量变化。在Matlab里跑这个扫描不需要大改代码,只要在runSimulation外面包一层for循环即可。实际跑下来你会发现一个明显拐点:传播时延超过帧传输时间的1%后,吞吐量开始显著下滑,这是因为载波监听的“先听后说”机制在长传播时延下信息滞后严重,冲突概率上升。

4.3 TDMA时隙分配在不连续业务下的吞吐天花板

TDMA的实现比随机接入协议简单得多:每个节点只在属于自己的时隙内发送,没有冲突,无需退避。在仿真中把“包到达”事件和“时隙边界”事件关联即可。但这里有一个建模陷阱:如果节点在非本节点时隙有包到达,它必须等到下一个自己的时隙才能发送,引入的等待时延就是“排队时延”。没有队列管理逻辑的TDMA仿真,往往会低估实际时延。

协议冲突窗口理论最大吞吐实现复杂度
纯ALOHA2倍帧时18.4%
Slotted ALOHA1倍帧时36.8%
非坚持CSMA随传播时延变化接近100%(理想)
TDMA100%(固定帧长)

这张表是你在写论文或做技术方案对比时的标准参考。仿真结果和理论值的偏差控制在10%以内,就说明你的事件驱动框架是正确的;偏差过大,优先检查统计计数逻辑而不是协议规则。

5. 结果分析与调试:吞吐量、时延、丢包率三个必查点

5.1 吞吐量、时延与丢包率的统计口径

仿真代码写完,最容易被质疑的就是统计口径。吞吐量建议用“成功接收的比特数/仿真时长”而非“发送的比特数/仿真时长”,后者会把冲突导致的无效传输也算进去,数值虚高。平均时延建议只统计成功接收的帧,从包的“第一个比特准备好发送”开始计时,到“最后一个比特被成功接收”为止——这个口径和网络仿真工具NS-3的定义一致,方便后续做跨平台对比验证。丢包率分母用“所有到达的包总数”,分子是“超过重试上限被丢弃的包数”,注意不要和冲突率混为一谈,冲突只是丢包的诱因,不是丢包本身。

5.2 三个高频错误的排查方法

第一个高频错误是同时刻事件计数重复。两个及以上节点在同一时刻产生发送事件时,发送结束处理函数会为每个节点各执行一次冲突计数,而真实冲突只需要记一次。修正办法是给发送结束事件加上一个共享标识——所有在同一时刻开始发送的事件携带相同的冲突编号,发送结束后只计数一次。在Matlab里可以用ev.extra.conflictTag字段实现。

第二个高频错误是退避计时器没有随信道状态挂起。这在CSMA的实现中尤其常见:节点在退避过程中信道变忙,退避计时器应该暂停计时,待信道空闲后继续。如果你的实现让退避计时器自顾自地走,仿真结果会表现为负载高时丢包率反而下降,这个反直觉现象是退避逻辑错误的典型信号。

第三个高频错误是随机数种子未隔离。每个独立仿真运行前调用rng(runIndex)设置种子,保证不同参数配置之间可以复现、可以对比。很多仿真结果画出来曲线毛刺极多,不是因为算法有问题,而是因为每次都重新生成随机数序列。

5.3 用批量实验快速验证协议差异

把整个仿真框架跑成批处理实验,常用做法是写一个脚本遍历负载值:

lambdaList = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05]; throughput = zeros(length(lambdaList), numRuns); for i = 1:length(lambdaList) for r = 1:numRuns rng(r); params.lambda = lambdaList(i); [stats, ~] = runSimulation(params, @alohaProtocol); throughput(i, r) = stats.successCount * params.frameSize / params.simTime; end end meanThroughput = mean(throughput, 2); plot(lambdaList * params.frameSize / params.dataRate, meanThroughput);

注意横坐标的换算:lambda * frameSize / dataRate就是“到达负载G”,它表示单位信道时间内新到达的业务量,量纲为1。用G作为横轴,不同数据速率的仿真结果可以直接对比,这是多址接入协议性能分析的标准坐标选择。负载轴用“到达速率”而不用“到达间隔”,能让曲线更加平滑,便于观察ALOHA的“吞吐回落”现象——负载超过0.5后吞吐量不升反降,这个指标性拐点是协议局限性的最直观体现。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询