<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:
| Question | Surface | Header |
|---|---|---|
| "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.
Header
#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).
| Call | Returns |
|---|---|
alp_has(alp_cap_id_t cap) | bool — true 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:
| Group | Ids |
|---|---|
| Buses / converters | ALP_CAP_ID_HW_I2C, HW_I3C, HW_SPI, HW_UART, HW_I2S, HW_PDM, HW_ADC, HW_DAC, HW_CAN, HW_CAN_FD |
| Timers / system | ALP_CAP_ID_HW_RTC, HW_WDT, HW_QENC, HW_TIMER, HW_PWM |
| Connectivity / display | ALP_CAP_ID_HW_ETHERNET, HW_USB, HW_MIPI_CSI, HW_MIPI_DSI, HW_LCDIF |
| DMA paths | ALP_CAP_ID_XSPI_DMA, HEXSPI_DMA, EMMC_DMA, QUADSPI_DMA |
| Compute | ALP_CAP_ID_NPU_DRPAI, HELIUM_MVE, NEON |
| Graphics | ALP_CAP_ID_GPU2D, DAVE2D, DMA2D |
| Crypto | ALP_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:
| SoC | ALP_SOC_I3C_COUNT |
|---|---|
| Alif Ensemble E3 | 1 |
| Alif Ensemble E4 | 2 |
| Alif Ensemble E5 | 1 |
| Alif Ensemble E6 | 2 |
| Alif Ensemble E7 | 1 |
| Alif Ensemble E8 | 2 |
Renesas RZ/V2N (renesas:rzv2n:n44) | 1 |
| NXP i.MX 93 | 0 |
| DEEPX DX-M1 | 0 |
| no SoC selected | UINT16_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
| Field | Type | Notes |
|---|---|---|
flags | uint32_t | Bitwise-OR of alp_instance_cap_t values. |
max_sample_rate | uint32_t | 0 = not applicable to this class. |
max_resolution_bits | uint16_t | Instance resolution. |
channel_count | uint16_t | Channels on this instance. |
alp_instance_cap_t flags
| Flag | Value | Meaning |
|---|---|---|
ALP_INSTANCE_CAP_DMA | 1 << 0 | Instance has a DMA path. |
ALP_INSTANCE_CAP_HW_OVERSAMPLE | 1 << 1 | Hardware oversampling available. |
ALP_INSTANCE_CAP_HW_TRIGGER | 1 << 2 | Hardware trigger source available. |
ALP_INSTANCE_CAP_DIFFERENTIAL | 1 << 3 | Differential input mode available. |
| Call | Returns |
|---|---|
alp_capabilities_has(const alp_capabilities_t *c, alp_instance_cap_t f) | bool — true 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:
| Macro | Example |
|---|---|
ALP_SOC_REF_STR | "alif:ensemble:e7" — the active silicon reference, also what backend selection matches on |
ALP_SOC_<CLASS>_COUNT | ALP_SOC_ADC_COUNT, ALP_SOC_PWM_COUNT, … |
ALP_SOC_ADC_MAX_RESOLUTION_BITS | 24 on the Alif Ensemble family, 12 on RZ/V2N |
ALP_SOC_GPU2D / ALP_SOC_DAVE2D / ALP_SOC_NEON … | Boolean 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:
| SoC | Macro | Old → New |
|---|---|---|
| E4 | ALP_SOC_I2C_COUNT | 5 → 6 |
| E4 | ALP_SOC_SPI_COUNT | 5 → 6 |
| E6 | ALP_SOC_I2C_COUNT | 5 → 6 |
| E6 | ALP_SOC_SPI_COUNT | 5 → 6 |
| E6 | ALP_SOC_WDT_COUNT | 3 → 4 |
| E8 | ALP_SOC_SPI_COUNT | 5 → 6 |
RZ/V2N (n44) | ALP_SOC_ETHERNET_COUNT | 0 → 2 |
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:
| SoC | Unverified keys |
|---|---|
| Alif Ensemble E3, E4, E6, E7, E8 | pdm, pdm_lp |
| Alif Ensemble E5 | the 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
<alp/backend.h>— howALP_SOC_REF_STRdrives backend selection<alp/i3c.h>— the class gated by the newALP_CAP_ID_HW_I3C- Architecture: capability validation and E1M portability bound
- Examples: adc-voltmeter — exercises the SoC-level gate and the instance-level gate in one program
<alp/e1m_pinout.h>— the instance-id space (ALP_E1M_<CLASS>_COUNT) the bounds are checked against