如何用C#多阶段任务替代循环休眠实现Arduino串口控制?
优化C#串口与Arduino伺服通信的多阶段任务实现
核心优化思路
用TaskCompletionSource<T>结合串口DataReceived事件实现无轮询的异步等待,替代原有的Thread.Sleep轮询方案,同时用异步Task串联多阶段任务,既降低资源占用,又提升响应性和线程安全性。
实现步骤与代码示例
1. 封装串口通信基础类
先封装一个串口操作类,核心是实现异步等待指定响应的方法:
using System.IO.Ports; using System.Threading.Tasks; public class ArduinoSerialComm { private readonly SerialPort _serialPort; private TaskCompletionSource<string>? _responseTcs; private readonly object _lockObj = new object(); public ArduinoSerialComm(string portName, int baudRate = 9600) { _serialPort = new SerialPort(portName, baudRate) { ReadTimeout = 5000, WriteTimeout = 5000, NewLine = "\r\n" }; _serialPort.DataReceived += SerialPort_DataReceived; _serialPort.Open(); } private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { lock (_lockObj) { if (_responseTcs == null) return; string response = _serialPort.ReadLine().Trim(); // 匹配到目标响应时,完成任务 if (_responseTcs.Task.Status != TaskStatus.RanToCompletion) { _responseTcs.SetResult(response); } } } /// <summary> /// 发送指令并异步等待指定响应 /// </summary> /// <param name="command">要发送的指令</param> /// <param name="expectedResponses">等待的目标响应集合</param> /// <returns>实际接收到的响应</returns> public async Task<string> SendCommandAndWaitResponse(string command, params string[] expectedResponses) { lock (_lockObj) { if (_responseTcs != null && !_responseTcs.Task.IsCompleted) { throw new InvalidOperationException("已有未完成的等待任务,禁止并发发送指令"); } _responseTcs = new TaskCompletionSource<string>(); } try { _serialPort.WriteLine(command); string receivedResponse = await _responseTcs.Task; // 验证响应是否符合预期,不符合则抛出异常 if (expectedResponses.Length > 0 && !expectedResponses.Contains(receivedResponse)) { throw new InvalidOperationException($"收到非预期响应:{receivedResponse}"); } return receivedResponse; } finally { lock (_lockObj) { _responseTcs = null; } } } public void Close() { if (_serialPort.IsOpen) { _serialPort.Close(); } } }
2. 实现多阶段异步任务(以归位流程为例)
把归位拆分为「发送归位指令→等待归位完成→发送回退指令→等待回退完成」四个异步阶段,用await依次执行:
public class ServoController { private readonly ArduinoSerialComm _serialComm; public ServoController(ArduinoSerialComm serialComm) { _serialComm = serialComm; } /// <summary> /// 异步执行归位+回退完整流程 /// </summary> public async Task ExecuteHomeAndRetractAsync() { // 阶段1:发送归位指令,等待"Home done"响应 Console.WriteLine("发送归位指令..."); await _serialComm.SendCommandAndWaitResponse("HOME", "Home done"); Console.WriteLine("归位完成"); // 阶段2:发送回退指令,等待"Retract done"响应 Console.WriteLine("发送回退指令..."); await _serialComm.SendCommandAndWaitResponse("RETRACT", "Retract done"); Console.WriteLine("回退完成"); } }
3. 调用示例(主线程/UI线程均可)
class Program { static async Task Main(string[] args) { using var serialComm = new ArduinoSerialComm("COM3"); var servoController = new ServoController(serialComm); try { await servoController.ExecuteHomeAndRetractAsync(); Console.WriteLine("完整流程执行完毕"); } catch (Exception ex) { Console.WriteLine($"执行出错:{ex.Message}"); } } }
关键优势说明
- 无轮询休眠:通过
DataReceived事件触发任务完成,CPU资源占用极低 - 天然异步:用
await替代线程阻塞,不会卡死UI线程(如果是WinForms/WPF应用) - 线程安全:通过锁控制
TaskCompletionSource的并发访问,避免多实例冲突 - 可扩展性:新增任务阶段只需添加
await SendCommandAndWaitResponse调用,逻辑清晰
注意事项
- 确保Arduino端发送的响应末尾包含串口类指定的
NewLine(示例中是\r\n),否则ReadLine会无法正确读取 - 可根据需求添加超时处理:给
TaskCompletionSource设置超时,避免无限等待 - 如果需要支持并发任务(比如控制两台电机独立操作),可以给每个电机分配独立的
ArduinoSerialComm实例,或者修改通信协议加入电机标识
内容的提问来源于stack exchange,提问作者Cameron Hadland
相关产品推荐
相关产品推荐

