Skip to content

API guide

The interfaces by task: what to call, in what order, and what to do with the result. Every block is compiled by the host build from tests/docs/.

The shape of the API

Fifteen interfaces, each a header in inc/base/, and the classes that implement them:

Interface For ESP32 STM32 Host
BaseAdc Analog inputs: counts and volts, averaging, multi-channel reads EspAdc StmAdc -
BaseBluetooth Bluetooth / BLE: scan and connect EspBluetooth - -
BaseCan CAN bus: classic and FD-sized frames, filters, bus status EspCan StmCan -
BaseGpio Digital pins: direction, polarity, pulls, interrupts EspGpio StmGpio -
BaseI2c One device on an I2C bus: write, read, write-then-read EspI2cDevice StmI2cDevice -
BaseLogger Tagged, levelled log output EspLogger StmLogger -
BaseNvs Key-value non-volatile storage: integers, strings, blobs EspNvs StmNvs HostNvs
BasePeriodicTimer Periodic callback at a microsecond period EspPeriodicTimer StmPeriodicTimer -
BasePio Programmable pulse I/O: symbol trains (WS2812, IR) EspPio - -
BasePwm PWM channels: frequency, duty, phase EspPwm StmPwm -
BaseSpi One device on an SPI bus: full-duplex transfers EspSpiDevice StmSpiDevice -
BaseTemperature On-chip or external temperature sensor EspTemperature StmTemperature -
BaseUart Serial port: write, read, flush, printf EspUart StmUart -
BaseUsbSerialJtag USB Serial/JTAG console EspUsbSerialJtag - -
BaseWifi Wi-Fi station and access point EspWifi - -

Every function on this page takes the interface, so it runs on any of the platforms in the table.

Initialise

// Construction only stores the configuration. Initialise before the first call: most
// operations return *_ERR_NOT_INITIALIZED on an object that was never initialised.
bool bring_up(BaseGpio& pin, BaseSpi& spi) {
  return pin.EnsureInitialized() && spi.EnsureInitialized();
}

EnsureInitialized() returns true at once when the object is already initialised, so calling it at the top of a function is cheap.

Handle errors

// Every interface has an error enum and a constexpr *ErrToString().
void report(hf_spi_err_t err) {
  if (err != hf_spi_err_t::SPI_SUCCESS) {
    const auto text = HfSpiErrToString(err); // std::string_view
    std::printf("SPI: %.*s\n", static_cast<int>(text.size()), text.data());
  }
}

Compare against the success code of the interface (SPI_SUCCESS, I2C_SUCCESS, ...). A few convenience methods return bool instead (BaseI2c::ReadRegister(), the three-argument BaseSpi::Transfer()); use the status-returning form when you need to know why a call failed.

Drive and read pins

BaseGpio works in logical states: SetActive(), SetInactive(), Toggle() and IsActive(bool&) apply the polarity given at construction (HF_GPIO_ACTIVE_HIGH or HF_GPIO_ACTIVE_LOW), so the caller never thinks about electrical levels. SetDirection(), SetOutputMode() (push-pull or open-drain) and SetPullMode() change the configuration at run time. Interrupts are optional: ConfigureInterrupt(), EnableInterrupt() and WaitForInterrupt() return GPIO_ERR_NOT_SUPPORTED on a platform without them.

Talk to an I2C device

A BaseI2c object is one device at one address; the bus is owned by the platform's bus class.

Call Does
Write(data, len, timeout_ms) One write transaction
Read(data, len, timeout_ms) One read transaction
WriteRead(tx, tx_len, rx, rx_len, timeout_ms) Write, repeated start, read: the usual register read
WriteRegister(reg, value) / ReadRegister(reg, value) One-byte register helpers, return bool
GetDeviceAddress() The device's address

See the Quickstart for a complete register read.

Transfer over SPI

// One full-duplex transfer: chip select is driven by the device for the whole call.
hf_spi_err_t read_id(BaseSpi& spi, hf_u8_t (&rx)[4]) {
  const hf_u8_t tx[4] = {0x9F, 0x00, 0x00, 0x00}; // JEDEC "read ID"
  return spi.Transfer(tx, rx, sizeof(tx), /*timeout_ms=*/10);
}

Transfer() is full duplex; pass nullptr for rx to write only (Write()), or for tx to read only (Read()). TransferChain() sends several equal-length frames with a gap between them, for daisy-chained parts.

Read an analog input

// Average 8 samples of one channel, in volts.
bool read_bus_voltage(BaseAdc& adc, hf_channel_id_t channel, float& volts) {
  return adc.ReadChannelV(channel, volts, /*numOfSamplesToAvg=*/8) == hf_adc_err_t::ADC_SUCCESS;
}

ReadChannelCount() returns raw counts; ReadMultipleChannels() reads several channels in one call.

Set a PWM duty cycle

// 25 % duty on one channel; the frequency was set when the channel was configured.
bool set_fan(BasePwm& pwm, hf_channel_id_t channel) {
  return pwm.EnableChannel(channel) == hf_pwm_err_t::PWM_SUCCESS &&
         pwm.SetDutyCycle(channel, 0.25F) == hf_pwm_err_t::PWM_SUCCESS;
}

Duty is a fraction from 0.0 to 1.0. SetDutyCycleRaw() takes timer counts; StartAll(), StopAll() and UpdateAll() act on every channel at once.

Use a serial port

// Write a line, then read whatever has arrived without waiting.
hf_u16_t echo(BaseUart& uart, hf_u8_t* buf, hf_u16_t size) {
  const hf_u8_t hello[] = {'h', 'i', '\n'};
  if (uart.Write(hello, sizeof(hello), /*timeout_ms=*/10) != hf_uart_err_t::UART_SUCCESS) {
    return 0;
  }
  hf_u16_t n = uart.BytesAvailable();
  if (n > size) {
    n = size;
  }
  return (n > 0U && uart.Read(buf, n, 0) == hf_uart_err_t::UART_SUCCESS) ? n : 0U;
}

Store settings

// Count boots in non-volatile storage.
hf_u32_t count_boot(BaseNvs& nvs) {
  hf_u32_t boots = 0;
  (void)nvs.GetU32("boots", boots); // NVS_ERR_KEY_NOT_FOUND on first boot: keep 0
  ++boots;
  if (nvs.SetU32("boots", boots) == hf_nvs_err_t::NVS_SUCCESS) {
    (void)nvs.Commit();
  }
  return boots;
}

SetString() / GetString() and SetBlob() / GetBlob() store text and binary values; GetMaxKeyLength() and GetMaxValueSize() give the platform limits. Commit() writes pending changes to storage. The host build has HostNvs, an in-memory implementation for tests.

Run something periodically

// Call a function every millisecond. The callback runs in the timer's context: keep it short.
void on_tick(void* user_data) {
  auto* ticks = static_cast<hf_u32_t*>(user_data);
  ++*ticks;
}

bool start_1khz(BasePeriodicTimer& timer, hf_u32_t& ticks) {
  return timer.SetCallback(on_tick, &ticks) == hf_timer_err_t::TIMER_SUCCESS &&
         timer.Start(/*period_us=*/1000) == hf_timer_err_t::TIMER_SUCCESS;
}

GetMinPeriod(), GetMaxPeriod() and GetResolution() give the platform limits in microseconds.

Send and receive CAN frames

CAN has its own page: CAN.

Next: CAN, or Porting to implement these interfaces on a new MCU.