ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hart.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 hart.h
6 * @brief HART / HART-IP process-instrument protocol codec (PROTOCORE_ENABLE_HART).
7 *
8 * HART (Highway Addressable Remote Transducer, FieldComm) is the field-instrument protocol that rides
9 * the 4-20 mA current loop as an FSK signal, and - as **HART-IP** - travels over UDP/TCP 5094 as the
10 * gateway-friendly, front-end-free path. This is the wire codec for both:
11 *
12 * - The **HART command frame**: `[delimiter][address...][command][byte-count][data...][checksum]`, where
13 * the checksum is the longitudinal XOR parity of every byte from the delimiter through the last data
14 * byte (the preamble of 0xFF sync bytes is transport, not checksummed). Short (1-byte polling) and
15 * long (5-byte unique-ID) addressing are both handled by passing the address bytes.
16 * - The **HART-IP message header** (8 octets): version, message type, message id, status, a 2-byte
17 * sequence number, and the 2-byte total message length - wraps a HART PDU for UDP/TCP transport.
18 *
19 * Pure, zero heap, no stdlib, host-testable. The FSK physical layer (a HART modem IC over UART) is the
20 * hardware-gated path; HART-IP needs no front end.
21 */
22
23#ifndef PROTOCORE_HART_H
24#define PROTOCORE_HART_H
25
26#include "protocore_config.h" // the entry point: protocore_types.h for the widths
27
28#if PROTOCORE_ENABLE_HART
29
31
32// This module holds nothing between calls, so it carves no borrow and states none. An entry
33// takes one all the same, and never reads it, so every namespace in the tree is invoked the
34// same way.
35
36/** @brief HART frame delimiter frame-type bits (low 3 bits) + long-address bit (bit 7). Wire values,
37 * the LONG_ADDR bit is OR'd in, so integer constants in a namespacing struct (cast-free). */
38#define HART_DELIM_BACK 0x01 ///< burst (field device, unsolicited).
39#define HART_DELIM_STX 0x02 ///< master -> field device (request).
40#define HART_DELIM_ACK 0x06 ///< field device -> master (response).
41#define HART_DELIM_LONG_ADDR 0x80 ///< OR into the delimiter for 5-byte unique-ID addressing.
42
43/** @brief HART-IP message types + common message ids (wire constants). */
44#define HARTIP_MSG_REQUEST 0
45#define HARTIP_MSG_RESPONSE 1
46#define HARTIP_MSG_PUBLISH 2
47#define HARTIP_ID_SESSION_INIT 0
48#define HARTIP_ID_SESSION_CLOSE 1
49#define HARTIP_ID_KEEPALIVE 2
50#define HARTIP_ID_TOKEN_PDU 3 ///< a HART token-passing PDU (a HART frame) is the payload.
51#define HARTIP_HEADER_LEN 8
52
53/** @brief A parsed HART frame (pointers into the input buffer). */
54typedef struct
55{
56 uint8_t delimiter;
57 const uint8_t *addr;
58 size_t addr_len; ///< 1 (short) or 5 (long), derived from the delimiter's long-address bit.
59 uint8_t command;
60 uint8_t byte_count;
61 const uint8_t *data;
62 size_t data_len;
63} HartFrame;
64
65/** @brief A parsed HART-IP message header + payload slice (the payload points into the input buffer). */
66typedef struct
67{
68 uint8_t version; ///< HART-IP protocol version (1)
69 uint8_t msg_type; ///< message type (HartIp::HARTIP_MSG_*)
70 uint8_t msg_id; ///< message id (HartIp::HARTIP_ID_*)
71 uint8_t status; ///< status / error byte (0 in a request)
72 uint16_t seq; ///< sequence number
73 uint16_t total_len; ///< total message length (header + payload) declared in the header
74 const uint8_t *payload; ///< the payload after the 8-octet header, or nullptr if none
75 size_t payload_len; ///< payload length (total_len - 8)
76} HartIpHeader;
77
78/** @brief What checksum takes: bytes, len. */
79typedef struct
80{
81 const uint8_t *bytes;
82 size_t len;
83} HartChecksumArgs;
84
85/** @brief What build takes: delimiter, addr, addr_len, command, data, ... */
86typedef struct
87{
88 uint8_t delimiter; ///< frame delimiter (e.g. HART_DELIM_STX, OR HART_DELIM_LONG_ADDR for long addressing)
89 const uint8_t *addr; ///< address bytes (1 for short, 5 for long)
90 size_t addr_len; ///< 1 or 5
91 uint8_t command; ///< HART command number
92 const uint8_t *data; ///< data bytes (may be null when data_len == 0)
93 size_t data_len; ///< number of data bytes (also the frame's byte-count field)
94 uint8_t *out; ///< output buffer
95 size_t cap; ///< capacity of out
96} HartBuildArgs;
97
98/** @brief What parse takes: frame, len, out. */
99typedef struct
100{
101 const uint8_t *frame;
102 size_t len;
103 HartFrame *out;
104} HartParseArgs;
105
106/** @brief What ip_build_header takes: msg_type, msg_id, status, seq, ... */
107typedef struct
108{
109 uint8_t msg_type; ///< HARTIP_MSG_*
110 uint8_t msg_id; ///< HARTIP_ID_*
111 uint8_t status; ///< status byte (0 in a request)
112 uint16_t seq; ///< sequence number
113 uint16_t total_len; ///< total message length including this header (header + payload)
114 uint8_t *out;
115 size_t cap;
116} HartIpBuildHeaderArgs;
117
118/** @brief What ip_parse_header takes: buf, len, out. */
119typedef struct
120{
121 const uint8_t *buf;
122 size_t len;
123 HartIpHeader *out;
124} HartIpParseHeaderArgs;
125
126/**
127 * @brief HART / HART-IP process-instrument protocol codec (PROTOCORE_ENABLE_HART).
128 *
129 * A caller sets the members a call takes, invokes it through ::Hart with the bytes it runs
130 * out of, and reads the outcome off the same handle.
131 *
132 * Hart.checksum_args.bytes = ...;
133 * Hart.checksum_args.len = ...;
134 * Hart.checksum(work);
135 * // Hart.value is what the call reports
136 *
137 * @var HartNs::checksum_args what checksum takes: bytes, len
138 * @var HartNs::build_args what build takes: delimiter, addr, addr_len, command, data,
139 * @var HartNs::parse_args what parse takes: frame, len, out
140 * @var HartNs::ip_build_header_args what ip_build_header takes: msg_type, msg_id, status, seq,
141 * @var HartNs::ip_parse_header_args what ip_parse_header takes: buf, len, out
142 * @var HartNs::ok true if the frame is well-formed and the checksum matches; fills out
143 * @var HartNs::value the value a call reports
144 * @var HartNs::n the frame length written, or 0 if it would not fit or addr_len is ...
145 * @var HartNs::checksum longitudinal XOR checksum of len bytes (the HART frame check byte)
146 * @var HartNs::build build a HART command frame (no preamble - the transport prepends ...
147 * @var HartNs::parse validate + parse a HART frame (checksum checked)
148 * @var HartNs::ip_build_header build the 8-octet HART-IP message header into out (>= 8 bytes)
149 * @var HartNs::ip_parse_header parse an 8-octet HART-IP message header and expose its payload ...
150 *
151 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
152 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
153 * a caller drives every namespace the same way.
154 */
155typedef struct
156{
157 HartChecksumArgs checksum_args;
158 HartBuildArgs build_args;
159 HartParseArgs parse_args;
160 HartIpBuildHeaderArgs ip_build_header_args;
161 HartIpParseHeaderArgs ip_parse_header_args;
162 proto_bool ok;
163 uint8_t value;
164 size_t n;
165} HartVars;
166
167/** @brief The operands and the outcome. */
168extern HartVars HartV;
169
170/** @brief The entries. */
171typedef struct
172{
173 void (*const checksum)(uint8_t *work);
174 void (*const build)(uint8_t *work);
175 void (*const parse)(uint8_t *work);
176 void (*const ip_build_header)(uint8_t *work);
177 void (*const ip_parse_header)(uint8_t *work);
178} HartNs;
179
180// What the table binds, defined once in the .c and taking one parameter each: everything
181// else an entry needs is an operand in HartV or a region of the borrow at a fixed offset.
182void protocore_hart_checksum(uint8_t *work);
183void protocore_hart_build(uint8_t *work);
184void protocore_hart_parse(uint8_t *work);
185void protocore_hart_ip_build_header(uint8_t *work);
186void protocore_hart_ip_parse_header(uint8_t *work);
187
188// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
189// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
190// `Hart.checksum(work)` resolves to a named function and becomes a DIRECT call. An extern table
191// leaves the call indirect and the symbol live at every level, -O2 -flto included.
192static const HartNs Hart __attribute__((unused)) = {
193 .checksum = protocore_hart_checksum,
194 .build = protocore_hart_build,
195 .parse = protocore_hart_parse,
196 .ip_build_header = protocore_hart_ip_build_header,
197 .ip_parse_header = protocore_hart_ip_parse_header,
198};
199
201
202#endif // PROTOCORE_ENABLE_HART
203
204#endif // PROTOCORE_HART_H
#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