【C#】一种优雅的基于winform的串口通信管理

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 处理函数 真正处理收到的数据内容

相关推荐
南泽兆1 小时前
MCU选型的五大维度--助力嵌入式产品设计
单片机·嵌入式硬件
SuperW3 小时前
蓝桥杯嵌入式十五届模拟二(串口DMA,占空比的另一种测量方式)
单片机·职场和发展·蓝桥杯
yagerfgcs5 小时前
【配置Visual Assistant VC助手 for Visual Studio 2017】
ide·c#·visual studio
cccc卌6 小时前
MCU如何查看系统有没有卡死
单片机·嵌入式硬件
PfCoder7 小时前
WinForm真入门(13)——ListBox控件详解
windows·c#·visual studio·winform
xMathematics8 小时前
STM32嵌入式开发从入门到实战:全面指南与项目实践
stm32·单片机·嵌入式硬件
SundayBear8 小时前
嵌入式MCU常用模块
单片机·嵌入式硬件·常用模块
冻结的鱼8 小时前
在 STM32 中实现电机测速的方法介绍
stm32·单片机·嵌入式硬件
getapi9 小时前
51单片机烧录程序演示教程
stm32·单片机·51单片机
佟格湾10 小时前
聊透多线程编程-线程池-6.C# APM(异步编程模型)
开发语言·后端·c#·多线程