Skip to content

Commit e352b90

Browse files
wip
1 parent 9a4c454 commit e352b90

File tree

2 files changed

+152
-68
lines changed

2 files changed

+152
-68
lines changed

.idea/dictionaries/project.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libcanard/canard.c

Lines changed: 150 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ const uint_least8_t canard_len_to_dlc[65] = {
109109

110110
static size_t smaller(const size_t a, const size_t b) { return (a < b) ? a : b; }
111111
static size_t larger(const size_t a, const size_t b) { return (a > b) ? a : b; }
112+
static uint64_t min_u64(const uint64_t a, const uint64_t b) { return (a < b) ? a : b; }
113+
static uint64_t max_u64(const uint64_t a, const uint64_t b) { return (a > b) ? a : b; }
112114
static int64_t min_i64(const int64_t a, const int64_t b) { return (a < b) ? a : b; }
113115
static int64_t max_i64(const int64_t a, const int64_t b) { return (a > b) ? a : b; }
114116
static canard_us_t sooner(const canard_us_t a, const canard_us_t b) { return min_i64(a, b); }
@@ -1171,17 +1173,6 @@ static byte_t rx_parse(const uint32_t can_id,
11711173
return (is_v0 ? 1U : 0U) | (is_v1 ? 2U : 0U);
11721174
}
11731175

1174-
// f(2, 3)==31, f(2, 2)==0, f(2, 1)==1
1175-
static byte_t rx_transfer_id_forward_difference(const byte_t a, const byte_t b)
1176-
{
1177-
CANARD_ASSERT((a <= CANARD_TRANSFER_ID_MAX) && (b <= CANARD_TRANSFER_ID_MAX));
1178-
int16_t diff = (int16_t)(((int16_t)a) - ((int16_t)b));
1179-
if (diff < 0) {
1180-
diff = (int16_t)(diff + (int16_t)(1U << CANARD_TRANSFER_ID_BIT_LENGTH));
1181-
}
1182-
return (byte_t)diff;
1183-
}
1184-
11851176
// Idle sessions are removed after this timeout even if reassembly is not finished.
11861177
// This is not related to the transfer-ID timeout and does not affect the correctness;
11871178
// it is only needed to improve the memory footprint when remotes cease sending messages.
@@ -1193,19 +1184,92 @@ static byte_t rx_transfer_id_forward_difference(const byte_t a, const byte_t b)
11931184
// by a transfer at the priority one higher, and so on up to the maximum.
11941185
#define FOREACH_SLOT(i) for (size_t i = 0; (i) < CANARD_PRIO_COUNT; (i)++)
11951186

1187+
// The seqno is the transfer-ID unrolled at the session layer. It is used to handle transfer-ID wraparound across
1188+
// priority levels. The minimum sufficient bit width is approx. 44, which given 10000 transfers per second will take
1189+
// approx. 55 years to overflow. We use bit fields for aggressive dynamic memory footprint optimization.
1190+
#define RX_SEQNO_BITS (48 - IFACE_INDEX_BIT_LENGTH - 1)
1191+
static_assert(RX_SEQNO_BITS >= 44, "not enough bits for seqno");
1192+
#define RX_SEQNO_MASK ((UINT64_C(1) << RX_SEQNO_BITS) - 1U)
1193+
11961194
// Reassembly state at a specific priority level.
11971195
// Maintaining separate state per priority level allows preemption of higher-priority transfers without loss.
11981196
typedef struct
11991197
{
12001198
canard_us_t start_ts;
1201-
size_t total_size; // The raw payload size before the implicit truncation and CRC removal.
1202-
uint16_t crc;
1203-
byte_t transfer_id : CANARD_TRANSFER_ID_BIT_LENGTH;
1204-
byte_t expected_toggle : 1;
1205-
byte_t iface_index : IFACE_INDEX_BIT_LENGTH;
1206-
byte_t payload[]; // Extent-sized.
1199+
uint64_t seqno : RX_SEQNO_BITS; // Unrolled transfer-ID.
1200+
uint64_t iface_index : IFACE_INDEX_BIT_LENGTH;
1201+
uint64_t expected_toggle : 1;
1202+
uint64_t crc : 16;
1203+
uint32_t total_size; // The raw payload size seen before the implicit truncation and CRC removal.
1204+
byte_t payload[]; // Extent-sized.
12071205
} rx_slot_t;
12081206
#define RX_SLOT_OVERHEAD (offsetof(rx_slot_t, payload))
1207+
static_assert(RX_SLOT_OVERHEAD == 20, "unexpected layout");
1208+
1209+
static rx_slot_t* rx_slot_new(const canard_subscription_t* const sub,
1210+
const canard_us_t start_ts,
1211+
const uint64_t seqno,
1212+
const byte_t iface_index)
1213+
{
1214+
rx_slot_t* const slot = mem_alloc(sub->owner->mem.rx_payload, RX_SLOT_OVERHEAD + sub->extent);
1215+
if (slot != NULL) {
1216+
memset(slot, 0, RX_SLOT_OVERHEAD);
1217+
slot->start_ts = start_ts;
1218+
slot->crc = sub->crc_seed;
1219+
slot->seqno = seqno & RX_SEQNO_MASK;
1220+
slot->expected_toggle = kind_is_v1(sub->kind) ? 1 : 0;
1221+
slot->iface_index = iface_index & ((1U << IFACE_INDEX_BIT_LENGTH) - 1U);
1222+
}
1223+
return slot;
1224+
}
1225+
1226+
static void rx_slot_destroy(const canard_subscription_t* const sub, rx_slot_t* const slot)
1227+
{
1228+
mem_free(sub->owner->mem.rx_payload, RX_SLOT_OVERHEAD + sub->extent, slot);
1229+
}
1230+
1231+
static void rx_slot_write_payload(rx_slot_t* const slot, const size_t extent, const canard_bytes_t payload)
1232+
{
1233+
if (slot->total_size < extent) {
1234+
const size_t copy_size = smaller(payload.size, (size_t)(extent - slot->total_size));
1235+
(void)memcpy(&slot->payload[slot->total_size], payload.data, copy_size);
1236+
}
1237+
slot->total_size = (uint32_t)(slot->total_size + payload.size); // Before truncation.
1238+
}
1239+
1240+
// A compact representation is needed because we need to store an array of these in dynamic memory.
1241+
typedef struct
1242+
{
1243+
uint16_t limbs[3];
1244+
} rx_seqno_packed_t;
1245+
1246+
static uint64_t rx_seqno_unpack(const rx_seqno_packed_t v)
1247+
{
1248+
return ((uint64_t)v.limbs[0]) | (((uint64_t)v.limbs[1]) << 16U) | (((uint64_t)v.limbs[2]) << 32U);
1249+
}
1250+
static rx_seqno_packed_t rx_seqno_pack(const uint64_t v)
1251+
{
1252+
return (rx_seqno_packed_t){
1253+
{ (uint16_t)(v & 0xFFFFU), (uint16_t)((v >> 16U) & 0xFFFFU), (uint16_t)((v >> 32U) & 0xFFFFU) }
1254+
};
1255+
}
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_MAX + 1U);
1264+
} else if (delta < -((int16_t)(CANARD_TRANSFER_ID_MAX / 2U) + 1)) {
1265+
delta += (int16_t)(CANARD_TRANSFER_ID_MAX + 1U);
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_MAX + 1U);
1270+
}
1271+
return (uint64_t)(((int64_t)ref_seqno) + ((int64_t)delta));
1272+
}
12091273

12101274
// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
12111275
// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
@@ -1217,41 +1281,20 @@ typedef struct
12171281
// - Non-start frames never create state.
12181282
// - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
12191283
// - Timeout is consulted only for start-of-transfer admission.
1220-
// - Slot matching for continuation uses exact match: priority, transfer-ID, toggle, and iface.
1284+
// - Slot matching for continuation uses exact match: priority, transfer-ID/seqno, toggle, and iface.
12211285
typedef struct
12221286
{
12231287
canard_tree_t index;
12241288
canard_listed_t list_animation; // On update, session moved to the tail; oldest pushed to the head.
12251289
canard_us_t last_admitted_start_ts;
12261290
rx_slot_t* slots[CANARD_PRIO_COUNT]; // Indexed by priority level to allow preemption.
12271291
canard_subscription_t* owner;
1228-
byte_t last_admitted_transfer_id;
1292+
rx_seqno_packed_t seqno_frontier[CANARD_PRIO_COUNT];
1293+
byte_t iface_index;
12291294
byte_t node_id;
12301295
} rx_session_t;
12311296
static_assert((sizeof(void*) > 4) || (sizeof(rx_session_t) <= 120), "too large");
12321297

1233-
static rx_slot_t* rx_slot_new(const canard_subscription_t* const sub,
1234-
const canard_us_t start_ts,
1235-
const byte_t transfer_id,
1236-
const byte_t iface_index)
1237-
{
1238-
rx_slot_t* const slot = mem_alloc(sub->owner->mem.rx_payload, RX_SLOT_OVERHEAD + sub->extent);
1239-
if (slot != NULL) {
1240-
memset(slot, 0, RX_SLOT_OVERHEAD);
1241-
slot->start_ts = start_ts;
1242-
slot->crc = sub->crc_seed;
1243-
slot->transfer_id = transfer_id & CANARD_TRANSFER_ID_MAX;
1244-
slot->expected_toggle = kind_is_v1(sub->kind) ? 1 : 0;
1245-
slot->iface_index = iface_index & ((1U << IFACE_INDEX_BIT_LENGTH) - 1U);
1246-
}
1247-
return slot;
1248-
}
1249-
1250-
static void rx_slot_destroy(const canard_subscription_t* const sub, rx_slot_t* const slot)
1251-
{
1252-
mem_free(sub->owner->mem.rx_payload, RX_SLOT_OVERHEAD + sub->extent, slot);
1253-
}
1254-
12551298
static int32_t rx_session_cavl_compare(const void* const user, const canard_tree_t* const node)
12561299
{
12571300
return ((int32_t)(*(byte_t*)user)) - ((int32_t)CAVL2_TO_OWNER(node, rx_session_t, index)->node_id);
@@ -1315,13 +1358,61 @@ static size_t rx_session_scan(rx_session_t* const ses, const canard_us_t now)
13151358
return n_slots;
13161359
}
13171360

1318-
static void rx_slot_write_payload(const rx_session_t* const ses, rx_slot_t* const slot, const canard_bytes_t payload)
1361+
static void rx_session_record_admission(rx_session_t* const ses,
1362+
const canard_prio_t priority,
1363+
const uint64_t seqno,
1364+
const canard_us_t ts,
1365+
const byte_t iface_index)
13191366
{
1320-
if (slot->total_size < ses->owner->extent) {
1321-
const size_t copy_size = smaller(payload.size, ses->owner->extent - slot->total_size);
1322-
(void)memcpy(&slot->payload[slot->total_size], payload.data, copy_size);
1367+
ses->seqno_frontier[priority] = rx_seqno_pack(seqno); // nothing older than this at this priority from now on
1368+
ses->last_admitted_start_ts = ts;
1369+
ses->iface_index = iface_index;
1370+
}
1371+
1372+
// Maximum seqno seen from the given highest priority level (numerically lowest, inclusive) and down.
1373+
static uint64_t rx_session_seqno_frontier(const rx_session_t* const ses, const canard_prio_t highest_priority)
1374+
{
1375+
uint64_t seqno = 0;
1376+
for (size_t i = (size_t)highest_priority; i < CANARD_PRIO_COUNT; i++) {
1377+
seqno = max_u64(seqno, rx_seqno_unpack(ses->seqno_frontier[i]));
1378+
}
1379+
return seqno;
1380+
}
1381+
1382+
// Frame admittance state machine update. A complex piece, redesigned after v4 to support priority preemption.
1383+
// Key ideas: 1. Separate reassembly state per priority level. 2. TID is linearized into seqno.
1384+
// Once we admit a transfer at some priority with a certain seqno, we know that any older seqno at this or higher
1385+
// priority would be stale, since only higher priority transfers can preempt lower priority ones.
1386+
static bool rx_session_should_admit(const rx_session_t* const ses,
1387+
const canard_us_t ts,
1388+
const canard_prio_t priority,
1389+
const bool start,
1390+
const bool toggle,
1391+
const uint64_t seqno,
1392+
const byte_t iface_index)
1393+
{
1394+
const rx_slot_t* const slot = ses->slots[priority];
1395+
if (!start) {
1396+
// Continuation not resumed if seqno match is prevented by transfer-ID wraparound, even if the LSb would match.
1397+
// This is to mitigate the risk of frankentransfers, where distinct transfers are stitched together.
1398+
// Such old slots would eventually either time out or be replaced on next start frame.
1399+
return (slot != NULL) && (slot->seqno == seqno) && (slot->iface_index == iface_index) &&
1400+
(slot->expected_toggle == toggle);
13231401
}
1324-
slot->total_size += payload.size; // Before truncation.
1402+
// In the case of a single priority level, the seqno would be considered new if it is greater than last admitted.
1403+
// Priority preemption makes this simple condition insufficient, because a newer higher-priority transfer with
1404+
// a greater seqno may push older lower-priority transfers aside, causing the receiver to observe seqno going
1405+
// backward for new transfers. Our solution is to keep a dedicated seqno frontier per priority level.
1406+
//
1407+
// The lowest bound of a genuinely new seqno would then be located at the lowest priority level, since that level
1408+
// cannot preempt others. To decide if a given seqno is new, one needs to scan all priority levels from the
1409+
// current one (inclusive) down to the lowest level (numerically greater).
1410+
//
1411+
// Higher priority levels (numerically lesser) may have greater seqno frontiers which bear no relevance.
1412+
const bool seqno_new = rx_session_seqno_frontier(ses, priority) >= seqno;
1413+
const bool iface_match = ses->iface_index == iface_index;
1414+
const bool timed_out = ts > (ses->last_admitted_start_ts + ses->owner->transfer_id_timeout);
1415+
return ((slot == NULL) || (slot->seqno < seqno)) && (seqno_new || timed_out) && (iface_match || timed_out);
13251416
}
13261417

13271418
// Returns false on OOM, no other failure modes.
@@ -1333,6 +1424,7 @@ static bool rx_session_update(canard_subscription_t* const sub,
13331424
CANARD_ASSERT((sub != NULL) && (frame != NULL) && (frame->payload.data != NULL) && (ts >= 0));
13341425
CANARD_ASSERT(frame->end || (frame->payload.size >= 7));
13351426

1427+
// Only start frames may create new states.
13361428
rx_session_factory_context_t factory_context = { .owner = sub, .node_id = frame->src };
13371429
rx_session_t* const ses =
13381430
CAVL2_TO_OWNER(frame->start ? cavl2_find_or_insert(&sub->sessions, //
@@ -1348,20 +1440,11 @@ static bool rx_session_update(canard_subscription_t* const sub,
13481440
return !frame->start;
13491441
}
13501442

1351-
// Frame admittance state machine. A highly complex piece, redesigned after v4 to support priority preemption.
1352-
// TID forward difference illustration: f(2,3)==31, f(2,2)==0, f(2,1)==1
1353-
const bool tid_new = rx_transfer_id_forward_difference(ses->last_admitted_transfer_id, frame->transfer_id) > 1;
1354-
const bool timed_out = ts > (ses->last_admitted_start_ts + ses->owner->transfer_id_timeout);
1355-
bool accept = false;
1356-
const rx_slot_t* const slot = ses->slots[frame->priority];
1357-
if (!frame->start) {
1358-
accept = (slot != NULL) && (slot->transfer_id == frame->transfer_id) && (slot->iface_index == iface_index) &&
1359-
(slot->expected_toggle == frame->toggle);
1360-
} else {
1361-
accept = ((slot == NULL) || (slot->transfer_id != frame->transfer_id)) && (tid_new || timed_out);
1362-
}
1363-
if (!accept) {
1364-
return true; // Frame not needed; not a failure to accept.
1443+
// Decide admit or drop.
1444+
const uint64_t seqno =
1445+
rx_seqno_linearize(rx_session_seqno_frontier(ses, canard_prio_exceptional), frame->transfer_id);
1446+
if (!rx_session_should_admit(ses, ts, frame->priority, frame->start, frame->toggle, seqno, iface_index)) {
1447+
return true; // Rejection is not a failure.
13651448
}
13661449

13671450
// The frame must be accepted. If this is the start of a new transfer, we must update state.
@@ -1370,17 +1453,16 @@ static bool rx_session_update(canard_subscription_t* const sub,
13701453
rx_slot_destroy(sub, ses->slots[frame->priority]);
13711454
ses->slots[frame->priority] = NULL;
13721455
if (!frame->end) { // more frames to follow, must store in-progress state
1373-
ses->slots[frame->priority] = rx_slot_new(sub, ts, frame->transfer_id, iface_index);
1456+
ses->slots[frame->priority] = rx_slot_new(sub, ts, seqno, iface_index);
13741457
if (ses->slots[frame->priority] == NULL) {
13751458
sub->owner->err.oom++;
13761459
return false;
13771460
}
13781461
}
1379-
ses->last_admitted_start_ts = ts;
1380-
ses->last_admitted_transfer_id = frame->transfer_id;
1462+
rx_session_record_admission(ses, frame->priority, seqno, ts, iface_index);
13811463
}
13821464

1383-
// TODO acceptance
1465+
// TODO acceptance is not yet implemented.
13841466

13851467
return false;
13861468
}
@@ -1396,10 +1478,10 @@ bool canard_new(canard_t* const self,
13961478
const size_t filter_count,
13971479
canard_filter_t* const filter_storage)
13981480
{
1399-
bool ok = (self != NULL) && (vtable != NULL) && (vtable->now != NULL) && (vtable->tx != NULL) &&
1400-
(vtable->filter != NULL) && mem_valid(memory.tx_transfer) && mem_valid(memory.tx_frame) &&
1401-
mem_valid(memory.rx_session) && mem_valid(memory.rx_payload) &&
1402-
((filter_count == 0U) || (filter_storage != NULL)) && (node_id <= CANARD_NODE_ID_MAX);
1481+
const bool ok = (self != NULL) && (vtable != NULL) && (vtable->now != NULL) && (vtable->tx != NULL) &&
1482+
(vtable->filter != NULL) && mem_valid(memory.tx_transfer) && mem_valid(memory.tx_frame) &&
1483+
mem_valid(memory.rx_session) && mem_valid(memory.rx_payload) &&
1484+
((filter_count == 0U) || (filter_storage != NULL)) && (node_id <= CANARD_NODE_ID_MAX);
14031485
if (ok) {
14041486
(void)memset(self, 0, sizeof(*self));
14051487
self->node_id = (node_id <= CANARD_NODE_ID_MAX) ? node_id : CANARD_NODE_ID_ANONYMOUS;

0 commit comments

Comments
 (0)