Problem Overview
Energy meters and power-quality devices frequently publish 32-bit quantities in INT32-M10K (signed 32-bit, Modulus-10000) format rather than a straight two's-complement 32-bit integer. A generic Modbus master that reads two registers and casts them to int32 returns a wrong number, because the two words are not binary-weighted halves — they are decimal-weighted digits.
Typical register map entry from a device documentation table:
| Label | Address | Registers | Format |
|---|---|---|---|
| kWh del | 40089 | 2 | INT32-M10K |
Rapid SCADA has no built-in M10K element type in the Modbus driver, so the decode has to be done in the configuration database using channel formulas. The pattern below splits the pair into two 16-bit reads and recombines them in a calculated channel.
How INT32-M10K Is Encoded
The value is expressed as two signed decimal groups of four digits, with the high group weighted by 10,000:
Value = RegisterHigh * 10000 + RegisterLow
Both registers are signed 16-bit integers, and both carry the sign of the overall value. Worked example for -12,345,678:
| Word | Decimal | Hex (16-bit, two's complement) | Contribution |
|---|---|---|---|
| Register high (40089) | -1234 | FB2E | -1234 × 10000 = -12,340,000 |
| Register low (40090) | -5678 | E9D2 | -5,678 |
| Result | -12,345,678 | ||
Key consequences of the encoding:
- The low word is constrained to
-9999 … 9999. Any low word outside that band means you are reading the wrong register, the wrong word order, or the device is not actually using M10K. - A plain two's-complement 32-bit cast of
FB2E E9D2yields-81,530,414, not-12,345,678. This mismatch is the fastest way to confirm a device is using M10K. - Zero has one representation only when both words are zero; there is no separate negative-zero condition to trap.
- The same family often extends to
INT64-M10Kacross four registers with weights 1012, 108, 104, 1. Verify the weighting against your device manual before reusing the formula for four-register points.
Communicator Configuration: Read the Words Separately
Configure the Modbus device in Communicator so each register lands in its own tag, rather than letting the driver combine them.
- Add the Modbus device and create a holding-register element group starting at the mapped address. Remember the 4xxxxx convention:
40089normally corresponds to protocol address88(zero-based) with function code 03. If the device documentation is zero-based already, use89. Confirm by reading a register with a known constant value. - Define two elements of 1 register each — for example
kWh_del_hiandkWh_del_lo— instead of one 2-registerintelement. - Select a 16-bit element type. Reading as unsigned (
ushort) is acceptable and often more portable, because the sign is restored in the formula. If the driver offers a signed 16-bit type and the device conforms strictly to the format, that also works and removes one conversion step. - Bind each element to an input channel in the configuration database. Note the channel numbers — the decode formula references them.
Practical channel allocation example:
| Channel | Name | Role |
|---|---|---|
| 101 | kWh del high (raw) | Input, raw 16-bit word, hidden from operators |
| 102 | kWh del low (raw) | Input, raw 16-bit word, hidden from operators |
| 103 | kWh del | Calculated, engineering value in kWh |
Formula Implementation
Rapid SCADA compiles channel formulas as C# expressions, and you can add reusable helper methods in the formula source of the configuration database (Formulas table in v5, Scripts in v6). Add one helper and call it from a calculated channel:
// Reusable helper: decode signed 32-bit Modulus-10000
// hi, lo are raw 16-bit register values read as unsigned (0..65535)
public double DecodeM10K(double hi, double lo)
{
if (hi > 32767) hi -= 65536; // restore two's complement sign
if (lo > 32767) lo -= 65536;
return hi * 10000 + lo;
}
Formula for the calculated channel 103:
DecodeM10K(Val(101), Val(102))
If the Communicator element type is already signed 16-bit, the two if lines are no-ops and can be dropped:
Val(101) * 10000 + Val(102)
A minimal two-channel variant, applying a scaling formula on each raw input channel and summing them, also works — multiply the high channel by 10,000 in its own input-channel formula (the raw value is available as Cnl in the input channel formula) and add the two in the display channel. That approach is simpler to type but harder to maintain, because the ×10,000 scaling is invisible on the calculated channel and breaks silently if the sign conversion is missing on one of the two channels.
Stat(101) and Stat(102) before returning the decoded value and returning an undefined status otherwise. Check the formula reference for your Rapid SCADA version for the exact status constants and the current status-setting syntax.Version Notes and Data Typing
| Item | Rapid SCADA 5 | Rapid SCADA 6 |
|---|---|---|
| Helper code location | Formulas table in Administrator | Scripts / formula source in Administrator |
| Channel that carries the decode | Input channel of calculated type, Formula field | Channel with data type set to calculated, Formula field |
| Reference to another channel |
Val(n), Stat(n)
|
Val(n), Stat(n)
|
| Raw value of own channel | Cnl |
Verify in the version's formula reference |
Channel values are stored as double precision. A 53-bit mantissa represents every integer up to 253 exactly, so the full M10K range decodes with no rounding error — including the INT64-M10K case up to 1016 only if you stay below 9,007,199,254,740,992. Above that, the least significant digits of a 64-bit totalizer are no longer exact in a double.
Driver and module source, plus the issue tracker for the current release line, are published in the official repository: https://github.com/RapidScada/scada-v6. If your device family needs M10K natively, the Modbus driver source there is the starting point for adding a custom element type instead of doing the decode in formulas.
Verification and Troubleshooting
-
Static check. With the device running, note the two raw values in the Communicator device data page. Compute
hi * 10000 + loby hand and compare with the meter's local display. They must match digit for digit. - Sign check. Force or wait for a negative reading (import/export energy, reactive power, signed power factor). Both words must go negative together. If only the high word changes sign, the device violates the format — see below.
-
Word-order check. If the decoded value is unstable and huge while the meter display is small, the words are swapped. Swap the arguments:
DecodeM10K(Val(102), Val(101)). -
Range check. Add a temporary calculated channel that outputs
Val(102)alone. If it ever exceeds ±9999, the low register is not an M10K low word. - Rollover check. Watch the transition as the low word crosses 9999 → 0. The high word must increment by exactly 1 and the decoded total must increase by 1, with no step of 10,000 or 65,536.
Common Failure Modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Value is roughly 6.5× too large or negative when it should be small | Unsigned 16-bit raw not sign-corrected; a value of 65,536 or 655,360,000 offset appears | Apply the > 32767 → -65536 correction to both words |
| Decoded value off by a multiple of 65,536 only when negative | Non-conforming device sends the low word unsigned while the high word is signed | Sign-correct the high word only, then add the unsigned low word; validate against the meter display across zero |
| Result matches a plain int32 cast, not M10K | Point is actually INT32 or UINT32, not M10K | Re-read the register map; use the driver's native 2-register integer element |
| Energy totalizer resets or jumps backwards | Counter rollover at the device limit, or the two registers were read in separate Modbus transactions across an update boundary | Read both registers in one request block; add rollover handling in the formula or use a totalizer channel |
| Value updates only on one word | The two elements are in different polling groups with different periods | Place both registers in the same element group so they are acquired atomically |
Scaling to Engineering Units
M10K decoding produces the raw counter value only. Apply the device's documented energy scale factor separately — many meters publish kWh in whole units, others in 0.1 kWh or Wh. Extend the calculated channel formula rather than changing the raw channels, so the raw counter stays available for diagnostics:
DecodeM10K(Val(101), Val(102)) * 0.1 // device reports 0.1 kWh per count
Set the channel's unit and display format in the configuration database so the operator sees the correct decimal places, and set the channel type to a counter/totalizer type if you want Rapid SCADA to compute consumption between periods.
What does INT32-M10K mean in a Modbus register map?
It is a signed 32-bit value encoded as two decimal-weighted 16-bit registers: Value = RegisterHigh * 10000 + RegisterLow. Both registers are signed, and the low register is limited to -9999 through 9999.
Why does my int32 read of an M10K point return the wrong number?
A standard int32 cast treats the words as binary-weighted (high × 65536), but M10K weights the high word by 10,000. For example, FB2E E9D2 casts to -81,530,414 but decodes as -12,345,678 in M10K.
How do I convert an unsigned 16-bit Modbus register to a signed value in a Rapid SCADA formula?
Subtract 65536 when the raw value exceeds 32767: if (v > 32767) v -= 65536;. Apply it to both the high and low words before combining them.
Where do I put reusable decode code in Rapid SCADA?
Add the helper method to the formula source in Administrator (Formulas table in version 5, Scripts in version 6), then call it from a calculated channel's Formula field using Val(n) to reference the raw input channels.
How can I confirm the high and low registers are not swapped?
Check that the register mapped as the low word always stays within -9999 to 9999 and that the decoded total matches the meter's local display. If the value is wildly large and jumps unpredictably, swap the two arguments in the decode call.