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:

  1. Construction — Accepts base interface references (not owned)
  2. Lazy initializationInitialize() or EnsureInitialized() creates the CRTP driver
  3. Thread safetyRtosMutex protects all public methods
  4. Error propagation — Maps driver errors to interface error codes
  5. 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 for RtosUniqueLock<RtosMutex>, RAII lock guard
  • PeriodicTimer — Wrapper around FreeRTOS software timers
  • BaseThread — Abstract base for FreeRTOS tasks
  • OsQueue, 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.