Skip to content

LPC845-BRK: deploy must always unwedge the shared VCOM (pyocd/SWD flash leaves CDC in Windows error 31) #565

Closed

Description

@zackees

Summary

Flashing the LPC845-BRK via pyOCD/SWD wedges the board's shared USB VCOM on Windows: after a flash the CDC serial port goes to Device Manager error 31 ("This device cannot start") and only a physical USB replug brings it back. This breaks any flash-then-monitor flow (e.g. the FastLED AutoResearch RPC harness), which must reopen the serial port immediately after deploy.

Requirement: deploy must always unwedge — after every flash the VCOM must be usable again with no physical intervention.

Why this happens

The LPC845-BRK's on-board debug probe is a USB composite device exposing both a CMSIS-DAP HID interface and a CDC VCOM bridge (wired to P0_24/P0_25, per crates/fbuild-build/src/nxplpc/assets/arduino_stub/HardwareSerial.cpp). pyOCD opens the CMSIS-DAP HID to flash over SWD; its post-flash target reset leaves the composite device's CDC function wedged on Windows (error 31). Observed workaround so far is "don't issue a pyocd reset after flash" + physical replug.

Current state in fbuild

There is no LPC deployer wired yet. The daemon's platform dispatch falls through to:

crates/fbuild-daemon/src/handlers/operations/deploy.rs:707
    "deployer for {:?} not yet implemented",   // Platform::NxpLpc lands here

So today the board is flashed by pyOCD outside fbuild, with no recovery step — hence the manual replug. This is the deploy gap tracked by #551; this issue adds the "always unwedge" acceptance criterion to it.

Proposed fix

Implement an fbuild-deploy Deployer for Platform::NxpLpc that owns the unwedge:

  1. Prefer lpc21isp ISP-over-UART (the README's primary path, upload.protocol in lpc845brk.json). ISP flashing runs over the VCOM serial line itself and never opens the CMSIS-DAP HID, so the composite device cannot wedge. This is the cleanest "always unwedge."
  2. If SWD/pyOCD is used, own a deterministic post-flash recovery instead of leaving it to pyocd's default:
    • Skip pyocd's post-flash reset (the step correlated with the wedge); rely on the baked-in vector checksum + a clean DAP disconnect to start the image.
    • If the VCOM is still wedged, force a Windows USB re-enumeration of the composite devnode (restart the device / cycle the port) before returning success — modeled on the existing TeensyDeployer state machine, which already "takes a wedged Windows host and recovers it" (crates/fbuild-deploy/src/teensy/).
  3. Verify on hardware (LPC845-BRK): flash → reopen VCOM → AutoResearch RPC echo succeeds with no replug.

Acceptance

  • fbuild deploy <lpc845 project> -e lpc845brk flashes and the VCOM is immediately openable afterward, repeatably, with zero physical replugs.

Related: #551 (LPC8xx Stage 7 deploy support), #4 (use pyocd to flash).

Activity

  1. zackees commented on Jun 15, 2026

    @zackees
    MemberAuthor

    /clud-fix burn-down — skipped (downstream of #551)

    Touched as part of the #586 meta sweep. This issue is strictly downstream of #551 (no LPC Deployer exists yet, so there's nothing to attach the VCOM-unwedge logic to). Investigation report on #551 sketches the planned fix shape — including a dedicated vcom_unwedge.rs module specifically for the acceptance criterion this issue tracks. Will re-attempt this one automatically once #551 lands the LPC deployer scaffold.

    Filed via Claude Code — /clud-fix skill burn-down pass.

  2. zackees commented on Jun 16, 2026

    @zackees
    MemberAuthor

    Confirmed wedge on 2026-06-15 with refined repro + likely actual trigger

    Reproduced today running bash autoresearch lpc845brk --pin-toggle-rx --upload-port COM10 on Windows 10 against the LPC845-BRK. The wedge is real and consistent. Adding more diagnostic detail because the probable trigger is different from what this issue's original body assumed -- and that changes the fix shape.

    What FastLED's autoresearch actually does today

    ci/autoresearch/phases.py:1684-1716 invokes two separate pyocd subprocesses back-to-back:

    # Subprocess #1: pyocd load --no-reset firmware.bin
    load_cmd = pyocd_cmd_prefix + [
        "load", "--target", target,
        "--no-reset",
        str(firmware_bin),
    ]
    
    # Subprocess #2: pyocd commander -O reset_type=sw -c reset -c go -c quit
    reset_cmd = pyocd_cmd_prefix + [
        "commander", "--target", target,
        "-O", "reset_type=sw",
        "-c", "reset",
        "-c", "go",
        "-c", "quit",
    ]

    Each subprocess opens and closes the CMSIS-DAP HID interface independently. On the LPC845-BRK that interface is part of a composite USB device that also exposes the application VCOM CDC.

    Observation today, after the merged work

    I retested with the architected workarounds in place:

    • pyocd is using --no-reset on the load + reset_type=sw (NVIC SYSRESETREQ) on the separate commander -- not a hardware SRST, so the original "post-flash reset toggles the probe" hypothesis is ruled out for this exact code path.

    • The CMSIS-DAP probe firmware on the LPC4322 / on-board debugger is not observably reset by reset_type=sw (which is a target-side NVIC write, transparent to the probe).

    • And yet the CDC VCOM still wedges into Windows error 31 every single time. Reproducibly. The follow-up sequence is:

      1. pyocd load --no-reset -- opens HID, writes flash, closes HID. (Subprocess Esp32-s3 PSRAM use #1 exits.)
      2. pyocd commander -O reset_type=sw -c reset -c go -c quit -- opens HID again, sends NVIC reset to the target, closes HID. (Subprocess cdc on boot warning #2 exits.)
      3. Within ~50 ms of step 2, the Windows CDC sibling drops to error 31 and stays there until physical USB replug.

    The fact that this still happens with --no-reset + reset_type=sw means the wedge trigger is not the reset itself. It is something about the second HID open-close cycle on a composite device that confuses Windows' CDC driver.

    Refined root-cause hypothesis

    The Windows usbccgp.sys (USB composite class generic parent driver) treats CMSIS-DAP HID + CDC as two function drivers under one device. When pyocd opens the HID, usbccgp serializes the HID interface. When pyocd closes it and then a different process re-opens it within a short window, usbccgp emits a function-level reset event on the device. That function-level reset doesn't affect the HID side (pyocd just re-handshakes) -- but the CDC sibling sees its endpoint cleared and the higher-level usbser.sys driver doesn't recover, going to error 31.

    Two practical implications:

    1. A single pyocd invocation that does load + reset would likely sidestep this. Either pyocd flash --reset (full unified command) or pyocd load -O reset_type=sw firmware.bin (single subprocess, default-on reset) keeps the HID open for the whole sequence.
    2. The --no-reset + separate commander pattern is the active trigger, not the reset semantic. This is why reset_type=sw (which was added specifically for VCOM safety) doesn't save us -- it's not the reset that wedges the CDC, it's the inter-subprocess HID re-open.

    Proposed fix for the autoresearch path (one-line in ci/autoresearch/phases.py)

    Collapse the two subprocesses into one:

    # Single subprocess: load + reset_type=sw all in one HID session
    load_cmd = pyocd_cmd_prefix + [
        "load", "--target", target,
        "-O", "reset_type=sw",   # was: --no-reset, then a 2nd commander call
        str(firmware_bin),
    ]
    result = subprocess.run(load_cmd, env=env)
    # (no more 2nd `pyocd commander -c reset` step)

    I'd expect this to drop the wedge incidence dramatically -- maybe entirely, maybe to "rare" depending on whether the reset itself also nudges the CDC. Worth testing.

    If it still wedges after that, the architected answer is #595's lpc21isp path -- which sidesteps the composite HID entirely by going ISP-over-UART. (Currently lpc21isp isn't installed on this host, so I haven't been able to A/B that path empirically. The merged Rust LpcDeployer in fbuild#595 is the prerequisite.)

    Acceptance criteria for closing this issue haven't shifted

    The issue body's original requirement -- "deploy must always unwedge the shared VCOM" -- remains correct. The fix shape changes from "switch to SW reset" (already attempted, doesn't work) to one of:

    I lean A first (try the unified pyocd load -O reset_type=sw), B as the architected long-term answer, and C if neither A nor B holds 100% of the time.

    Cross-references from today's bench session

    Happy to test the A fix locally and report back.

    Filed via Claude Code on behalf of zachvorhies@protonmail.com.

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

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions