当前位置: 首页 > news >正文

一台设备的代码从头到尾长什么样:从粗浅到通用,新手怎么一步步搭

这篇解决一个问题:拿到一台新设备的机械图纸和工艺要求,软件从第一行代码到整机跑起来,到底要写哪些东西,按什么顺序想,有哪些写法,从最粗糙到最通用怎么演进。

一、先看清一台设备软件要管什么

写代码之前先想清楚:这台机器有哪些「物理部件」,软件要管它们的什么。

不管你是 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,问题全来了:

  1. bool 语义混乱:g_bBufferReady是「料道有料」还是「料道准备好交」还是「手臂可以来取」?没人记得清。
  2. 竞态:线程 A 刚读完g_bBufferReady == true,线程 B 立刻改成false,A 还以为有料。
  3. 加模块要改老代码:加一个 Buffer 模块,手臂线程里要加if (!g_bBufferReady) ...,料道线程也要改,到处插入。
  4. 停机不干净:g_bRunning = false后,手臂可能正卡在WaitArrive里,几秒后才退出。这期间料道还在进盘。
  5. 报警靠弹窗:AfxMessageBox打在运动线程里,弹窗不点,线程永远卡住。
  6. 硬件 API 散落:g_pCard->MoveTo直接写在业务流程里,换一张卡要改几十处。
  7. 没法复用:换一台设备,这些代码一行都用不上,因为全写死了。

这一层的问题本质是:没有抽象,没有边界,所有东西搅在一起。

三、第二层写法:抽象硬件,封装接口

痛够了之后,第一件事是:把硬件 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; } // ... } }

改进了什么

  1. 换卡只改封装层:业务代码调Axis::MoveTo,不知道底下是 DMC5812 还是雷赛。换卡只改Axis内部。
  2. 超时统一处理:WaitArrive自带超时,不会永远卡住。
  3. 错误有返回值:RunRet<MotionErr>告诉调用方成功失败,不用自己猜。
  4. 可测试:Axis可以 mock,单元测试不用真卡。

还有什么问题

  1. bool 互相通知还在:g_bBufferReady竞态没解决。
  2. 线程还是硬写:手臂线程里取料放料全写死,换流程要改线程函数。
  3. 没有状态机:流程是线性代码,停在某一步恢复不了。
  4. 没有统一管控:停机还是靠g_bRunning,没法统一停所有模块。
  5. 报警还是散落:每个线程自己Alarm,没有统一出口。

这一层解决了「硬件封装」,但没解决「模块组织和协作」。

四、第三层写法:Actor 基类 + 状态机 + 注册表

接着把「模块」抽象出来。每个模块(手臂、料道、Buffer、测试台)都是一个 Actor,有统一的生命周期接口,有注册表统一管理。

思路

  1. 抽一个Actor基类:所有模块继承它,统一Run/Stop/Home/EndLot/WorkFlow
  2. 每个 Actor 一个线程,线程里跑状态机(switch + Step)。
  3. 用注册表统一管理所有 Actor,一键启停。
  4. 资源占用用SetUsedBy/SetNotUsedBy代替裸 bool。
  5. 报警走统一出口,严重故障走全局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();

改进了什么

  1. 统一管控:RunAll / StopAll / HomeAll一键操作所有模块。
  2. 状态机替代线性代码:停在某步能恢复,不从头跑。
  3. 资源占用有协议:SetUsedBy代替裸 bool,记录谁在用。
  4. 报警统一出口:所有模块走Actor::Alarm,不各自弹窗。
  5. 加模块不改老代码:新 Actor 注册进g_Actors,老 Actor 不动。

还有什么问题

  1. 流程写死在 Actor 里:手臂的取放逻辑写死在WorkFlow,换流程(上料变下料)要改WorkFlow
  2. 硬件配置散落:哪个气缸挂哪个传感器,写在SetHardWare里,几十行重复。
  3. 模块间协作还是手动:SetUsedBy解决了互斥,但「等对方就绪」还是轮询。
  4. 换型困难:换产品要改坐标、改流程、改 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 + 状态机),快速跑起来

量产机型

第四层(策略 + 工厂 + 配置驱动),可维护可换型

产品平台

配置文件驱动装配,流程仍用代码

标准化平台

脚本/流程图驱动,工艺工程师可编辑

不要一上来就追求「最通用」,那是过度设计。先跑到第三层,痛了再上第四层,第四层痛了再考虑平台化。

七、新手从零搭建一台设备代码的思考顺序

把前面所有内容浓缩成一个实操指南,按这个顺序想就不会乱。

第一步:拆物理部件

拿到机械图纸,列出所有会动的、能看的、能测的部件。每个部件起个名字,记下它的类型(轴/气缸/传感器/吸嘴)。

第二步:封装硬件

每类硬件封装一个类:AxisCylinderSensorObjPicker。业务代码只调封装接口,不直接碰控制卡 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 散落

集中初始化

工厂函数

适合

原型验证

小设备

单机量产

多机型平台


九、可复用结论

  1. 设备软件 = 硬件封装 + Actor 组织 + 状态机流程 + 协作协议 + 异常处理 + 人机交互。这六块是任何设备都逃不掉的。
  2. 新手从第一层开始不丢人,能跑通比什么都重要。但要知道每一层的问题在哪,痛了再升级。
  3. 硬件封装是第一步:业务代码不直接调控制卡,换卡只改封装层。
  4. Actor + 状态机是骨架:统一生命周期、统一管控、流程可停可恢复。
  5. 策略 + 工厂 + 配置是进阶:流程可换、硬件可装配、路线可配置。
  6. 平台化是远期目标:配置驱动装配、流程图驱动执行,但别在样机阶段就追求。
  7. 思考顺序:拆部件 → 封硬件 → 划 Actor → 画状态机 → 设计协作 → 抽策略 → 工厂装配 → UI 数据。

一台设备的代码从第一行到整机跑起来,不是一蹴而就的,是逐层演进的。理解了每一层解决了什么问题、还剩什么问题,你就能根据项目阶段选合适的写法,不会在样机阶段搞平台化,也不会在量产阶段还在裸 new。

http://www.cnnetsun.cn/news/4355040.html

相关文章:

  • 腾讯音乐2023春招移动客户端笔试复盘与解题思路
  • kkce.com:为什么网站测速要算TCP RTT而非只看TTFB?-快快测
  • Do Large Language Model Agents Exhibit a Survival Instinct? An Empirical Study in a Sugarscape-St...
  • 蔚来数据分析岗笔试复盘:SQL、Python与业务思维全解析
  • Level 4自动驾驶系统设计50——中间件 0
  • SpringBoot与若依框架实战:快速构建图书管理系统全流程指南
  • 学Simulink——UPS系统中双向DC-AC逆变器的并联均流控制仿真
  • MA模型入门:从误差项预测到Python量化实践
  • 燃气灶选购全攻略:看懂定时、防干烧与双气源关键点
  • SageMaker 推理在 Lambda 里卡了 4 秒:删掉两个配置后成本降了 80%
  • 如何5分钟跑通OpenVoice:开源语音克隆完整上手指南
  • 305M 打赢 1014M:pytorch-image-models 官方实测数据里的选型判断
  • 加载项与桌面端的协同诊断 一份联调日志
  • JS 导出 Excel 文件、SheetJS(xlsx)前端导出 Excel
  • 房地产销售中的守盘
  • S/4 HANA ABAP实战:从CDS视图到RAP框架的转型指南
  • AI Agent协同工作流:从通信协议到工程实践
  • 【单片机毕业设计推荐】基于 STM32 的人体健康运动监测终端设计与实现 基于 STM32 的老人跌倒报警与生理参数采集系统设计(023707)
  • 【单片机毕业设计推荐】基于 STM32 或 51 单片机的环境火情监测与短信报警系统设计 基于 STM32 或 51 单片机的室内烟雾温湿度智能监控装置设计(023807)
  • 基于SpringBoot的员工信息管理系统(毕设源码+文档)
  • 2026大专论文降重打分:81%降到5%的实测
  • 加载项与结构化数据 销售台账的实时问答
  • OpenVoice 语音克隆本地部署:10分钟克隆出你的专属声音
  • Folo 入门实战:一站式 AI RSS 阅读器,5 分钟搭好你的统一信息流
  • Folo:把多来源信息汇成一条时间线的开源 AI RSS 阅读器
  • 海尔488升十字对开门冰箱:超薄嵌入与AI变频深度解析
  • AI编程提示词精简80%效果更佳:Claude Code高效协作实践
  • RevokeMsgPatcher 微信防撤回补丁:四步装好 PC 端微信/QQ/TIM 防撤回与多开
  • Deep Q-Learning实战:交叉路口自适应信号控制与训练优化
  • MKVToolNix v80.0 完全指南:无损封装、批量处理与自动化实践