Skip to content

Commit cb98eb3

Browse files
stab
1 parent f74453f commit cb98eb3

File tree

1 file changed

+41
-127
lines changed

1 file changed

+41
-127
lines changed

libcanard/canard.c

Lines changed: 41 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,48 +1229,6 @@ static void rx_slot_advance(rx_slot_t* const slot, const size_t extent, const ca
12291229
slot->expected_toggle ^= 1U;
12301230
}
12311231

1232-
// This value is unreachable even if seqno is incremented by full transfer-ID period every frame at 5k frames/s;
1233-
// at that rate, it would take about 56 years to wrap around.
1234-
#define RX_SEQNO_MAX ((UINT64_C(1) << 48U) - 1U)
1235-
1236-
// A compact representation is needed because we need to store an array of these in dynamic memory.
1237-
typedef struct
1238-
{
1239-
uint16_t limbs[3];
1240-
} rx_seqno_packed_t;
1241-
1242-
static uint64_t rx_seqno_unpack(const rx_seqno_packed_t v)
1243-
{
1244-
return ((uint64_t)v.limbs[0]) | (((uint64_t)v.limbs[1]) << 16U) | (((uint64_t)v.limbs[2]) << 32U);
1245-
}
1246-
static rx_seqno_packed_t rx_seqno_pack(const uint64_t v)
1247-
{
1248-
return (rx_seqno_packed_t){
1249-
{ (uint16_t)(v & 0xFFFFU), (uint16_t)((v >> 16U) & 0xFFFFU), (uint16_t)((v >> 32U) & 0xFFFFU) }
1250-
};
1251-
}
1252-
1253-
// The linearizer logic results in a behavior that differs compared to v4 significantly: v4 used to admit any
1254-
// transfer-ID except the same and the one older compared to the last transfer; in terms of the linearization logic it
1255-
// means that the reference point was biased.
1256-
static uint64_t rx_seqno_linearize(const uint64_t ref_seqno, const byte_t transfer_id)
1257-
{
1258-
CANARD_ASSERT(transfer_id <= CANARD_TRANSFER_ID_MAX);
1259-
const byte_t ref_tid = (byte_t)(ref_seqno & CANARD_TRANSFER_ID_MAX);
1260-
int16_t delta = ((int16_t)transfer_id) - ((int16_t)ref_tid); // NOLINT(*-narrowing-conversions)
1261-
// Select the nearest congruent seqno in the half-open interval [-16, +16) around the reference.
1262-
if (delta > (int16_t)(CANARD_TRANSFER_ID_MAX / 2U)) {
1263-
delta -= (int16_t)CANARD_TRANSFER_ID_MODULO;
1264-
} else if (delta < -((int16_t)(CANARD_TRANSFER_ID_MAX / 2U) + 1)) {
1265-
delta += (int16_t)CANARD_TRANSFER_ID_MODULO;
1266-
}
1267-
// Near the origin the backward representative may underflow, so use the next forward one instead.
1268-
if ((delta < 0) && (ref_seqno < (uint64_t)(-delta))) {
1269-
delta += (int16_t)CANARD_TRANSFER_ID_MODULO;
1270-
}
1271-
return (uint64_t)(((int64_t)ref_seqno) + ((int64_t)delta));
1272-
}
1273-
12741232
// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
12751233
// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
12761234
// resulting in 1 KiB o1heap blocks per session, very expensive. Here we use a much less RAM-heavy approach with
@@ -1305,9 +1263,9 @@ typedef struct
13051263
canard_us_t last_admission_ts;
13061264
rx_slot_t* slots[CANARD_PRIO_COUNT]; // Indexed by priority level to allow preemption.
13071265
canard_subscription_t* owner;
1308-
rx_seqno_packed_t seqno_frontier[CANARD_PRIO_COUNT];
1309-
byte_t iface_index;
13101266
byte_t node_id;
1267+
byte_t last_admitted_transfer_id : CANARD_TRANSFER_ID_BIT_LENGTH;
1268+
byte_t iface_index : IFACE_INDEX_BIT_LENGTH;
13111269
} rx_session_t;
13121270
static_assert((sizeof(void*) > 4) || (sizeof(rx_session_t) <= 120), "too large");
13131271

@@ -1375,109 +1333,67 @@ static size_t rx_session_cleanup(rx_session_t* const ses, const canard_us_t now)
13751333
return n_slots;
13761334
}
13771335

1378-
// Maximum seqno seen from the given highest priority level (numerically lowest, inclusive) and down.
1379-
static uint64_t rx_session_seqno_frontier(const rx_session_t* const ses, const canard_prio_t highest_priority)
1380-
{
1381-
uint64_t seqno = 0;
1382-
for (size_t i = (size_t)highest_priority; i < CANARD_PRIO_COUNT; i++) {
1383-
seqno = max_u64(seqno, rx_seqno_unpack(ses->seqno_frontier[i]));
1384-
}
1385-
return seqno;
1386-
}
1387-
13881336
static void rx_session_record_admission(rx_session_t* const ses,
1389-
const canard_prio_t priority,
1390-
const uint64_t seqno,
1337+
const byte_t transfer_id,
13911338
const canard_us_t ts,
13921339
const byte_t iface_index)
13931340
{
1394-
// Seqno per priority cannot go back. When we reset it on a transfer-ID timeout, we simply bump the seqno state by
1395-
// a multiple of transfer-ID overflow periods to ensure monotonicity. Earlier I tried dumb direct approaches
1396-
// where we erase the seqno states to zero on tid-timeout, or introduce epoch counters, or per-seqno timestamp,
1397-
// but this solution is so much simpler while achieves the same goal: leave older seqnos behind, start new epoch.
1398-
CANARD_ASSERT(seqno > rx_seqno_unpack(ses->seqno_frontier[priority]));
1399-
ses->seqno_frontier[priority] = rx_seqno_pack(seqno); // nothing older than this at this & higher prio from now on
1400-
ses->last_admission_ts = ts;
1401-
ses->iface_index = iface_index;
1341+
ses->last_admission_ts = ts;
1342+
ses->last_admitted_transfer_id = transfer_id & CANARD_TRANSFER_ID_MAX;
1343+
ses->iface_index = iface_index & ((1U << IFACE_INDEX_BIT_LENGTH) - 1U);
14021344
}
14031345

1404-
#define RX_SESSION_ADMISSION_REJECTED (UINT64_MAX)
1405-
#define RX_SESSION_ADMISSION_CONTINUATION (UINT64_MAX - 1)
1406-
14071346
// Frame admittance solver. A complex piece, redesigned after v4 to support priority preemption.
1408-
// Key ideas: 1. Separate reassembly state per priority level. 2. TID is linearized into seqno.
1409-
// Once we admit a transfer at some priority with a certain seqno, we know that any older seqno at this or higher
1410-
// priority would be stale, since only higher priority transfers can preempt lower priority ones.
1411-
// On a transfer-ID timeout the seqno is bumped by a full transfer-ID timeout to ensure that it becomes the new
1412-
// frontier matching the current transfer-ID while maintaining monotonicity.
1413-
static uint64_t rx_session_solve_admission(const rx_session_t* const ses,
1414-
const canard_us_t ts,
1415-
const canard_prio_t priority,
1416-
const bool start,
1417-
const bool toggle,
1418-
const byte_t transfer_id,
1419-
const byte_t iface_index)
1347+
static bool rx_session_solve_admission(const rx_session_t* const ses,
1348+
const canard_us_t ts,
1349+
const canard_prio_t priority,
1350+
const bool start,
1351+
const bool toggle,
1352+
const byte_t transfer_id,
1353+
const byte_t iface_index)
14201354
{
14211355
// Continuation frames cannot create new state so their handling is simpler.
14221356
// They are only accepted if there is a slot with an exact match of all transfer parameters.
14231357
// We ignore the transfer-ID timeout to avoid breaking transfers that are preempted for a long time,
14241358
// and especially to allow reassembly of multi-frame transfers even when the transfer-ID timeout is zero.
14251359
if (!start) {
14261360
const rx_slot_t* const slot = ses->slots[priority];
1427-
const bool admit = (slot != NULL) && (slot->transfer_id == transfer_id) && (slot->iface_index == iface_index) &&
1428-
(slot->expected_toggle == toggle);
1429-
return admit ? RX_SESSION_ADMISSION_CONTINUATION : RX_SESSION_ADMISSION_REJECTED;
1361+
return (slot != NULL) && (slot->transfer_id == transfer_id) && (slot->iface_index == iface_index) &&
1362+
(slot->expected_toggle == toggle);
14301363
}
1431-
1432-
// It is best to postpone seqno derivation until the last moment because it is costly.
1433-
// Life would have been so much easier if we could just use normal non-wrapping IDs like we have in Cyphal/UDP!
1434-
const uint64_t frontier_global = rx_session_seqno_frontier(ses, canard_prio_exceptional);
1435-
const uint64_t seqno = rx_seqno_linearize(frontier_global, transfer_id);
1436-
14371364
// Duplicate start frames do not require special treatment because a duplicate frame can only follow the original
14381365
// without any frames belonging to the same transfer in between (see the assumptions). If we get a duplicate start,
14391366
// with a nonzero TID timeout it will be rejected as not-new; even if the timeout is zero, accepting the duplicate
14401367
// will simply restart the slot without loss of information since there were no other frames in between.
14411368
//
1442-
// In the case of a single priority level, the seqno would be considered new if it is greater than last admitted.
1443-
// Priority preemption makes this simple condition insufficient, because a newer higher-priority transfer with
1444-
// a greater seqno may push older lower-priority transfers aside, causing the receiver to observe seqno going
1445-
// backward for new transfers. Our solution is to keep a dedicated seqno frontier per priority level.
1446-
// The lowest bound of a genuinely new seqno would then be located at the lowest priority level, since that level
1447-
// cannot preempt others. To decide if a given seqno is new, one needs to scan all priority levels from the
1448-
// current one (inclusive) down to the lowest level (numerically greater).
1449-
// Higher priority levels (numerically lesser) may have greater seqno frontiers which bear no relevance.
1450-
//
1451-
// The original design had a special case that enabled admittance of a transfer with the next seqno from a
1369+
// The original design had a special case that enabled admittance of a transfer with the next transfer-ID from a
14521370
// different interface if there is no pending reassembly in progress; see details here and the original v4 code:
14531371
// https://github.com/OpenCyphal/libcanard/issues/228. This behavior is no longer present here mostly because we
14541372
// now update the session state only on admittance and not upon completion of a transfer, which changes the logic
14551373
// considerably. One side effect is that even after a timeout (potentially a very long time as long as the session
14561374
// survives) we will still reject a new transfer arriving from a different interface if it happened to roll the
1457-
// same transfer-ID timeout. This is not an issue because we would still accept new transfers on the same iface,
1375+
// same transfer-ID. This is not an issue because we would still accept new transfers on the same iface,
14581376
// and after the RX_SESSION_TIMEOUT the session is destroyed and all new transfers will be accepted unconditionally.
1459-
const uint64_t frontier_priority = rx_session_seqno_frontier(ses, priority);
1460-
const bool seqno_new = seqno > frontier_priority;
1461-
const bool iface_match = ses->iface_index == iface_index;
1462-
const bool timed_out = ts > (ses->last_admission_ts + ses->owner->transfer_id_timeout);
1463-
const bool admit = (seqno_new && iface_match) || (iface_match && timed_out) || (timed_out && seqno_new);
1464-
if (!admit) {
1465-
return RX_SESSION_ADMISSION_REJECTED;
1466-
}
1467-
1468-
// It is vital that seqno is monotonically increasing even across timeouts, otherwise following a transfer-ID
1469-
// timeout further arrivals on higher priority levels may be rejected if their seqnos are greater.
1470-
// Instead of sweeping seqnos across all priority levels, we ensure monotonicity without breaking transfer-ID
1471-
// matching by bumping the seqno by the minimal required number of overflow periods.
1472-
uint64_t admitted_seqno = seqno;
1473-
if (!seqno_new) {
1474-
static const uint64_t mod = CANARD_TRANSFER_ID_MODULO;
1475-
CANARD_ASSERT(frontier_priority >= admitted_seqno);
1476-
const uint64_t periods = ((frontier_priority - admitted_seqno) + mod) / mod;
1477-
admitted_seqno += periods * mod;
1478-
}
1479-
CANARD_ASSERT(admitted_seqno > frontier_priority);
1480-
return admitted_seqno;
1377+
//
1378+
// This design has a problem: suppose the transfer-ID modulo is 4, i.e., the values are {0, 1, 2, 3};
1379+
// a low-priority transfer is sent but preempted by higher-priority peers before it hits the bus.
1380+
// This may potentially be addressed with local transfer-ID unrolling into seqno as illustrated.
1381+
//
1382+
// SENDER SIDE
1383+
// P=low 0
1384+
// P=high 1 2 3 0
1385+
//
1386+
// RECEIVER SIDE
1387+
// P=low 0 <-- rejected as duplicate!
1388+
// P=high 1 2 3 0
1389+
//
1390+
// RECEIVER SIDE [with unrolling]
1391+
// P=low 5 <-- thanks to unrolling we observe this as not a duplicate
1392+
// P=high 1 2 3 4 <-- unrolled
1393+
const bool tid_new = transfer_id != ses->last_admitted_transfer_id;
1394+
const bool iface_match = ses->iface_index == iface_index;
1395+
const bool timed_out = ts > (ses->last_admission_ts + ses->owner->transfer_id_timeout);
1396+
return (tid_new && iface_match) || (iface_match && timed_out) || (timed_out && tid_new);
14811397
}
14821398

14831399
// Returns false on OOM, no other failure modes.
@@ -1514,16 +1430,14 @@ static bool rx_session_update(canard_subscription_t* const sub,
15141430
}
15151431

15161432
// Decide admit or drop.
1517-
const uint64_t seqno = rx_session_solve_admission(
1433+
const bool admit = rx_session_solve_admission(
15181434
ses, ts, frame->priority, frame->start, frame->toggle, frame->transfer_id, iface_index);
1519-
if (seqno == RX_SESSION_ADMISSION_REJECTED) {
1435+
if (!admit) {
15201436
return true; // Rejection is not a failure.
15211437
}
15221438

15231439
// The frame must be accepted. If this is the start of a new transfer, we must update state.
1524-
if (seqno != RX_SESSION_ADMISSION_CONTINUATION) {
1525-
CANARD_ASSERT(seqno <= RX_SEQNO_MAX);
1526-
CANARD_ASSERT(frame->start);
1440+
if (frame->start) {
15271441
// Animate only when a new transfer is started to manage load. Correctness-wise there is not much difference.
15281442
enlist_tail(&sub->owner->rx.list_session_by_animation, &ses->list_animation);
15291443
// Destroy the old slot if it exists (if we're discarding a stale transfer).
@@ -1541,7 +1455,7 @@ static bool rx_session_update(canard_subscription_t* const sub,
15411455
}
15421456
}
15431457
// Register the new state only after we have a confirmation that we have memory to store the frame.
1544-
rx_session_record_admission(ses, frame->priority, seqno, ts, iface_index);
1458+
rx_session_record_admission(ses, frame->transfer_id, ts, iface_index);
15451459
}
15461460

15471461
// TODO acceptance is not yet implemented.

0 commit comments

Comments
 (0)