news 2026/5/31 5:20:24

C#串口通信入门:同步与异步收发详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#串口通信入门:同步与异步收发详解

目录

一、前置准备

1. 核心类与依赖

2. 串口核心参数(必须匹配硬件 / 通信端)

二、同步收发(入门首选,逻辑简单)

完整示例(控制台程序)

核心方法说明

同步收发注意事项

三、异步收发(非阻塞,适合 UI 程序)

方式 1:事件驱动(DataReceived)

核心逻辑

完整示例(WinForm 程序)

方式 2:异步方法(ReadAsync/WriteAsync)

核心示例(控制台程序)

异步收发注意事项

四、常见问题与避坑指南

五、同步 vs 异步 适用场景总结

六、入门实操建议


串口通信是嵌入式、工控、硬件交互中最常用的通信方式之一,C# 提供了System.IO.Ports.SerialPort类封装了串口操作,本文从入门级角度讲解同步、异步收发的核心逻辑、代码示例和注意事项。

一、前置准备

1. 核心类与依赖

C# 串口操作依赖SerialPort类,所在命名空间:System.IO.Ports

  • .NET Framework:默认内置,无需额外安装;
  • .NET Core/.NET 5+:需手动安装 NuGet 包System.IO.Ports(NuGet 管理器中搜索安装)。

2. 串口核心参数(必须匹配硬件 / 通信端)

参数说明
PortName串口名(如 COM1、COM3,Windows 可在「设备管理器」查看)
BaudRate波特率(如 9600、115200,需与通信方一致)
Parity奇偶校验(None/Odd/Even,默认 None)
DataBits数据位(通常 8)
StopBits停止位(通常 One)
ReadTimeout同步读取超时时间(毫秒,默认 InfiniteTimeout)
WriteTimeout同步写入超时时间(毫秒,默认 InfiniteTimeout)

二、同步收发(入门首选,逻辑简单)

同步操作的特点:代码按顺序执行,读写操作会阻塞当前线程,直到操作完成或超时。适合简单的控制台程序、非 UI 场景。

完整示例(控制台程序)

using System; using System.IO.Ports; using System.Text; namespace SerialPortSyncDemo { class Program { static void Main(string[] args) { // 1. 创建串口实例并配置参数 SerialPort serialPort = new SerialPort { PortName = "COM1", // 替换为你的串口名 BaudRate = 9600, // 波特率 Parity = Parity.None, // 无奇偶校验 DataBits = 8, // 8位数据位 StopBits = StopBits.One, // 1位停止位 ReadTimeout = 500, // 同步读取超时500ms WriteTimeout = 500 // 同步写入超时500ms }; try { // 2. 打开串口 if (!serialPort.IsOpen) { serialPort.Open(); Console.WriteLine("串口已打开"); } // 3. 同步发送数据 string sendData = "Hello SerialPort!"; // 字符串转字节数组(编码需与接收方一致,通常UTF8/GBK) byte[] sendBytes = Encoding.UTF8.GetBytes(sendData); serialPort.Write(sendBytes, 0, sendBytes.Length); // 发送字节数组 // 也可以直接发字符串:serialPort.Write(sendData); Console.WriteLine($"已发送:{sendData}"); // 4. 同步接收数据(两种方式:读字节/读字符串) // 方式1:读取指定长度字节(适合已知数据长度) // byte[] recvBytes = new byte[serialPort.BytesToRead]; // serialPort.Read(recvBytes, 0, recvBytes.Length); // 方式2:读取所有可用字节(更通用) StringBuilder recvData = new StringBuilder(); if (serialPort.BytesToRead > 0) // 先判断是否有数据可读 { byte[] recvBytes = new byte[serialPort.BytesToRead]; int readLen = serialPort.Read(recvBytes, 0, recvBytes.Length); recvData.Append(Encoding.UTF8.GetString(recvBytes, 0, readLen)); Console.WriteLine($"已接收:{recvData}"); } else { Console.WriteLine("暂无接收数据"); } } catch (Exception ex) { // 捕获常见异常:串口不存在、被占用、超时等 Console.WriteLine($"操作失败:{ex.Message}"); } finally { // 5. 关闭串口(必须释放资源) if (serialPort.IsOpen) { serialPort.Close(); Console.WriteLine("串口已关闭"); } // 释放资源 serialPort.Dispose(); } Console.WriteLine("按任意键退出..."); Console.ReadKey(); } } }

核心方法说明

方法作用
serialPort.Open()打开串口(必须先打开才能读写)
serialPort.Write(byte[] buffer, int offset, int count)发送字节数组(推荐,避免编码问题)
serialPort.Write(string text)发送字符串(内部默认用 ASCII 编码,需注意编码匹配)
serialPort.Read(byte[] buffer, int offset, int count)读取指定长度字节到缓冲区,返回实际读取的字节数
serialPort.ReadLine()读取一行数据(直到换行符\n,适合文本协议)
serialPort.BytesToRead获取接收缓冲区中待读取的字节数(判断是否有数据)
serialPort.Close()关闭串口(必须调用,否则串口会被占用)

同步收发注意事项

  1. 阻塞问题:同步读写会阻塞当前线程,若在 UI 线程(如 WinForm/WPF)中使用,会导致界面卡死;
  2. 超时设置:建议设置ReadTimeout/WriteTimeout,避免线程永久阻塞;
  3. 异常处理:必须捕获IOException(串口占用 / 不存在)、TimeoutException(超时)等异常;
  4. 编码一致:发送和接收的编码(UTF8/GBK/ASCII)必须统一,否则会乱码。

三、异步收发(非阻塞,适合 UI 程序)

异步操作的特点:读写操作不阻塞当前线程,适合 WinForm/WPF 等 UI 场景,避免界面卡死。C# 串口异步有两种常用方式:

  • 事件驱动(DataReceived):最常用,串口收到数据时自动触发事件;
  • 异步方法(ReadAsync/WriteAsync):基于 Task 的异步编程(.NET 5 + 推荐)。

方式 1:事件驱动(DataReceived)

核心逻辑
  1. 注册DataReceived事件,串口收到数据时触发事件处理方法;
  2. 在事件处理方法中读取数据(注意:事件处理线程不是 UI 线程,跨线程访问 UI 需用Invoke);
  3. 发送数据仍可使用同步Write(或异步WriteAsync)。
完整示例(WinForm 程序)
using System; using System.IO.Ports; using System.Text; using System.Windows.Forms; namespace SerialPortAsyncDemo { public partial class MainForm : Form { private SerialPort _serialPort; public MainForm() { InitializeComponent(); // 初始化串口 InitSerialPort(); } /// <summary> /// 初始化串口配置 /// </summary> private void InitSerialPort() { _serialPort = new SerialPort { PortName = "COM1", BaudRate = 9600, Parity = Parity.None, DataBits = 8, StopBits = StopBits.One, ReadTimeout = 500, WriteTimeout = 500 }; // 注册数据接收事件 _serialPort.DataReceived += SerialPort_DataReceived; } /// <summary> /// 串口数据接收事件(触发线程:非UI线程) /// </summary> private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { SerialPort sp = (SerialPort)sender; if (sp.BytesToRead <= 0) return; // 读取数据(同步读取,因为事件线程是后台线程,阻塞无影响) byte[] recvBytes = new byte[sp.BytesToRead]; int readLen = sp.Read(recvBytes, 0, recvBytes.Length); string recvData = Encoding.UTF8.GetString(recvBytes, 0, readLen); // 跨线程更新UI(WinForm需用Invoke) this.Invoke(new Action(() => { txtRecv.Text += $"[{DateTime.Now:HH:mm:ss}] 接收:{recvData}\r\n"; })); } catch (Exception ex) { this.Invoke(new Action(() => { txtRecv.Text += $"接收失败:{ex.Message}\r\n"; })); } } /// <summary> /// 打开串口按钮点击事件 /// </summary> private void btnOpen_Click(object sender, EventArgs e) { try { if (!_serialPort.IsOpen) { _serialPort.Open(); btnOpen.Text = "关闭串口"; txtRecv.Text += "串口已打开\r\n"; } else { _serialPort.Close(); btnOpen.Text = "打开串口"; txtRecv.Text += "串口已关闭\r\n"; } } catch (Exception ex) { MessageBox.Show($"串口操作失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// <summary> /// 发送数据按钮点击事件 /// </summary> private void btnSend_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show("请先打开串口", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { string sendData = txtSend.Text.Trim(); if (string.IsNullOrEmpty(sendData)) return; // 异步发送(也可以用同步Write,UI线程无感知) byte[] sendBytes = Encoding.UTF8.GetBytes(sendData); _serialPort.BaseStream.WriteAsync(sendBytes, 0, sendBytes.Length); txtRecv.Text += $"[{DateTime.Now:HH:mm:ss}] 发送:{sendData}\r\n"; txtSend.Clear(); } catch (Exception ex) { MessageBox.Show($"发送失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// <summary> /// 窗体关闭时释放串口 /// </summary> private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (_serialPort.IsOpen) { _serialPort.Close(); } _serialPort.Dispose(); } } }

方式 2:异步方法(ReadAsync/WriteAsync)

.NET 5+ 推荐使用基于 Task 的异步方法,逻辑更清晰,无需注册事件。

核心示例(控制台程序)
using System; using System.IO.Ports; using System.Text; using System.Threading.Tasks; namespace SerialPortAsyncTaskDemo { class Program { static async Task Main(string[] args) { SerialPort serialPort = new SerialPort { PortName = "COM1", BaudRate = 9600, Parity = Parity.None, DataBits = 8, StopBits = StopBits.One }; try { serialPort.Open(); Console.WriteLine("串口已打开"); // 异步发送数据 string sendData = "Async Hello!"; byte[] sendBytes = Encoding.UTF8.GetBytes(sendData); await serialPort.BaseStream.WriteAsync(sendBytes, 0, sendBytes.Length); Console.WriteLine($"已异步发送:{sendData}"); // 异步接收数据 byte[] recvBytes = new byte[1024]; int readLen = await serialPort.BaseStream.ReadAsync(recvBytes, 0, recvBytes.Length); string recvData = Encoding.UTF8.GetString(recvBytes, 0, readLen); Console.WriteLine($"已异步接收:{recvData}"); } catch (Exception ex) { Console.WriteLine($"异步操作失败:{ex.Message}"); } finally { if (serialPort.IsOpen) { serialPort.Close(); } serialPort.Dispose(); } Console.WriteLine("按任意键退出..."); Console.ReadKey(); } } }

异步收发注意事项

  1. 跨线程 UI 访问DataReceived事件在后台线程触发,WinForm/WPF 中更新 UI 必须用Invoke/Dispatcher
  2. 数据拼接:若单次接收的数据不完整(如长报文),需在事件中拼接数据,直到收到完整报文(如按结束符分割);
  3. 资源释放:异步操作中仍需确保串口最终关闭,避免资源泄漏;
  4. BaseStreamReadAsync/WriteAsync需通过serialPort.BaseStream调用(SerialPort 类本身无直接的 ReadAsync 方法)。

四、常见问题与避坑指南

  1. 串口被占用

    • 原因:串口未关闭、其他程序占用;
    • 解决:确保程序退出前调用Close(),任务管理器结束占用串口的程序。
  2. 接收数据乱码

    • 原因:编码不匹配、波特率 / 数据位等参数不一致;
    • 解决:统一编码(UTF8/GBK),核对串口参数与硬件一致。
  3. DataReceived 事件不触发

    • 原因:串口未打开、无数据接收、参数错误;
    • 解决:检查串口状态,确认硬件发送数据,核对波特率等参数。
  4. UI 线程卡死

    • 原因:在 UI 线程执行同步读写;
    • 解决:改用事件驱动或异步方法,同步操作放在后台线程(Task.Run)。

五、同步 vs 异步 适用场景总结

方式优点缺点适用场景
同步逻辑简单、代码少阻塞线程、UI 易卡死控制台程序、简单测试
异步非阻塞、适合 UI 程序逻辑稍复杂、需处理线程WinForm/WPF、工控程序

六、入门实操建议

  1. 先用串口调试助手(如 SSCOM)测试硬件是否正常收发;
  2. 先写同步示例,熟悉串口基本操作;
  3. 再尝试 WinForm 异步示例,重点理解跨线程 UI 更新;
  4. 调试时关注串口参数、编码、异常处理三个核心点。

通过以上内容,你可以快速掌握 C# 串口同步 / 异步收发的核心逻辑,入门级场景足以应对大部分硬件交互需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/29 19:07:09

云端UML设计革命:PlantUML Editor如何重塑你的建模工作流

云端UML设计革命&#xff1a;PlantUML Editor如何重塑你的建模工作流 【免费下载链接】plantuml-editor PlantUML online demo client 项目地址: https://gitcode.com/gh_mirrors/pl/plantuml-editor 在软件开发的世界里&#xff0c;UML建模往往是设计环节中最令人头疼的…

作者头像 李华
网站建设 2026/5/31 0:49:58

Vite与React-InlineSVG的完美结合:解决SVG引入问题

引言 在现代Web开发中,React和Vite已经成为了许多开发者的首选工具。React-InlineSVG库提供了一种便捷的方式来在React应用中内联SVG文件。然而,在使用Vite构建工具时,引入SVG文件可能会遇到一些问题。本文将详细介绍如何在Vite环境下使用react-inlinesvg库,并通过一个实际…

作者头像 李华
网站建设 2026/5/29 18:33:52

巧妙利用泛型方法打印表格数据

在编程中,如何将一个对象列表以表格形式打印出来是一个常见的问题。例如,我们可能需要将一组具有相同属性但值不同的对象显示成类似Excel表格的形式。本文将介绍如何使用C#中的泛型方法来实现这个功能。 问题描述 假设我们有一个Header类,包含BlNo(提单号)、Descr(描述…

作者头像 李华
网站建设 2026/5/29 21:04:36

52、无权重图的增长模型

无权重图的增长模型 1. 无权重图增长模型概述 在图论中,无权重图的增长模型是一类重要的研究对象。这类模型的特点是节点数量和边的数量会随时间变化(通常是增长)。在选择图的表示方法时,需要考虑到这种动态变化。而且,在时间 $t$ 时,新到达的节点 $i$ 与现有节点 $j$ …

作者头像 李华
网站建设 2026/5/29 20:42:20

20、Ubuntu Server网络服务搭建与管理指南

Ubuntu Server网络服务搭建与管理指南 1. DNS服务器测试 为了进一步测试DNS服务器,我们可以使用 dig 命令,就像之前测试缓存时那样。可以针对本地和外部资源进行测试,例如: dig webserv.local.lan dig www.packtpub.com正常情况下,会看到类似如下的响应: ;; Quer…

作者头像 李华