Resources Numerical accuracy Calculation policy

Controller standard functions and integer types

Timers, counters, arithmetic functions and type conversions of the soft controller.

How the precision is kept

Integer types up to 32 bits are wrapped exactly in two's complement. The 64-bit integer types are held as 64-bit floating numbers and are exact only up to 2⁵³; they are not wrapped. Timers compare elapsed milliseconds held as numbers. Real to integer conversion rounds to nearest with ties to even.

Open item. Hold the 64-bit integer types in BigInt so they stay exact and wrap at 64 bits.

Calculations under this policy

6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

std.js Shared shell, every app 64-bit 6 calculations Reviewed

Tests bin/test-engines.mjs, tools/plc-test/test.mjs

Wrap into an integer type

Exact

A number brought into the range of SINT, INT, DINT and the other integer types.

v mod 2ⁿ, moved into the signed range

The value is cut toward zero, reduced modulo 2ⁿ and moved into the signed range, all without rounding for types up to 32 bits. Types of 53 bits or more are returned unwrapped, so a 64-bit integer is exact only while it stays below 2⁵³.

  • std.js
  • lines 8 to 15
/** Wrap a number into an integer type (two's complement). 64-bit types are held as doubles: exact to 2^53, said in the docs. */
export function wrapInt(type, v) {
  const spec = INT_TYPES[type]; v = Math.trunc(v);
  if (!spec || !Number.isFinite(v)) return Number.isFinite(v) ? v : 0;
  const [bits, signed] = spec; if (bits >= 53) return v;
  const mod = 2 ** bits; v = ((v % mod) + mod) % mod;
  return signed && v >= mod / 2 ? v - mod : v;
}

On-delay timer

64-bit

The elapsed time and output of a TON block.

ET = min(now − t₀, PT); Q = ET ≥ PT

Elapsed time is the difference of two scan times in milliseconds. The output switches on the first scan where the difference reaches PT, so its timing has the resolution of the scan.

  • std.js
  • lines 36 to 40
case 'TON': {
  if (g('IN')) { if (!st.run) { st.run = true; st.t0 = now; } const et = Math.min(now - st.t0, g('PT')); v.set('ET', et); v.set('Q', et >= g('PT')); }
  else { st.run = false; v.set('ET', 0); v.set('Q', false); }
  break;
}

Up counter

Exact

The count and output of a CTU block.

CV ← CV + 1 on a rising edge while CV < 32767; Q = CV ≥ PV

Whole-number steps on 64-bit numbers, which hold these values without rounding. The count holds at the largest INT value instead of wrapping.

  • std.js
  • lines 56 to 60
case 'CTU': {
  const max = intMax('INT');
  if (g('R')) v.set('CV', 0); else if (g('CU') && !st.cu && g('CV') < max) v.set('CV', g('CV') + 1);
  st.cu = g('CU'); v.set('Q', g('CV') >= g('PV')); break;
}

Round to nearest, ties to even

Exact

The rounding used when a real is converted to an integer.

nearest whole number; a tie goes to the even neighbor

Math.round sends ties toward +∞; this corrects a tie to the even neighbor. A fraction of exactly one half is always representable, so the tie test is exact.

  • std.js
  • line 78
const roundEven = (x) => { const r = Math.round(x); return Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r; };

Arithmetic functions

64-bit

ABS, SQRT, LN, LOG, EXP, the trigonometric functions, MIN, MAX, LIMIT, TRUNC, SHL and SHR.

SQRT, LN, LOG, EXP, SIN and the rest from Math; SHL: x × 2ⁿ; SHR: ⌊x ÷ 2ⁿ⌋; TRUNC toward zero

Each function is the matching Math call on a 64-bit number. ASIN and ACOS are not clamped and return NaN outside [−1, 1]. SHR rounds down, which for a negative value is an arithmetic shift.

  • std.js
  • lines 81 to 95
export const STD_FN = {
  ABS: { n: [1, 1], t: (a) => a[0], f: (x) => Math.abs(x) },
  SQRT: { n: [1, 1], t: () => 'REAL', f: (x) => Math.sqrt(x) },
  LN: { n: [1, 1], t: () => 'REAL', f: (x) => Math.log(x) },
  LOG: { n: [1, 1], t: () => 'REAL', f: (x) => Math.log10(x) },
  EXP: { n: [1, 1], t: () => 'REAL', f: (x) => Math.exp(x) },
  SIN: { n: [1, 1], t: () => 'REAL', f: Math.sin }, COS: { n: [1, 1], t: () => 'REAL', f: Math.cos }, TAN: { n: [1, 1], t: () => 'REAL', f: Math.tan },
  ASIN: { n: [1, 1], t: () => 'REAL', f: Math.asin }, ACOS: { n: [1, 1], t: () => 'REAL', f: Math.acos }, ATAN: { n: [1, 1], t: () => 'REAL', f: Math.atan },
  MIN: { n: [2, 8], t: (a) => a.includes('REAL') ? 'REAL' : a[0], f: (...x) => Math.min(...x) },
  MAX: { n: [2, 8], t: (a) => a.includes('REAL') ? 'REAL' : a[0], f: (...x) => Math.max(...x) },
  LIMIT: { n: [3, 3], t: (a) => a[1], f: (mn, x, mx) => Math.min(Math.max(x, mn), mx) },
  SEL: { n: [3, 3], t: (a) => a[1], f: (g, a, b) => (g ? b : a) },
  TRUNC: { n: [1, 1], t: () => 'DINT', f: (x) => Math.trunc(x) },
  SHL: { n: [2, 2], t: (a) => a[0], f: (x, n) => x * 2 ** n },
  SHR: { n: [2, 2], t: (a) => a[0], f: (x, n) => Math.floor(x / 2 ** n) },

Type conversions

64-bit

REAL_TO_INT, INT_TO_REAL, BOOL_TO_INT, TIME_TO_REAL and the other A_TO_B functions.

to an integer type: round to nearest (ties to even), then wrap; to a real type: unchanged

A real that is out of range for the integer type wraps around instead of saturating. Conversions to a real type return the number unchanged, and a time is its count of milliseconds.

  • std.js
  • lines 99 to 110
/** `A_TO_B` conversions, e.g. REAL_TO_INT (nearest, ties to even — the standard leaves the tie rule to the implementer), INT_TO_REAL, BOOL_TO_INT, TIME_TO_REAL (milliseconds). */
export function conversion(name) {
  const m = /^([A-Z_0-9]+?)_TO_([A-Z_0-9]+)$/.exec(name); if (!m) return null;
  const [, from, to] = m, ok = (t) => t === 'BOOL' || t in INT_TYPES || REAL_TYPES.includes(t) || t === 'TIME' || t === 'STRING';
  if (!ok(from) || !ok(to)) return null;
  return { from, to, f: (x) => {
    if (to === 'BOOL') return !!x;
    if (to === 'STRING') return String(x);
    if (to in INT_TYPES) return wrapInt(to, from in INT_TYPES || from === 'BOOL' ? num(x) : roundEven(num(x)));
    return num(x);
  } };
}

In detail

Method
Functions and function blocks written from IEC 61131-3 (the file cites clauses 6.6.2 and 6.6.3).
Number types
64-bit numbers for every type; integer types are wrapped after a conversion.
Limits
Integer values of the 64-bit types beyond 2⁵³ lose their low bits. SHL multiplies by 2ⁿ and SHR divides and rounds down; neither wraps inside the function. Counters stop at the ends of the INT range.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach