Skip to main content

<alp/cap.h> — Capability Queries

Capability validation is the SDK's core portability idiom: ask the silicon, not the board name. Instead of forking source on #ifdef CONFIG_BOARD_*, portable code asks whether a capability is present and degrades cleanly when it is not.

There are two distinct questions, and the SDK answers them with two different surfaces:

QuestionSurfaceHeader
"Does this silicon have an ADC at all?"ALP_HAS(HW_ADC) (compile-time) / alp_has(ALP_CAP_ID_HW_ADC) (runtime)<alp/cap.h>
"Does this opened ADC handle support DMA?"alp_capabilities_has(caps, ALP_INSTANCE_CAP_DMA)<alp/cap_instance.h>

<alp/cap.h> is the umbrella include — it pulls in both <alp/soc_caps.h> (the generated SoC constants) and <alp/cap_instance.h> (the instance-level types), so a single #include <alp/cap.h> gives you the whole capability surface.

#include <alp/cap.h> /* alp_has() / ALP_HAS(); also pulls in cap_instance.h */

SoC-level: does the silicon have it?

alp_has() reads a generated table for the active CONFIG_ALP_SOC_<...> selection. The same source then runs on every SoM with no per-board forks:

if (!alp_has(ALP_CAP_ID_HW_ADC)) {
printf("[adc] no ADC on this SoC (%s) -- skipping\n", ALP_SOC_REF_STR);
return 0;
}

ALP_HAS(HW_ADC) is the compile-time twin. Because every capability collapses to a constant expression (ALP_SOC_ADC_COUNT > 0), it is legal inside #if and static_assert — use it when the unused branch should be dropped from the binary entirely. alp_has() is the runtime form, useful when the branch must stay linked (e.g. a shared library, or a log line naming the capability).

CallReturns
alp_has(alp_cap_id_t cap)booltrue if the active SoC offers the capability. An out-of-range cap returns false.
alp_cap_name(alp_cap_id_t cap)const char * — the symbolic name ("HW_I2C", "GPU2D", …), or NULL if cap is out of range. Points at static storage.
ALP_HAS(<CAP>)Constant expression. Takes the bare suffix (ALP_HAS(HW_ADC)), not the enum id.

:::caution The two spellings take different arguments alp_has() takes the enum id — alp_has(ALP_CAP_ID_HW_ADC). ALP_HAS() takes the bare suffix and pastes it — ALP_HAS(HW_ADC). They are not interchangeable. :::

Capability ids

alp_cap_id_t covers buses and blocks alike:

GroupIds
Buses / convertersALP_CAP_ID_HW_I2C, HW_I3C, HW_SPI, HW_UART, HW_I2S, HW_PDM, HW_ADC, HW_DAC, HW_CAN, HW_CAN_FD
Timers / systemALP_CAP_ID_HW_RTC, HW_WDT, HW_QENC, HW_TIMER, HW_PWM
Connectivity / displayALP_CAP_ID_HW_ETHERNET, HW_USB, HW_MIPI_CSI, HW_MIPI_DSI, HW_LCDIF
DMA pathsALP_CAP_ID_XSPI_DMA, HEXSPI_DMA, EMMC_DMA, QUADSPI_DMA
ComputeALP_CAP_ID_NPU_DRPAI, HELIUM_MVE, NEON
GraphicsALP_CAP_ID_GPU2D, DAVE2D, DMA2D
CryptoALP_CAP_ID_CRYPTOCELL, INLINE_AES, CAU

ALP_CAP_ID_COUNT bounds the enum.

HW_I3C — new, and deliberately last in the enum

ALP_CAP_ID_HW_I3C gates the I3C controller class. Its derivation follows the usual shape:

#define ALP_CAP_HW_I3C (ALP_SOC_I3C_COUNT > 0)

The id sits at the end of the capability alias table — immediately before ALP_CAP_ID_COUNT, not next to ALP_CAP_ID_HW_I2C where it would read naturally. That placement is deliberate: inserting it in alphabetical position would renumber every ALP_CAP_ID_* after it, and those numbers are ABI. New capability ids are appended, never spliced in.

ALP_SOC_I3C_COUNT on the silicon the SDK currently describes:

SoCALP_SOC_I3C_COUNT
Alif Ensemble E31
Alif Ensemble E42
Alif Ensemble E51
Alif Ensemble E62
Alif Ensemble E71
Alif Ensemble E82
Renesas RZ/V2N (renesas:rzv2n:n44)1
NXP i.MX 930
DEEPX DX-M10
no SoC selectedUINT16_MAX

The NXP i.MX 93 0 is absence of data, not a claim the part has no I3C — see the i.MX 93 caveat below.

:::note With no SoC selected, the capability layer is permissive When no CONFIG_ALP_SOC_<...> is chosen (the default — e.g. a bare native_sim build), every generated constant defaults to UINT16_MAX and ALP_SOC_REF_STR is "unknown". Every alp_has() therefore returns true and every bound check passes. This is deliberate: it lets host builds proceed and rely on open() failing gracefully instead. Apps that want real runtime validation must select a specific SoC. :::

Instance-level: what can this handle do?

SoC-level caps answer "is there an ADC on this die". They cannot answer "does the instance I just opened have a DMA path" — that depends on the backend and the instance. Each peripheral class therefore exposes an alp_<class>_capabilities() call returning a descriptor the backend's probe() populated at open time and cached in the handle:

const alp_capabilities_t *caps = alp_adc_capabilities(adc);
if (alp_capabilities_has(caps, ALP_INSTANCE_CAP_HW_OVERSAMPLE)) {
/* ... */
}

Classes exposing this today include ADC, DAC, and GPU2D. The descriptor pointer is valid for the handle's lifetime; a NULL handle yields NULL.

alp_capabilities_t

FieldTypeNotes
flagsuint32_tBitwise-OR of alp_instance_cap_t values.
max_sample_rateuint32_t0 = not applicable to this class.
max_resolution_bitsuint16_tInstance resolution.
channel_countuint16_tChannels on this instance.

alp_instance_cap_t flags

FlagValueMeaning
ALP_INSTANCE_CAP_DMA1 << 0Instance has a DMA path.
ALP_INSTANCE_CAP_HW_OVERSAMPLE1 << 1Hardware oversampling available.
ALP_INSTANCE_CAP_HW_TRIGGER1 << 2Hardware trigger source available.
ALP_INSTANCE_CAP_DIFFERENTIAL1 << 3Differential input mode available.
CallReturns
alp_capabilities_has(const alp_capabilities_t *c, alp_instance_cap_t f)booltrue if (c->flags & f) is non-zero. Returns false when c is NULL, so it is safe to chain directly off alp_<class>_capabilities() without a NULL check.

<alp/soc_caps.h> — the generated constants

<alp/soc_caps.h> has no functions. It is generated by scripts/gen_soc_caps.py from metadata/socs/{vendor}/{family}/{part}.json and defines the raw per-SoC constants that everything above is derived from:

MacroExample
ALP_SOC_REF_STR"alif:ensemble:e7" — the active silicon reference, also what backend selection matches on
ALP_SOC_<CLASS>_COUNTALP_SOC_ADC_COUNT, ALP_SOC_PWM_COUNT, …
ALP_SOC_ADC_MAX_RESOLUTION_BITS24 on the Alif Ensemble family, 12 on RZ/V2N
ALP_SOC_GPU2D / ALP_SOC_DAVE2D / ALP_SOC_NEONBoolean block-presence flags

ALP_CAP_<NAME> is the portable derivation (#define ALP_CAP_HW_ADC (ALP_SOC_ADC_COUNT > 0)), and ALP_HAS(cap) is just (ALP_CAP_##cap).

Prefer ALP_HAS() / alp_has() in application code — the ALP_SOC_* constants are the generated substrate and are more likely to churn as new silicon is onboarded. Reach for them directly only when you need the actual number (a count or a bit-depth) rather than a yes/no.

The *_open() dispatchers also use these bounds internally: alp_dac_open() rejects a channel >= ALP_SOC_DAC_COUNT with ALP_ERR_INVAL before touching a backend — a portable capability gate that becomes a no-op under the permissive no-SoC default. See Architecture: capability validation.

ALP_CAP_HW_ETHERNET now reads true on V2N / V2M

ALP_SOC_ETHERNET_COUNT for renesas:rzv2n:n44 was 0 and is now 2, so ALP_HAS(HW_ETHERNET) flips from false to true on the V2N and V2M modules.

This was a generator bug, not a hardware change. The count lambda matched only the JSON key ethernet, while n44.json spells it ethernet_1g: 2 — so two real MACs were silently counted as zero. Any code that gated on ALP_HAS(HW_ETHERNET) on V2N/V2M was taking the wrong branch before this fix.

Everything else is unchanged: every Alif Ensemble SoC (E3..E8) is ALP_SOC_ETHERNET_COUNT = 1, and DEEPX DX-M1 is 0.

:::caution NXP i.MX 93 reads 0 — that is absence of data, not absence of hardware imx93.json carries pending_reference_manual_ingestion: true. Its peripheral counts have not been ingested from the reference manual yet, so a 0 there means "not yet described to the SDK". Do not read ALP_SOC_ETHERNET_COUNT == 0 on i.MX 93 as a claim that the part has no Ethernet — no count may be inferred from a key that was never ingested. :::

A true ALP_CAP_HW_ETHERNET also does not imply a portable Ethernet API. There is no <alp/ethernet.h> and no <alp/net.h> — the capability tells you the silicon has MACs, nothing more.

Counts corrected in v0.15.0

Several ALP_SOC_*_COUNT values were wrong in v0.13.0. They are compile-time visible, so a static_assert or an #if written against the old numbers can change behaviour when you upgrade:

SoCMacroOld → New
E4ALP_SOC_I2C_COUNT56
E4ALP_SOC_SPI_COUNT56
E6ALP_SOC_I2C_COUNT56
E6ALP_SOC_SPI_COUNT56
E6ALP_SOC_WDT_COUNT34
E8ALP_SOC_SPI_COUNT56
RZ/V2N (n44)ALP_SOC_ETHERNET_COUNT02

None of these change the yes/no answer from ALP_HAS() except ALP_SOC_ETHERNET_COUNT on n44 — one more reason to gate on the capability rather than on a raw count.

UNVERIFIED provenance markers

Not every generated count is backed by a primary source. The generator now emits a per-SoC comment naming the peripheral keys whose counts are not:

/* UNVERIFIED (count not backed by a primary source): pdm, pdm_lp */

It is driven by a new peripherals_unverified field in metadata/schemas/soc-spec-v1.schema.json, and it is ABI-neutral — comments only. No #define is added, moved, or changed by it.

Currently marked unverified:

SoCUnverified keys
Alif Ensemble E3, E4, E6, E7, E8pdm, pdm_lp
Alif Ensemble E5the full 32-key list (every ingested peripheral key)
NXP i.MX 93(empty — peripherals_unverified: [], so no comment is emitted)

If you are consuming a peripheral count rather than a yes/no capability, read the marker for the SoC you are building for first: on E5 no count carries a primary source today. An empty or absent marker is not a blanket verification claim either — on i.MX 93 it sits alongside pending_reference_manual_ingestion: true, which is the bigger caveat.

ABI status

[ABI-EXPERIMENTAL]<alp/cap.h> carries the marker because it includes <alp/cap_instance.h> (v0.7 new). Both promote to [ABI-STABLE] once at least three vendor families exercise the instance-cap surface.

<alp/soc_caps.h> is [ABI-STABLE] since v0.1 (generated capability constants).

See also

Questions about this page? Discuss in Community Forum