Skip to content

Commit b962dd3

Browse files
node-ID occupancy update
1 parent 62236fe commit b962dd3

File tree

1 file changed

+70
-29
lines changed

1 file changed

+70
-29
lines changed

libcanard/canard.c

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,29 @@ static byte_t popcount(const uint64_t x)
127127
#endif
128128
}
129129

130+
// The splitmix64 PRNG algorithm. Original work by Sebastiano Vigna, released under CC0-1.0 (public domain dedication).
131+
// Source http://xoshiro.di.unimi.it/splitmix64.c.
132+
static uint64_t splitmix64(uint64_t* const state)
133+
{
134+
uint64_t z = (*state += 0x9E3779B97F4A7C15ULL);
135+
z = (z ^ (z >> 30U)) * 0xBF58476D1CE4E5B9ULL;
136+
z = (z ^ (z >> 27U)) * 0x94D049BB133111EBULL;
137+
return z ^ (z >> 31U);
138+
}
139+
140+
// Obtains a decent-quality pseudo-random number in [0, bound). Returns 0 if bound<=1
141+
static uint64_t random(canard_t* const self, const uint64_t bound)
142+
{
143+
return (bound > 0) ? (splitmix64(&self->prng_state) % bound) : 0;
144+
}
145+
146+
// Returns true with a probability of approximately 1/p_reciprocal. If p_reciprocal<=1, result is always true.
147+
static bool chance(canard_t* const self, const uint64_t p_reciprocal) { return random(self, p_reciprocal) == 0; }
148+
149+
// Least significant bit at limb #0, bit #0.
150+
static void bitmap_set(uint64_t* const b, const size_t i) { b[i / 64U] |= (UINT64_C(1) << (i % 64U)); }
151+
static bool bitmap_test(const uint64_t* const b, const size_t i) { return (b[i / 64U] & (1ULL << (i % 64U))) != 0; }
152+
130153
static void* mem_alloc(const canard_mem_t memory, const size_t size) { return memory.vtable->alloc(memory, size); }
131154
static void* mem_alloc_zero(const canard_mem_t memory, const size_t size)
132155
{
@@ -842,6 +865,7 @@ static void tx_eject_pending(canard_t* const self, const byte_t iface_index)
842865
// This is needed to allow safe node-ID change on collision detection, such that remotes don't reassemble multiframe
843866
// frankentransfers from different nodes sharing the same ID.
844867
// The complexity is linear in the number of enqueued transfers (not frames!).
868+
// This is safe to invoke from any context since it doesn't reach anywhere outside of the TX pipeline.
845869
static void tx_purge_continuations(canard_t* const self)
846870
{
847871
tx_transfer_t* tr = LIST_HEAD(self->tx.agewise, tx_transfer_t, list_agewise);
@@ -1575,38 +1599,57 @@ static bool rx_filter_configure(const canard_t* const self)
15751599

15761600
// --------------------------------------------- MISC ---------------------------------------------
15771601

1578-
// The splitmix64 PRNG algorithm. Original work by Sebastiano Vigna, released under CC0-1.0 (public domain dedication).
1579-
// Source http://xoshiro.di.unimi.it/splitmix64.c.
1580-
static uint64_t splitmix64(uint64_t* const state)
1581-
{
1582-
uint64_t z = (*state += 0x9E3779B97F4A7C15ULL);
1583-
z = (z ^ (z >> 30U)) * 0xBF58476D1CE4E5B9ULL;
1584-
z = (z ^ (z >> 27U)) * 0x94D049BB133111EBULL;
1585-
return z ^ (z >> 31U);
1586-
}
1587-
1588-
// Obtains a decent-quality pseudo-random number in [0, cardinality).
1589-
static uint64_t random(canard_t* const self, const uint64_t cardinality)
1590-
{
1591-
CANARD_ASSERT(cardinality > 0);
1592-
return splitmix64(&self->prng_state) % cardinality;
1593-
}
1594-
15951602
static void node_id_occupancy_reset(canard_t* const self)
15961603
{
15971604
self->node_id_occupancy_bitmap[0] = 1; // Reserve 0 for compatibility with v0
15981605
self->node_id_occupancy_bitmap[1] = 0;
15991606
}
16001607

16011608
// Records the seen node-ID and reallocates the local node if a collision is found.
1602-
static void node_id_occupancy_update(canard_t* const self, const byte_t src, const byte_t dst)
1603-
{
1604-
(void)self;
1605-
(void)src;
1606-
(void)dst;
1607-
// TODO not implemented.
1608-
// TODO purge pending TX transfers that have already emitted the first frame.
1609-
rx_filter_configure(self);
1609+
static void node_id_occupancy_update(canard_t* const self, const byte_t src)
1610+
{
1611+
// Update the node-ID occupancy bitmap. We cannot detect departures of an individual node, so in the presence of
1612+
// churn the slots will be eventually exhausted. We mitigate this by applying probabilistic purge once the
1613+
// population count exceeds some threshold, such that the purge probability becomes 1 when the bitmap is almost
1614+
// full. Such non-deterministic purge is preferable because it avoids spurious synchronization effects.
1615+
if (!bitmap_test(self->node_id_occupancy_bitmap, src) || (self->node_id == src)) {
1616+
bitmap_set(self->node_id_occupancy_bitmap, src);
1617+
const byte_t cap = CANARD_NODE_ID_CAPACITY;
1618+
const byte_t pc = popcount(self->node_id_occupancy_bitmap[0]) + popcount(self->node_id_occupancy_bitmap[1]);
1619+
const byte_t zc = cap - pc; // zero count, i.e., the number of free slots
1620+
const bool purge = (pc > (cap / 2)) && chance(self, zc); // deny purge unless getting dense
1621+
CANARD_ASSERT(zc > 0); // the algorithm guarantees some free slots always
1622+
1623+
// Check for collision, reroll if needed. This must be done before we purge the occupancy map.
1624+
if (self->node_id == src) {
1625+
// Uniformly pick a cleared bit from the bitmap. It will become our new node-ID.
1626+
size_t cleared_bit_index = (size_t)random(self, zc);
1627+
CANARD_ASSERT(cleared_bit_index < zc);
1628+
size_t bit_index = 0;
1629+
while (cleared_bit_index > 0) {
1630+
if (!bitmap_test(self->node_id_occupancy_bitmap, bit_index++)) {
1631+
cleared_bit_index--;
1632+
}
1633+
CANARD_ASSERT(bit_index < CANARD_NODE_ID_CAPACITY);
1634+
}
1635+
1636+
// Assign the new ID.
1637+
CANARD_ASSERT((0 < bit_index) && (bit_index < CANARD_NODE_ID_CAPACITY));
1638+
self->node_id = (byte_t)bit_index;
1639+
CANARD_ASSERT(!bitmap_test(self->node_id_occupancy_bitmap, self->node_id));
1640+
1641+
// Update dependent states.
1642+
tx_purge_continuations(self);
1643+
self->rx.filters_dirty = true;
1644+
self->err.collision++;
1645+
}
1646+
1647+
// The purge is delayed because we need the original occupancy map for reallocation.
1648+
if (purge) {
1649+
node_id_occupancy_reset(self);
1650+
bitmap_set(self->node_id_occupancy_bitmap, src);
1651+
}
1652+
}
16101653
}
16111654

16121655
bool canard_new(canard_t* const self,
@@ -1701,11 +1744,9 @@ static void ingest_frame(canard_t* const self,
17011744
const frame_t frame)
17021745
{
17031746
// Update the node-ID occupancy/collision monitoring states before routing the message.
1704-
// We do this only on start frames because non-start frames have the version detection ambiguity,
1705-
// which is not a problem for the routing logic (new states can only be created on start frames),
1706-
// but it may cause phantom occupancy detection. Also, doing it only on start frames reduces the load.
1747+
// We do this only on start frames only mostly to manage load.
17071748
if (frame.start) {
1708-
node_id_occupancy_update(self, frame.src, frame.dst);
1749+
node_id_occupancy_update(self, frame.src);
17091750
}
17101751
// Route the frame to the appropriate destination internally.
17111752
canard_subscription_t* const sub = rx_route(self, &frame);

0 commit comments

Comments
 (0)