The following list contains main design goals in the order of importance.
uavcan.protocol.NodeStatus
and responds to uavcan.protocol.GetNodeInfo
should not require more than 4K of ROM.
For reference, a similar application based on libuavcan requires 19K of ROM (LPC11C24).According to the core design goals defined above, the functionality of the library should be restricted to the bare minimum. The following features are considered to comprise the bare minimum:
The following features are intentionally not supported by the library:
Entire state of the library should be kept in one instance of a C structure. Every API call of the library that depends on the state will be accepting the aforementioned instance as its first argument.
The library should implement a block memory allocator that will be used by the following subsystems (each is described below):
The number of blocks in the pool will be defined at compile time. 32 bytes is probably the optimal choice considering typical object sizes (see below). For reference, libuavcan uses 64-byte blocks.
Implementation of the block allocation algorithm can be borrowed from libuavcan.
Transfer buffers should be implemented as a singly-linked lists of blocks, where every block is an instance of the following structure:
typedef struct CanardBufferBlock
{
struct CanardBufferBlock* next;
uint8_t data[];
} CanardBufferBlock;
#define CANARD_BUFFER_BLOCK_DATA_SIZE (CANARD_MEM_BLOCK_SIZE - sizeof(CanardBufferBlock))
Where CANARD_MEM_BLOCK_SIZE
is the allocation size (32 bytes).
According to the transport layer specification, the following operations must be defined for buffers:
New blocks should be allocated ad hoc, however their removal should happen at once, after the data is processed upon completion of transfer reception.
The library will have to keep some state associated with every unique incoming transfer. The concept of unique transfer is explained in the specification here. Every unique incoming transfer can be identified with the following four values:
Upon reception of a CAN frame, the library will have to check whether the frame should be accepted and how it should be processed. Detailed description of the logic is available in the specification, here we just define a C structure that keeps all of the relevant states. Such a structure will have to be instantiated and maintained for every unique incoming transfer that the library is interested in. Since the number of unique incoming transfers cannot be determined statically, these structures will be allocated at run time using the memory pool. Hence, size of the structure must not exceed the size of the allocatable block (32 bytes).
typedef struct CanardRxState
{
struct CanardRxState* next;
CanardBufferBlock* buffer_blocks;
uint64_t timestamp_usec;
const uint32_t dtid_tt_snid_dnid;
uint16_t payload_crc;
uint16_t payload_len : 10;
uint16_t transfer_id : 5;
uint16_t next_toggle : 1;
uint8_t buffer_head[];
} CanardRxState;
#define CANARD_MULTIFRAME_RX_PAYLOAD_HEAD_SIZE (CANARD_MEM_BLOCK_SIZE - sizeof(CanardRxState))
Few things to note:
timestamp_usec
keeps the timestamp at which the transfer that is currently being received was started, i.e. when the first frame of it was received.
This value is needed for detection and removal of stale RX state objects, and it is also passed to the application as a transfer reception timestamp.Value of the field dtid_tt_snid_dnid can be computed with the following helper macro:
#define CANARD_MAKE_TRANSFER_DESCRIPTOR(data_type_id, transfer_type, \
src_node_id, dst_node_id) \
((data_type_id) | ((transfer_type) << 16) | \
((src_node_id) << 18) | ((dst_node_id) << 25))
Keeping the transfer ID in a single scalar rather than in separate fields should be beneficial in terms of ROM footprint and linked list search speed.
Using the concepts defined above, the frame reception procedure can be defined roughly as follows:
Frames that are scheduled for transmission should be stored in a prioritized TX queue. When the CAN driver is ready to accept a frame, the application will take the highest priority frame from the TX queue and pass it to the driver. The TX queue should be stored as a singly-linked list of CAN frames sorted in the descending order of priority (so that the highest priority frame is always at the root of the list). There’s an implementation of a function that compares priorities of two CAN frames according to the CAN specification.
#define CANARD_CAN_FRAME_MAX_DATA_LEN 8
/**
* CAN driver IO is based on this type.
*/
typedef struct
{
uint32_t id;
uint8_t data[UC_CAN_FRAME_MAX_DATA_LEN];
uint8_t data_len;
} CanardCANFrame;
#define CANARD_CAN_FRAME_EFF (1U << 31) ///< Extended frame format
#define CANARD_CAN_FRAME_RTR (1U << 30) ///< Remote transmission request
#define CANARD_CAN_FRAME_ERR (1U << 29) ///< Error frame
/**
* This type is internal to the library, it should not be exposed in the header file.
*/
typedef struct CanardTxQueueItem
{
struct CanardTxQueueItem* next;
CanardCANFrame frame;
} CanardTxQueueItem;
The library should be single-threaded, not thread-aware. Hence the API will be not thread-safe, which is OK as most applications will likely be running all of the UAVCAN-related logic in one thread.
The documentation should provide advices about how to integrate the library in a multithreaded environment.
The following list provides a high-level description of the major use cases:
The library should provide the following functions for the application:
The application should provide the following functions for the library (the application will bind the library to these functions dynamically by means of function pointers):
Note that the proposed API does not make any assumptions about the CAN driver interface or its implementation. A rough draft of the API definitions is provided below:
#define CANARD_BROADCAST_NODE_ID 0
#define CANARD_MIN_NODE_ID 1
#define CANARD_MAX_NODE_ID 127
#define CANARD_MEM_BLOCK_SIZE 32
typedef enum
{
CanardResponse,
CanardRequest
} CanardRequestResponse;
typedef enum
{
CanardTransferTypeResponse = 0,
CanardTransferTypeRequest = 1,
CanardTransferTypeBroadcast = 2
} CanardTransferType;
/**
* This structure represents a received transfer for the application.
* An instance of it is passed to the application via callback when
* the library receives a new transfer.
*/
typedef struct
{
/**
* Timestamp at which the first frame of this transfer was received.
*/
uint64_t timestamp_usec;
/**
* Payload is scattered across three storages:
* - Head points to CanardRxState.buffer_head (length of which is up to Canard_PAYLOAD_HEAD_SIZE), or to the
* payload field (possibly with offset) of the last received CAN frame.
* - Middle is located in the linked list of dynamic blocks.
* - Tail points to the payload field (possibly with offset) of the last received CAN frame
* (only for multi-frame transfers).
*
* The tail offset depends on how much data of the last frame was accommodated in the last allocated
* block.
*
* For single-frame transfers, middle and tail will be NULL, and the head will point at first byte
* of the payload of the CAN frame.
*
* In simple cases it should be possible to get data directly from the head and/or tail pointers.
* Otherwise it is advised to use canardDecodeScalar().
*/
const uint8_t* payload_head; ///< Always valid, i.e. not NULL.
const CanardBufferBlock* payload_middle; ///< May be NULL if the buffer was not needed. Always NULL for single-frame transfers.
const uint8_t* payload_tail; ///< Last bytes of multi-frame transfers. Always NULL for single-frame transfers.
uint16_t payload_len;
/**
* These fields identify the transfer for the application logic.
*/
uint16_t data_type_id; ///< 0 to 255 for services, 0 to 65535 for messages
uint8_t transfer_type; ///< See @ref CanardTransferType
uint8_t transfer_id; ///< 0 to 31
uint8_t priority; ///< 0 to 31
uint8_t source_node_id; ///< 1 to 127, or 0 if the source is anonymous
} CanardRxTransfer;
/**
* Definition is not provided in this draft.
* This structure should include at least the following:
* - Two callback function pointers
* - Local node ID
* - Memory pool and the associated state variables
* - List of RX state objects
* - An optional user-provided untyped pointer to user-specific data
*/
struct CanardInstance;
/**
* Initializes the library state.
* Local node ID will be set to zero, i.e. the node will be anonymous.
*/
void canardInit(CanardInstance* out_ins,
void* mem_arena,
size_t mem_arena_size,
CanardOnTransferReception on_reception,
CanardShouldAcceptTransfer should_accept,
void* user_reference);
/**
* Assigns a new node ID value to the current node.
*/
void canardSetLocalNodeID(CanardInstance* ins, uint8_t self_node_id);
/**
* Returns node ID of the local node.
* Returns zero if the node ID has not been set.
*/
uint8_t canardGetLocalNodeID(const CanardInstance* ins);
/**
* Sends a broadcast transfer.
* If the node is in passive mode, only single-frame transfers will be allowed.
*/
int canardBroadcast(CanardInstance* ins,
uint64_t data_type_signature,
uint16_t data_type_id,
uint8_t* inout_transfer_id,
uint8_t priority,
const void* payload,
uint16_t payload_len);
/**
* Sends a request or a response transfer.
* Fails if the node is in passive mode.
*/
int canardRequestOrRespond(CanardInstance* ins,
uint8_t destination_node_id,
uint64_t data_type_signature,
uint16_t data_type_id,
uint8_t* inout_transfer_id,
uint8_t priority,
CanardRequestResponse kind,
const void* payload,
uint16_t payload_len);
/**
* Returns a pointer to the top priority frame in the TX queue.
* Returns NULL if the TX queue is empty.
*/
const CanardCANFrame* canardPeekTxQueue(const CanardInstance* ins);
/**
* Removes the top priority frame from the TX queue.
*/
void canardPopTxQueue(CanardInstance* ins);
/**
* Processes a received CAN frame with a timestamp.
*/
void canardHandleRxFrame(CanardInstance* ins,
const CanardCANFrame* frame,
uint64_t timestamp_usec);
/**
* Traverses the list of transfers and removes those that were last updated more than
* timeout_usec microseconds ago.
*/
void canardCleanupStaleTransfers(CanardInstance* ins,
uint64_t timeout_usec,
uint64_t current_time_usec);
/**
* The library calls this function when it receives first frame of a transfer to determine whether
* the transfer should be received.
* If the application returns true, the pointer out_data_type_signature must be written with the data type signature.
*/
typedef bool (*CanardShouldAcceptTransfer)(const CanardInstance* ins,
uint64_t* out_data_type_signature,
uint16_t data_type_id,
CanardTransferType transfer_type,
uint8_t source_node_id);
/**
* This function will be invoked by the library every time a transfer is successfully received.
* If the application needs to send another transfer from this callback, it is recommended
* to call canardReleaseRxTransferPayload() first, so that the memory that was used for the block
* buffer can be released and re-used by the TX queue.
*/
typedef void (*CanardOnTransferReception)(CanardInstance* ins,
const CanardRxTransfer* transfer);
/**
* This function can be used to extract values from received UAVCAN transfers. It decodes a scalar value -
* boolean, integer, character, or floating point - from the specified bit position in the RX transfer buffer.
* Simple single-frame transfers can also be parsed manually.
*
* Returns the number of bits successfully decoded, which may be less than requested if operation ran out of
* buffer boundaries, or negated error code, such as invalid argument.
*
* Caveat: This function works correctly only on platforms that use two's complement signed integer representation.
* I am not aware of any modern microarchitecture that uses anything else than two's complement, so it should
* not affect portability in any way.
*
* The type of value pointed to by 'out_value' is defined as follows:
*
* | bit_length | value_is_signed | out_value points to |
* |------------|-----------------|------------------------------------------|
* | 1 | false | bool (may be incompatible with uint8_t!) |
* | 1 | true | N/A |
* | [2, 8] | false | uint8_t, or char |
* | [2, 8] | true | int8_t, or char |
* | [9, 16] | false | uint16_t |
* | [9, 16] | true | int16_t |
* | [17, 32] | false | uint32_t |
* | [17, 32] | true | int32_t, or 32-bit float |
* | [33, 64] | false | uint64_t |
* | [33, 64] | true | int64_t, or 64-bit float |
*/
int canardDecodeScalar(const CanardRxTransfer* transfer, ///< The RX transfer where the data will be copied from
uint32_t bit_offset, ///< Offset, in bits, from the beginning of the transfer
uint8_t bit_length, ///< Length of the value, in bits; see the table
bool value_is_signed, ///< True if the value can be negative; see the table
void* out_value); ///< Pointer to the output storage; see the table
/**
* This function can be used to encode values for later transmission in a UAVCAN transfer. It encodes a scalar value -
* boolean, integer, character, or floating point - and puts it to the specified bit position in the specified
* contiguous buffer.
* Simple single-frame transfers can also be encoded manually.
*
* Caveat: This function works correctly only on platforms that use two's complement signed integer representation.
* I am not aware of any modern microarchitecture that uses anything else than two's complement, so it should
* not affect portability in any way.
*
* The type of value pointed to by 'value' is defined as follows:
*
* | bit_length | value points to |
* |------------|------------------------------------------|
* | 1 | bool (may be incompatible with uint8_t!) |
* | [2, 8] | uint8_t, int8_t, or char |
* | [9, 16] | uint16_t, int16_t |
* | [17, 32] | uint32_t, int32_t, or 32-bit float |
* | [33, 64] | uint64_t, int64_t, or 64-bit float |
*/
void canardEncodeScalar(void* destination, ///< Destination buffer where the result will be stored
uint32_t bit_offset, ///< Offset, in bits, from the beginning of the destination buffer
uint8_t bit_length, ///< Length of the value, in bits; see the table
const void* value); ///< Pointer to the value; see the table
/**
* This function can be invoked by the application to release pool blocks that are used to store
* the payload of this transfer.
*/
void canardReleaseRxTransferPayload(CanardInstance* ins,
CanardRxTransfer* transfer);
As noted above, this shall remain an optional part of the library, as some applications may opt to implement serialization and deserialization manually. The DSDL compiler should generate a tiny standalone single-header library for every data type the user needs to use in deserialized form. The generated library should include the following entities:
The primitive DSDL types will be mapped to C types as follows:
DSDL type | C type |
---|---|
intX |
int8_t, int16_t, int32_t, int64_t |
uintX |
uint8_t, uint16_t, uint32_t, uint64_t |
bool |
bool (from stdbool.h) |
float16 |
float |
float32 |
float |
float64 |
double |
A dynamic DSDL array that contains values of type T and has maximum length N will be converted into two fields: one of them will be of type T mapped to the corresponding native C type as defined above; the second field will be of integer type uint16_t. The first field will be given the same name as the original array, the second field’s name will be appended with “_len”. An example is provided below.
DSDL definition | C definition |
---|---|
Foo[<5] bars |
Foo bars[4]; uint16_t bars_len; |
The suggested approach is to keep all of the library’s functions in just one C file, and expose the entire API via just one header file.
Please refer to the Zubax coding conventions.
The library should be equipped with a testing suite (like libuavcan). The testing suite should be based on the Google Test library (in which case it can be written in C++), or it can be just a dedicated application with a custom testing environment (in which case it is recommended to stick to C99).
The testing suite does not have to be portable - it is quite acceptable to make it require an x86 or AMD64 machine running OS X or Linux. Since 64-bit systems are not supported by the proposed design, on AMD64 systems the library should be compiled in 32-bit mode (on GCC use flag -m32).
A continuous integration environment like Travis CI should be set up early in the project to run the test suite on each commit / pull request.
It has been agreed to name the library “libcanard”.
The library must be released under the MIT open source license. A short summary can be found on tl;dr legal. The license statement must declare that the code was developed by the UAVCAN project team.