BERGSONNE

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.

Duty is permil, not percent
Everywhere a duty cycle appears it’s 0–1000 (parts per mille): 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:

●L0M0+5/8●L4M45/8●W5M334/8●H5M335/8WCH (RISC-V) · Nordic (nRF54) — in development

See the implementation status for the full matrix.

Known gaps

What the SDK itself lists as missing here, straight from the headers:

Timer
mediumDSLTick event period is set in config.json, not from DSL

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.

mediumDSLNo DSL access to per-channel capture / PWM

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.

mediumC APINo encoder / quadrature mode

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.

lowC APINo one-pulse mode (OPM)

STM32 timers can fire a single pulse of programmable width and then auto-disable — useful for camera trigger / strobe / ultrasound ranging. Not wrapped.

PWM
mediumDSLNo DSL access to PWM frequency

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.

mediumDSLNo DSL access to periodic 'every' callbacks

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.

mediumC APINo complementary PWM / dead-time (TIM1 advanced)

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.

lowC APINo phase / center-aligned modes

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)

Default-instance · Tier 2
void core_pwm_duty(uint8_t pad, uint16_t duty_permil);
Set PWM duty cycle on a pad. Timer handle resolved from config.json.
Lower-level · Tier 1
hal_status_t core_pwm_init(core_timer_t * h, TIM_TypeDef * instance, uint32_t freq_hz);
Initialize a timer for PWM output at the given frequency. Clock is auto-resolved from SYSCLK_HZ (core_config.h).
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);
Set PWM duty cycle for a channel. channel: 1–4 duty_permil: 0–1000 (0 = off, 500 = 50%, 1000 = 100%)
void core_pwm_set_freq(core_timer_t * h, uint32_t freq_hz);
Change PWM frequency (recalculates PSC/ARR, resets all channel duties).
void core_pwm_start(core_timer_t * h);
Start the PWM timer.
void core_pwm_stop(core_timer_t * h);
Stop the PWM timer.
hal_status_t core_pwm_init_pad(core_timer_t * h, uint8_t pad, uint32_t freq_hz);
Initialize PWM on a pad. Timer instance resolved from config.json.
void core_pwm_set_pad(core_timer_t * h, uint8_t pad, uint16_t duty_permil);
Set PWM duty on a pad (0–1000 permil).
hal_timer_t * core_pwm_timer_for_pad(uint8_t pad);
The timer driving a pad. Emitted per project by coregen into core_init.c when any TIM<n> pad is configured.
hal_status_t core_every_us(core_timer_t * h, TIM_TypeDef * instance, uint32_t period_us, hal_callback_t cb, void * ctx);
Configure a periodic callback at a fixed interval. period_us: interval in microseconds (1 – 1000000) cb: callback function (called from ISR context!) ctx: user context passed to callback Clock is auto-resolved from SYSCLK_HZ (core_config.h).
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);
Start the periodic timer.
void core_every_stop(core_timer_t * h);
Stop the periodic timer.

Generated from core_pwm.h — tiles@f70bca2.

Timer (core_timer)

Lower-level · Tier 1
hal_status_t core_timer_init_freq(core_timer_t * h, TIM_TypeDef * instance, uint32_t freq_hz);
Initialize a timer at a given overflow frequency. Use for PWM output and periodic tick — the frequency is how often the counter wraps (= the PWM frequency). core_timer_init_freq(&t, TIM2, 1000); // overflows at 1 kHz
void core_timer_set_trgo(core_timer_t * h, uint32_t mms);
Route an internal timer event to TRGO so another peripheral can be paced by it, with no pin, no channel and no interrupt involved. LL_TIM_MMS_UPDATE is the periodic-pacer case: every counter overflow emits a trigger. Pair it with core_adc_set_trigger() to run the ADC at an exact rate instead of free-running. core_timer_init_freq(&t, TIM2, 800); core_timer_set_trgo(&t, LL_TIM_MMS_UPDATE); core_timer_start(&t);
hal_status_t core_timer_init_tick(core_timer_t * h, TIM_TypeDef * instance, uint32_t tick_hz);
Initialize a timer at a given tick rate, free-running to max count. Use for input capture — the tick rate sets the measurement resolution, and the counter runs as long as possible before wrapping (0xFFFF for 16-bit, 0xFFFFFFFF for 32-bit TIM2). core_timer_init_tick(&t, TIM2, 1000000); // 1 us per tick core_timer_capture_init(&t, 1); core_timer_start(&t);
void core_timer_start(core_timer_t * h);
Start the timer counter.
void core_timer_stop(core_timer_t * h);
Stop the timer counter.
void core_timer_set_freq(core_timer_t * h, uint32_t freq_hz);
Change the timer frequency (recalculates PSC/ARR).
void core_timer_pwm_set(core_timer_t * h, uint8_t channel, uint16_t duty_permil);
Set PWM duty cycle for a channel. channel: 1–4 duty_permil: 0–1000 (0 = off, 500 = 50%, 1000 = always on)
void core_timer_pwm_set_pad(core_timer_t * h, uint8_t pad, uint16_t duty_permil);
Set PWM duty cycle by pad number (requires coregen: only defined when the project's config.json binds a timer to a pad). Resolves the channel from the pad's timer assignment; pads without a timer are ignored.
void core_timer_capture_init(core_timer_t * h, uint8_t channel);
Configure a channel for input capture (rising edge). The timer must already be initialized with core_timer_init().
uint32_t core_timer_capture_read(core_timer_t * h, uint8_t channel);
Read the last captured value from a channel.
hal_status_t core_timer_enable_tick(core_timer_t * h, core_callback_t cb, void * ctx);
Enable periodic tick on an already-initialized timer. Does NOT touch PSC/ARR — the timer keeps its existing timebase. The callback fires on each counter overflow at the timer's frequency. Typical pattern: core_timer_init(&t, TIM2, 1000); // 1 kHz timebase core_timer_pwm_set(&t, 1, 500); // CH1 = 50% PWM (permil) core_timer_enable_tick(&t, on_tick, NULL); // also fire ISR at 1 kHz core_timer_start(&t);
void core_timer_disable_tick(core_timer_t * h);
Disable the tick callback (clears UIE, keeps timer running).
hal_status_t core_tick_init(core_timer_t * h, TIM_TypeDef * instance, uint32_t period_us, core_callback_t cb, void * ctx);
Convenience: initialize a timer as a tick-only source. Sets up the timebase AND enables the update interrupt. Use this when the timer's only job is a periodic callback. core_tick_init(&t, TIM3, 500000, on_tick, NULL); // 2 Hz core_timer_start(&t);

Generated from core_timer.h — tiles@f70bca2.