Timers & PWM
The general-purpose timers drive three everyday jobs: PWM output for LEDs and motors, input capture for measuring incoming signals, and periodic callbacks at a fixed rate. core_pwm / core_timer sit on hal_timer and ll_tim. Use the Core / HAL / LL toggle at the top of the sidebar to switch.
Overview
A timer counts a prescaled clock up to a reload value. From that one mechanism you get a PWM frequency (the reload) and duty (a compare value), a capture timebase, or a periodic interrupt. You pick the timer instance (TIM1, TIM2, TIM15, …); the clock is resolved for you.
500 is 50%. Easy to misread as percent.Two Core surfaces share the hardware: core_pwm (including a Tier 2 core_pwm_duty(pad, …) that resolves the timer from config.json) and the lower-level core_timer for capture and explicit control.
PWM output
Init a timer at the PWM frequency, set a channel’s duty, start. The Tier 2 core_pwm_duty is even shorter when the pad is declared in config.json:
#include "core.h"
core_timer_t pwm;
core_pwm_init(&pwm, TIM2, 2000); // 2 kHz
core_pwm_set(&pwm, 1, 750); // channel 1, 75% (permil)
core_pwm_start(&pwm);
// Tier 2 — timer/channel resolved from config.json:
core_pwm_duty(7, 500); // pad 7, 50%Input capture
Capture latches the counter on each rising edge of an input — measure a pulse’s period or frequency. Init a timebase, arm a channel, read the latched value:
core_timer_t cap;
core_timer_init_freq(&cap, TIM2, 1000); // 1 kHz timebase
core_timer_capture_init(&cap, 1);
core_timer_start(&cap);
uint32_t edge = core_timer_capture_read(&cap, 1);Periodic callbacks
For a fixed-rate ISR — sampling, control loops — use the “every” helper or core_tick_init. The callback fires from the timer interrupt at the period you set:
static void on_tick(void *ctx) { /* runs every 1 ms */ }
core_timer_t t;
core_every_us(&t, TIM6, 1000, on_tick, NULL);
core_every_start(&t);Cross-architecture support
The timebase and PWM are the verified baseline across the family; capture, encoder, and low-power-timer modes vary. The core_timer / core_pwm contract is the same everywhere:
See the implementation status for the full matrix.
Known gaps
What the SDK itself lists as missing here, straight from the headers:
The DSL's `Core.Timer.tick` event fires at the period baked into coregen from config.json's timer.tick_ms. There's no `Core.Timer.set_tick_ms()` host call to retune at runtime — the period is fixed for the life of the program.
core_timer_capture_init / capture_read / pwm_set are Tier 1 — they take a core_timer_t* the DSL can't construct. PWM has its own default-instance helper (Core.PWM.duty); capture / measurement has no DSL surface.
The hardware supports incremental encoder mode (CH1+CH2 quadrature counting). The wrapper exposes neither encoder init nor read. Motion / rotary-encoder tiles can't use it without dropping into ll_tim.
STM32 timers can fire a single pulse of programmable width and then auto-disable — useful for camera trigger / strobe / ultrasound ranging. Not wrapped.
Tier 2 only exposes duty. Frequency is fixed at coregen time from config.json's timer.freq_hz. DSL programs that want to sweep frequency (e.g., a buzzer playing notes) need set_freq exposed per-pad — the underlying tal_pwm has the path, but the wrapper + default-instance dispatch don't.
core_every_us is Tier 1 only — it takes a C function pointer for the ISR callback. The DSL surface is `Core.Timer.tick` (in core_timer.h) which fires at config.json's timer.tick_ms; finer custom periods aren't reachable.
SDK roadmap Tier 2 item: TIM1's complementary outputs + dead-time insertion + break input are needed for half-bridge / motor / power-converter tiles. The PWM wrapper is single-channel only.
Edge-aligned PWM only. STM32 timers can do center-aligned (Mode 1 / 2 / 3) and per-channel phase offsets — useful for low-EMI switching. Not surfaced.
From the @studio unsupported notes in core_timer.h, core_pwm.h — tiles@6af026f.
API reference
PWM (core_pwm)
void core_pwm_duty(uint8_t pad, uint16_t duty_permil);hal_status_t core_pwm_init(core_timer_t * h, TIM_TypeDef * instance, uint32_t freq_hz);hal_status_t core_pwm_init_clk(core_timer_t * h, TIM_TypeDef * instance, uint32_t pclk_hz, uint32_t freq_hz);void core_pwm_set(core_timer_t * h, uint8_t channel, uint16_t duty_permil);void core_pwm_set_freq(core_timer_t * h, uint32_t freq_hz);void core_pwm_start(core_timer_t * h);void core_pwm_stop(core_timer_t * h);hal_status_t core_pwm_init_pad(core_timer_t * h, uint8_t pad, uint32_t freq_hz);void core_pwm_set_pad(core_timer_t * h, uint8_t pad, uint16_t duty_permil);hal_timer_t * core_pwm_timer_for_pad(uint8_t pad);hal_status_t core_every_us(core_timer_t * h, TIM_TypeDef * instance, uint32_t period_us, hal_callback_t cb, void * ctx);hal_status_t core_every_us_clk(core_timer_t * h, TIM_TypeDef * instance, uint32_t pclk_hz, uint32_t period_us, hal_callback_t cb, void * ctx);void core_every_start(core_timer_t * h);void core_every_stop(core_timer_t * h);Generated from core_pwm.h — tiles@f70bca2.
Timer (core_timer)
hal_status_t core_timer_init_freq(core_timer_t * h, TIM_TypeDef * instance, uint32_t freq_hz);void core_timer_set_trgo(core_timer_t * h, uint32_t mms);hal_status_t core_timer_init_tick(core_timer_t * h, TIM_TypeDef * instance, uint32_t tick_hz);void core_timer_start(core_timer_t * h);void core_timer_stop(core_timer_t * h);void core_timer_set_freq(core_timer_t * h, uint32_t freq_hz);void core_timer_pwm_set(core_timer_t * h, uint8_t channel, uint16_t duty_permil);void core_timer_pwm_set_pad(core_timer_t * h, uint8_t pad, uint16_t duty_permil);void core_timer_capture_init(core_timer_t * h, uint8_t channel);uint32_t core_timer_capture_read(core_timer_t * h, uint8_t channel);hal_status_t core_timer_enable_tick(core_timer_t * h, core_callback_t cb, void * ctx);void core_timer_disable_tick(core_timer_t * h);hal_status_t core_tick_init(core_timer_t * h, TIM_TypeDef * instance, uint32_t period_us, core_callback_t cb, void * ctx);Generated from core_timer.h — tiles@f70bca2.

