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.