modm API documentation
|
Classes | |
class | modm::amnb::Node< TxBufferSize, MaxHeapAllocation > |
Macros | |
#define | MODM_AMNB_HANDLER_STORAGE sizeof(void*) |
Enums | |
enum | modm::amnb::Type : uint8_t { modm::amnb::Type::Broadcast = 0b000 << 5, modm::amnb::Type::Request = 0b010 << 5, modm::amnb::Type::Response = 0b011 << 5, modm::amnb::Type::Error = 0b100 << 5, modm::amnb::Type::UserError = 0b101 << 5 } |
Functions | |
modm::IOStream & | modm::operator<< (modm::IOStream &s, const modm::amnb::Error error) |
modm::IOStream & | modm::operator<< (modm::IOStream &s, const modm::amnb::InterfaceStatus status) |
class modm_packed | modm::amnb::modm_aligned (4) Message |
modm::IOStream & | modm::operator<< (modm::IOStream &s, const modm::amnb::Type type) |
enum | Error : uint8_t { Ok = 0, RequestTimeout = 1, WrongArgumentSize = 2, NoAction = 3, ResponseAllocationFailed = 4, RequestAllocationFailed = 5, UserError = 6, Unknown = 7 } |
template<typename T > | |
Response | modm::amnb::ErrorResponse (T error) |
enum | InterfaceStatus : uint8_t { Ok = 0, HeaderInvalid, DataInvalid, MediumBusy, MediumEmpty, SyncReadFailed, HeaderReadFailed, DataReadFailed, AllocationFailed, SyncWriteFailed, HeaderWriteFailed, DataWriteFailed } |
lbuild module: modm:communication:amnb
The AMNB (Asynchronous Multi-Node Bus) is a RPC communication system over a shared half-duplex UART bus. It is intended as a low-cost replacement for higher level protocols like CAN or Ethernet, which are not available in every device unlike UART.
A node can provide two communication services, each with an 8-bit ID:
Messages support up to 28 bytes of in-place data, and up to 8kB of heap data and are protected with 8-bit and 16-bit CCIT CRC sums. The receiving node can refuse to allocate any data, which will discard messages with >28B payload, or define a dynamic allocation threshold (for example <=1kB) above which the message will also be refused. This allows devices with very little RAM resources to share the same bus and same protocol as larger devices.
Nodes are identified with a unique 8-bit address. The protocol does not define a central node, so any node can initiate communication as arbitration is provided via a CSMA/CD mechanism, with the retransmission count and backoff time controlled by the service ID, with higher IDs having higher priority.
Data serialization is done by copying their memory representation and are described as C/C++ types which must be available for both the sender and receiver, for example, in the form of shared header files. The types must be trivially constructable, since no constructor (or destructor) is called.
The protocol requires an open-drain + pullup config on the UART TX pin and an input + pullup config on the RX pin. Additionally, a loopback from TX to RX is required so that collisions can be detected.
There are three bus configurations:
Some UART peripherals have a built-in half-duplex mode, where the TX/RX pins are connected internally. The bus is just one pin now with an external pullup or if you use internal pullups with a slow baudrate, zero external components.
Alternately you can connect all TX pins and RX pins together in two separate lines and use a diode from RX ->- TX to create the loopback with internal or external pullups:
And lastly you can use a differential transceiver with automatic direction control (like a CAN transceiver) to connect nodes that are further away. Note that you can still connect local nodes before the transceiver (but without the diode now):
Note that small glitches on the bus can be interpreted as a UART start condition and will trigger a byte reception. In order to recognize this, the UART must be configured as 8 data bits and 1 even parity bit (9E1)!
A list of the types modm::amnb::Listener
and modm::amnb::Action
store the callbacks for the IDs you want to subscribe to. The callbacks store a closure up to size MODM_AMNB_HANDLER_STORAGE
, which can be overwritten to increase storage. The argument type is deduced from the callback's signature and it must match the message size otherwise the message is discarded.
All listeners are given the sender ID as first argument and return nothing. The second argument is optional and may be a type passed as const reference or a const data buffer:
Actions don't know the sender, and are passed either nothing, a const argument reference or a const data buffer. They can return a modm::amnb::Response
with an optional return type, or a modm::amnb::ErrorResponse
with an optional user error type. Not returning anything still sends an implicit (good) Response.
The node class manages the whole stack via its update()
function which must be called in the main loop continuously.
To broadcast, the node.broadcast(id, args)
function places the message in the transmit queue, the size of which you can control. It returns false if the queue is full.
Requests node.request<ReturnType=void>(id, args)
must be made from within a fiber, since they can take some time. The response is a modm::amnb::Result
object, which contains the result, system error and user error.
Note that if a request returns a user error type, you must define it as node.request<ReturnType, UserErrorType>(id, args)
and then explicitly check for it via response.hasUserError()
. Note that the request itself may fail at a system level, for example sending a request with a payload too large for the receiver will return with a modm::amnb::Error::RequestAllocationFailed
error.
node.update()
, therefore you must copy the result into your own storage!node.update()
must be called during requests to service the protocol and node.request(...)
won't call it, creating a deadlock!There are three message formats:
The header contains these common fields:
SYNC
: Synchronization sequenceCRC8
: checksum for the headerADDR
: Node address (sender or receiver)COMMAND
: Command code (listener/action ID)TYPE
: Message type (upper 3 bits)Type | Meaning |
---|---|
000 | Broadcast |
010 | Request |
011 | Response |
100 | System Error |
101 | User Error |
Note that there are two sync bytes prefixed to every messages, since the UART hardware in most devices does not signal when it is receiving, therefore collision avoidance it not possible for the first byte, only collision detection and only after the first byte has been sent. The second byte acts as a "detection buffer" in case another node starts sending during the first byte.
Messages with <=28B data:
CRC8
: checksum for the header and dataTYPE/LENGTH
: Message type (upper 3 bits) and length (lower 5 bits) <= 28Messages with <8kB data:
LENGTH
: >28 and <8kCRC16
: checksum for the dataDATA
: Up to 8kB payload (nodes may support much less though!)Generated with: yes in [yes, no]