ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Halcon视觉检测结果如何通过C#与S7协议驱动西门子PLC

Halcon视觉检测结果如何通过C#与S7协议驱动西门子PLC 做机器视觉这行的朋友迟早会遇到一个需求视觉系统算出了结果怎么让设备动起来几年前我在一条组装线上调视觉定位Halcon跑完模板匹配结果只停在工控机屏幕上还得靠操作员拿鼠标看坐标再手动按按钮把工件挪到下一个工位。累倒不说了节拍根本提不上去。后来被逼着把Halcon和C#上位机、西门子PLC全部串起来——用S7协议走以太网直接让PLC接收定位结果控制气缸和模组动作才算真正把视觉变成了产线的一部分。这篇内容就是我从零搭这套系统的完整记录从开发环境准备、S7通讯库选型、C#侧PLC读写类封装到Halcon检测结果怎么换算成PLC认得的数据再到一个能用的触发-检测-反馈闭环状态机。适合正在做机器视觉上位机集成、又对西门子PLC通讯不熟的朋友参考哪怕你之前完全没碰过S7协议跟着做也能跑通。1. 视觉检测结果如何喂给PLC先想清楚整体架构1.1 为什么必须让视觉直接驱动PLC很多刚入行的同学会问视觉检测完把结果显示在HMI上让操作员看一眼不是很简单吗确实简单但产线上追求的是稳定节拍和无人干预。缺陷件需要自动剔除机械手需要视觉坐标去抓取螺纹测量结果需要自动判断是否合格并记录。这些动作如果靠人传递效率低下且容易出错。所以视觉系统必须作为一个智能传感器实时把判断结果发给PLC由PLC去执行最终动作。拿我负责过的一个拧紧工位举例工件到位PLC给出拍照信号C#上位机采集图像Halcon做螺丝孔定位计算出偏差角度和坐标然后通过S7协议把结果发给PLCPLC再控制拧紧轴偏移相应角度。整个流程从拍照到数据返回不超过150毫秒完全不影响生产节拍。没有这个联动环节视觉系统就是个半成品。1.2 这套系统的数据流长什么样典型的Halcon C# 西门子PLC联动系统数据流大概是这样的工业相机采集图像 ↓ Halcon算法在C#中调用执行匹配/测量/缺陷检测 ↓ C#上位机解析结果整理成bool/int/float等基本数据 ↓ S7协议以太网 ↓ 西门子PLC根据视觉结果执行逻辑控制如剔除、报警、纠偏 ↓ 执行机构气缸、伺服、指示灯等反向链路同样存在PLC通过输入信号比如红外传感器检测到工件到位触发上位机拍照上位机收到位信号后立即采集和处理。所以通讯不是单向的而是双向握手。1.3 为什么选S7协议而不是Modbus TCP或OPC UA西门子PLC常用的以太网通讯方式有S7协议、Modbus TCP、OPC UA以及Profinet等。很多初学者会纠结选哪个。我的个人经验如下S7协议是西门子PLC原生支持的最直接协议不需要PLC侧额外组态通信功能块只需要设置好允许PUT/GET通信即可。延迟很低资料多适合C#上位机直接读写M区、DB块。Modbus TCP在西门子PLC上需要组态Modbus TCP通信指令才能做从站且只支持有限的数据类型映射读字符串很麻烦。OPC UA功能强大跨平台配合工业级网关很稳定但对一个简单项目来说部署成本太高而且配置授权麻烦。如果你的现场已经有OPC UA服务器当然可以用但如果是新做一个视觉站我建议直接用S7协议省心省力。另外S7协议不需要购买额外授权西门子PLC默认支持只要设置PLC的防护等级允许从外部读写就行。2. 联调前必须搞定的开发环境Halcon、VS与S7通讯库选型2.1 Halcon版本与授权问题我用的是Halcon 20.11这个版本稳定深度学习模块也齐全。如果你的项目只需要基础的形状匹配、Blob分析、测量工具Halcon 13、17、19这些老版本也完全够用。但如果你要上深度学习分类或缺陷检测我建议至少用20.11之后的功能版本深度学习算子更丰富。授权方面Halcon分为开发时授权和运行时授权。开发时用试用License也能跑但导出运行时DLL部署到产线机器必须有正式的Runtime License。很多朋友踩过这个坑开发环境一切正常生成exe拷到工控机上报错License error就是因为没有安装对应Runtime授权。另外Halcon是x64为主的所以VS工程的平台目标必须要设成x64否则导入halcondotnet.dll时大概率会出现BadImageFormatException。2.2 Visual Studio版本选择与Halcon的依赖C#上位机我习惯用Visual Studio 2022.NET Framework 4.8。为什么不用.NET Core因为Halcon的halcondotnet.dll老版本对.NET Core的兼容性有些微妙要折腾一堆配置文件。如果你是全新项目直接选.NET Framework 4.7.2以上最省事如果公司有统一需求用.NET 6/8也可以只要确认halcondotnet.dll能被正常引用、运行时能找到原生C的halcon.dll就行。添加Halcon C#库的路径通常在C:\Program Files\MVTec\HALCON-20.11\bin\dotnet35\halcondotnet.dll注意有些版本分了dotnet35和dotnetstandard等目录选匹配你的.NET版本的目录。然后在VS中添加引用后还需要把以下目录里的DLL复制到输出目录或设置生成后事件自动复制C:\Program Files\MVTec\HALCON-20.11\bin\x64-win64里面的halcon.dll、halconxl.dll等一堆原生DLL是必不可少的。最简单的做法把整个目录拷贝到项目输出文件夹或者把路径添加到系统环境变量PATH中。我个人更喜欢直接拷贝到输出目录避免生产电脑上环境变量被改坏。2.3 S7通讯库怎么选Sharp7、S7.Net Plus还是西门子官方库.Net平台上常用的S7通讯库主要有这三个库性质支持PLC上手难度特点S7.Net Plus开源S7-200/300/400/1200/1500低API封装完整读DB块能直接映射为对象但对复杂数据类型支持有限Sharp7开源S7-200/300/400/1200/1500中底层API更接近原始协议适合自己封装数据字节序控制灵活Siemens.Simatic.S7.Communication官方较新固件版本的S7-1200/1500中官方维护但授权和许可证机制较繁琐适合企业级项目我做视觉上位机时大部分需求就是读写M点、读写几个DB块里的整数、浮点数、布尔量。这种情况下我选择Sharp7它对字节序、数据偏移的控制非常透明现场调错时一眼能看到问题。S7.Net Plus虽然API友好但有时候处理BOOL和BYTE混合排列会踩坑。Sharp7的NuGet包叫Sharp7很小底层基于Snap7。安装完就能用。2.4 必须要提前确认的PLC侧设置无论选哪个库PLC侧的设置都决定了能不能连上。西门子S7-1200/1500默认情况下不允许外部设备通过PUT/GET来访存需要在CPU属性里打开允许来自远程对象的PUT/GET通信访问在博途软件中设备组态 → CPU → 属性 → 防护与安全 → 连接机制。如果不勾这一项C#上位机连上去执行读取时会直接返回错误。还要确认PLC的IP地址、子网掩码和上位机在同一个网段工控机网口直连PLC或走交换都行。另外机架号Rack和槽号Slot要按实际填S7-1200一般填0和1S7-300通常Rack0、Slot2S7-400可能是Rack1、Slot3。填错就连不上。3. 用Sharp7封装一个靠谱的C# S7通讯类3.1 为什么建议二次封装而不是到处直接调S7ClientSharp7提供的S7Client类用起来并不难但如果你在窗体的拍照按钮、定时器、扫码事件里都直接new S7Client那就等着连接数爆炸吧。S7协议建立连接是有开销的频繁断连重连会导致PLC通信负载高甚至把PLC通讯资源耗尽。我的做法是封装一个静态的PlcService类全局只维护一个S7Client实例所有上位机逻辑都通过这个实例读写。using Sharp7; public class PlcService { private S7Client _client; private string _ip; private int _rack; private int _slot; private readonly object _lock new object(); public bool IsConnected { get; private set; } public PlcService(string ip, int rack 0, int slot 1) { _ip ip; _rack rack; _slot slot; _client new S7Client(); } public bool Connect() { lock (_lock) { int result _client.ConnectTo(_ip, _rack, _slot); if (result 0) { IsConnected true; return true; } IsConnected false; return false; } } public void Disconnect() { lock (_lock) { _client.Disconnect(); IsConnected false; } } public bool EnsureConnected() { if (IsConnected) return true; return Connect(); } /// summary读取指定偏移处的M区字节/summary public byte[] ReadMerker(int startByte, int size) { lock (_lock) { var buffer new byte[size]; int result _client.ReadArea(S7Area.MK, 0, startByte, size, S7WordLen.Byte, buffer); if (result ! 0) { IsConnected false; throw new Exception($Read MK failed: {_client.ErrorText(result)}); } return buffer; } } public void WriteMerker(int startByte, byte[] data) { lock (_lock) { int result _client.WriteArea(S7Area.MK, 0, startByte, data.Length, S7WordLen.Byte, data); if (result ! 0) { IsConnected false; throw new Exception($Write MK failed: {_client.ErrorText(result)}); } } } /// summary读取DB块中指定偏移和长度的字节流/summary public byte[] ReadDb(int dbNumber, int startByte, int size) { lock (_lock) { var buffer new byte[size]; int result _client.ReadArea(S7Area.DB, dbNumber, startByte, size, S7WordLen.Byte, buffer); if (result ! 0) { IsConnected false; throw new Exception($Read DB{dbNumber} failed: {_client.ErrorText(result)}); } return buffer; } } public void WriteDb(int dbNumber, int startByte, byte[] data) { lock (_lock) { int result _client.WriteArea(S7Area.DB, dbNumber, startByte, data.Length, S7WordLen.Byte, data); if (result ! 0) { IsConnected false; throw new Exception($Write DB{dbNumber} failed: {_client.ErrorText(result)}); } } } }代码里我用了lock (_lock)因为上位机里定时器和按钮事件可能在不同线程同时操作通讯不加锁会偶发异常。3.2 常用的数据类型转换方法S7协议里数据是以字节流传输的C#端读到的byte[]需要自己解析。Sharp7帮我们封装了S7.GetBitAt、S7.GetIntAt、S7.GetRealAt等静态方法。比如PLC的M区第1个字节的第0位可以用下面的代码读取byte[] bytes s7.ReadMerker(0, 2); bool trigger S7.GetBitAt(bytes, 0, 0);写一个Bool到M区byte[] bytes new byte[1]; S7.SetBitAt(ref bytes, 0, 0, true); s7.WriteMerker(0, bytes);其实Sharp7官方的Demo里有S7Client直接读取Bit的例程但底层还是先把整字节读出来再通过GetBitAt解析。所以你自己封装时读一个Bool最少要读1个字节。读取DB块里的REAL4字节浮点数byte[] dbBytes s7.ReadDb(1, 0, 4); float realValue S7.GetRealAt(dbBytes, 0);要注意的是S7协议中DB块的偏移量是按字节算的。例如DB1中第0偏移是Real类型占4个字节那么下一个变量在DB1中的偏移就是4。很多新手直接按C#结构体去理解偏移忽略了字节对齐导致读出来的数据完全不对。在PLC中DB块变量的偏移可以在博途里看到对着偏移写就行。3.3 断线重连与心跳检测现场环境里设备断电、网线松动是家常便饭。如果上位机不处理断线就会出现通讯对象卡死。我的做法是搞一个后台定时器每500毫秒检查一次连接状态如果连接断开就尝试重连。同时在PLC里做一个心跳M点上位机每200毫秒翻转一次M200.0PLC逻辑监控该点如果超过1秒没有变化就判定上位机掉线执行安全停机。心跳写入代码如下private int _heartBit 0; private Timer _heartTimer; private void StartHeartBeat() { _heartTimer new Timer(200); _heartTimer.Elapsed (s, e) { lock (_lock) { if (_plc.EnsureConnected()) { byte[] data new byte[1]; S7.SetBitAt(ref data, 0, 0, _heartBit % 2 0); _plc.WriteMerker(200, data); _heartBit; } } }; _heartTimer.Start(); }注意重连不能太频繁建议间隔1秒以上否则PLC通讯连接还没完全释放又会占一个连接。4. Halcon检测结果如何变成PLC需要的数据4.1 在C#中调用Halcon的基本姿势C#调用Halcon通常分两种方式一是直接在工程里引用halcondotnet.dll用using HalconDotNet声明HDevEngine二是把Halcon的.hdev程序导出为C#代码然后用HTuple接收结果。我推荐后者因为视觉工程师用Halcon的HDevelop调试好算法后导出代码几乎是零成本。比如我在HDevelop里写了一个找圆心的小算法read_image (Image, part.png) threshold (Image, Region, 0, 100) connection (Region, ConnectedRegions) select_shape_max (ConnectedRegions, MaxRegion) smallest_circle (MaxRegion, Row, Column, Radius)导出为C#代码后在VS里运行就可以拿到Row、Column、Radius这三个HTuple。之后需要用HTuple转成doubleHTuple hRow, hCol, hRadius; // 这里是Halcon导出的算子执行代码省略 double row hRow.D; double col hCol.D; double radius hRadius.D;HTuple.D属性可以安全地取出Double值。如果结果是整数型用.I取int也行但要注意溢出问题。4.2 检测结果的数据映射定义PLC变量表把视觉结果通知PLC前一定要先和电气工程师确认好PLC侧的变量地址和类型。最好写一份视觉通讯变量表双方共同签字。例如变量说明数据类型PLC地址备注拍照触发信号BoolM100.0PLC→视觉上升沿有效视觉在位状态BoolM100.1视觉→PLC运行正常时置1检测结果OK/NGBoolM100.2视觉→PLCOK为1NG为0测量值1RealDB100.DBD0测量结果单位mm测量值2RealDB100.DBD4测量结果单位mm视觉任务号IntDB100.DBW81定位2测量3缺陷检测这样定义的变量表后续不管是调试还是维护都有据可查不会出现相互猜地址的尴尬。4.3 写入结果前要做的数据校验与转换Halcon算出的坐标可能是亚像素级比如Row123.456789而PLC需要的是一个浮点数。直接写入DB块当然可以但要注意PLC侧REAL和C#的float精度是一致的都是IEEE 754单精度。如果你用C#的double去转换需要手动转成floatfloat rowForPlc (float)row; byte[] rowBytes S7.FloatToBytes(rowForPlc);在实际产线上还真遇到过精度不一致导致机械手定位偏移的问题。后来排查发现上位机用double发给PLCPLC内部又把REAL当32位浮点计算精度损失在最后一位。所以建议在写入PLC前先统一转成float。如果视觉任务是判断合格/不合格我一般还会加一个简单的连续稳定判断逻辑连续两次检测结果相同才向PLC发送最终结果避免因偶发图像抖动导致误判。5. 搭建一个完整的触发-检测-反馈联动流程5.1 用状态机管理不同工位阶段上位机里不能一股脑地读、拍、算、写。因为PLC的执行节奏和视觉处理速度往往不同步。我用一个简单的状态机来管理空闲Idle ↓ 收到PLC拍照触发M100.0上升沿 等待相机硬触发或软件触发CaptureWait ↓ 图像采集完成 图像处理中Processing ↓ Halcon计算完成 发送结果中Sending ↓ 写入PLC完成 回到空闲代码示意enum WorkState { Idle, CaptureWait, Processing, Sending } private WorkState _state WorkState.Idle; private void CommunicationTimer_Elapsed(object sender, ElapsedEventArgs e) { try { switch (_state) { case WorkState.Idle: // 读取M100.0 if (triggerRaised) { _state WorkState.CaptureWait; GrabImageAsync(); } break; case WorkState.CaptureWait: if (imageReady) ProcessImageAsync(); _state WorkState.Processing; break; case WorkState.Processing: if (processingDone) { WriteResultToPlc(); _state WorkState.Sending; } break; case WorkState.Sending: if (writeConfirmed) { _state WorkState.Idle; ClearTrigger(); // 告诉PLC本次结果已接收复位触发 } break; } } catch (Exception ex) { LogError(ex.Message); _state WorkState.Idle; } }这种方式比在阻塞式while里死等更稳不容易卡死UI或者丢信号。5.2 拍照触发方式用PLC的上升沿还是周期轮询PLC给上位机的拍照信号我建议用上升沿触发。什么意思就是PLC把M100.0从0变成1上位机检测到这个边沿变化后才采集图像而不是只要读到M100.01就一直拍。如果上位机是周期轮询比如每100ms扫一次你只判断电平同一批工件停留时间较长时就会重复触发拍出多张图浪费节拍。PLC侧最好用边沿指令来置位一个拍照请求标志上位机处理完后再复位这个标志。或者反过来上位机检测到边沿后立刻先模拟PLC把它复位写入0确保不会二次触发。两种方式都能用我习惯让PLC保持这个信号直到上位机给出应答这样逻辑最清晰PLC: M100.0 1请求拍照 上位机: 检测到M100.0为1 → 拍照 → 处理 → 写结果到DB100 → 置位M100.3视觉已应答 PLC: 看到M100.3为1执行后续动作并复位M100.0 上位机: 检测到M100.0为0也复位M100.3这个握手协议能有效避免信号丢失。5.3 手自动模式切换别疏忽产线调试时设备肯定需要手动单步测试。视觉上位机也要支持手动模式和自动模式切换。我一般用PLC的M100.7作为模式选择0为手动1为自动。上位机在自动模式下才响应触发信号手动模式下上位机只做图像显示和结果展示不写回PLC方便调试算法。有些人嫌麻烦把模式切换放在上位机界面里做。但实际情况是操作员更熟悉HMI或设备上的旋钮所以模式信号最好由PLC给出上位机只做从动。我见过一个项目上位机某次重启后默认跑到自动模式结果手动调试时气缸突然动作差点出事故。后来坚持所有安全相关模式都只归PLC管上位机只管执行。5.4 完整的集成示例片段最后给一个集成片段展示从读触发到写结果的代码主线// 读M100.0判断拍照触发 byte[] mData _plc.ReadMerker(100, 1); bool grabTrigger S7.GetBitAt(mData, 0, 0); if (grabTrigger _currentState WorkState.Idle) { _currentState WorkState.CaptureWait; // 软件触发相机采图 _frameGrabber.StartGrab(); // 假如已经同步获取到HImage对象直接执行Halcon处理 HImage image _lastGrabImage; HTuple hv_R null, hv_C null, hv_Radius null; HObject region null; // 这里放你从HDevelop导出的Halcon算法 // 略... double row hv_R.D; double col hv_C.D; double radius hv_Radius.D; // 将结果写入DB100 byte[] resultBytes new byte[12]; byte[] rowBytes S7.FloatToBytes((float)row); byte[] colBytes S7.FloatToBytes((float)col); byte[] radiusBytes S7.FloatToBytes((float)radius); Buffer.BlockCopy(rowBytes, 0, resultBytes, 0, 4); Buffer.BlockCopy(colBytes, 0, resultBytes, 4, 4); Buffer.BlockCopy(radiusBytes, 0, resultBytes, 8, 4); _plc.WriteDb(100, 0, resultBytes); // 写OK/NG状态到M100.2 bool ok radius 50; // 示意 byte[] mStatus new byte[1]; S7.SetBitAt(ref mStatus, 0, 2, ok); _plc.WriteMerker(100, mStatus); // 应答PLC byte[] ackData new byte[1]; S7.SetBitAt(ref ackData, 0, 3, true); _plc.WriteMerker(100, ackData); _currentState WorkState.Idle; }这个片段把前面讲到的知识点串了起来。实际项目中你还需要做超时保护例如拍照后2秒内没有采集完成上位机强制复位状态并报警防止状态机卡死。6. 联调中真实遇到的坑与排查方法6.1 连接失败先别怀疑代码按这个顺序排查C#上位机连不上PLC我碰到过很多次。正确的排查顺序是ping测试在工控机命令行里ping PLC的IP如果不通先查网线、交换机、IP设置。很多时候是工控机装了多个网卡默认路由不对。检查PLC侧允许PUT/GET访问S7-1200/1500默认是禁用的。博途里这个选项藏的有点深但必须打开。确认机架号和槽号S7-1200/1500是0和1S7-300/400按实际硬件配置填。填错就是常见的Error 32: CLIDisconnect。看上位机防火墙Windows防火墙会拦截S7通讯端口TCP 102。开发时直接关掉防火墙测试能连上再把它加白名单。PLC连接资源满如果之前有设备异常断连没有释放PLC的连接池会满。这时候重启PLC或等待自动释放。Sharp7返回的错误码很有用比如0x00000100S7ERR_CLI_DISCONNECTED表示连接没建立0x00000004S7ERR_PARAMS多半是参数错比如地址或缓冲区大小不对。6.2 读出来的数据和PLC监控画面不一致这是最让人抓狂的问题PLC监控里DB100.DBD0明明是123.5上位机读出来却是乱七八糟的大数。排查下来基本都是偏移量写错或者数据类型解析错。举个例子PLC侧的DB100是这样定义的名称类型偏移量测量值1Real0测量值2Real4结果Int8合格标志Bool10.0如果上位机把测量值2写到偏移2去读那读出来的字节其实包括测量值1的后半段和测量值2的前半段拼出来的float自然不对。所以一定要打开博途的偏移量视图对着偏移抄。另外BOOL在DB块里的排列是按位的。比如DB100.DBX10.0、DB100.DBX10.1、DB100.DBX10.2这三个Bool都位于偏移量10的同一个字节里上位机读取时只需要读1个字节然后用S7.GetBitAt解析第0位、第1位、第2位。很多同学直接去读10、11、12三个字节不仅读错还浪费通信资源。6.3 Halcon的HTuple转数值时的隐蔽坑Halcon导出的代码返回的是HTuple它可能是int、double甚至字符串类型。如果你直接.D取double遇到原始数据是int类型时其实也能转成功因为HTuple内部做了转换。但如果是字符串类型的handle比如Region的ID.D会抛异常。还有一个小坑Halcon图像坐标系和PLC坐标系往往不一致。比如相机拍的图像左上角为原点行坐标向下增加而机械手坐标系通常向右为X、向上为Y。所以发给PLC前要做坐标变换float plcX (float)(row); // 根据实际标定关系换算 float plcY (float)(-col); // 比如Y方向需要取反千万不要直接把像素坐标发过去除非你确认PLC侧已经做过标定。我第一套联动项目就因为这个机械手抓了个寂寞。后来老老实实做了一次九点标定把像素坐标转成机器人基座坐标才稳定下来。6.4 高频读写导致PLC扫描周期变长S7通讯会占用PLC的通讯处理时间如果上位机每10ms就读写一次大字节块PLC的扫描周期可能从5ms飙到15ms进而影响设备运动控制。经验做法是非实时数据比如检测结果、测量值采用事件触发或100ms级别的周期写实时性要求高的信号比如启动、停止命令用单独的M点控制不要和DB大块一起写一次读写的数据量尽量小不要一次读512字节只为了一个Bool在PLC里把通讯数据集中放到一个专门的DB块并启用优化的块访问其实如果启用优化块访问S7协议可能无法直接读取需要关闭或使用DB访问接口这里注意要和电气沟通。我见过一个项目上位机割接时用了循环定时器每20ms读一个100字节的DB块结果PLC本来2ms的循环胖到20ms导致伺服抖动。把读取周期改到200ms后一切正常。6.5 UI线程被通讯阻塞的规避C# WinForm上位机里如果直接在按钮点击事件里同步调用S7通讯网络卡顿时界面会假死。一定要把通讯逻辑放到后台线程并用Control.BeginInvoke更新UI。比如Task.Run(() { var result _plc.ReadDb(1, 0, 4); this.BeginInvoke(new Action(() { textBox1.Text result.Length.ToString(); })); });当然如果你把轮询都放在一个后台定时器里也需要注意定时器回调里不能直接操作UI控件。最后说一个我个人的小习惯所有视觉和PLC联动的程序我都会在界面上加一个连接状态指示灯和一个最近通讯错误信息文本框。第一次现场调试时这个文本能帮你少求很多人。这套基于Halcon C# S7协议联动的思路我已经在装配检测、定位引导、缺陷分选多个项目里用上了。第一篇文章先和你把这些地基打牢下一步就可以聊聊怎么用Halcon做模板匹配、测量工具的详细参数调优以及怎么处理多相机并行和PLC复杂逻辑的结合。如果你也在做类似集成欢迎按这套方法先跑通一个最简单的拍照-识别-输出闭环后面很多高级功能都是在这个骨架上长出来的。
返回列表