ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
modbus.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file modbus.h
6 * @brief Zero-heap Modbus TCP slave/server (Modbus Application Protocol v1.1b3).
7 *
8 * Split like the CoAP/SNMP services into a pure, host-testable core and an
9 * ESP32-only TCP transport:
10 *
11 * - protocore_modbus_process_adu() takes a complete Modbus TCP ADU (MBAP header + PDU) and
12 * produces the response ADU in a caller buffer - no sockets, no heap. It is
13 * unit-tested on the host (env:native_modbus).
14 * - protocore_modbus_rx() is the ProtoConn::PROTO_MODBUS data handler dispatched by the session layer;
15 * it frames ADUs out of the rx ring and feeds them through
16 * protocore_modbus_process_adu(). The slave keeps no per-connection state (a partial
17 * frame waits in the rx ring), so no accept/close hooks are needed. Open the
18 * port with listen(502, ProtoConn::PROTO_MODBUS).
19 *
20 * The data model is four fixed BSS tables (coils, discrete inputs, holding
21 * registers, input registers). The application reads and writes them with the
22 * accessors below; a write arriving from a client also fires protocore_modbus_on_write().
23 *
24 * Supported function codes: 0x01 Read Coils, 0x02 Read Discrete Inputs,
25 * 0x03 Read Holding Registers, 0x04 Read Input Registers, 0x05 Write Single Coil,
26 * 0x06 Write Single Register, 0x0F Write Multiple Coils, 0x10 Write Multiple
27 * Registers. Any other function code returns exception 0x01 (Illegal Function).
28 *
29 * Modbus has no authentication or encryption - run it only on a trusted network.
30 */
31
32#ifndef PROTOCORE_MODBUS_H
33#define PROTOCORE_MODBUS_H
34
35#include "protocore_config.h" // the entry point: protocore_types.h for the widths
36
37#if PROTOCORE_ENABLE_MODBUS
38
40
41// PROTOCORE_MODBUS_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
42// it into its arena. A caller takes them once and passes the pointer to every call. How they
43// are carved is this module's and is never named here.
44
45/** @brief Largest Modbus TCP ADU (7-byte MBAP + 253-byte PDU). */
46#define MODBUS_ADU_MAX 260
47
48/** @brief Modbus function codes (Modbus Application Protocol §6). */
49typedef enum PROTO_ENUM_PACKED
50{
51 MODBUS_FC_READ_COILS = 0x01,
52 MODBUS_FC_READ_DISCRETE_INPUTS = 0x02,
53 MODBUS_FC_READ_HOLDING_REGS = 0x03,
54 MODBUS_FC_READ_INPUT_REGS = 0x04,
55 MODBUS_FC_WRITE_SINGLE_COIL = 0x05,
56 MODBUS_FC_WRITE_SINGLE_REG = 0x06,
57 MODBUS_FC_WRITE_MULTIPLE_COILS = 0x0F,
58 MODBUS_FC_WRITE_MULTIPLE_REGS = 0x10,
59 MODBUS_FC_MASK_WRITE_REG = 0x16,
60 MODBUS_FC_READ_WRITE_MULTIPLE_REGS = 0x17,
61} ModbusFunction;
62
63/** @brief Modbus exception codes (Modbus Application Protocol §7). */
64typedef enum PROTO_ENUM_PACKED
65{
66 MODBUS_EX_ILLEGAL_FUNCTION = 0x01,
67 MODBUS_EX_ILLEGAL_DATA_ADDRESS = 0x02,
68 MODBUS_EX_ILLEGAL_DATA_VALUE = 0x03,
69 MODBUS_EX_SERVER_FAILURE = 0x04,
70} ModbusException;
71
72/**
73 * @brief Notified after a client write is applied to the data model.
74 *
75 * @param fc the write function code (5, 6, 0x0F, or 0x10).
76 * @param start first coil/register address written.
77 * @param count number of coils/registers written.
78 */
79typedef void (*ModbusWriteCb)(uint8_t fc, uint16_t start, uint16_t count);
80
81/** @brief What on_write takes: cb. */
82typedef struct
83{
84 ModbusWriteCb cb;
85} ModbusOnWriteArgs;
86
87/** @brief What get_coil takes: addr. */
88typedef struct
89{
90 uint16_t addr;
91} ModbusGetCoilArgs;
92
93/** @brief What set_coil takes: addr, on. */
94typedef struct
95{
96 uint16_t addr;
97 proto_bool on;
98} ModbusSetCoilArgs;
99
100/** @brief What get_discrete_input takes: addr. */
101typedef struct
102{
103 uint16_t addr;
104} ModbusGetDiscreteInputArgs;
105
106/** @brief What set_discrete_input takes: addr, on. */
107typedef struct
108{
109 uint16_t addr;
110 proto_bool on;
111} ModbusSetDiscreteInputArgs;
112
113/** @brief What get_holding_reg takes: addr. */
114typedef struct
115{
116 uint16_t addr;
117} ModbusGetHoldingRegArgs;
118
119/** @brief What set_holding_reg takes: addr, value. */
120typedef struct
121{
122 uint16_t addr;
123 uint16_t value;
124} ModbusSetHoldingRegArgs;
125
126/** @brief What get_input_reg takes: addr. */
127typedef struct
128{
129 uint16_t addr;
130} ModbusGetInputRegArgs;
131
132/** @brief What set_input_reg takes: addr, value. */
133typedef struct
134{
135 uint16_t addr;
136 uint16_t value;
137} ModbusSetInputRegArgs;
138
139/** @brief What process_adu takes: req, req_len, resp, ... */
140typedef struct
141{
142 const uint8_t *req;
143 size_t req_len;
144 uint8_t *resp;
145 size_t protocore_resp_cap;
146} ModbusProcessAduArgs;
147
148/** @brief What rtu_process_adu takes: req, req_len, resp, ... */
149typedef struct
150{
151 const uint8_t *req;
152 size_t req_len;
153 uint8_t *resp;
154 size_t protocore_resp_cap;
155 uint8_t my_addr;
156} ModbusRtuProcessAduArgs;
157
158/** @brief What rx takes: slot. */
159typedef struct
160{
161 uint8_t slot;
162} ModbusRxArgs;
163
164/** @brief The Layer 5 dispatch record; server/core/proto_handler.h defines it. */
165struct ProtoHandler;
166
167/**
168 * @brief Zero-heap Modbus TCP slave/server (Modbus Application Protocol v1.1b3).
169 *
170 * A caller sets the members a call takes, invokes it through ::Modbus with the bytes it runs
171 * out of, and reads the outcome off the same handle.
172 *
173 * Modbus.server_init(work);
174 *
175 * @var ModbusNs::on_write_args what on_write takes: cb
176 * @var ModbusNs::get_coil_args what get_coil takes: addr
177 * @var ModbusNs::set_coil_args what set_coil takes: addr, on
178 * @var ModbusNs::get_discrete_input_args what get_discrete_input takes: addr
179 * @var ModbusNs::set_discrete_input_args what set_discrete_input takes: addr, on
180 * @var ModbusNs::get_holding_reg_args what get_holding_reg takes: addr
181 * @var ModbusNs::set_holding_reg_args what set_holding_reg takes: addr, value
182 * @var ModbusNs::get_input_reg_args what get_input_reg takes: addr
183 * @var ModbusNs::set_input_reg_args what set_input_reg takes: addr, value
184 * @var ModbusNs::process_adu_args what process_adu takes: req, req_len, resp,
185 * @var ModbusNs::rtu_process_adu_args what rtu_process_adu takes: req, req_len, resp,
186 * @var ModbusNs::rx_args what rx takes: slot
187 * @var ModbusNs::ok a call's true/false outcome
188 * @var ModbusNs::value the value a call reports
189 * @var ModbusNs::n number of response bytes written, or 0 to send nothing
190 * @var ModbusNs::ptr the pointer a call reports
191 * @var ModbusNs::server_init zero the entire data model and clear the write callback
192 * @var ModbusNs::on_write register a callback invoked after each client write (nullable)
193 * @var ModbusNs::get_coil get_coil
194 * @var ModbusNs::set_coil set_coil
195 * @var ModbusNs::get_discrete_input get_discrete_input
196 * @var ModbusNs::set_discrete_input set_discrete_input
197 * @var ModbusNs::get_holding_reg get_holding_reg
198 * @var ModbusNs::set_holding_reg set_holding_reg
199 * @var ModbusNs::get_input_reg get_input_reg
200 * @var ModbusNs::set_input_reg set_input_reg
201 * @var ModbusNs::process_adu process one Modbus TCP ADU and build the response ADU. Parses the ...
202 * @var ModbusNs::rtu_process_adu process one complete Modbus RTU ADU (`[addr][PDU][CRC16]`) for slave
203 * @var ModbusNs::rx frame and process received Modbus ADUs for the connection on slot
204 * @var ModbusNs::handler handler
205 *
206 * @c work is PROTOCORE_MODBUS_BORROW bytes the CALLER took, at an address it knows. It is not held past the call, so
207 * nothing here aliases it. How those bytes are carved is this module's and is never named here.
208 */
209typedef struct
210{
211 ModbusOnWriteArgs on_write_args;
212 ModbusGetCoilArgs get_coil_args;
213 ModbusSetCoilArgs set_coil_args;
214 ModbusGetDiscreteInputArgs get_discrete_input_args;
215 ModbusSetDiscreteInputArgs set_discrete_input_args;
216 ModbusGetHoldingRegArgs get_holding_reg_args;
217 ModbusSetHoldingRegArgs set_holding_reg_args;
218 ModbusGetInputRegArgs get_input_reg_args;
219 ModbusSetInputRegArgs set_input_reg_args;
220 ModbusProcessAduArgs process_adu_args;
221 ModbusRtuProcessAduArgs rtu_process_adu_args;
222 ModbusRxArgs rx_args;
223 proto_bool ok;
224 uint16_t value;
225 size_t n;
226 const struct ProtoHandler *ptr;
227} ModbusVars;
228
229/** @brief The operands and the outcome. */
230extern ModbusVars ModbusV;
231
232/** @brief The entries. */
233typedef struct
234{
235 void (*const server_init)(uint8_t *work);
236 void (*const on_write)(uint8_t *work);
237 void (*const get_coil)(uint8_t *work);
238 void (*const set_coil)(uint8_t *work);
239 void (*const get_discrete_input)(uint8_t *work);
240 void (*const set_discrete_input)(uint8_t *work);
241 void (*const get_holding_reg)(uint8_t *work);
242 void (*const set_holding_reg)(uint8_t *work);
243 void (*const get_input_reg)(uint8_t *work);
244 void (*const set_input_reg)(uint8_t *work);
245 void (*const process_adu)(uint8_t *work);
246 void (*const rtu_process_adu)(uint8_t *work);
247 void (*const rx)(uint8_t *work);
248 void (*const handler)(uint8_t *work);
249} ModbusNs;
250
251// What the table binds, defined once in the .c and taking one parameter each: everything
252// else an entry needs is an operand in ModbusV or a region of the borrow at a fixed offset.
253void protocore_modbus_server_init(uint8_t *work);
254void protocore_modbus_on_write(uint8_t *work);
255void protocore_modbus_get_coil(uint8_t *work);
256void protocore_modbus_set_coil(uint8_t *work);
257void protocore_modbus_get_discrete_input(uint8_t *work);
258void protocore_modbus_set_discrete_input(uint8_t *work);
259void protocore_modbus_get_holding_reg(uint8_t *work);
260void protocore_modbus_set_holding_reg(uint8_t *work);
261void protocore_modbus_get_input_reg(uint8_t *work);
262void protocore_modbus_set_input_reg(uint8_t *work);
263void protocore_modbus_process_adu(uint8_t *work);
264void protocore_modbus_rtu_process_adu(uint8_t *work);
265void protocore_modbus_rx(uint8_t *work);
266void protocore_modbus_handler(uint8_t *work);
267
268// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
269// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
270// `Modbus.server_init(work)` resolves to a named function and becomes a DIRECT call. An extern table
271// leaves the call indirect and the symbol live at every level, -O2 -flto included.
272static const ModbusNs Modbus __attribute__((unused)) = {
273 .server_init = protocore_modbus_server_init,
274 .on_write = protocore_modbus_on_write,
275 .get_coil = protocore_modbus_get_coil,
276 .set_coil = protocore_modbus_set_coil,
277 .get_discrete_input = protocore_modbus_get_discrete_input,
278 .set_discrete_input = protocore_modbus_set_discrete_input,
279 .get_holding_reg = protocore_modbus_get_holding_reg,
280 .set_holding_reg = protocore_modbus_set_holding_reg,
281 .get_input_reg = protocore_modbus_get_input_reg,
282 .set_input_reg = protocore_modbus_set_input_reg,
283 .process_adu = protocore_modbus_process_adu,
284 .rtu_process_adu = protocore_modbus_rtu_process_adu,
285 .rx = protocore_modbus_rx,
286 .handler = protocore_modbus_handler,
287};
288
289/**
290 * @brief The PROTOCORE_MODBUS_BORROW bytes this module's state lives in.
291 *
292 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
293 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
294 * walks, so the state lasts the life of the program.
295 *
296 * @return the span.
297 */
298uint8_t *protocore_modbus_span(void);
299
301
302#endif // PROTOCORE_ENABLE_MODBUS
303
304#endif // PROTOCORE_MODBUS_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
Per-protocol connection event/poll callbacks (the server's dispatch vtable).
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97