In short: A Modbus request is just a slave address, a one-byte function code, a data field and a CRC; five codes (01, 03, 05, 06, 16) cover almost every real job, and any failure comes back as the same function code with bit 7 set plus a one-byte exception code.
Why the function code is the heart of Modbus
Modbus has no sessions, no discovery and no data typing. The only thing that tells a slave what to do is the function code. Everything after it is interpreted according to that single byte. If you can read the function code and the byte counts, you can decode any capture from a logic analyser, a USB-RS485 dongle or Wireshark without documentation. For the wider context of masters, slaves and the two serial framings, read our primer on Modbus communication.
Modbus organises data into four tables: coils (read/write bits), discrete inputs (read-only bits), input registers (read-only 16-bit words) and holding registers (read/write 16-bit words). Function codes are simply verbs that act on those tables. Addresses on the wire are zero-based, so the register a vendor manual calls "40108" is sent as address 0x006B (107). Forgetting this off-by-one is the most common reason a first read returns a neighbouring value.
Anatomy of an RTU frame
Key fact: Modbus data is big-endian, but the CRC-16 (polynomial 0xA001, reflected) is transmitted low byte first. Mixing those two up is the classic reason a hand-built frame is silently ignored.
The five workhorse function codes
| FC | Name | Table | Request data | Example request |
|---|---|---|---|---|
| 01 | Read Coils | Coils | Start (2), quantity (2) | 01 01 00 13 00 08 CC 09 |
| 03 | Read Holding Registers | Holding regs | Start (2), quantity (2) | 11 03 00 6B 00 02 B7 47 |
| 05 | Write Single Coil | Coils | Address (2), FF00 = ON / 0000 = OFF | 01 05 00 AC FF 00 4C 1B |
| 06 | Write Single Register | Holding regs | Address (2), value (2) | 01 06 00 01 00 03 98 0B |
| 16 (0x10) | Write Multiple Registers | Holding regs | Start (2), qty (2), byte count (1), values | 01 10 00 01 00 02 04 00 0A 01 02 92 30 |
Note that function code 16 is decimal; on the wire it is the byte 0x10. Datasheets mix decimal and hex freely, which trips up many people reading a capture for the first time.
Reading: FC 01 and FC 03 in detail
For FC 03, the example above asks slave 0x11 for two registers starting at address 107. A typical reply is 11 03 04 02 2B 00 64 9B A9. The third byte is the byte count (4), followed by two 16-bit values: 0x022B (555) and 0x0064 (100). The master must trust its own request to know what those words mean; nothing in the reply identifies a temperature or a setpoint. A common convention is a 32-bit float or integer across two consecutive registers, and the word order (ABCD versus CDAB) is a vendor choice you have to look up.
FC 01 packs coils into bytes, least-significant bit first. Asking slave 1 for 8 coils from address 19 returns 01 01 01 CD 90 1D. The data byte 0xCD is 1100 1101 in binary, so coils 19, 21, 22, 25 and 26 are ON (bit 0 is address 19, so reading upward gives 1, 0, 1, 1, 0, 0, 1, 1 for addresses 19 to 26). If the quantity is not a multiple of eight, the unused high bits of the last byte are zero-padded.
Writing: FC 05, 06 and 16
Single writes are echoed. FC 05 to coil 0x00AC with value 0xFF00 switches it on, and a healthy slave replies with the identical eight bytes. Only 0xFF00 and 0x0000 are legal; 0x0001 is an illegal data value, a frequent bug when engineers reuse integer code. FC 06 behaves the same way for a single holding register: writing 3 to address 1 produces an exact echo of 01 06 00 01 00 03 98 0B.
FC 16 is the one to prefer for multi-register data. Writing 0x000A and 0x0102 to addresses 1 and 2 sends 01 10 00 01 00 02 04 00 0A 01 02 92 30. The reply does not echo the data, only the start address and quantity: 01 10 00 01 00 02 10 08. Writing a 32-bit setpoint with two separate FC 06 calls is dangerous, because a drive may act on the first half before the second arrives. A single FC 16 write is applied atomically by most devices.
Exception responses
When a slave understands the frame but cannot carry it out, it replies with the original function code OR-ed with 0x80, followed by a single exception code. A failed FC 03 therefore comes back with function byte 0x83. If the master asks slave 1 for a register that does not exist, the reply is 01 83 02 C0 F1: address 1, function 0x83, exception 02 (Illegal Data Address), CRC. The CRC is still computed over the whole short frame, so a corrupted exception is rejected like any other message.
| Code | Name | Typical cause |
|---|---|---|
| 01 | Illegal Function | Device does not implement that FC (e.g. FC 16 on a read-only meter) |
| 02 | Illegal Data Address | Start plus quantity runs outside the mapped range; often an off-by-one |
| 03 | Illegal Data Value | Quantity too large (over 125 registers for FC 03) or coil value not FF00/0000 |
| 04 | Slave Device Failure | Internal fault while executing the action |
| 06 | Slave Device Busy | Long job in progress; master should retry later |
fc & 0x80.Key fact: No response is not an exception. A timeout means a wrong address, a CRC failure, a baud or parity mismatch, or a missing RS-485 termination; a slave never sends an exception for a corrupted frame.
Practical debugging habits
- Cap FC 03 at 125 registers and FC 01 at 2000 coils per request; the 256-byte RTU frame limit forces this.
- Poll no faster than the device updates; 9600 baud moves roughly one 8-byte request and 9-byte reply in about 20 ms including the 3.5-character gaps.
- Keep an eye out for 8E1 versus 8N1 mismatches; the spec default is even parity.
- Log the raw hex. A tool such as Modbus Poll, pymodbus or a Wireshark dissector makes exception 02 obvious in seconds.
The same protocol logic appears in FPGA and SoC peripherals: a Modbus slave core is a small FSM plus CRC engine, and teams verifying such blocks often drive them with UVM testbenches in Synopsys VCS or Cadence Xcelium. For a related serial standard, compare it with our RS-485 differential signaling article, and if you need help integrating industrial interfaces into silicon, Avecas design services works on exactly this kind of mixed-signal and protocol IP.
Want to go from decoding frames to designing the hardware behind them? Explore the embedded and protocol tracks in our CourseTron courses and build a working Modbus slave in RTL.
FAQ
What is the difference between function codes 06 and 16?
FC 06 writes exactly one 16-bit holding register and echoes the request. FC 16 (0x10) writes a block of registers in one frame and replies with only the start address and quantity. Use FC 16 for 32-bit values so both halves update together.
Why does my slave return exception 02?
The requested start address, or start plus quantity, falls outside the device register map. Check for the zero-based offset: manual address 40001 is wire address 0.
How do I recognise an exception response in a capture?
Look for a function byte of 0x80 or higher, for example 0x83 or 0x86, and a reply of only five bytes: address, function, exception code and two CRC bytes.
Are function codes the same in Modbus TCP?
Yes. The PDU (function code plus data) is identical, but TCP replaces the address and CRC with a 7-byte MBAP header, using the unit identifier for addressing and relying on TCP for integrity.