简介:面向轨道交通信号系统课程学习与目标检测实验复盘需求,这份文档以车载系统与控制模式实验为主线,串联CBTC环境下的ATC初始化、RM/CM/AM模式转换、ATS计划接收与运行曲线观察等环节,并关联OpenCV图像处理与Anaconda环境配置背景。内容涵盖实验目的、器材、操作内容、过程记录、实验分析与总结,涉及列车线路设置、到站开关门、提前发车、跳停提速等典型场景,可作为课程实验报告撰写与操作流程对照的参考。压缩包共1个doc文件,约27.52MB,单文件集中呈现完整实验记录与配图说明,便于按实验步骤检索。已有135人学习下载,适合轨道交通信号、列车控制及目标检测相关方向的学生和技术人员用于理解车载控制逻辑、整理实验分析思路并查漏补缺。
1. 从一次「点错按钮整组重启」说起:CBTC 车载实验里真正被约束的是什么
做轨道信号实验的人,第一次接触车载控制模式时,最容易踩到一个反直觉的坑:目标检测在这里根本不是一道“算法题”。给一张前向相机图,框出前车、信号机或者轨道上的异物,那只是感知层的一小步;真正决定列车能不能动、能跑多快、什么时候必须停下来的,是车载 ATP 手里那张「我已经被谁授权、授权到哪一米」的账。这套账算错一步,界面上不会给你任何红色提示,最常见的结果是三台计算机同时重启,整组重来。
值得拆的地方也在这里:CBTC 仿真实验里的 RM、CM、AM 三种模式,绿线黄线两条运行曲线,以及目标检测输出的那个距离值,本质上是同一套状态机在不同抽象层上的投影。搞懂它们的耦合关系,比背下二十步操作顺序有用得多——因为顺序是从状态迁移规则里长出来的,不是界面设计者的个人偏好。
2. 上线握手:三台计算机之间到底交换了哪几个字段
实验环境里每台列车至少要占用三台计算机:一台跑区域控制器(ZC),一台跑车载软件,一台跑 ATS 与驾驶台。很多人把这三台机器当成“三个窗口”,其实它们是三个职责完全不重叠的实体,任何一台缺位,另外两台都会停在一个静止的中间状态上。
2.1 角色边界:谁授权、谁计算、谁下计划
区域控制器负责的是列车占用的登记和移动授权(MA)的发放;它只认两样东西——列车 ID 和位置报告。ATS 负责的是计划层,也就是这列车今天跑哪条线、几点发车、停哪几个站、站间给多少运行时分。车载软件夹在中间,向上接收计划、向下执行模式,向内计算曲线。三者之间没有“谁指挥谁”的树状关系,更像是三份独立账本的对账关系。
这解释了一个新手最常见的困惑:为什么列车已经加进来了、界面也亮了,但驾驶台推牵引毫无反应?因为 ATS 那边还没下发运行任务。没有任务,车载就没有目标点,也就没有曲线,牵引指令进了状态机之后找不到可执行的分支,只能被丢弃。原文实验里专门记了这一条,属于典型的“看起来像手感问题,实际是状态缺失”。
2.2 注册握手的三次交换
严格来说,让一列车上线要经历三次交换,缺一不可。第一次是「加车」:在 ZC 侧创建一个列车实体,分配车组号,并把这个实体绑定到某个初始占用区段上——存车线、车辆段、停车场,都是合法的初始位置。第二次是「ATC 钥匙接通」:车载从待机态进入有人值守态,向 ZC 建立心跳链路。第三次才是「ATC 初始化」:车载把当前位置报告上去,ZC 拿它和占用表比对,一致才回一个注册成功。
顺序之所以不能反,是因为第二次交换建立的是“链路”,第三次交换建立的是“身份”。链路没通,身份报告发不出去;身份没建立,后面的 MA 根本不会算到这列车头上。常见的实现思路是把这三步写成一个带守卫的状态机,每一步只接受来自前一个状态的请求。
from dataclasses import dataclass @dataclass class OnboardCtx: train_id: str | None = None # 加车时由 ZC 分配,未加车为 None direction: str | None = None # "UP"/"DOWN",决定 MA 往哪个方向延伸 key_on: bool = False # ATC 钥匙状态,对应心跳链路是否建立 initialized: bool = False # 是否已向 ZC 完成位置注册 balise_id: str | None = None # 最近应答器编号,定位的绝对基准 offset_cm: int = 0 # 相对应答器的走行偏移,单位厘米 def atc_init(ctx: OnboardCtx) -> str: # 守卫一:没有列车实体,ZC 收到任何位置报告都会直接丢弃 if ctx.train_id is None: return "REJECT:NO_TRAIN" # 守卫二:钥匙未接通,车载不建链,请求根本发不出去 if not ctx.key_on: return "REJECT:KEY_OFF" # 守卫三:方向未选定,ZC 无法判断占用区段该往哪头延伸 if ctx.direction is None: return "REJECT:NO_DIRECTION" # 幂等处理:重复初始化直接返回上次结果,不产生新的注册事务 if ctx.initialized: return "OK:ALREADY" ctx.initialized = True return "OK:REGISTERED"这段守卫逻辑里有三个值得注意的点。train_id为空时返回的是拒绝而不是异常,因为 ZC 侧本来就不会为未知 ID 建占用记录,拒绝是正常路径而不是故障路径。direction之所以是硬性前置条件,是因为移动授权本身是「从车头往前进方向延伸的一段轨道」,没有方向就没有延伸方向,ZC 无法计算出 MA 的终点。最后的幂等分支是最容易被忽略的一格:实验里手快点两下初始化,第二次不会有任何界面反馈,它只是安静地返回了OK:ALREADY,看上去像“没反应”,实际上状态是对的。
参数上还有一处细节值得单独说:位置报告用的是「应答器编号 + 偏移」,而不是一个绝对的里程值。列车在两台应答器之间运行时,绝对位置是靠测速累积推算的,误差会随距离漂移;每次压过应答器,累计误差被清零一次。这就是为什么列车停在远离应答器的位置时,你看到的定位偶尔会“跳一下”。
2.3 存车线出发与车辆段出发的路径差异
这两种出发点在实验里都支持,但走的流程并不一样。从车辆段或停车场出发,前提是该段信号已经开放,列车以 RM 模式开到转换轨,在转换轨上完成模式升级;从存车线出发,则直接在存车线上手动选择初始位置再初始化。后者的关键动作是选对上下行——向右为上行,反之为下行。方向一旦选错,初始化会“成功”,注册回执也正常,但 MA 会朝着相反方向延伸,你推牵引时列车纹丝不动,界面还一切正常。这种失败没有任何报错,只能靠人核对。
3. RM / CM / AM 三档模式:授权收紧与放开的一条滑轨
把三种模式理解成“三档车速”是错的,它们区分的是「谁承担防护责任」和「谁承担驾驶动作」。这两件事在三种模式里的分配完全不同,切换的本质是把责任一项一项地交接出去。
3.1 RM:把防护责任交回司机
RM(限制人工驾驶)模式下,车载不做基于 MA 的完整速度防护,只做最基本的超速监督,常见的限值是 25 km/h。为什么是这个量级?因为这个模式的使用场景是车辆段、停车场、转换轨这类“没有连续位置报告和连续进路”的区域,ZC 在那里给不出可靠的移动授权,既然给不出,就只能退化成靠司机看地面信号行车,车载只兜一个最低限度的底。
这带来一个必须记住的推论:RM 模式下能开多快,取决于你看得见多远,而不是界面显示多远。原文实验里从车辆段用 RM 开到转换轨这一步,本质上就是在“没有 MA 保护”的区间里跑完最后一段盲区。
3.2 CM 与 AM 的分界:人控与机控,但防护都在 ATP 手里
CM(编码人工驾驶)模式下,牵引和制动由人给,但 ATP 全程盯着速度包络,一旦逼近常用制动曲线(SBI)就先报警、再干预。AM(自动列车驾驶)模式下,ATO 接管牵引和制动,人只负责开门、关门和发车确认这几件事。两者的共同点是防护责任都在车载 ATP 手里,区别只在执行机构听谁的。
所以从 CM 升到 AM,技术上不是“提速”,而是“把手从手柄上拿开”。这也是为什么 AM 的前置条件比 CM 苛刻得多:需要有有效的计划、有效的 MA、方向一致、ATO 使能。缺一条,AM 按钮就是灰的。原文实验里的操作顺序——先点 ATO 点亮,再点 ATO 发车点熄——其实对应的是“使能”和“触发”两个不同的事务,点亮是置使能位,点熄才是真正发出发车命令。
3.3 切换的前置条件:一张表比背步骤靠谱
| 切换方向 | 硬性前置条件 | 谁批准 | 失败时的表现 |
|---|---|---|---|
| 待机 → RM | 已加车、位置已选、方向一致、钥匙接通 | 车载本地 | 初始化无回执,界面停在灰态 |
| RM → CM | RM 参数点亮后熄灭、方向按钮已给、档位在零位 | 车载本地 | CM 按钮不可用,无提示 |
| CM → AM | MA 有效、计划已接收、ATO 使能、车门关闭 | 车载 + ZC | AM 按钮灰显或按下无响应 |
| AM → CM | 人工接管牵引或制动 | 车载本地 | 自动降级,曲线保留 |
| 任意 → RM | 停车、档位回零 | 车载本地 | 需重新走一遍模式点亮流程 |
表格里最值得盯的是 RM → CM 那一行。实验里“参数选择 RM 点亮 → 方向按钮向前 → RM 点熄”这三步,实际是在做一次“声明 + 确认”的握手:点亮是声明要退出 RM,方向是确认运行意图,点熄才是提交。少任何一步,状态机都不认。
速度监督这一层的实现,通常就是把当前速度和包络值做比较,中间留一段折算成速度当量的制动余量:
def speed_supervision(v_train, v_permit, latency_s=1.2, decel=0.8): # v_train / v_permit 单位 km/h;latency_s 为系统反应延迟 # decel 为常用制动减速度,单位 m/s^2 # 3.6 是 m/s -> km/h 的换算系数 margin = latency_s * decel * 3.6 v_sbi = v_permit - margin if v_train > v_permit: return "EMERGENCY_BRAKE" if v_train > v_sbi: return "SERVICE_BRAKE" return "NORMAL"margin这一项是拿时间换距离的保守折算:车载从“发现超速”到“制动真正建立”之间有延迟,这段延迟里列车已经跑掉了一段距离,必须提前从允许速度里扣掉。把latency_s调大,曲线会更保守、列车跑得更慢;调小则容易出现“贴着曲线跑、最后一刻才触发”的紧张感。仿真里这个参数通常比真实车辆更宽松,这也是实验里感觉列车响应“很积极”的原因之一。
4. 绿线与黄线:运行曲线和目标检测在同一个坐标系里怎么对齐
两条曲线并排显示的时候,很容易以为它们是同一个东西的两种画法。实际上它们来自两个完全不同的约束源,一条管“安全”,一条管“效率”。
4.1 黄线:进路开放度决定的移动授权包络
黄线由进路开放情况和 MA 终点共同决定。计算方式是反推:从 MA 终点往前,按制动模型倒着算出一条速度包络,任何一点的速度都不能超过“从这个点开始全力制动、刚好能在终点前停住”的速度。所以黄线的形状是典型的“越接近终点越低”的下降曲线,前方进路没开放,黄线终点就卡在靠近车头的位置,列车只能一点点往前蹭。
这解释了一个在实验里反复出现的现象:站间时间明明给得很长,车却开不起来。不是速度给不上去,而是前方进路没排通,黄线终点就在眼前,ATP 一直在压着速度。
4.2 绿线:站间运行时分反推的计划曲线
绿线来自 ATS 下发的计划。你在运行图上拖动两个站的时间点,实际改的是这段站间的运行时分;系统拿这个时分反推一条目标速度曲线,通常是牵引—惰行—制动三段式:起步段按牵引特性加速到目标速度,中间惰行维持,进站前按制动特性减速。站间时分给得越紧,目标速度越高;给得越松,绿线越平。
所以绿线和黄线的关系不是“主备”,而是“期望”与“上限”。列车实际跑出来的速度,永远取两者中更保守的那个。实验里“两站之间时间点拉得太长,到站开门后 AM 无响应”这一条,根子就在这里:计划里的发车时刻还没到,ATO 没有新的目标点,自然不会响应发车命令。解决办法是用提前发车把计划时刻强制刷新,而不是反复按 AM。
4.3 目标检测落在哪一层:把像素变成“前方多少米有占用”
视觉检测在这套系统里的定位很明确:它是给 MA 和曲线提供冗余感知的一路输入,不是唯一的定位依据。列车自身的绝对位置靠应答器加测速累积,视觉负责的是“确认前方轨道上有没有本不该出现的东西”这类需要看的问题。
从检测框到“前方多少米”,中间要过一步几何换算。常见做法是先把相机标定出内参,再用安装高度和俯角,把检测框底边中点映射到地面平面上:
import numpy as np # 相机内参(标定得到),fx/fy 为焦距像素值,cx/cy 为主点 K = np.array([[900, 0, 640], [0, 900, 360], [0, 0, 1]], dtype=float) H = 1.35 # 相机光心距轨面高度,单位 m def pixel_to_distance(u, v, pitch_deg=6.0): """把检测框底边中点 (u, v) 映射成沿轨面的纵向距离(米)""" theta = np.deg2rad(pitch_deg) # 相机俯角 y = (v - K[1, 2]) / K[1, 1] # 归一化像平面纵坐标 denom = y * np.cos(theta) + np.sin(theta) if denom <= 1e-6: return float("inf") # 射线与地面近乎平行,距离不可信 return H / denom这里最关键的不是公式,而是它的误差特性。距离和像平面纵坐标不是线性关系,v上几个像素的偏差,在近处可能只差半米,在六十米开外能差出十几米。加上轨道场景本身的问题——道床纹理重复、钢轨反光、隧道里光照突变——远距离检测框的底边基本只能当“有/无”用,不能当精确测距用。
这也正是设计上的取舍点:把检测结果当作触发降速或提示的补充信号,而不是直接拿它去改写 MA。MA 由 ZC 根据占用和进路计算,是安全相关的一路;检测是感知相关的一路,两者在曲线上的权重完全不同。实验里设置跳停让列车冲到 80 km/h,黄线依然按进路和 MA 在限制,就是这个道理——跳停改的是计划层(少停一站),不改进路层。
5. 避坑与排查:五条能让整组实验重来的操作
以下五条都是实际做实验时最容易翻车的位置,按“现象—原因—解决”记,比记操作手册有效。
5.1 上线与初始化阶段
一、点了初始化,界面毫无变化。现象是回执区域一直空白,车载停在灰态。原因通常是没先加车,ZC 侧根本不存在这个车组号的记录,位置报告发过去被静默丢弃。解决顺序是先加车到初始位置,确认存车线上出现车体,再走钥匙接通和初始化。判断是否成功的依据不是界面变亮,而是 ZC 侧给回了注册成功的回执。
二、注册成功但列车不动。现象是驾驶台推牵引没有任何速度显示,曲线区也是空的。原因是上下行选反了,MA 朝着列车尾部延伸,车头方向没有有效授权。检查办法是对照计划里的线路方向,确认“向右为上行”这一条,然后重新选位置、重新初始化。这种失败最阴险的地方在于所有回执都是正常的。
5.2 模式与发车阶段
三、CM 按钮点不亮。现象是从 RM 想升级到 CM,按钮一直是灰的。原因是 RM 参数没有按“点亮 → 给方向 → 点熄”的完整流程走完,状态机认为列车还处在 RM 声明中。解决是退回 RM 状态重走一遍,注意中间的方向按钮不能跳。
四、到站关门后 AM 无响应。现象是车门开关都正常,再点 AM 发车完全没反应。原因是计划里的发车时刻还没到,ATO 没有刷新目标点。这时候不需要反复按 AM,直接在列车处执行提前发车,把计划时刻强制刷新,再重新进 AM 即可。
5.3 曲线与检测阶段
五、检测框抖动导致距离值乱跳。现象是同一辆前车,算出来的距离在几米范围内反复横跳。原因是逐帧检测没有做时序平滑,单帧的框位置噪声被直接放大成了距离噪声。解决是加一层滑动中值或卡尔曼滤波,并且对远处目标做置信度加权,宁可让距离值迟钝一点,也不要让它抖。
注意:以上五条里,第一、二、三条的共同特征是“没有任何报错”。在轨道这类系统里,静默失败是常态而不是缺陷,判断成功与否必须看回执字段,不能看界面颜色。
6. 设计取舍:为什么这套系统宁可停住,也不肯猜一下
把这套系统的行为串起来看,会发现一个贯穿始终的原则:在状态不确定的时候,它选择停下,而不是选择推测。初始化顺序不能跳步、方向未定就拒绝注册、MA 没延伸就压着速度、重复初始化返回上一次的结果而不是重新开一个事务——每一条单独看都像是“不够智能”,合起来才构成一个可验证的安全边界。
这种取舍的代价很直接。实验里点错一步就整组重启,本质上是因为三台机器的状态必须严格一致:ZC 认为你在 A 区段、ATS 认为你在等发车、车载认为自己还没初始化,这三份账一旦对不上,继续往下跑只会产生更难排查的错位。重启是把状态强制拉回同一个已知点,代价是时间,收益是可复现。这和很多互联网系统“尽力降级、先服务再说”的思路正好相反,原因也简单:那边的错误代价是一次请求失败,这边的错误代价是物理意义上的越界。
感知层的定位也是同一个逻辑的延伸。目标检测在实验里被摆在辅助位置,不是因为技术不成熟,而是因为它输出的量本质上是一个带噪估计。让它去改写 MA,等于把一个统计意义上的置信度塞进一个要求确定性的授权链里,两边的语义根本对不上。合理的用法是让它在“确认前方是否有占用”这件事上做冗余投票,触发降速或告警,把最终决策权留给基于应答器和进路的确定性那一路。
要验证自己对这套机制的理解是否到位,有个很朴素的办法:不看界面,只看日志。把一次完整运行的状态迁移序列打出来——加车、钥匙接通、初始化回执、模式点亮序列、MA 终点变化、曲线重算时刻——然后和另一次成功运行的序列逐项比对,第一处出现差异的地方就是根因所在。这比盯着界面猜快得多,也是我后来每次做类似实验前的固定动作。
说实话,第一次做这个实验的时候我也觉得那些按钮顺序有点玄学,直到把状态迁移日志从头到尾对了一遍,才发现每一次“没反应”背后都有一个明确的守卫条件在拦着。从那以后我每次进实验室,都会先把日志窗口拉出来放在旁边,而不是等翻车了再去翻黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取