Architecture Overview
Layered Design
The HardFOC platform follows a strict layered architecture:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌──────────────────────────────────────────────────────┐
│ Application / API │
│ (Vortex.h, managers/*.h) │
├──────────────────────────────────────────────────────┤
│ HANDLERS (hf-core) │
│ As5047uHandler Bno08xHandler Pca9685Handler │
│ Pcal95555Handler NtcTemperatureHandler │
│ Tmc9660Handler Tmc5160Handler Tle92466edHandler │
│ Max22200Handler Ws2812Handler Logger │
├──────────────────────────────────────────────────────┤
│ BASE INTERFACES (internal) │
│ BaseSpi BaseI2c BaseGpio BaseAdc │
│ BaseTemperature BaseEncoder BaseImu │
├──────────────────────────────────────────────────────┤
│ CRTP DEVICE DRIVERS (external) │
│ hf-tmc9660-driver hf-bno08x-driver │
│ hf-as5047u-driver hf-pca9685-driver │
│ hf-pcal95555-driver hf-ntc-thermistor-driver │
│ hf-tmc5160-driver hf-tle92466ed-driver │
│ hf-max22200-driver hf-ws2812-rmt-driver │
├──────────────────────────────────────────────────────┤
│ PLATFORM IMPLEMENTATIONS │
│ EspI2cBus/Device EspSpi EspGpio EspAdc │
│ (hf-internal-interface-wrap/inc/mcu/esp32/) │
└──────────────────────────────────────────────────────┘
Handler Pattern
Each handler follows a consistent pattern:
- Construction — Accepts base interface references (not owned)
- Lazy initialization —
Initialize()orEnsureInitialized()creates the CRTP driver - Thread safety —
RtosMutexprotects all public methods - Error propagation — Maps driver errors to interface error codes
- Zero overhead — CRTP dispatch means no virtual function overhead in the driver layer
Ownership Model
1
2
3
4
5
Manager (owns) → Handler (owns) → CRTP Driver Instance
│
└── References (not owned) → BaseI2c/BaseSpi/BaseGpio
│
└── Platform creates → EspI2c/EspSpi/EspGpio
Key rule: Handlers do NOT own their communication interfaces. The platform layer (or test setup) must ensure interfaces outlive the handler.
RTOS Integration
RtosMutex— Recursive mutex wrapper (xSemaphoreCreateRecursiveMutex), allows same-thread re-entrant locking (e.g.EnsureInitialized()→Initialize())MutexLockGuard— Typedef forRtosUniqueLock<RtosMutex>, RAII lock guardPeriodicTimer— Wrapper around FreeRTOS software timersBaseThread— Abstract base for FreeRTOS tasksOsQueue,OsEventFlags,OsSemaphore— Standard RTOS primitives
All handlers use RtosMutex internally. The MutexLockGuard RAII pattern ensures
deadlock-free operation even on early returns.
Communication Adapters (TMC9660 Example)
The TMC9660 is the most complex handler due to its multi-subsystem architecture:
1
2
3
4
5
6
7
Tmc9660Handler
├── HalSpiTmc9660Comm (CRTP adapter: BaseSpi → SpiCommInterface)
│ └── Tmc9660CtrlPins (RST, DRV_EN, FAULTN, WAKE)
├── SpiDriver (tmc9660::TMC9660<HalSpiTmc9660Comm>)
├── Gpio wrappers (GPIO17, GPIO18 → BaseGpio)
├── Adc wrapper (multi-channel → BaseAdc)
└── Temperature wrapper (chip temp → BaseTemperature)
The visitDriver() template pattern routes calls through a type-erased facade,
keeping the public API non-templated while preserving zero-overhead dispatch internally.
The withDriver() pattern (used by TLE92466ED and MAX22200) is a simplified variant
that acquires the mutex, ensures initialization, and invokes a callable on the driver
in a single atomic step.
Callback Conventions
Base interfaces use raw function pointers with a void* user_data parameter for
hardware-level callbacks. This avoids std::function overhead and heap allocation in
ISR-safe and performance-critical paths:
1
2
3
4
// Typical base interface callback typedef
using InterruptCallback = void(*)(BaseGpio* gpio,
hf_gpio_interrupt_trigger_t trigger,
void* user_data);
Handlers pass non-capturing lambdas (which decay to function pointers) and route the
user_data back to this:
1
2
3
4
5
gpio.RegisterInterrupt(
[](BaseGpio* /*gpio*/, hf_gpio_interrupt_trigger_t /*trigger*/, void* ctx) {
static_cast<MyHandler*>(ctx)->OnInterrupt();
},
this);
Interfaces that carry this pattern: BaseGpio, BaseTemperature, BasePio,
BasePeriodicTimer, BasePwm, BaseCan, and their ESP32 implementations.
Higher-level communication interfaces (BaseBluetooth, BaseWifi, BaseLogger) still
use std::function because their callbacks carry richer event data where the void*
user-data pattern is not sufficient.