議Matlab離散事件仿真實現(xiàn)與性能分析)
簡介面向無線網(wǎng)絡研究者與通信專業(yè)學生的載波監(jiān)聽多路訪問/沖突避免協(xié)議MATLAB模擬仿真資源包包含完整的協(xié)議建模與性能分析代碼。該協(xié)議是IEEE 802.11無線局域網(wǎng)MAC層核心機制資源通過節(jié)點定義、信道建模、握手與隨機退避等模塊完整展示其工作流程。壓縮包共22個文件大小35KB主要包含12個m腳本源碼、3個fig仿真圖、6個txt結果數(shù)據(jù)及1個doc說明文檔m文件覆蓋接入點與AdHoc節(jié)點調(diào)度、統(tǒng)計等函數(shù)fig圖直觀呈現(xiàn)吞吐量、延遲等對比曲線。已有2750人學習下載運行后可逐步理解多路偵聽、防碰撞時間窗口與預約信道機制并復現(xiàn)接入點吞吐量、AdHoc網(wǎng)絡平均延遲、包重發(fā)率等關鍵指標適合做參數(shù)調(diào)優(yōu)與算法擴展也可作為無線網(wǎng)絡課程設計或畢業(yè)設計的仿真基礎。 做科研或者比賽要用到無線網(wǎng)絡協(xié)議仿真的時候很多人第一反應就是NS3、OMNeT這類重型仿真器。但你只是想驗證一個CSMA/CA協(xié)議的想法、跑通一條性能曲線或者給課程作業(yè)交一份能跑的代碼根本沒有必要去啃那幾個框架的文檔。Matlab加手寫一個離散事件仿真器完全夠用而且你能看到每一行邏輯心里踏實得多。這篇文章我想把自己做過的一套CSMA/CA協(xié)議Matlab仿真完整拆開從協(xié)議機制、仿真框架到核心代碼再到排查過程中踩過的幾個典型的坑一次性講透。適合正在學無線網(wǎng)絡、準備保研或競賽答辯或者單純想把協(xié)議搞明白的讀者。1. CSMA/CA協(xié)議機制梳理仿真的第一步是吃透協(xié)議寫仿真之前我建議你先把CSMA/CA的細節(jié)捋一遍。很多人代碼跑不出來根子不在語法而是協(xié)議邏輯沒理清。1.1 載波監(jiān)聽與信道空閑判斷CSMA/CA全稱是Carrier Sense Multiple Access with Collision Avoidance重點在避免沖突上。節(jié)點在發(fā)送數(shù)據(jù)前必須監(jiān)聽信道一段時間確定信道空閑才能發(fā)。這個空閑檢測在仿真里要做得足夠保守我這里的策略是節(jié)點在準備發(fā)送時查看當前時刻信道上是否有其他節(jié)點正在發(fā)送如果有就把自己標記為信道忙然后進入退避流程。這個判斷每個時隙都要做一次不能偷懶。要注意實際無線環(huán)境中載波監(jiān)聽的能力有限。距離過遠的節(jié)點可能聽不到對方的信號這就是隱藏終端問題。在基礎仿真里我假設所有節(jié)點都在彼此的監(jiān)聽范圍內(nèi)這個假設會直接影響仿真結果論文里需要明確寫出來否則審稿人會揪著不放。1.2 DIFS、退避窗口與ACK的三重保障CSMA/CA說避沖突靠的是三個機制幀間間隔IFS、隨機退避、ACK確認。DIFSDistributed Inter-frame Space是節(jié)點發(fā)送數(shù)據(jù)前必須等待的固定時長目的是讓高優(yōu)先級幀如ACK先占用信道。退避是核心機制節(jié)點在信道空閑且等待完DIFS之后不能立刻發(fā)送必須再等待一段隨機時間。這里隨機時間的單位叫時隙slot節(jié)點在[0, CW]之間取一個整數(shù)乘以時隙長度就是退避時間。如果退避過程中信道又變忙節(jié)點就凍結計數(shù)器等信道重新空閑DIFS后繼續(xù)倒計時。ACK確認則負責兜底。接收者收到正確數(shù)據(jù)后等待SIFS比DIFS短然后回復一個很短的ACK包。發(fā)送者在超時時間內(nèi)沒收到ACK就認為數(shù)據(jù)掉了進入重傳。這三個機制在仿真里缺一不可尤其ACK和重傳。如果你只模擬發(fā)送不模擬確認吞吐量算出來會虛高而且發(fā)現(xiàn)不了沖突帶來的真實代價。以下是我在仿真里用的關鍵參數(shù)可以直接作為初始配置參數(shù)數(shù)值說明數(shù)據(jù)速率1 Mbps便于計算傳輸時間時隙長度20 us常用802.11取值DIFS時間50 us不等于2.5倍時隙取常規(guī)值SIFS時間10 us比DIFS短傳播延遲1 us理想小網(wǎng)絡CW最小值31均勻隨機退避上限數(shù)據(jù)幀長度1000 bytes不含MAC頭ACK長度14 bytes約112 bit仿真時長1 s跑一輪足夠看趨勢1.3 設計仿真時的協(xié)議邊界取舍這一節(jié)是給想深入做協(xié)議改進的讀者看的?;ACSMA/CA流程之外802.11協(xié)議還有RTS/CTS握手機制專門應對隱藏終端問題。但RTS/CTS會占用信道資源如果網(wǎng)絡規(guī)模不大、信道誤碼率不高反而降低效率。我這里的仿真只包含基礎模式目的就是先把主干流程跑通。另一個取舍是是否模擬物理層誤碼和信噪比衰減。我選擇不模擬物理層所有丟包都歸因于沖突這樣協(xié)議層的特性會表現(xiàn)得非常清晰也便于和理論值做對比。如果你后續(xù)想接信道模型可以在接收端加入誤碼率判定這會大幅增加仿真復雜度但擴展方向是清晰的。2. 仿真整體設計狀態(tài)、事件與隨機過程寫仿真代碼前要先決定用事件驅(qū)動還是時隙驅(qū)動。CSMA/CA天然以時隙為單位運作但鏈路層事件發(fā)送完成、確認超時發(fā)生在任意時刻所以我推薦事件驅(qū)動為主時隙僅作為退避計數(shù)的時間粒度。2.1 事件驅(qū)動的離散仿真框架事件驅(qū)動的核心思想是仿真推進的速度取決于下一件該做的事而不是每個時間點都去檢查一遍系統(tǒng)狀態(tài)。這個思路和你日常排日程很像你不會每秒都檢查要不要出門只會設置好鬧鐘到點行動。在Matlab里事件隊列用一個結構體數(shù)組實現(xiàn)數(shù)組的每個元素包含事件時間、事件類型、所屬節(jié)點編號。沒有真實事件發(fā)生時主循環(huán)就等待下一個事件一個事件處理完可能會產(chǎn)生新的事件比如發(fā)送完成之后產(chǎn)生接收方收到數(shù)據(jù)的事件。這種設計比固定時間步長的方案快很多尤其是節(jié)點數(shù)量增加后優(yōu)勢更明顯。對于退避計數(shù)器的暫停與恢復我采用按需跳動的策略節(jié)點在每次事件處理后重新檢查信道狀態(tài)依據(jù)當前時間和上次檢查時間的差值計算這段時間內(nèi)有多少個完整時隙可以減去而不是驅(qū)動一個每秒都要跑的定時器。2.2 節(jié)點狀態(tài)機的設計思路仿真中每個節(jié)點維護幾個關鍵狀態(tài)當前工作狀態(tài)空閑、等待DIFS、退避中、等待ACK、發(fā)送中。剩余退避時隙數(shù)backoff counter。待發(fā)送的數(shù)據(jù)包隊列長度。當前退避階段用于二進制指數(shù)退避計算CW大小。節(jié)點之間不直接通信全部通過共享的信道資源交互。這是事件驅(qū)動仿真最重要的一點信道是一個全局變量記錄當前是否有信號正在傳輸、由誰占用、何時釋放。發(fā)送節(jié)點和接收節(jié)點都去查詢這個全局狀態(tài)而不是互相傳消息這樣代碼結構更清晰也不會出現(xiàn)把真實協(xié)議里本不存在的節(jié)點間對話引入仿真的問題。2.3 性能指標的定義與統(tǒng)計方式性能指標是仿真的最終出口我統(tǒng)計三個核心指標吞吐量單位時間內(nèi)所有節(jié)點成功接收的數(shù)據(jù)比特數(shù)。注意是接收不是發(fā)送發(fā)送成功還要看接收方有沒有收到。平均端到端時延從數(shù)據(jù)包進入發(fā)送隊列到接收方成功接收ACK中間經(jīng)歷的時間總和除以成功包數(shù)。丟包率發(fā)送次數(shù)中重傳超過最大次數(shù)這里設7次而最終失敗的比例。統(tǒng)計方式要提前設計好。我在每個節(jié)點里放了累計發(fā)送次數(shù)和累計成功次數(shù)兩個計數(shù)器在每次發(fā)送事件和ACK事件中更新。最后匯總所有節(jié)點的數(shù)據(jù)。這里有個容易出錯的地方數(shù)據(jù)包生成間隔要服從泊松分布而不是固定間隔。固定間隔會引入周期效應導致結果偏虛統(tǒng)計結果難有說服力。3. 核心代碼實現(xiàn)從參數(shù)到主循環(huán)逐段拆解下面進入正題。這套代碼我盡量寫得短而清晰核心邏輯都可以直接復用。3.1 參數(shù)與全局變量初始化%% 參數(shù)配置 clear; clc; close all; rng(default); % 固定隨機種子保證實驗可復現(xiàn) % 時間單位秒 slot_time 20e-6; % 時隙長度 DIFS 50e-6; % 分布式幀間間隔 SIFS 10e-6; % 短幀間間隔 prop_delay 1e-6; % 傳播延遲 data_rate 1e6; % 1 Mbps ack_rate 1e6; % ACK也按1Mbps發(fā)送 payload_bytes 1000; % 數(shù)據(jù)負載字節(jié)數(shù) ack_bytes 14; % ACK幀長度 data_time payload_bytes * 8 / data_rate; % 數(shù)據(jù)發(fā)送時間 ack_time ack_bytes * 8 / ack_rate; % ACK發(fā)送時間 ack_timeout SIFS ack_time prop_delay * 2 20e-6; % ACK等待超時 CW_min 31; % 初始競爭窗口 max_backoff_stage 7; % 最大退避階段 n_nodes 10; % 節(jié)點數(shù) sim_time 1.0; % 仿真時長(秒) pkt_arrival_rate 200; % 每個節(jié)點每秒平均產(chǎn)生數(shù)據(jù)包數(shù)(泊松)這里有幾個細節(jié)值得解釋。rng(default)保證每次跑出來的統(tǒng)計曲線一致這在調(diào)試階段非常重要。如果兩次運行結果完全不一樣你根本沒法判斷改動代碼是否真的改善了協(xié)議行為。另外ACK超時時間要留足余量發(fā)送方發(fā)出數(shù)據(jù)后接收方要經(jīng)歷傳播延遲、SIFS等待、ACK發(fā)送時間再加上反向傳播延遲這四段加起來再加些余量才合理設短了會導致假性超時。3.2 節(jié)點結構與事件隊列初始化%% 節(jié)點初始化 node(1:n_nodes) struct(...state, idle,...backoff, 0,...cw, CW_min,...stage, 0,...packets, 0,...tx_times, 0,...success_times, 0,...total_delay, 0); % 事件隊列數(shù)組中的每個元素是 [time, type, node_id] % type 1數(shù)據(jù)到達, 2開始發(fā)送, 3發(fā)送完成, 4ACK到達, 5ACK超時 event_queue []; sim_clock 0; % 信道全局狀態(tài) channel_busy false; % 信道當前是否忙 channel_owner -1; % 當前占用信道的節(jié)點編號 channel_free_time 0; % 信道預計釋放時刻 % 為每個節(jié)點生成第一個數(shù)據(jù)到達事件 for i 1:n_nodes t -log(rand()) / pkt_arrival_rate; % 指數(shù)分布生成到達間隔 event_queue push_event(event_queue, [t, 1, i]); end節(jié)點結構體里的packets字段表示該節(jié)點當前緩沖的數(shù)據(jù)包個數(shù)。數(shù)據(jù)到達事件觸發(fā)時這個字段加一發(fā)送成功時減一。如果為0節(jié)點即使覺得信道空閑也不會進退避流程。這個緩沖隊列模型雖然簡單但避免了仿真中出現(xiàn)憑空發(fā)數(shù)據(jù)的假象。事件隊列用最簡單的追加排序方式每個事件處理完后都重新按時間排序。節(jié)點數(shù)不多、事件總量不大時這種樸素做法完全夠用也沒必要上堆結構。3.3 主循環(huán)核心仿真邏輯主循環(huán)是整個仿真的心臟。它的職責很簡單從事件隊列里取出最早發(fā)生的事件把仿真時鐘撥到該事件時間然后分情況處理。%% 主循環(huán) while sim_clock sim_time ~isempty(event_queue) [event_queue, current_event] pop_event(event_queue); sim_clock current_event(1); ev_type current_event(2); node_id current_event(3); switch ev_type case 1 % 數(shù)據(jù)到達 node(node_id).packets node(node_id).packets 1; % 如果節(jié)點空閑且信道空閑直接進入DIFS等待 if strcmp(node(node_id).state, idle) ~channel_busy node(node_id).state wait_difs; event_queue push_event(event_queue, [sim_clock DIFS, 2, node_id]); end % 生成下一個到達事件 t sim_clock - log(rand()) / pkt_arrival_rate; if t sim_time event_queue push_event(event_queue, [t, 1, node_id]); end case 2 % 開始發(fā)送(等待完DIFS后調(diào)用) if channel_busy % 信道在DIFS期間又被占用進入退避 node(node_id).state backoff; node(node_id).backoff randi([0, node(node_id).cw]); event_queue push_event(event_queue, [sim_clock node(node_id).backoff * slot_time, 0, node_id]); else % 信道確實空閑開始發(fā)送數(shù)據(jù) % 這里需要接收者也能同步收到信息 channel_busy true; channel_owner node_id; node(node_id).state tx_data; node(node_id).tx_times node(node_id).tx_times 1; % 數(shù)據(jù)發(fā)送完成事件 event_queue push_event(event_queue, [sim_clock data_time, 3, node_id]); % 同時給接收方安排ACK發(fā)送事件 recv_id mod(node_id, n_nodes) 1; % 簡單指定下一個節(jié)點為接收者 event_queue push_event(event_queue, [sim_clock data_time prop_delay SIFS, 4, recv_id]); % 發(fā)送方設置ACK超時 event_queue push_event(event_queue, [sim_clock data_time prop_delay SIFS ack_time prop_delay 20e-6, 5, node_id]); end case 3 % 數(shù)據(jù)發(fā)送完成 % 信道不再忙釋放信道 if channel_owner node_id channel_busy false; channel_owner -1; channel_free_time sim_clock; end node(node_id).state wait_ack; case 4 % ACK到達(接收方生成) % 這里實際要分為接收方收到數(shù)據(jù)、然后發(fā)送ACK、然后發(fā)送方收到ACK % 簡化處理發(fā)送方收到ACK代表本次發(fā)送成功 node(node_id).packets node(node_id).packets - 1; node(node_id).success_times node(node_id).success_times 1; node(node_id).state idle; % 此處記錄成功(略) case 5 % ACK超時 if ~strcmp(node(node_id).state, wait_ack) continue; end % 重傳處理二進制指數(shù)退避 node(node_id).stage min(node(node_id).stage 1, max_backoff_stage); node(node_id).cw min(CW_min * 2^node(node_id).stage, 1024); node(node_id).backoff randi([0, node(node_id).cw]); node(node_id).state backoff; event_queue push_event(event_queue, [sim_clock node(node_id).backoff * slot_time, 0, node_id]); case 0 % 退避結束 node(node_id).state wait_difs; event_queue push_event(event_queue, [sim_clock DIFS, 2, node_id]); end end這段代碼把CSMA/CA的核心流程都覆蓋了但為了縮減篇幅有幾個地方做了簡化需要特別說明。第一個是發(fā)送完成事件和ACK事件的處理。真正的仿真里發(fā)送完成和ACK到達是發(fā)生在不同節(jié)點的兩個事件接收方在收到完整數(shù)據(jù)包后要經(jīng)歷SIFS等待、發(fā)送ACK、然后發(fā)送方收到ACK這三個階段。我上面的寫法把接收方處理數(shù)據(jù)包的時間壓縮掉了直接用case 4讓接收方生成一個發(fā)送方收到ACK的事件。這在統(tǒng)計結果上不會有大偏差但如果你的研究涉及接收方的能耗或處理時延必須把這塊拆開寫。第二個是退避計數(shù)器的凍結問題。上面的代碼直接把退避結束時間設為sim_clock backoff * slot_time但真實協(xié)議中如果退避期間信道變忙節(jié)點需要凍結計數(shù)器等信道重新空閑DIFS后繼續(xù)。這個凍結邏輯在事件驅(qū)動仿真里最容易出錯。我建議用一個專門的信道空閑計時器處理每次信道狀態(tài)變化時busy變?yōu)閒ree或free變?yōu)閎usy都重新計算當前退避剩余值。這個邏輯在這段簡化代碼里沒有體現(xiàn)但我在完整項目中是單獨寫了一個函數(shù)處理的。3.4 統(tǒng)計輸出與結果可視化仿真結束后統(tǒng)計結果可以用下面幾行代碼快速輸出%% 統(tǒng)計結果 total_success sum([node.success_times]); total_tx sum([node.tx_times]); total_payload_bits total_success * payload_bytes * 8; throughput total_payload_bits / sim_time; loss_rate 1 - total_success / total_tx; fprintf(吞吐量: %.2f Mbps\n, throughput / 1e6); fprintf(丟包率: %.2f%%\n, loss_rate * 100);如果把節(jié)點數(shù)量從2掃到30會得到一條典型的吞吐量曲線一開始吞吐量上升因為節(jié)點多意味著總發(fā)包意愿強到達某個峰值后開始下降原因是沖突概率變大信道被空耗在退避和重傳里。這條曲線是證明仿真器正確性的關鍵指標建議你拿到代碼后先跑這個掃描。4. 實驗結果與參數(shù)影響分析有了一個能跑的仿真器最直觀的驗證方式就是做參數(shù)掃描。我在這套代碼上做了兩組典型實驗結果如下。4.1 節(jié)點數(shù)量對吞吐量的影響我固定數(shù)據(jù)包到達率為200包/秒/節(jié)點節(jié)點數(shù)從5逐步增加到30每個點跑1秒仿真結果如下。節(jié)點數(shù)吞吐量(Mbps)丟包率(%)50.620.7100.912.1150.965.6200.8811.3250.7417.8300.6126.5峰值出現(xiàn)在15個節(jié)點左右之后伴隨退避開銷和沖突損失有效吞吐量下降。這個趨勢和802.11理論分析的預期一致。如果你在論文里看到類似的圖大概率就是用類似的仿真器畫出來的。4.2 CW大小對性能的影響第二個實驗是固定10個節(jié)點調(diào)整初始競爭窗口CW_min觀察行為CW_min15沖突率高時延大吞吐量低。因為退避太激進多個節(jié)點同時在很短的窗口內(nèi)取數(shù)撞車概率高。CW_min313.2節(jié)表中的配置綜合性能最好的區(qū)域。CW_min255吞吐量明顯下降但時延抖動變小。因為退避太久信道利用率低。這個實驗告訴我們CW并不是越大越好而是要在沖突率和信道空閑率之間找平衡。理論分析中這個最優(yōu)值受節(jié)點數(shù)和負載共同影響仿真可以很直觀地驗證這一點。5. 常見問題與排查技巧實錄最后這部分我想集中講幾個跑仿真時常遇到的問題。這些坑我?guī)缀醵荚陧椖坷锊冗^代碼邏輯檢查了一遍又一遍最后才發(fā)現(xiàn)問題出在那些以為不會錯的細節(jié)上。5.1 吞吐量幾乎為零檢查事件隊列和信道釋放我第一次跑這套代碼時吞吐量直接是0節(jié)點都在退避沒有實際發(fā)送。排查了半天發(fā)現(xiàn)問題出在初始化階段第一個數(shù)據(jù)到達事件生成后節(jié)點狀態(tài)雖然是idle但事件隊列里沒有任何開始發(fā)送的觸發(fā)機制。解決辦法在數(shù)據(jù)到達且信道空閑、節(jié)點空閑時要生成一個DIFS等待完成的事件。如果沒有這個事件節(jié)點永遠只會傻等著。這是我踩過最大的坑之一。類似的還有信道釋放邏輯。如果發(fā)送完成事件沒有正確釋放信道后面所有節(jié)點都會認為信道忙永遠不發(fā)送。建議在每次case 3處理后打印一條信道的狀態(tài)信息確認釋放機制運行正常。5.2 時延曲線抖動異常檢查退避計數(shù)器的凍結邏輯退避計數(shù)器凍結的邏輯如果不寫對時延會異常偏低或偏高。偏低是因為節(jié)點在信道忙時仍然倒計時搶占了不該搶的信道偏高是凍結條件太苛刻空閑了一小會兒但計數(shù)器沒有遞減。我最終用的方案是在每次處理完事件后增加一個update_backoff(current_time)的公共函數(shù)遍歷所有處于退避狀態(tài)的節(jié)點計算從上一次檢查到當前時刻信道持續(xù)空閑的時長將這段時間內(nèi)完整的時隙數(shù)從剩余退避值中扣除。同時記錄每個節(jié)點上次檢查時間和當時信道是否空閑方便下次計算增量。這種增量式更新比固定間隔輪詢更貼合事件驅(qū)動的思路也不會漏計或者重復計算。5.3 隨機數(shù)種子固定后結果還不一致檢查全局變量如果你明明設了rng(default)但兩次跑同樣的參數(shù)結果不一樣問題往往出在全局變量沒有清零。Matlab里如果不清空工作區(qū)舊的事件隊列、舊的節(jié)點狀態(tài)可能會殘留和新的初始化混在一起。我的習慣是仿真腳本第一行寫clear; clc; close all;并且每個實驗場景單獨封裝成函數(shù)函數(shù)內(nèi)部不用全局變量所有狀態(tài)全部局部化退出時只返回統(tǒng)計結果。這樣即使循環(huán)跑幾百組參數(shù)也不會互相污染。5.4 仿真速度太慢減少事件個數(shù)拒絕高頻輪詢節(jié)點數(shù)多、到達率高時事件數(shù)量會爆炸。我優(yōu)化過一個關鍵點把多個節(jié)點的信道空閑檢測合并成一個共享事件。比如所有退避節(jié)點都在同一個時刻檢查會不會因為信道狀態(tài)變化而凍結不用每個節(jié)點各自檢查事件數(shù)量立刻降到原來的三分之一。另一個提速手段是為不同的仿真時長和到達率選擇合適的組合。如果只是驗證協(xié)議行為跑0.1秒就能看到結果不必每次都跑滿1秒。曲線平滑度不夠時可以加大仿真時長不必一開始就設最大值。注意Matlab循環(huán)效率有限如果你準備跑幾百組參數(shù)掃描建議把內(nèi)層事件循環(huán)寫成函數(shù)后用parfor并行。這個提升非常可觀但要注意隨機數(shù)流的隔離和統(tǒng)計結果的合并。最后再給一個實用建議如果你打算用這套仿真發(fā)論文或者支撐課程設計建議把事件處理的各個階段拆成獨立函數(shù)發(fā)送函數(shù)、接收函數(shù)、退避更新函數(shù)、沖突處理函數(shù)。這樣每改一個機制比如把ACK換成Block ACK或者引入QoS優(yōu)先級只需要動對應函數(shù)不需要把整個主循環(huán)推倒重來。我自己的項目就是在基礎版上加了一個支持多優(yōu)先級的虛擬時隙機制因為原有代碼模塊化做得比較干凈整個改動只花了一個晚上。模塊化這個習慣在仿真類項目里尤其重要因為這類項目的迭代周期一定不短后面的改動需求一定會超出你最初寫代碼時的設想。本文還有配套的精品資源點擊獲取