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