Longhorn HAL (LHAL)¶
Platform-independent C++ peripheral interfaces so application code can build for both the MCU and the host — for unit tests, integration tests, and sims — while keeping ST's HAL underneath on real hardware.
application code (App/…)
│ depends only on
▼
//drivers/lhal (pure virtual interfaces, no ST headers)
│
┌────────────┴─────────────┐
▼ ▼
lhal::stm32::* lhal::host::*
adapters over ST HAL in-memory fakes
handles (firmware) (tests & sims)
Interfaces (include/lhal/*.hpp)¶
| Interface | Covers |
|---|---|
lhal::Gpio |
digital read/write/toggle |
lhal::Uart |
blocking + async read/write (DMA/IT on target) |
lhal::I2cMaster |
write / read / write-then-read (register access) |
lhal::CanBus |
send, polled receive, RX callback; classic + CAN FD |
lhal::Clock |
Millis() / DelayMs() (+ wrap-safe ElapsedMs) |
All interfaces are heap-free and exception-free; callbacks are function pointer + context (they may run in ISR context on target).
Using it in a board¶
Application logic lives in its own library against LHAL only (see
//boards/VCU for the reference layout):
cc_library( # MCU flags come from the per-family
name = "vcu_app", # platform/toolchain (//platforms, //toolchains),
deps = ["//drivers/lhal"], # so a plain cc_library links into firmware
... # and host tests alike
)
firmware_project(
name = "vcu",
extra_deps = [":vcu_app", "//drivers/lhal:stm32_headers"],
extra_srcs = ["//drivers/lhal:stm32_srcs"],
...
)
cc_test( # the same app logic, tested on the host
name = "vcu_app_test",
deps = [":vcu_app", "//drivers/lhal:host", "@googletest//:gtest_main"],
)
The board's main.cpp is hand-written (no CubeMX main.c): bring-up stays
at the ST HAL level (clocks, pin mux, peripheral handles + MSP), then wraps
the handles in LHAL adapters and hands them to the app:
lhal::stm32::Uart debug(&huart1); // huart1 configured by board code
lhal::stm32::Can can(&hfdcan1); // filters configured by board code
can.Start();
vcu::App app({.clock = &clock, .can = &can, ...});
while (true) app.Step();
On the host, the same app runs against lhal::host::* fakes: an in-memory
CanNetwork connecting multiple nodes, injectable UART, fake I2C devices
(I2cDevice), and a manually-advanced TestClock.
STM32 backend notes¶
- Adapters wrap handles the board configured (CubeMX-style
MX_*_Initor hand-written). LHAL deliberately does not own peripheral init: pin mux, clocks, and DMA channel wiring stay board code. - Built as sources inside the firmware binary (
stm32_srcs), exactly like the ST HAL sources — so the board's*_hal_conf.hand device define apply. Each adapter is guarded by itsHAL_*_MODULE_ENABLED, so boards that don't enable a module pay nothing. - The family header is auto-detected (
__has_include), so the same backend serves stm32g4/h7/f4/f0 boards. - UART async uses DMA when the handle has a DMA channel linked
(
HAL_LINKDMAin the MSP), interrupt mode otherwise. LHAL defines the globalHAL_UART_*Callback/HAL_FDCAN_RxFifo0Callbackfunctions to route completion — don't define those elsewhere in a board that linksstm32_srcs. - Escape hatch: every adapter exposes
handle(). Peripherals without an LHAL abstraction (SPI, timers, ADC, …) just keep using ST HAL directly — the headers are already on the include path.
Running tests¶
bazel test //drivers/lhal:lhal_host_test # remote (Linux executors)
bazel test --config=local //drivers/lhal:lhal_host_test # this machine
Host tests target Linux on the default (remote) config — see the test
--platforms block in .bazelrc. Host binaries (e.g. :vcu_sim) should
be run with --config=local.
Adding a peripheral abstraction¶
- Interface in
include/lhal/<peripheral>.hpp(pure virtual, heap-free). - STM32 adapter in
include/lhal/stm32/(+stm32/*.cppif it needs callback dispatch), guarded by itsHAL_*_MODULE_ENABLED. - Host fake in
include/lhal/host/with test helpers (inject/inspect). - Tests in
test/lhal_host_test.cpp.