PL2303 USB serial reads fail on Linux: fix the driver binding first

8 min read · Published November 10, 2026

A PL2303 USB serial adapter that worked on Windows and reads nothing on Linux is usually held by the wrong driver: usbserial loaded with vendor and product ID arguments claims the device and blocks the pl2303 driver. Remove the loaded modules, run modprobe pl2303 with no arguments, and reopen the port at the known 38400 8-O-1 framing with a 2 second test timeout.

A PL2303 USB serial adapter that shows the PLC's output in a Windows terminal but reads nothing on Linux is usually held by the wrong kernel driver. The serial settings stay innocent until that is fixed. The setup that failed loaded the generic usbserial module by hand with vendor=0x067b product=0x2303 and then loaded pl2303 on top. Those ID arguments hand the adapter to the generic driver and keep the dedicated pl2303 driver from attaching, which leaves minicom reporting Offline and output arriving only sometimes. Clear the related modules and run modprobe pl2303 on its own: it loads usbserial as a dependency, the node comes back, and the port reads at the framing Windows already confirmed.

Why does a PL2303 adapter read on Windows but fail on Linux?

Because Windows and Linux run separate driver stacks. The Linux side was loaded with modprobe usbserial vendor=0x067b product=0x2303, which hands the adapter to the generic usbserial driver and stops the dedicated pl2303 driver from ever binding to it.

The vendor and product values exist for the generic fallback. The usbserial module covers adapters whose chips have no dedicated kernel driver, and the ID arguments are how you tell that fallback which device to claim. The PL2303 chip has a dedicated driver that carries its own ID table, so the arguments buy nothing here and cost plenty: usbserial claims the device, pl2303 never attaches, and reads arrive only intermittently. The working order is the reverse. With no related modules loaded, a bare modprobe pl2303 brings in the dedicated driver and loads usbserial automatically as its dependency.

What do the symptoms point to before you change a setting?

The symptom splits the fault into layers before you touch baud or parity: no device node or minicom stuck on Offline points at the driver binding, a read that hangs forever points at timeout=None, and bytes that arrive garbled point at the serial framing.

  • Minicom reports Offline every time. The adapter has no working binding on this side, so the terminal has nothing dependable to report; the PLC keeps sending regardless.
  • Python opens the port and then hangs. timeout=None puts read() into blocking mode, and it returns only when bytes arrive; with the binding broken, none do.
  • Output shows on Windows and only sometimes on Linux. The PLC, the cable and the format are already known good, so what changes between attempts is the module state on the Linux box.
  • Bytes arrive but the characters are wrong. Framing or flow control. This layer only exists once a node is open, so it is the second check.
  • The node name moves between replugs. Linux hands out ttyUSB numbers in detection order, so take the current path from dmesg each time.

An empty return after a timeout has its own follow-up pattern, covered from the programming side in fix a USB serial adapter timeout when programming a text panel.

Which layer is broken: the USB driver or the serial settings?

Check the USB driver layer first whenever a Windows terminal shows the same PLC string through the same cable, because that result already rules out the PLC, the cable and the 38400 8-O-1 framing, and leaves the Linux attachment of the adapter as the suspect.

The two layers fail differently. A broken binding delivers nothing at all, so no reader can be blamed. A framing error opens the port and delivers wrong characters: 38400 read as 19200 produces steady gibberish, and a flow-control mismatch holds bytes back or swallows control characters that were meant as data. Changing baud or parity cannot repair a port that never bound, which is why the fix below works on the module layer and leaves the PLC program untouched. When the evidence lands on framing after all, the same mismatch is described in HMI panel not talking to the PLC: serial settings mismatch.

How do you check the driver binding before opening the port?

Run dmesg right after you plug the adapter in and look for two things: the USB ID line naming vendor 0x067b and product 0x2303, and the line that attaches the converter to a ttyUSB node; the second line is the binding.

Four checks, in this order.

  1. Plug the adapter in and read dmesg: you want the ID line with vendor 0x067b and product 0x2303, followed by a line that names the converter driver and the ttyUSB node it created.
  2. List the loaded modules and note whether usbserial and pl2303 are present, and whether usbserial was loaded with ID options this boot.
  3. Look for the node itself in /dev, for example /dev/ttyUSB0.
  4. Close minicom and any Python process first, so a reader holding the port open cannot disguise itself as a dead binding.

How do you load the driver so the port comes back?

Unload the related modules, then run one command with no arguments: modprobe pl2303. The dedicated driver registers the adapter's IDs, pulls usbserial in as a dependency on its own, and the node appears at the next plug-in.

  1. Close minicom and any Python process that has the port open.
  2. Remove the modules left over from the manual ID load, pl2303 and usbserial. On a machine where other USB serial devices are running, check first what depends on them, because a second adapter may need usbserial loaded with its own IDs.
  3. Run modprobe pl2303 with no arguments. The module loader pulls usbserial in as a dependency by itself, which is the behavior the manual sequence was defeating.
  4. Unplug and replug the adapter, then read dmesg again: the attach line and a node path such as /dev/ttyUSB0 confirm the binding.
  5. Use the exact node path dmesg printed in minicom or in the application.

Keep the manual ID command out of this machine's routine from here on. Module options given on the command line apply to the module for every device it touches, so options meant for one adapter land on all of them. If a second USB serial device on the box genuinely needs usbserial with IDs, configure that device separately and leave the shared pl2303 load clean.

Which serial settings do you open the port with once the node exists?

Open the port with the exact framing Windows displayed: 38400 baud, 8 data bits, odd parity, 1 stop bit, hardware flow control off, and give the read a 2 second timeout while you test so a dead layer returns instead of hanging.

Setting Value Role in the test
baudrate 38400 Must match the PLC serial port configuration.
bytesize 8 (serial.EIGHTBITS) Character width.
parity odd (serial.PARITY_ODD) Character framing.
stopbits 1 (serial.STOPBITS_ONE) Character framing.
rtscts 0 Hardware flow control off.
xonxoff 1 Software flow control on, as in the run that worked on Windows.
timeout 2 Test value; None blocks forever, so keep a number while diagnosing.

In serial shorthand these values read as 38400 8-O-1: 38400 baud, 8 data bits, odd parity, 1 stop bit. The shorthand covers only speed, width, parity and stop bits, so the rtscts and xonxoff lines carry the flow-control half of the configuration. Keep the values Windows displayed for the first comparison, then change one line at a time and trigger the same button press. If the framing is right and bytes still stop partway through a message, compare flow control against the PLC port configuration.

import serial

ser = serial.Serial(
    "/dev/ttyUSB0",              # the node path dmesg printed for this adapter
    baudrate=38400,
    parity=serial.PARITY_ODD,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS,
    rtscts=0,
    xonxoff=1,
    timeout=2,                   # illustrative test value; None blocks forever
)
data = ser.read(20)

Every name in that call is the pyserial API; the node path and the 2 second timeout are this test's values. Trigger the button once: a 20 character bytes object back inside the timeout clears the driver layer and the framing layer in one observation, and an empty bytes object after 2 seconds means no bytes reached the port, which sends you back to the module checks. The string the PLC sends also needs a terminator before the application can tell one message from the next, so capture the raw bytes and parse around the character the PLC actually sends. Joining arriving bytes into complete lines is the same delimiter problem described in accumulate multiline serial reads into variables in CODESYS.

What does a working example look like from plug-in to first read?

Take a logging box that must read a 20 character ASCII line sent by the PLC on each button press through a PL2303 adapter at vendor 0x067b, product 0x2303: at 38400 baud the line occupies the wire for 5.7 milliseconds, well inside a 2 second read timeout.

The line time is checkable arithmetic. With odd parity and one stop bit, each character takes 1 start bit + 8 data bits + 1 parity bit + 1 stop bit = 11 bits. A 20 character line takes 20 × 11 = 220 bits. At 38400 bit/s the line occupies the wire for 220 ÷ 38400 = 0.0057 s, about 5.7 ms, and the port passes 38400 ÷ 11, roughly 3490 characters per second. The 2 second timeout is about 350 times the line time, so an empty read after a confirmed button press cannot be blamed on a slow link: the bytes stopped before the driver.

Run the scenario end to end.

  1. Remove the modules from the earlier ID load, then run modprobe pl2303 on its own.
  2. Replug the adapter; dmesg shows the pl2303 attach and creates /dev/ttyUSB0.
  3. Open the port with the table above and the 2 second timeout.
  4. Press the button: 20 characters return in roughly 5.7 ms of wire time, far inside the 2 s budget.
  5. Press it five times: five strings arrive, and the application logs each one.

That is the whole result: the port reads at every press, with no change to the PLC program and no change to the 38400 8-O-1 format.

How do you confirm the fix holds and keep it holding?

Trigger the PLC's send event at least five times and require the same string every time, first from minicom and then from the application itself, and recheck dmesg for the current ttyUSB path every time the adapter is replugged.

Close the terminal before you open the port from the application, so a reader holding the device open is caught as its own fault rather than folded into the driver question. Once the modules sit in their normal state, the kernel binds pl2303 to the adapter at plug-in with no manual command; the manual load in the fix above exists to clear the damaged state the ID arguments created. If a second USB serial adapter joins the box later, run the dmesg check again before assuming the first adapter still owns /dev/ttyUSB0.

Recommendation: On a Linux box whose dmesg names a PL2303 adapter as vendor 0x067b and product 0x2303, clear the related modules, run modprobe pl2303 with no arguments, keep the read timeout at 2 seconds while testing, and reserve modprobe usbserial vendor=... product=... for adapters whose chips have no dedicated kernel driver.

Questions

When is modprobe usbserial with vendor and product arguments the right command?

For adapters built on chips that have no dedicated kernel driver. The generic usbserial module has no ID table of its own, so the vendor and product values tell it which device to claim. A PL2303 adapter has a dedicated driver that already lists its IDs, so the bare modprobe pl2303 is the correct command there, and the ID form only interferes.

Which ttyUSB node belongs to my adapter when two USB serial devices are plugged in?

Watch dmesg at the moment you plug the PL2303 adapter in: the attach line names the USB path and the node it created, for example ttyUSB0. Unplug and replug one device at a time and note which node appears and disappears. Node numbers are handed out in detection order, so a different plug order gives different numbers; read the path from dmesg each time.

Does the same PL2303 failure appear on Windows?

It can, with a different surface. Windows shows the adapter under Ports (COM & LPT) in Device Manager, or under Other devices with an error when the driver is missing or the wrong version is installed, which has been a known issue with PL2303 adapters on Windows 11. The fix on that side is the driver package from the adapter maker, not module options.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote