Build a custom Modbus RTU device descriptor in CODESYS

9 min read · Published November 5, 2026

A custom device descriptor in CODESYS gives a Modbus RTU drive named registers and channels that appear as soon as the device is added. Build the register list with function codes and scaling, confirm the reference-to-offset conversion with one holding-register read, then package channels, library and function block. Keep reads grouped within the drive's supported range and write channels gated.

Yes. CODESYS can carry a custom device description for a Modbus RTU slave, so the drive appears in the project tree with readable register names and, at the deeper integration levels, channels and a function block already in place. The work that decides whether the description is worth having happens before the description file: a register list built from the drive's serial communication manual, and one confirming read that settles how addresses are counted.

This article walks that order. It shows what belongs in the register list, how a documented 4x reference turns into the offset the protocol carries, how a single status read confirms the convention, how the descriptor and its default channels get built, and when a plain structure with a function block does the same job for less effort.

Can CODESYS describe a Modbus RTU slave with named registers?

Yes. A custom device description gives the slave a named place in the project tree: registers and coils carry readable names, channels can be predefined so they exist the moment the device is added, and a library plus a function block in IOConfigGlobals can load with it.

Under that sits the structure the CODESYS Modbus Master (RTU) SL data sheet describes: multiple slave devices inserted below a Modbus master, each carrying its own slave address and a series of Modbus commands. A custom description builds on it in three levels. Level one pairs readable names with the registers and coils the application uses, which ends the per-project address lookup.

Level two predefines the channels, so adding the device to a project produces configured reads and writes that only need mapping into variables. Level three installs a library into the Library Manager and creates a function block in IOConfigGlobals with the channel parameters already mapped into it, so using the drive means adding the device and calling the block. Every level above the first rests on the same two foundations: a correct register list and one confirming read.

What goes into the register list before you build anything?

Write down each register the application needs with six things attached: its documented address, function code, read or write access, register count, scaling and permitted range, and keep status reads separate from command writes from the first line.

Pull the entries from the drive's serial communication manual, from the drive maker or its distributor, and match the manual to the drive firmware in the field. Function code 03 reads holding registers, 06 writes a single register, and 16 (hex 0x10) writes a block of them, so each list line names the one it needs.

A value wider than 16 bits, such as a 32-bit REAL spread over two registers, needs its word order decided here too; the article on converting a CODESYS REAL into two Modbus WORD registers works that choice through. The split matters later: status and measured values feed displays and logic, while command and configuration writes change machine behavior, so the two groups get different treatment in the application and in testing.

Register reference or protocol offset: which number does the channel editor want?

The wire carries 0-based offsets and manuals commonly print 1-based references, and the gap for the standard holding-register block is exactly 40001: a register documented as reference 40002 is address 1 on the wire, so type reference minus 40001 into the channel and confirm it with one read.

The conversion for the 4x block is offset = reference - 40001. Some manuals print plain decimal offsets already, and a few print hexadecimal, so the manual's own explanation of its address column comes first.

A convention mismatch hides well: communication succeeds, the value updates on schedule, and the register behind it is the neighbor of the one you wanted. Resolve the rule from the manual and the channel editor's documentation, then confirm it with the read in the next section. One known value settles the rule for every register in the manual, so no map gets corrected register by register.

What confirms the connection before you package any channels?

One holding-register read settles it: set the master to the drive's slave address and serial format, 19200 baud with 8 data bits, even parity and 1 stop bit as the specification's default, read one status register the drive shows on its keypad, and compare the two numbers.

The descriptor packages data access; wiring, framing and a duplicated slave address are separate hardware and settings jobs, and repackaging a descriptor fixes none of them. Take the configured slave address and serial settings from the drive's own parameters, set the master to match, and check the segment wiring against the manual; on a drive still at factory settings, the default framing quoted above is the first thing to try.

Configure a single read for the status register picked from the list, run it, and compare the result with the keypad. Three outcomes sort the faults:

  • A timeout points at wiring, framing, the address or a slave that is not answering, and the article on fixing a Modbus RTU master that gets no reply and address errors walks that pair of failures.
  • An exception means the drive answered and rejected the request, so check the function code, the address, the quantity and the access permission.
  • A value that returns but disagrees with the keypad points at the address convention, the scaling or the word order.

Read only until the numbers repeat; command writes stay out of this test.

How do you build the descriptor and define its default channels?

Build it in the device-description format your CODESYS version documents, attach the validated names and addresses, define one channel per register group, then install the description and add the device to a clean test project to watch the channels appear. The syntax and the install workflow belong to the CODESYS version in use, so that version's device-description development documentation is the reference for those steps.

  1. Enter every required register and coil into the description with the name validated against the manual.
  2. Define the default channels from the confirmed list, each carrying its function code, starting address and quantity.
  3. Check each grouped read: keep it inside a range the drive supports from end to end, because a request that crosses an unsupported register fails as a whole.
  4. Install the description through the version's documented workflow.
  5. Add the device to a clean test project.
  6. Compare what appears against the single-read test: expected names, expected functions, expected addresses.
  7. Connect machine logic only after that comparison passes.

For scale, the Modbus application protocol specification caps a function 03 read at 125 registers and a function 16 write at 123, limits wider than most drive maps need. A register list alone is not an installable device; the description file is.

Worked example: which offset reads the status word and output frequency?

Offset 0 and offset 2: with the status word documented at reference 40001 and the output frequency at reference 40003, both read in one function 03 request of two registers from starting address 0; a returned frequency count of 3000 at 0.01 Hz per bit reads 30.00 Hz.

Scenario. A drive sits on the bench at slave address 1, 19200 8-E-1, running with its keypad showing 30.0 Hz. Its manual lists the status word at 4x reference 40001 and the output frequency at 40003, 16-bit, scaled 0.01 Hz per bit.

The conversions:

  • Status word: 40001 - 40001 = 0, so its starting address is 0.
  • Output frequency: 40003 - 40001 = 2, so its starting address is 2.
  • One function 03 request at address 0 with quantity 2 covers both registers, and the whole span sits inside the drive's supported range.

The master sends function 03, address 0, quantity 2. The drive returns 16#0008 for the status word and 16#0BB8 for the frequency. Decoding: 16#0BB8 = 11 × 256 + 184 = 3000, and 3000 × 0.01 Hz per bit = 30.00 Hz, matching the keypad's 30.0 Hz. The status word decodes against the bit table in the same manual. The convention is confirmed, and the channels can be packaged.

The counter-case shows what the step catches: a channel typed as 40001 sends a request for offset 40001, which sits far past the end of this drive's map, and the drive answers with exception code 02, illegal data address, so nothing reads at all. Every other register in this manual subtracts 40001 the same way, and one confirming read carries the rule across all of them.

When is a register structure or function block enough instead of a descriptor?

When the map serves one project or a handful of drives, skip the descriptor: a structure with a name for every register, such as Read(Register.CmdSpeed), plus one function block that handles the requests, exports cleanly and imports into the next project with no description file to maintain.

Build the structure with one member per register, write the access calls through it, and put the request handling and scaling in a single function block with one property per value; export the module or save it into a project library and the next project imports the whole thing. That is the fastest route to reuse, and it keeps communication behavior visible in code the team already reads.

The descriptor route earns its keep when the same map is about to repeat across projects and teams: adding the device produces named channels with no address lookups, and the deepest level adds the library and the IOConfigGlobals function block so the application maps parameters and starts using values immediately. A function block approach also puts device settings in code you control at runtime, which is the pattern the article on changing Modbus device settings at runtime with ModbusFB applies. Choose the level by counting projects: three or more projects on one map justify the description file.

How do you keep command writes and stale data out of the machine logic?

Map reads and writes through one function block, expose a validity flag beside every value, and gate command writes behind the machine's own conditions, so a timeout degrades to a known state instead of a speed reference that froze at its last value.

Scaling, unit conversion and validity live in that one block, so the rest of the program sees engineering values plus a fresh flag and never a raw register. Drive the flag from the master's communication diagnostics and decide now what the machine does when it drops: hold the last safe state, stop, or alarm.

Keep each write as narrow as the job needs, one register with function 06 where a function 16 block write would touch neighbors, and keep write channels behind the application's interlocks. A frozen speed reference is the classic way a serial link becomes a production incident, and the article on making a Modbus master reconnect after a slave outage covers the recovery side.

A status word carried with a fresh flag is also a clean feed for shop-floor visibility; the screens in our shop-floor and andon board category are built for that kind of board. The safety rule that goes with command writes: keep them off the live machine until the convention test has passed on a bench drive, because a channel pointed at the wrong register writes the wrong parameter.

Recommendation: package a full device descriptor once the same drive map is heading into its third project; below that count, ship the named register structure with one function block. Whichever route you take, confirm the offset convention first with a single function 03 read of one status register at the drive's slave address, and package no channel until that read matches the drive keypad.

Questions

Does a device descriptor change what travels on the wire?

No. The bus still sees the same binary RTU frames: slave address, function code, starting address and register count. A descriptor changes what the programming environment shows you: readable parameter names, channels that already exist when the device is added, and a library and function block when the description carries them. Two projects addressing the same drive, one with raw channels and one with the descriptor, put identical traffic on the wire.

What serial format does Modbus RTU use by default?

The Modbus serial specification's default is 19200 baud, 8 data bits, even parity and 1 stop bit; with parity switched off, the frame uses 2 stop bits. A drive that has been configured keeps its own values, so read the slave address and serial format from the drive's parameters before setting the master. Both ends of the segment must match, because one mismatched parameter times out every request on it.

How many RTU slaves can one master poll?

Valid slave addresses run from 1 to 247, with 0 reserved for broadcast writes that no slave answers. The practical count on one segment comes from scan time: every request and its response hold the bus in turn, so ten drives take roughly ten times the bus time of one at the same polling period. Size the period from the freshest value the application needs.

Can one descriptor serve several drive models?

Only when the models share one register map, including addresses, function codes, scaling and word order for multi-register values. Where they differ, build one description per model and put the model name in it, so a project installs the right file. A channel typed from the wrong map usually lands on a neighboring register, and the value that comes back can still look plausible.

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