How to filter UAVCAN messages on STM32 with libcanard (SOLVED)

I recently had a serious headache trying to figure out how to filter UAVCAN messages using libcanard’s stm32 drivers, and I finally figured it out! So I wanted to share what I learned to hopefully save others the same headache.

TL;DR
You must pay attention to what kind of message you’re trying to filter on.

  1. If it’s a service frame like GetNodeInfo, then you must set the filter_config.id to the GetNodeInfo ID and bit shift it 16 bits to the left. Your mask must also be bit shifted to the left, but make sure that your mask is only 1 byte
filter.id = UAVCAN_PROTOCOL_GETNODEINFO_ID << 16;
filter.mask = 0xFF << 16;
  1. If it’s a message frame like NodeStatus, then your id should be set to the NodeStatus ID and bit shift it to the left 8 bits. Also shift your mask to the left, but unlike service messages, the message ID takes up two bytes in the frame, so your mask must be 0xFFFF if you want to make your filter as tight as possible
filter.id = UAVCAN_PROTOCOL_NODESTATUS_ID << 8;
filter.mask = 0xFFFF << 8;

How filtering on the STM32 works with libcanard

The stm32 drivers for libcanard are great! But… there aren’t a lot of examples out their that show how to use them. But once you figure out the special sauce to use the API calls, they work perfectly. Going forward the assumption here is that you currently are receiving UAVCAN messages and are not using the built in stm32 filter configuration data structures CAN_FilterInitTypeDef

To set up filters:

  • Create an instance of the CanardSTM32AcceptanceFilterConfiguration struct.
  • Assign the struct’s ID member according to the UAVCAN message you wish to allow
  • Assign the struct’s Mask member according to how permissive you want it to be for allowing other messages that do not match the ID assigned above.

How not to set up filters

CanardSTM32AcceptanceFilterConfiguration filter_config[2];
//My intention is to allow only messages with an ID of NodeStatus and GetNodeInfo, but this will
//not work
filter_config[0].id = UAVCAN_PROTOCOL_NODESTATUS_ID;
filter_config[0].mask = 0xFF; //Mask is intended to check every bit in the ID field
                                           //but there are several things wrong

filter_config[1].id = UAVCAN_PROTOCOL_GETNODEINFO_ID;
filter_config[1].mask = 0xFF; 

if (canardSTM32ConfigureAcceptanceFilters(&filter_config, 2) == 0) {
	printf("Succesfully configured Canard acceptance filters\n");
    }
    else {
	printf("Failed to configure Canard acceptance filters\n");
    }

The above code will not work and will result in nearly all messages not making it through the filter. The reason why is that

  1. UAVCAN messages have different types: Messages Frames, Anonymous Frames and Service Frames
  2. The message frames are formatted differently depending on the type of message

Take a look at the image of the CAN frame formatting from the dronecan docs

The first seven bits of every message frame is filled with the source node ID. And one thing that isn’t totally clear in the libcanard stm32 driver documentation is that the filter fields for ID and mask are meant to be applied to the entire can frame. This allows you to control every aspect of the filtering down to the bit.

What I did wrong in the previous example
When I set the id member of the filter struct, my uavcan message ID’s were placed in the first byte of a 32 bit number. And that first byte is reserved for the source node ID and a flag reserved for service messages. So while I intended to filter by message type ID, what I was actually doing was filtering by source node ID. To make things worse, I’m using a number that doesn’t even fit into the 7 bit field. What resulted was an absolute mess that does a bunch of unintended things.

How to fix my code example

  1. Message frames use the second and third bytes to contain the Message Type ID. So if we want to filter for message frames, we need to bit shift the message ID 8 bits to the left so that they sit in the second and third bytes of uint32_t
  2. The mask previously used will not block every message that doesn’t match NODESTATUS ID, but will likely work and occasionally allow unwanted messages to slip through. To allow only message frames of a specific ID, we must set the mask to 0xFFFF and bit shift it 8 bits to the left
//Don't do this!
//filter_config[0].id = UAVCAN_PROTOCOL_NODESTATUS_ID;
//filter_config[0].mask = 0xFF;

//Do this instead
filter_config[0].id = UAVCAN_PROTOCOL_NODESTATUS_ID << 8;
filter_config[0].mask = 0xFFFF << 8; //Exclusively filter for node status id
  1. Service messages can only have an ID that fits in a single byte and the Service Frames store the message ID in the third bit only. To the filter for GETNODEINFO, we need to set the filter id to GETNODEINFO and bit shift it 16 bits to the left. We also need to set the filter mask to 0xFF and bit shift it 16 bits to the left
//Don't do this!
//filter_config[1].id = UAVCAN_PROTOCOL_GETNODEINFO_ID;
//filter_config[1].mask = 0xFF;

//Do this instead
filter_config[1].id = UAVCAN_PROTOCOL_GETNODEINFO_ID << 16;
filter_config[0].mask = 0xFF << 16;

With these changes, we’ve fixed our filters and now should only be receiving NodeStatus messages and GetNodeInfo messages! I will just note that, you can make filters tighter or more permissive by playing with the bits you set for the ID and mask. For example, if you wanted to receive BeginFirmwareUpdate messages from node ID 42, you could do it like this:

//Set third byte (service type ID) to BeginFirmwareUpdate
filter.id = UAVCAN_PROTOCOL_FILE_BEGINFIRMWAREUPDATE_ID << 16;
//Set first byte (Source node ID) to 42 with OR operator
filter.id |= 42;

//Set third byte of mask to require all bits to match the service ID byte
filter.mask = 0xFF << 16;
//Set first byte of mask to require all bits to match the source node ID byte
filter.mask |= 0xFF;

Conclusion
I hope this helps someone in the future. I’ve just gone through a lot of headaches trying to figure this stuff out, and documentation is understandably scarce. Good luck.