news 2026/9/1 15:19:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一台设备的代码从头到尾长什么样:从粗浅到通用,新手怎么一步步搭

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

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

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

不管你是 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。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 15:10:08

腾讯音乐2023春招移动客户端笔试复盘与解题思路

3月下旬&#xff0c;我参加了腾讯音乐2023年春招移动客户端岗的第二批笔试。腾讯音乐这个招牌不用多介绍&#xff0c;旗下的QQ音乐、酷狗、酷我基本覆盖了国内主流的在线音乐用户&#xff0c;移动客户端岗主要就是做这几款App的迭代和体验优化。第二批笔试比起第一批&#xff0…

作者头像 李华
网站建设 2026/9/1 15:10:01

kkce.com:为什么网站测速要算TCP RTT而非只看TTFB?-快快测

把 网站测速​ 收敛成“TTFB 350ms、首字节快就健康”&#xff0c;是混淆了应用层指标与网络层基线的典型降维。TTFB&#xff08;Time to First Byte&#xff09;在 HTTP/1.1TLS1.2 下近似等于 1RTT&#xff08;TCP 握手&#xff09; 2RTT&#xff08;TLS&#xff09; 1RTT&…

作者头像 李华
网站建设 2026/9/1 15:08:12

Do Large Language Model Agents Exhibit a Survival Instinct? An Empirical Study in a Sugarscape-St...

《大型语言模型智能体是否表现出生存本能?——基于糖域风格模拟的实证研究》总结与翻译 一、文章主要内容 本文围绕大型语言模型(LLM)智能体是否存在自发生存本能展开研究,通过构建糖域(Sugarscape)风格的模拟环境,探究不同LLM智能体在无明确生存编程情况下的行为表现…

作者头像 李华
网站建设 2026/9/1 15:05:37

蔚来数据分析岗笔试复盘:SQL、Python与业务思维全解析

先说我自己的背景&#xff0c;2024届硕士&#xff0c;投的是蔚来数据分析岗&#xff0c;和大多数人一样&#xff0c;从官网投递到收到笔试链接大概隔了一周多。当时一起投的还有几家新势力车企&#xff0c;但蔚来这套笔试做下来&#xff0c;感觉在题型设计和业务贴合度上确实是…

作者头像 李华
网站建设 2026/9/1 15:05:05

Level 4自动驾驶系统设计50——中间件 0

第 10 章:车规级中间件选型与配置实战 10.1 静态 SOME/IP 控制指令与动态 DDS 海量点云/图像数据的骨干网通信选型 10.1.1 大模型并网下的车载骨干网数据挤兑与中间件断层 在第四篇中,我们完成了 Level 4 级“主/从双片冗余 SoC + 片外安全 MCU”跨芯片张量并行与跨域技术…

作者头像 李华
网站建设 2026/9/1 15:04:31

SpringBoot与若依框架实战:快速构建图书管理系统全流程指南

最近在帮朋友做一个图书管理的小项目&#xff0c;原本想从零开始搭建&#xff0c;但考虑到时间成本和功能完整性&#xff0c;最终选择了基于 若依&#xff08;RuoYi&#xff09; 这个优秀的开源后台管理系统进行二次开发。结合 SpringBoot 的快速开发能力&#xff0c;整个项…

作者头像 李华