简介:本资源面向TSN(时间敏感网络)初学者与网络仿真方向的研究生、工程师,提供基于TSNkit与OMNeT++的调度与仿真完整工程。内容涵盖TSN基础机制(时间同步、流量整形、IEEE 802.1Qbv优先级调度、帧预留)、OMNeT++环境搭建、TSNkit组件导入与配置、EDF/WEDF等调度策略实现,以及网络拓扑定义、节点参数配置与仿真结果分析,并涉及Python脚本与INET Framework接口的交互方式。压缩包共约2000个文件,以1448个C++头文件、208个XML配置、58个Python脚本、41个文本说明及若干prefs、launch、sh脚本为主,整体约83.24MB,目录结构清晰,便于按模块检索。目前已有403人学习下载,适合希望快速搭建TSN仿真场景、理解调度算法实现细节并动手复现实验的读者参考。
1. 从一次工业现场联调说起:TSNkit 和 OMNeT++ 到底能帮你验证什么
去年帮一个做运动控制的朋友排查问题,他们用标准以太网跑多轴同步,示波器上周期抖动能到几百微秒,伺服偶尔报跟随误差。换交换机、调优先级都试过,问题依旧。后来我建议他先用仿真把 IEEE 802.1Qbv 的时间感知整形(TAS)门控逻辑跑一遍,看看在给定流量模型下理论抖动下限是多少,再决定硬件选型。他用的就是 TSNkit 加 OMNeT++ 这套组合。TSNkit 是挂在 OMNeT++ 上的 TSN 组件库,把时间同步、流量整形、优先级调度这些 IEEE 802.1 标准里的机制做成了可配置模块;OMNeT++ 负责离散事件仿真引擎和结果采集。这个压缩包OMNeT_TSNkit-master里就是这套环境的源码、示例拓扑和仿真脚本。它适合两类人:一类是想搞懂 TSN 调度算法到底怎么算的协议开发者,另一类是需要在不买硬件的前提下评估网络配置是否满足确定性时延的工业网络工程师。Python 标签在这里的作用主要是后处理仿真结果和批量跑参数扫描,不是仿真内核本身。
2. 把 TSNkit 挂进 OMNeT++:环境搭建与第一个可跑通的仿真
2.1 为什么选 OMNeT++ 而不是 ns-3 或纯 Python 模拟
做 TSN 仿真,选型时绕不开三个选项:ns-3、OMNeT++ 加 INET、或者自己用 Python 写离散事件循环。ns-3 的 TSN 支持相对零散,TAS 门控和帧抢占的模型需要自己补不少代码。纯 Python 模拟器写起来快,但一旦拓扑超过十几个节点、流量类别超过三类,事件调度的性能和时钟精度就成了黑匣子,结果可信度下降。OMNeT++ 的优势在于它本身就是为大规模离散事件仿真设计的,C++ 内核跑事件循环,INET 框架提供了完整的以太网协议栈,TSNkit 在此基础上补齐了 Qbv、Qbu、帧抢占和 CBS 信用整形器。常见做法是:用 OMNeT++ 做仿真内核,TSNkit 提供 TSN 专用模块,Python 只负责生成配置文件和分析.sca、.vec结果文件。这样分工明确,调试时也能分层定位问题。
2.2 安装 OMNeT++ 与导入 TSNkit 源码
OMNeT++ 的安装方式取决于操作系统。Linux 下我一般直接下源码编译,因为后面要改 TSNkit 的 C++ 模块,IDE 和命令行工具都得能用。Windows 下用官方安装包更省事,但注意安装路径不要有空格和中文,否则opp_makemake生成的 Makefile 会出玄学错误。
# 以 OMNeT++ 6.x 为例,Linux 源码编译 wget https://github.com/omnetpp/omnetpp/releases/download/omnetpp-6.0.1/omnetpp-6.0.1-linux-x86_64.tgz tar xzf omnetpp-6.0.1-linux-x86_64.tgz cd omnetpp-6.0.1 source setenv ./configure make -j$(nproc)编译完成后,把OMNeT_TSNkit-master解压到 OMNeT++ 的samples或独立工作区。TSNkit 通常以 OMNeT++ 项目形式组织,包含src、simulations、ned等目录。导入后先别急着跑复杂场景,用 IDE 打开simulations下的示例 ini 文件,确认ned路径和image路径指向正确。
# 在 OMNeT++ 工作区中构建 TSNkit 项目 cd OMNeT_TSNkit-master opp_makemake -f --deep -I/path/to/inet/src -L/path/to/inet/src -lINET make -j$(nproc)这里--deep表示递归扫描子目录源文件,-I和-L指向 INET 框架的头文件和库路径。如果 INET 没装,TSNkit 里依赖以太网帧格式和接口模块的部分会编译失败。参数说明:-f强制覆盖旧 Makefile,避免残留配置干扰。编译通过后,用opp_run跑一个最小示例,看事件调度器能否正常初始化。
2.3 配置第一个 TAS 门控仿真:拓扑、流量与 ini 参数
TSNkit 的示例通常包含一个线性拓扑或星型拓扑,节点类型有交换机、终端节点和时钟主节点。第一次跑通建议用官方自带的tsn_tas_basic之类的 ini,先不改拓扑,只改流量参数,观察门控列表对时延的影响。
# omnetpp.ini 片段:定义一个 TAS 门控场景 [Config TAS_Basic] network = TSN_LinearTopology *.switch.queue[*].queueType = "TASQueue" *.switch.queue[*].gateControlList = "gcl_8queue.xml" *.terminal[*].app[0].startTime = 0.1s *.terminal[*].app[0].interval = 1ms *.terminal[*].app[0].packetSize = 500Byte *.terminal[*].app[0].priority = 3gateControlList指向一个 XML 文件,里面定义了每个队列的门在什么时间窗口开、什么时间关。priority决定流量进入哪个队列,TAS 按队列做时间片轮转。常见坑是门控周期和流量发送周期不匹配,导致某些队列永远排不上,仿真结果里时延直接爆表。我一般会先把门控周期设成流量周期的整数倍,再逐步调相位。
3. 调度算法落地:从 EDF 到 WEDF 在 TSNkit 里怎么改、怎么验
3.1 TSNkit 中调度器模块的代码结构与扩展点
TSNkit 的调度逻辑通常放在src/scheduling或类似目录下,核心类继承自 OMNeT++ 的cSimpleModule。以 EDF(Earliest Deadline First)为例,调度器在每个时隙检查队列中帧的截止时间,选最小的先发。WEDF(Weighted EDF)在此基础上引入权重,影响截止时间的计算或队列选择顺序。要改调度策略,一般继承基类并重写selectFrame()或scheduleGate()方法。
// 伪代码示意:EDF 调度器核心选择逻辑 cMessage* EDFScheduler::selectFrame(QueueList& queues) { cMessage* selected = nullptr; simtime_t earliestDeadline = SIMTIME_MAX; for (auto& q : queues) { if (q.isEmpty()) continue; cMessage* head = q.front(); simtime_t deadline = head->getArrivalTime() + head->getDeadline(); if (deadline < earliestDeadline) { earliestDeadline = deadline; selected = head; } } return selected; }这段逻辑的关键参数是deadline,它通常由流量类别决定,在 ini 或 XML 里配置。WEDF 会把deadline除以权重,权重越大截止时间越靠前。改完后重新编译,用同一个 ini 跑两次,对比.vec文件里的端到端时延。
3.2 用 Python 做参数扫描与结果后处理
仿真跑一次只能看一组参数。实际调优时,门控列表的相位、队列权重、流量周期都有多个候选值,手动改 ini 效率太低。我一般用 Python 脚本生成一批 ini 文件,批量调用opp_run,再用pandas读.sca和.vec做统计。
import subprocess import pandas as pd from pathlib import Path # 参数扫描:门控周期从 500us 到 2ms,步长 250us results = [] for gcl_period in [500e-6, 750e-6, 1e-3, 1.25e-3, 1.5e-3, 1.75e-3, 2e-3]: ini_path = Path(f"omnetpp_{gcl_period}.ini") ini_path.write_text(f""" [Config TAS_Scan] network = TSN_LinearTopology *.switch.queue[*].gateControlList = "gcl_{gcl_period}.xml" *.terminal[*].app[0].interval = 1ms """) subprocess.run(["opp_run", "-u", "Cmdenv", "-f", str(ini_path), "-n", "../ned:../src", "-l", "../src/TSNkit"], check=True) # 读取标量结果文件 sca = pd.read_csv("results/General-#0.sca", sep=" ", comment="#", header=None, names=["type", "module", "name", "value"]) avg_delay = sca[sca["name"] == "endToEndDelay:mean"]["value"].mean() results.append({"gcl_period": gcl_period, "avg_delay": avg_delay}) df = pd.DataFrame(results) print(df.sort_values("avg_delay"))这段脚本的逻辑是:为每个门控周期生成独立 ini,跑完仿真后从.sca里提取端到端时延均值,最后排序找最优周期。参数说明:-u Cmdenv表示用命令行界面跑,不弹图形窗口;-n指定 ned 文件搜索路径;-l加载编译好的库。注意.sca文件里的记录格式可能因 OMNeT++ 版本不同而有差异,解析前先用head看一眼实际内容。
3.3 验证调度策略是否生效:看哪些指标、怎么对比
跑完仿真不能只看平均时延。TSN 的核心是确定性,所以要看时延的分布和最大值。.vec文件里记录了每个帧的端到端时延,用 Python 画累积分布函数(CDF)最直观。如果 EDF 和 WEDF 的 CDF 曲线几乎重合,说明权重设置没起作用,或者流量优先级配置有问题。另一个指标是队列积压,看交换机出口队列的最大长度,如果某个队列持续增长,说明门控窗口分配不合理,高优先级流量被低优先级堵住了。我一般会同时导出时延 CDF、队列长度时间序列和门控状态时序图,三张图对在一起看,才能判断调度器是否按预期工作。
4. 避坑与排查:TSNkit 仿真里最容易翻车的五个地方
4.1 现象:仿真跑完时延全是零或异常小
原因通常是流量根本没发出来,或者接收端没统计到。检查 ini 里app的startTime是否晚于仿真结束时间,以及terminal节点的app数组下标是否写错。TSNkit 的示例里终端节点可能有多个 app,只配了app[0]但实际流量从app[1]发。解决:在finish()里打印发送和接收计数,确认包确实在流动。
4.2 现象:门控列表加载失败,仿真启动即报错
XML 格式对不上,或者路径没写对。TSNkit 的门控列表 XML 有固定 schema,队列数量、时间单位、周期字段名都不能错。常见错误是把duration写成length,或者时间单位用了us但解析器只认ns。解决:先用官方示例的 XML 跑通,再逐字段替换,每次只改一个值。
4.3 现象:EDF 和 WEDF 结果完全一样
权重参数没传到调度器里,或者调度器根本没被实例化。检查 ned 文件里交换机队列的schedulerClass是否指向了你改的那个类,以及 ini 里有没有覆盖默认调度器。另一个可能是流量优先级全设成了同一个值,EDF 和 WEDF 在这种输入下退化成同一种行为。解决:给不同流量类别设不同优先级和权重,再跑对比。
4.4 现象:仿真速度极慢,事件数爆炸
门控周期设得太小,比如 1us,而仿真时长是 10s,事件数直接上亿。TSN 仿真里门控切换本身就是事件,周期越小事件越密。解决:先用较大的门控周期(比如 1ms)验证逻辑,确认无误后再逐步缩小,同时用sim-time-limit限制仿真时长,别一上来就跑全天。
4.5 现象:Python 后处理读不到 .vec 文件里的数据
OMNeT++ 默认可能不记录向量,需要在 ini 里显式开启**.vector-recording = true。另外.vec文件是二进制或文本格式取决于配置,Python 解析时要用opp_scavetool先转成 CSV 再读,别直接硬解。解决:在 ini 里加output-vector-file = results/$(configname).vec,跑完后用opp_scavetool x results/*.vec -o results.csv导出。
5. 进阶技巧:用 Python 驱动批量仿真并自动生成门控表
5.1 从流量矩阵反推门控窗口的脚本思路
实际项目中,流量矩阵是已知的:哪些节点在什么周期发多少数据、截止时间是多少。手动写门控 XML 容易出错,我一般用 Python 根据流量矩阵算一个初始门控表,再丢给 TSNkit 验证。核心逻辑是:把超周期内所有流量的发送时间对齐到门控周期,按优先级分配时间片,确保每个队列在截止时间前有足够的开门窗口。
import xml.etree.ElementTree as ET def generate_gcl(flows, cycle_ns=1000000, slot_ns=125000): """flows: list of dict(priority, period_ns, size_bytes, deadline_ns)""" root = ET.Element("GateControlList", cycle=str(cycle_ns)) # 按优先级分组,每组分配连续时间片 for prio in sorted(set(f["priority"] for f in flows)): group = [f for f in flows if f["priority"] == prio] # 简化:每个优先级占一个 slot,实际需按带宽算 entry = ET.SubElement(root, "GateEntry", queue=str(prio)) entry.set("open", "0") entry.set("close", str(slot_ns)) slot_ns += slot_ns return ET.tostring(root, encoding="unicode") flows = [ {"priority": 7, "period_ns": 1000000, "size_bytes": 200, "deadline_ns": 500000}, {"priority": 5, "period_ns": 2000000, "size_bytes": 1500, "deadline_ns": 1500000}, {"priority": 3, "period_ns": 5000000, "size_bytes": 500, "deadline_ns": 4000000}, ] print(generate_gcl(flows))这段脚本输出一个简化的门控 XML,每个优先级占一个固定时间片。参数说明:cycle_ns是门控超周期,通常取所有流量周期的最小公倍数;slot_ns是每个队列的开门时长,需要根据流量大小和链路速率反算。实际使用时,这个初始表还要用 TSNkit 跑一遍,看时延是否满足截止时间,不满足就调整 slot 顺序或长度。
5.2 用 opp_scavetool 和 pandas 做结果聚合
批量跑完几十组参数后,手动看结果不现实。我习惯用opp_scavetool把所有.sca和.vec导出成 CSV,再用 pandas 做分组聚合。比如按门控周期分组,算每组时延的 99 分位和最大值,直接筛出满足确定性要求的配置。
# 导出所有标量结果到 CSV opp_scavetool x results/*.sca -o all_scalars.csv -F CSV-R # 导出向量结果(只导时延相关) opp_scavetool x results/*.vec -o all_vectors.csv -F CSV-R -f "name(endToEndDelay:vector)"导出后用 pandas 读all_vectors.csv,按run分组算分位数。注意 CSV 里可能有多个仿真运行的记录,用run或config列区分。这一步的坑是.vec文件可能很大,导出前先用-f过滤,别把队列长度这种高频记录也导出来,否则 CSV 几个 G,pandas 直接卡死。
5.3 一个我常犯的错误:忽略仿真预热时间
早期跑 TSN 仿真时,我总把sim-time-limit设成 1s,然后统计整个 1s 的时延。后来发现前 100ms 里队列还没稳定,门控表刚启动,时延数据波动很大,拉高了均值。从那以后我每次都在 ini 里加**.warmup-period = 0.1s,统计时只取预热后的数据。OMNeT++ 的warmup-period会自动丢弃预热阶段的结果,省得手动截断。这个习惯帮我避免了好几次误判——有一次差点因为预热期的异常时延把一套本来合格的配置否掉。希望帮到你。
本文还有配套的精品资源,点击获取