@@ -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 ;
13121270static_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-
13881336static 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