開發(fā):C#/C++/VB.net通訊實(shí)現(xiàn))
簡(jiǎn)介這是一套基恩士激光位移傳感器的跨語言Demo面向工業(yè)自動(dòng)化領(lǐng)域使用C#、C或VB.NET開發(fā)上位機(jī)程序的工程師解決與傳感器通信、參數(shù)讀寫和測(cè)量數(shù)據(jù)獲取等問題。壓縮包內(nèi)共365個(gè)文件大小僅1.27MB包含292個(gè)htm說明文檔、18個(gè)dll驅(qū)動(dòng)與依賴庫、14個(gè)h頭文件、8個(gè)cpp源碼、4個(gè)cs示例以及VB工程對(duì)應(yīng)的frm/bas/vbp文件覆蓋三種語言的完整工程結(jié)構(gòu)。其中LKG3000_DLL_ver2_10為傳感器驅(qū)動(dòng)庫htm文檔提供API調(diào)用說明cpp/cs/bas示例分別演示了P/Invoke、LoadLibrary/GetProcAddress等典型調(diào)用方式便于對(duì)比三種語言的實(shí)現(xiàn)差異同時(shí)還可從示例中把握DLL加載異常處理、異步讀取、數(shù)據(jù)格式轉(zhuǎn)換與線程安全等開發(fā)要點(diǎn)。已有2142人學(xué)習(xí)/瀏覽該資源適合需要快速上手基恩士LKG系列傳感器二次開發(fā)的嵌入式或工業(yè)軟件開發(fā)者參考后可顯著減少查閱手冊(cè)和調(diào)試底層接口的時(shí)間。 車間里最常見的一個(gè)場(chǎng)景設(shè)備裝好了基恩士激光位移傳感器的數(shù)值在觸摸屏上跳得挺好但到了上位機(jī)這邊怎么讀都不對(duì)或者干脆讀不上來。我自己就替現(xiàn)場(chǎng)處理過好幾回這種事最后發(fā)現(xiàn)十有八九不是傳感器的問題而是上位機(jī)代碼沒寫對(duì)。所以這篇就把基恩士激光位移傳感器在C#、C、VB.net三語言下的demo開發(fā)從頭到尾捋一遍適合剛接觸工業(yè)上位機(jī)開發(fā)的工程師也適合那些項(xiàng)目做了一半、卡在通訊環(huán)節(jié)需要找參考的人。1. 拿到的第一步把位移傳感器“變成”一個(gè)網(wǎng)絡(luò)設(shè)備1.1 先分清型號(hào)和接口別只會(huì)找網(wǎng)口基恩士激光位移傳感器這個(gè)大家族里IL系列、LK-G5000系列、CL-3000系列都是常見選手長(zhǎng)得不一樣通信方式也不完全一樣。有的型號(hào)支持模擬量輸出直接接PLC的模擬量模塊就能用4到20 mA或者0到10 V這種本質(zhì)上不需要寫上位機(jī)代碼。但如果要做數(shù)據(jù)記錄、遠(yuǎn)程監(jiān)控、自動(dòng)判定良品就走數(shù)字通信。目前新一點(diǎn)的型號(hào)基本都帶Ethernet接口也就是常說的網(wǎng)口走TCP/IP通信。還有一些老型號(hào)走RS-232C串口這就涉及串口編程了。寫demo之前第一個(gè)動(dòng)作是去官網(wǎng)下載對(duì)應(yīng)型號(hào)的通信手冊(cè)確認(rèn)三個(gè)事有沒有以太網(wǎng)口、走TCP還是UDP、默認(rèn)端口是多少。很多人上來就套網(wǎng)上通用的代碼結(jié)果型號(hào)對(duì)不上浪費(fèi)一整天。1.2 基恩士通訊協(xié)議的核心約定ASCII指令加換行符基恩士激光位移傳感器的以太網(wǎng)通信大多數(shù)系列用的是一條一條的ASCII命令。上位機(jī)發(fā)一條短指令過去傳感器回一串ASCII碼格式類似“返回狀態(tài) 數(shù)值 單位”中間可能有逗號(hào)或空格分隔。指令通常以CR或者CRLF結(jié)尾不同系列、不同固件版本會(huì)有細(xì)微差別。這一點(diǎn)非常容易踩坑。很多人在網(wǎng)上找到一段別人寫好的demo命令發(fā)過去返回全是問號(hào)或者直接沒反應(yīng)。原因往往是換行符不對(duì)或者指令關(guān)鍵字跟當(dāng)前系列不匹配。我從一個(gè)項(xiàng)目上拿到的經(jīng)驗(yàn)是開發(fā)前先把通信手冊(cè)里的指令表完整看一遍里面會(huì)明確寫每條指令的格式、返回幀的每個(gè)字節(jié)代表什么。特別是“讀取當(dāng)前測(cè)量值”這條指令有的系列叫MS有的系列用MR還有的直接用類似“LON”這種帶參數(shù)的寫法。代碼層面其實(shí)很簡(jiǎn)單難的是命令沒選對(duì)。1.3 連接前必須確認(rèn)的三個(gè)參數(shù)IP地址基恩士傳感器出廠默認(rèn)IP一般是192.168.0.1這一類具體以手冊(cè)為準(zhǔn)。用配置軟件或者直接網(wǎng)頁登錄修改注意和電腦的IP在同一個(gè)網(wǎng)段。端口號(hào)有些型號(hào)固定端口有些可以通過配置軟件改。默認(rèn)值和對(duì)應(yīng)關(guān)系手冊(cè)里都有。觸發(fā)源傳感器是連續(xù)測(cè)量還是外部觸發(fā)測(cè)量會(huì)直接決定上位機(jī)讀數(shù)的頻率和意義。連續(xù)測(cè)量模式下上位機(jī)發(fā)一條讀指令傳感器返回當(dāng)前瞬時(shí)值外部觸發(fā)模式下傳感器收到外部信號(hào)才測(cè)一次上位機(jī)如果一直發(fā)讀指令可能讀到一條錯(cuò)誤提示。這三個(gè)參數(shù)沒搞對(duì)程序?qū)懙迷倨炼际前状?。我見過一個(gè)現(xiàn)場(chǎng)調(diào)試人員折騰了兩天最后發(fā)現(xiàn)是電腦IP和傳感器IP不在同一網(wǎng)段Ping都不通更別提讀數(shù)據(jù)了。2. C#版本W(wǎng)inForm上位機(jī)讀取和顯示實(shí)測(cè)值C#的demo是需求最旺盛的。畢竟現(xiàn)在新項(xiàng)目里用WinForm做界面很常見C#寫起來比MFC舒服太多而且對(duì)接數(shù)據(jù)庫、導(dǎo)出Excel都方便。2.1 封裝一個(gè)精簡(jiǎn)的連接類把TcpClient放進(jìn)對(duì)的地方C#里讀寫TcpClient是基本功但很多初學(xué)者喜歡把連接代碼直接塞進(jìn)按鈕點(diǎn)擊事件里一錘子買賣。實(shí)際項(xiàng)目里建議封裝成一個(gè)類連接、斷開、發(fā)送命令、接收響應(yīng)都做成方法。這樣后續(xù)換傳感器型號(hào)、換協(xié)議只改這個(gè)類就可以了。public class KeyenceLaserClient { private TcpClient _client; private NetworkStream _stream; private readonly object _lockObj new object(); public bool Connect(string ip, int port, int timeoutMs 500) { try { _client new TcpClient(); IAsyncResult result _client.BeginConnect(ip, port, null, null); bool ok result.AsyncWaitHandle.WaitOne(timeoutMs); if (!ok) { _client.Close(); return false; } _client.EndConnect(result); _stream _client.GetStream(); _stream.ReadTimeout timeoutMs; return _stream.CanWrite _stream.CanRead; } catch { return false; } } public string SendReceive(string command) { lock (_lockObj) { byte[] sendBytes Encoding.ASCII.GetBytes(command \r\n); _stream.Write(sendBytes, 0, sendBytes.Length); _stream.Flush(); byte[] recvBytes new byte[1024]; int len _stream.Read(recvBytes, 0, recvBytes.Length); return Encoding.ASCII.GetString(recvBytes, 0, len).Trim(); } } public void Close() { _stream?.Close(); _client?.Close(); } }注意我給連接加了超時(shí)這是仿“生產(chǎn)環(huán)境”的常規(guī)配置。電腦網(wǎng)線沒插好、傳感器沒通電、IP被占用這些情況如果直接調(diào)用Connect在WinForm里會(huì)造成界面假死超時(shí)機(jī)制能避免這個(gè)體驗(yàn)問題。2.2 實(shí)時(shí)刷新UI的正確姿勢(shì)別在UI線程里收數(shù)據(jù)第一個(gè)版本跑通之后很多人會(huì)掉進(jìn)第二個(gè)坑在Timer事件里發(fā)指令、讀數(shù)據(jù)、直接更新TextBox。如果傳感器響應(yīng)速度稍慢或者網(wǎng)絡(luò)有抖動(dòng)WinForm就會(huì)出現(xiàn)“閃退”或者界面卡住這是因?yàn)镾ocket的接收操作阻塞了UI線程。正確做法是用BackgroundWorker或者Task把收發(fā)指令的循環(huán)放到后臺(tái)線程拿到結(jié)果后用Invoke或者BeginInvoke回UI線程刷新控件。簡(jiǎn)單說就是連接在后臺(tái)收發(fā)在后臺(tái)界面只管顯示。private async void btnStart_Click(object sender, EventArgs e) { await Task.Run(() { while (_isReading) { string raw _laserClient.SendReceive(MS); double value ParseValue(raw); BeginInvoke((Action)(() lblValue.Text value.ToString(F3))); Thread.Sleep(50); } }); }50毫秒刷新一次是現(xiàn)場(chǎng)比較穩(wěn)的頻率。如果追求更高實(shí)時(shí)性可以壓縮到20毫秒但要看傳感器的通信周期否則可能連發(fā)命令導(dǎo)致傳感器來不及響應(yīng)。2.3 歸零、閾值這些控制命令怎么發(fā)讀取數(shù)值只是第一步現(xiàn)場(chǎng)還經(jīng)常需要?dú)w零、設(shè)置判定閾值、啟動(dòng)/停止測(cè)量。這類控制命令和讀取命令的本質(zhì)區(qū)別是它們往往有執(zhí)行條件。比如歸零得保證傳感器已經(jīng)穩(wěn)定并且表面沒有正在往探頭前面移動(dòng)的物體否則歸零后的基準(zhǔn)面就是錯(cuò)的。再比如閾值設(shè)置好的做法是封裝一個(gè)方法把測(cè)量值、上下閾值都作為參數(shù)傳進(jìn)去返回設(shè)置成功還是失敗然后記錄到日志里。我在項(xiàng)目里習(xí)慣把所有發(fā)送過的命令和返回結(jié)果都寫進(jìn)日志這個(gè)習(xí)慣在調(diào)試時(shí)救了我很多次。現(xiàn)場(chǎng)排查問題時(shí)翻日志比摸傳感器可靠得多。2.4 掉線了怎么辦斷線重連的簡(jiǎn)單實(shí)現(xiàn)工業(yè)現(xiàn)場(chǎng)網(wǎng)絡(luò)環(huán)境不像辦公室那么干凈交換機(jī)不穩(wěn)定、網(wǎng)線被踩、傳感器重啟都是可能發(fā)生的。一個(gè)靠譜的上位機(jī)程序必須處理斷線重連。我的做法是Read操作拋異常時(shí)標(biāo)記當(dāng)前連接已斷開嘗試重新Connect重連失敗則等幾秒再試同時(shí)把異常信息拋到UI線程在界面上給一個(gè)“通訊斷開”的醒目提示。重連成功之后要重新做一次初始化命令比如重新打開測(cè)量、恢復(fù)之前的閾值配置。這個(gè)“恢復(fù)現(xiàn)場(chǎng)”的步驟很多新手會(huì)漏掉導(dǎo)致重連成功但數(shù)據(jù)不對(duì)。3. C版本適合嵌入工控機(jī)和配合運(yùn)動(dòng)控制卡C的demo多數(shù)出現(xiàn)在兩類場(chǎng)景一類是工控機(jī)上的老程序以前用MFC寫的界面現(xiàn)在要接一臺(tái)新的激光位移傳感器另一類是配合運(yùn)動(dòng)控制卡做實(shí)時(shí)檢測(cè)數(shù)據(jù)要直接參與運(yùn)動(dòng)控制邏輯C的實(shí)時(shí)性優(yōu)勢(shì)比C#更明顯。3.1 Winsock初始化這步最容易被殺毒軟件攔怎么處理Windows下C寫Socket第一步就是用WSAStartup初始化Winsock。這個(gè)函數(shù)本身不難但有個(gè)讓人抓狂的坑某些工控機(jī)上裝了安全軟件WSAStartup返回的不是0而是錯(cuò)誤碼程序直接崩掉。遇到這種情況先看錯(cuò)誤碼。如果是WSAEINPROGRESS或者WSAEINVALIDPROVIDER多半是第三方網(wǎng)絡(luò)驅(qū)動(dòng)或安全軟件跟Winsock沖突。處理辦法是更新驅(qū)動(dòng)、把程序加入白名單或者用管理員權(quán)限運(yùn)行。代碼層面要注意WSAStartup用MAKEWORD(2, 2)請(qǐng)求2.2版本并且一定記得在程序退出時(shí)調(diào)用WSACleanup否則下次啟動(dòng)可能報(bào)“Socket句柄耗盡”。#include winsock2.h #include ws2tcpip.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), wsa) ! 0) { printf(WSAStartup failed\n); return -1; } SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addr { 0 }; addr.sin_family AF_INET; addr.sin_port htons(9000); inet_pton(AF_INET, 192.168.0.1, addr.sin_addr); if (connect(sock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return -1; } const char* cmd MS\r\n; send(sock, cmd, (int)strlen(cmd), 0); char buf[256] { 0 }; recv(sock, buf, sizeof(buf), 0); printf(Sensor value: %s\n, buf); closesocket(sock); WSACleanup(); return 0; }inet_pton在老版本Visual Studio里可能要用inet_addr替代這也是環(huán)境相關(guān)的細(xì)節(jié)。3.2 命令發(fā)送與返回?cái)?shù)據(jù)解析注意字節(jié)序和符號(hào)位C里面解析返回?cái)?shù)據(jù)時(shí)最容易栽在“看似數(shù)字、實(shí)則是ASCII字符串”這個(gè)點(diǎn)上。基恩士的返回值通常是一串ASCII字符比如“00234.567”而不是二進(jìn)制浮點(diǎn)數(shù)。所以直接拿int*去強(qiáng)轉(zhuǎn)讀取內(nèi)存地址是行不通的必須先把char數(shù)組轉(zhuǎn)成字符串再按業(yè)務(wù)需求轉(zhuǎn)換。如果傳感器返回的是負(fù)數(shù)字符串里會(huì)帶負(fù)號(hào)解析時(shí)要用atof這類函數(shù)而不是簡(jiǎn)單的前綴截取。如果數(shù)據(jù)精度要求高要注意顯示位數(shù)基恩士有些型號(hào)能到0.01微米但前提是你在解析時(shí)保留足夠的小數(shù)位并且確認(rèn)傳感器的顯示單位是mm、um還是nm。單位搞錯(cuò)了一個(gè)看似離譜的測(cè)量結(jié)果會(huì)讓你懷疑整個(gè)代碼寫錯(cuò)了實(shí)際上只是單位換算問題。3.3 多線程收數(shù)傳感器數(shù)據(jù)頻率與工控程序的配合C程序里如果只是簡(jiǎn)單地在主循環(huán)里recv數(shù)據(jù)頻率一高就很容易丟包或者主線程被一次網(wǎng)絡(luò)超時(shí)卡住幾百毫秒影響整個(gè)控制周期。實(shí)際項(xiàng)目里建議給傳感器單獨(dú)開一個(gè)接收線程把最近一次的有效測(cè)量值存在一個(gè)共享變量里主控制循環(huán)需要數(shù)據(jù)時(shí)直接從共享變量讀。這里要注意的是共享變量的讀寫要做線程同步最簡(jiǎn)單的方式是加一個(gè)臨界區(qū)或者用std::atomic修飾防止讀到半截?cái)?shù)據(jù)。我見過程序員在這里不加鎖結(jié)果測(cè)量值偶爾出現(xiàn)“跳變”怎么排查都查不出原因最后發(fā)現(xiàn)問題出在多線程訪問同一個(gè)變量上。3.4 和運(yùn)動(dòng)控制卡聯(lián)動(dòng)的數(shù)據(jù)傳遞思路如果位移傳感器要和運(yùn)動(dòng)控制卡聯(lián)動(dòng)比如測(cè)高然后調(diào)整壓頭位置那么數(shù)據(jù)路徑就不能走“傳感器到PC再到控制卡”這種串行鏈路否則延遲很高。正確的思路是PC只負(fù)責(zé)讀取和記錄運(yùn)動(dòng)控制邏輯放到控制卡內(nèi)部傳感器通過I/O或現(xiàn)場(chǎng)總線直接給控制卡信號(hào)。這里C的優(yōu)勢(shì)就出來了調(diào)用控制卡廠商的SDK通常本身就是C接口C直接對(duì)接很順。建議把讀傳感器和調(diào)用控制卡SDK放在同一個(gè)高優(yōu)先級(jí)線程里循環(huán)周期控制在10毫秒以內(nèi)這對(duì)傳感器的響應(yīng)速度和控制卡的命令執(zhí)行時(shí)間都有要求需要實(shí)測(cè)確認(rèn)。4. VB.net版本老項(xiàng)目里最“省事”的重寫選擇4.1 遇到的老項(xiàng)目場(chǎng)景還沒上位的設(shè)備、維護(hù)人員只熟VB我知道很多年輕工程師對(duì)VB.net有偏見但在工廠里轉(zhuǎn)一圈就會(huì)發(fā)現(xiàn)大量在役設(shè)備的上位機(jī)是VB.net甚至VB6寫的尤其是十幾年前投產(chǎn)、至今還在穩(wěn)定跑的生產(chǎn)線。這些項(xiàng)目的維護(hù)人員可能不懂C#但能看懂VB。所以當(dāng)這類產(chǎn)線要新增一臺(tái)激光位移傳感器做數(shù)據(jù)采集時(shí)寫一個(gè)VB.net的demo往往比全部推倒重來更現(xiàn)實(shí)。另外有些傳感器廠商提供的演示程序樣例里同時(shí)有VB.net版本說明這個(gè)語言在工業(yè)圈一直都有需求。既然應(yīng)用廠在用那代碼就得有人寫。4.2 一個(gè)能跑通的TcpClient代碼注意事項(xiàng)寫在注釋里VB.net的TcpClient用法和C#幾乎一樣只是語法不同。關(guān)鍵點(diǎn)還是那幾個(gè)超時(shí)設(shè)置、數(shù)據(jù)解析、UI線程切換。Imports System.Net.Sockets Imports System.Text Public Class LaserSensorClient Private client As TcpClient Private stream As NetworkStream Public Function Connect(ip As String, port As Integer) As Boolean Try client New TcpClient() Dim result As IAsyncResult client.BeginConnect(ip, port, Nothing, Nothing) Dim ok As Boolean result.AsyncWaitHandle.WaitOne(500) If Not ok Then client.Close() Return False End If client.EndConnect(result) stream client.GetStream() stream.ReadTimeout 500 Return True Catch ex As Exception Return False End Try End Function Public Function SendReceive(cmd As String) As String Dim sendBytes As Byte() Encoding.ASCII.GetBytes(cmd vbCrLf) stream.Write(sendBytes, 0, sendBytes.Length) stream.Flush() Dim recvBytes(1024) As Byte Dim len As Integer stream.Read(recvBytes, 0, recvBytes.Length) Return Encoding.ASCII.GetString(recvBytes, 0, len).Trim() End Function End Class那段BeginConnect是重點(diǎn)不加超時(shí)的話如果IP地址根本不存在UI會(huì)卡住幾十秒才彈錯(cuò)誤。加了超時(shí)之后連接失敗能快速反饋給操作員而不是干等。4.3 VB.net和C#混調(diào)DLL時(shí)的坑事件委托與線程切換有些老項(xiàng)目里外購(gòu)的采集卡SDK只提供C#版本或者C的DLLVB.net要引用它就得走COM Interop或者自己包裝一層。這中間最容易踩的坑是DLL回傳數(shù)據(jù)時(shí)觸發(fā)的事件在VB.net里收到的線程不是UI線程直接更新界面控件會(huì)拋異常。解決辦法是寫一個(gè)委托在事件回調(diào)里用Me.Invoke切回UI線程。這個(gè)坑我在C#里也踩過但在VB.net里遇到得更多因?yàn)槔享?xiàng)目的開發(fā)者往往不太注意線程模型控件更新代碼習(xí)慣性隨手就寫。5. 三種語言demo都跑通后我總結(jié)的避坑清單5.1 同一個(gè)系列的命令也可能不一樣基恩士激光位移傳感器不是一個(gè)型號(hào)而是一個(gè)大家族。同樣是讀當(dāng)前測(cè)量值IL系列的指令和LK-G5000系列的可能就不同甚至同系列不同固件版本之間都有差異。所以最穩(wěn)妥的做法是拿到一臺(tái)傳感器先看面板標(biāo)簽上的型號(hào)再找對(duì)應(yīng)手冊(cè)查指令表不要默認(rèn)“上次那個(gè)型號(hào)能用這次也一定行”。我在現(xiàn)場(chǎng)就吃過這個(gè)虧。上一臺(tái)傳感器用“MS”讀數(shù)值跑得好好的換了一臺(tái)同品牌更高精度的型號(hào)指令發(fā)過去一直返回錯(cuò)誤碼后來翻手冊(cè)才知道新系列要求先發(fā)一條選擇命令進(jìn)入特定模式后才能讀數(shù)據(jù)。5.2 返回值里有中文和單位編碼問題部分傳感器返回的數(shù)據(jù)里帶了中文說明文字比如“OK”、“NG”這樣的判定或者帶單位的字符串。如果你用ASCII解碼中文會(huì)變成亂碼。處理方法是先確認(rèn)返回?cái)?shù)據(jù)的編碼格式大多數(shù)是UTF-8或ASCII但有的歷史型號(hào)是本地代碼頁。最簡(jiǎn)單的辦法是用Encoding.Default來解碼但要注意這臺(tái)電腦的Windows區(qū)域設(shè)置可能會(huì)影響結(jié)果。更穩(wěn)妥的是先判斷前幾個(gè)字節(jié)是不是固定的數(shù)字開頭只截取數(shù)字部分做解析繞開中文的干擾。這個(gè)“只取數(shù)字”的思路在工業(yè)現(xiàn)場(chǎng)非常實(shí)用別管協(xié)議里帶多少額外信息最終要的其實(shí)就是那一個(gè)測(cè)量值。5.3 關(guān)于超時(shí)、重試、日志三個(gè)建議超時(shí)的設(shè)置要分場(chǎng)景。讀取測(cè)量值的命令超時(shí)建議設(shè)短一點(diǎn)500毫秒足夠如果超過1秒還沒返回說明鏈路已經(jīng)不正常了再等也是白等。但執(zhí)行歸零、校準(zhǔn)這類動(dòng)作超時(shí)要放寬到3到5秒因?yàn)閭鞲衅鲀?nèi)部可能需要時(shí)間穩(wěn)定。重試不能無限重試最多連續(xù)重試3次每次間隔200毫秒還不行就拋異常并報(bào)警通知操作員。無限重試會(huì)讓程序在傳感器斷線時(shí)陷入死循環(huán)界面表現(xiàn)為“卡死”。日志一定要記錄。不管用C#、C還是VB.net每發(fā)一條指令、每收到一條響應(yīng)都帶上時(shí)間戳寫入日志文件。定位現(xiàn)場(chǎng)問題時(shí)這個(gè)日志文件的價(jià)值遠(yuǎn)超任何調(diào)試工具。我個(gè)人的習(xí)慣是日志文件按天切分日志里同時(shí)記錄原始返回值和解析后的數(shù)值。這樣即使解析代碼本身有bug也能從原始數(shù)據(jù)里還原真實(shí)情況不用返工。本文還有配套的精品資源點(diǎn)擊獲取