2024年5月27日 星期一

7.VB.NET 進階篇 筆記 - 串列埠(Serial Port)

VB.NET SerialPort 串列埠筆記(基礎篇)

VB.NET SerialPort 串列埠 筆記(基礎篇)

串列埠通訊常用於桌面程式與外部設備連線,例如條碼掃描器、電子秤、PLC、感測器、儀表設備、測試治具與工控模組。VB.NET 可透過 System.IO.Ports.SerialPort 類別完成 COM 埠掃描、參數設定、連線開關、資料送出與資料接收。

SerialPort 的重點不是只有「打開 COM 埠」。完整流程通常包含:確認通訊參數、選擇埠號、開啟連線、送出資料、接收資料、跨執行緒更新畫面、關閉並釋放資源。

串列埠通訊的整體流程

先理解資料怎麼進出

SerialPort 可以想成程式和外部設備之間的一條資料通道。程式寫入資料後,資料會送到設備;設備回傳資料後,程式再從接收緩衝區讀出。

  • 程式端:設定 PortNameBaudRate 等參數,並呼叫 Open 建立連線。
  • 設備端:依照相同通訊參數接收或回傳資料。
  • 送出資料:使用 WriteWriteLine
  • 接收資料:常見做法是處理 DataReceived 事件,再用讀取方法取出緩衝區內容。

SerialPort 基礎流程:

  1. 取得可用 COM 埠清單。
  2. 選擇埠號並設定通訊參數。
  3. 呼叫 Open 開啟串列埠。
  4. 使用 WriteWriteLine 送出資料。
  5. 使用 DataReceived 接收資料。
  6. 透過 BeginInvoke 回到 UI 執行緒更新畫面。
  7. 結束時呼叫 CloseDispose 釋放資源。

通訊參數整理

串列埠能否正確通訊,第一步是確認雙方參數一致。設備文件常出現 9600, 8, N, 1 這類寫法,代表鮑率 9600、資料位元 8、Parity=None、StopBits=One。

參數 用途 常見設定
PortName 指定使用哪一個 COM 埠。 COM3COM4COM7
BaudRate 傳輸速率。雙方不一致時常出現亂碼。 960019200115200
DataBits 每個資料單位的位元數。 8 最常見
Parity 同位檢查方式。 Parity.NoneParity.OddParity.Even
StopBits 每個資料單位結尾的停止位元。 StopBits.OneStopBits.Two
Handshake 流量控制方式。 Handshake.NoneHandshake.XOnXOffHandshake.RequestToSend
NewLine WriteLineReadLine 使用的換行結尾。 vbCrLfvbLf、設備指定字元
ReadTimeout / WriteTimeout 避免讀寫作業無限制等待。 1000 毫秒、2000 毫秒

參數不一致的常見現象

  • 鮑率錯誤時,文字資料常會變成亂碼。
  • 資料位元、停止位元或同位檢查不一致時,資料可能錯位、遺失或完全無法解析。
  • 設備回傳二進位資料時,若用文字方式讀取,畫面可能顯示異常符號。

Windows Forms 表單配置

完整測試畫面建議主控項

  • ComboBox1:顯示可用 COM 埠。
  • Button1:重新整理埠號清單。
  • Button2:開啟串列埠。
  • Button3:關閉串列埠。
  • TextBox1:輸入要送出的文字。
  • Button4:送出文字。
  • TextBox2:顯示接收資料,建議設定 Multiline=TrueScrollBars=Vertical
  • Label1:顯示連線狀態。

基礎流程與範例實作

場景一:掃描可用 COM 埠

埠號不建議直接寫死。設備插拔後,COM 埠可能改變,因此通常會先透過 SerialPort.GetPortNames() 取得目前系統可用的埠號。

需要的主控項
  • ComboBox1
  • Button1
  • Label1
範例程式碼
VB.NET / Windows Forms
Imports System.IO.Ports

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        LoadPortList()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        LoadPortList()
    End Sub

    Private Sub LoadPortList()
        Dim portNames() As String = SerialPort.GetPortNames()
        Array.Sort(portNames)

        ComboBox1.Items.Clear()
        ComboBox1.Items.AddRange(portNames)

        If ComboBox1.Items.Count > 0 Then
            ComboBox1.SelectedIndex = 0
            Label1.Text = String.Format("找到 {0} 個通訊埠", ComboBox1.Items.Count)
        Else
            Label1.Text = "未找到可用通訊埠"
        End If
    End Sub
End Class
畫面結果概念
ComboBox1: COM3 COM4 COM7 Label1: 找到 3 個通訊埠
邏輯解析
  • GetPortNames() 會取得目前系統可辨識的 COM 埠。
  • Array.Sort 讓顯示順序較固定。
  • 重新整理按鈕適合設備插拔後再次掃描。

場景二:建立 SerialPort 並開啟 / 關閉連線

通訊開始前必須呼叫 Open。通訊結束後應呼叫 Close,避免埠號被目前程式占住,導致其他程式或下次連線無法使用。

需要的主控項
  • ComboBox1
  • Button1:重新整理埠號。
  • Button2:開啟串列埠。
  • Button3:關閉串列埠。
  • Label1
範例程式碼
VB.NET / Windows Forms
Imports System.IO.Ports
Imports System.IO

Public Class Form1
    Private devicePort As SerialPort

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        devicePort = New SerialPort()
        ConfigureSerialPort()
        LoadPortList()
        Label1.Text = "狀態:尚未連線"
    End Sub

    Private Sub ConfigureSerialPort()
        devicePort.BaudRate = 9600
        devicePort.DataBits = 8
        devicePort.Parity = Parity.None
        devicePort.StopBits = StopBits.One
        devicePort.Handshake = Handshake.None
        devicePort.NewLine = vbCrLf
        devicePort.ReadTimeout = 1000
        devicePort.WriteTimeout = 1000
    End Sub

    Private Sub LoadPortList()
        Dim portNames() As String = SerialPort.GetPortNames()
        Array.Sort(portNames)

        ComboBox1.Items.Clear()
        ComboBox1.Items.AddRange(portNames)

        If ComboBox1.Items.Count > 0 Then
            ComboBox1.SelectedIndex = 0
        End If
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        LoadPortList()
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        Try
            If ComboBox1.SelectedItem Is Nothing Then
                MessageBox.Show("尚未選取通訊埠。")
                Exit Sub
            End If

            If devicePort.IsOpen Then
                MessageBox.Show("串列埠已經開啟。")
                Exit Sub
            End If

            ConfigureSerialPort()
            devicePort.PortName = ComboBox1.SelectedItem.ToString()
            devicePort.Open()
            Label1.Text = String.Format("狀態:已連線 {0}", devicePort.PortName)

        Catch ex As UnauthorizedAccessException
            Label1.Text = "狀態:埠號被占用"
        Catch ex As IOException
            Label1.Text = "狀態:連線失敗,設備可能已拔除"
        Catch ex As Exception
            Label1.Text = String.Format("狀態:開啟失敗 - {0}", ex.Message)
        End Try
    End Sub

    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
        If devicePort IsNot Nothing AndAlso devicePort.IsOpen Then
            devicePort.Close()
            Label1.Text = "狀態:已關閉"
        End If
    End Sub
End Class
邏輯解析
  • ConfigureSerialPort 統一設定通訊參數,避免開啟前漏設。
  • UnauthorizedAccessException 常見於 COM 埠被其他程式占用。
  • IOException 常見於設備拔除或連線狀態異常。
  • 關閉前先檢查 IsOpen,避免重複關閉。

場景三:送出文字命令

許多設備以文字命令控制,例如查詢狀態、清除資料、啟動量測或讀取重量。若設備要求每筆指令以換行結尾,可使用 WriteLine;若設備不需要換行,應改用 Write

需要的主控項
  • ComboBox1
  • Button2:開啟串列埠。
  • TextBox1:輸入要送出的文字。
  • Button4:送出文字。
  • Label1
範例程式碼
VB.NET / Windows Forms
Imports System.IO.Ports

Public Class Form1
    Private devicePort As New SerialPort()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        devicePort.BaudRate = 9600
        devicePort.DataBits = 8
        devicePort.Parity = Parity.None
        devicePort.StopBits = StopBits.One
        devicePort.NewLine = vbCrLf
        Label1.Text = "狀態:尚未連線"
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        If ComboBox1.SelectedItem Is Nothing Then
            MessageBox.Show("請先選擇通訊埠。")
            Exit Sub
        End If

        If Not devicePort.IsOpen Then
            devicePort.PortName = ComboBox1.SelectedItem.ToString()
            devicePort.Open()
            Label1.Text = String.Format("狀態:已連線 {0}", devicePort.PortName)
        End If
    End Sub

    Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
        If Not devicePort.IsOpen Then
            MessageBox.Show("請先開啟串列埠。")
            Exit Sub
        End If

        Dim commandText As String = TextBox1.Text.Trim()

        If commandText = String.Empty Then
            MessageBox.Show("送出內容不可為空白。")
            Exit Sub
        End If

        devicePort.WriteLine(commandText)
        Label1.Text = String.Format("已送出:{0}", commandText)
    End Sub
End Class
邏輯解析
  • 送出前先檢查 IsOpen,避免未連線時寫入。
  • WriteLine 會自動附加 NewLine 指定的結尾字元。
  • 若設備協議要求不加結尾符號,應改用 devicePort.Write(commandText)

場景四:使用 DataReceived 接收資料並更新畫面

DataReceived 會在接收緩衝區有資料時觸發,但它不是在 UI 執行緒執行。因此事件內不能直接修改 TextBox2,必須透過 BeginInvoke 回到表單執行緒。

需要的主控項
  • TextBox2:顯示接收資料。
  • Label1:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.IO.Ports
Imports System.IO

Public Class Form1
    Private WithEvents devicePort As SerialPort
    Private isClosing As Boolean = False

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        devicePort = New SerialPort()
        devicePort.BaudRate = 9600
        devicePort.DataBits = 8
        devicePort.Parity = Parity.None
        devicePort.StopBits = StopBits.One
        devicePort.NewLine = vbCrLf

        TextBox2.Multiline = True
        TextBox2.ScrollBars = ScrollBars.Vertical
        Label1.Text = "狀態:等待接收"
    End Sub

    Private Sub devicePort_DataReceived(sender As Object,
                                         e As SerialDataReceivedEventArgs) Handles devicePort.DataReceived
        Dim incomingText As String = String.Empty

        Try
            incomingText = devicePort.ReadExisting()
        Catch ex As InvalidOperationException
            Return
        Catch ex As IOException
            Return
        End Try

        If incomingText.Length = 0 Then
            Return
        End If

        If isClosing OrElse Me.IsDisposed OrElse Not Me.IsHandleCreated Then
            Return
        End If

        Me.BeginInvoke(New MethodInvoker(Sub()
                                             TextBox2.AppendText(incomingText)
                                         End Sub))
    End Sub

    Private Sub Form1_FormClosing(sender As Object,
                                  e As FormClosingEventArgs) Handles MyBase.FormClosing
        isClosing = True

        If devicePort IsNot Nothing Then
            If devicePort.IsOpen Then
                devicePort.Close()
            End If

            devicePort.Dispose()
        End If
    End Sub
End Class
畫面輸出概念(TextBox2)
ST,GS,00125.50 ST,GS,00125.45 ST,GS,00125.52
邏輯解析
  • WithEvents 搭配 Handles devicePort.DataReceived 接收資料抵達事件。
  • ReadExisting() 會讀出目前緩衝區中可取得的文字。
  • DataReceived 不是 UI 執行緒,更新控制項需使用 BeginInvoke
  • isClosing 可降低表單關閉時仍嘗試更新畫面的風險。

常見讀寫方法與事件

成員 用途 適用情境
Write 送出指定文字或位元組,不自動加結尾。 設備協議要求精準控制送出內容時。
WriteLine 送出文字並附加 NewLine 每筆指令需要換行結尾時。
Read 讀取指定長度的字元或位元組。 封包長度固定或處理二進位資料時。
ReadLine 讀到 NewLine 為止。 設備每筆資料都有固定結尾符號時。
ReadExisting 讀出目前緩衝區可取得的文字。 簡單文字設備、測試與除錯。
DataReceived 接收緩衝區有資料時觸發。 資料抵達時立即處理,不想用輪詢時。

重要提醒:DataReceived 代表「有資料進入緩衝區」,不保證剛好是一整筆完整訊息。若設備資料有固定結尾符號,通常需要累積緩衝內容,再依結尾符號切出完整資料。

完整實作範例

以下範例整合埠號掃描、開啟、關閉、送出與接收,適合作為 Windows Forms SerialPort 的最小完整測試版本。

需要的主控項
  • ComboBox1
  • Button1:重新整理埠號。
  • Button2:開啟串列埠。
  • Button3:關閉串列埠。
  • TextBox1:輸入要送出的文字。
  • Button4:送出文字。
  • TextBox2:顯示接收資料。
  • Label1:顯示狀態。
VB.NET / Windows Forms
Imports System.IO.Ports
Imports System.IO

Public Class Form1
    Private WithEvents devicePort As SerialPort
    Private isClosing As Boolean = False

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        devicePort = New SerialPort()
        ConfigureSerialPort()

        TextBox2.Multiline = True
        TextBox2.ScrollBars = ScrollBars.Vertical

        LoadPortList()
        Label1.Text = "狀態:尚未連線"
    End Sub

    Private Sub ConfigureSerialPort()
        devicePort.BaudRate = 9600
        devicePort.DataBits = 8
        devicePort.Parity = Parity.None
        devicePort.StopBits = StopBits.One
        devicePort.Handshake = Handshake.None
        devicePort.NewLine = vbCrLf
        devicePort.ReadTimeout = 1000
        devicePort.WriteTimeout = 1000
    End Sub

    Private Sub LoadPortList()
        Dim portNames() As String = SerialPort.GetPortNames()
        Array.Sort(portNames)

        ComboBox1.Items.Clear()
        ComboBox1.Items.AddRange(portNames)

        If ComboBox1.Items.Count > 0 Then
            ComboBox1.SelectedIndex = 0
        End If
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        LoadPortList()
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        Try
            If ComboBox1.SelectedItem Is Nothing Then
                MessageBox.Show("沒有可用的通訊埠。")
                Exit Sub
            End If

            If devicePort.IsOpen Then
                MessageBox.Show("串列埠已經開啟。")
                Exit Sub
            End If

            ConfigureSerialPort()
            devicePort.PortName = ComboBox1.SelectedItem.ToString()
            devicePort.Open()
            Label1.Text = String.Format("狀態:已連線 {0}", devicePort.PortName)

        Catch ex As UnauthorizedAccessException
            Label1.Text = "狀態:埠號被占用"
            MessageBox.Show("通訊埠可能已被其他程式使用。")
        Catch ex As IOException
            Label1.Text = "狀態:連線失敗"
            MessageBox.Show("設備可能已拔除或通訊埠狀態異常。")
        Catch ex As Exception
            Label1.Text = "狀態:連線失敗"
            MessageBox.Show(String.Format("開啟串列埠失敗:{0}", ex.Message))
        End Try
    End Sub

    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
        CloseSerialPort()
    End Sub

    Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
        If devicePort Is Nothing OrElse Not devicePort.IsOpen Then
            MessageBox.Show("請先完成串列埠連線。")
            Exit Sub
        End If

        Dim sendText As String = TextBox1.Text.Trim()

        If sendText = String.Empty Then
            MessageBox.Show("請輸入要送出的內容。")
            Exit Sub
        End If

        Try
            devicePort.WriteLine(sendText)
            Label1.Text = String.Format("已送出:{0}", sendText)
        Catch ex As TimeoutException
            Label1.Text = "狀態:送出逾時"
        Catch ex As InvalidOperationException
            Label1.Text = "狀態:串列埠尚未開啟"
        Catch ex As IOException
            Label1.Text = "狀態:送出失敗,連線可能中斷"
        End Try
    End Sub

    Private Sub devicePort_DataReceived(sender As Object,
                                         e As SerialDataReceivedEventArgs) Handles devicePort.DataReceived
        Dim incomingText As String = String.Empty

        Try
            incomingText = devicePort.ReadExisting()
        Catch ex As InvalidOperationException
            Return
        Catch ex As IOException
            Return
        End Try

        If incomingText.Length = 0 Then
            Return
        End If

        If isClosing OrElse Me.IsDisposed OrElse Not Me.IsHandleCreated Then
            Return
        End If

        Me.BeginInvoke(New MethodInvoker(Sub()
                                             TextBox2.AppendText(incomingText)
                                         End Sub))
    End Sub

    Private Sub CloseSerialPort()
        If devicePort Is Nothing Then
            Return
        End If

        Try
            If devicePort.IsOpen Then
                devicePort.Close()
            End If

            Label1.Text = "狀態:已關閉"
        Catch ex As IOException
            Label1.Text = "狀態:關閉時發生通訊錯誤"
        Catch ex As Exception
            Label1.Text = String.Format("狀態:關閉失敗 - {0}", ex.Message)
        End Try
    End Sub

    Private Sub Form1_FormClosing(sender As Object,
                                  e As FormClosingEventArgs) Handles MyBase.FormClosing
        isClosing = True

        If devicePort IsNot Nothing Then
            If devicePort.IsOpen Then
                devicePort.Close()
            End If

            devicePort.Dispose()
        End If
    End Sub
End Class
操作流程概念
1. 按「重新整理」取得可用 COM 埠 2. 選擇埠號後按「開啟串列埠」 3. 在 TextBox1 輸入命令後按「送出文字」 4. 接收資料會持續追加到 TextBox2 5. 結束程式時自動關閉並釋放 SerialPort

常見問題與排查方向

埠號開不起來

  • COM 埠已被其他程式占用。
  • 設備已拔除,但畫面仍保留舊埠號。
  • USB 轉串列埠驅動異常。
  • 同一程式中重複開啟尚未關閉的 SerialPort。

資料出現亂碼

  • 鮑率不一致。
  • 資料位元、停止位元或同位檢查設定不一致。
  • 設備回傳二進位資料,但程式使用文字方式讀取。
  • 字元編碼與設備輸出格式不一致。

跨執行緒更新畫面錯誤

  • DataReceived 不是在 UI 執行緒中執行。
  • 不能在 DataReceived 內直接修改 TextBoxLabel 等控制項。
  • 更新畫面時應使用 InvokeBeginInvoke

重點整理

  1. SerialPort 是 VB.NET 與 COM 埠設備通訊的主要類別。
  2. 通訊前必須確認 PortNameBaudRateDataBitsParityStopBits 是否與設備一致。
  3. 表單程式常見流程是:掃描埠號 → 開啟 → 送出 → 接收 → 關閉。
  4. WriteLine 會附加 NewLine,是否適用需依設備協議判斷。
  5. DataReceived 不是 UI 執行緒,更新畫面必須使用 InvokeBeginInvoke
  6. DataReceived 不保證一次收到完整資料,正式協議通常需要額外做緩衝與封包解析。
  7. 程式結束時應關閉並釋放 SerialPort,避免通訊埠被占用。