Run on a microcontroller
The portable crates run on a microcontroller with no operating system and no heap. Each one is a sans-IO core: it reads no clock and owns no UART or socket. Your firmware feeds it what the line does, stamped with the time, and carries out the actions it hands back. This page says which crates a product needs, how they share time, how much memory they take, and what the adapter between them and your hardware must do.
This page describes v0.15.0. The firmware images, the device image among them, live in the repository, not on crates.io. Nothing here is a hardware qualification: the images are measured and checked natively, and run under emulation by hand.
Crates by product
Section titled “Crates by product”Every crate below is no_std and needs no allocator. The cores re-export what their API speaks, so you can often depend on one crate: bacnet-mstp-core re-exports its codec as codec and bacnet-npdu as npdu, and bacnet-router-core always re-exports npdu, and bip_core and mstp_core with its bip and mstp features. Depend on the application crates with default-features = false to get their allocation-free profile.
| Product | Data link | Network layer | Application layer | Firmware image that measures it |
|---|---|---|---|---|
| MS/TP device | bacnet-mstp-core (with bacnet-mstp-codec) |
bacnet-npdu |
bacnet-types, bacnet-encoding and bacnet-services, no default features |
device |
| B/IP device | bacnet-bip-core’s NodePort (with bacnet-bip-codec) |
bacnet-npdu |
the same three crates | node, for the link alone |
| B/IP BBMD | bacnet-bip-core’s BbmdPort and Tables |
bacnet-npdu |
the same three crates, when it hosts a Device object | bbmd, for the link alone |
| Router between B/IP and MS/TP | bacnet-router-core with bip and mstp |
bacnet-router-core |
the same three crates, for its Device object | router, for routing alone |
A router is also a device: Clause 6.6 of the standard holds it to the device requirements of Clause 22.1.5, so a product router adds a Device object and the ReadProperty service to the routing the router image measures. B/IPv6, BACnet/SC and Ethernet stay hosted, on the standard library.
The device image in firmware/link is the reference for an MS/TP device: it answers Who-Is with an I-Am and ReadProperty for a Device object and an Analog Input, with the Error, Reject or Abort each refused request needs. Its source, firmware/link/src/device/app.rs, is the application part to copy. It is a starting point, not a conformant Device as it stands: it lacks Property_List and other properties Clause 12.11 requires of a Device object, and its Vendor_Identifier of 0 is a placeholder. A product fills these in with its own.
One clock for every core
Section titled “One clock for every core”All the cores take bacnet-time’s Instant and Duration: nanoseconds on a 64-bit unsigned integer, from an epoch you choose. That is about 584 years of range. Build an Instant from your timer with Instant::from_micros or Instant::from_millis.
- Feed one clock to every core. Read the timer once per event and pass that
nowto each call. The cores compare deadlines across calls, so two clocks that drift apart confuse them. - Time never runs backwards. A
nowearlier than the latest one a core has seen is taken as that latest one, so a clock that steps back holds the timers still instead of failing. - Merge deadlines with
bacnet_time::earliest. Each core reports its next deadline as an optionalInstant,Nonewhen nothing is due.earliestskips theNones.Option::mindoes not: it picksNone, and a timer merged that way never fires. - Sleep until the merged deadline or the next received octet, then call
on_deadlineon each core whose deadline came, and poll its actions. - No 64-bit division. The cores and
bacnet-timeavoid it, so a 32-bit part doesn’t link the roughly one-kilobyte division routine. - On a host, the
stdfeature ofbacnet-timeaddsHostClock, which maps the standard library’s monotonic clock onto the same timeline.
Storage sizing
Section titled “Storage sizing”Each core’s large buffers are yours, and a const fn builds them from zeros, so firmware keeps them in a static in the zero-initialized section, .bss, costing no flash. Build the core itself on the stack over that static; it borrows the buffers and is a few hundred bytes. For MS/TP, one StandardStorage bundle holds the receive buffer and the queue and delivery slots, their counts given as its three type parameters; ExtendedStorage is the same, sized for extended frames.
CI links each firmware image for four targets and checks it against a budget in firmware/link/size-budget.toml. These sizes were measured for the Cortex-M0+ target, thumbv6m-none-eabi, with the size-optimized firmware profile. Flash is code, read-only data and the vector table. Static RAM is initialized and zeroed data. The stack column is the budget in firmware/link/stack-budget.toml for the BBC micro:bit board under emulation (the router’s is the LM3S6965 board’s, since its statics don’t leave a 16-kibibyte part room for its stack); each budget is the deepest stack measured plus a quarter. Each image links a test scenario as well as its stack, so compare images with care.
| Image | What it links | Flash (bytes) | Static RAM (bytes) | Stack budget (bytes) |
|---|---|---|---|---|
mstp |
MS/TP codec and master core, standard frames | 18,388 | 5,528 | 2,560 |
mstp-extended |
the same with extended frames | 17,220 | 7,696 | 3,776 |
node |
B/IP foreign-device node and its port | 16,164 | 240 | 6,816 |
bbmd |
a BBMD’s port and tables | 13,284 | 240 | 2,976 |
device |
a whole MS/TP device with Who-Is and ReadProperty | 30,912 | 3,496 | 3,328 |
router |
a B/IP-to-MS/TP router whose B/IP port is the BBMD | 45,900 | 15,632 | 5,584 |
What drives the RAM:
- An MS/TP standard frame carries up to 501 bytes of NPDU. The
deviceimage’s 3,496 bytes of static RAM are mostly its storage bundle: one unicast slot, two broadcast slots and one delivery slot, about 3.3 kilobytes, plus 88 bytes of application scratch and an 88-byte buffer for the answer’s NPDU. - An extended frame carries up to 1,497 bytes. Clause 9.3 asks them of a router’s MS/TP port, and their buffers are most of the router’s 15.6 kilobytes of static RAM. A device needs them only to take NPDUs longer than 501 bytes.
- Each more queue slot costs about one NPDU: 501 bytes for standard frames, 1,497 for extended.
- The
deviceimage links no formatting code (core::fmt), no 64-bit division and nomemmove; the check counts each and fails if one appears.
Adapter rules
Section titled “Adapter rules”The adapter is the code between a core and your hardware. The rules below keep the cores honest about the line; the crate documentation of bacnet-mstp-core and bacnet-bip-core gives them in full.
- Poll after every call. After each call into a core, poll its actions until it reports idle, and carry each one out before the next event.
- A frame’s octets come with its start. An MS/TP
Transmitaction names a frame. Right before you enable the line driver, callon_transmit_started: it returns the octets only if it confirms the start, so the line is never driven for a frame the core took back. A start within the turnaround time of line activity is refused and the frame comes back later as a new action. Callon_transmit_completewhen the last stop bit has left. - Echo begins at the confirmed start. Until then keep the driver off and the receiver on, and feed the core every octet and receive error, draining the receive buffer right before the start call. What you hear between the start and the completion is your own echo, which the core ignores.
- Inputs can’t fail. The
on_*calls that report the line never return an error. An input that breaks the contract is ignored and counted in the core’s adapter-error counter; watch it in testing. Only building a core, a transmit start and a request’s answer return errors. - Answer a request in its reply window. A data-expecting-reply frame comes with a request id. Hand the core your answer with
complete_request; if the answer isn’t ready, the core sends Reply Postponed for you, and if there is none, abandon the request so the core stops waiting. - Report where each datagram went. A B/IP adapter tells the core, for each datagram it reads, whether it came to this node alone or to the subnet’s broadcast address. An IP stack that can’t tell passes
None, and the core drops the datagram rather than guess. - Counters drain.
take_countersreturns 32-bit counts since the last take and starts them again from zero. Add each take to totals of your own, as wide as you need.
Two host-runnable examples follow these rules and compile in CI: crates/bacnet-mstp-core/examples/node.rs, two master nodes on a simulated line, and crates/bacnet-router-core/examples/bip_mstp_router.rs, a router whose B/IP port is the BBMD, in the main loop firmware runs.
cargo run -p bacnet-mstp-core --example nodecargo run -p bacnet-router-core --features bip,mstp --example bip_mstp_routerWhat still needs an allocator
Section titled “What still needs an allocator”Without alloc, the application crates have the enums, the object identifier, the error type, tag and primitive encoding and decoding, borrowed views of strings and application-tagged values, the APDU header (with Error, Reject and Abort), and Who-Is, I-Am and ReadProperty, request and acknowledgement. Everything else in them needs alloc and the bytes crate:
- the other services, such as WriteProperty, ReadPropertyMultiple and change-of-value subscriptions;
- the owned APDU and NPDU types, and segmentation;
- owned property values (
PropertyValue) and the constructed types; - every encoder that writes into a growable
BytesMutbuffer.
bacnet-objects, with its object database, builds without the standard library too, given an allocator and a target with atomic compare-and-swap (so not ARMv6-M): depend on it with default-features = false, features = ["alloc"]. Each save then runs on the caller’s thread, and the application supplies the storage; the file backends need the standard library. The Tokio client, server and transports need the standard library. Firmware that wants the application crates builds them with alloc and supplies a global allocator; the apdu firmware image does that with a one-kilobyte heap.
Sources
Section titled “Sources”firmware/link (the images, their scenarios and budgets) · Rust API notes · CI’s firmware link test.
Release v0.15.0 ·Build and integrate. Pre-1.0 APIs; partial conformance.Support & limitations