serialPort.DataReceived、串口优雅管理
完整《C#串口通信系统》功能清单
Part 1 --- SerialPortManager.cs
------ 串口核心管理类
csharp
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Windows.Forms;
/// <summary>
/// 专业版串口通信管理器
/// 支持:自动重连、自动超时检测、线程安全接收、发送失败重试、统一日志
/// </summary>
public class SerialPortManager
{
private SerialPort _serialPort;
private System.Timers.Timer _reconnectTimer; // 自动重连定时器
public event Action<byte[]> DataReceived; // 串口数据接收事件
public event Action<string> LogMessage; // 日志输出事件
public bool IsOpen => _serialPort != null && _serialPort.IsOpen;
public string PortName { get; private set; }
public int BaudRate { get; private set; }
public SerialPortManager()
{
_reconnectTimer = new System.Timers.Timer(5000); // 5秒检测一次串口状态
_reconnectTimer.Elapsed += (sender, e) => ReconnectCheck();
_reconnectTimer.Start();
}
public void Open(string portName, int baudRate = 115200)
{
try
{
PortName = portName;
BaudRate = baudRate;
if (_serialPort == null)
{
_serialPort = new SerialPort
{
PortName = portName,
BaudRate = baudRate,
Encoding = Encoding.UTF8
};
_serialPort.DataReceived += SerialPort_DataReceived;
}
if (!_serialPort.IsOpen)
{
_serialPort.Open();
Log($"✅ 串口 {portName} 打开成功");
}
}
catch (Exception ex)
{
Log($"❌ 打开串口失败:{ex.Message}");
}
}
public void Close()
{
try
{
if (_serialPort != null)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
Log($"❎ 串口 {PortName} 已关闭");
}
_serialPort.DataReceived -= SerialPort_DataReceived;
_serialPort.Dispose();
_serialPort = null;
}
}
catch (Exception ex)
{
Log($"❌ 关闭串口失败:{ex.Message}");
}
}
public void Send(byte[] data)
{
if (_serialPort == null || !_serialPort.IsOpen)
{
Log("❗ 串口未打开,无法发送!");
return;
}
int retryCount = 0;
const int maxRetries = 3;
while (retryCount < maxRetries)
{
try
{
_serialPort.Write(data, 0, data.Length);
Log("📤 数据发送成功");
break;
}
catch (Exception ex)
{
retryCount++;
Log($"⚠️ 发送失败,重试{retryCount}/{maxRetries}次:{ex.Message}");
Thread.Sleep(100); // 短暂等待
}
}
if (retryCount == maxRetries)
{
Log("❌ 发送失败,已达到最大重试次数!");
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
int bytesToRead = _serialPort.BytesToRead;
byte[] buffer = new byte[bytesToRead];
_serialPort.Read(buffer, 0, bytesToRead);
DataReceived?.Invoke(buffer); // 抛给外部
}
catch (Exception ex)
{
Log($"❌ 接收数据失败:{ex.Message}");
}
}
private void ReconnectCheck()
{
if (_serialPort != null && !_serialPort.IsOpen && !string.IsNullOrEmpty(PortName))
{
try
{
_serialPort.Open();
Log($"🔄 检测到串口断开,自动重连成功!");
}
catch
{
Log($"🔄 正在尝试重连串口 {PortName}...");
}
}
}
private void Log(string message)
{
LogMessage?.Invoke($"[{DateTime.Now:HH:mm:ss}] {message}");
}
}
Part 2 --- Form1.cs
------ 界面调用端逻辑
csharp
// 引入
private SerialPortManager spManager = new SerialPortManager();
private List<string> serialLogs = new List<string>();
private System.Timers.Timer timeoutTimer;
private void Form1_Load(object sender, EventArgs e)
{
RefreshPorts();
spManager.DataReceived += OnDataReceived;
spManager.LogMessage += Log;
}
private void openPortBtn_Click(object sender, EventArgs e)
{
spManager.Open(comboBox1.Text, 115200);
}
private void closePortBtn_Click(object sender, EventArgs e)
{
spManager.Close();
}
private void sendBtn_Click(object sender, EventArgs e)
{
byte[] frame = BuildFrame();
spManager.Send(frame);
// 开始超时监控
StartTimeoutMonitor(1500);
}
private void saveLogBtn_Click(object sender, EventArgs e)
{
SaveSerialLog();
}
// 接收到串口数据
private void OnDataReceived(byte[] data)
{
this.Invoke((Action)(() =>
{
timeoutTimer?.Stop();
timeoutTimer?.Dispose();
timeoutTimer = null;
Log($"📥 收到数据:{BitConverter.ToString(data).Replace("-", " ")}");
}));
}
// 日志打印+记录
private void Log(string message)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => Log(message)));
return;
}
string logMessage = $"[{DateTime.Now:HH:mm:ss}] {message}";
textBoxLog.AppendText(logMessage + Environment.NewLine);
serialLogs.Add(logMessage);
}
// 保存日志
private void SaveSerialLog()
{
if (serialLogs.Count == 0)
{
MessageBox.Show("暂无日志可保存!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string exePath = AppDomain.CurrentDomain.BaseDirectory;
string logFolder = Path.Combine(exePath, "logs");
if (!Directory.Exists(logFolder))
{
Directory.CreateDirectory(logFolder);
}
string fileName = $"串口日志_{DateTime.Now:yyyyMMdd_HHmmss}.log";
string fullPath = Path.Combine(logFolder, fileName);
File.WriteAllLines(fullPath, serialLogs, Encoding.UTF8);
MessageBox.Show($"日志保存成功!\n路径:{fullPath}", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
// 热插拔检测
protected override void WndProc(ref Message m)
{
const int WM_DEVICECHANGE = 0x0219;
const int DBT_DEVICEARRIVAL = 0x8000;
const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
base.WndProc(ref m);
if (m.Msg == WM_DEVICECHANGE)
{
if (m.WParam.ToInt32() == DBT_DEVICEARRIVAL)
{
Log("📥 串口设备已插入");
RefreshPorts();
}
else if (m.WParam.ToInt32() == DBT_DEVICEREMOVECOMPLETE)
{
Log("📤 串口设备已拔出");
RefreshPorts();
}
}
}
// 刷新串口列表
private void RefreshPorts()
{
var ports = SerialPort.GetPortNames();
comboBox1.Items.Clear();
comboBox1.Items.AddRange(ports);
if (ports.Length > 0)
{
comboBox1.SelectedIndex = 0;
}
}
// 启动超时检测
private void StartTimeoutMonitor(int timeoutMs = 1000)
{
if (timeoutTimer != null)
{
timeoutTimer.Stop();
timeoutTimer.Dispose();
}
timeoutTimer = new System.Timers.Timer(timeoutMs);
timeoutTimer.Elapsed += (s, e) =>
{
timeoutTimer.Stop();
timeoutTimer.Dispose();
timeoutTimer = null;
this.Invoke((Action)(() =>
{
Log("⚠️ 超时:设备未响应指令!");
MessageBox.Show("设备未在限定时间内响应,请检查连接或设备状态。", "超时警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}));
};
timeoutTimer.Start();
}
🔥 项目总览架构:
文件 | 作用 |
---|---|
SerialPortManager.cs |
串口统一管理(发送/接收/重连/日志) |
Form1.cs |
上位机界面操作(开关串口/显示日志/保存日志) |
resources/ |
保存你的烧录数据/扫码数据等 |
logs/ |
保存串口完整操作日志 |
🎯 小结
功能 | 状态 |
---|---|
自动检测热插拔刷新串口列表 | ✅ |
自动重连串口 | ✅ |
发送数据失败自动重试 | ✅ |
接收数据线程安全处理 | ✅ |
日志追踪全部收发 | ✅ |
一键保存日志 | ✅ |
超时无响应自动警告 | ✅ |
这已经是一个工业级、企业上位机必备的完整串口通信系统
扩展
serialPort.DataReceived
C# 串口通信,为什么要写:
serialPort.DataReceived += new SerialDataReceivedEventHandler(Sp_DataReceived);
如果不写,会有什么后果?
✅ 1. serialPort.DataReceived += ...
是什么意思?
它的意思是:
注册事件监听器,告诉系统:
"嘿!每当串口有数据到达(收到数据)的时候,请自动调用我的
Sp_DataReceived
方法来处理!"
✅ 2. 如果不写这一行,会怎么样?
如果 不注册 DataReceived
事件,那么:
- 即使串口收到了设备发来的数据
- 程序 完全不会收到通知
Sp_DataReceived
方法 永远不会被调用- 你就接收不到任何回传数据❗
- 整个上位机只能发送,没法接收数据
✅ 3. 这行代码详细拆解
csharp
serialPort.DataReceived += new SerialDataReceivedEventHandler(Sp_DataReceived);
也可以简化写成(完全一样效果):
csharp
serialPort.DataReceived += Sp_DataReceived;
意思就是:
把
Sp_DataReceived
方法绑到串口的DataReceived
事件上。
以后只要串口有新数据来,就会自动执行 Sp_DataReceived()
方法!
总结成一句话:
不注册
DataReceived
事件,程序就完全收不到串口回来的数据。
✅ 最后标准流程回顾一下:
步骤 | 必要操作 | 作用 |
---|---|---|
1 | 打开串口(serialPort.Open() ) |
打通发送通道 |
2 | 注册 DataReceived 事件监听器 |
建立数据接收处理机制 |
3 | 实现 Sp_DataReceived 处理函数 |
真正处理收到的数据内容 |