USB CDC ACM Serial Debugging: Line Coding, Control Line State, and Missing Data
How to debug USB CDC ACM virtual serial devices by inspecting SET_LINE_CODING, SET_CONTROL_LINE_STATE, bulk endpoints, and firmware behavior.
USB CDC ACM devices are used for virtual serial ports, device consoles, firmware tools, telemetry, test fixtures, and embedded diagnostics. They look simple from the application side: "open a COM port or /dev/ttyACM*, set a baud rate, read and write bytes. Underneath, the host and device still exchange class-specific USB requests and bulk transfers."
When a CDC device enumerates but no data moves, the capture can show whether the problem is descriptor layout, line coding, control line state, endpoint traffic, or firmware buffering.
CDC ACM Has Control and Data Interfaces
A typical CDC ACM device exposes a communication interface and a data interface. The host may send class-specific requests before data transfer begins.
Important evidence:
- communication interface descriptor
- data interface descriptor
- CDC functional descriptors
- notification endpoint
- bulk IN endpoint
- bulk OUT endpoint
SET_LINE_CODINGGET_LINE_CODINGSET_CONTROL_LINE_STATE
If these requests never arrive, the host may not have bound the expected CDC driver.
Baud Rate Is Often a Signal, Not a Physical UART
For many USB CDC devices, the baud rate is not physically meaningful in the same way it is for a UART. But the host still sends line coding. Firmware may use it to configure a bridge, ignore it, or validate it.
Capture questions:
- did host send
SET_LINE_CODING? - what baud rate, parity, stop bits, and data bits were requested?
- did firmware accept the request?
- did the host set DTR or RTS with
SET_CONTROL_LINE_STATE? - does firmware wait for DTR before sending?
Many "no serial output" problems are actually "firmware waits for DTR and the host never asserted it" or "application opened the port but did not configure it as expected."
Bulk Endpoints Prove Data Movement
After setup, CDC data usually moves over bulk endpoints. If writes from host appear on bulk OUT but no bulk IN response follows, firmware may not be sending. If bulk IN data appears but the application does not display it, the issue may be host application behavior.
Inspect:
- endpoint direction
- transfer lengths
- repeated NAK/timeout behavior
- actual payload bytes
- status of transfers
- ordering relative to line-state requests
This is how a USB capture becomes more useful than a terminal screenshot.
Where Bus Scope Fits
Bus Scope should help firmware teams keep descriptors, class requests, and raw endpoint data in one session. For CDC ACM debugging, it should answer:
- did the host bind CDC?
- what line coding did it send?
- did DTR/RTS change?
- did bulk OUT carry commands?
- did bulk IN carry responses?
- did the issue happen before or after serial-style traffic began?
For searches like "USB CDC ACM no data," "virtual COM port no output," or "SET_CONTROL_LINE_STATE DTR," the evidence is not in the terminal alone. It is in the USB class requests and endpoint transfers.
Identify the interface pair before inspecting serial data
The short answer: verify that the host bound a CDC ACM function with the expected communication and data interfaces, then follow the class requests and bulk endpoints attached to that function. Composite devices can expose several interfaces, and a COM-port label alone does not prove that the application opened the intended one.
| Evidence | What it proves | Failure clue |
|---|---|---|
| Interface Association/CDC functional descriptors | Which communication and data interfaces belong together | Missing or inconsistent functional descriptors |
| Interface numbers | Recipient for class-specific requests | wIndex targets a different interface |
| Bulk IN/OUT endpoint descriptors | Where serial payload should travel | Application waits on an endpoint that does not carry data |
| Notification endpoint | Optional serial-state signalling | Missing notifications may affect driver expectations |
| Driver binding event | Host recognised the function as CDC ACM | No class requests may indicate a binding/configuration issue |
Capture enumeration before opening the port. Starting a trace after the failure can leave out the descriptor evidence that explains why class requests or endpoint traffic look unusual.
Interpret line coding and control line state correctly
SET_LINE_CODING carries data rate, stop bits, parity, and data bits. On a USB-native virtual serial device, the advertised rate may be a compatibility signal rather than a physical UART setting; on a USB-to-UART bridge it may configure a real downstream link. The capture cannot decide which design the firmware intended, but it can show exactly what the host asked for and whether the device accepted it.
| Request | Capture fields to inspect | Diagnostic question |
|---|---|---|
| SET_LINE_CODING | Interface, seven-byte payload, status | Did host send the expected rate/parity/format and did device ACK? |
| GET_LINE_CODING | Returned seven bytes | Does firmware return a coherent/current configuration? |
| SET_CONTROL_LINE_STATE | DTR/RTS bit values and status | Does firmware start output only after DTR assertion? |
| SEND_BREAK, if used | Duration and preceding traffic | Is a terminal/tool exercising a special control path? |
Compare a working terminal and a failing application. If one sends DTR and the other does not, “no serial output” is no longer a generic data-path bug; it is a reproducible control-line difference. Do not assume DTR must be required—verify the device behaviour across captures.
Follow bulk data in both directions
Once configuration is complete, serial payload normally uses bulk OUT for host-to-device and bulk IN for device-to-host. Bulk NAKs are a normal flow-control response; a NAK by itself is not an error. Look at duration, retries, completed transfers, and whether data eventually appears.
| Trace pattern | What it supports | What it does not prove |
|---|---|---|
| Bulk OUT completes, no later IN payload | Host delivered command; firmware may be silent | That firmware crashed without additional evidence |
| Bulk IN payload completes, app shows nothing | Device sent bytes | Application/parser/terminal display is correct |
| Repeated IN polling/NAKs | Host is ready but device has no data yet | A USB fault by itself |
| OUT transfer stalls/resets | Endpoint/configuration/firmware issue | Exact root cause without descriptors and logs |
| Data follows DTR assertion | Control line may gate firmware output | Electrical UART behaviour behind a bridge |
Preserve raw payload with direction and time. A printable terminal transcript may hide control bytes, framing, or an earlier command that initiated the response.
Reproduce the no-data symptom in stages
- Capture enumeration and the port-open sequence.
- Record line coding and DTR/RTS state.
- Send one known command or wait for expected boot output.
- Check bulk OUT completion and subsequent bulk IN payload.
- Repeat with a working host application or OS if available.
- Export a window covering control requests through the last relevant bulk transfer.
| QA question | Evidence-backed answer |
|---|---|
| Did the host bind CDC ACM? | Descriptor/binding and class-request sequence |
| Which line settings were requested? | Exact SET_LINE_CODING payload |
| Was DTR/RTS asserted? | SET_CONTROL_LINE_STATE bits |
| Did bytes cross the bus? | Completed bulk IN/OUT transfer data |
| Where did the observable path stop? | Before configuration, after control state, at OUT, or at IN |
USB CDC ACM DTR/RTS debugging goes deeper on control state, USB serial COM-port disconnects covers re-enumeration, and USB descriptor debugging for HID and CDC helps validate the function before payload analysis.
Does a virtual COM baud rate always configure hardware?
No. It may configure a physical UART, be stored for compatibility, or be ignored by USB-native firmware. The capture shows the request and response; firmware documentation or code is needed to establish the design intent.
Why does one terminal work while another application receives no data?
Compare their class requests, DTR/RTS settings, line coding, opened interface, and initial bulk OUT data. Small differences in port-open behaviour can change a device that waits for host readiness.
For a useful firmware or application handoff, include the descriptor set, interface number, exact line-coding bytes, control-line state, first outgoing payload, first missing expected response, host OS/driver, and device firmware revision. This turns “serial is dead” into a bounded, repeatable report.
If completed bulk traffic proves that bytes reach the device and the expected response never returns, USB evidence has reached its limit. Add device-side logging around command parsing, buffering, task scheduling, and any downstream UART/transport instead of repeatedly changing terminal settings without a new hypothesis.
Likewise, if device-to-host bulk IN data is present but the application displays nothing, preserve the bytes, encoding assumptions, and read boundaries. The next investigation belongs to application buffering, protocol framing, permissions, or UI handling—not to USB enumeration.
Continue through the Bus Scope blog, review USB bulk transfer timeout analysis, or download Bus Scope to keep enumeration, control, and payload evidence in one session.
How do you validate a CDC ACM line-coding correction?
Build a small matrix of line coding and control-line state rather than testing one terminal until
it happens to work. Preserve interface number, SET_LINE_CODING payload bytes, decoded baud/data
bits/parity/stop bits, GET_LINE_CODING response when supported, DTR/RTS state, notification
endpoint activity, first bulk OUT bytes, first bulk IN response, and the application that opened
the port.
| Boundary | Evidence | Pass condition |
|---|---|---|
| Interface selection | CDC control/data interfaces and endpoint map | Requests target the intended control interface |
| Line coding | Exact seven-byte payload and decoded values | Firmware accepts and applies supported settings |
| Readback | GET_LINE_CODING or device-side confirmation |
Returned state matches the accepted configuration |
| Control lines | DTR/RTS transition and timing | Device readiness policy is explicit and repeatable |
| Data path | First OUT command and IN response | Bytes use the expected endpoints and framing |
| Reopen | Close/open sequence under same settings | No stale buffer or hidden terminal state is required |
Compare a working terminal and failing application with the same requested parameters. A terminal may assert DTR, send a newline, or delay before the first write; the visible difference is a controlled next test, not proof that the terminal “supports USB better.” If the firmware bridges to a physical UART, pair the USB trace with authorized device-side evidence before claiming the downstream line changed.
Repeat after unplug/re-enumeration and after one unsupported setting. The device should reject or normalize according to its documented policy and remain responsive. Use the CDC control-line state guide to isolate DTR/RTS behavior and the Bus Scope sessions guide to retain both application traces.
Decode the seven SET_LINE_CODING bytes independently: four little-endian baud-rate bytes followed
by stop-bit, parity, and data-bit fields. Preserve both raw and decoded forms. A host UI displaying
115200 does not prove the expected bytes reached the intended interface, and a firmware log
printing 115200 does not prove endpoint zero completed its status stage.
Run a matrix with the documented default, one common alternative, an unsupported value, close/open,
and unplug/reconnect. If the device normalizes unsupported settings, record the value returned by
GET_LINE_CODING; if it rejects them, record the exact control-transfer status and prove the next
valid request still succeeds.
Then send a short payload with visible framing, such as a known command plus terminator, and retain the corresponding bulk OUT/IN sequence. Check whether DTR or RTS must precede it and whether the device waits for a notification or delay. Acceptance joins class control and data behavior: the selected line state survives reopening, the first command is parsed once, the expected response returns, and no stale bytes leak from an earlier session.
Record host-side text encoding, newline convention, read chunk boundaries, and timeout separately from line coding. Baud, parity, stop bits, and data bits belong to the CDC/UART contract; application framing and encoding can still make correct USB bytes appear as an empty or corrupted terminal.
Keep that distinction explicit in the final handoff.
<!-- multilingual-related-reading:start -->Practical questions
What is the fastest reliable way to start?
Use the smallest representative case, write down the expected result, and change one variable. Confirm the basic path before adding filters, effects, edits, automation, or a larger source. This creates a baseline that can be compared after every later decision.
Related guides
Continue with the same-language pages below. They cover adjacent stages without changing the canonical owner of this topic:
<!-- multilingual-related-reading:end -->