serialPort.DataReceived、串口优雅管理
完整《C#串口通信系统》功能清单
Part 1 — SerialPortManager.cs
—— 串口核心管理类
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
—— 界面调用端逻辑
// 引入
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. 这行代码详细拆解
serialPort.DataReceived += new SerialDataReceivedEventHandler(Sp_DataReceived);
也可以简化写成(完全一样效果):
serialPort.DataReceived += Sp_DataReceived;
意思就是:
把
Sp_DataReceived
方法绑到串口的DataReceived
事件上。
以后只要串口有新数据来,就会自动执行 Sp_DataReceived()
方法!
总结成一句话:
不注册
DataReceived
事件,程序就完全收不到串口回来的数据。
✅ 最后标准流程回顾一下:
步骤 | 必要操作 | 作用 |
---|---|---|
1 | 打开串口(serialPort.Open() ) | 打通发送通道 |
2 | 注册 DataReceived 事件监听器 | 建立数据接收处理机制 |
3 | 实现 Sp_DataReceived 处理函数 | 真正处理收到的数据内容 |