Skip to content

✨ (minor) v6: Add can_interrupt_dispatcher - #111

Merged
kammce merged 4 commits into
mainfrom
v6-can-interrupt-multiplexer
Oct 23, 2026
Merged

kammce merged 4 commits into
mainfrom
v6-can-interrupt-multiplexer

Conversation

@kammce

@kammce kammce commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Closes #109. Depends on libhal/libhal#252, which adds hal::can_receive_filter to can_interrupt::on_receive(). This PR's CI needs libhal/5.0.0-alpha.5, which #252 cuts when it merges.

hal::can_interrupt_dispatcher<N> lets several subscribers share one hal::can_interrupt, which in libhal v5 accepts a single subscriber. Each subscriber passes its own filter when it subscribes, the same as with a real interrupt, so whoever wires up the dispatcher doesn't need to know any driver's IDs.

hal::can_interrupt_dispatcher<2> dispatcher(can_interrupt);
motor_controller motor(dispatcher[0]);  // subscribes with { .id = 0x120, .mask = 0x1FFF'FFFF }
bus_logger logger(dispatcher[1]);       // subscribes for every message

Behavior

  • Subscribes once to the real interrupt, for every message.
  • Each slot is a full hal::can_interrupt with one subscriber. A second subscriber throws hal::subscription_busy.
  • Every dispatcher size uses one slot type, hal::can_interrupt_slot, so dispatcher<2> and dispatcher<7> don't each instantiate their own slot class and vtable. Its constructor is private: only a dispatcher creates slots.
  • Filtering: each slot stores the hal::can_receive_filter its subscriber passed to on_receive(). The filter is { id, mask } from libhal.
    • A message passes when (message.id & mask) == (id & mask). It compares IDs only, so one filter works for standard and extended messages.
    • The default (mask 0) passes every message.
    • A new subscriber brings its own filter, replacing the previous subscriber's.
  • On each received message, calls every subscribed slot whose filter passes it, in slot order, in interrupt context: O(N) per message.
  • A compiler-only fence (std::atomic_signal_fence) keeps the filter written before the callback that makes the slot live, since the interrupt reads them in that order. It emits no instructions and isn't std::atomic.
  • This filters in software, after any hardware filter, so it only sees messages the hardware already accepted.
  • Same structure and lifetime rules as edge_triggered_interrupt_dispatcher (✨ v6: Add edge_triggered_interrupt_dispatcher #110):
    • lifetimebound constructor parameter and operator[], with operator[] deleted on a temporary;
    • not copyable or movable;
    • the subscription to the real interrupt is declared last.

Tests

  • tests/can.test.cpp:
    • subscribing and unsubscribing, and subscription_busy on a taken interrupt;
    • delivery to every subscribed slot, and one subscriber per slot;
    • out_of_range;
    • the dispatcher subscribing to the real interrupt for every message;
    • subscriber filters (a block of IDs, an exact ID, the default);
    • one filter across standard and extended IDs;
    • a new subscriber replacing the previous filter.
  • Compile checks, added to the files from ✨ v6: Add edge_triggered_interrupt_dispatcher #110: the same rejected and lifetime-analysis cases as the edge-triggered dispatcher.

Testing

macOS, LLVM 23, against libhal built locally from libhal/libhal#252 (as libhal/latest, via replace_requires): all 16 test suites pass, and both compile-check files pass.

@kammce
kammce force-pushed the v6-edge-triggered-multiplexer branch from d1b62ad to 7f68f87 Compare October 14, 2026 22:03
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from 5567d82 to c5bbe68 Compare October 14, 2026 22:03
@kammce
kammce force-pushed the v6-edge-triggered-multiplexer branch from 7f68f87 to 251186c Compare October 14, 2026 23:50
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from c5bbe68 to a024694 Compare October 14, 2026 23:50
@kammce
kammce force-pushed the v6-edge-triggered-multiplexer branch from 251186c to 79d0dbd Compare October 15, 2026 02:44
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from a024694 to 37c0ca3 Compare October 15, 2026 02:44
@kammce
kammce force-pushed the v6-edge-triggered-multiplexer branch from 79d0dbd to dc6c906 Compare October 15, 2026 05:38
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from 37c0ca3 to 5642c09 Compare October 15, 2026 05:38
@kammce
kammce force-pushed the v6-edge-triggered-multiplexer branch from dc6c906 to e5ecd8e Compare October 17, 2026 01:26
Base automatically changed from v6-edge-triggered-multiplexer to main October 17, 2026 02:46
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from 5642c09 to 08184be Compare October 17, 2026 03:02
@kammce kammce changed the title ✨ v6: Add can_interrupt_multiplexer ✨ v6: Add can_interrupt_dispatcher Oct 17, 2026
hal::can_interrupt accepts one subscriber at a time. The dispatcher
subscribes once to a real CAN receive interrupt and provides N slots.
Each slot is a full can_interrupt with its own subscriber and an optional
hal::can_dispatch_filter, which matches a message when its ID format
(standard or extended) matches and (id & mask) == (filter.id & mask). A
slot without a filter receives every message. Messages reach each
matching subscriber in slot order, in interrupt context.

A slot's filter can only change while it has no subscriber, so the
interrupt never reads a half-written filter.

operator[] is lifetimebound and deleted on a temporary dispatcher, the
dispatcher cannot outlive its interrupt, and its subscription is its
last member so it is removed before the slots are destroyed. Compile
checks cover the same misuse as the edge-triggered dispatcher.
@kammce
kammce force-pushed the v6-can-interrupt-multiplexer branch from 08184be to e56eed2 Compare October 17, 2026 14:44
@kammce kammce changed the title ✨ v6: Add can_interrupt_dispatcher ✨ (minor) v6: Add can_interrupt_dispatcher Oct 17, 2026
libhal's can_interrupt::on_receive() now takes a hal::can_receive_filter
(libhal/libhal#252), so each subscriber passes the filter for the IDs it
handles. A slot stores the filter its subscriber passed, and the
dispatcher subscribes to the real interrupt for every message.

- Drops can_dispatch_filter and slot::allow()/filter(); libhal's
  can_receive_filter replaces them. It compares IDs only, so one filter
  works for standard and extended messages.
- A compiler fence keeps the filter written before the callback that
  makes the slot live, since the interrupt reads them in that order.
- Requires libhal >= 5.0.0-alpha.5.
Comment thread v6/tests/can.test.cpp Outdated
Move the nested slot out of can_interrupt_dispatcher<N> into
hal::can_interrupt_slot, so dispatchers of different sizes no longer
instantiate their own slot class and vtable.

Use static_cast<void>() instead of a C-style void cast in the tests.
Only a dispatcher creates slots, so user code cannot make one.

@kammce kammce left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@kammce
kammce merged commit d12cfd1 into main Oct 23, 2026
10 checks passed
@kammce
kammce deleted the v6-can-interrupt-multiplexer branch October 23, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v6: Add can_interrupt multiplexer

1 participant