@studio annotations
A small set of @studio directives inside a header’s Doxygen comments turns an SDK module or a tile driver into Studio palette blocks, DSL imports, reference pages, and coverage tables. You write the header; the generators derive everything else — nobody hand-edits palette JSON or coverage tables.
Overview
Studio and these docs are built from the C headers in the SDK. The bridge is a handful of @studio tags placed inside /** … */ blocks. Two discovery rules govern everything:
- A file opts in with a file-level tag —
@studio category(SDK modules) or@studio tile(tile drivers). Without it, the file is skipped. - A function becomes a palette block / DSL call only if its Doxygen block carries
@studio expose. Functions without it still appear on the SDK reference page (so init/lifecycle helpers are documented) — they just aren’t palette blocks.
Standard Doxygen feeds the same output: @brief becomes the summary + palette tooltip, and @param names are the DSL parameter names (there’s no rename — pick C names that read well).
Two pipelines
The directive set is split across two independent generators. A tag parsed by one is ignored by the other, so it helps to know which drives what:
- Palette / DSL / manifests —
gen_studio_manifest.pyparsescategory,tile,expose,event,param, and the buffer tags intomanifests/*.json. - Docs / coverage tables — the driver-docs parser reads
expose section=andunsupportedinto the per-driver JSON that powers Tile Drivers. - Simulator / SDK coverage —
twinand thecoverageblock feed the SDK’s coverage + simulator-status views.
File-level directives
@studio category <name> [label=] [icon=]
Required on every SDK core_*.h that exposes anything. The name becomes the category id (and the manifests/sdk-docs/<name>.json filename); label/icon are display.
/**
* core_pwm.h — PWM output and periodic timers
* @studio category pwm label=Core.PWM icon=◲
*/@studio tile [label=] [icon=]
The tile-driver equivalent — exactly one per tile_*.h. Opts the driver into the palette and flips its Studio-ready state.
/**
* @studio tile label=Sense.BP icon=◇
*/@studio event name=<id> [payload=] [mask= read= read_type=]
Declares an event the module/tile can raise (becomes on Core.Pad.rising(pad: int) { … } in the DSL). payload is comma-separated name:type pairs. Tile events add mask (a bit in the tile’s interrupt status), read (a fn that fills the payload), and read_type (its struct):
/**
* @studio tile label=Sense.I.6P6 icon=▦
* @studio event name=tap mask=ICM42686P_INT_STATUS3_TAP_DET \
* payload=count:int,axis:int,direction:int \
* read=tile_sense_i_6p6_get_tap_result read_type=sense_i_6p6_tap_result_t
*/Function-level directives
@studio expose category=<name> name=<dsl_name> [section=] [returns=] [availability=]
The core directive — marks a function palette-exposed and DSL-callable. category must match the file’s category/tile; name is the DSL-facing name. returns gives the DSL return type (int, bool, float, string, or a fixed array like int[3]); availability limits it to certain Cores.
/**
* Set PWM duty cycle on a pad.
*
* @studio expose category=pwm name=duty
* @param pad [1..64] Pad configured as TIMx.<ch> in config.json.
* @param duty_permil [0..1000] 0 = off, 500 = 50%, 1000 = full.
*/
static inline void core_pwm_duty(uint8_t pad, uint16_t duty_permil);section=<bucket> (read by the docs parser) groups the function into a named row group on the driver page — e.g. @studio expose section=fifo.
Shaping parameters & outputs
@studio param <c> enum {KEY=label, …}— friendly labels for an int-valued parameter (codegen still emits the C identifier).@studio param <c> type=int[16]— override a pointer parameter’s DSL type (fixed-length array input).@studio out_buffer <c> type=<ctype> (length=N | cap_param=<c>)— promote a pointer-out to a typed array: fixed-length collapses into the return value;cap_parammode surfaces a writable array with a separate count.@studio out_scalar <c> type=<ctype>— a scalar pointer the function writes into; the DSL caller passes an lvalue, back-filled on return. Multiple are allowed (multi-out functions).@studio in_buffer <c> type=<elem> (length=N | length_param=<c>)— a caller-passed array (and its length param), collapsed to one DSL array argument.@studio twin <full|regs|noop>— the simulator-twin fidelity for this function, feeding the SDK coverage view.
/**
* Read raw accelerometer [X, Y, Z].
*
* @studio expose category=tile name=get_raw_accels returns=int[3]
* @studio out_buffer buffer type=int16_t length=3
*/
void tile_sense_i_6p6_get_raw_accels(tile_t *tile, int16_t *buffer);Coverage & gaps
@studio unsupported severity=<common|advanced|niche> category="…"
Declares a chip capability the driver doesn’t cover, with the rationale on the following lines. These become the driver page’s coverage-gap rows — the canonical, derived way to record a gap. An optional section= groups it.
/**
* @studio unsupported severity=advanced category="Alternate bus modes (SPI / I3C)"
* I²C-only today. The ILPS22QS supports 3/4-wire SPI and I3C SDR; adding
* them needs the driver framework to plumb a second bus abstraction.
*/@studio coverage
A file-level block on SDK modules (id, name, page, blurb) describing the module for the SDK coverage table.
How the docs & palette derive
The manifest generator runs from the SDK and writes the checked-in manifests; a --check mode in CI fails the build if a header changes without its manifest being regenerated, so the palette and docs can never silently fall behind the code:
tools/gen_studio_manifest.py # write manifests
tools/gen_studio_manifest.py --check # CI: verify manifests match headersTile-driver pages flow through the driver-docs parser into the per-tile JSON the site renders. To see the annotations in the context of writing a driver, see Writing a tile driver.

