C# WinForm与Arduino FTDI232双向串口通信问题求解
问题根因
所有异常的核心诱因是串口对象的错误使用逻辑:
- 串口通信全程只需要初始化1次SerialPort实例、保持长连接即可,不需要每次发送指令都重新实例化对象、反复开关串口
- 现有代码在Form_Load中已经初始化了绑定数据接收事件的串口实例,但点击LED控制按钮时又重新创建新的SerialPort对象,发完指令就关闭连接,直接冲掉了原有正常工作的串口实例,既会导致
DataReceived接收事件失效,也会造成串口资源被异常占用无法释放。
对应修复方案
1. 应用退出自动释放串口
为窗体添加FormClosing生命周期事件,在窗口触发关闭动作时主动检测串口状态,关闭并释放串口资源,无需用户手动操作。
2. 收发状态切换异常修复
删除LED开/关按钮事件中重复创建SerialPort、执行Open/Close串口的逻辑,直接复用Form_Load阶段初始化完成的全局myPort实例发送数据即可。串口保持长连接状态下不存在收发状态切换的冲突,不会出现通信拒绝问题。
3. 发送LED指令后接收失效修复
该问题完全是发送指令时重复创建串口、关闭原有连接导致的,复用全局串口实例发送数据后,DataReceived事件会持续处于监听状态,不会丢失Arduino回传的数据。
修正后可直接运行的代码
public partial class Form1 : Form { public SerialPort myPort; private DateTime dateTime; private string inData; const string com = "COM8"; public Form1() { InitializeComponent(); // 注册窗体关闭事件,程序退出时自动回收串口资源 this.FormClosing += Form1_FormClosing; } private void start_btn_Click(object sender, EventArgs e) // 点亮LED { if (myPort == null || !myPort.IsOpen) { MessageBox.Show("串口未连接", "操作错误"); return; } try { myPort.WriteLine("A"); error_tb.Text = ""; } catch (Exception exc) { MessageBox.Show(exc.Message, "发送失败"); } } private void stop_btn_Click(object sender, EventArgs e) // 熄灭LED { if (myPort == null || !myPort.IsOpen) { MessageBox.Show("串口未连接", "操作错误"); return; } try { myPort.WriteLine("B"); error_tb.Text = ""; } catch (Exception exc2) { MessageBox.Show(exc2.Message, "发送失败"); } } void MyPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { inData = myPort.ReadLine(); this.Invoke(new EventHandler(displayData_event)); } catch (Exception ex) { // 跨线程更新错误提示,避免接收异常导致程序崩溃 this.Invoke((Action)(() => { error_tb.Text = $"接收异常: {ex.Message}"; })); } } private void displayData_event(object sender, EventArgs e) { dateTime = DateTime.Now; string time = $"{dateTime.Hour}:{dateTime.Minute}:{dateTime.Second}"; // 追加日志而非覆盖,保留所有历史接收记录 values_tb.AppendText($"{time}\t\t\t\t\t{inData}\r\n"); } private void Form1_Load(object sender, EventArgs e) // 程序启动初始化串口 { myPort = new SerialPort(); myPort.BaudRate = 9600; myPort.PortName = com; myPort.Parity = Parity.None; myPort.DataBits = 8; myPort.StopBits = StopBits.One; // 配置读写超时,避免ReadLine永久阻塞线程 myPort.ReadTimeout = 500; myPort.WriteTimeout = 500; myPort.DataReceived += MyPort_DataReceived; try { myPort.Open(); values_tb.Text = ""; error_tb.Text = "串口连接成功"; } catch (Exception exc3) { MessageBox.Show(exc3.Message, "串口打开失败"); } } // 窗口关闭时自动释放串口资源 private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (myPort == null) return; try { if (myPort.IsOpen) myPort.Close(); myPort.Dispose(); } catch { // 退出阶段的串口关闭异常直接忽略,不阻塞程序退出流程 } } private void exit_btn_Click(object sender, EventArgs e) // 退出程序 { Application.Exit(); } private void button2_Click(object sender, EventArgs e) // 手动关闭串口(可选保留) { try { if (myPort != null && myPort.IsOpen) { myPort.Close(); error_tb.Text = "串口已手动断开"; } } catch (Exception exc4) { MessageBox.Show(exc4.Message, "关闭失败"); } } }
配套配置注意事项
- 代码中新增了串口读写超时配置,避免
ReadLine()方法因为等不到行结束标志永久卡死后台线程 - 数据显示区改用
AppendText方法追加日志,原有直接赋值Text的写法会覆盖之前的接收记录,只能看到最后一条数据 - Arduino端回传数据请使用
Serial.println()方法发送,该方法会自动在数据末尾添加\r\n换行标志,匹配C#端ReadLine()的读取规则,避免超时。
内容的提问来源于stack exchange,提问作者KilerKazanu
相关产品推荐
相关产品推荐

