跳到主要内容

AOI 光学检测工站

1. 设备简介

AOI (Automated Optical Inspection, 自动光学检测) 是 3C 电子、PCB、精密五金、 新能源电池极片等制造场景的核心质量工位。以相机 + 光源 + 算法组合, 对产品外观缺陷 (划伤、脏污、缺料、偏移、字符错漏) 进行逐件 100% 检测。

本案例为某珠三角 PCBA 工厂贴片后 AOI 工位, 由 Darra 平台统一承接 AOI 的 运动控制 / 视觉触发 / 节拍调度, 视觉算法由厂内工程师自行用 Python + OpenCV / Halcon 迭代。Darra 不内置任何视觉算法, 而是提供 Python 3.11 协程宿主, 让算法团队自由 import cv2 / import halcon / import torch, 并在保存 .py热更新, 无需重启 PLC。

主要工位流程:

  1. 上游贴片机来板 → 输送带到位
  2. X/Y/θ 三轴伺服对位夹紧
  3. 光源亮 + 上下相机硬触发 (EtherCAT DC 0.5ms 同步)
  4. Python 协程异步推理 (OpenCV 快检 + ONNX 深度模型二检)
  5. OK/NG 分拣 → MES 追溯 (BoardID + 缺陷坐标 + 图像留档)

关键能力: 单板节拍 ≤ 3.5 s, 硬触发延迟 ≤ 0.5 ms, 检出率 ≥ 99.5%, 误报率 ≤ 0.3%, 算法团队可脱离机电工程师独立发版。


2. 项目模块组成

本工站是 机械 + 电气 + 软件 三层协同项目, 全部由 Darra IDE 统一管理。

层次模块职责
机械XY/Z/θ 对位平台将来板精准定位到相机视场中心
机械相机支架 + 光源上下双面打光 + 图像采集
机械NG 分拣机构气缸推出 NG 板至废品道
电气EtherCAT 主干1ms 周期, 驱动伺服 + IO + 光源
电气GigE/USB3 图像总线独立与控制总线, 避免视觉流量占用 EtherCAT
电气Cat 3 安全回路光幕 + 急停 + 双触点继电器
软件PLC 主程序状态机调度 (7 状态), SCL 语言
软件运动工艺对象MC_MoveAbs + 三轴联动, 内置
软件Python 脚本宿主异步协程, 保存即热更新
软件HMI 组态8 页 (主界面 / 缺陷热图 / 报警 / 配方 / MES / 统计 / 维护 / 设置)
软件MES 网关每板上传 50+ 字段

3. 机械装配架构

装配要点:

  • XY 滚珠丝杠精度 ±3 μm, θ 轴采用直驱电机 (免减速器回差)
  • Z 轴采用 400W 带抱闸伺服, 保证断电不坠落
  • 顶部相机固定在 Z 轴下方, 随 Z 下降到工作高度
  • 底部相机反向安装, 与背光板形成 "透射模式"
  • 安全光幕 20 光轴 14mm 栅距, 双侧防护

4. 程序架构

4.1 OB/FB 调用树

4.2 状态机流转

任务调度:

  • OB1 (1ms): 主逻辑、状态机、运动指令发送
  • OB35 (250us): 安全监控 + 高速光电 IO 刷新
  • OB100: 上电初始化 + 伺服使能 + 回零

5. 硬件 BOM

单位: 人民币 (含税)。2026 年 3 月珠三角参考价, 批量可再议 10~15%。

序号分类品牌 / 型号规格数量单价 (¥)小计 (¥)
1工控机研华 IPC-610MBi7-12700 / 32G / 1T SSD + RTX A2000 6G118,50018,500
2触摸屏昆仑通态 MCGS 21.5"1920×1080 电容多点14,2004,200
3伺服驱动汇川 SV660NEtherCAT 400W41,6806,720
4伺服电机汇川 ISMH 系列400W 2500ppr 带抱闸41,3205,280
5远程 IO汇川 AM600-CPU1608TP + 扩展32DI + 32DO + 8AI + 4AO13,8503,850
6工业相机 (上)海康 MV-CS050-10GC500 万彩色 GigE14,8004,800
7工业相机 (下)海康 MV-CU050-90UM500 万黑白 USB3.013,6003,600
8工业镜头海康 MVL-KF2528M-12MP25mm F2.8 定焦 500万级21,2002,400
9环形光源奥普特 OPT-RI15050-W环形白光 150mm12,8002,800
10背光光源奥普特 OPT-DBL100-B背光板蓝 100×10012,4002,400
11光源控制器奥普特 OPT-APD4024F-4-V4 通道数字可调, EtherCAT11,9001,900
12对位平台自制滚珠丝杠组件XY 100×100 / Z 30 / θ ±5°18,5008,500
13安全光幕嘉准 JH-LG-120020 光轴 14mm21,4502,900
14光电传感器邦纳 BGS 漫反射PNP / 15 mm83202,560
15NG 分拣气缸SMC MXH20-60Z双行程 60mm2480960
16电磁阀岛亚德客 4V210-08 × 8EtherCAT 从站11,9801,980
17工业交换机华三 S1224F-HPWR24 口工业网管11,8001,800
18UPS山特 C3K3 kVA 长效13,2003,200
19线缆 / 配件汇川 Cat5e / 动力线 / 气管-14,6004,600
20机柜 + 机架威图 TS8 + 铝型材800×600×2000 + 工作台112,00012,000
电气小计¥94,950
机械结构件¥53,050
合计¥148,000

6. IO 模块配置 (IDE 操作流程)

6.1 选择 IO 模块

在 Darra IDE 里依次操作:

  1. 工程 → IO 设备向导 → 选择 "EtherCAT 主站"
  2. 扫描从站 (Alt+F5), 自动发现:
    • 汇川 AM600-CPU1608TP (节点 1)
    • 汇川 SV660N ×4 (节点 2~5, X/Y/Z/θ)
    • 奥普特 OPT-APD4024F-4-V (节点 6)
    • 亚德客阀岛 8 路 (节点 7)
  3. 逐个从站 → 设置别名 (Alias) 便于后期维护

6.2 IO 地址分配表

模块通道地址方向类型别名
AM600 DICh0I0.0DIBOOLSEN_BoardIn
AM600 DICh1I0.1DIBOOLSEN_BoardReady
AM600 DICh2I0.2DIBOOLSEN_CamTop_Ready
AM600 DICh3I0.3DIBOOLSEN_CamBot_Ready
AM600 DICh4I0.4DIBOOLSEN_NGBin_Full
AM600 DICh5I0.5DIBOOLSEN_EStop
AM600 DICh6I0.6DIBOOLSEN_LightCurtain_OK
AM600 DOCh0Q0.0DOBOOLACT_Clamp
AM600 DOCh1Q0.1DOBOOLACT_CamTrig_Top
AM600 DOCh2Q0.2DOBOOLACT_CamTrig_Bot
AM600 DOCh3Q0.3DOBOOLACT_NG_Cyl
AM600 DOCh4Q0.4DOBOOLACT_Tower_Green
AM600 DOCh5Q0.5DOBOOLACT_Tower_Red
AM600 AOCh0AQ0AOINTLED_Top_Intensity
AM600 AOCh1AQ1AOINTLED_Bot_Intensity
APD4024Ch0~3-通过 CoE 写LED_Channel1~4
SV660N_XPDO-运动轴AXIS_REFAxisX
SV660N_YPDO-运动轴AXIS_REFAxisY
SV660N_ZPDO-运动轴AXIS_REFAxisZ
SV660N_θPDO-运动轴AXIS_REFAxisTheta

6.3 IO 用途分工

组别用途参与设备
物料感知来板到位、夹紧状态、分拣状态SEN_BoardIn / BoardReady / NGBin_Full
视觉握手相机硬触发 + 就绪反馈ACT_CamTrig_Top/Bot, SEN_CamTop/Bot_Ready
光源调光顶部 / 底部亮度LED_Top/Bot_Intensity (AO) + APD4024 (CoE)
执行机构夹紧、分拣气缸、报警灯ACT_Clamp, ACT_NG_Cyl, ACT_Tower_*
安全回路光幕 + 急停硬接 Cat 3 继电器SEN_EStop, SEN_LightCurtain_OK
运动控制X/Y/Z/θ 四轴AxisX/Y/Z/θ via EtherCAT PDO

6.4 数据绑定到机器人

本工站不使用独立机器人, 而是由 Darra 内置 3D 插补工艺对象驱动 XY/Z/θ 四轴构成虚拟 4 自由度机器人, 对位操作直接在 PLC 工程里调用:

(* 将 IO 采集的板位置传递给 "虚拟机器人" *)
Robot.TargetPose.X := Recipe.PickX;
Robot.TargetPose.Y := Recipe.PickY;
Robot.TargetPose.Z := Recipe.PickZ;
Robot.TargetPose.C := 0.0; (* θ 归零 *)
Robot.MoveL(Vel := 400, Acc := 2000);

绑定规则 (在 IDE 的 "机器人配置" 对话框里设置):

机器人通道Darra 轴物理对应
J1 (X)SV660N 节点 2对位 X 丝杠
J2 (Y)SV660N 节点 3对位 Y 丝杠
J3 (Z)SV660N 节点 4Z 升降
J4 (C)SV660N 节点 5θ 旋转台

运动指令由 FB_Motion 统一封装, 失败时自动回退到原点。


7. 核心数据结构 UDT + DB

// 板数据 UDT
TYPE UDT_AOI_Board :
STRUCT
BoardID : STRING[32]; // 板序号 (MES 下发)
Recipe : STRING[16]; // 产品型号
ArriveTime : DT; // 到站时间戳
PosX_mm : LREAL; // 实际 X 位置
PosY_mm : LREAL; // 实际 Y 位置
Theta_deg : LREAL; // 角度偏差
ImageTopPath : STRING[128]; // 顶部图像路径
ImageBotPath : STRING[128]; // 底部图像路径
InspectState : USINT; // 0=未检 1=检测中 2=OK 3=NG
DefectCount : UDINT; // 缺陷总数
CycleTime_ms : UDINT; // 节拍耗时
END_STRUCT
END_TYPE

// 缺陷结果 UDT (单个)
TYPE UDT_DefectResult :
STRUCT
DefectType : USINT; // 1=短路 2=断路 3=缺件 4=偏移 5=极性 6=字符
LocationX_px : UINT; // 缺陷像素 X
LocationY_px : UINT; // 缺陷像素 Y
WorldX_mm : LREAL; // 世界坐标 X (mm)
WorldY_mm : LREAL; // 世界坐标 Y (mm)
Confidence : REAL; // AI 置信度 0.0~1.0
Severity : USINT; // 0=轻微 1=一般 2=严重
ImageCrop : STRING[128]; // 局部图路径 (切片)
END_STRUCT
END_TYPE

// 配方 UDT
TYPE UDT_Recipe :
STRUCT
RecipeName : STRING[32];
PickX, PickY, PickZ : LREAL; // 对位目标
LEDTop_Pct : USINT; // 顶部光源亮度
LEDBot_Pct : USINT; // 底部光源亮度
ExposureMs : UINT; // 曝光时间
ModelPath : STRING[128]; // AI 模型路径
UseAI : BOOL; // 是否启用深度模型
END_STRUCT
END_TYPE

// 全局 DB
VAR_GLOBAL
CurBoard : UDT_AOI_Board;
Defects : ARRAY[1..64] OF UDT_DefectResult;
CurRecipe : UDT_Recipe;
TotalPass : UDINT;
TotalFail : UDINT;
END_VAR

8. 核心程序代码

8.1 主状态机 (7 状态)

PROGRAM PRG_AOI_Main
VAR
State : INT := 0;
TON_Timeout : TON;
Py_Result : INT; // 0=Pending 1=OK 2=NG
CycleStart, CycleEnd : DT;
END_VAR

CASE State OF
0: (* Idle: 等板到位 *)
IF SEN_BoardIn THEN
CycleStart := DT#NOW;
CurBoard.BoardID := MES.ReadBoardID();
CurBoard.Recipe := MES.ReadRecipe(CurBoard.BoardID);
CurBoard.ArriveTime := CycleStart;
ACT_Clamp := TRUE;
State := 1;
END_IF

1: (* Loading: 夹紧 + 对位轴联动 *)
Robot.TargetPose.X := CurRecipe.PickX;
Robot.TargetPose.Y := CurRecipe.PickY;
Robot.TargetPose.Z := CurRecipe.PickZ;
Robot.TargetPose.C := 0.0;
Robot.MoveL(Vel := 400, Acc := 2000);
IF Robot.Done THEN
State := 2;
END_IF

2: (* Positioning: 光源亮, 硬触发相机 *)
LED_Top_Intensity := CurRecipe.LEDTop_Pct;
LED_Bot_Intensity := CurRecipe.LEDBot_Pct;
ACT_CamTrig_Top := TRUE;
ACT_CamTrig_Bot := TRUE;
TON_Timeout(IN := TRUE, PT := T#100ms);
IF TON_Timeout.Q THEN
ACT_CamTrig_Top := FALSE;
ACT_CamTrig_Bot := FALSE;
TON_Timeout(IN := FALSE);
State := 3;
END_IF

3: (* Imaging: 等相机回传 → 调 Python 异步脚本 *)
IF SEN_CamTop_Ready AND SEN_CamBot_Ready THEN
Py_Result := 0;
PyScript.InvokeAsync(
Script := 'aoi_inspect.py',
Method := 'run',
Arg1 := ADR(CurBoard),
Arg2 := ADR(Py_Result)
);
State := 4;
END_IF

4: (* Inspecting: 等 Python 回调 *)
IF Py_Result <> 0 THEN
IF Py_Result = 1 THEN
CurBoard.InspectState := 2; (* OK *)
TotalPass := TotalPass + 1;
ELSE
CurBoard.InspectState := 3; (* NG *)
TotalFail := TotalFail + 1;
END_IF
State := 5;
ELSIF TON_Timeout.ET > T#3s THEN
CurBoard.InspectState := 3;
Alarm(Code := 1001, Msg := 'Python 视觉超时');
State := 5;
END_IF

5: (* Sorting: NG 分拣 *)
IF CurBoard.InspectState = 3 THEN
ACT_NG_Cyl := TRUE;
TON_Timeout(IN := TRUE, PT := T#300ms);
IF TON_Timeout.Q THEN
ACT_NG_Cyl := FALSE;
TON_Timeout(IN := FALSE);
State := 6;
END_IF
ELSE
State := 6;
END_IF

6: (* Done: 写 MES + 归零 *)
CycleEnd := DT#NOW;
CurBoard.CycleTime_ms := UDINT(CycleEnd - CycleStart);
MES.UploadBoard(CurBoard);
ACT_Clamp := FALSE;
State := 0;
END_CASE
END_PROGRAM

8.2 Python 异步脚本桥接 (FB)

FUNCTION_BLOCK FB_PyAsync
VAR_INPUT
Script : STRING;
Method : STRING;
Arg1, Arg2 : POINTER TO BYTE;
END_VAR
VAR_OUTPUT
Done : BOOL;
Error : BOOL;
END_VAR
(* Darra 平台内置 PyBridge 实例调度, 脚本在独立协程池运行,
不阻塞 PLC 1ms 主周期。结果通过共享内存指针回传。
脚本文件变化 30ms 内热加载, 不重启 PLC。 *)
END_FUNCTION_BLOCK

8.3 Python 视觉脚本 (OpenCV + ONNX)

# scripts/aoi_inspect.py
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import onnxruntime as ort
from darra import plc, log, camera

# 懒加载模型 (只加载一次)
_session = None
def _get_session():
global _session
if _session is None:
_session = ort.InferenceSession(
"scripts/aoi_model.onnx",
providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
return _session

async def run(board_ptr, result_ptr):
"""主入口, 由 SCL 调用。异步协程, 不阻塞 PLC。"""
board = plc.read_udt(board_ptr, "UDT_AOI_Board")

# 1. 抓取相机最新帧 (零拷贝)
img_top = camera.grab("CamTop", timeout_ms=50)
img_bot = camera.grab("CamBot", timeout_ms=50)
if img_top is None or img_bot is None:
log.warn(f"[AOI] 相机取图失败 Board={board.BoardID}")
plc.write_int(result_ptr, 2) # NG
return

# 2. OpenCV 预检 (模板匹配 + 边缘检测)
defects = _rule_based_check(img_top, img_bot, board.Recipe)

# 3. ONNX 深度模型推理 (可疑区域二次确认)
if defects or _need_ai(board.Recipe):
ai_defects = _ai_inference(img_top, img_bot)
defects = _merge(defects, ai_defects)

# 4. 写结果
board.DefectCount = len(defects)
board.ImageTopPath = _save(img_top, board.BoardID, "top")
board.ImageBotPath = _save(img_bot, board.BoardID, "bot")
plc.write_udt(board_ptr, board)

plc.write_int(result_ptr, 1 if len(defects) == 0 else 2)
log.info(f"[AOI] Board={board.BoardID} Defects={len(defects)}")

def _rule_based_check(top, bot, recipe):
"""基于 OpenCV 的模板匹配 + 边缘检测。"""
tmpl = cv2.imread(f"templates/{recipe}_top.png", cv2.IMREAD_GRAYSCALE)
gray = cv2.cvtColor(top, cv2.COLOR_BGR2GRAY)
res = cv2.matchTemplate(gray, tmpl, cv2.TM_CCOEFF_NORMED)
# ... 逐元件比对 ...
return []

def _ai_inference(top, bot):
"""ONNX 模型推理缺陷定位。"""
sess = _get_session()
blob = cv2.dnn.blobFromImage(top, 1/255.0, (640, 640), swapRB=True)
outputs = sess.run(None, {"images": blob})
# ... 解析 YOLO 输出 ...
return []

def _save(img, bid, side):
path = f"D:/AOI/images/{bid}_{side}.jpg"
cv2.imwrite(path, img, [cv2.IMWRITE_JPEG_QUALITY, 85])
return path

def _need_ai(recipe): return recipe.startswith("HIGHREL_")
def _merge(a, b): return list(set(a + b))

热更新机制: 保存 aoi_inspect.py 时, Darra 脚本宿主监听文件变化, 30ms 内重新加载模块, 正在执行的协程跑完才切换, 业务无感升级。

8.4 机器人 3D 插补调用

(* 对位轴构成 "虚拟 4 自由度机器人" *)
Robot.TargetPose.X := Cur.PickX;
Robot.TargetPose.Y := Cur.PickY;
Robot.TargetPose.Z := Cur.PickZ;
Robot.TargetPose.C := 0.0;
Robot.MoveL(Vel := 400, Acc := 2000, Jerk := 20000);

(* 软限位保护 (防止撞镜头) *)
MC_SoftLimit(Axis := AxisZ, Min := -5.0, Max := 28.0);

9. 运行与调试

9.1 上电启动流程

  1. 上电后 PLC 运行 OB100: 伺服使能 + 回零 (X/Y/Z 逐一到机械原点)
  2. HMI 主界面显示 "待机" + Robot 实时坐标
  3. 操作员选择配方 (产品型号), PLC 加载 CurRecipe
  4. 按 "启动", 进入 Idle 状态, 等来板触发

9.2 调试要点

环节调试方法
相机硬触发延迟用示波器同时观察 ACT_CamTrig_Top 与相机曝光信号, 应 < 0.5ms
对位精度标定板反复定位 100 次, 测 Cpk, 要求 > 1.33
Python 脚本热更新保存脚本, 观察 Darra IDE 日志 "Script Reloaded" 出现
视觉算法用 IDE 的 "Python 交互" 窗口逐行执行, OpenCV 显示窗口调试
MES 丢包日志中 MES.UploadBoard 失败 → 自动持久化队列到 SQLite, 恢复后补传
节拍瓶颈HMI 显示各状态耗时直方图, 找最长状态优化

9.3 日志位置

日志路径
PLC 运行日志Darra IDE → 日志窗口, 按模块 AOI_Main 过滤
Python 脚本 stdoutD:/DarraPLC/logs/py_*.log
MES 上传队列D:/DarraPLC/queue/mes_board.db
图像留档D:/AOI/images/<BoardID>_top.jpg / _bot.jpg

10. Darra 功能对照表

需求Darra 对应功能说明
四轴对位工艺对象 MC_MoveAbs + 3D 插补内置, 不需外购运动卡
第三方视觉库Python 3.11 脚本宿主import cv2 / import halcon / import torch 自由
深度学习推理onnxruntime + GPU 透传支持 CUDA / CPU 双后端
算法热更新脚本宿主 FileWatch保存即生效, 不重启 PLC
硬触发相机EtherCAT DC + DO 同步输出延迟 < 0.5ms
MES 对接内置 OPC UA + REST + SQLite 离线队列无需第三方网关
HMI 组态Darra HMI Designer静态按钮绑定 SCL 变量
配方管理配方引擎 + JSON 持久化支持版本控制
安全回路硬接外部 Cat 3 安全继电器Darra 不做安全 PLC
远程运维DarraSync 加密隧道厂内算法团队可远程调试
数据追溯UDT + DT 时间戳 + SQLite每板 50+ 字段留档 90 天

11. 项目总结

11.1 交付成果

  • 节拍: 3.1 s/pcs (目标 3.5, 超标 11%)
  • 检出率: 99.7% (1000 片盲测)
  • 误报率: 0.21% (目标 < 0.3%)
  • 硬触发延迟: 0.42ms 实测
  • 算法热更新: 保存到生效 28ms 平均, 业务不中断

11.2 关键技术点

  1. Darra 不内置视觉算法, 而是彻底开放 Python 协程宿主, 算法团队自由 选用 OpenCV / HALCON / PyTorch / PaddlePaddle / 自研 .so, 不锁厂
  2. 协程异步: 视觉推理 (耗时 50~120ms) 在独立协程池运行, PLC 1ms 主周期不受影响
  3. 硬触发同步: EtherCAT DC 分布时钟 + DO 输出到相机, 延迟 < 0.5ms
  4. 对位 = 虚拟机器人: XY/Z/θ 四轴由 Darra 3D 插补工艺对象驱动, 省运动卡
  5. 算法团队与机电团队解耦: 机电工程师负责 PLC + HMI, 算法工程师只改 .py

11.3 后续扩展方向

  • 锂电池极片 AOI: 同平台复制, 只换视觉模型和光源
  • 跨产品缺陷库: 多站数据汇总到 DarraCloud, 联合训练基础模型
  • 低代码视觉流程: 规划中的 Darra Vision Flow, 拖拽组态替代部分 .py 编写

Darra 平台以**"控制器 + Python 脚本宿主"**双引擎架构, 让 AOI 工位的 运动控制稳如 PLC视觉算法灵活如上位机, 两者在同一 IDE 下协同, 既解决了传统专机"算法闭源"的痛点, 又避免了"PLC + 工控机 + 视觉软件"三套系统的碎片化。