Cross-EVK Portability — <alp/board.h> and BOARD_* Aliases
One example, two EVKs. The <alp/board.h> facade plus the portable BOARD_* aliases let the same firmware source build unchanged for both official EVKs — the E1M EVK (35 × 35) and the E1M-X EVK (45 × 65) — for every interface the e1m-spec declares as a common role (STANDARD.md §7.2).
How it works
Three pieces, all generated from board.yaml:
-
ALP_BOARD_<SLUG>compile define — the build emitsALP_BOARD_E1M_EVKorALP_BOARD_E1M_X_EVKfrom theboard.yamlpreset. -
<alp/board.h>facade — selects the active board's generated routes header from that define:#include <alp/board.h> /* resolves to alp_e1m_evk_routes.h oralp_e1m_x_evk_routes.h at compile time */Building without any
ALP_BOARD_*define is a compile error with a pointed message — set the example'spreset:, or pass-DALP_BOARD_E1M_EVK/-DALP_BOARD_E1M_X_EVKby hand. -
BOARD_*aliases — each board's routes header defines the sameBOARD_*names, mapped to that board's own macros. The mapping comes from theboard_alias:field one1m_routes:entries in the board definition; parity across boards is CI-enforced, so an alias can never silently exist on one EVK only.
#include <alp/board.h>
alp_pwm_t *led = alp_pwm_open(BOARD_PWM_LED_RED, &cfg); /* builds on both EVKs */
Alias catalogue
| Alias | Role | E1M EVK | E1M-X EVK |
|---|---|---|---|
BOARD_UART_DEBUG | Console UART | EVK_UART_PORT_DEBUG | XEVK_UART_PORT_DEBUG |
BOARD_UART_ARDUINO | Arduino UNO UART | EVK_UART_PORT_ARDUINO | XEVK_UART_PORT_ARDUINO |
BOARD_I2C_SENSORS | Shared sensor + IO-expander bus | EVK_I2C_BUS_SENSORS | XEVK_I2C_BUS_SENSORS |
BOARD_SPI_ARDUINO | Arduino UNO SPI | EVK_SPI_BUS_ARDUINO | XEVK_SPI_BUS_ARDUINO |
BOARD_CAN0 | CAN-FD bus | EVK_CAN_VEHICLE_BUS | XEVK_CAN_BUS0 |
BOARD_DAC0 | Arduino DAC output | EVK_DAC_ARDUINO_DAC0 | XEVK_DAC0 |
BOARD_DAC1 | Audio line out | EVK_DAC_AUDIO_LINE_OUT | XEVK_DAC1 |
BOARD_I2S_AUDIO | Audio codec / amp bus | EVK_I2S_AUDIO_CODEC | XEVK_I2S_AUDIO |
BOARD_PWM_LED_RED / _GREEN / _BLUE | RGB status LED PWM | EVK_PWM_LED_* | XEVK_PWM_LED_* |
BOARD_PIN_LED_RED / _GREEN / _BLUE | RGB status LED GPIO | EVK_PIN_LED_* | XEVK_PIN_LED_* |
BOARD_PWM_ARD1..3 | Arduino PWM headers | EVK_ARD_PWM1..3 | XEVK_ARD_PWM1..3 |
BOARD_ENC_ROTARY | Rotary encoder (PEC12R) | EVK_ENC_ROTARY | XEVK_ENC_ROTARY |
BOARD_PIN_ENCODER_SW | Encoder push switch | EVK_PIN_ENCODER_SW | XEVK_PIN_ENCODER_SW |
BOARD_PIN_BMI323_INT1 | BMI323 data-ready | EVK_PIN_BMI323_INT1 | XEVK_PIN_BMI323_INT1 |
Declaring portability: supported_boards:
A portable example declares the presets it is verified to build for at the top level of its board.yaml:
supported_boards: [e1m-evk, e1m-x-evk]
CI builds one scenario per entry, so portability stays proven rather than claimed. When omitted, the list defaults to the single preset:. This is for examples — per ADR-0011 application firmware still targets one SoM family.
The portability lint
Portability is enforced, not just encouraged. scripts/check_example_portability.py runs over every example that has a board.yaml and is a hard gate in the pr, full, and release quality profiles — so a portability regression fails CI rather than landing quietly.
Its headline rule: a customer-facing example must not #include <zephyr/drivers/...> directly. That's the vendor driver-class layer the portable <alp/*.h> surfaces exist to hide — reach for <alp/display.h>, <alp/pwm.h>, <alp/i2c.h> and friends instead. (Commented-out includes don't trip it; the checker strips comments before matching.)
The other rules it enforces:
| Rule | Severity | What it checks |
|---|---|---|
| Zephyr driver includes | error | No direct #include <zephyr/drivers/*.h> |
| Chip family match | error | Every chips: entry has a manifest whose families: includes the example's own SoM family |
supported_boards: parity | error | Each entry has a matching ALP_BOARD_<SLUG> Twister variant in testcase.yaml |
| Pinout namespace | error | No mixing <alp/e1m_pinout.h>/ALP_E1M_* with <alp/e1m_x_pinout.h>/ALP_E1M_X_* |
| Chip includes declared | error | An <alp/chips/X.h> include has a matching chips: entry |
| Ring classification | info | Reports each example as ring1-cross-family / ring2-chip-bound / ring3-som-bound |
Rings 2 and 3 are an accepted, intentional category — not migration debt. A chip-bound or SoM-bound example is a legitimate thing to write; the classification exists to make the distribution visible, not to shame it.
The allowlist is reasoned, not a mute button
A handful of examples genuinely have no portable surface to route through yet. Those are named individually with the reason — never silently exempted:
| Example | Why it's exempt |
|---|---|
multicore/rpmsg-v2n | mbox.h — raw OpenAMP/MHU mailbox transport for AMP core-to-core messaging; no portable <alp/*.h> IPC surface exists yet |
peripheral-io/alp-console | pwm.h (RGB status LED) + gpio.h/pinctrl.h (the on-module Wi-Fi/BLE bridge's own control-transport HAL); a pre-existing gap, with the LED path tracked as separate follow-up work |
v2n/v2n-ethernet-dual | mdio.h — raw PHY register access for a link-diagnostics demo; no portable MDIO surface exists |
v2n/v2n-xspi-flash-readwrite | flash.h — no portable <alp/flash.h> surface exists yet |
Each entry states why it isn't a migration target. A new entry is only appropriate with a real "no portable surface exists" reason — not as a way to silence the gate.
Scope: only directories containing a board.yaml are checked, at one or two levels under examples/. Zephyr-specific register/bench tools that carry no board.yaml (e.g. examples/aen/*-regcheck/) are outside this check by construction.
When NOT to use the facade
Form-factor-specific firmware (anything touching pads that exist on only one EVK) should include the specific routes header directly — alp_e1m_evk_routes.h / alp_e1m_x_evk_routes.h — and use the EVK_* / XEVK_* macros. The facade exists for the common-role subset, not as a universal indirection.
See also
board.yamlreference —board_alias:andsupported_boards:fields- Peripheral I/O — the
E1M_*portable pin layer underneath - EVK page — what each EVK populates