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 (interfaces.TIM2.freq)
"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{}
tilesarrayno[]
usbobjectnodisabled
bootloaderstringno"none"
timerobjectnonone✅ (Studio)
pinsobjectno✅ (legacy alias for pads)
ble, 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.L0.1STM32L01132 MHzno
Core.ST.L4.1STM32L42280 MHzyes
Core.ST.L4.2STM32L42280 MHzyes
Core.ST.W5STM32WBA55100 MHzno
Core.ST.H5.1STM32H523250 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.

Radio (WBA55) needs HSE — there is no ble switch
To run the wireless Core’s radio you must pick a clock level whose source is HSE (check config.clock in the Core definition). A ble key does nothing (see Ignored keys).

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 the I²C kernel clock is hardware-fixed at HSI16 (16 MHz) regardless of SYSCLK, capping it at 400 kHz; on H5 it follows SYSCLK. A too-low clock can make a fast speed illegal.

SPI

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

USART / UART

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

Timer / PWM

FieldTypeDefaultNotes
freqint1000overflow/PWM frequency in Hz; shared by all channels on that timer

Channels come from the TIM<n>.<ch> pad functions; duty cycle is set at runtime via the core_pwm API, not in config. There is no pwm, capture, timers, or iwdg 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

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

usb

FieldDefaultNotes
enabledfalsetrue adds USB-CDC init to core_init()
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 / H5yes
"rom"ST ROM DfuSe, app at 0x08000000L4 (full); H5 (power-cycle)yes

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

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
bleenable the radio / HSENo-op. Use an HSE-sourced clock level.
debugemit SWD/JTAG definesNo-op.
isp / programmingboot0_pad, methods…No-op.
timersper-timer freq/tickNo-op. Use interfaces.TIM<n>.freq.
pwmper-channel dutyNo-op. Duty is a runtime API call.
captureinput-capture mappingNo-op (not config-driven).
iwdgwatchdog enable/timeoutNo-op.

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.
  • 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.1",
  "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.1",
  "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

{
  "core": "Core.ST.W5",
  "clock": "max",
  "pads": {
    "9": "SPI1.CLK", "6": "SPI1.MOSI", "7": "SPI1.MISO", "8": "SPI1.CS"
  },
  "interfaces": { "SPI1": { "mode": 0, "prescaler": 8 } },
  "tiles": [
    { "tile": "Drive.H", "bus": "SPI1", "instance": 0, "cs_pad": "8" }
  ]
}

PWM on two channels

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

ADC + UART logging

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

USB CDC + ROM-DFU bootloader

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