Skip to content

Porting

How to bring the interfaces to a new MCU: what to implement, in what order, and how to check it.

What a port is

A port is a folder inc/mcu/<platform>/ plus src/mcu/<platform>/ with one class per interface you need. Each class derives from a Base* class and implements its pure virtual methods with the vendor SDK. Nothing in inc/base/ or in the code above it changes. Start with the interfaces your board's drivers use (usually GPIO, SPI, I2C, UART, then CAN, ADC, PWM and a timer), not all fifteen.

The methods to implement

Interface Methods a port must implement
BaseAdc (7) Initialize(), Deinitialize(), GetMaxChannels(), IsChannelAvailable(), ReadChannelV(), ReadChannelCount(), ReadChannel()
BaseBluetooth (45) Initialize(), Deinitialize(), IsInitialized(), Enable(), Disable(), IsEnabled(), SetMode(), GetMode(), GetLocalAddress(), SetDeviceName(), GetDeviceName(), ConfigureClassic(), SetDiscoverable(), IsDiscoverable(), ConfigureBle(), StartAdvertising(), StopAdvertising(), IsAdvertising(), StartScan(), StopScan(), IsScanning(), GetDiscoveredDevices(), ClearDiscoveredDevices(), Connect(), Disconnect(), IsConnected(), GetConnectedDevices(), Pair(), Unpair(), IsPaired(), GetPairedDevices(), SendData(), GetAvailableData(), ReadData(), DiscoverServices(), DiscoverCharacteristics(), ReadCharacteristic(), WriteCharacteristic(), SubscribeCharacteristic(), GetState(), GetRssi(), RegisterEventCallback(), UnregisterEventCallback(), RegisterDataCallback(), UnregisterDataCallback()
BaseCan (8) Initialize(), Deinitialize(), SendMessage(), ReceiveMessage(), SetReceiveCallback(), SetAcceptanceFilter(), GetStatus(), Reset()
BaseGpio (12) SetDirectionImpl(), GetDirectionImpl(), SetOutputModeImpl(), GetOutputModeImpl(), SetPullModeImpl(), GetPullModeImpl(), SetPinLevelImpl(), GetPinLevelImpl(), IsPinAvailable(), GetMaxPins(), GetDescription(), Initialize()
BaseI2c (6) Initialize(), Deinitialize(), Write(), Read(), WriteRead(), GetDeviceAddress()
BaseLogger (26) Initialize(), Deinitialize(), IsInitialized(), EnsureInitialized(), SetLogLevel(), GetLogLevel(), Error(), Warn(), Info(), Debug(), Verbose(), Log(), LogV(), LogWithLocation(), Flush(), IsLevelEnabled(), GetStatistics(), GetDiagnostics(), ResetStatistics(), ResetDiagnostics(), IsHealthy(), GetLastError(), GetLastErrorMessage(), PrintStatistics(), PrintDiagnostics(), PrintStatus()
BaseNvs (15) Initialize(), Deinitialize(), SetU32(), GetU32(), SetString(), GetString(), SetBlob(), GetBlob(), EraseKey(), Commit(), KeyExists(), GetSize(), GetDescription(), GetMaxKeyLength(), GetMaxValueSize()
BasePeriodicTimer (12) Initialize(), Deinitialize(), Start(), Stop(), SetPeriod(), GetPeriod(), GetStats(), ResetStats(), GetDescription(), GetMaxPeriod(), GetMinPeriod(), GetResolution()
BasePio (14) Initialize(), Deinitialize(), ConfigureChannel(), Transmit(), StartReceive(), StopReceive(), IsChannelBusy(), GetChannelStatus(), GetCapabilities(), SetTransmitCallback(), SetReceiveCallback(), SetErrorCallback(), ClearChannelCallbacks(), ClearCallbacks()
BasePwm (15) Initialize(), Deinitialize(), EnableChannel(), DisableChannel(), IsChannelEnabled(), SetDutyCycle(), SetDutyCycleRaw(), SetFrequency(), SetPhaseShift(), StartAll(), StopAll(), UpdateAll(), SetComplementaryOutput(), GetDutyCycle(), GetFrequency()
BaseSpi (4) Initialize(), Deinitialize(), Transfer(), GetDeviceConfig()
BaseTemperature (5) Initialize(), Deinitialize(), ReadTemperatureCelsiusImpl(), GetSensorInfo(), GetCapabilities()
BaseUart (8) Initialize(), Deinitialize(), Write(), Read(), BytesAvailable(), FlushTx(), FlushRx(), Printf()
BaseUsbSerialJtag (6) Initialize(), Deinitialize(), Write(), Read(), FlushTx(), IsHostConnected()
BaseWifi (30) Initialize(), Deinitialize(), IsInitialized(), SetMode(), GetMode(), ConfigureStation(), Connect(), Disconnect(), IsConnected(), GetRssi(), GetIpInfo(), ConfigureAccessPoint(), StartAccessPoint(), StopAccessPoint(), IsAccessPointActive(), GetConnectedStationCount(), StartScan(), GetScanResults(), IsScanning(), GetState(), GetConnectedSsid(), GetConnectedBssid(), SetPowerSave(), GetPowerSave(), RegisterEventCallback(), UnregisterEventCallback(), GetMacAddress(), SetMacAddress(), GetChannel(), SetChannel()

Everything else in an interface has a working default in the base class: lazy initialisation, polarity handling, statistics, batch helpers, and "not supported" returns for optional features. Override those only to do better (for example, a hardware TransferChain() for SPI, or GetBusStatus() for CAN).

A complete GPIO port

The smallest complete implementation, compiled by the host build from tests/docs/port_gpio.cpp. BaseGpio turns SetActive() into SetPinLevelImpl(HIGH) or LOW depending on the polarity, and validates the object before calling the port.

#include "BaseGpio.h"

// A GPIO for a new MCU. BaseGpio does the polarity, validation and statistics;
// the port only moves the pin.
class MyMcuGpio : public BaseGpio {
public:
  explicit MyMcuGpio(
      hf_pin_num_t pin, hf_gpio_direction_t dir = hf_gpio_direction_t::HF_GPIO_DIRECTION_OUTPUT,
      hf_gpio_active_state_t active = hf_gpio_active_state_t::HF_GPIO_ACTIVE_HIGH) noexcept
      : BaseGpio(pin, dir, active) {}

  bool Initialize() noexcept override {
    // Configure the pin's mux, direction and pulls with the vendor SDK.
    return SetDirectionImpl(current_direction_) == hf_gpio_err_t::GPIO_SUCCESS;
  }

  bool IsPinAvailable() const noexcept override {
    return pin_ >= 0 && pin_ < GetMaxPins();
  }
  hf_u8_t GetMaxPins() const noexcept override {
    return 32; // pins on this MCU
  }
  const char* GetDescription() const noexcept override {
    return "MyMcu GPIO";
  }

protected:
  hf_gpio_err_t SetDirectionImpl(hf_gpio_direction_t d) noexcept override {
    direction_ = d;
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_err_t GetDirectionImpl(hf_gpio_direction_t& d) const noexcept override {
    d = direction_;
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_err_t SetOutputModeImpl(hf_gpio_output_mode_t m) noexcept override {
    mode_ = m;
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_err_t GetOutputModeImpl(hf_gpio_output_mode_t& m) const noexcept override {
    m = mode_;
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_err_t SetPullModeImpl(hf_gpio_pull_mode_t p) noexcept override {
    pull_ = p;
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_pull_mode_t GetPullModeImpl() const noexcept override {
    return pull_;
  }
  hf_gpio_err_t SetPinLevelImpl(hf_gpio_level_t level) noexcept override {
    level_ = level; // write the output data register here
    return hf_gpio_err_t::GPIO_SUCCESS;
  }
  hf_gpio_err_t GetPinLevelImpl(hf_gpio_level_t& level) noexcept override {
    level = level_; // read the input data register here
    return hf_gpio_err_t::GPIO_SUCCESS;
  }

private:
  hf_gpio_direction_t direction_{hf_gpio_direction_t::HF_GPIO_DIRECTION_INPUT};
  hf_gpio_output_mode_t mode_{hf_gpio_output_mode_t::HF_GPIO_OUTPUT_MODE_PUSH_PULL};
  hf_gpio_pull_mode_t pull_{hf_gpio_pull_mode_t::HF_GPIO_PULL_MODE_FLOATING};
  hf_gpio_level_t level_{hf_gpio_level_t::HF_GPIO_LEVEL_LOW};
};

Rules for an implementation

Rule Why
Constructors store configuration only; Initialize() touches hardware and sets initialized_ Objects are created as globals and members before the scheduler runs
Return the interface's error codes, never vendor codes Callers compare against *_SUCCESS and the *_ERR_* list
Every method is noexcept; no exceptions, no RTTI The component builds with -fno-exceptions -fno-rtti
Allocate in Initialize(), never in an interrupt Interrupt handlers must not block or allocate
Guard shared state with PlatformMutex It becomes a real mutex when a backend is configured (see Architecture)
Callbacks from interrupts get copies, not pointers into driver buffers The buffer may be reused before the callback's work runs
timeout_ms = 0 means "do not wait" BaseCan::TrySendMessage() and polling receives rely on it

Wire it into the build

  1. Add HF_TARGET_MCU_<NAME> and its family block to inc/utils/McuSelect.h.
  2. Add a monotonic time source inc/mcu/<platform>/<X>Time.h and its #elif in inc/utils/HfTime.h.
  3. Add a PlatformMutexBackend.h for the platform's RTOS where the stack is assembled (hf-core does this for FreeRTOS).
  4. Build the sources with the platform's toolchain; the ESP-IDF component in the root CMakeLists.txt is the model.

Check a port

Check How
It compiles against the interface Instantiate every class once, as port_gpio.cpp does; an unimplemented pure virtual is a compile error
Lifecycle Call each method before Initialize() (expect *_ERR_NOT_INITIALIZED), then after
Polarity An active-low BaseGpio: SetActive() must drive the pin low
CAN Loopback mode, then TrySendMessage() until CAN_ERR_QUEUE_FULL, then GetBusStatus()
On hardware Port the matching ESP32 test application from examples/esp32/main

The ESP32 classes are the reference implementation; the STM32 classes show the CubeMX-handle style.

Next: Utilities.