Skip to content

[macOS] macOS 下第一次通过串口连接esp32单片机读到了固件默认打印信息导致握手必失败第二次正常 #56

Description

@SlenderData

环境

  • 系统:macOS 27.0
  • CPU:Apple Silicon / M4
  • .NET SDK:10.0.400
  • EasyCon 分支:dev
  • USB 转串口芯片:CP210x
  • 单片机:ESP32-S3
  • 波特率:115200
  • 串口:
    • /dev/cu.usbserial-2110
    • /dev/tty.usbserial-2110

问题描述

我正在尝试在 macOS 上从源码编译运行 EasyCon 的 dev 分支。

Avalonia GUI 可以正常编译和运行,macOS 也可以正常识别 USB 串口。

但是 EasyCon 无法完成与 ESP32-S3 的连接握手,最终提示:

单片机连接失败: Timeout

同一个 ESP32、同一个固件、同一个 USB 转串口设备:

  • 在 Windows 上使用 EasyCon 可以正常连接;
  • 在 macOS 上使用 Python + pyserial 可以正常通信;
  • 在 macOS 上使用一个独立的 .NET System.IO.Ports 测试程序也可以正常通信。

因此目前怀疑问题出在 EasyCon 自身的串口连接/握手逻辑,而不是硬件、固件、串口驱动或 .NET 本身。


Windows 测试

同一块 ESP32 连接 Windows 后,通过 EasyCon 发送:

A5 A5 81

单片机返回:

80

连接正常。


macOS + Python 测试

使用 Python + pyserial,连接:

/dev/cu.usbserial-2110

测试代码:

import serial
import time

port = "/dev/cu.usbserial-2110"

ser = serial.Serial(
    port=port,
    baudrate=115200,
    timeout=1,
    dsrdtr=False,
    rtscts=False,
)

ser.dtr = False
ser.rts = False

time.sleep(1)

ser.reset_input_buffer()

ser.write(bytes.fromhex("A5 A5 81"))
ser.flush()

time.sleep(0.2)

data = ser.read(ser.in_waiting or 1)

print("收到:", data.hex(" ").upper())

ser.close()

测试结果:

/opt/homebrew/bin/uv run /Users/xxx/PycharmProjects/PythonProject/.venv/bin/python /Users/xxx/PycharmProjects/PythonProject/串口测试.py 
收到: 80

因此 macOS 下 USB 串口本身工作正常。


macOS + .NET 独立测试

为了排除 .NET 串口库的问题,我另外创建了一个最小的 .NET 10 Console 项目,只使用 System.IO.Ports。

测试代码:

using System.IO.Ports;

const string portName = "/dev/cu.usbserial-2110";

using var port = new SerialPort(portName, 115200)
{
    ReadTimeout = 1000,
    WriteTimeout = 1000,
    DtrEnable = false,
    RtsEnable = false,
    Handshake = Handshake.None
};

Console.WriteLine($"打开 {portName}");

port.Open();

Console.WriteLine("串口已打开");

Thread.Sleep(1000);

port.DiscardInBuffer();
port.DiscardOutBuffer();

byte[] command = { 0xA5, 0xA5, 0x81 };

Console.WriteLine("发送: A5 A5 81");

port.Write(command, 0, command.Length);

Thread.Sleep(200);

Console.WriteLine($"BytesToRead = {port.BytesToRead}");

if (port.BytesToRead > 0)
{
    byte[] buffer = new byte[port.BytesToRead];

    int count = port.Read(buffer, 0, buffer.Length);

    Console.WriteLine(
        "收到: " +
        string.Join(
            " ",
            buffer.Take(count).Select(x => x.ToString("X2"))
        )
    );
}

运行结果:

打开 /dev/cu.usbserial-2110
串口已打开
发送: A5 A5 81
BytesToRead = 1
收到: 80

因此:

macOS
  ↓
System.IO.Ports
  ↓
CP210x
  ↓
ESP32-S3
  ↓
A5 A5 81
  ↓
80

这一整条链路是正常的。


EasyCon 的实际表现

EasyCon 可以打开串口,并且可以发送:

/dev/cu.usbserial-2110 >> A5 A5 81

但是无法正常完成握手。

返回信息出现了大量 ESP32-S3 ROM 启动信息,例如:

45 53 50 2D 52 4F 4D 3A 65 73 70 33 32 73 33 ...

转换成 ASCII 后可以看到:

ESP-ROM:esp32s3...

完整日志中可以看到:

[20:53:03] /dev/cu.usbserial-2110 >> A5 A5 81
[20:53:03] /dev/cu.usbserial-2110 << 45 53 50 2D 52 4F 4D ...
...
[20:53:04] /dev/cu.usbserial-2110 >> A5 A5 81
[20:53:04] /dev/cu.usbserial-2110 << FC 25
[20:53:04] /dev/cu.usbserial-2110 << 4E 29 01 84 84 94 84 02 45 A6 E1 39 AA 4C 8A 0E FD
[20:53:05] 单片机连接失败: Timeout

也就是说,EasyCon 并不是完全收不到串口数据,而是收到了 ESP32 的启动信息以及其他数据,但没有正确完成预期的 80 握手。


/dev/cu.* 和 /dev/tty.*

我分别测试了:

/dev/cu.usbserial-2110

和:

/dev/tty.usbserial-2110

两者结果基本一致,均无法完成 EasyCon 的握手。


其他尝试

目前已经尝试:

  1. 从 dev 分支重新编译 EasyCon;
  2. Avalonia GUI 正常编译、运行;
  3. 确认 .NET SDK 10.0.400 正常;
  4. 确认 macOS 可以正常识别 CP210x;
  5. 使用 /dev/cu.usbserial-2110;
  6. 使用 /dev/tty.usbserial-2110;
  7. 使用 Python + pyserial 测试 A5 A5 81 -> 80,正常;
  8. 使用独立 .NET System.IO.Ports 测试 A5 A5 81 -> 80,正常;
  9. 在打开串口后增加等待时间,再发送握手,仍然无法连接。

对比结果

环境 测试 结果
Windows EasyCon → A5 A5 81 → 80 ✅ 正常
macOS Python + pyserial → A5 A5 81 → 80 ✅ 正常
macOS 独立 .NET System.IO.Ports → A5 A5 81 → 80 ✅ 正常
macOS EasyCon → A5 A5 81 ❌ Timeout

因此目前的问题应该不是:

  • ESP32 固件问题
  • USB 转串口硬件问题
  • CP210x 驱动问题
  • 波特率问题
  • macOS 无法使用串口
  • .NET System.IO.Ports 完全不支持 macOS

而更可能与 EasyCon 的串口打开、ESP32 重启后的串口数据处理、或者握手逻辑有关。

Activity

  1. SlenderData commented on Aug 25, 2026

    @SlenderData
    Author

    免费的古法网页对话 GPT 只能把问题排查到这里了,临时的解决方案是跳过握手,直接将串口当作已连接的设备使用。

    替换文件 /src/EasyCon.Device/Connection/TTLSerialClients.cs

    using System;
    using System.Collections.Concurrent;
    using System.Diagnostics;
    using System.IO.Ports;
    using static EasyDevice.Connection.SerialPortClient;
    
    namespace EasyDevice.Connection;
    
    internal class TTLv2SerialClient(string name, int port) : IConnection
    {
        private bool _forceConnected = false;
        
        private SerialPortClient _serialPortClient = new(name, port);
    
        public override event BytesTransferedHandler BytesSent;
        public override event BytesTransferedHandler BytesReceived;
        public override event StatusChangedHandler StatusChanged;
    
        public override Status CurrentStatus
        {
            get
            {
                if (_forceConnected)
                    return Status.Connected;
    
                return _serialPortClient.CurrentState switch
                {
                    ConnectionState.Connected => Status.Connected,
                    ConnectionState.Connecting => Status.Connecting,
                    _ => Status.Error
                };
            }
            protected set => throw new NotImplementedException();
        }
    
        public override void Connect()
        {
            _serialPortClient.DataReceived += (sender, args) =>
            {
                BytesReceived?.Invoke(_serialPortClient.ConnectPort, args.Data);
            };
    
            Task.Run(() =>
            {
                try
                {
                    Console.WriteLine(
                        $"[{_serialPortClient.ConnectPort}] 准备打开串口..."
                    );
    
                    if (!_serialPortClient.Open())
                    {
                        throw new Exception("串口打开失败");
                    }
    
                    Console.WriteLine(
                        $"[{_serialPortClient.ConnectPort}] 串口打开成功"
                    );
    
                    // ========================================================
                    // 临时测试:跳过 EasyCon 的 A5 A5 81 -> 80 握手
                    // 只要串口能够成功打开,就认为设备已经连接
                    // ========================================================
    
                    _forceConnected = true;
    
                    Console.WriteLine(
                        $"[{_serialPortClient.ConnectPort}] 跳过握手,强制认为已连接"
                    );
    
                    StatusChanged?.Invoke(Status.Connected);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
    
                    _forceConnected = false;
    
                    StatusChanged?.Invoke(Status.Error);
                }
            });
        }
    
        public override void Disconnect()
        {
            _serialPortClient.Close();
        }
    
        public override void Write(params byte[] val)
        {
            BytesSent?.Invoke(_serialPortClient.ConnectPort, val);
            _serialPortClient.SendCommand(val);
        }
    
        public override void ClearQueue()
        {
            // SerialPortClient 无发送队列,空实现
        }
    }
    
    class TTLSerialClient : IConnection
    {
        readonly string _connStr;
        readonly int _port;
    
        SerialPort _sport;
    
        readonly List<byte> _inBuffer = new();
        readonly ConcurrentQueue<byte[]> _outQueue = new();
        DateTime _time = DateTime.MinValue;
        Status _status = Status.Connecting;
    
        public override event BytesTransferedHandler BytesSent;
        public override event BytesTransferedHandler BytesReceived;
        public override event StatusChangedHandler StatusChanged;
    
        Task _t;
        CancellationTokenSource source;
    
    
        TimeSpan _Timeout => TimeSpan.FromMilliseconds(Timeout);
    
        public double Timeout { get; set; } = 200;
    
        public bool Connected { get; protected set; }
    
        public override Status CurrentStatus
        {
            get => _status;
    
            protected set
            {
                if (_status == value)
                    return;
                _status = value;
                Task.Run(() => StatusChanged?.Invoke(_status));
            }
        }
    
        public TTLSerialClient(string connStr, int port = 115200)
        {
            _connStr = connStr;
            _port = port;
        }
    
        public override void Connect()
        {
            if (_t != null)
                return;
    
            _sport = new SerialPort(_connStr, _port);
    
            source = new CancellationTokenSource();
            var token = source.Token;
            _t = Task.Run(() =>
            {
                Loop();
            },
            token);
        }
    
        public override void Disconnect()
        {
            source?.Cancel();
            Connected = false;
            ClearQueue();
        }
    
        void Loop()
        {
            try
            {
                byte[] inBuffer = new byte[2550];
    
                _sport.Open();
    
                Console.WriteLine($"[{_connStr}] 串口打开成功");
    
                // ========================================================
                // 临时 macOS 测试模式:
                // 只要串口能够成功打开,就直接认为设备已连接。
                // 完全跳过 A5 A5 81 -> 80 握手。
                // ========================================================
    
                Connected = true;
                CurrentStatus = Status.Connected;
    
                Console.WriteLine(
                    $"[{_connStr}] 跳过 A5 A5 81 -> 80 握手,强制认为设备已连接"
                );
    
                StatusChanged?.Invoke(Status.Connected);
    
                var outBuffer = new List<byte>();
    
                while (!source.IsCancellationRequested)
                {
                    // ====================================================
                    // read
                    // ====================================================
                    if (_sport.BytesToRead > 0)
                    {
                        int count = _sport.Read(
                            inBuffer,
                            0,
                            inBuffer.Length
                        );
    
    #if DEBUG
                        Debug.WriteLine(
                            $"[{_connStr}] " +
                            string.Join(
                                " ",
                                inBuffer
                                    .Take(count)
                                    .Select(b => b.ToString("X2"))
                            )
                        );
    #endif
    
                        lock (_inBuffer)
                        {
                            _inBuffer.Clear();
                            _inBuffer.AddRange(
                                inBuffer.Take(count)
                            );
                        }
    
                        BytesReceived?.Invoke(
                            _connStr,
                            _inBuffer.ToArray()
                        );
                    }
    
                    // ====================================================
                    // write
                    // ====================================================
                    outBuffer.Clear();
    
                    while (_outQueue.TryDequeue(out var item))
                    {
                        outBuffer.AddRange(item);
                    }
    
                    if (outBuffer.Count > 0)
                    {
                        var bytes = outBuffer.ToArray();
    
                        _sport.Write(
                            bytes,
                            0,
                            bytes.Length
                        );
    
                        BytesSent?.Invoke(
                            _connStr,
                            bytes
                        );
                    }
    
                    Thread.Sleep(1);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(
                    $"[{_connStr}] 串口异常: {ex}"
                );
    
                Connected = false;
                CurrentStatus = Status.Error;
                StatusChanged?.Invoke(Status.Error);
            }
            finally
            {
                _sport.Close();
            }
        }
    
        public override void Write(params byte[] val)
        {
            if (_t == null)
                return;
    #if DEBUG
            Debug.WriteLine("Output: " + string.Join(" ", val.Select(b => b.ToString("X2"))));
    #endif
            _outQueue.Enqueue(val);
            _time = DateTime.Now;
        }
    
        public override void ClearQueue()
        {
            while (_outQueue.TryDequeue(out _))
            {
            }
        }
    }
  2. ca1e commented on Sep 1, 2026

    @ca1e
    Contributor

    这是esp固件已知问题,暂时没有很好的解决办法.

    根本原因就像你说的那样,打开串口后收到了 ESP32 的启动信息,这些信息根据s3固件作者排查无法通过编译条件完全排除.

    暂时的避免方法就是再连一次,让第一次把调试信息刷完第二次即可正常连接.

    临时跳过握手的方案不便采纳,因为串口的通用性,只有握手协议通讯成功才会被认定单片机可用

    如果还有其他好办法欢迎反馈

  3. changed the title [-][macOS] macOS 下无法通过串口连接单片机,但 Windows 和独立串口测试均正常[/-] [+][macOS] macOS 下第一次通过串口连接esp32单片机读到了固件默认打印信息导致握手必失败第二次正常[/+] on Sep 1, 2026
  4. added
    bugSomething isn't working
    on Sep 1, 2026
  5. zhantss commented on Sep 20, 2026

    @zhantss
    Contributor

    @SlenderData 正常行为串口插上之后这些固件信息就会输出掉,我怀疑macos环境下的连接串口会导致DTR/RTS跳变触发硬件复位,ESP32官方的开发板通常会不接这个引脚,所以即使不改程序也不会出现问题,你用的开发板可能包含了自动下载电路。你的python代码中加入了对DTR/RTS的禁用,所以macos下表现正常,但是你的.NET程序没有复现怀疑是某种重新链接所以没有复现。

    由于我没有macos,可以尝试在.NET程序中一并禁用DTR/RTS来尝试是否可以修复这个问题,如果有必要可以连硬件流控一并关闭 @ca1e

  6. SlenderData commented on Oct 2, 2026

    @SlenderData
    Author

    @zhantss 感谢思路,确实是 DTR/RTS 导致的复位,关键在于撤销的先后顺序。打开串口时 DTR/RTS 会先被拉高,SerialPort 随后先撤销 DTR、再撤销 RTS,中间短暂的“RTS 高、DTR 低”状态经自动下载电路拉低 EN,ESP32 就复位了。改成先撤销 RTS、再撤销 DTR 就不会复位。

    @ca1e 修复已提交 PR:#67。没有跳过握手,只在非 Windows 平台调整了撤销顺序。macOS 和 Linux(树莓派 5)实测第一次连接就能握手成功,测试程序和结果都在 PR 里。

    Windows 上只有插上 USB 后首次连接会复位,再连一次就能解决,影响不大,这次不处理。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions