一台设备的代码从头到尾长什么样:从粗浅到通用,新手怎么一步步搭
这篇解决一个问题:拿到一台新设备的机械图纸和工艺要求,软件从第一行代码到整机跑起来,到底要写哪些东西,按什么顺序想,有哪些写法,从最粗糙到最通用怎么演进。
一、先看清一台设备软件要管什么
写代码之前先想清楚:这台机器有哪些「物理部件」,软件要管它们的什么。
不管你是 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 立刻改成false,A 还以为有料。 - 加模块要改老代码:加一个 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; } RunRet<MotionErr> MoveTo(double pos) { m_card->MoveTo(m_axisId, pos); return RunRet<MotionErr>(); } RunRet<MotionErr> WaitArrive(double pos, int timeoutMs = 10000) { int t = 0; while (!m_card->IsArrived(m_axisId)) { Sleep(10); t += 10; if (t > timeoutMs) return RunRet<MotionErr>(MotionErr::Timeout); } return RunRet<MotionErr>(); } RunRet<MotionErr> 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: RunRet<CylinderErr> MoveToWork() { m_valve->WriteBit(true); int t = 0; while (!m_arriveSen->ReadBit()) { Sleep(10); t += 10; if (t > m_timeoutMs) return RunRet<CylinderErr>(CylinderErr::Timeout); } return RunRet<CylinderErr>(); } RunRet<CylinderErr> 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自带超时,不会永远卡住。 - 错误有返回值:
RunRet<MotionErr>告诉调用方成功失败,不用自己猜。 - 可测试:
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 vector<Actor*> 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_ptr<Result> ret) { // 统一报警出口:日志 + 信号 + 蜂鸣 Log(ret->GetSerialErrorString()); m_SignalAlarmOut.emit(time, code, desc); return 0; } };手臂 Actor
enum class ArmStep { WaitRun, CalPick, Pick, MoveToPlace, Place, Done }; class Arm : public Actor { Axis* m_zAxis; Axis* m_xAxis; vector<Picker*> m_pickers; Step<ArmStep> m_step; vector<StationLine> 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; // 策略对象 vector<StationLine> 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_bRunning | g_bRunning | StopAllActors | 同左+ControllerStop |
换型 | 改代码 | 改代码 | 改状态机 | 换 m_StLine |
加硬件 | 裸 new 散落 | 裸 new 散落 | 集中初始化 | 工厂函数 |
适合 | 原型验证 | 小设备 | 单机量产 | 多机型平台 |
九、可复用结论
- 设备软件 = 硬件封装 + Actor 组织 + 状态机流程 + 协作协议 + 异常处理 + 人机交互。这六块是任何设备都逃不掉的。
- 新手从第一层开始不丢人,能跑通比什么都重要。但要知道每一层的问题在哪,痛了再升级。
- 硬件封装是第一步:业务代码不直接调控制卡,换卡只改封装层。
- Actor + 状态机是骨架:统一生命周期、统一管控、流程可停可恢复。
- 策略 + 工厂 + 配置是进阶:流程可换、硬件可装配、路线可配置。
- 平台化是远期目标:配置驱动装配、流程图驱动执行,但别在样机阶段就追求。
- 思考顺序:拆部件 → 封硬件 → 划 Actor → 画状态机 → 设计协作 → 抽策略 → 工厂装配 → UI 数据。
一台设备的代码从第一行到整机跑起来,不是一蹴而就的,是逐层演进的。理解了每一层解决了什么问题、还剩什么问题,你就能根据项目阶段选合适的写法,不会在样机阶段搞平台化,也不会在量产阶段还在裸 new。
