Libcanard
A compact implementation of the Cyphal/CAN protocol in C for high-integrity real-time embedded systems
Install / Use
/learn @OpenCyphal/LibcanardREADME
Cyphal/CAN transport in C
</div>Libcanard is a robust implementation of the Cyphal/CAN transport layer in C for high-integrity real-time embedded systems.
Cyphal is an open lightweight data bus standard designed for reliable intravehicular communication in aerospace and robotic applications via CAN bus, Ethernet, and other robust transports.
Read the docs in libcanard/canard.h.
Find examples, starters, tutorials on the Cyphal forum.
If you want to contribute, please read CONTRIBUTING.md.
Features
- Full test coverage and extensive static analysis.
- Compliance with automatically enforceable MISRA C rules (reach out to https://forum.opencyphal.org for details).
- Detailed time complexity and memory requirement models for the benefit of real-time high-integrity applications.
- Purely reactive API without the need for background servicing.
- Support for the Classic CAN and CAN FD.
- Support for redundant network interfaces.
- Compatibility with 8/16/32/64-bit platforms.
- Compatibility with extremely resource-constrained baremetal environments starting from 32K ROM and 32K RAM.
- Implemented in ≈1000 SLoC.
Platforms
The library is designed to be usable out of the box with any conventional 8/16/32/64-bit platform, including deeply embedded baremetal platforms, as long as there is a standard-compliant compiler available. The platform-specific media IO layer (driver) is supposed to be provided by the application:
+---------------------------------+
| Application |
+-------+-----------------+-------+
| |
+-------+-------+ +-------+-------+
| Libcanard | | CAN driver |
+---------------+ +-------+-------+
|
+-------+-------+
| CAN controller|
+---------------+
The OpenCyphal Development Team maintains a collection of various platform-specific components in a separate repository at https://github.com/OpenCyphal/platform_specific_components. Users are encouraged to search through that repository for drivers, examples, and other pieces that may be reused in the target application to speed up the design of the media IO layer (driver) for the application.
Example
The example augments the documentation but does not replace it.
The library requires a constant-complexity deterministic dynamic memory allocator. We could use the standard C heap, but most implementations are not constant-complexity, so let's suppose that we're using O1Heap instead. We are going to need basic wrappers:
static void* memAllocate(void* const user_reference, const size_t size)
{
(void) user_reference;
return o1heapAllocate(my_allocator, size);
}
static void memFree(void* const user_reference, const size_t size, void* const pointer)
{
(void) user_reference;
(void) size;
o1heapFree(my_allocator, pointer);
}
Init a library instance:
const struct CanardMemoryResource memory = {NULL, memFree, memAllocate};
struct CanardInstance canard = canardInit(memory);
canard.node_id = 42; // Defaults to anonymous; can be set up later at any point.
In order to be able to send transfers over the network, we will need one transmission queue per redundant CAN interface:
const struct CanardMemoryResource tx_memory = {NULL, memFree, memAllocate};
struct CanardTxQueue queue = canardTxInit(
100, // Limit the size of the queue at 100 frames.
CANARD_MTU_CAN_FD, // Set MTU = 64 bytes. There is also CANARD_MTU_CAN_CLASSIC.
tx_memory);
Publish a message (message serialization not shown):
static uint8_t my_message_transfer_id; // Must be static or heap-allocated to retain state between calls.
const struct CanardTransferMetadata transfer_metadata = {
.priority = CanardPriorityNominal,
.transfer_kind = CanardTransferKindMessage,
.port_id = 1234, // This is the subject-ID.
.remote_node_id = CANARD_NODE_ID_UNSET, // Messages cannot be unicast, so use UNSET.
.transfer_id = my_message_transfer_id,
};
++my_message_transfer_id; // The transfer-ID shall be incremented after every transmission on this subject.
int32_t result = canardTxPush(&queue, // Call this once per redundant CAN interface (queue).
&canard,
tx_deadline_usec, // Transmission deadline (absolute monotonic time in usec).
&transfer_metadata,
47, // Size of the message payload (see Nunavut transpiler).
"\x2D\x00" "Sancho, it strikes me thou art in great fear.",
NULL);
if (result < 0)
{
// An error has occurred: either an argument is invalid, the TX queue is full, or we've run out of memory.
// It is possible to statically prove that an out-of-memory will never occur for a given application if the
// heap is sized correctly; for background, refer to the Robson's Proof and the documentation for O1Heap.
}
Use Nunavut to automatically generate (de)serialization code from DSDL definitions.
The CAN frames generated from the message transfer are now stored in the queue.
We need to pick them out one by one and have them transmitted.
Normally, the following fragment should be invoked periodically to unload the CAN frames from the
prioritized transmission queue (or several, if redundant network interfaces are used) into the CAN driver:
for (struct CanardTxQueueItem* ti = NULL; (ti = canardTxPeek(&queue)) != NULL;) // Peek at the top of the queue.
{
if ((0U == ti->tx_deadline_usec) || (ti->tx_deadline_usec > getCurrentMicroseconds())) // Check the deadline.
{
if (!pleaseTransmit(ti)) // Send the frame over this redundant CAN iface.
{
break; // If the driver is busy, break and retry later.
}
}
// After the frame is transmitted or if it has timed out while waiting, pop it from the queue and deallocate:
canardTxFree(&queue, &canard, canardTxPop(&queue, ti));
}
💡 New in v4.0: optionally, you can now use canardTxPoll() that automates the above for you.
Transfer reception is done by feeding frames into the transfer reassembly state machine from any of the redundant interfaces. But first, we need to subscribe:
struct CanardRxSubscription heartbeat_subscription;
(void) canardRxSubscribe(&canard, // Subscribe to messages uavcan.node.Heartbeat.
CanardTransferKindMessage,
7509, // The fixed Subject-ID of the Heartbeat message type (see DSDL definition).
16, // The extent (the maximum possible payload size) provided by Nunavut.
CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC,
&heartbeat_subscription);
struct CanardRxSubscription my_service_subscription;
(void) canardRxSubscribe(&canard, // Subscribe to an arbitrary service response.
CanardTransferKindResponse, // Specify that we want service responses, not requests.
123, // The Service-ID whose responses we will receive.
1024, // The extent (see above).
CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC,
&my_service_subscription);
The "extent" refers to the minimum amount of memory required to hold any serialized representation of any compatible
version of the data type; or, in other words, it is the maximum possible size of received objects.
This parameter is determined by the data type author at the data type definition time.
It is typically larger than the maximum object size in order to allow the data type author to introduce more
fields in the future versions of the type;
for example, MyMessage.1.0 may have the maximum size of 100 bytes and the extent 200 bytes;
a revised version MyMessage.1.1 may have the maximum size anywhere between 0 and 200 bytes.
Extent values are provided per data type by DSDL transcompilers such as Nunavut.
In Libcanard we use the term "subscription" not only for subjects (messages), but also for RPC services.
We can subscribe and unsubscribe at runtime as many times as we want. Normally, however, an embedded application would subscribe once and roll with it. Okay, this is how we receive transfers:
struct CanardRxTransfer transfer;
const int8_t result = canardRxAccept(&canard,
rx_timestamp_usec, // When the frame was received, in microseconds.
