首页 / 资讯中心 / 文章详情

VC++ 6.0车站计算机联锁系统代码解析与维护实战指南

VC++ 6.0车站计算机联锁系统代码解析与维护实战指南 ★ FEATURED ARTICLE
简介这份车站计算机联锁程序代码基于VC6.0开发面向铁路信号专业学生、计算机联锁初学者及C项目研究者演示了标准车站界面的绘制与下行站基本联锁逻辑虽未包含故障处理但为理解联锁原理提供了可运行的仿真样本。压缩包共105个文件约4.1MB包含23个h头文件与21个cpp源文件构成核心逻辑另有dsp/dsw工程配置、res/rc界面资源、exe可执行程序及SQL2000数据库交互相关组件便于直接编译运行和断点调试。该资源已有1411人学习下载。通过研读代码可掌握轨道电路、信号机、道岔状态采集到安全控制指令输出的完整流程同时熟悉MFC对话框程序架构、ADO数据库操作及项目文件组织方式其CmpInterlock核心模块清晰划分了联锁运算与界面响应逻辑适合作为课程设计或毕业设计的改造起点。1. 车站计算机联锁VC 6.0代码到底在做什么做铁路信号这行的人几乎都绕不开车站计算机联锁这套东西。它是车站里管道岔、管信号机的“大脑”列车能不能进某条股道、信号机能不能给绿灯都得靠它的安全逻辑说了算。而直到现在市面上仍有大量在用和正在维护的联锁上位机程序就是用 VC 6.0 写出来的。拿到这类代码第一眼会觉得它和普通上位机软件没差别一样的 MFC 对话框一样的按钮、自绘画面。但真正往里看就会明白难点根本不在界面而在联锁运算那一块。这篇文章就按我实际梳理这类代码的习惯来写先拆层、拆线程、拆数据表再讲进路选排和信号开放的核心逻辑然后专门列几个我接手代码时踩过的坑——状态采集延迟、道岔超时、断电恢复、双机切换。最后给你一套能在自己电脑上把联锁逻辑跑起来验证的方法。适合正在接维护项目、想读懂代码的人也适合用 VC 6.0 做联锁仿真作业的读者。2. 先拆解代码结构联锁三层架构与线程划分2.1 三层架构操作显示层、联锁运算层、执行驱动层一套完整的车站计算机联锁系统在硬件上通常分为人机界面、联锁主机和现场设备三层。对应到 VC 6.0 代码里也会分成三个逻辑层。第一层是操作显示层用 MFC 写的界面程序负责显示站场图、进路排列状态、设备状态和报警信息接收车站值班员的鼠标操作。常见实现是用对话框加自绘控件站场图用位图加动态刷新它的特点是只管显示和输入不做安全判断。第二层是联锁运算层这是整个程序的核心。它接收操作层的命令结合现场采集的轨道、道岔、信号机状态按进路表做联锁运算输出控制命令。这一层代码必须按故障-安全原则设计对每一站状态的判断都必须冗余。第三层是执行驱动层跟轨道电路、转辙机、信号灯相连。常见有两种做法一种是联锁主机直接通过 I/O 卡采集和输出VC 代码里就要写驱动程序或调用动态库另一种是联锁主机通过串口或现场总线和现场执行单元通信那么这一层就是通信协议解析代码。# 代码目录的常见组织方式 Interlock/ ├── Interface/ // 操作显示层 │ ├── StationView.cpp // 站场显示 │ └── OperDlg.cpp // 值班员操作对话框 ├── Interlock/ // 联锁运算层 │ ├── Route.cpp // 进路建立/解锁 │ ├── Signal.cpp // 信号开放逻辑 │ ├── Switch.cpp // 道岔控制逻辑 │ └── Track.cpp // 区段状态处理 ├── DeviceIO/ // 执行驱动层 │ ├── SerialComm.cpp // 串口通信 │ └── Protocol.cpp // 与执行单元的协议解析 └── Common/ // 公共数据表与工具 ├── RouteTable.cpp ├── SwitchTable.cpp └── TrackTable.cpp在实际项目里模块划分并不统一。我见过有的代码把联锁运算直接写在按钮响应函数里点击“排列进路”按钮后同步把进路建立了。这种做法在小站仿真里能跑但拿到真实设备上非常危险——如果界面线程卡一下整个联锁运算就失去响应。所以我帮某个维护项目梳理代码时第一个任务就是把联锁运算从界面线程里拆出来让它在独立线程里按固定周期跑。三层结构有个很实在的好处联锁运算层可以在不依赖真实硬件的情况下单独测试。把 DeviceIO 层替换成仿真数据源就能在普通 PC 上把整套站场逻辑跑起来。这也是后面搭验证环境的基础。你可以把三层理解成界面是前台联锁是后台决策IO 是手脚三者各司其职。2.2 线程划分为什么联锁运算不能放在界面线程里VC 6.0 的 MFC 程序默认是单线程的。很多初学写的程序把逻辑都放在按钮响应函数里看起来没问题但联锁程序对实时性有硬要求。轨道区段的占用变化、道岔的到位反馈、信号机的灯丝状态这些信息随时可能从通信口进来。界面线程要处理绘图和鼠标消息没法保证每 200ms 就处理一次 IO更不敢在绘图过程中去读串口。常见做法是开两个线程一个是联锁运算线程以固定周期扫描所有状态另一个是通信线程专门收发与现场设备的报文。界面线程只在状态变化时被通知刷新。下面这个线程函数是典型的联锁运算周期结构// 联锁运算线程每个周期做一次采集、运算、输出 UINT InterlockThread(LPVOID pParam) { CInterlockApp* pApp (CInterlockApp*)pParam; while (!pApp-m_bStopThread) { DWORD dwStart GetTickCount(); pApp-CollectDeviceState(); // 采集轨道/道岔/信号机状态 pApp-ProcessCommands(); // 处理值班员操作命令 pApp-RunInterlockLogic(); // 执行联锁运算 pApp-OutputControlCmd(); // 输出控制命令 pApp-UpdateUI(); // 通知界面刷新 DWORD dwCost GetTickCount() - dwStart; if (dwCost 200) // 周期200ms Sleep(200 - dwCost); } return 0; }逻辑说明CollectDeviceState 把通信线程收到的实时状态写入共享数据区不直接去读串口。ProcessCommands 处理值班员的操作指令比如“排列进路 X”。RunInterlockLogic 是核心运算进路选排、信号开放判定都在这一步。OutputControlCmd 将运算结果通过通信线程发出去。每周期 200ms 是工程上的常见折中太快会导致 IO 压力大太慢会让人感觉信号开放不跟手。参数说明周期时间 200ms 不是一个统一标准不同项目常用 100ms 到 500ms 之间。共享数据区的访问原则上要做锁保护用 VC 6.0 时一般用 CRITICAL_SECTION 或简单标志位。注意不要在持锁时调用 Sleep否则另一个线程就卡死了。界面刷新不要每个周期都做我一般只在状态变化时置一个脏标记界面线程检测到标记再重绘不然站场图闪烁会很严重。2.3 四张数据表进路表、道岔表、信号机表、区段表联锁逻辑不是一堆 if-else 硬编码出来的。真正的工程代码都会把车站的静态拓扑信息——哪个信号机管哪条进路、哪组道岔在什么位置——抽成数据表。进路表是最重要的一张表每条进路记录它的始端信号机、终端信号机、经过的区段、道岔及其位置、需要照查的敌对进路、以及进路锁闭后要驱动的道岔动作。在 VC 6.0 里最常见是用结构体数组来组织typedef struct { int nRouteID; // 进路编号 int nStartSignal; // 始端信号机ID int nEndSignal; // 终端信号机ID int nTrackSections[8]; // 经过的轨道区段ID列表-1结尾 int nSwitches[4]; // 进路上的道岔ID列表 int nSwitchPos[4]; // 道岔要求位置1定位2反位 int nOppRoute[4]; // 敌对进路ID列表-1结尾 int nRouteType; // 0列车进路1调车进路 } ROUTE_TABLE;说明nTrackSections 数组长度 8 是在最大区段数小于 8 的小站常用写法。如果你在维护的站场有跨多股道的长进路要注意数组边界越界写是 VC 6.0 时代最隐蔽的内存问题。nSwitchPos 代表进路建立时要求道岔所处的位置。定位和反位在代码里通常用 1/2 表示但也有人用 0/1这个全看项目约定接手代码时第一件事就是对图纸。nOppRoute 里的敌对进路判断是联锁里最容易漏逻辑的点两条进路只要存在共用区段或者存在侧向冲突就必须互斥。区段表单独存储轨道区段的占用状态、锁闭状态。道岔表存道岔当前位置、允许转换状态、锁闭状态。信号机表存灯色、灯丝状态、开放允许等信息。四张表加进路表构成联锁运算的全部静态加动态数据源。动态状态每周期刷新在旧代码里经常能看到用全局数组保存状态而不是写在类成员里。这不算错但注意可维护性——如果你接手的代码里到处是 extern int track_status[64]先忍住别重构保证能跑比好看重要。3. 核心联锁逻辑用 VC 6.0 写进路选排与信号开放检查3.1 进路选排先查表、再转岔、再锁闭进路选排是指值班员点击“排列进路”命令后联锁程序把这条进路涉及的道岔转到要求位置并把进路锁闭起来的过程。逻辑顺序不能反先确认进路空闲再转岔道岔到位后锁闭进路和道岔最后才允许信号开放。这个顺序一旦乱就可能出现道岔还没到位信号就开放的事故。常规代码流程可以写成下面这个样子BOOL CInterlockLogic::ArrangeRoute(int nRouteID) { ROUTE_TABLE* pRt GetRouteByID(nRouteID); if (pRt NULL) return FALSE; // 1. 检查进路上的所有轨道区段空闲且未锁闭 for (int i 0; pRt-nTrackSections[i] ! -1; i) { if (GetTrackOccupied(pRt-nTrackSections[i]) || GetTrackLocked(pRt-nTrackSections[i])) { SetErrMsg(进路有区段占用或已锁闭); return FALSE; } } // 2. 检查敌对进路是否已建立或已锁闭 for (int j 0; pRt-nOppRoute[j] ! -1; j) { if (GetRouteActive(pRt-nOppRoute[j])) { SetErrMsg(敌对进路未解锁); return FALSE; } } // 3. 转换道岔到要求位置 for (int k 0; pRt-nSwitches[k] ! 0; k) { if (GetSwitchPos(pRt-nSwitches[k]) ! pRt-nSwitchPos[k]) { MoveSwitch(pRt-nSwitches[k], pRt-nSwitchPos[k]); } } // 4. 等待道岔全部到位后锁闭进路 if (AllSwitchInPos(pRt)) { LockRoute(pRt-nRouteID); SetRouteActive(pRt-nRouteID, TRUE); return TRUE; } return FALSE; }逻辑说明第三步只是发出转岔命令道岔转换需要时间实际工程代码里进路选排通常是一个状态机要等道岔到位反馈来了才能进入“锁闭”状态。道岔转换期间如果有人取消排列必须能把已转了一半的道岔停住并回转这叫道岔转换中途控制很多初版代码都漏掉。第四步的锁闭包括进路锁闭和道岔锁闭锁的是动态状态避免道岔在进路使用中被转动。SetRouteActive 是标记敌对进路关系的关键只要有敌对进路活动这条进路就别想建立。参数说明GetSwitchPos 返回的 1/2 要和你道岔表的定义保持一致。最常见的坑是设计图纸里定位写作 1、反位写作 2代码里却用了 0/1导致控制命令全部反转。接手代码时第一时间要把表里的定义和现场图纸对上。MoveSwitch 通常是异步的只发命令不能假设道岔已经到位这也是为什么有第 4 步的等待判断。3.2 信号开放检查区段空闲、道岔位置、敌对进路一条不能少进路锁闭之后信号机才允许开放。信号开放判断的每一项都不能单独作为充分条件必须全部满足缺一项都是拿安全开玩笑。我梳理信号开放检查代码时习惯按固定顺序逐项检查写出来的逻辑就是下面这种BOOL CInterlockLogic::CanOpenSignal(int nRouteID) { ROUTE_TABLE* pRt GetRouteByID(nRouteID); // 1. 进路必须处于锁闭状态 if (GetRouteLockState(nRouteID) ! LOCKED) return FALSE; // 2. 进路内区段空闲锁闭后、开放前再确认一次 for (int i 0; pRt-nTrackSections[i] ! -1; i) { if (GetTrackOccupied(pRt-nTrackSections[i])) return FALSE; } // 3. 道岔位置在要求位置 for (int k 0; pRt-nSwitches[k] ! 0; k) { if (GetSwitchPos(pRt-nSwitches[k]) ! pRt-nSwitchPos[k]) return FALSE; } // 4. 敌对进路未建立、未锁闭 for (int j 0; pRt-nOppRoute[j] ! -1; j) { if (GetRouteActive(pRt-nOppRoute[j])) return FALSE; } // 5. 信号机灯丝完好允许开放 if (GetSignalLampState(pRt-nStartSignal) ! LAMP_OK) return FALSE; return TRUE; }逻辑说明信号开放不是“一键就能放”而是要反复确认。第 1 步和第 4 步合起来是“联锁”二字的核心含义没有锁闭不能开有敌对进路不能开。第 2 步在锁闭后还要再查一次区段空闲是因为存在“锁闭时空闲、开放前突然占用”的极端情况这个重复检查不是多余的。参数说明GetRouteActive 判断敌对进路是否“已建立或已锁闭”比单纯判断信号是否已开放更严格。有些旧代码只判断信号机开放状态没判断敌对进路锁闭状态这个漏洞很致命。灯丝检查在仿真代码里经常被省略但真机上必须有。灯丝断丝会导致信号不能开放或自动降级为红灯。如果你在做仿真至少要把这套检查流程完整保留灯丝状态可以用一个开关模拟别偷懒删掉。3.3 状态采集与命令输出通信线程和联锁线程怎么配合联锁主机和现场设备之间在工程上有两种接法。一种是通过安全继电器接口直接用 I/O 卡采位和输出另一种是通过现场总线或串口通信。不管是哪种在 VC 6.0 代码里都逃不开一个通信线程不断读数据、联锁线程定期取数据的过程。这里最常见的反面教材是直接把采集代码写在联锁运算周期里用串口同步读// 反面示例直接在联锁周期里阻塞读串口 char buf[32]; DWORD dwRead 0; ReadFile(hCom, buf, 32, dwRead, NULL); // 设备无响应时会一直卡住这样写的问题很直接设备一旦不响应整个联锁运算停摆界面完全假死。我在某次维护中就遇到真实翻车现场某设备在高温天气下通信偶发中断程序直接卡在串口读上直到值班员反馈“信号全没了”才查出来。正确做法是把通信收和运算解耦分成两个线程// 通信线程循环收包解析后写入共享缓冲 void CommThread() { while (!bStop) { char recvBuf[64]; DWORD nRead 0; if (ReadFile(hCom, recvBuf, sizeof(recvBuf), nRead, NULL)) { EnterCriticalSection(cs); ParseAndStore(recvBuf, nRead); // 解析并写入共享缓冲 LeaveCriticalSection(cs); } } } // 联锁线程每个周期从共享缓冲取最新状态 void InterlockThread() { while (!bStop) { EnterCriticalSection(cs); CopyLatestState(g_DeviceState, g_RecvBuffer); LeaveCriticalSection(cs); RunInterlockLogic(); } }逻辑说明通信线程只负责收包和解析解析后的数据放进共享缓冲。联锁线程每个周期开始时取一次最新状态取完立即释放锁再去做运算。这样即使通信卡顿联锁线程也只是拿到旧状态不会卡死。参数说明通信线程读设备超时时间一般设 500ms 到 1000ms用 WaitForSingleObject 配合 OVERLAPPED I/O 实现。VC 6.0 里直接用同步 ReadFile 简单但必须开超时或独立线程别在联锁周期里同步读。共享缓冲要防止读写冲突用 CRITICAL_SECTION 最省事注意临界区对象要全局或类成员不能是局部变量。老项目的通信协议常见格式是固定字节包含帧头、设备号、数据、校验校验不对的帧直接丢弃不能因为一帧坏数据就中断程序。4. 联锁代码避坑指南五个经典翻车现场与处理4.1 信号不开放状态采集延迟导致的数据旧账现象进路条件肉眼看着全都满足区段没车、道岔在正确位置、敌对进路也没排但信号就是不开。值班员反复取消重排好几次信号才慢吞吞开放。原因状态采集有延迟。通信线程收到的轨道区段占用信息是上一周期的数据联锁线程却把旧状态当成实时状态。常见于只采集状态却没有加“状态有效时间”标志设备前一秒断线恢复后旧状态被当成实时状态参与运算。解决给每条动态状态加时间戳。只有采集时间在最近一个周期内才参与联锁运算超过周期阈值判为“状态超时”进路按不安全处理信号禁止开放。我在某站维护时就在轨道状态结构体里加了个 DWORD dwTimeStamp每次更新时赋值 GetTickCount()运算前先检查时间差。折腾几个小时后定位到的问题就是它。4.2 道岔不动控制命令发出但没有执行回执现象命令已经发给道岔了现场转辙机没动作程序里也不报任何错误。值班员盯着站场图道岔显示还是原来的位置过一会儿进路自动取消了但报警记录里什么都没有。原因道岔转换是通过继电器或转辙机控制器执行的命令帧发出去不代表装置动作。旧代码里 MoveSwitch 只负责发命令没有跟踪执行结果。通信线上有噪声丢包、控制电路故障、转辙机卡阻都可能导致命令没被执行但程序一无所知。解决加“控制输出-状态回执”的双向确认。道岔命令发出后记录目标位置然后在每个周期检查实际位置是否等于目标位置超过设定时间仍未到位则判定道岔转换超时给出报警并取消锁闭、回退进路。超时时间一般设 3 到 5 秒转辙机正常转换时间小于 2 秒留出余量。4.3 断电恢复后状态不一致初始化顺序必须分两步现象程序断电重启后站场图上显示某些区段还在锁闭状态但现场设备实际已经解锁了。值班员想重新排进路程序提示“进路未解锁”直接把人搞懵。原因联锁程序里进路锁闭、道岔锁闭等状态属于动态数据断电重启后程序按固定顺序初始化。有些老代码会用文件或内存映射保存状态但保存顺序和数据有效性没处理好导致恢复后的状态是“半锁闭”状态。解决初始化分两步。第一步清空全部动态状态然后读外部记忆数据第二步根据外部数据还原进路状态。如果外部记忆数据缺失只能按“全站处于解锁状态”处理然后通过与现场设备逐点核对来重建状态。这看起来慢但安全。注意不要在初始化时自动“补锁”那是拿猜测当事实。4.4 运行越来越卡VC 6.0 的 GDI 对象泄漏现象程序跑个两三天操作响应越来越慢站场图刷新像慢动作。重启程序又能坚持几天过几天又卡。值班员普遍反映“这软件要天天重启”。原因内存泄漏。VC 6.0 下最常见的是 GDI 对象泄漏和内存碎片。站场显示刷新时每帧创建新的画刷、画笔画布旧对象没释放。或每次状态变化都 new 一个字符串对象忘了 Delete。长时间跑下来句柄数飙升系统资源耗尽。解决写代码时养成好习惯凡是 CreateCompatibleDC、CreateFont、new 出来的对象用完后记得 DeleteObject、Delete。排查时开任务管理器看 GDI 对象数如果持续增长就是泄漏。还可以在代码里用 C 的析构机制封装 GDI 对象析构函数里统一释放。VC 6.0 时代这个风格不太流行但值得自己动手写一个。4.5 双机切换后逻辑异常状态同步不能只传快照现象双机热备系统切换后联锁逻辑出现异常。轨道区段明明占用着信号却开出来了或者明明该解锁的进路一直锁着。值班员说“切完机就不对劲”。原因主备机数据不同步。联锁主备机靠通信线同步状态如果同步的是“当前状态快照”而不是“状态变化日志”在某一瞬间切换就会丢失变化事件。比如轨道区段在切换瞬间刚变成占用备机没收到变化事件只收到一份之前的空闲快照于是保留旧的空闲状态做出了错误判断。解决同步数据设计时同步“操作日志加状态快照”。每次联锁运算产生的状态变化事件带序号备机按序号跟踪。如果序号丢失则拒绝对接。切换时先切换状态缓存再重启联锁周期不能直接拿主机的实时状态当全部历史。双机切换的代码逻辑要反复测试我在实验室里用一个定时器模拟主备切换每隔几十秒切一次跑一整个晚上看会不会出问题。5. 进阶VC 6.0 下搭建联锁仿真验证环境5.1 把 IO 层替换成虚拟设备在 PC 上把联锁跑起来写联锁代码时先把 IO 层独立出来是个能省下大量时间的习惯。我一般会在类 CDeviceIO 里定义纯虚接口真机实现是串口通信仿真实现是内存变量。这样联锁运算层不依赖真实硬件任何一台 PC 都能跑通。class CDeviceIO { public: virtual BOOL ReadTrackState(int nTrackID, BOOL* pOccupied) 0; virtual BOOL ReadSwitchPos(int nSwitchID, int* pPos) 0; virtual BOOL WriteSwitchCmd(int nSwitchID, int nPos) 0; virtual BOOL WriteSignalCmd(int nSignalID, int nColor) 0; }; class CSimDeviceIO : public CDeviceIO { public: BOOL ReadTrackState(int nTrackID, BOOL* pOccupied) { *pOccupied m_bTrackOccupied[nTrackID]; // 仿真空闲/占用 return TRUE; } BOOL WriteSwitchCmd(int nSwitchID, int nPos) { m_nSwitchPos[nSwitchID] nPos; // 仿真直接到位 return TRUE; } };逻辑说明仿真 IO 核心是状态翻转和命令直接生效。道岔从定位转反位真机需要两三秒仿真是立刻完成。所以我建议在仿真里加入延迟模拟比如移动到位前先延迟一小段时间否则发现不了道岔超时这类时序 bug。m_bTrackOccupied 这些仿真状态可以用 MFC 对话框上的复选框模拟也可以加一个定时器随机翻转看联锁能不能正确处理。参数说明仿真建议故意加入 2 到 3 秒道岔转换延迟、1 秒信号开放延迟把真实环境的不确定性引进来。随机占用要在所有区段上都做不能只测一两条否则边界情况全漏了。5.2 用固定的进路表做逐条进路遍历验证搭建完仿真环境最重要的验证就是逐条进路测试。把进路表里每条进路都排列一遍验证信号开放是否正常然后人为设置区段占用、道岔位置错误、敌对进路未解锁分别验证信号是否被拒绝开放。测试时我习惯准备一张纸把每条进路按列车进路和调车进路分类逐条打钩。做完这些后一定记得测取消进路、人工解锁、接近锁闭这些次级逻辑。很多仿真程序都栽在“正常排列都能过一到取消就不行”上。取消进路时程序要把已锁闭的进路完整解开道岔恢复可动这个反向流程比正向排列更容易出问题。处理这类问题的经验是别怕麻烦把每条进路的正反流程都跑一遍跑完再看双机切换和断电恢复。仿真环境里能发现的问题到了现场那就是事故隐患。这条习惯是我吃过亏换来的希望你也能用上希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站