BERGSONNE

Project Config

Every project carries a config.json — the single source of truth for its hardware setup. You declare the Core, a clock level, what each pad does, and the tiles you’re wiring; coregen turns it into all the C init code. This is the field-by-field authoring reference.

The mental model

The Core’s definition (in definitions/) is the menu: it lists which pads exist, what each pad can do, the available clock levels, and the on-board peripherals. Your config.json is the order. coregen validates the order against the menu, then does the wiring — resolving alternate-function numbers, solving the PLL, computing I²C timing, and emitting handles.

The single most important idea: most things are derived, not declared. You almost never name a peripheral directly. You assign pad functions, and coregen infers the peripherals from them:

You write in pads…coregen infers…
"I2C1.CLK" + "I2C1.DAT"an I2C1 bus (tuned by interfaces.I2C1)
"SPI1.CLK" + "SPI1.MOSI" …an SPI1 bus (tuned by interfaces.SPI1)
"USART2.TX" + "USART2.RX"a USART2 UART (tuned by interfaces.USART2)
"TIM2.1"a PWM/timer channel on TIM2 (1 kHz; change it at run time)
"ADC5" / "ADC_IN3"an ADC input channel
"DAC1.OUT"the DAC
interfaces tunes; it never creates
The interfaces section only tunes a bus that a pad function already brought into existence. If you put I2C1 there but no pad is set to an I2C1.* function, coregen warns and emits nothing.

Quickstart

Smallest valid file — LED blink, no peripherals:

{
  "core": "Core.ST.L4.2",
  "clock": "max",
  "pads": {}
}

A real project — one I²C tile on a USB-powered L4:

{
  "core": "Core.ST.L4.2",
  "clock": "max",
  "usb": { "enabled": true },
  "pads": {
    "4": "I2C1.CLK",
    "5": "I2C1.DAT"
  },
  "interfaces": {
    "I2C1": { "speed": 400000 }
  },
  "tiles": [
    { "tile": "Sense.I.6P6", "bus": "I2C1", "instance": 0 }
  ]
}

After editing, regenerate with make generate (coregen also runs automatically on a normal make). Never hand-edit generated files between coregen:begin/coregen:end markers — re-running coregen clobbers them. Change the config instead.

Top-level fields

coregen reads a fixed allowlist of keys. Anything not listed here is silently ignored — a typo’d key is not an error, it just does nothing.

KeyTypeRequiredDefaultRead?
corestringyes—✅
clockstringnotile's default✅
padsobjectno{}✅
interfacesobjectno{}✅ (tuning)
gpioobjectno{}✅
pullupsarrayno[]✅ (on-tile I²C pull-ups)
tilesarrayno[]✅
usbobjectnodisabled✅
bootloaderstringno"none"✅
iwdgobjectnodisabled✅
bleobjectnodisabled✅ (Core.ST.W5 radio + GATT contract)
threadobjectnodisabled✅ (Core.ST.W5 Thread network)
timerobjectnonone✅ (Studio)
pinsobjectno—✅ (legacy alias for pads)
scopeobjectnoenabledread by the Makefile
probeobjectnoself-poweredread by the flash tooling
debug, isp, programmingobjectno—❌ ignored

core — pick the MCU/core (required)

Selects definitions/<Core>.json. Prefer the public name (the DB stem also works).

Public nameMCUMax SYSCLKUSB?
Core.ST.L0STM32L01132 MHzno
Core.ST.L4STM32L42280 MHzyes
Core.ST.L4.2STM32L42280 MHzyes
Core.ST.W5STM32WBA55100 MHzno
Core.ST.H5STM32H523248 MHzyes

clock — performance level

A clock-level name the Core defines (typically "low", "medium", "high", "max"), not a frequency. "default" resolves to the tile’s schema default. coregen solves the PLL and applies any required voltage scaling. An unknown level is a hard error. Note the clock also gates bus speeds — see I²C below.

The radio needs clock medium or higher
On Core.ST.W5, enabling ble or thread with "clock": "low" is a build error: the 2.4 GHz radio needs voltage range 1. Some Cores also adjust low: on the L4 Cores and the H5 it runs at 16 MHz, because USB is always on there.

Pads & functions

The heart of the file. Key = pad number as a string ("4", not 4); value = a function the Core definition lists for that pad. coregen validates every entry — a bad pad or unavailable function prints the available options and exits.

FamilyFormExamples
GPIOGPIO.OUT / GPIO.IN"GPIO.OUT"
I2CI2C<n>.CLK / .DAT"I2C1.CLK", "I2C3.DAT"
SPISPI<n>.CLK/MOSI/MISO/CS"SPI1.CLK", "SPI1.CS"
USARTUSART<n>.TX / .RX"USART2.TX"
Timer/PWMTIM<n>.<ch> (ch 1–4)"TIM2.1", "TIM15.2"
ADCADC / ADC<n> / ADC_IN<n> [+]"ADC1", "ADC5", "ADC_IN3", "ADC7+"
DACDAC1.OUT"DAC1.OUT"
USBUSB.DP / USB.DM"USB.DP"
  • SPI CS is software-managed. SPI*.CS pads become plain GPIO outputs driven by hal_spi_set_cs() — not hardware NSS. Not configurable.
  • ADC is single-ended positive only. Differential negative inputs aren’t emitted; coregen emits one core_adc1 handle today.
  • A bus needs all its pads. An I2C bus needs both CLK and DAT; SPI needs CLK + MOSI + MISO.

Interfaces — tune the buses

Keyed by peripheral name (I2C1, SPI1, USART2, TIM2, …). Only tunes a bus a pad already created.

I²C

FieldTypeDefaultAllowed
speedint400000100000, 400000, 1000000
pullupsbooltrueenable internal pad pull-ups
I²C speed depends on the clock
Kernel-clock minimums are enforced (exit on failure): 100 kHz needs ≥1 MHz, 400 kHz needs ≥4 MHz, 1 MHz needs ≥16 MHz. On Core.ST.W5 and Core.ST.L0 the I²C kernel clock is fixed at HSI16 (16 MHz) whatever the SYSCLK; on the L4 Cores and the H5 it follows SYSCLK, so there a too-low clock can make a fast speed illegal.

On-tile pull-ups. Core.ST.L4 and Core.ST.W5 have I²C pull-up resistors on pads 4 and 5 that a GPIO switches on. Name the ones you want at the top level, and core_init() turns them on. At 400 kHz or more without them, coregen warns: the MCU’s internal pull-up is too weak.

"pullups": ["pad4", "pad5"]

I3C (Core.ST.H5). Add "i3c": true (and optionally i3c_speed, default 12500000) to an I²C bus and coregen runs it as I3C, emitting core_i3c1 instead of core_i2c1. It does so only when the MCU, both pads and every tile on the bus support I3C; otherwise the bus stays I²C and a NOTE says why. See I3C.

SPI

FieldTypeDefaultAllowed
modeint00–3 (CPOL = bit1, CPHA = bit0)
prescalerint82,4,8,16,32,64,128,256
bit_orderstring"msb""msb" | "lsb"
cs_polaritystring"active-low""active-low" | "active-high"

On the L4 Cores and the W5, coregen also checks SCK (SYSCLK / prescaler) against the datasheet limit: over it is an error.

USART / UART

FieldTypeDefaultNotes
baudint115200any rate
rx_interruptboolfalsetrue = interrupt-driven RX with a ring buffer

Timer / PWM

Each timer a TIM<n>.<ch> pad uses gets a handle (core_tim<n>) and starts at 1 kHz, shared by every channel on that timer. There is no config key for the frequency: no Core lists its timers as interfaces, so interfaces.TIM2 fails validation. Change it at run time with core_timer_set_freq(&core_tim2, hz), and set the duty with core_timer_pwm_set() (per mille, 0-1000). There is no pwm, capture, or timers section.

GPIO & tiles

gpio — per-pin electrical details

Optional refinements for pads set to GPIO.IN/GPIO.OUT (key = pad number string).

FieldDefaultAllowed / meaning
pull"none""none" | "up" | "down"
output_type"push-pull""push-pull" | "open-drain"
speed"medium"slew rate (passed through)
extinone"rising" | "falling" | "both"
defaultnone"high" | "low" (initial output state)

tiles — attach drivers to buses

FieldTypeRequiredMeaning
tilestringyesa name in coregen’s TILE_DRIVER_MAP
busstringyesmust match a configured I2C*/SPI* interface
instanceintno (0)I2C: address slot · SPI: per-CS index
cs_padstringSPI onlypad number of this tile's chip-select
bus2stringdual-bus tilesthe second bus (the other protocol)
cs_idintnoa dual-bus tile's chip-select id on its SPI bus

coregen emits a per-tile handle like tile_sense_i_6p6_i2c1_0 and adds the driver to the build. Valid tile names are the TILE_DRIVER_MAP dict in coregen.py.

USB, bootloader & watchdog

usb

FieldDefaultNotes
enabledfalsetrue adds core_usb.h to core.h. USB-CDC itself starts in core_init() on every L4 / H5 project either way, to keep the 1200-baud DFU touch working.
vid / pidSDK default"0x1209" or 4617
product / manufacturer / serialSDK defaultdescriptor overrides; CMSIS-DAP hosts match "CMSIS-DAP" in product

USB requires a USB-capable MCU (L4 or H5); on other cores it’s a no-op/warning.

bootloader

ValueLayoutCoresNeeds USB
"none"app at 0x08000000, SWD only (default)allno
"custom"8 KB DFU at 0x08000000, app at 0x08002000L4 / H5 — parkedyes
"rom"ST ROM DfuSe, app at 0x08000000L4, H5yes

Sets the BOOTLOADER / ROM_DFU Make variables. See Bootloading. timer.tick_ms feeds the Studio simulation tick only — not a hardware tick.

"custom" is parked
"rom" is what Studio and the templates use. The build refuses "custom" unless you also pass CUSTOM_BOOTLOADER_ACK=1, because flashing a custom-layout image onto a board that’s in ROM-DFU layout writes at 0x08002000 and corrupts the resident app.

On Core.ST.H5 the ROM’s DFU “leave” starts the new image without a reset, and the peripherals the ROM secured (TIM1-TIM8, ADC, I2C1/2, LPUART1 and others) can’t be clocked until the next reset. USB, SPI, I2C3 and USART1-3 are not affected.

iwdg

An independent watchdog started inside core_init(). Opt-in, so regenerating an existing project never surprises it with resets:

"iwdg": { "enabled": true, "timeout_ms": 5000 }

Once enabled, your loop must call core_watchdog_feed() or the Core resets. Pair it with "bootloader": "rom": repeated watchdog resets then escalate into ROM DFU rather than a soft-brick, which is the whole point. coregen warns if you enable it without ROM mode, since an un-fed watchdog would just reset-loop with no way out.

BLE, Thread, scope & probe

ble (Core.ST.W5)

"ble": {
  "enabled": true,
  "name": "MY-DEVICE",
  "tx_power": "medium",
  "adv_interval_ms": 100,
  "pairing": false,
  "contract": "ble_contract.json"
}
FieldDefaultNotes
enabledfalselink the BLE stack and generate the radio settings
nameproject nameadvertised name, BLE_DEVICE_NAME
tx_powerSDK default"low" | "medium" | "high" or 0-2
adv_interval_msSDK default20-10240 ms, or {min, max}
pairingfalseJust Works pairing + bonding; every characteristic then needs an encrypted link
contractnonethe GATT contract, inline or a file next to config.json; coregen generates ble_contract.{c,h}

Needs clock medium or higher. Include core_ble.h (and ble_contract.h with a contract) in main.c: core.h does not. See BLE.

thread (Core.ST.W5)

"thread": {
  "enabled": true,
  "channel": 15,
  "panid": "0x1234",
  "network_key": "<32 hex digits>",
  "network_name": "tiles-dev",
  "provisioning": false,
  "ble_provisioning": false
}

The node joins on boot. Also read: role ("ftd" only), ext_panid and mesh_local_prefix. Absent keys take development defaults that every node shares, so set your own network for anything real. With provisioning on, a network stored at run time wins over this one; ble_provisioning adds an open BLE service for that (development only). With ble enabled too, core_ble_init() starts Thread, not core_init(). See Thread.

scope

"scope": { "enabled": false }

The production switch for core_scope. Absent or true leaves it available (an unused module costs nothing); false compiles every core_scope_* call to nothing. Read by the Makefile, not coregen (make SCOPE_ENABLED=0 overrides it once).

probe

"probe": { "target_power": "5v" }

How a CoreProbe powers this board while flashing over SWD: off (the default, self-powered) or 5v. Read by make flash-coreprobe and Studio, never coregen. The logic level is sensed, not declared, and a board that already has power is never fed.

Keys that are silently ignored

coregen reads a fixed allowlist; the raw config is never handed to the templates. These keys are read by nothing and have no effect, even though older docs described some of them. Because unknown keys are ignored rather than rejected, using them is harmless but does nothing.

Ignored keyWhat people expectReality
debugemit SWD/JTAG definesNo-op.
isp / programmingboot0_pad, methods…No-op.
timersper-timer freq/tickNo-op. Timers run at 1 kHz; set the frequency at run time.
pwmper-channel dutyNo-op. Duty is a runtime API call.
captureinput-capture mappingNo-op (not config-driven).

Validation & errors

coregen is mostly fail-fast — a bad config exits before any compile, so it won’t silently generate wrong code.

  • Exits on: a pad that doesn’t exist or a function not available on it; an unknown clock level; an I²C speed out of set or its kernel clock too low; an SPI mode/prescaler out of range; an unknown tile, a tile on an unconfigured bus, or an SPI tile whose cs_pad doesn’t resolve; an invalid bootloader; an interfaces key the Core doesn’t list (timers included); ble or thread out of range, off Core.ST.W5, or with clock: "low"; an unknown pullups name.
  • Warns (non-fatal) on: a core name not matching the definition; an interfaces entry with no backing pads; a USB request on a non-USB Core.
  • Silent: unknown/typo’d top-level keys, and omitted optional fields (they take their defaults).

Recipes

GPIO in + out, falling-edge interrupt with pull-up

{
  "core": "Core.ST.L0",
  "clock": "max",
  "pads": { "11": "GPIO.OUT", "8": "GPIO.IN" },
  "gpio": { "8": { "pull": "up", "exti": "falling" } }
}

Two I²C buses at different speeds, three tiles

{
  "core": "Core.ST.L4",
  "clock": "max",
  "pads": {
    "4": "I2C1.CLK", "5": "I2C1.DAT",
    "2": "I2C3.CLK", "8": "I2C3.DAT"
  },
  "interfaces": {
    "I2C1": { "speed": 400000 },
    "I2C3": { "speed": 100000 }
  },
  "tiles": [
    { "tile": "Display.RGBW", "bus": "I2C1", "instance": 0 },
    { "tile": "Sense.I.6P6",  "bus": "I2C3", "instance": 0 },
    { "tile": "Sense.T.C",    "bus": "I2C3", "instance": 0 }
  ]
}

SPI tile with software CS (SPI3 on pads 2/6/7, CS on pad 3)

{
  "core": "Core.ST.W5",
  "clock": "high",
  "pads": {
    "2": "SPI3.CLK", "6": "SPI3.MOSI", "7": "SPI3.MISO", "3": "SPI3.CS"
  },
  "interfaces": { "SPI3": { "mode": 0, "prescaler": 8 } },
  "tiles": [
    { "tile": "Store.O.128", "bus": "SPI3", "instance": 0, "cs_pad": "3" }
  ]
}

PWM on two channels (1 kHz each; change with core_timer_set_freq())

{
  "core": "Core.ST.L4.2",
  "clock": "max",
  "pads": { "7": "TIM15.1", "8": "TIM2.1" }
}

ADC + UART logging

{
  "core": "Core.ST.L0",
  "clock": "high",
  "pads": { "6": "ADC1", "7": "USART2.TX", "2": "USART2.RX" },
  "interfaces": { "USART2": { "baud": 115200 } }
}

USB CDC + ROM-DFU bootloader + watchdog

Core.ST.L4.2’s USB is on pads 16/17 and always on, so it needs no pads.

{
  "core": "Core.ST.L4.2",
  "clock": "max",
  "bootloader": "rom",
  "usb": { "enabled": true },
  "iwdg": { "enabled": true, "timeout_ms": 5000 },
  "pads": {}
}
Next
See Coregen for how this config becomes C headers, and Bootloading for flashing.