Ownership¶
Who creates each object, who owns it, and how memory is allocated. The short version: the facade owns nothing but references; each manager is a singleton that owns its handlers and bus objects for the life of the program; other managers borrow them.
Figure description and sources
Managers on the left, hf-core handlers in the middle, chip drivers on the right. An arrow means "includes and uses": a manager owns or borrows the handler it points at, and each handler wraps exactly one driver. Several managers share the TMC9660 handler, because that one chip provides motor control, GPIO, ADC channels and a temperature sensor; MotorController owns it and the others reach it through MotorController. LedManager uses its driver directly, without a handler. Managers with no arrow (communication buses, NVS) talk to the interface wrap only.
Drawn from: lib/managers #includes; lib/core/handlers #includes; figure module docs/diagrams/figures/manager-dependencies.py.
Managers, handlers and drivers¶
From the #include lines of
lib/managers and the hf-core handlers; do not edit.
| Manager | Uses managers | hf-core handlers (driver) | Drivers used directly | Interface classes |
|---|---|---|---|---|
AdcManager |
MotorController |
tmc9660 (hf-tmc9660) |
- | BaseAdc |
CommChannelsManager |
- | - | - | BaseCan, BaseI2c, BaseSpi, BaseUart |
EncoderManager |
CommChannelsManager, GpioManager |
as5047u (hf-as5047u) |
- | BaseGpio, BaseSpi |
GpioManager |
CommChannelsManager, MotorController |
pcal95555 (hf-pcal95555), tmc9660 (hf-tmc9660) |
- | BaseGpio |
ImuManager |
CommChannelsManager, GpioManager |
bno08x (hf-bno08x) |
- | BaseGpio, BaseI2c, BaseSpi |
LedManager |
- | - | - | - |
MotorController |
CommChannelsManager, GpioManager |
tmc9660 (hf-tmc9660) |
- | - |
NvsManager |
- | - | - | BaseNvs |
TemperatureManager |
AdcManager, MotorController |
ntc (hf-ntc-thermistor), tmc9660 (hf-tmc9660) |
- | BaseTemperature |
Singletons¶
Every manager is a function-local static (GetInstance(), a Meyers singleton): it is constructed on first use and
never destroyed. Vortex itself is one, and its members (vortex.comms, vortex.motors, ...) are references to
the manager singletons, bound in its constructor. So vortex.motors and MotorController::GetInstance() are the
same object; prefer the facade in application code so the start-up order is respected.
Who owns what¶
| Object | Owner | How other code reaches it |
|---|---|---|
| SPI2 bus and its device handles, I2C bus and devices, UART, TWAI | The board binding (static storage in bsp_bind/esp32c6/); CommChannelsManager holds device indices and bounded runtime I2C slots |
GetSpiDevice(), GetI2cDevice(), GetUartBus(), GetCanBus() return borrowed pointers |
Tmc9660Handler (onboard at index 0, external TMC9660s at indices 2 and 3) |
MotorController (std::array<std::optional<Tmc9660Handler>, 4>) |
handler(index), visitDriver(); GpioManager, AdcManager and TemperatureManager borrow the onboard handler |
Pcal95555Handler |
GpioManager |
Through the expander pins it registers |
| GPIO pin objects | Static storage (see below); GpioManager holds one BaseGpio* per registered pin |
Get(name) / Get(enum) return a BaseGpio*, valid until GpioManager is de-initialised |
Bno08xHandler (onboard at index 0, up to three external) |
ImuManager |
GetBno08xHandler(index) |
As5047uHandler (none by default, up to four) |
EncoderManager |
GetAs5047uHandler(index), VisitDriver(index, fn) |
Tmc9660AdcWrapper |
AdcManager |
Get(name) / Get(enum) return BaseAdc* |
| ESP32 die temperature sensor | The board binding | TemperatureManager reads it by name |
NtcTemperatureHandler, TMC9660 temperature wrapper |
TemperatureManager |
Read calls by sensor name |
WS2812 status LED (Ws2812Handler) |
The board binding | LedManager calls only |
| Three NVS namespaces | The board binding | NvsManager::App(), Calibration(), Config() return BaseNvs& |
Borrowed pointers and references stay valid until the owning manager is de-initialised (Vortex::Shutdown() or the
manager's own Deinitialize()). Do not keep them across a shutdown.
GPIO pins have three owners¶
GpioManager stores every pin as a plain BaseGpio*; no pin object is on the heap or reference counted. The
objects come from three places:
- ESP32 pins: the board binding creates them (
McuGpioOpen(), static storage). - PCAL95555 pins:
GpioManagerkeeps them in a fixed array of 16 and attaches each to the expander handler (Pcal95555Handler::AttachStaticPin()); they are destroyed, after their pointers are cleared, inDeinitialize(). - TMC9660 pins (
TMC_GPIO17,TMC_GPIO18): the TMC9660 handler owns them. They are registered byRegisterTmc9660BridgePinsIfNeeded()once the onboard handler has come up, and removed byUnregisterTmc9660BridgePins(), whichMotorController::Deinitialize()calls before it destroys the handler.
So a pointer from Get() cannot outlive its object: every owner clears the entry before the object goes.
Allocation¶
Device tables are fixed-size arrays sized at compile time (from the pin map or a MAX_*_DEVICES constant), and the
objects in them live in place: handlers in std::optional slots (MotorController, ImuManager,
EncoderManager, AdcManager, TemperatureManager, the PCAL95555 handler), pin objects in fixed arrays, buses in
the board binding's static storage. Nothing in lib/api or lib/managers calls new, make_unique or
make_shared, and no manager stores a std::function. A host test counts operator new during steady-state calls
(LED, GPIO, bus lookups, CollectDiagnostics()) and requires zero. Allocation still happens inside hf-core handlers
and ESP-IDF drivers while they initialise; ADR 0002 records the rule and that
boundary.