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:
- Field device — Algodue UPT210 (default slave address 1, 9600 bps, 8N1, RS-485).
- 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).
-
SCADA — CitectSCADA / AVEVA Plant SCADA project polling the meter through the built-in Modbus RTU driver (board type
COMX, protocolMODBU).
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
MODBUprotocol 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… |
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 byte — slave address (1…247)
- 1 byte — function code (03 for holding registers, 04 for input registers, 06 for single write, 10 for multiple write)
- N bytes — payload (starting address, quantity, data)
- 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 of0x8005) - 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:
- Init
crc = 0xFFFF - Process
0x01:crc = 0xFFFF XOR 0x01 = 0xFFFE; eight right shifts with0xA001XOR on the pop yields0xE0FE - Process
0x03:crc = 0xE0FE XOR 0x03 = 0xE0FD; bit-shifts give0xC0FD - Process
0x00:crc = 0xC0FD XOR 0x00 = 0xC0FD; bit-shifts give0x807E - Process
0x1C:crc = 0x807E XOR 0x1C = 0x8062; bit-shifts give0x4031 - Process
0x00:crc = 0x4031 XOR 0x00 = 0x4031; bit-shifts give0xA018 - Process
0x10:crc = 0xA018 XOR 0x10 = 0xA008; bit-shifts give0x85C0
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:
- Open the project, navigate to Communications → I/O Devices.
- 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) - Compile the project. The kernel will report the device in the syslog.dbf as UPT210 - startup OK.
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 numberxxxx-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
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
- After compiling, open Citect Explorer and right-click the project, then Run As → Stand-alone Simulation.
- Open the Citect Kernel view. The line
UPT210 : startup OKconfirms the driver has opened the port and polled successfully. - 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).
- Force a refresh from the command line with
TagForceRead("UPT210_Voltage"). - Cross-check the floating-point values against the meter's local display. If the value is exactly divided by 10 or 100, the
ByteSwapflag is the wrong way — toggle[Modbus]ByteSwapincitect.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
0x8005reversed =0xA001— correct for Modbus, but0x1021is 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.DBW0with 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.