Skip to content

fix(device): 修复 macOS/Linux 下打开串口导致 ESP32 复位、握手失败 - #67

Merged
ca1e merged 1 commit into
EasyConNS:devfrom
SlenderData:fix/serial-dtr-rts-order
Oct 7, 2026
Merged

ca1e merged 1 commit into
EasyConNS:devfrom
SlenderData:fix/serial-dtr-rts-order

Conversation

@SlenderData

Copy link
Copy Markdown

变更说明

在 macOS 和 Linux 上,EasyCon 打开串口时会让带自动下载电路的 ESP32 开发板复位,导致握手每次都失败(#56)。

原因

  1. 在非 Windows 平台上,打开串口时系统会把 DTR 和 RTS 都拉高。
  2. SerialPort 默认 DtrEnable = false、RtsEnable = false,打开后会先撤销 DTR、再撤销 RTS。中间会短暂出现“RTS 拉高、DTR 未拉高”的状态,通过 ESP32 常见的自动下载电路把 EN 拉低,芯片就复位了。
  3. TTLSerialClient 打开串口后马上发 hello(A5 A5 81),这一次正好落在芯片重启期间被丢弃,握手超时。

改成先撤销 RTS、再撤销 DTR后,中间状态变成“DTR 拉高、RTS 未拉高”,只影响 GPIO0,EN 不受影响,所以不会复位。两种顺序最后都是两根线撤销,状态相同。

修改

只改了 src/EasyCon.Device/Connection/TTLSerialClient.cs,只在非 Windows 平台生效:

  • 创建 SerialPort 时,设置 DtrEnable = true、RtsEnable = true,让打开串口时两根线保持拉高。
  • Open() 之后,先设置 RtsEnable = false,再设置 DtrEnable = false。

Windows 走原来的逻辑,没有任何变化。握手流程和成功条件也完全不变,仍然以收到 0x80 为准。

为什么 Windows 不用这个方案:实测在 Windows 上这样做时,插上后首次打开仍然会复位,而且之后 hello 一直没有回复。我推测芯片在 GPIO0 被拉低时复位,进入了下载模式,但还没有用启动日志确认。

关联 Issue

Fixes #56

变更类型

  • Bug 修复
  • 小幅改进(不涉及核心功能与架构)
  • 功能 / 架构类变更(已先在 Issue 中讨论达成共识,Issue 链接:)
  • 文档 / 其他

人工审核声明

  • 我已逐行审核本 PR 的全部代码(包括 AI 生成的部分),理解并认可每一处改动
  • 我对本 PR 内容负责,它不是全由 Agent 决策产出

自查清单

  • dotnet build 通过
  • dotnet test EasyCon2.slnx -c Release 通过
  • dotnet format --verify-no-changes 无差异
  • 新功能 / 修复附带 NUnit 测试
  • 涉及脚本语言 / VM / 架构的改动已更新对应测试与文档

验证方式

测试硬件:ESP32-S3 + CP210x,115200。

EasyCon GUI 实测

平台 修复前 修复后
macOS 27.0(Apple Silicon),cu 和 tty 都测过 每次连接都复位,每次都失败 第一次 hello 就收到 80,没有启动日志,断开重连也一样 ✅
Linux:树莓派 5,Debian 13 每次连接都复位,每次都失败 第一次 hello 就收到 80 ✅
Windows 只有插上后首次连接会复位,失败后重连成功 代码没有改动,行为和原来一样

修复前 macOS 和 Linux 上的日志(每次连接都是这样):

>> A5 A5 81                      ← 被复位吞掉
<< 45 53 50 2D 52 4F 4D ...      ← "ESP-ROM:esp32s3..." 启动日志
<< FF                            ← 固件启动时输出的一个字节(每次不同)
>> A5 A5 81                      ← 已经是 9600 波特率的重试
<< 40 6A 08 ...                  ← 波特率不匹配,收到乱码
单片机连接失败: Timeout

修复后:

>> A5 A5 81
<< 80
单片机连接成功

撤销顺序的对比测试

用 .NET SerialPort 写了一个测试程序,每次打开串口后先监听 2 秒、不发数据,再发一次 hello:

  • default:.NET 默认设置,也就是修复前的做法。
  • ordered:本 PR 的做法。
平台 default ordered
macOS 每次打开都复位 不复位,hello 立即收到 80 ✅
Linux(树莓派 5) 每次打开都复位 不复位,hello 立即收到 80 ✅
Windows 只有插上后首次打开会复位 首次打开仍然复位,之后 hello 一直没有回复 ❌
测试代码(.NET 10 单文件程序:dotnet run wintest.cs -- <串口> default|ordered)
#:package System.IO.Ports@10.0.0
using System.IO.Ports;
using System.Text;

string port = args.Length > 0 ? args[0] : "COM6";
bool ordered = args.Length > 1 && args[1] == "ordered";

for (int i = 1; i <= 4; i++)
{
    var sp = ordered
        ? new SerialPort(port, 115200) { DtrEnable = true, RtsEnable = true }
        : new SerialPort(port, 115200);
    sp.ReadTimeout = 100;
    sp.Open();
    if (ordered)
    {
        sp.RtsEnable = false;
        sp.DtrEnable = false;
    }

    // 只监听 2 秒,不发任何数据:收到 ESP-ROM 说明这次打开触发了复位
    var buf = new List<byte>();
    var tmp = new byte[256];
    var t0 = DateTime.Now;
    while (DateTime.Now - t0 < TimeSpan.FromSeconds(2))
    {
        try { int n = sp.Read(tmp, 0, tmp.Length); buf.AddRange(tmp.Take(n)); }
        catch (TimeoutException) { }
    }

    // 再发一次 hello,看能否立即握手
    sp.DiscardInBuffer();
    sp.Write(new byte[] { 0xA5, 0xA5, 0x81 }, 0, 3);
    string reply;
    try { sp.ReadTimeout = 500; reply = sp.ReadByte().ToString("X2"); }
    catch (TimeoutException) { reply = "(超时)"; }
    sp.Close();

    bool rom = Encoding.ASCII.GetString(buf.ToArray()).Contains("ESP-ROM");
    Console.WriteLine($"{(ordered ? "ordered" : "default")} #{i}  复位: {rom,-5}  hello 回复: {reply}");
    Thread.Sleep(1000);
}
原始输出

macOS(/dev/cu.usbserial-2110)

default #1  复位: True   hello 回复: 80
default #2  复位: True   hello 回复: 80
default #3  复位: True   hello 回复: 80
default #4  复位: True   hello 回复: 80
ordered #1  复位: False  hello 回复: 80
ordered #2  复位: False  hello 回复: 80
ordered #3  复位: False  hello 回复: 80
ordered #4  复位: False  hello 回复: 80

Linux(树莓派 5,/dev/ttyUSB0)

default #1  复位: True   hello 回复: 80
default #2  复位: True   hello 回复: 80
default #3  复位: True   hello 回复: 80
default #4  复位: True   hello 回复: 80
ordered #1  复位: False  hello 回复: 80
ordered #2  复位: False  hello 回复: 80
ordered #3  复位: False  hello 回复: 80
ordered #4  复位: False  hello 回复: 80

Windows(COM6,每种模式运行前都拔插了 USB)

PS C:\Users\xxx\Downloads> dotnet run .\wintest.cs -- COM6 default
default #1  复位: True   hello 回复: 80
default #2  复位: False  hello 回复: 80
default #3  复位: False  hello 回复: 80
default #4  复位: False  hello 回复: 80
PS C:\Users\xxx\Downloads> dotnet run .\wintest.cs -- COM6 ordered
ordered #1  复位: True   hello 回复: (超时)
ordered #2  复位: False  hello 回复: (超时)
ordered #3  复位: False  hello 回复: (超时)
ordered #4  复位: False  hello 回复: (超时)

已知问题(不在本 PR 范围内)

Windows 上,插上 USB 后首次连接同样会因为复位而失败,重连后才能成功。这是原来就有的问题,原理相近,但不能用本 PR 的方式解决。

On macOS (and other non-Windows platforms) the OS raises DTR and RTS
when the port is opened. SerialPort then drops them DTR-first, which
briefly leaves RTS high with DTR low; through the auto-download circuit
on many ESP32 boards this pulls EN low and resets the chip on every
connect, so the hello sent right after opening was lost and the
handshake always timed out (EasyConNS#56).

On non-Windows platforms, open the port with DTR/RTS asserted and then
drop RTS before DTR. The final line state is unchanged (both low), but
the reset-triggering intermediate state is avoided. Windows behaviour
is left untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants