Skip to content
Version 6.1.0
Contact Us

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.

An RS-485 MS/TP bus with stations at MAC 1, 5, 12 and 20 passing the token in turn, from MAC 20 back to MAC 1. MAC 5 is this device: inside the CAS BACnet Stack, the application and network layers exchange NPDUs through queues with the MS/TP engine, one per port. The engine reaches your UART and RS-485 driver through the byte callbacks you register (receive bytes, send bytes, microseconds and an optional transmit enable). Your main loop calls BACnetStack_Tick() for the stack and BACnetStack_LoopMSTP() for the engine, every 1 to 5 ms and at least every 20 ms, which may run on its own thread.

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.

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.

  • With more than one data link compiled in (BACnet/IP and MS/TP), BACNET_NETWORK_PORT_DEFAULT does not name the MS/TP port: a BACnetStack_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), so BACnetStack_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.

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.

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.

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.

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).