diff --git a/v6/conanfile.py b/v6/conanfile.py index 3210277..af621ef 100644 --- a/v6/conanfile.py +++ b/v6/conanfile.py @@ -93,7 +93,7 @@ def build_requirements(self): def requirements(self): # libhal v5 is only released as pre-releases (5.0.0-alpha.N) so far. # Remove include_prerelease once 5.0.0 is published. - self.requires("libhal/[>=5.0.0-alpha.1 <6, include_prerelease]") + self.requires("libhal/[>=5.0.0-alpha.5 <6, include_prerelease]") def layout(self): build_path = Path("build") / ( diff --git a/v6/modules/can.cppm b/v6/modules/can.cppm index 98d806d..e819529 100644 --- a/v6/modules/can.cppm +++ b/v6/modules/can.cppm @@ -15,6 +15,9 @@ module; #include +#include +#include +#include #include #include #include @@ -146,9 +149,9 @@ export [[nodiscard]] constexpr std::uint16_t bit_width( * can bus peripheral. Returns std::nullopt if the target baud rate is not * achievable with the provided operating frequency. */ -export [[nodiscard]] std::optional -calculate_can_bus_divider(hertz p_operating_frequency, - hertz p_target_baud_rate) +export [[nodiscard]] std::optional calculate_can_bus_divider( + hertz p_operating_frequency, + hertz p_target_baud_rate) { can_bus_divider_t timing{}; @@ -301,4 +304,166 @@ private: hal::u32 m_id; std::size_t m_receive_cursor = 0; }; + +export template +class can_interrupt_dispatcher; + +/** + * @brief One subscriber's view of a CAN receive interrupt shared by + * `hal::can_interrupt_dispatcher` + * @ingroup CAN_Utilities + * + * Only a dispatcher can create slots; get them from its `operator[]`. Every + * dispatcher, whatever its slot count, uses this one type. + */ +export class can_interrupt_slot final : public can_interrupt +{ +public: + can_interrupt_slot(can_interrupt_slot const&) = delete; + can_interrupt_slot& operator=(can_interrupt_slot const&) = delete; + can_interrupt_slot(can_interrupt_slot&&) = delete; + can_interrupt_slot& operator=(can_interrupt_slot&&) = delete; + ~can_interrupt_slot() = default; + +private: + template + friend class can_interrupt_dispatcher; + + can_interrupt_slot() = default; + + void dispatch(can_message const& p_message) noexcept + { + // Read the callback once: a subscriber can be removed between messages. + auto* const callback = m_callback; + if (callback == nullptr) { + return; + } + // Pairs with the fence in driver_on_receive(): the filter is read after + // the callback, so it is never one written for an earlier subscriber. + std::atomic_signal_fence(std::memory_order_acquire); + if (not m_filter.matches(p_message)) { + return; + } + callback->callback(p_message); + } + + void driver_on_receive(can_receive_callback& p_callback, + can_receive_filter p_filter) override + { + if (m_callback != nullptr) { + throw subscription_busy(this); + } + // The slot has no subscriber, so the interrupt ignores it while the + // filter is written. The fence keeps the compiler from moving that + // write after the callback store that makes the slot live. + m_filter = p_filter; + std::atomic_signal_fence(std::memory_order_release); + m_callback = &p_callback; + } + + void driver_clear_receive() noexcept override + { + m_callback = nullptr; + } + + can_receive_callback* m_callback = nullptr; + can_receive_filter m_filter{}; +}; + +/** + * @brief Shares one CAN receive interrupt among several subscribers + * @ingroup CAN_Utilities + * + * `hal::can_interrupt` accepts one subscriber at a time. The dispatcher + * subscribes once to a real `hal::can_interrupt`, for every message, and + * provides `SlotCount` slots. Each slot is a `hal::can_interrupt_slot`, a + * `hal::can_interrupt` for one subscriber, and each subscriber passes its own + * `hal::can_receive_filter` when it subscribes, the same as with a real + * interrupt. + * + * Each received message is passed, in slot order and in interrupt context, to + * the subscriber of every slot whose filter passes it. This filters in + * software after any hardware filter, so it only sees messages the hardware + * already accepted. + * + * Neither the dispatcher nor its slots may outlive the real interrupt, and + * the slots may not outlive the dispatcher. + * + * Usage: + * + * ```cpp + * hal::can_interrupt_dispatcher<2> dispatcher(can_interrupt); + * // Each driver subscribes with the filter for the IDs it handles. + * motor_controller motor(dispatcher[0]); + * bus_logger logger(dispatcher[1]); + * ``` + * + * @tparam SlotCount - number of slots + */ +export template +class can_interrupt_dispatcher final : private can_receive_callback +{ +public: + /** + * @brief Subscribe to a CAN receive interrupt and provide its slots + * + * @warning Never construct the dispatcher from an interrupt service + * routine. It subscribes to the interrupt, which throws + * `hal::subscription_busy` if the interrupt already has a subscriber. + * + * @param p_interrupt - the CAN receive interrupt to share + * @throws hal::subscription_busy - if the interrupt already has a subscriber + */ + explicit can_interrupt_dispatcher(can_interrupt& p_interrupt + [[clang::lifetimebound]]) + : m_subscription(p_interrupt.on_receive(*this)) + { + } + + can_interrupt_dispatcher(can_interrupt_dispatcher const&) = delete; + can_interrupt_dispatcher& operator=(can_interrupt_dispatcher const&) = delete; + can_interrupt_dispatcher(can_interrupt_dispatcher&&) = delete; + can_interrupt_dispatcher& operator=(can_interrupt_dispatcher&&) = delete; + ~can_interrupt_dispatcher() = default; + + /** + * @brief Access a slot + * + * @param p_index - index of the slot + * @return can_interrupt_slot& - the slot, which must not outlive the + * dispatcher + * @throws hal::out_of_range - if p_index is not less than SlotCount + */ + [[nodiscard]] can_interrupt_slot& operator[]( + std::size_t p_index) & [[clang::lifetimebound]] + { + if (p_index >= SlotCount) { + throw hal::out_of_range(this, + { .m_index = p_index, .m_capacity = SlotCount }); + } + return m_slots[p_index]; + } + + /// A slot of a temporary dispatcher would dangle immediately. + can_interrupt_slot& operator[](std::size_t p_index) && = delete; + + /// @return std::size_t - number of slots + [[nodiscard]] static constexpr std::size_t size() noexcept + { + return SlotCount; + } + +private: + void callback(can_message const& p_message) noexcept override + { + for (auto& slot : m_slots) { + slot.dispatch(p_message); + } + } + + std::array m_slots{}; + // Last, so the real interrupt stops calling the dispatcher before the slots + // are destroyed. + subscription m_subscription; +}; } // namespace hal::inline v6 diff --git a/v6/tests/can.test.cpp b/v6/tests/can.test.cpp index 33872ce..0f74d8e 100644 --- a/v6/tests/can.test.cpp +++ b/v6/tests/can.test.cpp @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include @@ -26,6 +29,13 @@ using namespace mp_units::si::unit_symbols; namespace { +// Every dispatcher hands out the same slot type, whatever its slot count. +template +using can_slot_of = std::remove_reference_t< + decltype(std::declval&>()[0])>; +static_assert(std::is_same_v, hal::can_interrupt_slot>); +static_assert(std::is_same_v, hal::can_interrupt_slot>); + void check_validity(hal::hertz p_operating_frequency, hal::hertz p_target_baud_rate) { @@ -306,10 +316,213 @@ void message_finder_test() }; } +struct fake_can_interrupt final : hal::can_interrupt +{ + void receive(hal::can_message const& p_message) + { + if (callback != nullptr) { + callback->callback(p_message); + } + } + + hal::can_receive_callback* callback = nullptr; + hal::can_receive_filter filter{}; + +private: + void driver_on_receive(hal::can_receive_callback& p_callback, + hal::can_receive_filter p_filter) override + { + if (callback != nullptr) { + throw hal::subscription_busy(this); + } + callback = &p_callback; + filter = p_filter; + } + void driver_clear_receive() noexcept override + { + callback = nullptr; + } +}; + +struct message_recorder final : hal::can_receive_callback +{ + void callback(hal::can_message const& p_message) noexcept override + { + ids.push_back(p_message.id); + } + std::vector ids; +}; + +void can_interrupt_dispatcher_test() +{ + using namespace boost::ut; + + "subscribes to the interrupt for its lifetime"_test = []() { + // Setup + fake_can_interrupt interrupt; + + // Exercise + { + hal::can_interrupt_dispatcher<2> dispatcher(interrupt); + + // Verify + expect(interrupt.callback != nullptr); + } + + // Verify + expect(interrupt.callback == nullptr) << "destroying it unsubscribes"; + }; + + "throws when the interrupt already has a subscriber"_test = []() { + // Setup + fake_can_interrupt interrupt; + message_recorder other; + auto subscription = interrupt.on_receive(other); + + // Exercise + Verify + expect(throws( + [&]() { hal::can_interrupt_dispatcher<2> dispatcher(interrupt); })); + }; + + "forwards every message to every subscribed slot"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<3> dispatcher(interrupt); + message_recorder first; + message_recorder third; + auto first_subscription = dispatcher[0].on_receive(first); + auto third_subscription = dispatcher[2].on_receive(third); + + // Exercise + interrupt.receive({ .id = 0x111 }); + interrupt.receive({ .id = 0x222 }); + + // Verify + expect(that % first.ids == std::vector{ 0x111, 0x222 }); + expect(that % third.ids == std::vector{ 0x111, 0x222 }); + }; + + "a slot accepts one subscriber at a time"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<1> dispatcher(interrupt); + message_recorder first; + message_recorder second; + + // Exercise + Verify + { + auto subscription = dispatcher[0].on_receive(first); + expect(throws( + [&]() { auto busy = dispatcher[0].on_receive(second); })); + } + auto subscription = dispatcher[0].on_receive(second); + interrupt.receive({ .id = 0x333 }); + expect(that % first.ids.size() == 0); + expect(that % second.ids == std::vector{ 0x333 }); + }; + + "operator[] throws out_of_range past the last slot"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<2> dispatcher(interrupt); + + // Exercise + Verify + expect( + throws([&]() { static_cast(dispatcher[2]); })); + expect(that % dispatcher.size() == 2); + }; + + "the dispatcher subscribes to every message"_test = []() { + // Setup + fake_can_interrupt interrupt; + interrupt.filter = { .id = 0x1, .mask = 0x1 }; + + // Exercise + hal::can_interrupt_dispatcher<1> dispatcher(interrupt); + + // Verify + expect(interrupt.filter == hal::can_receive_filter{}); + }; + + "a slot only receives messages its subscriber's filter passes"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<3> dispatcher(interrupt); + message_recorder block; + message_recorder exact; + message_recorder everything; + auto block_subscription = + dispatcher[0].on_receive(block, { .id = 0x100, .mask = 0x7F0 }); + auto exact_subscription = + dispatcher[1].on_receive(exact, { .id = 0x123, .mask = 0x1FFF'FFFF }); + auto everything_subscription = dispatcher[2].on_receive(everything); + + // Exercise + interrupt.receive({ .id = 0x100 }); + interrupt.receive({ .id = 0x10F }); + interrupt.receive({ .id = 0x110 }); + interrupt.receive({ .id = 0x123 }); + + // Verify + expect(that % block.ids == std::vector{ 0x100, 0x10F }); + expect(that % exact.ids == std::vector{ 0x123 }); + expect(that % everything.ids == + std::vector{ 0x100, 0x10F, 0x110, 0x123 }); + }; + + "one filter works for standard and extended IDs"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<2> dispatcher(interrupt); + message_recorder low_bits; + message_recorder whole_id; + auto low_subscription = + dispatcher[0].on_receive(low_bits, { .id = 0x123, .mask = 0x7FF }); + auto whole_subscription = + dispatcher[1].on_receive(whole_id, { .id = 0x123, .mask = 0x1FFF'FFFF }); + + // Exercise + interrupt.receive({ .id = 0x123, .extended = false }); + interrupt.receive({ .id = 0x123, .extended = true }); + interrupt.receive({ .id = 0x1000'0123, .extended = true }); + + // Verify + expect(that % low_bits.ids == + std::vector{ 0x123, 0x123, 0x1000'0123 }) + << "an 11-bit mask also passes extended IDs with the same low bits"; + expect(that % whole_id.ids == std::vector{ 0x123, 0x123 }) + << "a 29-bit mask rejects extended IDs with other upper bits"; + }; + + "a new subscriber brings its own filter"_test = []() { + // Setup + fake_can_interrupt interrupt; + hal::can_interrupt_dispatcher<1> dispatcher(interrupt); + message_recorder first; + message_recorder second; + { + auto subscription = + dispatcher[0].on_receive(first, { .id = 0x10, .mask = 0x7FF }); + interrupt.receive({ .id = 0x10 }); + interrupt.receive({ .id = 0x20 }); + } + + // Exercise + auto subscription = dispatcher[0].on_receive(second); + interrupt.receive({ .id = 0x10 }); + interrupt.receive({ .id = 0x20 }); + + // Verify + expect(that % first.ids == std::vector{ 0x10 }); + expect(that % second.ids == std::vector{ 0x10, 0x20 }) + << "the second subscriber's default filter replaces the first one's"; + }; +} } // namespace int main() { divider_test(); message_finder_test(); + can_interrupt_dispatcher_test(); } diff --git a/v6/tests/compile_checks/dispatcher_analysis.cpp b/v6/tests/compile_checks/dispatcher_analysis.cpp index a8eac50..d7798db 100644 --- a/v6/tests/compile_checks/dispatcher_analysis.cpp +++ b/v6/tests/compile_checks/dispatcher_analysis.cpp @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// edge_triggered_interrupt_dispatcher misuse that Clang's lifetime safety -// analysis reports. +// Interrupt dispatcher misuse that Clang's lifetime safety analysis reports. #include "fixtures.hpp" @@ -43,3 +42,32 @@ dispatcher const* dispatcher_outlives_pin() static dispatcher shared(pin); return &shared; } + +using can_dispatcher = hal::can_interrupt_dispatcher<2>; + +hal::can_interrupt& can_slot_outlives_dispatcher( + fake_can_interrupt& p_interrupt) +{ + can_dispatcher shared(p_interrupt); + // expected-error@+2 {{is returned}} + // expected-error@+1 {{reference to stack memory}} + return shared[0]; +} + +hal::subscription can_subscription_outlives_dispatcher( + fake_can_interrupt& p_interrupt) +{ + static message_counter c; + can_dispatcher shared(p_interrupt); + // expected-error@+2 {{is returned}} + // expected-error@+1 {{address of stack memory}} + return shared[0].on_receive(c); +} + +can_dispatcher const* can_dispatcher_outlives_interrupt() +{ + fake_can_interrupt interrupt; // forgot static + // expected-error@+1 {{which will dangle}} + static can_dispatcher shared(interrupt); + return &shared; +} diff --git a/v6/tests/compile_checks/dispatcher_rejects.cpp b/v6/tests/compile_checks/dispatcher_rejects.cpp index 37eec7e..dd5440b 100644 --- a/v6/tests/compile_checks/dispatcher_rejects.cpp +++ b/v6/tests/compile_checks/dispatcher_rejects.cpp @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// edge_triggered_interrupt_dispatcher misuse that the type system rejects. +// Interrupt dispatcher misuse that the type system rejects. #include "fixtures.hpp" @@ -40,3 +40,32 @@ void copy_dispatcher(fake_pin& p_pin) // expected-error@+1 {{call to deleted constructor}} dispatcher copy = shared; } + +using can_dispatcher = hal::can_interrupt_dispatcher<2>; + +void index_temporary_can_dispatcher(fake_can_interrupt& p_interrupt) +{ + // expected-error@+1 {{overload resolution selected deleted operator '[]'}} + auto& slot = can_dispatcher(p_interrupt)[0]; + static_cast(slot); +} + +void can_dispatcher_on_temporary_interrupt() +{ + // expected-error@+1 {{no matching constructor}} + can_dispatcher shared(fake_can_interrupt{}); +} + +void copy_can_dispatcher(fake_can_interrupt& p_interrupt) +{ + can_dispatcher shared(p_interrupt); + // expected-error@+1 {{call to deleted constructor}} + can_dispatcher copy = shared; +} + +// A slot only works inside a dispatcher, so only a dispatcher creates one. +void construct_can_slot() +{ + // expected-error@+1 {{private constructor}} + hal::can_interrupt_slot slot; +} diff --git a/v6/tests/compile_checks/fixtures.hpp b/v6/tests/compile_checks/fixtures.hpp index 73eb608..fdb5876 100644 --- a/v6/tests/compile_checks/fixtures.hpp +++ b/v6/tests/compile_checks/fixtures.hpp @@ -47,3 +47,29 @@ struct counter final : hal::edge_triggered_callback { } }; + +struct fake_can_interrupt final : hal::can_interrupt +{ + hal::can_receive_callback* callback = nullptr; + +private: + void driver_on_receive(hal::can_receive_callback& p_callback, + hal::can_receive_filter) override + { + if (callback) { + throw hal::subscription_busy(this); + } + callback = &p_callback; + } + void driver_clear_receive() noexcept override + { + callback = nullptr; + } +}; + +struct message_counter final : hal::can_receive_callback +{ + void callback(hal::can_message const&) noexcept override + { + } +};