Skip to content

feat(dronecan): add Dynamic Node Allocation (DNA) server#11688

Draft
daijoubu wants to merge 127 commits into
iNavFlight:maintenance-10.xfrom
daijoubu:feature/dronecan-dna-server
Draft

feat(dronecan): add Dynamic Node Allocation (DNA) server#11688
daijoubu wants to merge 127 commits into
iNavFlight:maintenance-10.xfrom
daijoubu:feature/dronecan-dna-server

Conversation

@daijoubu

@daijoubu daijoubu commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a non-redundant (single-master) Dynamic Node Allocation (DNA) server per UAVCAN Specification §6.4.9, so peripherals with no configured node ID can request one automatically instead of requiring manual dronecan_node_id assignment on each device.

  • Three-stage UID handshake (6+6+4 bytes) for classic CAN, plus single-frame full-16-byte delivery for transports that can carry the whole UID in one message (e.g. CAN-FD)
  • Top-down node ID assignment starting at 125 (126/127 reserved for network maintenance tools), per spec — minimizes collisions with manually-assigned low IDs
  • Peripheral-requested preferred node ID honoured (search upward from preferred, then downward, then top-down fallback)
  • Persistent allocation table (own Parameter Group, PG_DRONECAN_DNA_SERVER) — same peripheral gets the same node ID across power cycles, saved only while disarmed
  • Live-node conflict detection: if a stored allocation collides with a node actively broadcasting NodeStatus, reassigns rather than corrupting state
  • Guarded by new dronecan_use_dna_server setting (default ON)

Dependencies

⚠️ Stacked on #11607 (DroneCAN H7/F7 driver fix) and #11683 (param-getset/getnodeinfo async slot) — both open, please do not merge until they land. This PR's unique changes are the 32 commits on top of feature/dronecan-param-getset.

Configurator companion PR: iNavFlight/inav-configurator#2672 (enable/disable toggle + node ID validation).

Test plan

  • Build matrix: F4 (MATEKF405), F7 (MATEKF765SE), H7 (KAKUTEH7WING), AT32 (IFLIGHT_BLITZ_ATF435), SITL (-DWARNINGS_AS_ERRORS=ON) — all pass, zero warnings
  • Unit tests: 16/16 pass in dronecan_dna_server_unittest (DNA-1 through DNA-16 — full handshake, UID re-use, table-full rejection, non-broadcast rejection, stage ordering/mismatch rejection, followup timeout reset, FC-node-ID exclusion, preferred-ID honouring including reserved-range and taken-ID fallback, top-down sequential assignment, live-node conflict reassignment, single-frame full-UID delivery, and a differential regression test proving a malformed 4-byte stage-1 message can't poison the accumulator against a legitimate follow-up handshake); dronecan_application_unittest (29/29) unaffected
  • Three independent code-review passes completed, all findings fixed and re-verified (not just re-reviewed on paper): two rounds caught rebase-artifact regressions (a lost 16-bit field mask + comment, a lost static qualifier, a stale doc description of the assignment order and test count), and a third pass caught that two newly-added tests didn't actually depend on the code change they were meant to cover — replaced with a differential test independently confirmed to fail against the pre-fix code and pass against the fix
  • Hardware: flashed to real KAKUTEH7WING, confirmed DNA allocation working end-to-end by the user

Reference

daijoubu/inav #4

daijoubu added 30 commits July 1, 2026 08:02
SJW=8 was overly conservative (80% of bit time at 1Mbps with 10 quanta).
SJW=3 is the standard value also used by the F7 driver.

Tested with 6037 arm/disarm cycles at 500kbps: TEC=0, REC=0, zero errors.
…DCAN

PLL2Q was 3 (266 MHz, invalid for FDCAN ≤ 80 MHz). Fix to 10 (80 MHz).
Extend PLL2 guard from USE_SDCARD_SDIO to USE_SDCARD_SDIO || USE_DRONECAN
so H7 boards with CAN but no SD card get PLL2 configured. Adopt upstream
PLL2M/N formula (VCI=1.6 MHz, VCO=800 MHz) and error check on
HAL_RCCEx_PeriphCLKConfig.
…pport

Remove redundant PeriphClkInitStruct clock config from canardSTM32CAN1_Init.
system_stm32h7xx.c already configures FDCAN to use PLL2Q (80 MHz) when
USE_DRONECAN is defined; duplicating it in the driver overwrites with PLL1.

Also add CAN1 pin definitions and USE_DRONECAN to KAKUTEH7WING target
(PD0/PD1, CAN1_STANDBY PD3 disabled by default).
Use HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN) instead of
HAL_RCC_GetPCLK1Freq() for bit timing calculation. FDCAN is clocked
from PLL2Q (80 MHz) configured in system_stm32h7xx.c; using PCLK1
(100 MHz) produced a ~25% baud rate error causing immediate bus-off.

Restore SJW to 3 for better synchronisation tolerance.
Remove high-frequency LOG_DEBUG messages from GNSS Fix/Fix2/Auxiliary
handlers, onTransferReceived, dronecanInit, and gps_dronecan HDOP path
that fired at 25 Hz and flooded the log.

Fix PLL2 VCO input to target 1.6 MHz (PLL2M = HSE/1600000, PLL2N = 500)
rather than 2.0 MHz, keeping the operating point clearly within
VCIRANGE_0 (1-2 MHz) as the original SDCARD-only code did with PLL2M=5.
VCO output remains 800 MHz; FDCAN (80 MHz via PLL2Q=10) and SDMMC
(200 MHz via PLL2R=4) outputs are unchanged.
Drop high-frequency and verbose-but-low-value LOG_DEBUG(CAN messages:
- dronecan.c: Battery Info (x2), GetNodeInfo, NodeStatus, TX success,
  RX loop, commented-out debug blocks
- canard_stm32h7xx_driver.c: timing computation intermediates
  (Baudrate, Max Quanta, Prescaler BS, Prescaler, Timings summary)
- canard_stm32f7xx_driver.c: same timing intermediates, TX success,
  In CAN Init, commented-out clock and RX blocks

Retain error-path messages (decode failed, TX/RX error, init failures)
and the single-line Prescaler/SJW/BS summary logged at init.
… operation

Tested at 1 Mbps on KAKUTEH7WING hardware and confirmed bus operational.
…ivers

Remove CubeMX boilerplate markers, commented-out dead code, and
development-time question comments from both drivers.
Fix: DroneCAN GNSS messages were being applied to gpsSolDRV regardless
of the configured GPS provider. Guard added in gps_dronecan.c where
it belongs, keeping CAN transport layer unaware of GPS config.
…g difference

F7 bxCAN HAL writes SJW directly to BTR register where hardware adds 1,
so stored value 3 gives 4 tq. This wider SJW is needed for reliable bus
operation on F7 targets and is different from H7 where SJW=1 is actual tq.
Prevents state machine from continuing in INIT state when the CAN
peripheral fails to initialize.
Prevents out-of-bounds access when STATE_DRONECAN_FAILED is active.
Prevents stale pre-bus-off frames from storming the bus on recovery.
With AutoRetransmission=ENABLE, frames that fail on a degraded bus
occupy FIFO slots indefinitely. All 32 slots fill, HAL_FDCAN_AddMessage
returns HAL_ERROR, and all outgoing traffic stalls permanently with no
indication until full bus-off. DroneCAN reliability is handled at the
application layer via periodic republishing.
Matches the H7 driver pattern. Previously the return value was silently
discarded; if timing computation failed, uninitialized stack bytes were
passed to HAL_CAN_Init.
The H7 FDCAN 128x11 recessive-bit recovery sequence takes up to 11.264ms
at 125kbps. The 1ms delay was restarting the counter before it could
complete, preventing the node from ever exiting bus-off. 20ms gives safe
margin above worst-case and allows time to detect immediate re-entry.
Guard against non-DroneCAN GPS provider at the transport boundary
(handle_GNSS* functions) rather than in each leaf function in
gps_dronecan.c. Also adds the guard to handle_GNSSRCTMStream which
had none. Removes stale UNUSED(pgnssAux) and placeholder comment
from dronecanGPSReceiveGNSSAuxiliary.
canardSTM32GetProtocolStatus() was called on every dronecanUpdate()
invocation (~500Hz) to detect bus-off. Moved into the existing 1Hz
task block — bus-off detection latency of up to 1s is acceptable.
Adds LOG_DEBUG to report BusOff and ErrorPassive flags each second
for bench diagnostics.
AutoBusOff=ENABLE handles the 128x11 recovery sequence automatically,
but ESR.BOFF is a sticky read-only flag that is NOT cleared when hardware
recovery completes. GetProtocolStatus() reads this flag, so the state
machine was permanently stuck in STATE_DRONECAN_BUS_OFF after any
bus-off event on F7 targets. Stop/Start re-enters init mode which
clears ESR.BOFF, allowing recovery detection to work correctly.
Comment incorrectly stated '25MHz' as a supported HSE value — 25MHz
fails the assert. CMake always provides HSE_VALUE per-target via
-DHSE_VALUE=<n> so the stm32h7xx_hal_conf.h fallback of 25MHz is never
used. Current targets use 8MHz (default) or 16MHz (KAKUTEH7WING).
…adence

GetProtocolStatus() was called every dronecanUpdate() cycle (~500Hz) in
BUS_OFF state. Moved inside the 20ms recovery timer block so it runs at
the same cadence as RecoverFromBusOff() — still detects recovery within
20ms but reduces MMIO reads from ~500/sec to ~50/sec.
HAL_CAN_Stop/Start called from the scheduler context with CAN interrupts
active caused a full FC lockup. Reverted to empty stub pending investigation
of a safe mechanism to clear the sticky ESR.BOFF flag on F7.
Unconditional 1Hz LOG_DEBUG was flooding the bootlog with healthy status
messages. Now only logs when an error condition is actually present.
HAL_CAN_AddTxMessage returns non-OK when all mailboxes are busy — a
normal transient condition at startup. The log was noise. Matches the
H7 driver which already handles this path silently.
DroneCAN float16 optional fields encode NaN when unpopulated. Without a
guard, NaN * 100 converts to 0 on Cortex-M (ARM VCVT saturation),
permanently blocking the HDOP fallback path. Also passes values through
gpsConstrainHDOP() to prevent uint16_t overflow for extreme DOP values.
…ompatible

gpsSolDRV has hdop but no vdop field. VDOP and EPV are not interchangeable
(different units, conversion requires receiver UERE). lastVDOP was a dead
store with no valid consumer.
…ames size

Guards the CLI state name array against future enum additions — if a new
state is added without updating the array, the build fails immediately.
daijoubu added 26 commits July 5, 2026 14:18
…G_WARNING

Error conditions (decode failures, transmit errors, receive errors, node
table full, anonymous node ID, bus-off permanent fault) were logged at
DEBUG level, making them invisible in normal operation. Raise all to
WARNING so they surface without requiring debug logging enabled.
Bus-off permanent fault raised to ERROR as it disables the CAN bus entirely.
Add bool dronecanUseDNAServer to dronecanConfig_t with a default of ON.
Guard the DNA allocation handler behind the setting so users can disable
plug-and-play node assignment without rebuilding firmware.
Regenerate docs/Settings.md from settings.yaml.
Tests cover the full 3-stage UID accumulation handshake, node ID lookup
and assignment, table-full rejection, non-broadcast source rejection,
stage mismatch and followup timeout handling, and the FC node ID skip.

DNA-9 is a known-failing test documenting the unimplemented preferred
node ID feature (peripheral requests a specific ID in Stage 1).
Capture the requested node_id from Stage 1 (the only stage where the
peripheral sets it) into a static accumulator variable. Pass it to
dnaLookupOrAssignNode at Stage 3 completion; the lookup uses it if the
ID is in the valid range and not already taken, falling back to
sequential assignment otherwise. Reset the accumulator on timeout and
after each completed handshake.

All 9 unit tests pass (DNA-1 through DNA-9).
DroneCAN.md:
- Mark Dynamic Node Assignment as Supported (was Planned)
- Fix setting names: dronecan_bitrate_kbps, correct default node_id=1
- Add dronecan_use_dna_server setting
- Update peripheral setup note: DNA handles node IDs automatically
- Update Example 4 to show DNA workflow
- Add Dynamic Node Allocation section covering the 3-stage handshake,
  preferred node IDs, allocation table persistence, and enable/disable

DroneCAN-Driver.md:
- Update header (date, branch)
- Fix settings table: remove non-existent dronecan_mode/dronecan_baudrate,
  add correct dronecan_bitrate_kbps and dronecan_use_dna_server entries
- Add DNA allocation message to Supported Message Types table
- Add DNA Server section: handshake stages, assignment priority,
  table persistence, entry point, unit test coverage
- Update document history
…the network. If a stored assignment is in use by another node, assign a new id and update the stored value.
- Assign node IDs from top down (125→1); 126-127 reserved for
  network maintenance tools. Add DRONECAN_DNA_MAX_NODE_ID = 125.
- Check live node table (dronecanGetNodeCount/dronecanGetNode) in
  isNodeAvailable so static-ID nodes on the bus are not double-assigned.
- On re-negotiation, detect if a previously stored node ID is now
  in use by another device; if so, fall through to assign a new ID
  and overwrite the existing table entry rather than creating a duplicate.
- Remove assigned flag; write path now returns directly from the
  table slot loop with LOG_ERROR + return 0 for table-full case.
- Add dronecanGetNodeCount/dronecanGetNode stubs to unit test.

All 9 unit tests pass.
dronecan_dna_server.c:
- Wrap uidStr/sprintf block in #ifdef USE_LOG — buffer and loop no
  longer execute in production builds where LOG_DEBUG is a no-op
- Add DNA_STAGE3_UID_LEN named constant and _Static_assert to catch
  UID length constant mismatches at compile time
- Replace raw unsigned subtraction in detectRequestStage with the
  named constant
- Remove misplaced post-response LOG_DEBUG (first_part flag)

dronecan_dna_server.h:
- Wrap contents in #ifdef USE_DRONECAN to prevent hard build errors
  on targets that include this header without DroneCAN support

dronecan_dna_server_unittest.cc:
- Make live node table stubs controllable (mock_node_table/count)
  so DNA-13 can simulate static-ID nodes on the network
- Reset mock node table in SetUp
- Update DNA-8 comment to reflect top-down assignment
- Update DNA-9 comment (feature is implemented, not failing)
- Add DNA-10: first sequential assignment is 125 (top-down)
- Add DNA-11: preferred ID in reserved range (126-127) falls back
- Add DNA-12: preferred ID already taken falls back to sequential
- Add DNA-13: stored ID conflicting with live node → reassigned
  and table entry updated in place

All 13 unit tests pass.
Per UAVCAN spec: when a peripheral requests a preferred node ID, search
upward from that ID first; if nothing is found upward, search downward
from preferred-1. Only fall back to top-down from 125 when no preference
is given or the preferred range is fully exhausted.

Update DNA-12 assertion to verify the upward search direction explicitly
(preferred=60 taken → expect 61, not an arbitrary valid ID).

All 13 unit tests pass.
Replace the implicit 'len < MAX_LENGTH' condition with an exact match
against DNA_STAGE3_UID_LEN so the check remains correct if either
constant is ever changed.
Targets without USE_DRONECAN still compile the file but the header
guard excluded all type definitions, causing build failures on
non-CAN targets (e.g. SPEEDYBEEF405WING).
Remove chatty DEBUG/VERBOSE messages that fired on every allocation
request. Keep INFO for successful assignments, WARNING/ERROR for
failure paths. Add warning when a stored node ID conflict forces
re-assignment. Drop unused stdio.h include.
…etPoolStats

These were present in feature/dronecan-param-getset but were lost when
the first DNA commit's conflict resolution used --theirs for the entire
dronecan.c file. Restoring the variable declaration, its increment on
BUS_OFF entry, and the two getter functions declared in dronecan.h and
called from cli.c. No logic change — pure restoration of param-getset
infrastructure needed for the build to link.
…ution

The first DNA server commit's conflict resolution used --theirs for the
entire dronecan.c, reverting to a pre-masking version. Restore the three
mask/unmask pairs that were present in feature/dronecan-param-getset:

- dronecanAsyncRequest(): around canardRequestOrRespond
- handle_GetNodeInfo(): around canardRequestOrRespond (GetNodeInfo response)
- send_NodeStatus(): around canardBroadcast

Also mask the dronecanDnaHandleAllocation() call site in the broadcast
dispatch (option b — keeps masking out of the user-written DNA server file).
dronecan_dna_server.c calls saveConfig() on disarm to persist node
allocations. Add a no-op stub so the test binary links.

Also add dronecan_dna_server.c to depends and Allocation DSDL source
to extra_sources so the application unittest compiles and links all
DNA server code paths.

All 186 unit tests pass.
…param-getset rebase

The rebase of feature/dronecan-dna-server onto the current
feature/dronecan-param-getset tip discarded two declarations that were
lost as stale-duplicate rebase artifacts: txErrCount (used in
dronecanUpdate/processCanardTxQueue but never declared, volatile since
it's touched from ISR context) and send_NodeStatus's canardBroadcast
return value (bc_res, referenced but never captured). Both are
build-breaking on every target.

Also: dronecan_dna_server.h now includes config/parameter_group.h
directly instead of relying on include order for PG_DECLARE; log
dnaSendResponse's broadcast failures instead of discarding them; and
upgrade NodeStatus broadcast failure logging to LOG_WARNING to match
its sibling failure paths.
…_pool static

Second review pass caught two more regressions reintroduced during the
param-getset rebase: vendor_specific_status_code lost its 16-bit mask
(and explanatory comment) picked up from param-getset's own fix, and
memory_pool lost its static qualifier as a side effect of making canard
extern for the DNA server (memory_pool itself is never referenced
outside this file).

Also: trim trailing whitespace at end of settings.yaml, and correct
docs/DroneCAN-Driver.md's DNA Server section, which described sequential
fallback as "1 upward" when the implementation actually scans top-down
from 125 with a preferred-ID up/down search first; also fixed the
stated unit test count (13, not 9).
FcNodeIdNeverAssigned previously ran 5 handshakes under top-down
assignment starting at 125 -- structurally incapable of ever reaching
down to FC_NODE_ID (5), since the allocation table (32 slots) plus the
live node table (32 slots) can occupy at most 64 IDs, nowhere near the
120 needed to force the scan that far down. The test passed without
ever exercising the skip guard it claimed to cover.

Request FC_NODE_ID as the peripheral's preferred ID instead: the
preferred-ID search starts at the requested value and scans upward, so
skipping the FC's own ID on a clean table must land on ID 6. This
directly and deterministically exercises isNodeAvailable(canard.node_id)
in a single handshake.
… allocation

detectRequestStage() previously accepted first_part_of_unique_id=true
for any of the three structurally-valid unique_id lengths (4, 6, 16),
including a 4-byte length that isn't a legitimate stage-1 length under
either the classic or single-frame delivery scheme. Tighten it to
accept exactly two: 6 bytes (classic multi-frame handshake) or 16 bytes
(the full UID in one message, e.g. a transport like CAN-FD that can
carry it in a single frame). The existing accumulation logic already
handles a single 16-byte stage-1 message correctly -- it completes
allocation immediately since currentUniqueId.len jumps straight to 16.

Add DNA-14 (single-frame full UID completes allocation immediately)
and DNA-15 (the same length without the stage-1 flag is rejected) to
cover both paths. Update docs/DroneCAN-Driver.md to describe the
single-frame delivery option and correct the test count.
…-1 fix

DNA-14 and DNA-15 (added in 9a48d9b) don't actually depend on that
commit's detectRequestStage() fix -- both of their (length, first_part)
combinations were already handled correctly before it, confirmed by
running them against 9a48d9b~1 where they pass unchanged.

Add DNA-16, which reproduces the actual regression the fix guards
against: a malformed first_part=true, len=4 message was previously
accepted as stage 1, setting currentUniqueId.len=4. getExpectedStage(4)
falls through to DNA_INVALID_STAGE (4 is neither 0, nor >=12, nor >=6),
so any legitimate follow-up message within the 500ms followup window
was rejected as a stage mismatch -- silently blocking real allocation
until the timeout reset the accumulator.

Verified as a genuine differential test: fails against
dronecan_dna_server.c from 9a48d9b~1 (pre-fix), passes against the
current fixed version, with no effect on the other 15 tests either way.

Also fix a missing break in DNA-14's table-scan loop for consistency
with the equivalent loop elsewhere in the file, and update the doc test
count to 16.
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Test firmware build ready — commit d427fad

Download firmware for PR #11688

240 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant