跳至内容

如何在 C# 中使用串口通信

2026-11-07

串口通信介绍

串口通信(Serial Communication)是一种通过单条数据线逐位传输数据的方式,常用于单片机、传感器、计算机等设备之间的数据交换。

核心机制是将数据拆分成比特流,通过单条线路按顺序发送。以发送字符 A 为例(ASCII 码 65,二进制 01000001),串口会在数据前加起始位 (0),数据位按约定顺序发送,最后加停止位 (1),接收方检测到起始位后按波特率定时采样还原数据。

一个完整的串口数据帧包含:

[起始位] + [数据位] + [校验位(可选)] + [停止位]

如何在 C#中使用

要想在 C#中使用,必须先安装 System.IO.Ports,这是因为串口通信库没有内置在标准库内,需要我们手动添加。

dotnet add package System.IO.Ports

创建串口通信的关键参数

当我们引入串口通信库后,会根据事先约定好的参数,创建串口连接。

而这些是需要事先约定好的参数,如下:

参数说明常见值
波特率(BaudRate)表示数据传输速率,即每秒钟传输的二进制位数(bps)。它是衡量通信速度的关键参数,通信双方必须设置为完全相同的波特率,否则会导致数据无法正确解析或出现乱码。9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600
数据位(DataBits)表示每个数据帧中有效数据的位数,即一个信息包里包含的实际数据位数量。它决定了每次传输的数据量大小,影响数据传输的效率和兼容性。5, 6, 7, 8(8 位最常用)
校验位(Parity)用于检测单比特错误的校验机制。通过在数据位后添加一个校验位,确保数据传输的完整性。奇校验确保 “1” 的总数为奇数,偶校验确保 “1” 的总数为偶数,无校验则不进行错误检测。None(无)、Odd(奇校验)、Even(偶校验)
停止位(StopBits)用于表示单个数据帧结束的标志位,固定为高电平。它帮助接收方识别数据帧的结束位置,确保数据同步和帧边界识别,防止数据粘连。1, 1.5, 2(1 位最常用)

补充说明:

  • 通信双方的所有参数必须完全一致才能实现正常通信。
  • 最常用的配置组合为:9600/115200 波特率、8 数据位、无校验、1 停止位(8 N 1)。

创建串口连接

到创建串口连接这一步,还需要了解一个参数:

参数说明常见值
串口名称(PortName)指操作系统分配给物理或虚拟串行端口的设备标识符。它是应用程序与特定串口硬件建立连接的关键参数,用于唯一识别和访问对应的串行通信端口。不同的操作系统使用不同的命名规则来标识串口设备。Windows 系统:COM 1, COM 2, COM 3, COM 4…
Linux 系统:/dev/ttyS 0, /dev/ttyS 1, /dev/ttyUSB 0, /dev/ttyACM 0…
macOS 系统:/dev/cu.usbserial-XXXX, /dev/tty.usbmodemXXXX…

简而言之,我们需要一个可正常使用的串口,来作为数据传输的出入口。

Note

在我做练习的过程当中,发现消费级笔记本电脑通常不再配备物理串口,但在工业控制领域工控机通常仍保留串口接口。如果没有物理串口,可以使用 USB 转串口适配器,或者在开发测试阶段使用虚拟串口软件创建配对的虚拟串口。

参考连接:为什么通信要使用虚拟串口和串口助手? - 知乎

步骤 1:引入串口类的命名空间

using System.IO.Ports;

步骤 2:初始化串口

现在假设,接下来要使用的串口名称为 COM1,其余参数为:

  • 波特率:9600
  • 数据位:8
  • 校验:无
  • 停止位:1
var sp = new SerialPort(
    portName: "COM1",
    baudRate: 9600,
    parity: Parity.None,
    dataBits: 8,
    stopBits: StopBits.One)
{
    // 设置编码方式为UTF-8
    Encoding = Encoding.UTF8,
    // 设置读写超时时间,统一为1秒
    ReadTimeout = 1000,
    WriteTimeout = 1000
};

同时设置数据编码方式和读写超时时间。

步骤 3:打开串口

sp.Open();

在这里可以添加 try-catch,来捕获异常,比如可能会发生的串口被占用等等。

try
{
    sp.Open();
}
catch (Exception e)
{
    // 捕获并显示所有可能的串口异常
    // 常见异常:UnauthorizedAccessException(串口被占用)、
    //          IOException(串口不存在)、ArgumentOutOfRangeException(参数无效)
    Console.Out.WriteLine($"异常:{e.Message}");
}

读写操作

在 C#中串口的读与写,都可以使用两种类型的数据,分别是字符串和字节流。

发送数据

直接写入字符串数据。使用 Write(string text) 将指定字符串数据发送。

sp.Write("Hi");

直接写入字节流数据。使用 Write(byte[] buffer, int offset, int count) 写入指定数量的字节。

byte[] data = Encoding.UTF8.GetBytes("Hi");
sp.Write(data, 0, data.Length);
  • buffer:将要发送的字节数组
  • offset:字节写入的偏移量,直接从字节数据中第几位开始向缓冲区复制。
  • count:根据其值,确定每次写入多少位字节。

发送字节流数据时,使用了 Encoding.UTF8 工具类,可以将字符串或者字节数组,按照 UTF-8 编码转换为指定的类型。

读取数据

直接获取字符串数据。使用 ReadExisting() 将会读取 接收缓冲区中当前可用的所有数据 ,按照预先设置的编码将字节流解码为 string 类型返回。

string data = sp.ReadExisting();

直接获取字节流数据。使用 Read(byte[] buffer, int offset, int count) 读取缓冲区内的数据。

var bytesToRead = sp.BytesToRead;
var buffer = new byte[bytesToRead];
sp.Read(buffer, 0, bytesToRead);
  • BytesToRead:返回的是调用时缓冲区中的字节数。

在这里,我们直接将缓冲区内的数据全部读取,在数据量不大的时候这么干。

数据接收事件

串口类提供了 DataReceived 事件,来实时接收串口数据。每当有数据抵达时,事件就会触发。

// 注册 DataReceived 事件
sp.DataReceived += (sender, args) =>
{
	var s = (SerialPort)sender;
    string received = s.ReadExisting(); // 读取所有可用数据
    Console.WriteLine($"接收:{received}");
};

关闭串口连接

关闭串口连接。

sp.Close();

销毁串口对象。

sp.Dispose();

实践

为了进一步掌握串口通信,我编写了一个功能简单的串口调试工具,具有收发数据的功能,可收发字符串或 16 进制的数据。

接下来,主要展示收发数据的核心逻辑,其余功能的细节,可查阅此仓库。

发送数据

关于发送哪种数据,由 ToggleButton 按钮控制。通过命令绑定的方式,将 ToggleButton 的 IsChecked 作为参数传递,如果为真,则采用 16 进制发送数据,否则反之。

[ObservableProperty]
private string _sendData = string.Empty;

[RelayCommand]
private void Send(bool isHexMode)
{
    // 验证串口是否已打开
    if (_serialPort is not { IsOpen: true })
    {
        MessageBox.Show("串口未打开");
        return;
    }

    // 根据模式选择发送方式
    try
    {
        if (!isHexMode)
        {
            // 发送文本数据
    		_serialPort?.Write(SendData);
        }
        else
        {
            // 分割十六进制字符串,支持空格、回车、换行作为分隔符
            var hexValues = SendData.Split([' ', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries);

            // 将分割好的数组转换为 byte 数组
            var bytes = hexValues
                .Select(hex => Convert.ToByte(hex, 16))
                .ToArray();
			
            // 发送字节数据
            _serialPort?.Write(bytes, 0, bytes.Length);
        }
    }
    catch (Exception e)
    {
        MessageBox.Show($"发送数据失败: {e.Message}");
    }
}

在这里使用了 CommunityToolkit.Mvvm 框架,来简化 MVVM 模式的开发。

读取数据

在这里,也是通过 ToggleButton 按钮,来控制数据类型。不过由于是使用“数据接收事件”,来实时接收数据,就不能直接将 IsChecked 的值作为命令参数传递,而是使用数据绑定,将其值同步到 ViewModel 中指定的属性上。

接下来,在构造函数中,添加“数据接收事件”。

_serialPort.DataReceived += OnSerialDataReceived;

数据接收事件:

/// <param name="sender">触发事件的串口对象</param>
/// <param name="args">串口数据接收事件参数</param>
private void OnSerialDataReceived(object sender, SerialDataReceivedEventArgs args)
{
    // 类型转换
    var sp = (SerialPort)sender;

    // 读取缓冲区中的所有数据
    var bytesToRead = sp.BytesToRead;
    var buffer = new byte[bytesToRead];
    sp.Read(buffer, 0, bytesToRead);
    
    string data;
    
    if (IsReceiveHexMode)
    {
        // 十六进制模式:将字节数组转换为空格分隔的大写十六进制字符串
        data = string.Join(" ", buffer.Select(b => b.ToString("X2")));
    }
    else
    {
        // 文本模式:使用UTF-8解码字节数组
        data = Encoding.UTF8.GetString(buffer);
    }
}

结束语

在这里提供的代码实现,都是简略的,是为了直观的呈现最核心的部分。

参考连接

最后更新于