Problem Overview
An S7-1200 CPU (firmware V4.x running TIA Portal V16) is connected to an AMCI NR25 series digital encoder over Modbus RTU. The MB_CLIENT instruction is being used to read the encoder velocity register. The MB_CLIENT block returns status 16#7004 (connection successfully established) on its STATUS output, but the MB_DATA_PTR data block holds 16#0000 for every cycle. The same encoder, polled from a non-Siemens master (Modbus Poll, a third-party PLC, or an industrial PC running libmodbus), returns valid 32-bit velocity counts. The engineer assumes the issue is byte ordering (the AMCI encoder is little endian; the S7-1200 stores a WORD in big endian), so the SWAP instruction is wrapped around MB_DATA_PTR. SWAP of an all-zero DWORD still returns all zeros, which proves the request never produced meaningful PDU data. The actual root cause is a wrong Modbus function code being passed to the MB_CLIENT block.
MB_DATA_PTR is permanently 0. Swapping, masking, or scaling produces no change because the payload never contained a valid process value in the first place.Root Cause: Wrong Modbus Function Code (FC 0 vs FC 4 / MB_CODE 104)
Modbus function code 0 is not a legal public function code. The Modbus Application Protocol Specification (MODICON, now Schneider Electric) reserves function code 0 for legacy use and the public code space starts at FC 1 (Read Coils). When MB_CLIENT is configured with an MB_MODE/MB_CODE of 0 the block still issues a request, but the slave either ignores the function code (it is not in the dispatch table) and times out, or it answers with an exception response that MB_CLIENT silently drops because no error bit is wired in the engineer's logic. The result is a 7004 connection status and a data block full of zeros.
The AMCI NR25 holds the velocity value in an input register (function group 3xxxx). To read input registers the master must send Modbus function code 4. In the Siemens MB_CLIENT block, the corresponding MB_CODE value is:
| MB_CODE (dec) | Modbus FC | Function |
|---|---|---|
| 1 | FC 01 | Read Coils |
| 2 | FC 02 | Read Discrete Inputs |
| 3 | FC 03 | Read Holding Registers |
| 4 / 104 | FC 04 | Read Input Registers (use 104 for >9999 addressing) |
| 5 | FC 05 | Write Single Coil |
| 6 | FC 06 | Write Single Holding Register |
| 15 | FC 15 | Write Multiple Coils |
| 16 | FC 16 | Write Multiple Holding Registers |
| 0 | (reserved) | Not legal - produces silent zero response on most slaves |
The Siemens MB_CLIENT instruction historically used 1/2/3/4 for the read codes, which limits the data address to 0-9999 (Modicon style). For modern encoders and instruments that number registers 0-65535 the user data address field is the full 16-bit value. To accommodate this without changing the on-the-wire Modbus function code, TIA Portal V15.1 and later (including V16) accept MB_CODE = 104 as a synonym for FC 4 and pass the full 16-bit register address without a 1-based offset. This is the value the engineer should be using to read the AMCI velocity input register.
Why Endianness Is Not the Issue
MB_CLIENT is a transparent Modbus transport. It frames a request, sends the bytes, receives the response payload, and stores it byte-for-byte into MB_DATA_PTR as an array of bytes. The block does not examine, reorder, or interpret the payload - it has no concept of "velocity", "encoder count", or "signed 32-bit". If a slave returns 0x12 0x34 0x56 0x78, those four bytes land in the data block in the exact same order.
The S7-1200 CPU is a big endian machine. When you map an ARRAY[0..3] OF BYTE over a DWORD, byte 0 ends up in the most significant position of the DWORD. The AMCI NR25 returns velocity in little endian - low byte first, high byte last. So a velocity of 0x00012345 arrives as 0x45 0x23 0x01 0x00 in the data block. If you read it as a DWORD, you see 0x45230100, which looks wrong. That is a display issue, not a comms issue - the data is there and meaningful.
SWAP on a DWORD (SWAP_DWORD) reverses the byte order and yields 0x00012345. SWAP on a WORD reverses the two bytes of each 16-bit word. If SWAP applied to MB_DATA_PTR returns zero, the payload itself is zero, which means either the slave never responded or it returned a non-data error/exception. That is the diagnostic signature of a wrong function code, not an endian problem.
Prerequisites
- S7-1200 CPU with firmware V4.0 or later. CPU 1211C, 1212C, 1214C, 1215C, and 1217C all support MB_CLIENT as of FW V4.0. S7-1200 Programmable Controller - System Manual
- TIA Portal V16 (or V15.1 with HSP 0236 for MB_CLIENT 4.x instructions). V17/V18 use the same instruction symbols.
- AMCI NR25 (or NR25E) encoder datasheet, register map, and Modbus address table. Velocity is on page 76 of the AMCI NR25 manual, register 30002 (1-based) or 0x0001 (0-based in 3xxxx group).
- Modbus RTU cable: shielded twisted pair, 120 ohm termination at both ends if bus length exceeds 10 m at 115.2 kbaud. A and B labeled consistently with the encoder manual.
- CM 1241 RS485 / CM 1241 RS232 communications module, or the onboard RS485 of the CPU 1217C. Set the same baud, parity, and stop bits as the encoder (the NR25 ships at 19200-8-N-1 by default).
- User data block (DB) of sufficient size to hold the response. A single 32-bit velocity reading needs 4 bytes; allocate at least 8 bytes to leave room for status and scaling.
Register Addressing and the Siemens 1-Offset
The AMCI NR25 manual lists the velocity register as 30002. The first three digits (3) indicate the function group (input registers). The last four digits are the offset within the group. In Modbus PDU on the wire, the slave expects a 0-based address, so 30002 is sent on the wire as register address 0x0001 (counting from 0, the first input register is address 0).
Siemens MB_CLIENT traditionally adds a +1 offset so that engineers can enter "30002" in the MB_DATA_ADDR input. To get a 0-based 16-bit value of 0x0001 on the wire, the engineer enters 30003 in the MB_DATA_ADDR field of the MB_CLIENT block. With MB_CODE = 104 the offset is removed and the value entered is the literal 16-bit register address; in that case use 1 (or the 0-based value from the encoder manual) directly. The engineer in the original incident correctly used 30003 with the older-style MB_CODE 4, which is equivalent to using 1 with MB_CODE 104.
| Encoder manual | Wire register (0-based) | MB_DATA_ADDR with MB_CODE 4 | MB_DATA_ADDR with MB_CODE 104 |
|---|---|---|---|
| 30002 | 0x0001 | 30003 | 1 |
| 30010 | 0x0009 | 30011 | 9 |
Step-by-Step Fix
- Open the FB that contains the MB_CLIENT call. Locate the
MB_MODEorMODEinput. The older MB_CLIENT (V1-V3) has a single MODE input; the V4 MB_CLIENT (introduced in TIA V15.1) has separated MODE and DATA_ADDR into a dedicated PLC data type (the MB_MASTER / MB_CLIENT instance DB). - Change MB_MODE / MB_CODE from 0 to 104 (recommended for 0-65535 devices such as the AMCI NR25) or 4 (legacy, 0-9999 devices with +1 Siemens offset). Both call Modbus FC 4 on the wire.
- Set
MB_DATA_ADDRto the 0-based register address. For the AMCI NR25 velocity this is1when using MB_CODE 104, or30003when using MB_CODE 4. - Set
MB_DATA_LENto2(read 2 x 16-bit words = 32 bits = one DWORD). - Set
MB_DATA_PTRto a P# pointer targeting a DWORD tag inside a non-optimized DB. Example:P#DB100.DBX0.0 DWORD 2reads 2 words (4 bytes) starting at byte 0 of DB100. - Wire the
REQinput. In TIA Portal V16 the MB_CLIENT V4.x block accepts a constant TRUE on REQ and processes requests cyclically; no rising edge is required. This is a documented change from the V3.x block. S7-1200 System Manual, MB_CLIENT section - Compile, download, and go online with the CPU. Open the instance DB and the target data DB in watch table.
- Force a one-shot trigger or leave REQ = TRUE. Monitor
DONE,ERROR, andSTATUS. - Confirm
DONEtoggles TRUE on each successful cycle,ERRORstays FALSE, andMB_DATA_PTR(now viewed as bytes) contains the encoder's response. The DWORD interpretation will still appear reversed because the encoder is little endian; the next step is the byte swap.
Handling Endianness When It Is Real
After the function code is corrected the data block will hold a meaningful value, but the DWORD view will be byte-reversed. There are three production-grade ways to bring the value into a usable big endian DWORD on the S7-1200.
Method 1 - READ_LITTLE / WRITE_LITTLE SCL Instructions (TIA V17+)
For S7-1200 CPUs running firmware V4.4 (and TIA Portal V17 or later) the basic instruction set includes four dedicated endian helpers that read or write a block of memory in either byte order. They are documented in the TIA Portal help under Basic instructions > Move operations > Read/write memory. Read and write big and little Endian instructions (SCL)
// SCL - read 4 bytes from MB_DATA_PTR as little endian into a big endian DWORD
#iStatus := READ_LITTLE(
SRC_VARIABLE := "dbComm".rawBytes, // ANY source pointer (e.g. P#DB100.DBX0.0 BYTE 4)
RET_VAL := #dwVelocityBigEndian, // destination DWORD
LEN := 4); // number of bytes to read
READ_LITTLE accepts an ANY pointer for the source and a destination tag; LEN defines how many bytes to read. The instruction reads the source bytes in the order the slave sent them (little endian) and stores them in the destination so the value is correct on a big endian CPU. WRITE_LITTLE performs the inverse, accepting a big endian source and writing bytes in little endian order to the target.
Method 2 - SWAP Instruction (LAD / FBD / SCL)
If you want to keep the data structure as a single DWORD in the data block, drop a SWAP block on a temporary DWORD after every successful read.
// SCL
#dwVelocitySwapped := SWAP(#dwVelocityRaw);
SWAP works on a single WORD or DWORD, not on an entire UDT or ARRAY in one call. If MB_DATA_PTR is 2 words (a DWORD), one SWAP_DWORD call is sufficient. For an odd number of words, swap them as individual WORDs in a loop.
Method 3 - Manual Byte Reorder in SCL
For maximum control - and when the engineer needs to mix endianness within a single request - read the response into an ARRAY[0..3] OF BYTE and reassemble.
// SCL - assume MB_DATA_PTR is an ARRAY[0..3] OF BYTE
#dwVelocityBigEndian.%B0 := #rawBytes[3];
#dwVelocityBigEndian.%B1 := #rawBytes[2];
#dwVelocityBigEndian.%B2 := #rawBytes[1];
#dwVelocityBigEndian.%B3 := #rawBytes[0];
This avoids any instruction overhead and is the easiest to debug in a watch table because the engineer can see every byte that came off the wire.
Sample MB_CLIENT Call (LAD-Style Parameters)
| Input | Value | Notes |
|---|---|---|
| REQ | TRUE | Constant TRUE runs cyclically in TIA V16 MB_CLIENT V4.x |
| CONNECT | TCON_IP_v4 / TCON_IP_RFC1006 / PtP | Depends on transport: TCP, TCP via RFC1006, or RS485 |
| MB_MODE | 104 | Read Input Registers (FC 4) with full 16-bit address |
| MB_DATA_ADDR | 1 | AMCI NR25 velocity = wire address 0x0001 |
| MB_DATA_LEN | 2 | Two 16-bit words (one DWORD) |
| MB_DATA_PTR | P#DB100.DBX0.0 DWORD 2 | 4 bytes of response data |
| DONE | %M10.0 | TRUE for one cycle on success |
| BUSY | %M10.1 | TRUE while request is in flight |
| ERROR | %M10.2 | TRUE if request failed |
| STATUS | %MW12 | 16-bit status (7004 = open OK, 0 = success per cycle) |
Status Code Reference
| STATUS (hex) | Meaning | Action |
|---|---|---|
| 7004 | Connection established, no request issued yet | Confirm REQ is TRUE and MODE is non-zero |
| 0000 | Last request successful | Read MB_DATA_PTR |
| 8380 | Received frame error (CRC / parity / length) | Check RS485 wiring, baud, parity |
| 8381 | Parity / framing error | Match serial parameters to encoder |
| 8382 | Overflow in receive buffer | Increase MB_DATA_LEN interpretation; check slave ID |
| 8383 | Negative acknowledgement from slave | Inspect Modbus exception code byte |
| 8384 | Send timeout / no response from slave | Verify slave address and that slave is powered |
| 8188 | Invalid MODE parameter | Set MB_MODE to a valid code (1, 2, 3, 4, 5, 6, 15, 16, or 104) |
| 80C8 | Slave does not support requested function | Verify the encoder exposes the requested function code |
| 80C9 | Modbus exception 02 (illegal data address) | Check MB_DATA_ADDR; confirm register exists on the slave |
Verification
- After downloading the corrected program, force the data DB to all zeros and watch it fill within one or two MB_CLIENT cycles. For a 19200 baud link the cycle time is dominated by the request/response turnaround - typically 5-15 ms.
- Apply the SWAP or READ_LITTLE step. The DWORD value should track the value displayed in the AMCI configuration software or on the encoder's web interface. Spin the encoder shaft by hand and confirm the value changes monotonically.
- Toggle the encoder power to verify the "locked at zero" behavior reported in the original incident is resolved. The AMCI NR25 can hold last-known state across power cycles if the encoder was last left in a partial-configuration state; a clean configuration followed by a power cycle typically clears the lock.
- Add scaling in SCL. The AMCI velocity register is signed 32-bit counts per the encoder's time base. Convert to engineering units with the resolver counts per revolution from the encoder nameplate (e.g. 4096, 8192, or 65536 CPR for the NR25 family) and the configured time base (default 1 ms).
- Trend the value over 30 seconds and confirm no zeros appear between samples. Persistent zeros after a successful first read typically indicate RS485 bus contention, not endianness.
Troubleshooting Matrix
| Observed Symptom | Likely Cause | Corrective Action |
|---|---|---|
| STATUS = 7004 forever, MB_DATA_PTR = 0 | REQ is FALSE, or MB_MODE = 0 | Set REQ = TRUE, set MB_MODE to 4 or 104 |
| STATUS = 0 each cycle, MB_DATA_PTR = 0, DONE toggles | Wrong register address (slave exception 02) | Verify register number against encoder datasheet, adjust MB_DATA_ADDR |
| STATUS = 0, MB_DATA_PTR reads as reversed DWORD | Endian mismatch - normal for little endian slaves | Apply SWAP or READ_LITTLE |
| STATUS = 8381, intermittent data | RS485 termination / biasing wrong, or A/B swapped | Add 120 ohm at both ends, swap A/B if needed, enable bias resistors per encoder manual |
| STATUS = 8384 every cycle | Slave not responding - wrong slave ID, no power, or shielded cable grounded at both ends | Check encoder power LED; verify slave address byte; isolate shield ground |
| STATUS = 80C9 on first poll after power-up | AMCI NR25 needs a configuration cycle before input registers are valid | Send the AMCI configuration sequence first, then poll input registers |
| DWORD value locked at 0 after working once | AMCI NR25 "freeze" state from incomplete configuration | Power-cycle encoder, reload the configuration file, then resume polling |
| SWAP of MB_DATA_PTR still shows 0 | Request never produced a data response (wrong FC, wrong address, or physical layer fault) | Switch focus from endianness to function code, register address, and wiring |
Best Practices for Modbus on the S7-1200
- Use MB_CLIENT V4.x (TIA V15.1+) so you can pass the full 16-bit register address with MB_CODE 104 for any slave that numbers registers 0-65535. The legacy MB_CODE 1-4 caps at address 9999 because it pre-appends a "3", "4", "0", or "1" offset before sending the wire address.
- Keep MB_DATA_PTR pointing to a non-optimized data block. Optimized blocks hide the byte layout and break P#-style ANY pointers used by MB_CLIENT.
- Use one MB_CLIENT instance per physical port. Multiple instance calls on the same TCON connection produce bus contention on RS485 and concurrent TCP connection errors on Ethernet.
- Sequence multi-register reads with the DONE bit of the previous call, never with a fixed timer. The DONE bit is set for exactly one scan at the end of the request; a falling-edge detector on DONE is the standard hand-off pattern.
- Always read Modbus exception bytes explicitly when ERROR is TRUE. The low byte of STATUS in 8xC8-8xCF codes contains the slave's exception code: 01 (illegal function), 02 (illegal data address), 03 (illegal data value), 04 (slave device failure), 05 (acknowledge), 06 (slave device busy).
- For deterministic cycle time, poll at the encoder's update rate, not faster. The AMCI NR25 ships the velocity register at 1 ms, 10 ms, or 100 ms time base depending on configuration; reading faster than the register updates returns a duplicate of the previous value, not a fresh sample.
Does the S7-1200 MB_CLIENT care about endianness?
No. MB_CLIENT is a transparent Modbus transport; it stores the response bytes in MB_DATA_PTR exactly as the slave sent them. If the data looks reversed, that is a display/scaling issue you handle in the application code with SWAP, READ_LITTLE, or manual byte reorder. Endianness is only a problem if the engineer is also reinterpreting the byte stream as a CPU-native DWORD on a big endian machine.
Why does MB_CODE 104 work when MB_CODE 4 reports an address error?
MB_CODE 4 (and 1, 2, 3) was designed for the legacy Modicon 1-9999 address space and pre-pends a function-group digit (3 for input registers) before sending the wire address, with an implicit +1 offset. That caps the register number at 9999. MB_CODE 104 sends the wire address verbatim with no offset and no group digit, allowing the full 0-65535 range that modern encoders and instruments use.
STATUS = 7004 forever, what does that mean?
7004 is the "connection established, no request yet" state. It confirms that the underlying TCON or PtP link is open. If DONE never sets and MB_DATA_PTR stays zero, either REQ is not being driven, MB_MODE is 0 (illegal), or the request is being dropped because the slave cannot parse the function code. The first diagnostic step is to verify MB_MODE and that REQ is TRUE for at least one scan.
Why does SWAP of MB_DATA_PTR return zero even though the encoder is moving?
Because the swap is applied to a DWORD whose bytes are all zero. That means the request never produced a real data response. SWAP of 0x00000000 is still 0x00000000. The encoder is not the source of the zero - the missing data is. Switch focus to the function code, register address, slave ID, and physical layer before chasing endianness.
Can I leave REQ constantly TRUE on the S7-1200 MB_CLIENT?
Yes, in TIA Portal V15.1 and later with the MB_CLIENT V4.x instruction. The V4.x block runs cyclically while REQ is TRUE; it does not require a rising edge. The V3.x block (and the older USS/Modbus RTU library blocks) do require a rising edge on REQ per request. If you migrate older code, the rising-edge generator can be removed.
How do I read 32-bit values from a little endian slave into a big endian DWORD?
Three options: (1) Use READ_LITTLE in SCL on TIA V17 / CPU FW V4.4 and later, which reads N bytes from a source ANY and stores them in the correct big endian order at the destination. (2) Use SWAP on the raw DWORD after the read. (3) Read into an ARRAY OF BYTE and reassemble manually using the slice operator. All three produce the same numeric result; READ_LITTLE is the cleanest when the data block layout is fixed.