— Mathematics, Measurements & Advanced Poll-Planning
(Module 9 · Advanced & Specialised Topics)
“Why optimise? The line stops when the bus stalls.”
In many plants a Modbus segment is the critical path between a PLC and the drives, meters, or safety relays that keep product moving. Shave milliseconds from the poll cycle and you free PLC scan-time, increase historian granularity, extend radio battery life, or simply avoid race conditions that crash production. This chapter is a deep-dive laboratory: theory → formula → code → oscilloscope proof.
Section | Outcome |
---|---|
28.1 | Throughput equations for RTU & TCP, including CRC and inter-frame gaps. |
28.2 | Baud-rate ladder — which step actually improves cycle time, when noise steals it back. |
28.3 | Optimal poll-blocking: ❶ size ❷ alignment ❸ atomicity (FC 23) ❹ broadcast read. |
28.4 | Concurrency & pipelining (TCP) — multi-TID window control with proofs. |
28.5 | Event-driven & delta strategies — change-of-value, hysteresis, and exception-based polling. |
28.6 | Real-time OS tuning on gateways (FreeRTOS/Scheduler, Linux PREEMPT_RT). |
28.7 | Energy & radio optimisation math (LoRa/4 G) — bytes ↔ mAh. |
28.8 | Benchmark kit: Saleae Logic + Python notebooks + Grafana dashboards. |
28.9 | Best-practice checklist & printable cheat card. |
28.1 Raw throughput mathematics
28.1.1 RTU frame timing
Tframe = (Nbytes + 2 CRC) × (StartBit + 8 Data + Parity + StopBits) / Baud
Tgap = 3.5 × (StartBit + 8 + P + S) / Baud
For 8E1 (11 bits/char):
Tframe(ms) = 11 × (N+2) / Baud(kBd)
Tgap(ms) = 38.5 / Baud(kBd)
Example – read 125 holding registers (8-bit Address + FC + 4 B start/qty + 2 B CRC + 2 +125×2 B response):
TX = 1+1+2+2 = 6 B
RX = 1+1+2+1+250+2 = 257 B
Total= 263 B
At 38 400 Bd → 11 × 263 / 38.4 ≈ 75 ms + 2 gaps (2 ×1 ms) ≈ 77 ms
Rule-of-thumb: RTU 38 k4 + 125-word block ≈ 80 ms roundtrip.
28.1.2 TCP frame timing
Tframe = RTT + (Header_IP + Header_MBAP + Payload) / LinkRate
GigE LAN RTT ≈ 0.25 ms; TCP/IP+ETH header = 54 B; MBAP = 7 B.
Block read 125 regs → 257 B payload, total 318 B:
Wire time 318 B /1 Gb ≈ 2.5 µs — dominated by RTT & slave delay ➜ focus on pipelining.
28.2 Baud-rate ladder & diminishing returns
Baud | Frame 125 regs (ms) | % gain over previous |
---|---|---|
9 600 | 303 | — |
19 200 | 151 | +50 % |
38 400 | 77 | +49 % |
57 600 | 52 | +32 % |
115 200 | 26 | +50 % |
230 400* | 14 | +46 % |
⚠ At >115 kBd noise margin collapses on >100 m un-shielded RS-485; plan repeaters.
28.3 Optimal poll-blocking strategies
28.3.1 Block size selection
#regs | Payload (B) | Overhead(B) | Efficiency |
---|---|---|---|
1 | 2 | 9 | 18 % |
10 | 20 | 9 | 69 % |
60 | 120 | 9 | 93 % |
125 | 250 | 9 | 96 % |
60–64 registers sweet-spot on noisy lines (shorter retries) + fits some slaves’ 128-B buffer.
28.3.2 Memory alignment rules
- Start address divisible by 2 → word-aligned.
- Group tags by update frequency:
- Fast (every scan) ⇒ bank 0.
- Slow (once/min) ⇒ bank 3.
28.3.3 Atomic read-modify-write with FC 23
Cycle-time comparison
Method | Frames | Roundtrip (38 k4) |
---|---|---|
Read (FC3) + Write (FC16) | 2 | 154 ms |
FC 23 | 1 | 90 ms |
Save 42 % bus time and remove race risk.
28.3.4 Broadcast read ID 0 (expert)
Use only on homogeneous sensor loops.
One frame → all slaves respond in silent mode and do not reply.
Reduces bus traffic at cost of non-confirming data.
Implementation: Master sets “direction flag” to mark broadcast value suspect until next confirmed slave reply (delta filter).
28.4 TCP concurrency & pipelining
28.4.1 Window-size algorithm
Optimal_window = ceil( Link_BW × RTT / Payload )
LAN 1 Gb, RTT 0.25 ms, payload 318 B → 1 frame in flight saturates; but latency can be hidden for multiple slaves.
28.4.2 Multi-TID pipeline code (Python asyncio)
WINDOW = 4
sem = asyncio.Semaphore(WINDOW)
async def pipereq(cli, unit, start, cnt):
async with sem:
return await cli.read_holding_registers(start, cnt, unit=unit)
await asyncio.gather(*(pipereq(cli, u, 0, 125) for u in range(1,9)))
Measured throughput 9 000 regs/s vs 1 800 regs/s single-flight on same network.
(Listing 28-1 provided.)
28.5 Event-driven and delta-based polling
Method | Gateway logic | Typical reduction |
---|---|---|
Change-of-value (CoV) | Publish only if | value-prev |
Hysteresis | Two thresholds (enter/exit) | Avoids chatter |
Time-weighted average | Send one average over N polls | 95 % |
Report-by-exception (limited vendor support) | Slave sends unsolicited FC 42 | 99 % |
Node-RED function snippet:
if(Math.abs(msg.payload - context.last) > context.band){
context.last = msg.payload;
return msg;
}
28.6 Real-time OS & gateway tuning
OS / Feature | Setting | Effect |
---|---|---|
Linux PREEMPT_RT | isolcpus=1 for IRQ + taskset gateway process | Cuts max jitter 800 µs → 90 µs |
FreeRTOS | Move UART ISR to higher priority than TCP stack | Prevents RX FIFO overflow on 115 kBd |
Clock sync | PTP/IEEE 1588 across gateways | Aligns sample timestamps < 1 µs |
(Fig-28-1: histogram of inter-poll jitter before/after RT patch.)
28.7 Energy & radio bandwidth calculus
28.7.1 Bytes→mA-h
E_tx (mAh) = (Bytes × 8 / Baud) × I_tx (A) × 1000 / 3600
LoRa 22 dBm radio, I_tx = 120 mA, 9600 Bd, 250 B
→ 7.4 µAh per poll.
With delta CoV reduce 90 % = 0.74 µAh ⇒ 5 yr on 3 Ah cell.
28.7.2 Radio link budget for RTU
CRC retransmissions cost ~~2 × frame bytes.
Plan for 10 % radio loss ➜ add 20 % battery margin.
28.8 Benchmark kit
- Saleae Logic 8 — capture RX/TX, measure Tgap.
- Python Jupyter workbook (
/tools/benchmark.ipynb
) — imports.logicdata
, auto plots latency. - Grafana JSON — dashboards for exception rate, throughput, jitter.
(Fig-28-2: Grafana screenshot).
28.9 Best-practice cheat card
Parameter | Rule-of-thumb |
---|---|
Block size | 60–125 regs or 400–800 coils |
Gap timeout | 1.5× manufacturer spec |
Baud | 38 k4 for ≤200 m; else 19 k2 |
Window (TCP) | 2–4 in-flight TIDs |
CoV deadband | 1 % FS or 1 LSB, whichever larger |
Retry | 1 (RTU), 2 (radio), back-off ×2 |
Jitter budget | < 10 % of process dead-time |
Chapter recap
- Equations first: know your frame time, gap, RTT before touching a cable.
- Baud alone never saves the day; block reads and pipeline windows do.
- Change-of-value and FC 23 are hidden gems for bandwidth.
- OS-level tuning (PREEMPT_RT, ISR priority) slashes jitter into microseconds.
- Battery-powered or radio links demand byte-level accounting—optimise-to-mAh.
Deliverables for download
- Listing 28-1 – async pipelined Python poller
- benchmark.ipynb – interactive notebook to compute & visualise cycle time
- grafana_modbus_perf.json – dashboard ready to import
- cheat_card.pdf – laminated quick reference
Next (Chapter 29): Modbus Compliance & Conformance Testing — how to verify that every optimisation still obeys the spec, passes Modbus.org test harnesses, and plays nicely with third-party masters and slaves.