MS/TP byte mode and packet mode
In byte mode the host only moves octets on and off the RS-485 transceiver. The stack receives and checks frames, answers Poll For Master, passes the token, receives COBS-encoded Extended Data frames (NPDUs of 502 to 1497 octets), and sends them for an NPDU of that size that a router or the host supplies (the stack itself limits APDUs on MS/TP to 480 octets, so it does not build one), and discards the echo of its own frames if the transceiver reads them back. There is no engine source to compile into your application: the engine is part of the stack.
| Registration | What it does |
|---|---|
BACnetStack_RegisterCallbackMSTPReceiveBytes() |
Return the octets the receiver has already received, up to the buffer size, without blocking. |
BACnetStack_RegisterCallbackMSTPSendBytes() |
Send one whole frame and return only after the last stop bit has left the transmitter. The driver is released as soon as it returns. |
BACnetStack_RegisterCallbackMSTPMicroseconds() |
One monotonic microsecond clock that wraps at 2^32. It must keep running while the send callback blocks. A clock that does not advance stops the stack sending anything on the bus. |
BACnetStack_RegisterCallbackMSTPTransmitEnable() |
Optional. Switches the RS-485 driver: true before a frame, false after it. |
BACnetStack_RegisterCallbackMSTPFrameHeader() |
Optional. Reports each valid frame header heard, for logs. |
BACnetStack_ReportMSTPReceiveError() |
Tell the stack the UART reported a framing, overrun or parity error. Safe from an interrupt handler. |
BACnetStack_LoopMSTP() |
Run the engine of one port. Never blocks, apart from inside the send callback. |
BACnetStack_GetMSTPCounters() |
Read the counters (below). |
The engine takes its settings from the port’s Network Port object, through the property callbacks you already register for it: MAC_Address
(BACnetStack_RegisterCallbackGetPropertyOctetString(), one octet, 0 to 127, required), Max_Master (which BACnetPropertyIdentifier calls maxManager) and Max_Info_Frames
(BACnetStack_RegisterCallbackGetPropertyUnsignedInteger(); 127 and 1 when the callback is missing) and Link_Speed in bits per second
(BACnetStack_RegisterCallbackGetPropertyReal(); 38400 when missing). They are read when byte mode starts and again after
BACnetStack_ActivateNetworkPortPendingChanges() or a ReinitializeDevice that activates pending changes. A port with no Network Port object, or
without a readable MAC_Address, stays idle. There are no setters for the engine’s settings.
Threading in byte mode
Section titled “Threading in byte mode”BACnetStack_LoopMSTP() may run on its own thread, at the same time as BACnetStack_Tick() and every other BACnetStack_* call except the ones named below. That is
the way to use it where BACnetStack_Tick() can take milliseconds, because MS/TP has deadlines that a long Tick would miss. The stack takes
no lock for it. The two threads share only four things, per port, and each is built to be shared without a lock:
- an outbound queue of NPDUs, from the stack to the engine (4 NPDUs of up to 1497 octets),
- an inbound queue of NPDUs, from the engine to the stack (8 NPDUs of up to 1497 octets),
- the configuration the engine reads at the start of its next loop,
- the counters.
When the outbound queue is full the stack’s send fails and BACnetStack_Send* returns false. At most 4 NPDUs wait in the queue and 3 more in the engine, so a burst of more than 7 sends made before the engine has taken any (a segmented request with a window over 7, several retries in one BACnetStack_Tick()) fails for the ones that do not fit; the failed send is reported to its caller and goes out later. When the inbound queue is full the NPDU just
received is dropped and counted. The engine never calls into the stack: a received NPDU waits in the inbound queue, and BACnetStack_Tick()
processes it through the normal receive path. A single-threaded host calls BACnetStack_Tick() and BACnetStack_LoopMSTP() from its main loop. Do not
add or remove Network Port objects, release a device, or call BACnetStack_Reset(), while another thread is inside BACnetStack_LoopMSTP(),
BACnetStack_ReportMSTPReceiveError() or BACnetStack_GetMSTPCounters(): they look the port up in the table those calls change. Registering or clearing
the MS/TP callbacks is safe at any time: the engine reads each one atomically, may call one you have just cleared once more if that call was already running, and always
releases the RS-485 driver with the callback that switched it on.
Things to know
Section titled “Things to know”- With more than one data link compiled in (BACnet/IP and MS/TP),
BACNET_NETWORK_PORT_DEFAULTdoes not name the MS/TP port: aBACnetStack_Send*call that passes it is refused and returns false. Name the MS/TP Network Port object’s instance. - A port gets its engine on the first
BACnetStack_Tick()after its Network Port object exists and the three required callbacks are registered (and again after the port is claimed anew), soBACnetStack_LoopMSTP()does nothing until that tick. - When the packet-mode send callback is missing, a retry for a port without an engine is reported once in the log, by a flag shared with the check the retry loop makes when it starts.
How often to call BACnetStack_LoopMSTP
Section titled “How often to call BACnetStack_LoopMSTP”The engine reads octets and checks its timers only inside BACnetStack_LoopMSTP(), so the gap between calls adds to every MS/TP deadline. A node must
start its reply to a Poll For Master, or a token pass, within Tusage_timeout (35 ms) of the last octet it received, after the
Tturnaround wait (40 bit times: 4.17 ms at 9600 baud and 1.04 ms at 38400, which the engine rounds up to whole milliseconds, 5 ms and 2 ms). Call it at least every 20 ms at any baud rate from 9600 up, and every 1 to 5 ms
if you can. A slower host loses replies and token passes, and the node drops out of the token ring and rejoins. The host’s receive buffer must also
hold the octets that arrive between calls.
A reply to a confirmed request is sent at once, as BACnet Data Not Expecting Reply, when the stack produces it within half of Treply_delay (125 ms) of
the request. With a stack running on a thread other than the engine’s, that means BACnetStack_Tick() must run often enough to process the request and queue the
answer in that time. If it does not, the engine sends Reply Postponed and the answer goes out on the node’s next token.
Counters
Section titled “Counters”BACnetStack_GetMSTPCounters() fills an array, one entry per BACnetMSTPCounter value: frames heard, frames for this station, frames sent, frames
dropped, octets lost, receive errors, whether the node is in the token ring, the last frame’s type, source and destination, NPDUs delivered to the stack, the
two queue-full counts, and the number of frames the MS/TP data link discarded for a missing or wrong connection string (packet mode). It returns false for a NULL
array, an unknown port, a port that never had byte callbacks registered, and a build without MS/TP. It fills at most as many entries as countersLength says.
Turning MS/TP off
Section titled “Turning MS/TP off”Define STACK_OPTION_DISABLE_DATA_LINK_MSTP to leave MS/TP out of a build. The functions above are still exported, so the export list is the same
in every build; they return false or do nothing.
Packet mode
Section titled “Packet mode”A port with none of the byte callbacks registered is in packet mode: the stack sees MS/TP only as 1-octet addresses on a Network Port, and your code does the framing, the CRCs and the token passing. The stack provides no engine for this mode. Write your own, or use byte mode.
| Direction | Your engine | Stack side |
|---|---|---|
| Inbound | Delivers the frame data (the NPDU), the source MAC and the destination MAC. | Your BACnetStack_RegisterCallbackReceiveMessageForPort() callback returns that frame to the stack on the next BACnetStack_Tick(), with a 1-octet source and a 1-octet destination (255 for a broadcast). |
| Outbound | Queues the frame and sends it the next time it holds the token. | Your BACnetStack_RegisterCallbackSendMessageForPort() callback receives the frame and a 1-octet destination. A broadcast arrives either with no address and broadcast true, or as the destination 0xFF (255) with broadcast false; send both to MAC 0xFF. |
A frame that arrives without a source or destination connection string is discarded and counted. In this mode there is no
BACnetStack_LoopMSTP() thread, so by default make every BACnetStack_* call from the thread that calls BACnetStack_Tick()
(a build with STACK_OPTION_THREAD_SAFE lifts this; see Stack lifecycle).
