4. 串口连接实战:串口扫描与识别、串口参数配置、串口打开与关闭、异常处理机制

串口通信,说白了就是地面站和飞控之间的「握手」。握不上,后面所有数据都免谈。我见过不少新手,代码写得挺漂亮,结果卡在串口打不开这一步,急得满头大汗。其实没那么玄乎,咱们一步步来。

4.1 串口扫描与识别

你的电脑上可能挂着好几个串口设备——USB转串口、蓝牙虚拟串口、甚至某些调试工具也会占一个。怎么找到飞控那个?

我个人习惯先做一次全量扫描。Windows下可以用COM1到COM256挨个试,Linux下就遍历/dev/ttyUSB*和/dev/ttyACM*。但别傻乎乎地全开一遍,那样容易把别的设备搞乱。

我的做法:先列出所有可用端口,再根据设备描述或VID/PID过滤。Pixhawk的USB Vendor ID通常是0x26AC,这个信息在设备管理器或lsusb里都能看到。
// 伪代码:串口扫描与识别
List<string> ports = SerialPort.GetPortNames();
foreach (string port in ports)
{
    try
    {
        SerialPort sp = new SerialPort(port);
        sp.Open();
        // 发送MAVLink心跳检测
        sp.Write(heartbeatPacket);
        Thread.Sleep(100);
        byte[] buffer = new byte[1024];
        int len = sp.Read(buffer, 0, buffer.Length);
        if (IsValidMAVLinkPacket(buffer, len))
        {
            Console.WriteLine($"发现飞控设备:{port}");
        }
        sp.Close();
    }
    catch { /* 跳过不可用的端口 */ }
}

这里有个坑——有些串口驱动会返回「幽灵端口」,明明设备拔了,列表里还挂着。我在项目中遇到过这种情况,后来加了一层「物理存在检测」,用GetDeviceProperties查硬件路径,才彻底解决。

4.2 串口参数配置

找到端口只是第一步。参数配不对,数据全是乱码。飞控常用的参数组合就一套:115200波特率、8数据位、1停止位、无校验。你想想看,如果飞控那边设的是57600,你这边用115200去读,出来的东西跟天书一样。

参数 常见值 说明
波特率 115200 / 57600 Pixhawk默认115200,老版本可能用57600
数据位 8 固定,别改
停止位 1 One
校验位 None MAVLink协议自带校验,串口层不需要
流控制 None 地面站一般不用硬件流控
注意:波特率差一点都连不上。我曾经调了一下午,最后发现是飞控固件被人改成了921600。所以,如果连不上,先确认飞控那边的实际配置,别光盯着代码。

4.3 串口打开与关闭

打开串口很简单,一行sp.Open()就行。但怎么关、什么时候关,才是体现水平的地方。

我见过有人把Close()写在finally块里,这没错。但更稳妥的做法是加上超时机制——如果5秒内没收到任何数据,自动关闭并重试。无人机在天上飞,地面站不能死等一个串口。

public bool OpenSerialPort(string portName, int baudRate)
{
    try
    {
        _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        _serialPort.ReadTimeout = 2000;  // 2秒超时
        _serialPort.WriteTimeout = 2000;
        _serialPort.Open();
        return true;
    }
    catch (UnauthorizedAccessException)
    {
        Log.Error($"端口 {portName} 被占用");
        return false;
    }
    catch (IOException ex)
    {
        Log.Error($"端口 {portName} 异常:{ex.Message}");
        return false;
    }
}

public void CloseSerialPort()
{
    if (_serialPort != null && _serialPort.IsOpen)
    {
        _serialPort.DiscardInBuffer();  // 清空缓冲区
        _serialPort.DiscardOutBuffer();
        _serialPort.Close();
        _serialPort.Dispose();
    }
}

嗯,这里要注意:Dispose()一定要调。不调的话,GC回收不及时,下次再开同一个端口就会报「端口被占用」。我早期吃过这个亏,后来养成了习惯——关串口必调Dispose。

4.4 异常处理机制

串口通信的异常种类多,而且很多是运行时才暴露的。我总结了一套分级处理策略:

  • 可恢复异常:超时、缓冲区溢出。重试3次,每次间隔500ms。
  • 不可恢复异常:端口不存在、设备断开。直接上报,让用户插拔设备。
  • 临界异常:数据校验失败、帧同步丢失。丢弃当前包,等待下一个同步头。

核心原则:不要让异常传播到UI线程。地面站的界面一旦卡死,操作员就什么都干不了了。所有串口操作都跑在后台线程,异常通过事件或回调通知UI层。

// 异常处理示例
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    try
    {
        int bytesToRead = _serialPort.BytesToRead;
        byte[] buffer = new byte[bytesToRead];
        _serialPort.Read(buffer, 0, bytesToRead);
        ProcessMAVLinkPacket(buffer);
    }
    catch (TimeoutException)
    {
        // 超时正常,继续等待
    }
    catch (InvalidOperationException)
    {
        // 端口已关闭,停止读取
        StopReading();
    }
    catch (Exception ex)
    {
        Log.Error($"数据接收异常:{ex}");
        // 上报给UI层
        OnSerialErrorOccurred(ex);
    }
}

为什么会这样?因为串口是物理设备,随时可能被拔掉、被其他程序抢占、甚至驱动崩溃。你不能假设它永远可用。我见过最极端的情况——USB线接触不良,导致串口每秒断开重连一次。如果没有异常处理,地面站直接就崩了。

4.5 本章知识体系

下面这张图把串口连接的完整流程串起来了。你可以把它贴在工位旁边,写代码时对照着看。

串口连接与数据采集流程 扫描可用串口 识别飞控设备 配置串口参数 打开串口 数据采集与解析 关闭串口与释放资源 异常处理 超时重试 / 上报UI 丢弃无效包 每个环节都可能抛出异常,必须分层处理。不要试图用一个 try-catch 包住所有逻辑。

串口连接这件事,说难不难,说简单也不简单。关键是把每个环节的异常都想到、处理好。做到这一步,你的地面站才算有了「工业级」的底子。