Configuring Citect Modbus RTU for Algodue UPT210 Energy Meter

Claire Rousseau15 min read
SCADA ConfigurationSchneider ElectricTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Configuring Citect Modbus RTU for the Algodue UPT210 Energy Meter

The Algodue UPT210 is a three-phase multifunction energy meter with an RS-485 Modbus RTU slave port. When it is brought into a Citect SCADA project (AVEVA / Schneider Electric CitectSCADA 2016 or later, including the current AVEVA Plant SCADA), the integrator faces two recurring obstacles: translating the meter's hex register map into Citect variable tags, and computing the Modbus RTU CRC-16 when the request must be built manually in CiCode. This reference walks through the field wiring, the register map, the driver configuration, the 4xxxxx addressing math, and the bit-by-bit CRC-16 algorithm with verified examples.

1. System Overview and Architecture

The reference architecture has three layers:

  1. Field device — Algodue UPT210 (default slave address 1, 9600 bps, 8N1, RS-485).
  2. Serial gateway / PC — Industrial PC running Citect runtime, with a multi-drop RS-485 port or a USB-to-RS-485 converter (FTDI FT232R or similar).
  3. SCADA — CitectSCADA / AVEVA Plant SCADA project polling the meter through the built-in Modbus RTU driver (board type COMX, protocol MODBU).

The Citect Modbus driver handles the request/response cycle, the inter-frame 3.5 character silence, and the CRC-16 verification automatically. The integrator's job is to expose the meter's internal registers as Citect tags using the correct 4xxxxx holding-register address. Only when the driver is bypassed (raw CiCode polling, custom protocol, or a redundant channel) does the CRC-16 need to be implemented by hand.

2. Prerequisites and Hardware Setup

Before configuring Citect, verify the physical layer.

2.1 RS-485 Cabling and Termination

  • Use a shielded twisted pair (Belden 3106A or equivalent) for the A+/B− pair. Connect the shield to ground at one end only.
  • Maximum bus length: 1200 m at 9600 bps, 500 m at 115200 bps (per the TIA-485-A specification).
  • Place a 120 Ω termination resistor at each end of the bus (across A+ and B−). The UPT210 exposes a 120 Ω resistor that can be enabled through its setup menu or hardware jumper; enable it only on the end-of-line meter.
  • Bias resistors (typically 680 Ω pull-up to +5 V on A+ and pull-down to GND on B−) are required on the master if the master transceiver does not include them internally. Most USB-to-RS-485 converters do not; add a BIAS-680/SITRANS terminal block or equivalent.

2.2 Communication Parameters

The UPT210 default configuration is 9600 bps, 8 data bits, no parity, 1 stop bit. Confirm by reading register $0002 (baud rate) and $0003 (parity) using a Modbus master tool first; this prevents the classic "driver reports no response" symptom caused by a baud-rate mismatch.

Parameter Default Range
Modbus address 1 1…247
Baud rate 9600 bps 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
Data bits 8 8 only
Parity None None / Even / Odd
Stop bits 1 1 / 2
Termination Disabled Enabled at end of line only

2.3 Software Prerequisites

  • CitectSCADA 2016 / 2018 / 2020 or AVEVA Plant SCADA 2020 / 2023.
  • Modbus RTU driver license and the MODBU protocol file deployed under [Citect]\Bin\Modbus.dbf.
  • Citect Explorer with project compilation rights.
  • Serial-port testing tool such as Modbus Poll or qModMaster to confirm the meter is reachable before the SCADA touches it.

3. Algodue UPT210 Modbus Register Map

The UPT210 keeps its data in 16-bit holding registers. The meter's own documentation uses a base-10000 hex notation: register $1000 means holding-register address 0x1000 (4096 decimal). All the metering values use two consecutive registers (4 bytes) and are encoded as IEEE-754 single-precision float in big-endian order.

Address (hex) Address (decimal) Length Parameter Encoding Unit
$1000 4096 2 reg System Voltage (L-N equivalent) IEEE-754 float BE V
$1002 4098 2 reg System Current IEEE-754 float BE A
$1004 4100 2 reg Active Power IEEE-754 float BE W
$1006 4102 2 reg Reactive Power IEEE-754 float BE var
$1008 4104 2 reg Apparent Power IEEE-754 float BE VA
$100A 4106 2 reg Power Factor IEEE-754 float BE
$100C 4108 2 reg Frequency IEEE-754 float BE Hz
$1010 4112 2 reg Active Energy (import) IEEE-754 float BE kWh
$1014 4116 2 reg Reactive Energy (import) IEEE-754 float BE kvarh
$0002 2 1 reg Baud rate code UINT16
$0003 3 1 reg Parity / stop bits UINT16
$001C 28 16 reg Per-phase instantaneous data block 16×IEEE-754 float BE V / A / W…
Endianness trap. Modbus RTU is big-endian: the high word of a float arrives first. Citect variable tags of type REAL are little-endian on x86 hosts, so the driver (or the byte_swap option in citect.ini) handles the swap when the tag is declared REAL. If you poll a raw 16-bit tag and assemble the value manually, swap the two registers before passing them to MakeReal().

4. Modbus RTU Frame Structure and CRC-16 Theory

Every Modbus RTU frame is composed of:

  1. 1 byte — slave address (1…247)
  2. 1 byte — function code (03 for holding registers, 04 for input registers, 06 for single write, 10 for multiple write)
  3. N bytes — payload (starting address, quantity, data)
  4. 2 bytes — CRC-16, low byte first, high byte second

The frame is delimited by a silent interval of at least 3.5 character times. The CRC-16 used in Modbus RTU is the CRC-16/IBM-3740 variant, also called CRC-16/MODBUS:

  • Polynomial: 0xA001 (bit-reversed form of 0x8005)
  • Initial value: 0xFFFF
  • Reflect input: yes
  • Reflect output: yes
  • Final XOR: 0x0000

The reference implementation is published on the srecord CRC-CCITT reference page (srecord CRC-CCITT 16-bit reference) and is the same polynomial used for DNP-3 and M-Bus when expressed in its non-reversed form. For Modbus RTU specifically the 0xA001 form must be used — do not substitute the CCITT polynomial 0x1021.

4.1 Worked CRC-16 Calculation

Take the request issued by the kernel in the field case:

Tx: 01 03 00 1C 00 10 85 C0
       |  |  |     |    |
       |  |  |     |    +-- CRC-16 high byte
       |  |  |     +------- CRC-16 low byte
       |  |  +------------- quantity high (0x00)
       |  +---------------- quantity low  (0x10 = 16 regs)
       +------------------- starting address 0x001C
+-------------------------- slave address

Run the CRC-16/MODBUS over 01 03 00 1C 00 10:

  1. Init crc = 0xFFFF
  2. Process 0x01: crc = 0xFFFF XOR 0x01 = 0xFFFE; eight right shifts with 0xA001 XOR on the pop yields 0xE0FE
  3. Process 0x03: crc = 0xE0FE XOR 0x03 = 0xE0FD; bit-shifts give 0xC0FD
  4. Process 0x00: crc = 0xC0FD XOR 0x00 = 0xC0FD; bit-shifts give 0x807E
  5. Process 0x1C: crc = 0x807E XOR 0x1C = 0x8062; bit-shifts give 0x4031
  6. Process 0x00: crc = 0x4031 XOR 0x00 = 0x4031; bit-shifts give 0xA018
  7. Process 0x10: crc = 0xA018 XOR 0x10 = 0xA008; bit-shifts give 0x85C0

The computed CRC is 0x85C0 and the on-wire bytes are lo first: 0xC0 0x85. The kernel in the field reports 85 C0 as the "error check" field, which corresponds to the CRC value itself, not to its byte-swapped form. Both are correct as long as the transmitter and receiver agree on the byte order.

5. Citect I/O Device Configuration

The recommended approach is to use the Citect Modbus driver and never write CRC code in CiCode. Configure the port in Citect Explorer:

  1. Open the project, navigate to Communications → I/O Devices.
  2. Right-click and add a new I/O device with the following properties:
    Field Value
    Name UPT210
    Board type COMX (the Citect internal serial board)
    Port COM3 (or the COM number assigned to the USB-RS485 adapter)
    Protocol MODBU (Modbus RTU master)
    Address 1 (Modbus slave address of the meter)
    Format 8N1
    Speed 9600
    Special Opt RTU (forces Modbus RTU framing; do not leave as ASCII)
  3. Compile the project. The kernel will report the device in the syslog.dbf as UPT210 - startup OK.
Special Opt flag. A common field failure is leaving the protocol in ASCII mode. With Special Opt = ASCII the driver frames requests with a colon and uses LRC, so the UPT210 (RTU only) silently ignores them and the kernel reports error_in_cmd-3 after a few retries.

6. Modbus Variable Tag Addressing in Citect

Citect uses Modicon-style 4xxxxx addresses for Modbus holding registers:

  • 4xxxx → Modbus holding register number xxxx-1
  • 3xxxx → Modbus input register
  • 0xxxx → Modbus coil
  • 1xxxx → Modbus discrete input

The UPT210 documentation lists addresses in hex. Convert to decimal, then add 40001:

Citect address = 40001 + decimal_register_address

Examples from the field case:

Meter address (hex) Decimal Citect tag address Description
$1000 4096 44097 System Voltage (REAL, 2 words)
$1006 4102 44103 System Current (REAL, 2 words)
$001C 28 40029 Per-phase block (REAL array, 16 words)
$0002 2 40003 Baud rate code (INT)

Create the tags in Citect Explorer under Variable Tags:

Tag name Address Data type Length (words) I/O device
UPT210_Voltage 44097 REAL 2 UPT210
UPT210_Current 44103 REAL 2 UPT210
UPT210_PF 44107 REAL 2 UPT210
UPT210_EnergyImp 44113 REAL 2 UPT210
UPT210_BaudCode 40003 INT 1 UPT210

For a multi-word REAL the driver reads the words in order and combines them. If the meter is little-endian, set [Modbus]ByteSwap = 1 in citect.ini; the UPT210 is big-endian and requires the swap.

7. CiCode Implementation of the CRC-16 Algorithm

Only implement the CRC-16 in CiCode when a serial channel is being driven by hand from a Cicode function — for example through DevOpen(), DevWrite() and DevRead(). Use the bitwise algorithm (no 256-entry table required):

//============================================================
//  fnCalcCRC16 - Modbus RTU CRC-16 (poly 0xA001, init 0xFFFF)
//  Input  : sPDU  - hex string of the Modbus PDU without CRC
//                    e.g. "0103001C0010"
//  Output : INT   - the 16-bit CRC value
//============================================================
INT
fnCalcCRC16(STRING sPDU)
    INT  i;
    INT  j;
    INT  crc;
    INT  byt;
    STRING sByte;

    crc = 16#FFFF;

    FOR i = 1 TO Len(sPDU) STEP 2 DO
        sByte = Mid(sPDU, i, 2);
        byt   = StrToInt("16#" + sByte);
        crc   = crc XOR byt;

        FOR j = 1 TO 8 DO
            IF (crc AND 1) = 1 THEN
                crc = (crc / 2) XOR 16#A001;
            ELSE
                crc = crc / 2;
            END
        END
    END

    RETURN crc;
END

Call site — issue a function-03 read of 16 registers starting at 0x001C:

STRING sPDU;
INT    nCRC;
STRING sTx;

sPDU  = "0103001C0010";            // slave, fcode, start, qty
nCRC  = fnCalcCRC16(sPDU);          // returns 0x85C0
sTx   = sPDU
      + LToStr(nCRC AND 16#FF, 16, 2)   // low byte first
      + LToStr((nCRC / 256)  AND 16#FF, 16, 2);
// sTx == "0103001C0010C085" on the wire
DevWrite(hPort, sTx);

7.1 Table-Driven Variant (Faster on Slow CPUs)

For high-latency serial channels a 256-entry lookup reduces the inner loop from eight shifts to one table read. The table is generated once and embedded as a constant array:

INT
fnCalcCRC16Table(STRING sPDU, INT crcTable[256])
    INT  crc;
    INT  i;
    INT  idx;
    INT  byt;
    STRING sByte;

    crc = 16#FFFF;

    FOR i = 1 TO Len(sPDU) STEP 2 DO
        sByte = Mid(sPDU, i, 2);
        byt   = StrToInt("16#" + sByte);
        idx   = (crc XOR byt) AND 16#FF;
        crc   = (crc / 256) XOR crcTable[idx];
    END

    RETURN crc;
END

Generate the table with the same reference algorithm used by the srecord CRC-CCITT page; only the polynomial differs (0xA001 for Modbus, 0x1021 for CCITT). The first ten entries for Modbus RTU are:

static const uint16_t modbus_crc16[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    ... 240 more entries ...
};

7.2 Verification of the CiCode Implementation

Test vectors that the function must reproduce exactly:

Input (hex string) Expected CRC Lo/Hi on wire
0103001C0010 0x85C0 C0 85
010300000002 0xC438 38 C4
01 06 00 02 00 05 0x2AC0 C0 2A
02 03 00 00 00 0A 0xC5C4 C4 C5

Test from a Citect command line using TestCicode or a temporary popup. If the result does not match, the most common cause is treating the shift as a signed right shift — on a 32-bit INTEGER you must mask with 0xFFFF after every XOR with 0xA001 or the high bits of negative numbers corrupt the polynomial.

8. Manual Modbus Polling with CRC-16

Use the Citect serial API rather than a third-party driver when the device must be polled outside the standard scan groups — for example on a watchdog timer, or in a redundant path that bypasses the main I/O server.

// Open the port once at startup
INT hPort = DevOpen("COM3", 9600, 8, "N", 1, 0);
IF hPort = -1 THEN
    // log and exit
END

STRING sPDU = "0103001C0010";
INT    nCRC = fnCalcCRC16(sPDU);
STRING sTx  = sPDU
            + WordToHex(nCRC AND 16#FF)            // low byte
            + WordToHex((nCRC SHR 8) AND 16#FF);    // high byte

DevPurge(hPort);
DevWrite(hPort, sTx);

// Wait for 3.5 character silence (at 9600 8N1 = ~3.8 ms)
Sleep(50);

STRING sRx = DevRead(hPort, 37);   // 5 header + 32 data + 2 CRC
INT nRxCRC = StrToInt("16#" + Right(sRx, 4));
IF nRxCRC = fnCalcCRC16(Left(sRx, Len(sRx) - 4)) THEN
    // valid frame, extract data
END
Timing. Modbus RTU demands a 3.5 character silence between frames. At 9600 bps with 11 bits per character this is 4 ms. Citect's Sleep(50) is conservative; reduce to Sleep(5) at 115200 bps. The driver handles this automatically, but a hand-rolled loop must enforce it explicitly or the UPT210 will reject overlapping requests.

9. Verification and Polling Diagnostics

  1. After compiling, open Citect Explorer and right-click the project, then Run As → Stand-alone Simulation.
  2. Open the Citect Kernel view. The line UPT210 : startup OK confirms the driver has opened the port and polled successfully.
  3. Add the variable tags to a graphics page using Numeric or Bar objects. The values should update on the configured scan period (default 250 ms for Citect, but the Modbus driver typically coalesces into a 1 s group).
  4. Force a refresh from the command line with TagForceRead("UPT210_Voltage").
  5. Cross-check the floating-point values against the meter's local display. If the value is exactly divided by 10 or 100, the ByteSwap flag is the wrong way — toggle [Modbus]ByteSwap in citect.ini.

9.1 Live Capture with a Modbus Sniffer

Insert a software sniffer such as modbus-cli or a hardware tap (Anybus Communicator, HMS IXXAT, or a plain Y-cable on a USB-RS485 adapter) to capture the actual on-wire bytes. Compare them to the expected PDU and CRC. A mismatched CRC is the leading indicator of:

  • Wrong polynomial (engineer used 0x8005 reversed = 0xA001 — correct for Modbus, but 0x1021 is sometimes accidentally used).
  • Byte-order mismatch (CRC sent MSB first instead of LSB first).
  • Polynomial XOR performed on signed integers that have shifted sign-extended.

10. Troubleshooting Matrix

Symptom in kernel/syslog Likely cause Fix
generic 000008 driver 00000257 error_in_cmd-3 16 Tag address wrong: Citect 4xxxxx does not match the meter's 0-based hex map Recompute address: 40001 + decimal(hex_register). For $1000 use 44097.
error_in_cmd-2 or timeout Baud rate or parity mismatch Confirm the meter and the I/O device use identical 8N1 / 9600 settings. Read $0002 and $0003 with Modbus Poll first.
error_in_cmd-5 (CRC error) Electrical noise on the bus, missing termination, or wrong CRC byte order Enable 120 Ω end termination, check bias resistors, capture the bus and verify the CRC byte order.
Tag reads zero permanently Byte-swap missing on a big-endian float Set [Modbus]ByteSwap = 1 in citect.ini and restart the driver.
Tag reads "---" (quality BAD) I/O device is stopped or the port is already open by another process Check Kernel → I/O Devices, confirm UPT210 is in state Running. Close any other application holding the COM port.
Meter responds in Modbus Poll but not in Citect Citect still configured for ASCII protocol Set Special Opt = RTU on the I/O device and recompile.
Intermittent errors after long uptime Driver cache not flushing or scan time too aggressive Add [Modbus]PollTime = 1000 in citect.ini and avoid polling more than 32 tags per device per second.
function 03 not supported Wrong function code (some UPT210 firmware revisions only support function 04 on certain blocks) Read the firmware-specific register map. Use function 04 for input-style blocks, 03 for holding registers.

11. Performance and Bus Loading Considerations

A single function-03 read of 16 registers is 8 bytes of request and 37 bytes of response, taking roughly 47 ms at 9600 bps (8N1). Citect groups up to 32 tags per request when they are contiguous and the same data type, so polling the 8 measurements used in this reference takes 2 transactions (8 + 8 registers), about 90 ms on the wire. Always check the bus load:

bus_load_pct = (transactions_per_second * (request_bytes + response_bytes) * 11) / baud_rate * 100

Keep the bus load below 40% to leave margin for alarm polling. For a 50-register refresh at 1 Hz, this works out to 22% bus load at 9600 bps — acceptable. Push the same to 100 ms refresh and the load rises to 220% and the driver will start dropping frames.

12. Alternative Platforms

The CRC-16/MODBUS algorithm is the same in every SCADA. If the same meter is later brought into a different platform:

  • AVEVA Plant SCADA 2023 / 2020: identical driver configuration. The 4xxxxx addressing rules are unchanged.
  • Siemens WinCC / TIA Portal: use the Modbus master driver in the "SIMATIC Modbus/TCP" package. Address 44097 maps to %DB450.DBW0 with the same byte-swap flag.
  • Ignition by Inductive Automation: use the Modbus TCP / RTU driver. Tag address is 44097 with word-swap enabled for REAL values.
  • Rockwell FactoryTalk View: poll the meter over a third-party ENBT gateway. The RSLinx Modbus OPC-DA driver uses the same 1-based address convention.

What is the correct Citect tag address for UPT210 register $1000?

Convert the hex address to decimal: $1000 = 4096. Citect uses 1-based Modicon-style addressing for Modbus holding registers, so the tag address is 40001 + 4096 = 44097. Declare it as type REAL with length 2 words and enable byte-swap for the IEEE-754 big-endian encoding.

Which CRC-16 polynomial does Modbus RTU use?

Modbus RTU uses CRC-16 with polynomial 0xA001 (bit-reversed 0x8005), initial value 0xFFFF, reflected input and output, and no final XOR. The same algorithm is published in the srecord CRC-CCITT reference and the Modbus Application Protocol V1.1b3 specification.

Why does the kernel report error_in_cmd-3 16 for the UPT210 device?

Error_in_cmd-3 indicates that the driver received a response whose length or content does not match the request, typically because the Citect tag address does not correspond to a real holding register in the meter. For UPT210 register $1000 the correct Citect address is 44097, not 404097 or 44096. A wrong parity setting or ASCII/RTU mode can produce the same code.

Can the CRC-16 be calculated in CiCode without a 256-entry lookup table?

Yes. The bitwise algorithm with a 0xFFFF initial value, eight conditional right shifts, and a conditional XOR with 0xA001 per byte is shown in the fnCalcCRC16 function above and reproduces the standard test vectors including 0x85C0 for the request 01 03 00 1C 00 10. Mask the working variable to 0xFFFF after every XOR to keep the polynomial stable on 32-bit integers.

How do I make Citect handle big-endian IEEE-754 floats from the UPT210?

Set ByteSwap = 1 under the [Modbus] section of citect.ini and declare the tag as type REAL with length 2. Citect then reads the two registers, swaps their byte order, and presents the value in the local REAL representation. Without the swap, the value will be the wrong number, often off by a factor of 10 or 100, or sign-flipped.

Back to blog