Libcanard v1 STM32 driver design guidelines

At Wednesday’s call, @lorenz.meier suggested that I prepare a design guideline for the low-level STM32 driver for Libcanard and Libuavcan. I decided to postpone the latter because Libuavcan is not yet finished and the low-level API may end up being slightly modified after we update it to support the UDP transport.

The guideline is organized as a header file definition documenting its API. It is based on the original v0 driver for Libcanard but with modifications to support redundant CAN bus interfaces.

The driver is not dependent on Libcanard to enable its reuse with other implementations in the future. To ensure that the driver is portable across different OS, it is best to include the following MIT-licensed bxCAN register definitions with the implementation:

_bxcan.h (12.7 KB)

The driver should be located under /stm32/bxcan/libcanard/.

FYI @scottdixon

The header:

///                         __   __   _______   __   __   _______   _______   __   __
///                        |  | |  | /   _   ` |  | |  | /   ____| /   _   ` |  ` |  |
///                        |  | |  | |  |_|  | |  | |  | |  |      |  |_|  | |   `|  |
///                        |  |_|  | |   _   | `  `_/  / |  |____  |   _   | |  |`   |
///                        `_______/ |__| |__|  `_____/  `_______| |__| |__| |__| `__|
///                            |      |            |         |      |         |
///                        ----o------o------------o---------o------o---------o-------
///
/// A generic low-level driver for the STM32 bxCAN macrocell. Designed to support any operating system and baremetal
/// systems. This driver is not compatible with FDCAN controllers available in newer STM32 MCUs such as STM32H7.
/// The driver has no external dependencies besides a tiny subset of the C standard library.
///
/// The driver does not use interrupts or critical sections internally, which makes it a good fit for certain hard
/// real-time systems where interrupts may be impossible to leverage (e.g., some motor control applications).
/// The application shall poll the RX queue with a sufficient frequency to prevent RX overrun. Given that the hardware
/// RX FIFO is three frames deep, the polling interval shall not exceed (shortest frame transmission time) x3.
/// At 1 Mbit/s for CAN 2.0B, this amounts to 192 microseconds. The application may be able to slightly extend
/// the interval by configuring the acceptance filters appropriately.
///
/// If the application is unable to poll the driver with a sufficient frequency, it is possible to enable the
/// interrupts and invoke the driver from there. It is safe since the driver is context-agnostic. Same holds for
/// the transmission management: it can be done by polling or from an interrupt depending on the timing requirements.
/// If interrupts are used, it is the responsibility of the caller to ensure adequate synchronization.
///
/// The driver automatically aborts all pending transmissions if the CAN controller enters the bus-off state.
/// This behavior is introduced to support the plug-and-play node-ID allocation protocol defined by UAVCAN.
///
/// The driver does not support TX timestamping. This feature is only required for time synchronization masters.
/// The suggested alternative is to implement the required logic at the driver layer directly, as is often done
/// with some other precise time protocols such as IEEE 1588.
///
/// The clocks of the CAN peripheral shall be enabled by the application before the driver is initialized.
/// The driver cannot do that because this logic is not uniform across the STM32 family.
///
/// The recommended approach for integration is to copy-paste the source files into the project tree and modify
/// as necessary. The .c file does not require any specific build options.
///
/// This software is distributed under the terms of the MIT License. Copyright (c) 2020 UAVCAN Development Team.

#pragma once

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C"
{
#endif

/// Trigger an assertion failure if an inner priority inversion is detected at runtime.
/// This setting has no effect if NDEBUG is defined (i.e., assertion checks disabled).
#if !defined(BXCAN_CONFIG_DEBUG_INNER_PRIORITY_INVERSION)
# define BXCAN_CONFIG_DEBUG_INNER_PRIORITY_INVERSION true
#endif

/// This is defined by the bxCAN hardware.
/// Devices with only one CAN interface have 14 filters (e.g. F103).
/// Devices with two CAN interfaces (e.g. F105, F446) have 28 filters, which are shared equally.
#define BXCAN_NUM_ACCEPTANCE_FILTERS             14U

/// A CAN acceptance filter configuration. Usable only with extended CAN 2.0B data frames.
/// It is not possible to configure acceptance of standard-ID or RTR frames.
/// The bits above 29-th shall be zero. Special case: if both mask and ID are zero, the filter will reject all frames.
typedef struct
{
    uint32_t extended_id;
    uint32_t extended_mask;
} BxCANFilterParams;

/// Bit timing parameters. Use bxCANComputeTimings() to derive these from the desired bus data rate.
/// Some applications may prefer to store pre-computed parameters instead of calculating them at runtime.
typedef struct
{
    uint16_t bit_rate_prescaler;        /// [1, 1024]
    uint8_t  bit_segment_1;             /// [1, 16]
    uint8_t  bit_segment_2;             /// [1, 8]
    uint8_t  max_resync_jump_width;     /// [1, 4] (recommended value is 1)
} BxCANTimings;

/// Initialization can be performed multiple times to switch between operating modes and/or bit rates.
/// For example, during the automatic bit-rate detection phase, the application may cycle through several
/// pre-configured bit rates (typically 1000/500/250/125 kbps) in silent mode until a match is found.
/// Returns true on success, false if initialization failed (INAK timeout).
bool bxCANConfigure(const uint8_t      iface_index,
                    const BxCANTimings timings,
                    const bool         silent);

/// Acceptance filter configuration. Unused filters shall be set to {0, 0} (all bits zero); they will reject all frames.
/// When the interface is reinitialized, hardware acceptance filters are reset, so this function shall be re-invoked.
/// While reconfiguration is in progress, some received frames may be lost.
/// Filters alternate between FIFO0/1 in order to equalize the load: even filters take FIFO0, odd filters take FIFO1.
/// This will cause occasional priority inversion and frame reordering on reception, but that is acceptable for UAVCAN,
/// and most other CAN-based protocols will tolerate this too since there will be no reordering within the same CAN ID.
void bxCANConfigureFilters(const uint8_t           iface_index,
                           const BxCANFilterParams params[BXCAN_NUM_ACCEPTANCE_FILTERS]);

/// This function is intended for error statistics tracking. The read is destructive.
/// Returns true if at least one of the following events took place since the previous invocation:
///     - TX frame aborted on timeout (deadline expiration).
///     - RX FIFO overrun.
///     - Bus error reported by the CAN controller.
bool bxCANReapError(const uint8_t iface_index);

/// Schedule a CAN frame for transmission.
///
/// The function takes the current time and the time when the scheduled frame will have to be aborted.
/// The time units may be arbitrary as long as it is guaranteed that the values will never overflow (typ. microseconds).
/// When looking for a free transmission slot, the function will ensure that none of the currently pending TX mailboxes
/// contain frames that have timed out (i.e., have not been transmitted before the deadline). If such mailboxes are
/// found they are aborted. This is done BEFORE the new frame is scheduled in order to free up expired mailboxes for
/// the new frame.
///
/// The return value is true if the frame is accepted for transmission, false otherwise. Acceptance logic:
///     - If all mailboxes are free, the frame is accepted.
///     - If all mailboxes are pending (busy), the frame is rejected.
///     - If at least one mailbox is pending, the frame is accepted only if its CAN ID value is smaller than that of
///       all pending mailboxes. This is done to avoid priority inversion; see the UAVCAN Specification for details.
///     - If current_time > deadline, the frame is discarded, TX timeout error is registered, and true is returned.
///
/// This interface does not support the transmission of CAN 2.0A (standard-ID) frames.
///
/// Redundantly interfaced configurations shall maintain a backlog queue to account for the fact that different
/// interfaces may be unable to transmit frames simultaneously due to different bus load (in terms of frames per
/// second). It is assumed that the outgoing frames are kept in a single prioritized transmission queue, which has
/// to be split into two queues to support different bus loads per interface. One approach is to implement naive
/// copying into two queues, but this approach leads to increased memory utilization. A more conservative approach
/// is to keep a unified queue keeping the outgoing frames for both interfaces, and a single backlog queue keeping
/// the frames for the interface that is lagging behind. This way, no memory overutilization will occur. One backlog
/// queue is sufficient since the faster interface will be fed directly from the shared queue. Graphically:
///
///     [FRAME SOURCE] ---> [ SHARED QUEUE ] --------------------------> [FAST INTERFACE]
///                                           \
///                                            ---> [BACKLOG QUEUE] ---> [SLOW INTERFACE]
///
/// Where "FRAME SOURCE" is typically a UAVCAN/CAN implementation library (like Libcanard), and "SHARED QUEUE" is the
/// main prioritized transmission queue (like the one maintained by Libcanard). Which interface is the FAST one and
/// which one is the SLOW one cannot be known in advance, obviously. They may change roles depending on the bus load
/// conditions. The backlog queue may be implemented as a naive static array that is scanned whenever a frame is
/// inserted or extracted; the linear time complexity may be acceptable because typically the backlog queue is small.
///
/// One should note that the need for maintaining a separate backlog queue arises out of the limitations of the bxCAN
/// macrocell. Certain advanced CAN controllers are equipped with a sufficiently deep hardware transmission queue
/// that relieves the application from manual queue management.
bool bxCANPush(const uint8_t     iface_index,
               const uint64_t    current_time,
               const uint64_t    deadline,
               const uint32_t    extended_can_id,
               const size_t      payload_size,
               const void* const payload);

/// Extract one frame from the RX FIFOs. FIFO0 checked first.
/// The out_payload memory shall be large enough to accommodate the largest CAN frame payload.
/// Returns true if received; false if both RX FIFOs are empty.
bool bxCANPop(const uint8_t   iface_index,
              uint32_t* const out_extended_can_id,
              size_t* const   out_payload_size,
              void* const     out_payload);

/// Given the bxCAN macrocell clock rate and the desired bit rate, compute the optimal timing register configuration.
/// The solution is optimized per the recommendations given in the specifications of UAVCAN, DeviceNet, CANOpen.
/// Units are SI. Typically, CAN is clocked from PCLK1.
/// Returns false if the requested bit rate cannot be set up at the current clock rate.
/// Returns true on success.
bool bxCANComputeTimings(const uint32_t      peripheral_clock_rate,
                         const uint32_t      target_bitrate,
                         BxCANTimings* const out_timings);

#ifdef __cplusplus
}
#endif

And it is implemented :tada:

Thanks @TomDeRybel for contributing this piece :man_technologist:

3 Likes

Nice work @TomDeRybel :+1:

Given the fact that there’s also a socketcan/libcanard folder availabe I’ve been wondering if there’d be interest for a similiar arduino/libcanard platform support?

Yes, definitely.

As a shoutout to everyone reading this: if you have a low-level driver that you can share under the MIT license, please help us out and send us a pull request.

Edit: it would be especially useful to have drivers for TI TCAN4550:

Hello, Pavel,
at first thank you for ready-made driver for bxCAN.
I started using the bxcan driver (STM32F103) for Libcanard and I want to use RX interrupts when a new message is pending in FIFO0 or FIFO1. I am curious why in bxcan.c when driver releases FIFO entry:

// Release FIFO entry we just read
*RFxR[i] |= BXCAN_RFR_RFOM | BXCAN_RFR_FOVR | BXCAN_RFR_FULL;

the bits BXCAN_RFR_FOVR and BXCAN_RFR_FULL are set?
In reference manual for STM32F103xx bits FOVRx and FULLx are set by hardware and reset by software (maybe for other MCU family this is not true). Will this cause problems when using receive interrupts?

They are actually reset (cleared) by writing ones:

1 Like

Thank you again, Pavel! I should be more careful with these abbreviations :sweat::

rc_w1 - Software can read as well as clear this bit by writing 1. Writing ‘0’ has no effect on
the bit value.
rs - Software can read as well as set this bit. Writing ‘0’ has no effect on the bit value.