串口通讯
开发专题 · System.IO.Ports.SerialPort(.NET Framework 4.8)
本页说明在 .NET Framework 4.8 窗体(或控制台)项目中用 System.IO.Ports.SerialPort 枚举端口、开关串口、收发文本/字节。目标框架以 LCode 默认的 .NET 4.8 为准;示例写法面向 C# 5.0 与 VB.NET(勿用 C# 6+ 语法如字符串插值、nameof)。操作文案以 LCode 中文界面为准。UI 线程与后台回调见 异步、同步与多线程。
选用哪种方式
| 方式 | 命名空间 / 程序集 | 适用 |
|---|---|---|
SerialPort(推荐) |
System.IO.Ports(程序集 System) |
RS-232 / USB 转串口等常见串口设备。框架自带,无需第三方包 |
| 工具箱组件「串行端口」 | 同上;设计器组件名 SerialPort |
拖到窗体后可在属性窗口设 PortName、BaudRate 等;底层仍是 SerialPort |
| 第三方串口库 | 另引 DLL | 需特殊协议栈时再考虑;本页不展开。LCode 主界面 NuGet暂不支持,待更新完善 |
在 LCode 中使用
按 窗体应用入门 建好「Windows 应用程序」后,任选其一:
- 代码创建(常用):窗体项目默认已引用
System,直接using System.IO.Ports;/Imports System.IO.Ports即可。一般不必再「添加引用」。 - 工具箱拖入:打开窗体设计器 → 工具箱组件区找到串行端口(英文类型名
SerialPort)→ 拖到窗体。组件出现在窗体下方托盘;在属性窗口设置端口名、波特率等。 - 若编译报找不到
System.IO.Ports:项目 → 添加引用 → .NET集合浏览器,确认勾选System→ 确定。详见 引用 DLL。 - 菜单 编译 → 编译解决方案(F8),确认无命名空间错误。
提示:本机须有可用 COM 口(物理串口或 USB 转串口驱动)。枚举结果因驱动与权限而异;虚拟串口对(如 com0com)便于无硬件联调。
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
PortName |
string |
端口名,如 COM1、COM3(须与设备管理器一致) |
BaudRate |
int |
波特率,常见 9600、115200;须与对端一致 |
DataBits |
int |
数据位,多为 8 |
Parity |
Parity |
校验:None / Odd / Even / Mark / Space |
StopBits |
StopBits |
停止位:One / Two / OnePointFive |
Handshake |
Handshake |
流控:None、XOnXOff、RequestToSend 等 |
Encoding |
Encoding |
文本读写编码;默认常为 ASCII。中文设备可按协议设 Encoding.GetEncoding("GB2312") 等 |
ReadTimeout / WriteTimeout |
int |
读写超时(毫秒);-1 表示无限等待(慎用,易卡住 UI) |
IsOpen |
bool |
是否已打开(只读) |
BytesToRead / BytesToWrite |
int |
接收/发送缓冲区中尚未处理的字节数 |
DtrEnable / RtsEnable |
bool |
部分设备上电/握手依赖 DTR/RTS,按硬件手册设置 |
NewLine |
string |
ReadLine / WriteLine 使用的行结束符,默认 \n |
常用方法
| 方法 | 说明 |
|---|---|
GetPortNames()(静态) |
返回本机当前串口名字符串数组 |
Open() / Close() |
打开 / 关闭端口。打开前须设好 PortName 与波特率等参数 |
Write(string) / WriteLine(string) |
按 Encoding 写文本;WriteLine 追加 NewLine |
Write(byte[], int, int) |
写原始字节(协议帧常用) |
Read(byte[], int, int) / ReadExisting() / ReadLine() |
读字节或已缓冲文本;无数据且设了超时会抛 TimeoutException |
DiscardInBuffer() / DiscardOutBuffer() |
清空收/发缓冲区 |
Dispose() |
释放底层句柄;窗体关闭时务必关闭并释放,避免端口被占用 |
常用事件
| 事件 | 说明 |
|---|---|
DataReceived |
接收缓冲区有数据时触发。回调不在 UI 线程,更新控件须 Invoke / BeginInvoke |
ErrorReceived |
帧错误、溢出等;可记日志或提示 |
PinChanged |
CTS/DSR/CD 等引脚状态变化(按需) |
注意:
DataReceived 中直接改 TextBox.Text 会引发跨线程异常。务必用 Control.Invoke(或先判断 InvokeRequired)。通则见异步专页。
枚举端口并打开 / 关闭
窗体上放 ComboBox cmbPorts、Button btnRefresh、Button btnOpen、Button btnClose。端口名与波特率按设备手册修改。
C#
using System;
using System.IO.Ports;
using System.Windows.Forms;
public partial class Form1 : Form
{
private SerialPort port;
public Form1()
{
InitializeComponent();
port = new SerialPort();
port.BaudRate = 9600;
port.DataBits = 8;
port.Parity = Parity.None;
port.StopBits = StopBits.One;
port.Handshake = Handshake.None;
port.ReadTimeout = 500;
port.WriteTimeout = 500;
RefreshPorts();
}
private void RefreshPorts()
{
cmbPorts.Items.Clear();
string[] names = SerialPort.GetPortNames();
Array.Sort(names);
cmbPorts.Items.AddRange(names);
if (cmbPorts.Items.Count > 0)
cmbPorts.SelectedIndex = 0;
}
private void btnRefresh_Click(object sender, EventArgs e)
{
RefreshPorts();
}
private void btnOpen_Click(object sender, EventArgs e)
{
if (port.IsOpen)
return;
if (cmbPorts.SelectedItem == null)
{
MessageBox.Show("请选择端口。");
return;
}
try
{
port.PortName = cmbPorts.SelectedItem.ToString();
port.Open();
MessageBox.Show("已打开 " + port.PortName);
}
catch (Exception ex)
{
MessageBox.Show("打开失败: " + ex.Message);
}
}
private void btnClose_Click(object sender, EventArgs e)
{
if (port.IsOpen)
port.Close();
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (port != null)
{
if (port.IsOpen)
port.Close();
port.Dispose();
port = null;
}
base.OnFormClosing(e);
}
}
VB.NET
Imports System.IO.Ports
Imports System.Windows.Forms
Public Class Form1
Private port As SerialPort
Public Sub New()
InitializeComponent()
port = New SerialPort()
port.BaudRate = 9600
port.DataBits = 8
port.Parity = Parity.None
port.StopBits = StopBits.One
port.Handshake = Handshake.None
port.ReadTimeout = 500
port.WriteTimeout = 500
RefreshPorts()
End Sub
Private Sub RefreshPorts()
cmbPorts.Items.Clear()
Dim names As String() = SerialPort.GetPortNames()
Array.Sort(names)
cmbPorts.Items.AddRange(names)
If cmbPorts.Items.Count > 0 Then
cmbPorts.SelectedIndex = 0
End If
End Sub
Private Sub btnRefresh_Click(sender As Object, e As EventArgs) Handles btnRefresh.Click
RefreshPorts()
End Sub
Private Sub btnOpen_Click(sender As Object, e As EventArgs) Handles btnOpen.Click
If port.IsOpen Then Return
If cmbPorts.SelectedItem Is Nothing Then
MessageBox.Show("请选择端口。")
Return
End If
Try
port.PortName = cmbPorts.SelectedItem.ToString()
port.Open()
MessageBox.Show("已打开 " & port.PortName)
Catch ex As Exception
MessageBox.Show("打开失败: " & ex.Message)
End Try
End Sub
Private Sub btnClose_Click(sender As Object, e As EventArgs) Handles btnClose.Click
If port.IsOpen Then port.Close()
End Sub
Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
If port IsNot Nothing Then
If port.IsOpen Then port.Close()
port.Dispose()
port = Nothing
End If
MyBase.OnFormClosing(e)
End Sub
End Class
发送与同步读取
按钮 btnSend、TextBox txtSend / txtRecv。同步 ReadExisting 适合「发完立刻读」的短应答;长时间监听请用下一节 DataReceived。
C#
private void btnSend_Click(object sender, EventArgs e)
{
if (!port.IsOpen)
{
MessageBox.Show("请先打开串口。");
return;
}
try
{
// 文本
port.Write(txtSend.Text);
// 或写字节帧:
// byte[] frame = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
// port.Write(frame, 0, frame.Length);
System.Threading.Thread.Sleep(50); // 给对端一点应答时间(按协议调整)
string reply = port.ReadExisting();
txtRecv.AppendText(reply);
}
catch (TimeoutException)
{
MessageBox.Show("读取超时。");
}
catch (Exception ex)
{
MessageBox.Show("收发失败: " + ex.Message);
}
}
VB.NET
Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click
If Not port.IsOpen Then
MessageBox.Show("请先打开串口。")
Return
End If
Try
' 文本
port.Write(txtSend.Text)
' 或写字节帧:
' Dim frame As Byte() = New Byte() {&H1, &H3, &H0, &H0, &H0, &H1}
' port.Write(frame, 0, frame.Length)
System.Threading.Thread.Sleep(50) ' 给对端一点应答时间(按协议调整)
Dim reply As String = port.ReadExisting()
txtRecv.AppendText(reply)
Catch ex As TimeoutException
MessageBox.Show("读取超时。")
Catch ex As Exception
MessageBox.Show("收发失败: " & ex.Message)
End Try
End Sub
DataReceived:异步收数并刷新界面
在构造函数或打开前订阅事件。收到数据后用 Invoke 回到 UI 线程追加文本。
C#
// 在构造函数中:
port.DataReceived += port_DataReceived;
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = port.ReadExisting();
if (string.IsNullOrEmpty(data))
return;
if (txtRecv.InvokeRequired)
{
txtRecv.BeginInvoke(new Action(delegate
{
txtRecv.AppendText(data);
}));
}
else
{
txtRecv.AppendText(data);
}
}
catch (Exception)
{
// 关闭过程中可能抛异常,按需记录
}
}
VB.NET
' 在构造函数中:
AddHandler port.DataReceived, AddressOf port_DataReceived
Private Sub port_DataReceived(sender As Object, e As SerialDataReceivedEventArgs)
Try
Dim data As String = port.ReadExisting()
If String.IsNullOrEmpty(data) Then Return
If txtRecv.InvokeRequired Then
txtRecv.BeginInvoke(New Action(Sub()
txtRecv.AppendText(data)
End Sub))
Else
txtRecv.AppendText(data)
End If
Catch ex As Exception
' 关闭过程中可能抛异常,按需记录
End Try
End Sub
提示:
DataReceived 不保证「一帧完整协议」一次到齐。二进制协议应自行按长度/校验拼包;文本协议可缓冲到遇到分隔符再处理。
注意与易错点
| 点 | 说明 |
|---|---|
| 端口占用 | 同一 COM 口同时只能被一个进程打开;关闭应用或调用 Close/Dispose 后再用其他工具访问。 |
| 参数不一致 | 波特率、数据位、校验、停止位任一与对端不符会导致乱码或无应答。 |
| UI 阻塞 | 在按钮事件里长时间 Read/Sleep 会假死窗体;监听场景优先 DataReceived,或把阻塞读放到后台线程。 |
| 跨线程 | DataReceived → 更新控件必须 Invoke/BeginInvoke。 |
| 打开后再改参数 | 多数属性须在 Open 之前设置;已打开时改 PortName 等会抛异常。 |
| C# 语言版本 | LCode / .NET 4.8 按 C# 5.0:勿用 $"…"、nameof、?. 等。 |