<alp/tmu.h> — Transcendental Math
Twelve portable single-precision math primitives that the active SoM's hardware math unit can accelerate when one is present. Every call is stateless — there is no alp_tmu_open, no handle, and no pool.
The value over calling libm directly is the seam: a single place where future SoCs can register their own math accelerators without the application changing.
Header
#include <alp/tmu.h>
Quick example
float s;
if (alp_tmu_sin(0.5f, &s) == ALP_OK) {
// s ~= 0.479425539
}
float h;
alp_tmu_hypot(3.0f, 4.0f, &h); // h == 5.0
Every call takes its output through a pointer and returns an alp_status_t, because the accelerated path can fail in ways libm cannot (a bridge timeout, an unsupported function). Check the status — do not assume out was written.
Which silicon actually has a math accelerator?
| SoM family | Path | Reality |
|---|---|---|
| V2N family (E1M-V2N101/102, E1M-V2M101/102) | GD32G553 supervisor MCU's TMU (CORDIC) engine, reached over the bridge protocol (CMD_TMU_COMPUTE) | Compiled only when CONFIG_ALP_SDK_V2N_SUPERVISOR=y. Three of the twelve functions are not natively supported — see below. |
| AEN family (Alif Ensemble) | libm | There is no Alif TMU backend in the SDK. Despite the Ensemble's FPU, alp_tmu_* on AEN is sinf/cosf/… |
| Every other Zephyr SoM | libm | Same wildcard backend, libm branch. |
| Yocto / baremetal | libm | — |
:::caution The hardware path is on the supervisor MCU, not the application SoC
On the V2N family the CORDIC engine belongs to the GD32G553 supervisor, so every accelerated call is a round-trip over SPI or I²C — the GD32 computes and returns the result. That is a fundamentally different cost profile from an on-die FPU instruction. Do not assume alp_tmu_* is faster than libm on the application core; measure it. The surface exists for portability and offload, not as a guaranteed speedup.
:::
V2N: tan, exp, and tanh are ALP_ERR_NOSUPPORT
The GD32G5's TMU has no native mode for tan, exp, or tanh. The bridge firmware's dispatch table marks all three as unsupported and returns a not-implemented status, and the host-side backend passes that straight back:
- The TMU dispatcher resolves its backend once with
alp_backend_select()and does not callalp_backend_select_next(). - So on a
CONFIG_ALP_SDK_V2N_SUPERVISOR=ybuild there is no fall-through to libm for these three.alp_tmu_tan,alp_tmu_exp, andalp_tmu_tanhreturnALP_ERR_NOSUPPORTon that configuration.
Composing them host-side (tan = sin/cos, tanh = sinh/cosh, exp = cosh + sinh) is noted as possible in the sources but is not implemented today. Portable code that needs these three on V2N must call libm directly or handle ALP_ERR_NOSUPPORT.
The other nine (sin, cos, atan, atan2, sqrt, log, sinh, cosh, hypot) map to native TMU modes.
:::caution V2N trig domain: |x| <= pi, no range reduction
On the bridge path, F32 trig angles are radians and the firmware does not range-reduce — the usable domain is |x| <= pi. The libm path has no such restriction. Code that feeds unreduced angles (an accumulating phase, say) will diverge between SoMs unless you reduce the argument yourself.
:::
Functions
All twelve share the same shape. out must be non-NULL — the dispatcher rejects NULL with ALP_ERR_INVAL before selecting a backend.
| Call | Computes |
|---|---|
alp_tmu_sin(float in_a, float *out) | sin(in_a), in_a in radians |
alp_tmu_cos(float in_a, float *out) | cos(in_a), in_a in radians |
alp_tmu_tan(float in_a, float *out) | tan(in_a), in_a in radians — NOSUPPORT on V2N bridge builds |
alp_tmu_atan(float in_a, float *out) | atan(in_a) → [-pi/2, pi/2] |
alp_tmu_atan2(float in_a, float in_b, float *out) | atan2(y=in_a, x=in_b) → [-pi, pi], quadrant-correct |
alp_tmu_sqrt(float in_a, float *out) | sqrt(in_a), in_a non-negative |
alp_tmu_log(float in_a, float *out) | ln(in_a), in_a strictly positive |
alp_tmu_exp(float in_a, float *out) | e^in_a — NOSUPPORT on V2N bridge builds |
alp_tmu_sinh(float in_a, float *out) | sinh(in_a) |
alp_tmu_cosh(float in_a, float *out) | cosh(in_a) |
alp_tmu_tanh(float in_a, float *out) | tanh(in_a) — NOSUPPORT on V2N bridge builds |
alp_tmu_hypot(float in_a, float in_b, float *out) | sqrt(in_a² + in_b²) |
alp_tmu_hypot is worth preferring over a hand-rolled sqrtf(a*a + b*b): it matches C99 hypotf semantics, and on the V2N backend it dispatches through the CORDIC "modulus" mode, giving a single-pass result without the intermediate overflow risk.
Status codes
| Code | When |
|---|---|
ALP_OK | Success; out written. |
ALP_ERR_INVAL | out is NULL. |
ALP_ERR_NOSUPPORT | The selected backend's hardware path does not implement this function (V2N: tan / exp / tanh). The libm fallback never returns this. |
ALP_ERR_NOT_IMPLEMENTED | No backend resolved for the class, or its ops slot is NULL. |
ALP_ERR_NOT_READY | The V2N supervisor is enabled but no bridge bus is configured. |
ALP_ERR_OUT_OF_RANGE | V2N backend only: input outside the function's mathematical domain (sqrt of a negative, log of zero). |
ALP_ERR_IO | Backend / bridge failure. |
Domain and accuracy
The surface mirrors C99 semantics, but the two paths disagree on how they report a domain error:
| Input | libm path | V2N bridge path |
|---|---|---|
sqrt(-1.0f) | out = NaN, returns ALP_OK | returns ALP_ERR_OUT_OF_RANGE |
log(0.0f) | out = -inf, returns ALP_OK | returns ALP_ERR_OUT_OF_RANGE |
Portable code that must discriminate should check the status and test the result with isnanf — neither check alone covers both backends.
Accuracy: the libm fallback delivers the platform's single-precision math accuracy directly. The CORDIC-backed V2N path is accurate to a few LSBs of binary32. Callers needing bit-exact reproducibility across SoMs (regression-test fingerprints, for example) must tolerate unit-in-last-place differences — this surface does not promise cross-SoM bit-identity.
Backends
| Backend | silicon_ref | Priority | Notes |
|---|---|---|---|
zephyr_drv | "*" | 100 | Single wildcard row covering every Zephyr-shaped SoM. The libm-vs-bridge split is internal: CONFIG_ALP_SDK_V2N_SUPERVISOR=y compiles the bridge path, every other build compiles the libm path. |
sw_fallback | "*" | 0 | libm. |
Because the wildcard zephyr_drv sits at priority 100 and sw_fallback at 0, zephyr_drv always wins on Zephyr builds (selection rules) — and since the dispatcher never falls through, sw_fallback is effectively the non-Zephyr floor rather than a runtime safety net.
The dispatcher caches the selected ops vtable on first call, so the steady-state hot path is two loads, a branch, and an indirect call. The cache is lock-free but published through acquire/release atomics; the registry is immutable after link and alp_backend_select() is deterministic, so racing first-callers resolve the identical backend.
The public header carries no gd32 references — the GD32 dispatch lives entirely in the backend, per the SDK's portability rule.
ABI status
[ABI-EXPERIMENTAL] — wave-1 GD32 CORDIC TMU helpers. The surface is limited and consolidation with <alp/dsp.h> is under consideration, so this header may be folded or renamed before v1.0. Pin your SDK to a specific commit if you depend on it.
See also
<alp/dsp.h>— FIR/IIR/FFT chains; the likely consolidation target<alp/backend.h>— the registry and the no-fall-through caveat above<alp/pid.h>and<alp/ahrs.h>— the other portable math surfaces