Skip to content

Platform Abstraction Layer

src/pal/Pal.h — zero #ifdef in module code; all platform differences resolved here.

Introduced in Release 4, Sprint 1A.


Why it exists

Before PAL, platform guards were scattered through module files. Every new module author had to know which ESP32 API to call and remember to wrap it. PAL moves that knowledge to one place: modules call pal::millis(), pal::log(), pal::udp_bind(), and so on — the right implementation is selected at compile time inside Pal.h.


Three-way platform switch

Macro Platform Toolchain
ARDUINO ESP32 with Arduino framework PlatformIO / arduino-esp32
IDF_VER ESP32 bare ESP-IDF 5.x / 6.x ESP-IDF CMake
(neither) PC / Raspberry Pi CMake, standard C++17

The pattern inside every PAL function:

inline T pal::foo() {
#if defined(ARDUINO)
    // Arduino ESP32 implementation
#elif defined(IDF_VER)
    // Bare ESP-IDF implementation (or stub returning 0 / false)
#else
    // PC / Raspberry Pi — POSIX / std::chrono / no-op
#endif
}

Function reference

Timing

Function Return Description
pal::micros() int64_t Microseconds since boot. ESP32: esp_timer_get_time(); PC: std::chrono::steady_clock
pal::millis() uint32_t Milliseconds since boot

Memory

Function Return Description
pal::psram_malloc(n) void* Allocates from PSRAM if available, falls back to internal heap; malloc() on PC
pal::psram_free(p) void free() on all platforms

GPIO

Function Description
pal::gpio_write(pin, value) digitalWrite on Arduino, gpio_set_level on IDF, no-op on PC

Logging

Function Description
pal::log(fmt, ...) Serial.printf on Arduino, printf everywhere else

System info

All system-info functions return float (or const char* / write to a caller-supplied buffer). ESP32 values come from ESP-IDF heap / chip APIs; PC returns 0.0f or "" where the concept doesn't apply.

Function Unit IDF PC
cpu_freq_mhz() MHz CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ 0
cpu_cores() count portNUM_PROCESSORS 0
total_heap_kb() KB heap_caps internal 0
free_heap_kb() KB heap_caps internal 0
max_alloc_kb() KB largest free block 0
heap_min_kb() KB minimum free since boot 0
core_temp() °C temperatureRead() (Arduino only) 0
total_psram_kb() KB MALLOC_CAP_SPIRAM 0
free_psram_kb() KB MALLOC_CAP_SPIRAM 0
flash_size_mb() MB esp_flash_get_size 0
flash_speed_mhz() MHz Arduino only 0
sketch_kb() KB Arduino OTA partition 0
free_sketch_kb() KB Arduino OTA partition 0
fs_total_kb() KB LittleFS partition 0
fs_used_kb() KB LittleFS partition 0
reset_reason() int esp_reset_reason() 0
sdk_version() const char* IDF version string ""
chip_model(buf, len) writes string e.g. "ESP32-S3 Rev 2" ""
mac_address(buf, len) writes string efuse MAC "00:00:00:00:00:00"
platform_version() const char* e.g. "arduino-esp32 3.1.0 (esp32dev)" "PC (pc)"

Filesystem

Function Return Description
pal::fs_begin() bool Mount LittleFS. On Arduino: tries without format first, logs before formatting. On IDF: esp_vfs_littlefs_register. On PC: always returns true

Note

board_build.littlefs_version = 2.0 must be set in platformio.ini. Without it PlatformIO's builder writes a v2.1 superblock that the ESP32 Arduino LittleFS driver rejects, silently wiping state (including WiFi credentials) on every boot.

FreeRTOS task helpers

Function Description
pal::task_create_pinned(fn, name, stack, arg, priority, core) xTaskCreatePinnedToCore on ESP32; no-op on PC
pal::yield() vTaskDelay(1) on ESP32; no-op on PC
pal::suspend_forever() vTaskDelay(portMAX_DELAY) on ESP32; while(true){} on PC
pal::reboot() esp_restart() after 100 ms on ESP32; printf no-op on PC

FreeRTOS binary semaphore

Used for core-to-core synchronisation (e.g. effects task signals driver task after blending is done).

Function Return Description
pal::sem_binary_create() void* Allocate a binary semaphore (initially empty); cast to SemaphoreHandle_t internally on ESP32
pal::sem_give(h) void Signal: xSemaphoreGive; no-op on PC
pal::sem_take(h, timeoutMs) bool Wait up to timeoutMs ms; xSemaphoreTake; always returns true on PC
pal::sem_delete(h) void Free the semaphore object; no-op on PC

Pass 100 (ms) rather than portMAX_DELAY so the task watchdog fires if Core 0 stalls.

UDP

Used by DeviceDiscoveryModule and (upcoming) Art-Net. The handle returned by udp_bind is an integer on all platforms — a pool-slot index on Arduino (where WiFiUDP is a stateful object), a raw file descriptor on PC.

Function Return Description
pal::udp_bind(port) int (handle) or -1 Bind a UDP socket for receiving on port
pal::udp_close(handle) void Release the socket
pal::udp_recv(handle, buf, len, from_ip, ip_len) bytes read or 0 Non-blocking read of one packet; populates from_ip
pal::udp_broadcast(port, buf, len) bool Fire-and-forget send to 255.255.255.255:port

Arduino note: a pool of 4 WiFiUDP slots is held in pal::_detail::udp_slot(i) (C++17 inline function with local static — ODR-safe across translation units). udp_bind finds a free slot; the returned index is the handle.

IDF note: all four UDP functions are stubs — discovery is inactive on bare IDF until the driver is wired.


IDF migration path

Most #elif defined(IDF_VER) branches call the same underlying ESP-IDF API that Arduino wraps. The only functions that still return 0 on IDF and need follow-up work are:

Function Reason
core_temp() IDF 5.x temperature sensor requires stateful init/enable/read/disable
flash_speed_mhz() Not exposed via public IDF API
sketch_kb() / free_sketch_kb() Arduino OTA-partition concept; no direct IDF equivalent
UDP functions Stubs — POSIX sockets need wiring under IDF VFS

Source

For design questions about PAL threading, semaphore scope, OS handle abstraction, and cross-platform strategy, see the Developer FAQ.