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 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.
| Key | Type | Required | Default | Read? |
|---|---|---|---|---|
core | string | yes | — | ✅ |
clock | string | no | tile's default | ✅ |
pads | object | no | {} | ✅ |
interfaces | object | no | {} | ✅ (tuning) |
gpio | object | no | {} | ✅ |
tiles | array | no | [] | ✅ |
usb | object | no | disabled | ✅ |
bootloader | string | no | "none" | ✅ |
timer | object | no | none | ✅ (Studio) |
pins | object | no | — | ✅ (legacy alias for pads) |
ble, debug, isp, programming | object | no | — | ❌ ignored |
core — pick the MCU/core (required)
Selects definitions/<Core>.json. Prefer the public name (the DB stem also works).
| Public name | MCU | Max SYSCLK | USB? |
|---|---|---|---|
Core.ST.L0.1 | STM32L011 | 32 MHz | no |
Core.ST.L4.1 | STM32L422 | 80 MHz | yes |
Core.ST.L4.2 | STM32L422 | 80 MHz | yes |
Core.ST.W5 | STM32WBA55 | 100 MHz | no |
Core.ST.H5.1 | STM32H523 | 250 MHz | yes |
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.
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.
| Family | Form | Examples |
|---|---|---|
| GPIO | GPIO.OUT / GPIO.IN | "GPIO.OUT" |
| I2C | I2C<n>.CLK / .DAT | "I2C1.CLK", "I2C3.DAT" |
| SPI | SPI<n>.CLK/MOSI/MISO/CS | "SPI1.CLK", "SPI1.CS" |
| USART | USART<n>.TX / .RX | "USART2.TX" |
| Timer/PWM | TIM<n>.<ch> (ch 1–4) | "TIM2.1", "TIM15.2" |
| ADC | ADC / ADC<n> / ADC_IN<n> [+] | "ADC1", "ADC5", "ADC_IN3", "ADC7+" |
| DAC | DAC1.OUT | "DAC1.OUT" |
| USB | USB.DP / USB.DM | "USB.DP" |
- SPI CS is software-managed.
SPI*.CSpads become plain GPIO outputs driven byhal_spi_set_cs()— not hardware NSS. Not configurable. - ADC is single-ended positive only. Differential negative inputs aren’t emitted; coregen emits one
core_adc1handle today. - A bus needs all its pads. An I2C bus needs both
CLKandDAT; SPI needsCLK+MOSI+MISO.
Interfaces — tune the buses
Keyed by peripheral name (I2C1, SPI1, USART2, TIM2, …). Only tunes a bus a pad already created.
I²C
| Field | Type | Default | Allowed |
|---|---|---|---|
speed | int | 400000 | 100000, 400000, 1000000 |
pullups | bool | true | enable internal pad pull-ups |
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
| Field | Type | Default | Allowed |
|---|---|---|---|
mode | int | 0 | 0–3 (CPOL = bit1, CPHA = bit0) |
prescaler | int | 8 | 2,4,8,16,32,64,128,256 |
cs_polarity | string | "active-low" | "active-low" | "active-high" |
USART / UART
| Field | Type | Default | Notes |
|---|---|---|---|
baud | int | 115200 | any rate |
rx_interrupt | bool | false | true = interrupt-driven RX with a ring buffer |
Timer / PWM
| Field | Type | Default | Notes |
|---|---|---|---|
freq | int | 1000 | overflow/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).
| Field | Default | Allowed / meaning |
|---|---|---|
pull | "none" | "none" | "up" | "down" |
output_type | "push-pull" | "push-pull" | "open-drain" |
speed | "medium" | slew rate (passed through) |
exti | none | "rising" | "falling" | "both" |
default | none | "high" | "low" (initial output state) |
tiles — attach drivers to buses
| Field | Type | Required | Meaning |
|---|---|---|---|
tile | string | yes | a name in coregen’s TILE_DRIVER_MAP |
bus | string | yes | must match a configured I2C*/SPI* interface |
instance | int | no (0) | I2C: address slot · SPI: per-CS index |
cs_pad | string | SPI only | pad 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
| Field | Default | Notes |
|---|---|---|
enabled | false | true adds USB-CDC init to core_init() |
vid / pid | SDK default | "0x1209" or 4617 |
product / manufacturer / serial | SDK default | descriptor 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
| Value | Layout | Cores | Needs USB |
|---|---|---|---|
"none" | app at 0x08000000, SWD only (default) | all | no |
"custom" | 8 KB DFU at 0x08000000, app at 0x08002000 | L4 / H5 | yes |
"rom" | ST ROM DfuSe, app at 0x08000000 | L4 (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 key | What people expect | Reality |
|---|---|---|
ble | enable the radio / HSE | No-op. Use an HSE-sourced clock level. |
debug | emit SWD/JTAG defines | No-op. |
isp / programming | boot0_pad, methods… | No-op. |
timers | per-timer freq/tick | No-op. Use interfaces.TIM<n>.freq. |
pwm | per-channel duty | No-op. Duty is a runtime API call. |
capture | input-capture mapping | No-op (not config-driven). |
iwdg | watchdog enable/timeout | No-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
clocklevel; an I²Cspeedout of set or its kernel clock too low; an SPImode/prescalerout of range; an unknown tile, a tile on an unconfigured bus, or an SPI tile whosecs_paddoesn’t resolve; an invalidbootloader. - Warns (non-fatal) on: a
corename not matching the definition; aninterfacesentry 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" }
}
