Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add acceptance filter configuration helper #171

Merged
merged 3 commits into from
Dec 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,29 @@ else
}
```

A simple API for generating CAN hardware acceptance filter configurations is also provided.
Acceptance filters are generated in an extended 29-bit ID + mask scheme and can be used to minimize the number of irrelevant transfers processed in software.

```c
// Generate an acceptance filter to receive only uavcan.node.Heartbeat.1.0 messages (fixed port ID 7509):
CanardAcceptanceFilterConfig heartbeat_config = canardMakeAcceptanceFilterConfigForSubject(7509);
// And to receive only uavcan.register.Access.1.0 services (fixed port ID 384):
CanardAcceptanceFilterConfig register_access_config = canardMakeAcceptanceFilterConfigForService(384, ins.node_id);

// You can also combine the two filter configurations into one (may also accept in other messages).
// This allows consolidating a large set of configurations to fit the number of hardware filters.
// For more information on the optimal subset of configurations to consolidate to minimize wasted CPU,
// see the UAVCAN specification.
CanardAcceptanceFilterConfig combined_config = canardConsolidateAcceptanceFilterConfigs(&heartbeat_config, &register_access_config);
configureHardwareFilters(combined_config.extended_can_id, combined_config.extended_mask);
```

Full API specification is available in the documentation.
If you find the examples to be unclear or incorrect, please, open a ticket.

## Revisions

### v2.0

- Dedicated transmission queues per redundant CAN interface with depth limits.
The application is now expected to instantiate `CanardTxQueue` (or several in case of redundant transport) manually.

Expand All @@ -225,6 +244,8 @@ else

- Support build configuration headers via `CANARD_CONFIG_HEADER`.

- Add API for generating CAN hardware acceptance filter configurations

### v1.1

- Add new API function `canardRxAccept2()`, deprecate `canardRxAccept()`.
Expand Down
42 changes: 42 additions & 0 deletions libcanard/canard.c
Original file line number Diff line number Diff line change
Expand Up @@ -1166,3 +1166,45 @@ int8_t canardRxUnsubscribe(CanardInstance* const ins,
}
return out;
}

CanardFilter canardMakeFilterForSubject(const CanardPortID subject_id)
{
CanardFilter out = {0};

out.extended_can_id = ((uint32_t) subject_id) << OFFSET_SUBJECT_ID;
out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_07 | (CANARD_SUBJECT_ID_MAX << OFFSET_SUBJECT_ID);

return out;
}

CanardFilter canardMakeFilterForService(const CanardPortID service_id, const CanardNodeID local_node_id)
{
CanardFilter out = {0};

out.extended_can_id = FLAG_SERVICE_NOT_MESSAGE | (((uint32_t) service_id) << OFFSET_SERVICE_ID) |
(((uint32_t) local_node_id) << OFFSET_DST_NODE_ID);
out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_23 | (CANARD_SERVICE_ID_MAX << OFFSET_SERVICE_ID) |
(CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID);

return out;
}

CanardFilter canardMakeFilterForServices(const CanardNodeID local_node_id)
{
CanardFilter out = {0};

out.extended_can_id = FLAG_SERVICE_NOT_MESSAGE | (((uint32_t) local_node_id) << OFFSET_DST_NODE_ID);
out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_23 | (CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID);

return out;
}

CanardFilter canardConsolidateFilters(const CanardFilter* a, const CanardFilter* b)
{
CanardFilter out = {0};

out.extended_mask = a->extended_mask & b->extended_mask & ~(a->extended_can_id ^ b->extended_can_id);
out.extended_can_id = a->extended_can_id & out.extended_mask;

return out;
}
58 changes: 58 additions & 0 deletions libcanard/canard.h
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ struct CanardInstance
CanardTreeNode* rx_subscriptions[CANARD_NUM_TRANSFER_KINDS];
};

/// CAN acceptance filter configuration with an extended 29-bit ID utilizing an ID + mask filter scheme.
/// Filter configuration can be programmed into a CAN controller to filter out irrelevant messages in hardware.
/// This allows the software application to reduce CPU load spent on processing irrelevant messages.
typedef struct CanardFilter
{
/// 29-bit extended ID. Defines the extended CAN ID to filter incoming frames against.
/// The bits above 29-th shall be zero.
uint32_t extended_can_id;
/// 29-bit extended mask. Defines the bitmask used to enable/disable bits used to filter messages.
/// Only bits that are enabled are compared to the extended_can_id for filtering.
/// The bits above 29-th shall be zero.
uint32_t extended_mask;
} CanardFilter;

/// Construct a new library instance.
/// The default values will be assigned as specified in the structure field documentation.
/// If any of the pointers are NULL, the behavior is undefined.
Expand Down Expand Up @@ -635,6 +649,50 @@ int8_t canardRxUnsubscribe(CanardInstance* const ins,
const CanardTransferKind transfer_kind,
const CanardPortID port_id);

/// Utilities for generating CAN controller hardware acceptance filter configurations
/// to accept specific subjects, services, or nodes.
///
/// Complex applications will likely subscribe to more subject IDs than there are
/// acceptance filters available in the CAN hardware. In this case, the application
/// should implement filter consolidation. See canardConsolidateFilters()
/// as well as the UAVCAN specification for details.

/// Generate an acceptance filter configuration to accept a specific subject ID.
CanardFilter canardMakeFilterForSubject(const CanardPortID subject_id);

/// Generate an acceptance filter configuration to accept both requests and responses for a specific service.
///
/// Users may prefer to instead use a catch-all acceptance filter configuration for accepting
/// all service requests and responses targeted at the specified local node ID.
/// See canardMakeFilterForServices() for this.
CanardFilter canardMakeFilterForService(const CanardPortID service_id, const CanardNodeID local_node_id);

/// Generate an acceptance filter configuration to accept all service
/// requests and responses targeted to the specified local node ID.
///
/// Due to the relatively low frequency of service transfers expected on a network,
/// and the fact that a service directed at a specific node is not likely to be rejected by that node,
/// a user may prefer to use this over canardMakeFilterForService()
/// in order to simplify the API usage and reduce the number of required hardware CAN acceptance filters.
CanardFilter canardMakeFilterForServices(const CanardNodeID local_node_id);

/// Consolidate two acceptance filter configurations into a single configuration.
///
/// Complex applications will likely subscribe to more subject IDs than there are
/// acceptance filters available in the CAN hardware. In this case, the application
/// should implement filter consolidation. While this may make it impossible to create
/// a 'perfect' filter that only accepts desired subject IDs, the application should apply
/// consolidation in a manner that minimizes the number of undesired messages that pass
/// through the hardware acceptance filters and require software filtering (implemented by canardRxSubscribe).
///
/// While optimal choice of filter consolidation is a function of the number of available hardware filters,
/// the set of transfers needed by the application, and the expected frequency of occurence
/// of all possible distinct transfers on the bus, it is possible to generate a quasi-optimal configuration
/// if information about the frequency of occurence of different transfers is not known.
/// For details, see the "Automatic hardware acceptance filter configuration" note under the UAVCAN/CAN section
/// in the Transport Layer chapter of the UAVCAN specification.
CanardFilter canardConsolidateFilters(const CanardFilter* const a, const CanardFilter* const b);

#ifdef __cplusplus
}
#endif
Expand Down
2 changes: 1 addition & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ gen_test_matrix(test_private
"-Wno-missing-declarations")

gen_test_matrix(test_public
"test_public_tx.cpp;test_public_rx.cpp;test_public_roundtrip.cpp;test_self.cpp"
"test_public_tx.cpp;test_public_rx.cpp;test_public_roundtrip.cpp;test_self.cpp;test_public_filters.cpp"
""
"-Wmissing-declarations")
71 changes: 71 additions & 0 deletions tests/test_public_filters.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// This software is distributed under the terms of the MIT License.
// Copyright (c) 2016-2021 UAVCAN Development Team.

#include "exposed.hpp"
#include "helpers.hpp"
#include "catch.hpp"

namespace
{
constexpr std::uint32_t OFFSET_PRIORITY = 26U;
constexpr std::uint32_t OFFSET_SUBJECT_ID = 8U;
constexpr std::uint32_t OFFSET_SERVICE_ID = 14U;
constexpr std::uint32_t OFFSET_DST_NODE_ID = 7U;

constexpr std::uint32_t FLAG_SERVICE_NOT_MESSAGE = std::uint32_t(1) << 25U;
constexpr std::uint32_t FLAG_ANONYMOUS_MESSAGE = std::uint32_t(1) << 24U;
constexpr std::uint32_t FLAG_REQUEST_NOT_RESPONSE = std::uint32_t(1) << 24U;
constexpr std::uint32_t FLAG_RESERVED_23 = std::uint32_t(1) << 23U;
constexpr std::uint32_t FLAG_RESERVED_07 = std::uint32_t(1) << 7U;

TEST_CASE("FilterSubject")
{
const std::uint16_t heartbeat_subject_id = 7509;
CanardFilter heartbeat_config = canardMakeFilterForSubject(heartbeat_subject_id);
REQUIRE((heartbeat_config.extended_can_id &
static_cast<std::uint32_t>(heartbeat_subject_id << OFFSET_SUBJECT_ID)) != 0);
REQUIRE((heartbeat_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0);
REQUIRE((heartbeat_config.extended_mask & FLAG_RESERVED_07) != 0);
REQUIRE((heartbeat_config.extended_mask & (CANARD_SUBJECT_ID_MAX << OFFSET_SUBJECT_ID)) != 0);
}

TEST_CASE("FilterService")
{
const std::uint16_t access_service_id = 7509;
const std::uint16_t node_id = 42;
CanardFilter access_config = canardMakeFilterForService(access_service_id, node_id);
REQUIRE((access_config.extended_can_id & static_cast<std::uint32_t>(access_service_id << OFFSET_SERVICE_ID)) != 0);
REQUIRE((access_config.extended_can_id & static_cast<std::uint32_t>(node_id << OFFSET_DST_NODE_ID)) != 0);
REQUIRE((access_config.extended_can_id & FLAG_SERVICE_NOT_MESSAGE) != 0);
REQUIRE((access_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0);
REQUIRE((access_config.extended_mask & FLAG_RESERVED_23) != 0);
REQUIRE((access_config.extended_mask & static_cast<std::uint32_t>(CANARD_SERVICE_ID_MAX << OFFSET_SERVICE_ID)) !=
0);
REQUIRE((access_config.extended_mask & static_cast<std::uint32_t>(CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID)) != 0);
}

TEST_CASE("FilterServices")
{
const std::uint8_t node_id = 42;
CanardFilter access_config = canardMakeFilterForServices(node_id);
REQUIRE((access_config.extended_can_id & static_cast<std::uint32_t>(node_id << OFFSET_DST_NODE_ID)) != 0);
REQUIRE((access_config.extended_can_id & FLAG_SERVICE_NOT_MESSAGE) != 0);
REQUIRE((access_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0);
REQUIRE((access_config.extended_mask & FLAG_RESERVED_23) != 0);
REQUIRE((access_config.extended_mask & static_cast<std::uint32_t>(CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID)) != 0);
}

TEST_CASE("Consolidate")
{
const std::uint16_t heartbeat_subject_id = 7509;
CanardFilter heartbeat_config = canardMakeFilterForSubject(heartbeat_subject_id);

const std::uint16_t access_service_id = 7509;
const std::uint8_t node_id = 42;
CanardFilter access_config = canardMakeFilterForService(access_service_id, node_id);

CanardFilter combined = canardConsolidateFilters(&heartbeat_config, &access_config);
REQUIRE((combined.extended_mask | heartbeat_config.extended_mask) == heartbeat_config.extended_mask);
REQUIRE((combined.extended_mask | access_config.extended_mask) == access_config.extended_mask);
}
} // namespace