You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# WinForm两个窗体间使用serialPort1串口通信的问题求解

问题根因

你当前的实现存在5个核心错误,直接导致串口收发失败:

  • 串口资源管理逻辑完全错误:两个窗体各自持有独立的SerialPort实例,切换窗体时先关闭当前窗体的串口、立刻在新窗体构造函数中尝试重新打开。Windows系统释放串口句柄需要数百毫秒延迟,这步Open操作大概率触发"端口被占用"异常,你没有加异常捕获,异常被静默吞掉后串口实际并未打开,自然无法收发。
  • Form1未绑定串口数据接收回调,就算串口正常打开,也收不到Arduino返回的任何数据。
  • 串口参数未做匹配校验:代码里没有显式配置波特率、校验位、数据位、停止位,默认参数和Arduino端设置的9600波特率可能不匹配,出现乱码、收不到数据的问题。
  • 接收逻辑存在死锁风险:C#端用ReadLine()读取数据,必须等到串口收到换行符才会返回;但Arduino端收到指令后只发送单个字符'r',没有换行也没有后续内容,会导致接收线程永久阻塞。同时Arduino端用readString()读取数据默认有1秒超时,响应延迟极高。
  • Form2关闭事件逻辑无效:关闭时新建Form1实例又立刻执行Close,属于无意义代码,还会造成串口资源泄漏。
最简正确实现方案

核心思路:全局只保留一个串口实例,程序生命周期内只打开一次,所有窗体共用,不要跟着窗体切换反复开关串口,不需要做复杂的跨窗体传值,实现成本极低。

第一步:配置全局串口实例

打开项目里的Program.cs,把串口初始化、打开、释放的逻辑统一放到这里,所有窗体直接调用全局实例即可:

using System;
using System.IO.Ports;
using System.Windows.Forms;

namespace Arduino_GUI
{
    static class Program
    {
        // 全局唯一串口实例,所有窗体共用
        public static SerialPort MainSerialPort = new SerialPort()
        {
            PortName = "COM3", // 替换为你自己的Arduino串口号
            BaudRate = 9600,
            Parity = Parity.None,
            DataBits = 8,
            StopBits = StopBits.One,
            ReadTimeout = 500,
            WriteTimeout = 500
        };

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // 程序启动时统一打开串口,加异常捕获
            try
            {
                if (!MainSerialPort.IsOpen) MainSerialPort.Open();
            }
            catch (Exception ex)
            {
                MessageBox.Show($"串口打开失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            Application.Run(new Form1());

            // 程序完全退出时再关闭串口释放资源
            if (MainSerialPort.IsOpen) MainSerialPort.Close();
        }
    }
}

第二步:修改两个窗体的串口逻辑

删掉两个窗体设计器中各自拖拽生成的serialPort1控件,所有串口操作都调用全局的Program.MainSerialPort。

Form1核心代码

using System;
using System.Windows.Forms;

namespace Arduino_GUI
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            // 不需要在这里重复打开串口
        }

        private void onButton_Click(object sender, EventArgs e)
        {
            Program.MainSerialPort.Write("A");
        }

        private void offButton_Click(object sender, EventArgs e)
        {
            Program.MainSerialPort.Write("a");
        }

        private void btnOForm2_Click(object sender, EventArgs e)
        {
            // 禁止关闭串口,直接切换窗体
            Form2 f2 = new Form2();
            f2.Show();
            this.Hide();
        }
    }
}

Form2核心代码

using System;
using System.IO.Ports;
using System.Windows.Forms;

namespace Arduino_GUI
{
    public partial class Form2 : Form
    {
        private delegate void UpdateUiDelegate(string content);
        public Form2()
        {
            InitializeComponent();
            // 仅在当前窗体加载时绑定接收事件
            Program.MainSerialPort.DataReceived += SerialPort_DataReceived;
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                string indata = Program.MainSerialPort.ReadLine().Trim();
                if (string.IsNullOrEmpty(indata)) return;
                // 跨线程更新UI
                this.Invoke(new UpdateUiDelegate(UpdateFormContent), indata);
            }
            catch { /* 忽略读超时异常即可 */ }
        }

        private void UpdateFormContent(string indata)
        {
            if (indata.Length < 1) return;
            char firstChar = indata[0];
            switch (firstChar)
            {
                case 'r':
                    label1.Text = indata.Substring(1);
                    break;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Program.MainSerialPort.WriteLine("2");
        }

        private void btnMain_Click(object sender, EventArgs e)
        {
            // 切回主窗体前解绑当前窗体的接收事件,避免内存泄漏
            Program.MainSerialPort.DataReceived -= SerialPort_DataReceived;
            Form1 f1 = new Form1();
            f1.Show();
            this.Close();
        }

        private void Form2_FormClosing(object sender, FormClosingEventArgs e)
        {
            // 删除原来无效的新建Form1逻辑,仅做事件解绑
            Program.MainSerialPort.DataReceived -= SerialPort_DataReceived;
        }
    }
}

第三步:修正Arduino端代码

解决读超时、返回数据不匹配的问题:

char cmd;

void setup() {
  Serial.begin(9600);
  delay(100); // 等待串口连接稳定
}

void loop() {
  if (Serial.available()) {
    // 直接读取首字符作为指令,避免readString的1秒超时问题
    cmd = Serial.read();
    // 清空缓冲区剩余数据,防止粘包
    while(Serial.available()) Serial.read();

    switch(cmd) {
      case 'A':
        // 这里写开灯的硬件控制逻辑
        break;
      case 'a':
        // 这里写关灯的硬件控制逻辑
        break;
      case '2':
        // 必须用println输出带换行的完整内容,和PC端ReadLine匹配,r后面拼接实际要显示的文本
        Serial.println("r传感器数据正常");
        break;
    }
  }
}

注意:如果需要动态选择串口,可以在程序启动页加一个下拉框,枚举本机所有可用串口让用户选择,选完再给全局串口实例的PortName赋值、打开串口即可,不要硬编码串口号。

内容的提问来源于stack exchange,提问作者Icon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:30:50