Skip to content

Commit bed61a9

Browse files
wip
1 parent 9a4c454 commit bed61a9

File tree

2 files changed

+151
-63
lines changed

2 files changed

+151
-63
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: 149 additions & 63 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.
@@ -1198,37 +1189,15 @@ static byte_t rx_transfer_id_forward_difference(const byte_t a, const byte_t b)
11981189
typedef struct
11991190
{
12001191
canard_us_t start_ts;
1201-
size_t total_size; // The raw payload size before the implicit truncation and CRC removal.
1192+
uint32_t total_size; // The raw payload size seen before the implicit truncation and CRC removal.
12021193
uint16_t crc;
12031194
byte_t transfer_id : CANARD_TRANSFER_ID_BIT_LENGTH;
1204-
byte_t expected_toggle : 1;
12051195
byte_t iface_index : IFACE_INDEX_BIT_LENGTH;
1196+
byte_t expected_toggle : 1;
12061197
byte_t payload[]; // Extent-sized.
12071198
} rx_slot_t;
12081199
#define RX_SLOT_OVERHEAD (offsetof(rx_slot_t, payload))
1209-
1210-
// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
1211-
// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
1212-
// resulting in 1 KiB o1heap blocks per session, very expensive. Here we use a much less RAM-heavy approach with
1213-
// sparse nodes in a tree with log-time lookup.
1214-
//
1215-
// Core invariants:
1216-
// - Only start-of-transfer may create/replace a slot.
1217-
// - Non-start frames never create state.
1218-
// - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
1219-
// - Timeout is consulted only for start-of-transfer admission.
1220-
// - Slot matching for continuation uses exact match: priority, transfer-ID, toggle, and iface.
1221-
typedef struct
1222-
{
1223-
canard_tree_t index;
1224-
canard_listed_t list_animation; // On update, session moved to the tail; oldest pushed to the head.
1225-
canard_us_t last_admitted_start_ts;
1226-
rx_slot_t* slots[CANARD_PRIO_COUNT]; // Indexed by priority level to allow preemption.
1227-
canard_subscription_t* owner;
1228-
byte_t last_admitted_transfer_id;
1229-
byte_t node_id;
1230-
} rx_session_t;
1231-
static_assert((sizeof(void*) > 4) || (sizeof(rx_session_t) <= 120), "too large");
1200+
static_assert(RX_SLOT_OVERHEAD <= 16, "unexpected layout");
12321201

12331202
static rx_slot_t* rx_slot_new(const canard_subscription_t* const sub,
12341203
const canard_us_t start_ts,
@@ -1252,6 +1221,84 @@ static void rx_slot_destroy(const canard_subscription_t* const sub, rx_slot_t* c
12521221
mem_free(sub->owner->mem.rx_payload, RX_SLOT_OVERHEAD + sub->extent, slot);
12531222
}
12541223

1224+
static void rx_slot_store(rx_slot_t* const slot, const size_t extent, const canard_bytes_t payload)
1225+
{
1226+
if (slot->total_size < extent) {
1227+
const size_t copy_size = smaller(payload.size, (size_t)(extent - slot->total_size));
1228+
(void)memcpy(&slot->payload[slot->total_size], payload.data, copy_size);
1229+
}
1230+
slot->total_size = (uint32_t)(slot->total_size + payload.size); // Before truncation.
1231+
}
1232+
1233+
// A compact representation is needed because we need to store an array of these in dynamic memory.
1234+
typedef struct
1235+
{
1236+
uint16_t limbs[3];
1237+
} rx_seqno_packed_t;
1238+
1239+
static uint64_t rx_seqno_unpack(const rx_seqno_packed_t v)
1240+
{
1241+
return ((uint64_t)v.limbs[0]) | (((uint64_t)v.limbs[1]) << 16U) | (((uint64_t)v.limbs[2]) << 32U);
1242+
}
1243+
static rx_seqno_packed_t rx_seqno_pack(const uint64_t v)
1244+
{
1245+
return (rx_seqno_packed_t){
1246+
{ (uint16_t)(v & 0xFFFFU), (uint16_t)((v >> 16U) & 0xFFFFU), (uint16_t)((v >> 32U) & 0xFFFFU) }
1247+
};
1248+
}
1249+
static uint64_t rx_seqno_linearize(const uint64_t ref_seqno, const byte_t transfer_id)
1250+
{
1251+
CANARD_ASSERT(transfer_id <= CANARD_TRANSFER_ID_MAX);
1252+
const byte_t ref_tid = (byte_t)(ref_seqno & CANARD_TRANSFER_ID_MAX);
1253+
int16_t delta = ((int16_t)transfer_id) - ((int16_t)ref_tid); // NOLINT(*-narrowing-conversions)
1254+
// Select the nearest congruent seqno in the half-open interval [-16, +16) around the reference.
1255+
if (delta > (int16_t)(CANARD_TRANSFER_ID_MAX / 2U)) {
1256+
delta -= (int16_t)(CANARD_TRANSFER_ID_MAX + 1U);
1257+
} else if (delta < -((int16_t)(CANARD_TRANSFER_ID_MAX / 2U) + 1)) {
1258+
delta += (int16_t)(CANARD_TRANSFER_ID_MAX + 1U);
1259+
}
1260+
// Near the origin the backward representative may underflow, so use the next forward one instead.
1261+
if ((delta < 0) && (ref_seqno < (uint64_t)(-delta))) {
1262+
delta += (int16_t)(CANARD_TRANSFER_ID_MAX + 1U);
1263+
}
1264+
return (uint64_t)(((int64_t)ref_seqno) + ((int64_t)delta));
1265+
}
1266+
1267+
// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
1268+
// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
1269+
// resulting in 1 KiB o1heap blocks per session, very expensive. Here we use a much less RAM-heavy approach with
1270+
// sparse nodes in a tree with log-time lookup.
1271+
//
1272+
// Design goals:
1273+
//
1274+
// - Admit frames from a single interface only, arbitrarily chosen, until it has been observed to be silent for
1275+
// at least one transfer-ID timeout period. This is because redundant interfaces may carry frames with a significant
1276+
// delay, which may cause a receiver to admit the same transfer multiple times without interface affinity.
1277+
//
1278+
// - Allow preemption of transfers by higher-priority ones, without loss of the preempted transfer's state.
1279+
//
1280+
// - The case of a zero transfer-ID timeout is a first-class use case. In this mode, duplication is tolerated by
1281+
// the application, but multi-frame transfers must still follow interface affinity to avoid incorrect reassembly.
1282+
//
1283+
// Core invariants:
1284+
// - Only start-of-transfer may create/replace a slot.
1285+
// - Non-start frames never create state.
1286+
// - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
1287+
// - Timeout is consulted only for start-of-transfer admission.
1288+
// - Slot matching for continuation uses exact match: priority, transfer-ID/seqno, toggle, and iface.
1289+
typedef struct
1290+
{
1291+
canard_tree_t index;
1292+
canard_listed_t list_animation; // On update, session moved to the tail; oldest pushed to the head.
1293+
canard_us_t last_admitted_start_ts;
1294+
rx_slot_t* slots[CANARD_PRIO_COUNT]; // Indexed by priority level to allow preemption.
1295+
canard_subscription_t* owner;
1296+
rx_seqno_packed_t seqno_frontier[CANARD_PRIO_COUNT];
1297+
byte_t iface_index;
1298+
byte_t node_id;
1299+
} rx_session_t;
1300+
static_assert((sizeof(void*) > 4) || (sizeof(rx_session_t) <= 120), "too large");
1301+
12551302
static int32_t rx_session_cavl_compare(const void* const user, const canard_tree_t* const node)
12561303
{
12571304
return ((int32_t)(*(byte_t*)user)) - ((int32_t)CAVL2_TO_OWNER(node, rx_session_t, index)->node_id);
@@ -1315,13 +1362,61 @@ static size_t rx_session_scan(rx_session_t* const ses, const canard_us_t now)
13151362
return n_slots;
13161363
}
13171364

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

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

1431+
// Only start frames may create new states.
13361432
rx_session_factory_context_t factory_context = { .owner = sub, .node_id = frame->src };
13371433
rx_session_t* const ses =
13381434
CAVL2_TO_OWNER(frame->start ? cavl2_find_or_insert(&sub->sessions, //
@@ -1348,20 +1444,11 @@ static bool rx_session_update(canard_subscription_t* const sub,
13481444
return !frame->start;
13491445
}
13501446

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.
1447+
// Decide admit or drop.
1448+
const uint64_t seqno =
1449+
rx_seqno_linearize(rx_session_seqno_frontier(ses, canard_prio_exceptional), frame->transfer_id);
1450+
if (!rx_session_should_admit(ses, ts, frame->priority, frame->start, frame->toggle, seqno, iface_index)) {
1451+
return true; // Rejection is not a failure.
13651452
}
13661453

13671454
// The frame must be accepted. If this is the start of a new transfer, we must update state.
@@ -1376,13 +1463,12 @@ static bool rx_session_update(canard_subscription_t* const sub,
13761463
return false;
13771464
}
13781465
}
1379-
ses->last_admitted_start_ts = ts;
1380-
ses->last_admitted_transfer_id = frame->transfer_id;
1466+
rx_session_record_admission(ses, frame->priority, seqno, ts, iface_index);
13811467
}
13821468

1383-
// TODO acceptance
1469+
// TODO acceptance is not yet implemented.
13841470

1385-
return false;
1471+
return true;
13861472
}
13871473

13881474
// --------------------------------------------- MISC ---------------------------------------------
@@ -1396,10 +1482,10 @@ bool canard_new(canard_t* const self,
13961482
const size_t filter_count,
13971483
canard_filter_t* const filter_storage)
13981484
{
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);
1485+
const bool ok = (self != NULL) && (vtable != NULL) && (vtable->now != NULL) && (vtable->tx != NULL) &&
1486+
(vtable->filter != NULL) && mem_valid(memory.tx_transfer) && mem_valid(memory.tx_frame) &&
1487+
mem_valid(memory.rx_session) && mem_valid(memory.rx_payload) &&
1488+
((filter_count == 0U) || (filter_storage != NULL)) && (node_id <= CANARD_NODE_ID_MAX);
14031489
if (ok) {
14041490
(void)memset(self, 0, sizeof(*self));
14051491
self->node_id = (node_id <= CANARD_NODE_ID_MAX) ? node_id : CANARD_NODE_ID_ANONYMOUS;

0 commit comments

Comments
 (0)