BERGSONNE

System

The services every Core gives you for free — blocking delays and a millisecond clock, the independent watchdog for recovering from hangs, and fault handlers that turn a crash into a readable register dump. None of them need a pad or any configuration. Use the Core / HAL / LL toggle at the top of the sidebar to see each at the layer you work in.

Overview

System features handle timing and fault recovery. They’re available on every Core tile and require no pad assignments or peripheral configuration — they’re part of the Core itself, not something you wire up.

Delays, timekeeping, and the fault handlers come in automatically through core.h — no extra includes. The watchdog is opt-in: include core_watchdog.h when you want it. Most calls are Tier 2 — you call them directly, no handle. A few watchdog helpers are Tier 1, meant for a one-time check at boot.

Delays & timing

Blocking delays and a free-running millisecond counter come from core.h. The counter is handy for timeouts and general timekeeping without tying up a hardware timer.

core_delay_ms(500);          // block for 500 ms
  core_delay_us(100);          // block for 100 us

  uint32_t start = core_millis();
  // ... your application code ...
  if (core_timeout(start, 1000)) {
      // 1 second has elapsed since 'start'
  }
  • core_delay_us is for short waits — for anything over a millisecond prefer core_delay_ms, which won’t starve the rest of the system as long.
  • core_millis() counts up from boot and wraps after ~49 days. Compare with core_timeout() rather than subtracting raw values, so a wrap doesn’t bite you.

Watchdog

The independent watchdog (IWDG) runs on its own low-speed internal oscillator (about 32 kHz; 37 kHz nominal on the L0, where it varies more from part to part), completely separate from the system clock. Once started, it cannot be stopped — only a full MCU reset disables it. If your code doesn’t refresh it before the timeout, the MCU resets itself. That’s exactly what you want in a deployed system: a hang, an infinite loop, or a deadlock all recover automatically.

#include "core.h"
  #include "core_watchdog.h"

  int main(void)
  {
      core_init();

      if (core_watchdog_caused_reset()) {
          // We rebooted from a watchdog reset — handle recovery
          core_watchdog_clear_flags();
      }

      core_watchdog_start(2000);   // 2-second timeout

      while (1) {
          // ... your application code ...
          core_watchdog_feed();    // must call within 2 seconds
      }
  }

Pass the timeout in milliseconds; the prescaler and reload are chosen for you. The usable range is roughly 100 ms to 28 s. start and feed are the everyday Tier 2 calls; core_watchdog_caused_reset() and core_watchdog_clear_flags() are Tier 1 helpers you typically call once, at boot, to detect and acknowledge a watchdog-induced reset.

Once started, it stays started
There is no stop. Start the watchdog only after init is complete and your loop is actually feeding it, or the first slow path will reset you mid-bring-up.

Fault handlers

The SDK installs handlers for the four CPU faults — HardFault, MemManage, BusFault, and UsageFault. No setup required: they’re compiled into every project and override the default infinite-loop handlers from the startup code. When one fires, the handler captures the stacked register frame (PC, LR, R0–R3, R12, PSR), runs your callback if you registered one, and then does what the Core can:

CoreRegister dumpThen
L4Over USB CDC, if USB enumeratedOne SOS, then the ROM bootloader (ROM-DFU builds); otherwise SOS forever
H5NoneOne SOS, then the ROM bootloader (ROM-DFU builds); otherwise SOS forever
L0, W5None (no USB)SOS on the LED forever

Rebooting into the ROM bootloader means a crashing app can always be reflashed over USB without a debugger. On the W5 and the H5 the cause of a fault is lost today (tracked in the known gaps below); a debugger or your own callback is how you catch it there.

Example output (Core.ST.L4)

The dump uses polled transmit that works with interrupts disabled, so it runs from fault context. If USB CDC never enumerated it is skipped and you still get the SOS.

*** HardFault ***
    PC  = 0xDEADDEAC
    LR  = 0x08002345
    R0  = 0x00000000
    R1  = 0x20001234
    R2  = 0x00000000
    R3  = 0x00000001
    R12 = 0x00000000
    PSR = 0x61000000
    CFSR = 0x00000001

  SOS...

The PC value is the instruction that faulted — look it up in your .map file or a disassembly to find the source line.

Optional callback

Register a callback to run first, on every Core — e.g. to stash the PC in a backup register for the next boot. It runs in fault context, so keep it minimal: no heap, no interrupts.

#include "core_fault.h"

  void my_fault_handler(hal_fault_type_t type, const hal_fault_frame_t *frame)
  {
      // Log fault PC to NVM, set a flag, etc. Keep it tiny.
      (void)type;
      (void)frame;
  }

  // In main, before your loop:
  core_fault_set_callback(my_fault_handler);

Brick recovery

On the USB Cores (L4 and H5, ROM-DFU builds) a bad app that hangs before USB comes up can’t brick the board. core_init() counts consecutive watchdog resets; after three with no healthy run in between, it stops launching the app, blinks SOS and parks in the ST ROM bootloader (USB 0483:DF11), so it can always be reflashed. A power cycle starts the count afresh.

Nothing to call. core_watchdog_feed() clears the count once the app has run for max(10 s, twice the watchdog timeout), so occasional resets in a healthy app never add up to a false park. core_recovery_clear() is there if you want to clear it sooner. Bench-verified on a Core.ST.L4; compile-only on the H5. The L0 and W5 have no USB, so no recovery path.

SWO debug output

core_debug_print() and core_debug_printf() send text over the SWD trace pin (ITM port 0) to a probe that reads SWO, with no UART or USB needed. Call core_debug_init() once. L4, W5 and H5 only: the L0’s Cortex-M0+ has no ITM, so the calls do nothing there. Compile-only so far; no DSL surface.

Known gaps

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

Timing
lowC APINo monotonic 64-bit clock

millis wraps at ~49.7 days. Long-running systems (industrial / datalogger) need either a 64-bit upcounter or a wrap-aware helper for diff-since-start.

Watchdog
lowDSLNo DSL access to caused_reset / clear_flags

These return / clear hardware flags that only matter on the very first boot iteration. Exposing them needs a story for "before studio_start runs" — Studio doesn't currently model that phase.

lowC APINo window watchdog (WWDG)

STM32 also has a windowed watchdog (must feed within a window, not just before the deadline). Useful for catching feed-too-fast bugs. Not wrapped here.

Fault
mediumC APINo structured fault report capture

The default handler dumps to USB CDC at runtime on the L4 only (the W5, H5 and L0 lose the cause), and there's no crash log that survives reset for post-mortem analysis. A small ring buffer in backup or NVM (with the captured PC / LR / xPSR / fault status registers) would make field debugging tractable.

Debug
lowDSLNo DSL surface

Debug output is intentionally Tier 1 — the DSL doesn't have a notion of "debug print to a hardware probe." DSL programs use Core.USB.print for visible output; SWO is reserved for users who are already in C and have a probe attached.

lowC APINo ITM channel selection / timestamps

All output goes to ITM stimulus port 0 with no timestamp packets. Multi-channel routing (e.g., separate streams for log vs. data) and ETM/CYCCNT correlation aren't wrapped.

From the @studio unsupported notes in core_timing.h, core_watchdog.h, core_fault.h, core_debug.h — tiles@6af026f.

API reference

Delays & timing

Default-instance · Tier 2
void core_delay_ms(uint32_t ms);
Blocking delay in milliseconds.
void core_delay_us(uint32_t us);
Blocking delay in microseconds. For delays > 1 ms prefer `delay_ms` — it won't starve the rest of the system as long.
uint32_t core_millis(void);
Milliseconds since boot (wraps at ~49 days).
int core_timeout(uint32_t start, uint32_t ms);
Check if a timeout has elapsed. start: value returned by core_millis() at the beginning ms: timeout duration in milliseconds Returns 1 if expired, 0 otherwise.
Lower-level · Tier 1
void core_cycle_init(void);
Enable the DWT cycle counter. Idempotent; call once at startup.
void core_delay_cycles(uint32_t cycles);
Busy-wait `cycles` CPU cycles (≈ ±a few cycles of loop overhead).
void core_delay_ns(uint32_t ns);
Busy-wait `ns` nanoseconds (rounds to whole CPU cycles).

Generated from core_timing.h — tiles@777be99.

Watchdog

Default-instance · Tier 2
void core_watchdog_start(uint32_t timeout_ms);
Start the independent watchdog with a timeout in milliseconds. Selects the best prescaler/reload combination automatically. Common values: 1000, 2000, 5000, 10000 (max ~28000). WARNING: Once started, the IWDG cannot be stopped, and it keeps running in Stop and Standby. core_stop_for() wakes to feed it; core_standby_for() refuses sleeps longer than half the timeout.
void core_watchdog_feed(void);
Feed the watchdog. Must be called before the timeout expires. On ROM-DFU builds this also retires the brick-recovery strike counter, once, after the app has run for max(10 s, 2 x the timeout) — no call needed.
Lower-level · Tier 1
int core_watchdog_running(void);
Returns 1 if this firmware has started the watchdog (core_watchdog_start).
uint32_t core_watchdog_sleep_chunk_ms(void);
The longest a sleep may run between feeds while the watchdog runs: half its timeout, in ms. 0 when the watchdog isn't running. The RTC that times the sleep and the IWDG share the LSI, so the margin holds even when the LSI is far from nominal (the L0's is 26-56 kHz).
int core_watchdog_caused_reset(void);
Check if the last reset was caused by the watchdog. On ROM_DFU builds, core_init() reads and clears the hardware flag early (for the strike counter), stashing the cause in reserved SRAM — so prefer that when it's valid; otherwise fall back to the raw RCC_CSR flag.
void core_watchdog_clear_flags(void);
Clear all reset flags (call after checking cause).
void core_watchdog_debug_freeze(void);
Freeze the IWDG while the core is halted under a debugger, so a breakpoint doesn't let the watchdog reset the chip out from under an SWD session. Firmware-side so it holds for any probe/toolchain (rev b exposes SWD on L4).

Generated from core_watchdog.h — tiles@4b95d38.

Fault handlers

Lower-level · Tier 1
void core_fault_set_callback(hal_fault_callback_t cb);
Register a fault callback, run in fault context before the SOS blink (and before the L4's USB register dump). Keep it minimal: no heap, no interrupts.

Generated from core_fault.h — tiles@f70bca2.

Brick recovery

Lower-level · Tier 1
uint32_t core_recovery_note_boot(void);
Account for this boot and return the running strike count. Call once, very early in core_init(), before clocks/user code.
int core_recovery_over_limit(uint32_t strikes);
True once we've had enough consecutive watchdog resets to give up on the app.
void core_recovery_clear(void);
Clear the strike counter now. core_watchdog_feed() already does this once the app has run healthy for max(10 s, 2x the timeout); call this only to declare health earlier than that.

Generated from core_recovery.h — tiles@6af026f.

SWO debug output

Lower-level · Tier 1
void core_debug_init(void);
Initialize SWO debug output using the project's SYSCLK_HZ (no-op on Core.ST.L0).
void core_debug_print(const char * str);
Print a string via SWO (no formatting).

Generated from core_debug.h — tiles@f70bca2.

core_debug_printf(fmt, ...) is a macro for hal_debug_printf.