Fix CODESYS COM.Open error 5004 on a serial port

6 min read · Published November 2, 2026

Error 5004 at COM.Open means the runtime never claimed a serial device, so baud, parity and wiring are out of the picture. On boards with onboard Bluetooth, the full UART sits on the radio and the header pins carry the small UART. Free the full UART with a device-tree overlay, confirm /dev/serial0 resolves to ttyAMA0, then loopback pins 8 and 10 before you touch the project.

Correction, November 2, 2026. Boot file paths updated for current images (/boot/firmware/), and the cause of the 5004 stated only as far as the open call shows it.

Error 5004 comes back from the COM.Open call itself: the runtime failed to open the port your number maps to, before any byte was framed. Baud, parity and wiring are out of the picture at this stage. On the popular single-board Linux controller with onboard Bluetooth, the full UART (ttyAMA0, a PL011) is attached to the Bluetooth module as shipped, and the header pins are driven by the small UART (ttyS0). Check that layout first, because a project mapped to ttyAMA0 is then opening the port the radio owns, not the header pins. The fix is one device-tree overlay, and the proof is a jumper between header pins 8 and 10.

What the error means at the open call

The CAA SerialCom library activates and configures a serial device through COM.Open, so this call is the first place an unusable port shows. Follow the chain from your project to the wire:

port number in the project
  -> SysCom entry in CODESYSControl.cfg
  -> device node under /dev
  -> kernel UART driver
  -> pin mux on GPIO14 and GPIO15
  -> line driver to RS232 levels

An open error forms in the first three hops. Everything below a successful open fails differently: a port that opens and moves no bytes points at wiring, voltage or the device settings. That split is the whole diagnostic. A 5004 sends you to the mapping; an empty receive block sends you to the wire.

Symptoms and what each points to

  • 5004 on a port number that worked on another controller. The mapping, not the hardware. Read the SysCom section of CODESYSControl.cfg on this board and compare every entry with ls /dev/tty*.
  • dmesg registers both UARTs. Lines naming ttyAMA0 as a PL011 rev2 at irq 87, and ttyS0 as a 16550 with a base baud of 31,250,000 Hz, say both drivers are loaded. Which one drives the header is decided by the symlinks.
  • ls -l /dev/serial0 resolves to ttyS0. Bluetooth holds the full UART and the header pins sit on the small UART. A project that opens ttyAMA0 on this layout is opening the radio's port, not the header pins.
  • The kernel command line shows 8250.nr_uarts=1 and console=tty1, with no serial console. The login shell is already out of the way. Keep it that way.
  • The open succeeds and the receive block stays empty. The mapping chain is clear; suspect the level shifter, the cable and the device settings.
  • Bytes arrive garbled at higher baud. The small UART's clock reference is drifting; see the worked example below.

Two routes from the UART to the header

Route Pins 8 and 10 carry Change needed Baud reference Bluetooth
A, full UART ttyAMA0 (PL011) enable_uart=1, dtoverlay=disable-bt, hciuart disabled its own clock, independent of the core clock lost
B, small UART ttyS0, as shipped point the SysCom entry at ttyS0 the core clock, so it shifts when that clock scales kept

Route A is the production answer, because the full UART holds its baud on its own clock. Route B answers a bench question in five minutes with no overlay work.

Recommendation: run route A on any link that works every shift: add enable_uart=1 and dtoverlay=disable-bt, disable hciuart, and point the SysCom entry at /dev/ttyAMA0. Keep ttyS0 for bench tests, and put a MAX3232-class shifter between the 3.3 V header and every RS232 device.

The fix in order

  1. Open /boot/firmware/config.txt (/boot/config.txt on images released before late 2023) and add two lines: enable_uart=1 and dtoverlay=disable-bt.
  2. Stop the service that attaches Bluetooth to the full UART: sudo systemctl disable --now hciuart.
  3. Check cmdline.txt in the same folder for a console=serial0 or console=ttyAMA0 argument and remove it if present.
  4. Reboot, then run ls -l /dev/serial0. The symlink must resolve to ttyAMA0.
  5. In CODESYSControl.cfg, make the SysCom entry for the port number you pass to COM.Open resolve to /dev/ttyAMA0. Restart the runtime after the edit; the configuration file is read at startup.
  6. Confirm the runtime can claim the node: ls -l /dev/ttyAMA0 shows the owning group, and sudo fuser -v /dev/ttyAMA0 must list no other holder.

Prove the pins with a jumper

Work below the PLC first.

  • Disconnect the level shifter and jumper GPIO14 (header pin 8, TXD) to GPIO15 (header pin 10, RXD).
  • Set the line and watch it: stty -F /dev/ttyAMA0 115200 raw -echo, then cat /dev/ttyAMA0 &, then echo hello > /dev/ttyAMA0. The word hello must come straight back.
  • No echo means the fault is still under Linux: symlink, overlay, permission or a holder. Leave the project alone until the shell loopback passes.
  • Stop the shell reader and confirm sudo fuser -v /dev/ttyAMA0 lists nothing.
  • Download the project and call COM.Open. The 5004 must be gone and the port must report open.
  • Keep the jumper, send a known string from the PLC and read it back with the SerialCom receive block. Matching bytes prove the path down to the pin.
  • Remove the jumper, insert the shifter, set the device's real baud, parity and stop bits, and repeat the exchange.

One safety point belongs here. The header pins are 3.3 V TTL. An RS232 line idles at a negative voltage and swings far beyond 3.3 V, and wired straight to the header it can destroy the pin driver. The shifter is part of the design, not an accessory.

Worked example: 9600 baud drifting on the small UART

Scenario: a checkweigher reports over RS232 at 9600 baud, 8N1, into a board left on route B. The bench loopback passes, because the board's transmitter and receiver share the same drifted clock; the checkweigher's clock does not, so in the cabinet the lines garble.

One bit at 9600 baud takes 1 / 9600 = 104.2 µs. A documented measurement on another board of the same family, with the core clock scaled, gave 156.3 µs per bit at the 9600 setting. Effective baud: 1 / 156.3 µs = 6400 baud. Error against the setting: 1 - (6400 / 9600) = 0.33, a 33 percent shortfall. A five-character report line at 8N1 is 5 bytes x 10 bits = 50 bits, due in 50 x 104.2 µs = 5.21 ms; on the drifted clock it takes 50 x 156.3 µs = 7.82 ms. The usual working tolerance for a 10-bit asynchronous frame is about 2 percent of baud error before sampling slips to the wrong bit, so at 33 percent every frame collapses.

How far your own board drifts depends on its clock settings; any drift past about 2 percent breaks frames the same way. Decision: this link moves to route A, whose baud reference sits on its own clock and holds 9600 exactly.

What still fails after the overlay

Cause Check Correction
The SysCom entry maps the port number to a device file that does not exist compare the entry with ls /dev/tty* correct the entry, or the port number passed to COM.Open
hciuart is still running systemctl status hciuart disable it and reboot
A serial console still claims the port grep the kernel command line in cmdline.txt, next to config.txt remove the serial console argument
The runtime user cannot open the node ls -l /dev/ttyAMA0 against the runtime process owner add the user to the owning group
The header is wired straight to an RS232 device the idle line reads about 3.3 V where RS232 idles negative insert a MAX3232-class shifter
Runtime package and SerialCom library versions out of step compare the installed runtime version with the library in Library Manager align the two versions

After the port opens

Once bytes move both ways, the work returns to the protocol. If the device speaks Modbus RTU, the next faults arrive as no reply and address errors, and when a slave drops out mid-run you reconnect the master after a slave outage. The data usually ends up on a screen, and whether that screen should be a dashboard or a simple sign is its own call: live factory data display, signage or dashboard.

Questions

Does error 5004 point at a wrong baud rate or parity setting?

No. The error appears at the open call, before any byte is framed, so the port settings in the project play no part. Changing baud, parity or stop bits will not clear a 5004. Work the mapping chain instead: the port number you pass, its SysCom entry in CODESYSControl.cfg, the device node it names, and any other process holding that node.

Can I skip the overlay and open ttyS0 instead?

Yes, for bench work. The small UART takes its baud reference from the core clock, so the effective baud moves whenever that clock scales. One documented case measured 156.3 microseconds per bit at a 9600 setting, an effective 6400 baud, 33 percent low. A loopback on a quiet bench clock can pass while a loaded board drifts, so use the full UART for any link that works every shift.

COM.Open now succeeds but the receive block stays empty. What changed?

The fault has moved below the mapping chain, to the wire. Split it with a jumper between header pins 8 and 10: if a shell loopback echoes, the board side is good and the remaining suspects are the level shifter, the cable and the device's own serial settings. If the loopback stays silent, go back to the symlink, overlay and permissions.

What do I lose by disabling onboard Bluetooth?

The radio. The overlay hands the full UART to the header pins, and the onboard Bluetooth module is left with no UART. If you need both, keep the small UART route at a low baud for short links, or add a USB serial adapter and point the SysCom entry at its device node, which normally appears as ttyUSB0.

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