
这篇解决一个问题拿到一台新设备的机械图纸和工艺要求软件从第一行代码到整机跑起来到底要写哪些东西按什么顺序想有哪些写法从最粗糙到最通用怎么演进。一、先看清一台设备软件要管什么写代码之前先想清楚这台机器有哪些「物理部件」软件要管它们的什么。不管你是 Handler、贴片机、分选机还是包装线拆到底都是这几类能动的电机轴X、Y、Z、旋转、变距每根要能 Move、WaitArrive、Home、读编码器。气缸顶升、抱盘、门锁、压紧每个要能 MoveTo(原位/工作位)、WaitArrive、查传感器。真空/破真空开、关、检测真空值。吸嘴一组阵列每只独立开关真空、检测、计数。能看的传感器到位、原点、安全、叠料、真空、安全触边读 ON/OFF。相机触发拍照、取结果、坐标转换。能测的测试机发指令、等结果、拿 Bin 分类。能协调的工位Buffer、料道、压台谁在用、谁在等、就绪握手。总控整机状态、启停、回零、结批、报警。能交互的UI按钮、状态显示、参数编辑、报警弹窗、手动调试。数据库报警记录、历史数据、批次信息。通信SECS/GEM、MES 上报。一台设备的软件就是把这些东西抽象成代码对象、组织成线程、串成流程、加上异常处理和人机交互。二、第一层写法一个手臂一个线程硬写到底新手第一次写设备软件最直觉的写法是这样的。思路每个会动的部件开一个线程线程里直接调硬件 API用一堆全局 bool 互相通知。代码骨架// 全局变量 bool g_bBufferReady false; bool g_bArmBusy false; int g_nMachineState 0; DMC5812* g_pCard nullptr; // 运动控制卡 // 手臂线程 void ArmThread() { while (g_bRunning) { Sleep(1); if (g_nMachineState ! RUNNING) continue; // 等料道准备好 if (!g_bBufferReady) continue; // 取料 g_pCard-MoveTo(AXIS_X, pickPosX); g_pCard-WaitArrive(AXIS_X); g_pCard-MoveTo(AXIS_Z, pickPosZ); g_pCard-WaitArrive(AXIS_Z); OpenVacuum(); Sleep(300); if (!CheckVacuum()) { AfxMessageBox(真空失败); continue; } g_pCard-MoveTo(AXIS_Z, safePosZ); // 搬运 g_pCard-MoveTo(AXIS_X, placePosX); g_pCard-WaitArrive(AXIS_X); // 放料 g_pCard-MoveTo(AXIS_Z, placePosZ); g_pCard-WaitArrive(AXIS_Z); CloseVacuum(); Sleep(200); g_pCard-MoveTo(AXIS_Z, safePosZ); g_bBufferReady false; g_bArmBusy false; } } // 料道线程 void ChannelThread() { while (g_bRunning) { Sleep(1); if (g_nMachineState ! RUNNING) continue; if (g_bArmBusy) continue; // 进盘、顶升、解锁 // ... g_bBufferReady true; g_bArmBusy true; } } // 启动 void StartMachine() { g_pCard new DMC5812(0); g_pCard-Init(); g_bRunning true; g_nMachineState RUNNING; std::thread(ArmThread).detach(); std::thread(ChannelThread).detach(); }为什么新手会这么写因为它「能跑」。两个线程一个管手臂一个管料道用两个 bool 互相通知逻辑直白调试断点一打就能看。为什么撑不住写两个线程、两个 bool 还行。写到第 5 个模块、第 10 个 bool问题全来了bool 语义混乱g_bBufferReady是「料道有料」还是「料道准备好交」还是「手臂可以来取」没人记得清。竞态线程 A 刚读完g_bBufferReady true线程 B 立刻改成falseA 还以为有料。加模块要改老代码加一个 Buffer 模块手臂线程里要加if (!g_bBufferReady) ...料道线程也要改到处插入。停机不干净g_bRunning false后手臂可能正卡在WaitArrive里几秒后才退出。这期间料道还在进盘。报警靠弹窗AfxMessageBox打在运动线程里弹窗不点线程永远卡住。硬件 API 散落g_pCard-MoveTo直接写在业务流程里换一张卡要改几十处。没法复用换一台设备这些代码一行都用不上因为全写死了。这一层的问题本质是没有抽象没有边界所有东西搅在一起。三、第二层写法抽象硬件封装接口痛够了之后第一件事是把硬件 API 藏起来业务代码不直接调控制卡。思路给每类硬件做一层封装电机轴封装成Axis气缸封装成Cylinder吸嘴封装成Picker。业务代码只调封装后的接口不知道底下是什么卡。代码骨架// 轴封装 class Axis { int m_axisId; DMC5812* m_card; public: bool Init(DMC5812* card, int id) { m_card card; m_axisId id; return true; } RunRetMotionErr MoveTo(double pos) { m_card-MoveTo(m_axisId, pos); return RunRetMotionErr(); } RunRetMotionErr WaitArrive(double pos, int timeoutMs 10000) { int t 0; while (!m_card-IsArrived(m_axisId)) { Sleep(10); t 10; if (t timeoutMs) return RunRetMotionErr(MotionErr::Timeout); } return RunRetMotionErr(); } RunRetMotionErr Home() { ... } double GetPos() { return m_card-GetEncoder(m_axisId); } }; // 气缸封装 class Cylinder { SwitchObj* m_valve; // 控制阀 SensorObj* m_arriveSen; // 到位传感器 SensorObj* m_originSen; // 原位传感器 int m_timeoutMs 3000; public: RunRetCylinderErr MoveToWork() { m_valve-WriteBit(true); int t 0; while (!m_arriveSen-ReadBit()) { Sleep(10); t 10; if (t m_timeoutMs) return RunRetCylinderErr(CylinderErr::Timeout); } return RunRetCylinderErr(); } RunRetCylinderErr MoveToOrigin() { ... } };业务代码变成// 手臂线程 void ArmThread() { while (g_bRunning) { Sleep(1); if (g_nMachineState ! RUNNING) continue; if (!g_bBufferReady) continue; auto ret m_xAxis-MoveTo(pickPosX); if (!ret.IsOK()) { Alarm(ret); break; } ret m_zAxis-MoveTo(pickPosZ); if (!ret.IsOK()) { Alarm(ret); break; } // ... } }改进了什么换卡只改封装层业务代码调Axis::MoveTo不知道底下是 DMC5812 还是雷赛。换卡只改Axis内部。超时统一处理WaitArrive自带超时不会永远卡住。错误有返回值RunRetMotionErr告诉调用方成功失败不用自己猜。可测试Axis可以 mock单元测试不用真卡。还有什么问题bool 互相通知还在g_bBufferReady竞态没解决。线程还是硬写手臂线程里取料放料全写死换流程要改线程函数。没有状态机流程是线性代码停在某一步恢复不了。没有统一管控停机还是靠g_bRunning没法统一停所有模块。报警还是散落每个线程自己Alarm没有统一出口。这一层解决了「硬件封装」但没解决「模块组织和协作」。四、第三层写法Actor 基类 状态机 注册表接着把「模块」抽象出来。每个模块手臂、料道、Buffer、测试台都是一个 Actor有统一的生命周期接口有注册表统一管理。思路抽一个Actor基类所有模块继承它统一Run/Stop/Home/EndLot/WorkFlow。每个 Actor 一个线程线程里跑状态机switch Step。用注册表统一管理所有 Actor一键启停。资源占用用SetUsedBy/SetNotUsedBy代替裸 bool。报警走统一出口严重故障走全局ControllerStop。Actor 基类class Actor { bool m_bRunFlag false; bool m_bThreadExist false; bool m_bEndedLot true; public: static vectorActor* g_Actors; // 注册表 static Actor* g_Controller; // 总控入口 void Run() { m_bRunFlag true; } void Stop() { m_bRunFlag false; } bool IsRun() { return m_bRunFlag; } void EndLot() { m_bEndingLot true; } bool GetEndedLot() { return m_bEndedLot; } int CreateThread() { if (!m_bThreadExist) { m_bThreadExist true; std::thread t(std::mem_fn(Actor::WorkFlow), this); t.detach(); } return 0; } static void RunAllActors() { for (auto* a : g_Actors) if (!a-GetEndedLot()) a-Run(); } static void StopAllActors() { for (auto* a : g_Actors) a-Stop(); } static void ControllerStop() { if (g_Controller) g_Controller-Stop(); } virtual void WorkFlow() 0; // 子类实现状态机 virtual void HomeFlow() 0; virtual bool HomeData() 0; int Alarm(shared_ptrResult ret) { // 统一报警出口日志 信号 蜂鸣 Log(ret-GetSerialErrorString()); m_SignalAlarmOut.emit(time, code, desc); return 0; } };手臂 Actorenum class ArmStep { WaitRun, CalPick, Pick, MoveToPlace, Place, Done }; class Arm : public Actor { Axis* m_zAxis; Axis* m_xAxis; vectorPicker* m_pickers; StepArmStep m_step; vectorStationLine m_StLine; public: void WorkFlow() override { while (true) { Sleep(1); if (!m_bThreadExist) return; if (!m_bRunFlag) continue; if (m_bEndedLot) { Stop(); continue; } switch (m_step.Get()) { case ArmStep::WaitRun: if (HasWork()) m_step.SetStep(ArmStep::CalPick); break; case ArmStep::CalPick: if (!CalPickStation()) { Alarm(); break; } m_step.SetStep(ArmStep::Pick); break; case ArmStep::Pick: if (!DoPick()) { Alarm(); break; } m_step.SetStep(ArmStep::MoveToPlace); break; case ArmStep::MoveToPlace: MoveToPlacePos(); m_step.SetStep(ArmStep::Place); break; case ArmStep::Place: if (!DoPlace()) { Alarm(); break; } m_step.SetStep(ArmStep::Done); break; case ArmStep::Done: ResetState(); m_step.SetStep(ArmStep::WaitRun); break; } } } };初始化和启动// 创建 auto* arm new Arm(1, ArmTray); auto* channel new Channel(2, TrayChannel); Actor::g_Actors.push_back(arm); Actor::g_Actors.push_back(channel); // 回零 Actor::HomeAllActors(); // 启动 Actor::RunAllActors(); Actor::BeginAllThreads(); // 每个 Actor 创建线程跑 WorkFlow // 停机 Actor::StopAllActors();改进了什么统一管控RunAll / StopAll / HomeAll一键操作所有模块。状态机替代线性代码停在某步能恢复不从头跑。资源占用有协议SetUsedBy代替裸 bool记录谁在用。报警统一出口所有模块走Actor::Alarm不各自弹窗。加模块不改老代码新 Actor 注册进g_Actors老 Actor 不动。还有什么问题流程写死在 Actor 里手臂的取放逻辑写死在WorkFlow换流程上料变下料要改WorkFlow。硬件配置散落哪个气缸挂哪个传感器写在SetHardWare里几十行重复。模块间协作还是手动SetUsedBy解决了互斥但「等对方就绪」还是轮询。换型困难换产品要改坐标、改流程、改 Bin 分配散落在各处。这一层解决了「模块组织和统一管控」但没解决「流程可配置」和「硬件可装配」。五、第四层写法策略模式 工厂 配置驱动把「流程」从 Actor 里抽出来变成策略对象把「硬件装配」用工厂函数集中把「路线」用配置对象描述。这就是本文前面所有设计模式落地后达到的层次。三个关键变化变化一流程变成策略不写死在 Actor 里class Arm : public Actor { TransportStrategy* m_Transport; // 策略对象 vectorStationLine m_StLine; // 路线配置 void WorkFlow() override { if (m_Transport) m_Transport-TransportFlow(); } };Actor 只管「能怎么动」策略管「怎么搬」。换流程不换 Actor换策略或换m_StLine。变化二硬件用工厂创建不裸 new// 一行创建一个气缸装配信息在工厂里 m_pTopLift CreateCylinder(CyAssembly::TopLift, 1, 顶升气缸); m_pCatch CreateCylinder(CyAssembly::Catch, 2, 抱盘气缸);变化三上下料用路线配置切换不写两套代码// 上料料道 → Buffer → 压台 arm-m_StLine.push_back(StationLine(channel, buffer, pos_Tray, {LoadIC})); // 下料压台 → Buffer → 料道 arm-m_StLine.push_back(StationLine(buffer, channel, pos_Tray, {Bin1}));同一个 Actor、同一个策略路线反过来就是下料。完整的初始化代码长这样void CControl::InitMachine() { // 1. 创建硬件对象工厂 m_pTopLift CreateCylinder(CyAssembly::TopLift, 1, 顶升气缸); m_pCatch CreateCylinder(CyAssembly::Catch, 2, 抱盘气缸); // 2. 创建轴 m_pXAxis new Axis(1, X轴); m_pZAxis new Axis(2, Z轴); // 3. 创建传感器 m_pArriveSen new SensorObj(1, 到位传感器); // 4. 创建 Actor手臂、料道、Buffer auto* arm new Arm(1, ArmTray); auto* channel new Channel(2, TrayChannel); auto* buffer new Buffer(3, Buffer); // 5. 给 Actor 挂硬件 arm-SetAxis(m_pXAxis, m_pZAxis); arm-SetPickers(CreatePickers()); // 6. 给 Actor 挂策略 arm-m_Transport new TargetTransport(arm); // 7. 注册到全局 Actor::g_Actors.push_back(接着上面被截断的地方继续。 cpp Actor::g_Actors.push_back(arm); Actor::g_Actors.push_back(channel); Actor::g_Actors.push_back(buffer); // 8. 配置路线上料 UpdateActorWorkMode(); // 根据 WorkMode 填 m_StLine // 9. 回零 Actor::HomeAllActors(); // 10. 启动 Actor::RunAllActors(); Actor::BeginAllThreads(); }这就是一台设备从零到跑起来的完整骨架。每一步职责清晰工厂建硬件 → Actor 组织模块 → 策略管流程 → 配置管路线 → 注册表管统一管控。六、还有没有更高级通用的写法说实话笔者目前也是最多了解到第四层第四层已经能撑住大多数半导体设备。但如果你做的是产品化平台要支持多种机型、快速换型、可视化配置还有两个方向可以走方向一配置文件驱动装配把「工厂函数里的 switch」搬到配置文件里变成数据驱动{ Cylinders: [ { id: 1, name: 顶升气缸, caps: [ {type: Ctrl, sensor: TopLiftValve}, {type: DetectArrive, sensor: TopLiftArriveSen} ], timeout: 3000, alarmBase: 8001 }, { id: 2, name: 抱盘气缸, caps: [ {type: Ctrl, sensor: CatchValve}, {type: DetectArrive, sensor: CatchCheckSen}, {type: DetectSafe, sensor: CatchSafeSen} ], timeout: 5000, alarmBase: 8101 } ] }好处换机型只改 JSON不改代码。坏处字符串名字打错运行时才崩调试多一层。适合产品成熟后做平台化不适合初期快速开发。方向二脚本/流程图驱动更进一步把状态机本身也变成配置。用流程图编辑器画步骤生成 XML/JSON引擎解释执行。Cylinder* CreateCylinderFromConfig(const json cfg) { auto* cy new Cylinder(cfg[id], cfg[name]); for (auto cap : cfg[caps]) cy-AddCap(cap[type], cap[sensor]); cy-SetTimeout(cfg[timeout]); cy-SetAlarmBase(cfg[alarmBase]); return cy; }好处工艺工程师不用懂 C 就能改流程。坏处引擎开发成本高调试更难性能有损耗。适合做标准化产品平台不适合单机定制项目。现实建议阶段用什么原型/样机第三层Actor 状态机快速跑起来量产机型第四层策略 工厂 配置驱动可维护可换型产品平台配置文件驱动装配流程仍用代码标准化平台脚本/流程图驱动工艺工程师可编辑不要一上来就追求「最通用」那是过度设计。先跑到第三层痛了再上第四层第四层痛了再考虑平台化。七、新手从零搭建一台设备代码的思考顺序把前面所有内容浓缩成一个实操指南按这个顺序想就不会乱。第一步拆物理部件拿到机械图纸列出所有会动的、能看的、能测的部件。每个部件起个名字记下它的类型轴/气缸/传感器/吸嘴。第二步封装硬件每类硬件封装一个类Axis、Cylinder、SensorObj、Picker。业务代码只调封装接口不直接碰控制卡 API。第三步划分 Actor把部件按功能分组每组一个 Actor。比如手臂吸嘴ArmActor料道气缸ChannelActor测试台TesterActor。每个 Actor 一个线程线程里跑状态机。第四步定义状态机每个 Actor 画状态流程取料→搬运→放料→完成。用enum switch实现每步只做一件事失败Alarm break。第五步设计协作协议Actor 之间怎么配合共享工位用SetUsedBy就绪握手用事件或条件变量异常走ControllerStop。第六步抽象策略需要时如果流程会变上料/下料/换型把流程抽成策略对象Actor 只委派。路线用StationLine配置描述。第七步工厂装配需要时如果硬件种类多、配置重复用工厂函数集中创建。差异用 Capability 组件化不用子类继承。第八步UI 和数据最后才做 UI 和数据库。UI 只读 Actor 状态、发命令不写业务逻辑。数据库只存报警和历史不参与流程控制。八、每一层写法的对比总表维度第一层 硬写第二层 硬件封装第三层 Actor状态机第四层 策略工厂硬件调用直接调卡 API封装成 Axis/Cylinder同左同左模块组织全局函数线程全局函数线程Actor 基类注册表同左流程线性代码写死线性代码状态机 switch策略对象配置协作裸 bool 轮询裸 bool 轮询SetUsedBy握手同左事件通道报警AfxMessageBox返回值统一 Alarm信号同左错误链停机g_bRunningg_bRunningStopAllActors同左ControllerStop换型改代码改代码改状态机换 m_StLine加硬件裸 new 散落裸 new 散落集中初始化工厂函数适合原型验证小设备单机量产多机型平台九、可复用结论设备软件 硬件封装 Actor 组织 状态机流程 协作协议 异常处理 人机交互。这六块是任何设备都逃不掉的。新手从第一层开始不丢人能跑通比什么都重要。但要知道每一层的问题在哪痛了再升级。硬件封装是第一步业务代码不直接调控制卡换卡只改封装层。Actor 状态机是骨架统一生命周期、统一管控、流程可停可恢复。策略 工厂 配置是进阶流程可换、硬件可装配、路线可配置。平台化是远期目标配置驱动装配、流程图驱动执行但别在样机阶段就追求。思考顺序拆部件 → 封硬件 → 划 Actor → 画状态机 → 设计协作 → 抽策略 → 工厂装配 → UI 数据。一台设备的代码从第一行到整机跑起来不是一蹴而就的是逐层演进的。理解了每一层解决了什么问题、还剩什么问题你就能根据项目阶段选合适的写法不会在样机阶段搞平台化也不会在量产阶段还在裸 new。