戰(zhàn):開發(fā)跨平臺(tái)串口監(jiān)控工具,打通Windows與VMware Linux串口通信)
簡(jiǎn)介面向嵌入式與工業(yè)控制開發(fā)者的Tcl/Tk串口通信資源包定位是幫助開發(fā)者借助Tcl腳本快速實(shí)現(xiàn)串口參數(shù)配置、數(shù)據(jù)收發(fā)與事件監(jiān)聽。包內(nèi)共134個(gè)文件以53個(gè)tcl源文件為主另有g(shù)if/bmp/xbm/jpg等圖形資源用于構(gòu)建監(jiān)控界面cfg配置文件用于設(shè)定波特率、校驗(yàn)位等串口參數(shù)配套bat/sh腳本自動(dòng)完成打包構(gòu)建并附帶tclkit解釋器和sdx工具鏈整體僅2.28MB。通過閱讀源碼可掌握open、fconfigure、puts、gets、fileevent等命令的實(shí)際用法理解串口事件驅(qū)動(dòng)模型及GUI聯(lián)動(dòng)方式便于快速搭建自己的調(diào)試工具。資源還包含較多圖形素材與示例配置適合希望以輕量腳本替代復(fù)雜上位機(jī)開發(fā)、快速驗(yàn)證硬件通信邏輯的Tcl/Tk使用者。已有545人學(xué)習(xí)下載。 搞Linux驅(qū)動(dòng)、嵌入式開發(fā)或者調(diào)試內(nèi)核啟動(dòng)流程的朋友大概率都碰到過這個(gè)需求宿主機(jī)是Windows上面開著一個(gè)VMware虛擬機(jī)跑Linux然后希望用Windows這邊的一個(gè)串口調(diào)試工具跟虛擬機(jī)里L(fēng)inux的串口接口做雙向通信。很多人第一反應(yīng)是掏出Python、Qt或者C#寫個(gè)小工具但我今天要分享的方案比較“老派”——用Tcl/Tk寫了一個(gè)叫Moni的串口通信監(jiān)控工具。單文件、免編譯、不依賴IDEWindows和Linux同一份代碼都能跑。這篇文章會(huì)從為什么選Tcl/Tk開始到Windows宿主機(jī)與VMware Linux之間怎么打通串口通路再到Moni的完整實(shí)現(xiàn)代碼和踩坑記錄全部展開講。1. 為什么我會(huì)用Tcl/Tk寫串口調(diào)試工具1.1 這個(gè)需求是怎么來的先交代一下背景。前陣子我在調(diào)一個(gè)Linux內(nèi)核模塊模塊中要跟外部設(shè)備通過UART交互。為了調(diào)試方便我把Linux跑在VMware里Windows宿主機(jī)上用串口調(diào)試工具和設(shè)備對(duì)話。結(jié)果發(fā)現(xiàn)常用串口助手連不上虛擬機(jī)或者說連上了但數(shù)據(jù)完全沒有反應(yīng)兩邊各說各話。折騰一圈后我意識(shí)到問題不在工具而在于我根本沒有把“宿主機(jī)串口”和“虛擬機(jī)內(nèi)串口”正確連通。更麻煩的是Windows便攜機(jī)上往往沒有物理串口連調(diào)試對(duì)象都要靠USB轉(zhuǎn)串口模塊。后來我用虛擬串口對(duì)把Windows的串口映射給VMware里的Linux用然后自己在Tcl/Tk里寫了一個(gè)輕量串口監(jiān)控程序Moni這才把整個(gè)鏈路打通。1.2 Tcl/Tk憑什么還值得用很多人一聽Tcl就搖頭覺得是古董語言。但作為調(diào)試工具Tcl/Tk有幾個(gè)特性是Python和C都沒法馬上替代的單文件腳本無需編譯拷到任何一臺(tái)機(jī)器上裝了Tcl/Tk runtime就能跑。Tcl天生基于事件循環(huán)fileevent處理串口異步讀寫非常自然不像C語言里要自己維護(hù)線程。Tk的text、entry、button等控件足夠完成串口工具界面不需要額外裝GUI庫。在網(wǎng)絡(luò)設(shè)備、嵌入式測(cè)試領(lǐng)域Tcl仍然是事實(shí)標(biāo)準(zhǔn)之一很多自動(dòng)化測(cè)試框架都預(yù)留了Tcl接口。這并不意味著Tcl能取代Python而是說當(dāng)你想快速做一個(gè)用于聯(lián)調(diào)、抓包、日志監(jiān)控的串口工具時(shí)Tcl/Tk是“性價(jià)比”很高的選擇。Moni這個(gè)名字也是隨口起的含義是Monitor監(jiān)控串口收發(fā)。2. 串口通信與虛擬串口底層的準(zhǔn)備工作2.1 串口通信必須搞懂的5個(gè)參數(shù)不管用什么語言寫串口程序都繞不開波特率、數(shù)據(jù)位、停止位、校驗(yàn)位和流控。這里用最簡(jiǎn)單的大白話解釋串口通信是兩個(gè)設(shè)備之間按位傳輸數(shù)據(jù)必須提前約定好“多長(zhǎng)時(shí)間算一位”波特率、“一次傳幾個(gè)數(shù)據(jù)位”數(shù)據(jù)位、“什么時(shí)候算傳完一組”停止位、“要不要多一個(gè)校驗(yàn)位來糾錯(cuò)”校驗(yàn)位、“誰來控制發(fā)送節(jié)奏”流控。我平時(shí)最常用115200,8,N,1也就是波特率115200、8個(gè)數(shù)據(jù)位、無校驗(yàn)、1個(gè)停止位流控關(guān)掉。這個(gè)組合在大多數(shù)串口設(shè)備和Linux控制臺(tái)場(chǎng)景下都是默認(rèn)值。調(diào)試時(shí)第一件事就是確認(rèn)兩端的參數(shù)一致否則收到的數(shù)據(jù)大概率是亂碼。參數(shù)常見取值說明波特率9600/115200/460800每秒傳輸?shù)腷it數(shù)量?jī)啥吮仨氁恢聰?shù)據(jù)位8少數(shù)用7每幀有效數(shù)據(jù)位數(shù)停止位1或2每幀結(jié)束后的間隔位校驗(yàn)位N無/E偶/O奇用于基礎(chǔ)錯(cuò)誤檢測(cè)流控?zé)o/硬件RTS-CTS/軟件XON-XOFF調(diào)試時(shí)建議先關(guān)閉如果數(shù)據(jù)鏈路不通比如換了USB轉(zhuǎn)串口線、宿主機(jī)的COM口號(hào)變了這些參數(shù)配置再正確也白搭。所以先要確認(rèn)設(shè)備管理器里能看到正確的COM號(hào)再把串口對(duì)的兩端搞清楚。2.2 Windows與VMware Linux之間打通串口的三種思路宿主機(jī)Windows和VMware里的Linux通信本質(zhì)上是要把虛擬機(jī)的串口設(shè)備映射到宿主機(jī)的一個(gè)“可以被程序打開的串口”上。常見的方案有三種物理串口直連VMware直接把宿主機(jī)的物理串口比如USB轉(zhuǎn)串口的COM3分配給虛擬機(jī)。缺點(diǎn)是宿主機(jī)自己就占用了這個(gè)物理串口沒法用Tcl程序同時(shí)打開調(diào)試對(duì)象只有一個(gè)口時(shí)基本不可行。VMware命名管道在VMware的虛擬機(jī)設(shè)置里添加串行端口選擇“使用命名管道”比如\\.\pipe\com_1讓宿主機(jī)程序通過管道和虛擬機(jī)通信。缺點(diǎn)是Windows下的命名管道需要CreateFile方式訪問Tcl原生open方式對(duì)\\\\.\\pipe\\路徑兼容性不穩(wěn)定折騰代價(jià)有點(diǎn)高。虛擬串口對(duì)com0com方案在宿主機(jī)安裝一個(gè)虛擬串口驅(qū)動(dòng)創(chuàng)建一對(duì)互聯(lián)的虛擬COM口比如CNCA0和CNCB0。把其中一個(gè)分配給VMware作為物理串口另一個(gè)留給宿主機(jī)的Tcl程序打開。兩邊數(shù)據(jù)通過驅(qū)動(dòng)內(nèi)部自動(dòng)轉(zhuǎn)發(fā)Tcl只要操作一個(gè)普通串口文件名就行。我最終采用的是第三種方案穩(wěn)定且直觀。com0com是免費(fèi)開源的虛擬串口驅(qū)動(dòng)專門解決Windows下缺少物理串口的問題創(chuàng)建的COM對(duì)在驅(qū)動(dòng)層面完成數(shù)據(jù)轉(zhuǎn)發(fā)對(duì)應(yīng)用層完全透明。2.3 com0com配置步驟安裝完com0com后用管理員權(quán)限打開它的配置界面。默認(rèn)會(huì)創(chuàng)建一個(gè)CNCA0 - CNCB0的串口對(duì)但VMware的串口選單通常需要識(shí)別到類似COM3、COM4這樣的名稱所以我習(xí)慣先把端口改名把CNCA0改成COM3CNCB0改成COM4。操作路徑是選中端口對(duì)后在參數(shù)里修改端口名然后應(yīng)用即可。之后在VMware里打開虛擬機(jī)設(shè)置添加串行端口類型選擇“使用物理串行端口”然后在下拉列表里選COM4。關(guān)鍵一步是勾選“打開電源時(shí)連接”否則虛擬機(jī)啟動(dòng)后串口不會(huì)自動(dòng)掛載。Linux虛擬機(jī)里這個(gè)串口默認(rèn)對(duì)應(yīng)/dev/ttyS0相當(dāng)于物理COM1。如果VMware添加的是第二個(gè)串口則可能是/dev/ttyS1可以用dmesg | grep tty確認(rèn)。3. Tcl/Tk串口收發(fā)核心實(shí)現(xiàn)與完整腳本3.1 打開串口的正確姿勢(shì)open fconfigureTcl在Windows下打開串口其實(shí)很直白用的就是open語句但有幾個(gè)細(xì)節(jié)必須注意。第一串口文件名建議用\\\\.\\COM3這種格式尤其是COM口編號(hào)大于等于10時(shí)系統(tǒng)要求必須帶\\\\.\\前綴否則打不開。第二打開后要立刻用fconfigure設(shè)置串口參數(shù)設(shè)置前不要寫任何數(shù)據(jù)。第三必須把-blocking設(shè)為0否則接收數(shù)據(jù)時(shí)read一旦等不到數(shù)據(jù)就會(huì)掛死整個(gè)界面。核心配置代碼是這段set fd [open \\\\.\\COM3 r] fconfigure $fd -mode 115200,n,8,1 -blocking 0 -buffering none -translation binary-mode的格式是“波特率,校驗(yàn)位,數(shù)據(jù)位,停止位”n表示無校驗(yàn)8表示8個(gè)數(shù)據(jù)位1表示1個(gè)停止位。-translation binary是為了避免Tcl把\r\n自動(dòng)轉(zhuǎn)換因?yàn)榇跀?shù)據(jù)里可能包含任意二進(jìn)制字節(jié)轉(zhuǎn)換會(huì)破壞原始數(shù)據(jù)。3.2 fileevent事件驅(qū)動(dòng)的接收邏輯串口數(shù)據(jù)是異步到達(dá)的如果寫一個(gè)死循環(huán)去輪詢界面會(huì)卡死。Tcl的解決方式是用fileevent注冊(cè)一個(gè)可讀回調(diào)一旦串口有數(shù)據(jù)到達(dá)Tcl的事件循環(huán)會(huì)自動(dòng)調(diào)用對(duì)應(yīng)的proc。proc on_readable {chan} { if {[eof $chan]} { catch {close $chan} set ::fd return } set data [read $chan] if {$data eq } { return } .txt.receive insert end $data .txt.receive see end } fileevent $fd readable [list on_readable $fd]這里有個(gè)細(xì)節(jié)read $chan默認(rèn)會(huì)讀到所有可用的數(shù)據(jù)。因?yàn)榇诒辉O(shè)置為非阻塞模式所以讀不到數(shù)據(jù)時(shí)read返回空字符串不會(huì)卡住界面。每次讀到數(shù)據(jù)后我把它追加到Tk的text控件里see end會(huì)自動(dòng)滾動(dòng)到底部這樣相當(dāng)于一個(gè)實(shí)時(shí)滾動(dòng)監(jiān)控窗口。3.3 完整Moni腳本界面與收發(fā)一體界面我做了上下三塊頂部是串口配置區(qū)包含端口號(hào)、波特率、參數(shù)選擇和打開/關(guān)閉按鈕中間是收發(fā)監(jiān)控區(qū)一個(gè)帶滾動(dòng)條的text控件底部是發(fā)送區(qū)一個(gè)輸入框和發(fā)送按鈕支持文本發(fā)送和Hex發(fā)送。完整腳本可以復(fù)制保存為moni.tcl然后執(zhí)行tclsh moni.tcl啟動(dòng)。package require Tk set ::fd proc refresh_ports {} { .fr.port configure values [split [exec reg query HKEY_LOCAL_MACHINE\\HARDWARE\\DEVICEMAP\\SERIALCOMM] \n] } proc open_serial {} { if {[info exists ::fd] $::fd ne } { close $::fd set ::fd .btn.open configure -text 打開串口 return } set port [.fr.port get] set baud [.fr.baud get] set mode [.fr.mode get] set err [catch {open \\\\.\\$port r} fd] if {$err} { tk_messageBox -icon error -message 打開$port失敗$fd return } fconfigure $fd -mode $baud,$mode -blocking 0 -buffering none -translation binary fileevent $fd readable [list on_readable $fd] set ::fd $fd .btn.open configure -text 關(guān)閉串口 } proc on_readable {chan} { if {[eof $chan]} { catch {close $chan} set ::fd .btn.open configure -text 打開串口 return } set data [read $chan] if {$data eq } { return } .txt.receive insert end $data .txt.receive see end } proc send_text {} { if {$::fd eq } { return } set msg [.fr.send get] if {$msg eq } { return } puts -nonewline $::fd $msg .fr.send delete 0 end } proc send_hex {} { if {$::fd eq } { return } set hexstr [.fr.send get] regsub -all {[\s]} $hexstr {} hexstr if {[string length $hexstr] 0 || [string length $hexstr] % 2 ! 0} { tk_messageBox -icon warning -message Hex格式錯(cuò)誤需偶數(shù)位 return } set bin [binary format H* $hexstr] puts -nonewline $::fd $bin .fr.send delete 0 end } proc clear_recv {} { .txt.receive delete 1.0 end } grid [ttk::frame .fr] -sticky we grid [ttk::label .fr.l1 -text 串口] -row 0 -column 0 grid [ttk::combobox .fr.port -width 10] -row 0 -column 1 -padx 4 grid [ttk::label .fr.l2 -text 波特率] -row 0 -column 2 grid [ttk::combobox .fr.baud -values {9600 19200 38400 57600 115200} -width 10] -row 0 -column 3 -padx 4 grid [ttk::label .fr.l3 -text 參數(shù)] -row 0 -column 4 grid [ttk::combobox .fr.mode -values {n,8,1 e,8,1 o,8,1 n,7,1} -width 8] -row 0 -column 5 -padx 4 grid [ttk::button .btn.open -text 打開串口 -command open_serial] -row 0 -column 6 -padx 8 grid [ttk::button .btn.clear -text 清空 -command clear_recv] -row 0 -column 7 grid [ttk::frame .mid -padding 4] -sticky nsew grid [text .txt.receive -width 80 -height 20 -wrap word] -row 0 -column 0 grid [ttk::scrollbar .mid.scroll -command {.txt.receive yview}] -row 0 -column 1 -sticky ns .txt.receive configure -yscrollcommand {.mid.scroll set} grid [ttk::frame .bottom -padding 4] -sticky we grid [ttk::entry .fr.send -width 60] -row 0 -column 0 grid [ttk::button .btn.send -text 文本發(fā)送 -command send_text] -row 0 -column 1 -padx 4 grid [ttk::button .btn.sendhex -text Hex發(fā)送 -command send_hex] -row 0 -column 2 .fr.port set COM3 .fr.baud set 115200 .fr.mode set n,8,1注意代碼里的.fr.send這個(gè)entry雖然放在名為bottom的frame里但名字前綴是.fr這是Tk控件路徑命名的靈活性不影響實(shí)際功能。實(shí)際如果要更嚴(yán)謹(jǐn)應(yīng)該單獨(dú)定義一個(gè).bottom.send避免和頂部frame混用不過這里僅為簡(jiǎn)化解說請(qǐng)按自己的習(xí)慣調(diào)整。實(shí)際使用中refresh_ports那段exec reg query不一定每次都能解析出干凈的端口列表因?yàn)閞eg query輸出包含表頭和多行信息。更穩(wěn)妥的做法是直接從輸出中提取“COMx”關(guān)鍵字。3.4 發(fā)送數(shù)據(jù)時(shí)容易忽略的細(xì)節(jié)串口發(fā)送本身不復(fù)雜puts -nonewline就能把字符串寫入串口注意一定要用-nonewline否則Tcl會(huì)自動(dòng)追加一個(gè)換行符這個(gè)換行符不是每次都想發(fā)的。Hex發(fā)送則用binary format H*把十六進(jìn)制字符串轉(zhuǎn)成二進(jìn)制數(shù)據(jù)再寫入串口這在調(diào)試二進(jìn)制協(xié)議、Modbus報(bào)文或AT指令時(shí)非常常用。比如發(fā)送41 42 43實(shí)際設(shè)備會(huì)收到“ABC”三個(gè)字節(jié)。這個(gè)功能讓我省去了在C程序里反復(fù)修改并編譯的麻煩直接在Moni界面里拼接數(shù)據(jù)包測(cè)試就完事了。4. 實(shí)測(cè)記錄Windows宿主機(jī)與VMware Linux的串口聯(lián)調(diào)全流程4.1 從開始到聯(lián)通的每一步操作虛擬機(jī)內(nèi)Linux側(cè)我先用命令驗(yàn)證串口是否存在并可用。假設(shè)Linux里的串口對(duì)應(yīng)/dev/ttyS0修改權(quán)限后設(shè)置參數(shù)并做一次發(fā)送測(cè)試sudo chmod 666 /dev/ttyS0 stty -F /dev/ttyS0 115200 raw echo hello from linux /dev/ttyS0然后在Windows宿主機(jī)打開Moni配置好COM3、115200、n,8,1點(diǎn)擊“打開串口”。此時(shí)把Linux側(cè)的cat掛上cat /dev/ttyS0再在Moni里發(fā)送“hello from windows”如果鏈路正常Linux的cat終端里會(huì)打印出這行字。反過來在Linux里echo寫入串口Moni的接收區(qū)也會(huì)立刻顯示。這一步通了整個(gè)串口通信鏈路就打通了。如果你的目標(biāo)是把Linux的啟動(dòng)日志通過串口輸出則可以在內(nèi)核啟動(dòng)參數(shù)里加consolettyS0,115200關(guān)于具體GRUB配置這里不展開思路是一樣的。4.2 最容易把時(shí)間耗光的問題清單我把自己踩過的坑和身邊朋友問得最多的問題整理成一張速查表按優(yōu)先級(jí)排好照著排查基本能解決九成的問題?,F(xiàn)象可能原因處理方法打開串口報(bào)錯(cuò)提示被占用其他軟件已打開該COM口關(guān)閉串口助手、調(diào)試器或換個(gè)空閑COM號(hào)VMware里沒有想要的COM口可選com0com端口名編過號(hào)但未被系統(tǒng)識(shí)別確認(rèn)com0com創(chuàng)建的是“COM4”而非“CNCB0”必要時(shí)改端口名后重啟VMware能打開串口但Linux側(cè)收不到數(shù)據(jù)虛擬機(jī)串口未連接或流控未關(guān)閉確認(rèn)VMware設(shè)置里勾選“打開電源時(shí)連接”關(guān)閉硬件流控?cái)?shù)據(jù)亂碼兩端波特率不一致檢查Moni和Linux中stty配置是否完全一致接收區(qū)一直不顯示數(shù)據(jù)-blocking未設(shè)置為0或沒有注冊(cè)fileevent檢查fconfigure配置和fileevent綁定代碼Linux下cat /dev/ttyS0無輸出虛擬串口分配給VMware的物理串口號(hào)與Moni操作的口是同一對(duì)確認(rèn)com0com的CNCA0對(duì)應(yīng)COM3、CNCB0對(duì)應(yīng)COM4別弄反方向4.3 幾個(gè)提升使用體驗(yàn)的小改動(dòng)基礎(chǔ)收發(fā)做好之后我陸續(xù)給Moni加了一些順手的功能。接收區(qū)加時(shí)間戳可以在on_readable里用clock format [clock seconds] -format %H:%M:%S拼接在數(shù)據(jù)前面。這個(gè)功能抓多層協(xié)議交互時(shí)特別有用能看清每段數(shù)據(jù)的到達(dá)順序和間隔。接收數(shù)據(jù)保存成日志我也經(jīng)常用。加一個(gè)“保存日志”按鈕用tk_getSaveFile彈出保存路徑然后遍歷text控件內(nèi)容寫入文件。這樣聯(lián)調(diào)一晚上的數(shù)據(jù)也能完整留檔方便第二天繼續(xù)分析。還有一個(gè)容易被忽視的點(diǎn)如果串口對(duì)里的數(shù)據(jù)流量很大Tk的text控件會(huì)越來越卡??梢栽趏n_readable里加一個(gè)判斷當(dāng)接收區(qū)超過比如100KB時(shí)自動(dòng)清掉一部分舊數(shù)據(jù)只保留最近的內(nèi)容。這是真實(shí)使用中才會(huì)意識(shí)到的優(yōu)化點(diǎn)文檔里一般不會(huì)提。根據(jù)我個(gè)人的實(shí)際體會(huì)用Tcl/Tk做串口監(jiān)控這個(gè)方向雖然沒有Python生態(tài)那么熱鬧但在“快速搞定、免編譯、跨平臺(tái)”這件事上確實(shí)很能打。Moni這個(gè)單文件腳本后來被我擴(kuò)展出了自動(dòng)回復(fù)、周期輪詢、斷言匹配等功能慢慢變成了一個(gè)稱手的串口調(diào)試基座。如果你也有宿主機(jī)Windows和VMware Linux之間的串口通信需求值得花一個(gè)小時(shí)把這條路走通之后所有串口聯(lián)調(diào)工作都會(huì)順暢很多。本文還有配套的精品資源點(diǎn)擊獲取