diff --git a/scripts/README.md b/scripts/README.md index f74eac07..c071f606 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -30,3 +30,43 @@ Devicetree binding headers for Series 2 are generated using the following script * Clock Control bindings: `gen_clock_control.py` * Pin Control bindings: `gen_pinctrl.py` * DAC bindings: `gen_vdac.py` + +## Devicetree Files + +### SoC Devicetree + +SoC devicetree for Series 2 is generated using the `gen_dts_soc_series2.py` script. +The script takes the following inputs: + +* `sdk` -- Path to Simplicity SDK or device package to extract data from. +* `family` -- The family to generate .dtsi files for. +* `soc-yml` -- The soc.yml to use for filtering. +* `out` -- The output path. + +Example usage: + +```sh +./scripts/gen_dts_soc_series2.py \ + -f xg24 \ + -s ~/sisdk-release/ \ + -o $ZEPHYR_BASE/dts/arm/silabs/ \ + -y $ZEPHYR_BASE/soc/silabs/soc.yml +``` + +### Board Devicetree + +Board devicetree is generated using the `gen_dts_board.py` script. +The script takes the following inputs: + +* `sdk` -- Path to Simplicity SDK or boards package to extract data from. +* `board` -- The board to generate .dts files for. +* `out` -- The output path. + +Example usage: + +```sh +./scripts/gen_dts_board.py \ + -b xg24_dk2601b \ + -s ~/sisdk-release/ \ + -o $ZEPHYR_BASE/boards/silabs/dev_kits/xg24_dk2601b/ +``` diff --git a/scripts/dts/__init__.py b/scripts/dts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/dts/board/__init__.py b/scripts/dts/board/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/dts/board/adc.py b/scripts/dts/board/adc.py new file mode 100644 index 00000000..b381b66c --- /dev/null +++ b/scripts/dts/board/adc.py @@ -0,0 +1,83 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + + +def pin_and_bus(b: Board, define: str): + port = b.config[f"{define}_PORT"][-1] + pin = int(b.config[f"{define}_PIN"]) + + analog_bus = { + "A": "A", + "B": "B", + "C": "CD", + "D": "CD", + } + + bus = f"{analog_bus[port]}{'ODD' if pin % 2 else 'EVEN'}0" + portpin = f"P{port}{pin}" + + return (portpin, bus) + + +def generate(b: Board, dt: Node, pinctrl: Node): + if b.config.get("SL_JOYSTICK_PORT"): + adc = Node(labels=["adc0"], status="okay") + channel_no = 0 + + joystick = Node("joystick", compatible="adc-keys") + joystick.add_phandle_array("io-channels", ["adc0", channel_no]) + joystick.add_int("keyup-threshold-mv", b.config.get("REFERENCE_VOLTAGE")) + dt.add_node(joystick) + + dt.add_include("zephyr/dt-bindings/input/input-event-codes.h") + + keys = { + "enter": "JOYSTICK_MV_C", + "left": "JOYSTICK_MV_W", + "down": "JOYSTICK_MV_S", + "up": "JOYSTICK_MV_N", + "right": "JOYSTICK_MV_E", + } + + for key, define in keys.items(): + if mv := b.config.get(define): + node = Node(f"{key}-key") + node.add_int("press-thresholds-mv", mv) + node.add_int("zephyr,code", f"INPUT_KEY_{key.upper()}") + joystick.add_node(node) + + pin, bus = pin_and_bus(b, "SL_JOYSTICK") + pins, props = b.pinctrl( + "iadc0", + "default", + PinctrlGroup( + "group0", + None, + abus=[f"ABUS_{bus}_IADC0"], + ), + ) + pinctrl.add_node(pins) + adc.add_props(props) + + adc.add_int("#address-cells", 1) + adc.add_int("#size-cells", 0) + + channel = Node("channel", reg=[channel_no]) + channel.add_int("zephyr,acquisition-time", "ADC_ACQ_TIME_DEFAULT") + channel.add_string("zephyr,gain", "ADC_GAIN_1") + channel.add_int("zephyr,input-positive", f"IADC_INPUT_{pin}") + channel.add_string("zephyr,reference", "ADC_REF_VDD_1") + channel.add_int("zephyr,resolution", 12) + channel.add_int("zephyr,vref-mv", b.config.get("REFERENCE_VOLTAGE")) + adc.add_node(channel) + + user = Node("zephyr,user") + user.add_phandle_array("io-channels", ["adc0", channel_no]) + dt.add_node(user) + + adc.add_include("zephyr/dt-bindings/adc/silabs-adc.h") + + return [adc] diff --git a/scripts/dts/board/bt.py b/scripts/dts/board/bt.py new file mode 100644 index 00000000..04b616c9 --- /dev/null +++ b/scripts/dts/board/bt.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + if "device_supports_bluetooth" in b.soc_features: + node = Node(labels=["bt_hci_silabs"], status="okay") + + dt.find("chosen").select("zephyr,bt-hci", node) + return [node] diff --git a/scripts/dts/board/buttons.py b/scripts/dts/board/buttons.py new file mode 100644 index 00000000..5609bba2 --- /dev/null +++ b/scripts/dts/board/buttons.py @@ -0,0 +1,35 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + button_spec = { + "button0": "SL_SIMPLE_BUTTON_BTN0", + "button1": "SL_SIMPLE_BUTTON_BTN1", + "button2": "SL_SIMPLE_BUTTON_BTN2", + } + buttons = [] + for label, define in button_spec.items(): + if define + "_PORT" not in b.config: + continue + btn = Node(f"button_{label[-1]}", [label]) + btn.add_prop(b.gpios([define], "gpios", False)) + btn.add_string("label", f"Push Button {label[-1]}") + btn.add_int("zephyr,code", f"INPUT_BTN_{label[-1]}") + buttons.append(btn) + + if buttons: + aliases = dt.find("aliases") + + buttons_dt = Node("buttons", compatible="gpio-keys") + for btn in buttons: + buttons_dt.add_node(btn) + aliases.select(f"sw{btn.labels[0][-1]}", btn) + dt.add_node(buttons_dt) + + dt.add_include("zephyr/dt-bindings/input/input-event-codes.h") diff --git a/scripts/dts/board/clocks.py b/scripts/dts/board/clocks.py new file mode 100644 index 00000000..842c127b --- /dev/null +++ b/scripts/dts/board/clocks.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + nodes = [] + + if b.config.get("SL_CLOCK_MANAGER_HFXO_EN"): + hfxo = Node(labels=["hfxo"], status="okay") + hfxo.add_int("precision", b.config.get("SL_CLOCK_MANAGER_HFXO_PRECISION")) + hfxo.add_array("ctune", [b.config.get("SL_CLOCK_MANAGER_HFXO_CTUNE")]) + nodes.append(hfxo) + dt.add_include(b.soc_dir / "clock-hfrcodpll.dtsi") + + if b.config.get("SL_CLOCK_MANAGER_LFXO_EN"): + lfxo = Node(labels=["lfxo"], status="okay") + lfxo.add_int("precision", b.config.get("SL_CLOCK_MANAGER_LFXO_PRECISION")) + lfxo.add_int("ctune", b.config.get("SL_CLOCK_MANAGER_LFXO_CTUNE")) + nodes.append(lfxo) + dt.add_include(b.soc_dir / "clock-lfxo.dtsi") + + return nodes diff --git a/scripts/dts/board/dac.py b/scripts/dts/board/dac.py new file mode 100644 index 00000000..31cf3262 --- /dev/null +++ b/scripts/dts/board/dac.py @@ -0,0 +1,17 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + candidates = ["vdac0", "vdac1"] + nodes = [] + + for dac in candidates: + if f"device_has_{dac}" in b.soc_features: + node = Node(labels=[dac], status="okay") + nodes.append(node) + + return nodes diff --git a/scripts/dts/board/dcdc.py b/scripts/dts/board/dcdc.py new file mode 100644 index 00000000..57c02b08 --- /dev/null +++ b/scripts/dts/board/dcdc.py @@ -0,0 +1,56 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, pinctrl: Node): + if b.soc_config.get("device.dcdc") == "BOOST": + # Boost DCDC is handled at SoC level + return + + if b.config.get("SL_DEVICE_INIT_DCDC_ENABLE") in [None, "1"]: + node = Node(labels=["dcdc"], status="okay") + node.add_include("zephyr/dt-bindings/regulator/silabs_dcdc.h") + + if b.config.get("SL_DEVICE_INIT_DCDC_BYPASS") != "1": + node.add_bool("regulator-boot-on", True) + + node.add_int("regulator-initial-mode", "SILABS_DCDC_MODE_BUCK") + + ipkvals = { + "3": 50, + "4": 65, + "5": 73, + "6": 80, + "7": 86, + "8": 93, + "9": 100, + "10": 106, + "11": 113, + "12": 120, + } + + ipkval = None + if b.config.get("SL_DEVICE_INIT_DCDC_PFMX_IPKVAL_OVERRIDE") == "1": + ipkval = b.config.get("SL_DEVICE_INIT_DCDC_PFMX_IPKVAL", "12") + elif b.soc_family == "xg23": + ipkval = "9" + if b.soc_config.get("device.efr32_subghz_hp_pa_max_output") == "20": + ipkval = "6" + elif b.soc_family == "xg24": + try: + b.soc_config.get("device.efr32_2g4hz_hp_pa_max_output") + ipkval = "9" + except KeyError: + ipkval = "12" + elif b.soc_family == "xg26": + ipkval = "12" + elif b.soc_family == "xg28": + ipkval = "9" + + if ipkval is not None: + node.add_int("silabs,pfmx-peak-current-milliamp", ipkvals[ipkval]) + + return [node] diff --git a/scripts/dts/board/exp_header.py b/scripts/dts/board/exp_header.py new file mode 100644 index 00000000..43eb751d --- /dev/null +++ b/scripts/dts/board/exp_header.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + gpio_map = [] + for i in range(3, 17): + prefix = f"SL_HAL_GPIO_INIT_EXP_{i}" + if prefix + "_PORT" not in b.config: + continue + + port = "&gpio" + b.config.get(prefix + "_PORT")[-1].lower() + pin = b.config.get(prefix + "_PIN") + + gpio_map.append([i, 0, port, pin, 0]) + + if gpio_map: + exp = Node("exp-header", labels=["exp_header"], compatible="silabs,exp-header") + exp.add_int("#gpio-cells", 2) + exp.add_array("gpio-map", gpio_map) + exp.prop("gpio-map").fmt_one_element_per_line = True + exp.add_hex_array("gpio-map-mask", [0xFFFFFFFF, 0x0]) + exp.add_hex_array("gpio-map-pass-thru", [0x0, "GPIO_DT_FLAGS_MASK"]) + + dt.add_node(exp) diff --git a/scripts/dts/board/gpio.py b/scripts/dts/board/gpio.py new file mode 100644 index 00000000..af0e1a07 --- /dev/null +++ b/scripts/dts/board/gpio.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, pinctrl: Node): + ports = { + "gpioa": [], + "gpiob": [], + "gpioc": [], + "gpiod": [], + } + + if port := b.config.get("SL_BOARD_ENABLE_VCOM_PORT"): + port = "gpio" + port[-1].lower() + pin = b.config.get("SL_BOARD_ENABLE_VCOM_PIN") + + hog = Node("board-controller-enable") + hog.add_bool("gpio-hog", True) + hog.add_array("gpios", [pin, "GPIO_ACTIVE_HIGH"]) + hog.add_bool("output-high", True) + + ports[port].append(hog) + + nodes = [] + + for port, children in ports.items(): + node = Node(labels=[port], status="okay") + node.add_nodes(children) + nodes.append(node) + + return nodes diff --git a/scripts/dts/board/i2c.py b/scripts/dts/board/i2c.py new file mode 100644 index 00000000..a37a80ab --- /dev/null +++ b/scripts/dts/board/i2c.py @@ -0,0 +1,91 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + +sensors = { + "veml6035": { + "node": Node( + "veml6035", labels=["veml6035"], compatible="vishay,veml7700", reg=[0x29] + ), + }, + "si7021": { + "node": Node( + "si7021", labels=["si7021"], compatible="silabs,si7006", reg=[0x40] + ), + "alias": "dht0", + "enable": ("vin-supply", "SL_BOARD_ENABLE_SENSOR_RHT", "rht_enable"), + }, + "si7210": { + "node": Node( + "si7210", labels=["si7210"], compatible="silabs,si7210", reg=[0x30] + ), + }, +} + + +def generate(b: Board, dt: Node, pinctrl: Node): + nodes = [] + + if i2c := b.config.get("SL_I2C_SENSOR_PERIPHERAL"): + node = Node(labels=[i2c.lower()], status="okay") + + pins, props = b.pinctrl( + i2c.lower(), + "default", + PinctrlGroup( + "group0", + b.signals(i2c, "SL_I2C_SENSOR", ["SDA", "SCL"]), + "bias-pull-up", + "drive-open-drain", + ), + ) + pinctrl.add_node(pins) + node.add_props(props) + nodes.append(node) + + for sensor in b.sensors: + if data := sensors.get(sensor): + sensor_dt = data["node"] + if en := data.get("enable"): + if b.config.get(en[1] + "_PORT"): + sensor_dt.add_phandle(en[0], en[2]) + + node.add_node(sensor_dt) + + if c := data.get("chosen"): + dt.find("chosen").select(c, sensor_dt) + + if alias := data.get("alias"): + dt.find("aliases").select(alias, sensor_dt) + + connectors = { + "QWIIC": "zephyr_i2c", + "MIKROE": "mikrobus_i2c", + } + + for connector, label in connectors.items(): + if i2c := b.config.get(f"SL_I2C_{connector}_PERIPHERAL"): + for node in nodes: + if node.labels[0] == i2c.lower(): + node.labels.append(label) + break + else: + node = Node(labels=[i2c.lower(), label], status="okay") + + pins, props = b.pinctrl( + i2c.lower(), + "default", + PinctrlGroup( + "group0", + b.signals(i2c, f"SL_I2C_{connector}", ["SDA", "SCL"]), + "bias-pull-up", + "drive-open-drain", + ), + ) + pinctrl.add_node(pins) + node.add_props(props) + nodes.append(node) + + return nodes diff --git a/scripts/dts/board/itm.py b/scripts/dts/board/itm.py new file mode 100644 index 00000000..435e2355 --- /dev/null +++ b/scripts/dts/board/itm.py @@ -0,0 +1,24 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + + +def generate(b: Board, dt: Node, pinctrl: Node): + node = Node(labels=["itm"]) + + pins, props = b.pinctrl( + "itm", + "default", + PinctrlGroup( + "group0", + ["GPIO_SWV_PA3"], + "drive-push-pull", + "output-high", + ), + ) + pinctrl.add_node(pins) + node.add_props(props) + + return [node] diff --git a/scripts/dts/board/leds.py b/scripts/dts/board/leds.py new file mode 100644 index 00000000..873181f0 --- /dev/null +++ b/scripts/dts/board/leds.py @@ -0,0 +1,29 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + led_spec = { + "led0": "SL_SIMPLE_LED_LED0", + "led1": "SL_SIMPLE_LED_LED1", + "led2": "SL_SIMPLE_LED_LED2", + } + leds = [] + for label, define in led_spec.items(): + if define + "_PORT" not in b.config: + continue + led = Node(f"led_{label[-1]}", [label]) + led.add_prop(b.gpios([define], "gpios")) + leds.append(led) + + if leds: + aliases = dt.find("aliases") + + leds_dt = Node("leds", compatible="gpio-leds") + for led in leds: + leds_dt.add_node(led) + aliases.select(led.labels[0], led) + dt.add_node(leds_dt) diff --git a/scripts/dts/board/memory.py b/scripts/dts/board/memory.py new file mode 100644 index 00000000..9fd36d8d --- /dev/null +++ b/scripts/dts/board/memory.py @@ -0,0 +1,72 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + flash = Node(labels=["flash0"]) + sram = Node(labels=["sram0"]) + + chosen = dt.find("chosen") + chosen.select("zephyr,flash", flash) + chosen.select("zephyr,sram", sram) + + partitions = Node("partitions") + partitions.add_bool("ranges", True) + partitions.add_int("#address-cells", 1) + partitions.add_int("#size-cells", 1) + flash.add_node(partitions) + + flash_size_kb = b.soc_config.get("memory.flash.size") // 1024 + + boot_partition_size_kb = 48 + storage_partition_size_kb = 32 + + remaining = flash_size_kb - boot_partition_size_kb - storage_partition_size_kb + image0_pages = remaining // 16 + image0_partition_size_kb = image0_pages * 8 + image1_partition_size_kb = remaining - image0_partition_size_kb + + boot_partition = Node( + "partition", + compatible="zephyr,mapped-partition", + labels=["boot_partition"], + reg=[0, f"DT_SIZE_K({boot_partition_size_kb})"], + ) + boot_partition.add_string("label", "mcuboot") + partitions.add_node(boot_partition) + offset = boot_partition_size_kb + + image0_partition = Node( + "partition", + compatible="zephyr,mapped-partition", + labels=["slot0_partition"], + reg=[offset * 1024, f"DT_SIZE_K({image0_partition_size_kb})"], + ) + image0_partition.add_string("label", "image-0") + partitions.add_node(image0_partition) + dt.find("chosen").select("zephyr,code-partition", image0_partition) + offset += image0_partition_size_kb + + image1_partition = Node( + "partition", + compatible="zephyr,mapped-partition", + labels=["slot1_partition"], + reg=[offset * 1024, f"DT_SIZE_K({image1_partition_size_kb})"], + ) + image1_partition.add_string("label", "image-1") + partitions.add_node(image1_partition) + offset += image1_partition_size_kb + + storage_partition = Node( + "partition", + compatible="zephyr,mapped-partition", + labels=["storage_partition"], + reg=[offset * 1024, f"DT_SIZE_K({storage_partition_size_kb})"], + ) + storage_partition.add_string("label", "storage") + partitions.add_node(storage_partition) + + return [flash] diff --git a/scripts/dts/board/pti.py b/scripts/dts/board/pti.py new file mode 100644 index 00000000..9f6b1be3 --- /dev/null +++ b/scripts/dts/board/pti.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + + +def generate(b: Board, dt: Node, pinctrl: Node): + if pti := b.config.get("SL_RAIL_UTIL_PTI_PERIPHERAL"): + node = Node(labels=[pti.lower()], status="okay") + + pins, props = b.pinctrl( + pti.lower(), + "default", + PinctrlGroup( + "group0", + b.signals(pti, "SL_RAIL_UTIL_PTI", ["DOUT", "DFRAME"]), + "drive-push-pull", + "output-high", + ), + ) + pinctrl.add_node(pins) + node.add_props(props) + + return [node] diff --git a/scripts/dts/board/pwmleds.py b/scripts/dts/board/pwmleds.py new file mode 100644 index 00000000..dda90580 --- /dev/null +++ b/scripts/dts/board/pwmleds.py @@ -0,0 +1,71 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + + +def generate(b: Board, dt: Node, pinctrl: Node): + led_spec = { + "pwm_led0": "SL_PWM_LED0", + "pwm_led1": "SL_PWM_LED1", + "pwm_led2": "SL_PWM_LED2", + } + timer = "timer0" + leds = [] + signals = [] + + for label, define in led_spec.items(): + idx = label[-1] + if define + "_OUTPUT_PORT" not in b.config: + continue + led = Node(f"pwm_led_{idx}", [label]) + + port = b.config[f"{define}_OUTPUT_PORT"] + pin = b.config[f"{define}_OUTPUT_PIN"] + invert = "LOW" in b.config.get(f"{define}_POLARITY", "HIGH") + + led.add_phandle_array( + "pwms", + [ + f"{timer}_pwm", + idx, + "PWM_MSEC(20)", + f"PWM_POLARITY_{'INVERTED' if invert else 'NORMAL'}", + ], + ) + leds.append(led) + signals.append((f"TIMER0_CC{idx}_P{port[-1]}{pin}", invert)) + + if leds: + aliases = dt.find("aliases") + + dt.add_include("zephyr/dt-bindings/pwm/pwm.h") + + leds_dt = Node("pwmleds", compatible="pwm-leds") + for led in leds: + leds_dt.add_node(led) + aliases.select(led.labels[0].replace("_", "-"), led) + dt.add_node(leds_dt) + + pins, props = b.pinctrl( + timer, + "default", + PinctrlGroup( + "group0", + [s[0] for s in signals], + "drive-push-pull", + "output-high" if signals[0][1] else "output-low", + ), + ) + + pinctrl.add_node(pins) + + t = Node(labels=[timer], status="okay") + + pwm = Node("pwm", labels=[f"{timer}_pwm"], status="okay") + for prop in props: + pwm.add_prop(prop) + t.add_node(pwm) + + return [t] diff --git a/scripts/dts/board/qwiic.py b/scripts/dts/board/qwiic.py new file mode 100644 index 00000000..0b5b82fa --- /dev/null +++ b/scripts/dts/board/qwiic.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + if b.config.get(f"SL_I2C_QWIIC_PERIPHERAL"): + gpio_map = [] + for i, p in enumerate(["SCL", "SDA"]): + prefix = f"SL_I2C_QWIIC_{p}" + if prefix + "_PORT" not in b.config: + continue + + port = "&gpio" + b.config.get(prefix + "_PORT")[-1].lower() + pin = b.config.get(prefix + "_PIN") + + gpio_map.append([i, 0, port, pin, 0]) + + if gpio_map: + conn = Node( + "stemma-qt-connector", + labels=["qwiic_connector"], + compatible="stemma-qt-connector", + ) + conn.add_int("#gpio-cells", 2) + conn.add_array("gpio-map", gpio_map) + conn.prop("gpio-map").fmt_one_element_per_line = True + conn.add_hex_array("gpio-map-mask", [0xFFFFFFFF, 0x0]) + conn.add_hex_array("gpio-map-pass-thru", [0x0, "GPIO_DT_FLAGS_MASK"]) + + dt.add_node(conn) diff --git a/scripts/dts/board/radio.py b/scripts/dts/board/radio.py new file mode 100644 index 00000000..cd66e560 --- /dev/null +++ b/scripts/dts/board/radio.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, pinctrl: Node): + radio = Node(labels=["radio"]) + + if voltage := b.config.get("SL_RAIL_UTIL_PA_VOLTAGE_MV"): + if int(voltage) != 3300: + radio.add_int("pa-voltage-mv", voltage) + + if switch_mode := b.config.get("SL_RAIL_UTIL_RF_PATH_SWITCH_RADIO_ACTIVE_MODE"): + if "COMBINE" in switch_mode: + radio.add_bool("rf-path-switch-combine", True) + + if b.config.get("SL_RAIL_UTIL_RF_PATH_SWITCH_RADIO_ACTIVE_PORT"): + radio.add_prop( + b.gpios( + ["SL_RAIL_UTIL_RF_PATH_SWITCH_RADIO_ACTIVE"], + "rf-path-switch-radio-active-gpios", + True, + ) + ) + + if b.config.get("SL_RAIL_UTIL_RF_PATH_SWITCH_CONTROL_PORT"): + radio.add_prop( + b.gpios( + ["SL_RAIL_UTIL_RF_PATH_SWITCH_CONTROL"], + "rf-path-switch-control-gpios", + False, + ) + ) + + if b.config.get("SL_RAIL_UTIL_RF_PATH_SWITCH_INVERTED_CONTROL_PORT"): + radio.add_prop( + b.gpios( + ["SL_RAIL_UTIL_RF_PATH_SWITCH_INVERTED_CONTROL"], + "rf-path-switch-control-gpios", + True, + ) + ) + + if radio.props: + return [radio] + else: + return None diff --git a/scripts/dts/board/rng.py b/scripts/dts/board/rng.py new file mode 100644 index 00000000..8ff20512 --- /dev/null +++ b/scripts/dts/board/rng.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + candidates = {"semailbox": "se", "cryptoacc": "trng"} + + for feat, label in candidates.items(): + if f"device_has_{feat}" in b.soc_features: + node = Node(labels=[label], status="okay") + return [node] diff --git a/scripts/dts/board/rtc.py b/scripts/dts/board/rtc.py new file mode 100644 index 00000000..466ed0fc --- /dev/null +++ b/scripts/dts/board/rtc.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + candidates = ["sysrtc0", "rtcc0"] + + for rtc in candidates: + if f"device_has_{rtc}" in b.soc_features: + node = Node(labels=[rtc], status="okay") + return [node] diff --git a/scripts/dts/board/sensor_enable.py b/scripts/dts/board/sensor_enable.py new file mode 100644 index 00000000..00bb7016 --- /dev/null +++ b/scripts/dts/board/sensor_enable.py @@ -0,0 +1,40 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + sensors = [ + "SL_BOARD_ENABLE_SENSOR_RHT", + "SL_BOARD_ENABLE_SENSOR_HALL", + "SL_BOARD_ENABLE_SENSOR_PRESSURE", + "SL_BOARD_ENABLE_SENSOR_LIGHT", + "SL_BOARD_ENABLE_SENSOR_IMU", + "SL_BOARD_ENABLE_SENSOR_MICROPHONE", + ] + + nodes = {} + + for en in sensors: + if en + "_PORT" not in b.config: + continue + gpios = b.gpios([en], "enable-gpios") + + name = f"{en.rsplit('_', 1)[-1].lower()}-enable" + + if str(gpios) in nodes: + nodes[str(gpios)].labels.append(name.replace("-", "_")) + nodes[str(gpios)].name = "sensor-enable" + else: + node = Node( + name, labels=[name.replace("-", "_")], compatible="regulator-fixed" + ) + node.add_prop(gpios) + + nodes[str(gpios)] = node + + for node in nodes.values(): + node.add_string("regulator-name", node.name) + dt.add_node(node) diff --git a/scripts/dts/board/spi.py b/scripts/dts/board/spi.py new file mode 100644 index 00000000..2046061f --- /dev/null +++ b/scripts/dts/board/spi.py @@ -0,0 +1,167 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup +from dts.prop import ( + ArrayProperty, + IntProperty, + BoolProperty, + Uint8ArrayProperty, + StringProperty, +) + +flashes = { + "mx25r8035f": [ + Uint8ArrayProperty("jedec-id", [0xC2, 0x28, 0x14]), + ArrayProperty("dpd-wakeup-sequence", [30000, 20, 35000]), + BoolProperty("has-dpd", True), + StringProperty("mxicy,mx25r-power-mode", "low-power"), + BoolProperty("zephyr,pm-device-runtime-auto", True), + IntProperty("size", "DT_SIZE_M(8)"), + IntProperty("spi-max-frequency", "DT_FREQ_M(33)"), + ], + "mx25r3235f": [ + Uint8ArrayProperty("jedec-id", [0xC2, 0x28, 0x16]), + ArrayProperty("dpd-wakeup-sequence", [30000, 20, 35000]), + BoolProperty("has-dpd", True), + StringProperty("mxicy,mx25r-power-mode", "low-power"), + BoolProperty("zephyr,pm-device-runtime-auto", True), + IntProperty("size", "DT_SIZE_M(32)"), + IntProperty("spi-max-frequency", "DT_FREQ_M(80)"), + ], +} + + +def default_spi_config(label): + node = Node(labels=[label], status="okay") + node.add_int("#address-cells", 1) + node.add_int("#size-cells", 0) + node.add_int("clock-frequency", "DT_FREQ_M(8)") + + return { + "node": node, + "out_signals": set(), + "in_signals": set(), + "reg": 0, + } + + +def add_cs_gpio(b: Board, node: Node, key: str, polarity: bool = None): + if gpio := node.prop("cs-gpios"): + gpio.value += b.gpios([key], "cs-gpios", polarity=polarity, raw=True) + else: + node.add_prop(b.gpios([key], "cs-gpios", polarity=polarity)) + + +def create_on_bus(node, name, compatible=None): + reg = 0 + if len(node.nodes): + reg = node.nodes[-1].address + 1 + + child = Node( + name, labels=[name], compatible=compatible, reg=[reg], reg_is_hex=False + ) + node.add_node(child) + + return child + + +def generate(b: Board, dt: Node, pinctrl: Node): + peripherals = {} + + if flash := b.config.get("SL_MX25_FLASH_SHUTDOWN_PERIPHERAL"): + if flash not in peripherals: + peripherals[flash] = default_spi_config(flash.lower()) + + peripherals[flash]["out_signals"].update( + b.signals( + flash, + "SL_MX25_FLASH_SHUTDOWN", + ["TX", "SCLK" if flash.startswith("E") else "CLK"], + ) + ) + peripherals[flash]["in_signals"].update( + b.signals(flash, "SL_MX25_FLASH_SHUTDOWN", ["RX"]) + ) + + add_cs_gpio(b, peripherals[flash]["node"], "SL_MX25_FLASH_SHUTDOWN_CS", False) + + child = create_on_bus( + peripherals[flash]["node"], b.spi_flash, compatible="jedec,spi-nor" + ) + if data := flashes.get(b.spi_flash): + child.add_props(data) + dt.find("aliases").select("spi-flash0", child) + + if display := b.config.get("SL_MEMLCD_SPI_PERIPHERAL"): + if display not in peripherals: + peripherals[display] = default_spi_config(display.lower()) + + peripherals[display]["out_signals"].update( + b.signals( + display, + "SL_MEMLCD_SPI", + ["TX", "SCLK" if display.startswith("E") else "CLK"], + ) + ) + + add_cs_gpio(b, peripherals[display]["node"], "SL_MEMLCD_SPI_CS", True) + + child = create_on_bus( + peripherals[flash]["node"], b.display, compatible="sharp,ls0xx" + ) + child.add_int("height", 128) + child.add_int("width", 128) + child.add_int("spi-max-frequency", "DT_FREQ_K(1100)") + try: + child.add_prop(b.gpios(["SL_MEMLCD_EXTCOMIN"], "extcomin-gpios")) + child.add_int("extcomin-frequency", 60) + except KeyError: + pass + try: + child.add_prop(b.gpios(["SL_BOARD_ENABLE_DISPLAY"], "disp-en-gpios")) + except KeyError: + pass + + dt.find("chosen").select("zephyr,display", child) + + if mikrobus := b.config.get("SL_SPIDRV_EUSART_MIKROE_PERIPHERAL"): + if mikrobus not in peripherals: + peripherals[mikrobus] = default_spi_config(mikrobus.lower()) + + peripherals[mikrobus]["node"].labels.append("mikrobus_spi") + peripherals[mikrobus]["node"].labels.append("zephyr_spi") + + peripherals[mikrobus]["out_signals"].update( + b.signals(mikrobus, "SL_SPIDRV_EUSART_MIKROE", ["TX", "SCLK"]) + ) + peripherals[mikrobus]["in_signals"].update( + b.signals(mikrobus, "SL_SPIDRV_EUSART_MIKROE", ["RX"]) + ) + + add_cs_gpio( + b, peripherals[mikrobus]["node"], "SL_SPIDRV_EUSART_MIKROE_CS", True + ) + + for name, data in peripherals.items(): + pins, props = b.pinctrl( + name.lower(), + "default", + PinctrlGroup( + "group0", + data["out_signals"], + "drive-push-pull", + "output-high", + ), + PinctrlGroup( + "group1", + data["in_signals"], + "input-enable", + "silabs,input-filter", + ), + ) + pinctrl.add_node(pins) + data["node"].add_props(props) + + return [p["node"] for p in peripherals.values()] diff --git a/scripts/dts/board/uart.py b/scripts/dts/board/uart.py new file mode 100644 index 00000000..f9f4be38 --- /dev/null +++ b/scripts/dts/board/uart.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node, PinctrlGroup + + +def generate(b: Board, dt: Node, pinctrl: Node): + if b.soc_family in ["xg22"]: + # Prefer EUART since USART can also do SPI + candidates = [ + "SL_IOSTREAM_EUSART_VCOM", + "SL_IOSTREAM_USART_VCOM", + ] + else: + # Prefer USART + candidates = [ + "SL_IOSTREAM_USART_VCOM", + "SL_IOSTREAM_EUSART_VCOM", + ] + + for c in candidates: + if peripheral := b.config.get(c + "_PERIPHERAL"): + node = Node(labels=[peripheral.lower()], status="okay") + + pins, props = b.pinctrl( + peripheral.lower(), + "default", + PinctrlGroup( + "group0", + b.signals(peripheral, c, ["TX"]), + "drive-push-pull", + "output-high", + ), + PinctrlGroup( + "group1", + b.signals(peripheral, c, ["RX"]), + "input-enable", + "silabs,input-filter", + ), + ) + pinctrl.add_node(pins) + node.add_props(props) + node.add_int("current-speed", 115200) + + chosen = dt.find("chosen") + chosen.select("zephyr,console", node) + chosen.select("zephyr,shell-uart", node) + chosen.select("zephyr,uart-pipe", node) + + return [node] diff --git a/scripts/dts/board/wdog.py b/scripts/dts/board/wdog.py new file mode 100644 index 00000000..1e221a51 --- /dev/null +++ b/scripts/dts/board/wdog.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from util.board import Board +from dts.node import Node + + +def generate(b: Board, dt: Node, _pinctrl: Node): + node = Node(labels=["wdog0"], status="okay") + + dt.find("aliases").select("watchdog0", node) + + return [node] diff --git a/scripts/dts/node.py b/scripts/dts/node.py new file mode 100644 index 00000000..4e9193c5 --- /dev/null +++ b/scripts/dts/node.py @@ -0,0 +1,302 @@ +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import textwrap +from pathlib import Path + +from .prop import * + + +class Node: + def __init__( + self, + name=None, + labels=None, + compatible=None, + reg=None, + peripheral_name=None, + is_ref=False, + reg_is_hex=True, + status=None, + ): + self.name = name + self.peripheral_name = peripheral_name + self.address = None + self.props = [] + self.nodes = [] + self.labels = labels if labels is not None else [] + self.parent = None + self.indent = 1 + self.includes = { + "local": [], + "system": [], + } + self.is_ref = is_ref or self.name is None + self.root = False + + if compatible: + if not isinstance(compatible, list): + compatible = [compatible] + self.add_prop(StringArrayProperty("compatible", compatible)) + + if reg: + if isinstance(reg, dict): + regs = [] + names = [] + for name, r in reg.items(): + names.append(name) + regs.append(r) + if reg_is_hex: + self.add_prop(HexArrayProperty("reg", regs)) + else: + self.add_prop(ArrayProperty("reg", regs)) + self.add_prop(StringArrayProperty("reg-names", names)) + else: + if reg_is_hex: + self.add_prop(HexArrayProperty("reg", reg)) + else: + self.add_prop(ArrayProperty("reg", reg)) + + if status is not None: + self.status(status) + + def __str__(self): + self.sort_props() + self.sort_nodes() + + c = "" + suffix = "" + + if self.root: + c += "/dts-v1/;\n" + + if self.includes["system"]: + for inc in sorted(self.includes["system"]): + c += f"#include <{inc[1]}>\n" + if self.includes["local"]: + if self.includes["system"]: + c += "\n" + for inc in sorted(self.includes["local"]): + c += f'#include "{inc[1]}"\n' + + if not self.labels and not self.props and not self.nodes and not self.parent: + # Empty root node, emit nothing except any registered includes + return c + + if self.includes["local"] or self.includes["system"]: + c += "\n" + + if self.is_ref: + if len(self.labels) > 1: + for l in sorted(self.labels[1:]): + suffix += f"\n{l}: &{self.labels[0]} {{}};\n" + c += f"&{self.labels[0]} {{\n" + else: + for l in self.labels: + c += f"{l}: " + a = f"@{self.address:x}" if self.address is not None else "" + c += f"{self.name}{a} {{\n" + + s = "" + for p in self.props: + if sp := str(p): + s += f"{sp}\n" + for n in self.nodes: + if s: + s += "\n" + s += str(n) + c += textwrap.indent(str(s), "\t") + c += "};\n" + + return c + suffix + + def sort_props(self): + # https://docs.kernel.org/devicetree/bindings/dts-coding-style.html#order-of-properties-in-device-node + def prop_sort_key(p): + if p.name == "compatible": + category = 1 + elif p.name == "reg": + category = 2 + elif p.name in ["ranges", "pins", "pinmux"]: + category = 3 + elif "," in p.name: + category = 5 + elif p.name == "status": + category = 6 + else: + category = 4 + return (category, p.name) + + self.props.sort(key=prop_sort_key) + + def sort_nodes(self): + # https://docs.kernel.org/devicetree/bindings/dts-coding-style.html#order-of-nodes + self.nodes.sort(key=lambda n: (n.address or 0, n.name)) + + def add_node(self, node): + node.parent = self + node.indent = self.indent + 1 + self.nodes.append(node) + + def add_nodes(self, nodes): + for node in nodes: + node.parent = self + node.indent = self.indent + 1 + self.nodes += nodes + + def add_prop(self, prop): + if prop.name == "reg": + self.address = prop.value[0] + if isinstance(self.address, list): + self.address = self.address[0] + prop.node = self + self.props.append(prop) + return prop + + def add_props(self, props): + for prop in props: + self.add_prop(prop) + + def add_bool(self, *args, **kwargs): + return self.add_prop(BoolProperty(*args, **kwargs)) + + def add_int(self, *args, **kwargs): + return self.add_prop(IntProperty(*args, **kwargs)) + + def add_array(self, *args, **kwargs): + return self.add_prop(ArrayProperty(*args, **kwargs)) + + def add_hex_array(self, *args, **kwargs): + return self.add_prop(HexArrayProperty(*args, **kwargs)) + + def add_uint8_array(self, *args, **kwargs): + return self.add_prop(Uint8ArrayProperty(*args, **kwargs)) + + def add_string(self, *args, **kwargs): + return self.add_prop(StringProperty(*args, **kwargs)) + + def add_string_array(self, *args, **kwargs): + return self.add_prop(StringArrayProperty(*args, **kwargs)) + + def add_phandle(self, *args, **kwargs): + return self.add_prop(PhandleProperty(*args, **kwargs)) + + def add_phandles(self, *args, **kwargs): + return self.add_prop(PhandlesProperty(*args, **kwargs)) + + def add_phandle_array(self, *args, **kwargs): + return self.add_prop(PhandleArrayProperty(*args, **kwargs)) + + def add_include(self, include, local=False, priority=0): + include_type = "local" if local else "system" + inc = (priority, Path(include)) + if inc not in self.includes[include_type]: + self.includes[include_type].append(inc) + + def update_includes(self, other): + for t in ["local", "system"]: + for inc in other.includes[t]: + self.add_include(inc[1], local=(t == "local"), priority=inc[0]) + + def remove_includes(self): + for t in ["local", "system"]: + self.includes[t] = [] + + def get_root(self): + node = self + while node.parent: + node = node.parent + return node + + def find(self, name, address=True): + if self.name == name: + return self + + name = name.lstrip("/") + if "/" in name: + name, query = name.split("/", 1) + else: + query = None + + for n in self.nodes: + if address: + a = f"@{n.address:x}" if n.address is not None else "" + node_name = f"{n.name}{a}" + else: + node_name = n.name + + if node_name == name: + if query: + return n.find(query, address) + else: + return n + + def prop(self, name): + for p in self.props: + if p.name == name: + return p + return None + + def status(self, value): + status = self.prop("status") + if not status: + status = self.add_string("status", value) + status.value = value + + def resolve_deferred_values(self, config): + if isinstance(self.address, DeferredValue): + self.address = config.get(self.address.value, self.peripheral_name) + + for prop in self.props: + if isinstance(prop.value, DeferredValue): + prop.value = config.get(prop.value.value, self.peripheral_name) + + for node in self.nodes: + node.resolve_deferred_values(config) + + +class DeleteNode(Node): + def __init__(self, name=None, label=None, reg=None): + if label is not None: + label = [label] + super().__init__(name, label, reg=reg) + + def __str__(self): + if self.labels: + return f"/delete-node/ &{self.labels[0]};\n" + else: + return f"/delete-node/ {self.name};\n" + + +class ClockNode(Node): + def __init__(self, name, parent, labels=None): + aliases = {"sysrtc0clk": "sysrtcclk"} + name = aliases.get(name, name) + + super().__init__(name, labels) + self.labels.append(name) + + self.add_prop(StringProperty("compatible", "fixed-factor-clock")) + self.add_prop(IntProperty("#clock-cells", "0")) + self.add_prop(PhandleProperty("clocks", parent)) + + +class ChosenNode(Node): + def __init__(self, name): + super().__init__(name, None) + + def select(self, key, node): + self.add_prop(PathProperty(key, node.labels[0])) + + +class PinctrlGroup(Node): + def __init__(self, name, pins, *props, abus=None): + super().__init__(name, None) + + if pins is not None: + self.add_prop(ArrayProperty("pins", [[s] for s in pins])) + if abus is not None: + self.add_prop(ArrayProperty("silabs,analog-bus", [[b] for b in abus])) + for prop in props: + if prop is not None: + self.add_prop(BoolProperty(prop, True)) diff --git a/scripts/dts/prop.py b/scripts/dts/prop.py new file mode 100644 index 00000000..315be2f3 --- /dev/null +++ b/scripts/dts/prop.py @@ -0,0 +1,232 @@ +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + + +class PropertyType: + PATH = "path" + PHANDLE = "phandle" + PHANDLES = "phandles" + PHANDLE_ARRAY = "phandle-array" + ARRAY = "array" + STRING = "string" + STRING_ARRAY = "string-array" + INTEGER = "integer" + UINT8_ARRAY = "uint8-array" + BOOLEAN = "boolean" + + +class Property: + def __init__(self, name, value, comment=None): + self.name = name + self.value = value + self.labels = [] + self.node = None + self.comment = comment + self.fmt_one_element_per_line = False + + def _str_format_comment(self): + import textwrap + + if not self.comment: + return "" + + comment = "/*" + max_width = 100 - 8 * self.node.indent - 6 + if len(self.comment) > max_width: + comment += ( + "\n" + + "\n".join( + textwrap.wrap( + self.comment, + width=max_width + 3, + initial_indent=" * ", + subsequent_indent=" * ", + ) + ) + + "\n" + ) + else: + comment += " " + self.comment + + comment += " */\n" + + return comment + + def _str_format_list(self, values): + values_str = self._str_format_comment() + + if self.node: + indent = self.node.indent + else: + indent = 0 + + name_len = len(self.name) + 2 + max_width = 100 - 8 * indent - name_len - 1 + indent = "\t" * (name_len // 8) + " " * (name_len % 8) + width = 0 + for v in values: + if self.fmt_one_element_per_line and width != 0: + values_str += "\n" + indent + width += len(v) + 2 + if not self.fmt_one_element_per_line and width > max_width: + values_str += "\n" + indent + width = len(v) + 2 + values_str += f" {v}," + + return f"{self.name} ={values_str.rstrip(',')};" + + +class DeleteProperty(Property): + def __init__(self, name, comment=None): + super().__init__(name, None, comment) + + def __str__(self): + c = self._str_format_comment() + return f"{c}/delete-property/ {self.name};" + + +class PathProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.PATH + + def __str__(self): + c = self._str_format_comment() + return f"{c}{self.name} = &{self.value};" + + +class PhandleProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.PHANDLE + + def __str__(self): + c = self._str_format_comment() + return f"{c}{self.name} = <&{self.value}>;" + + +class PhandlesProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.PHANDLES + + def __str__(self): + c = self._str_format_comment() + return f"{c}{self.name} = <{' '.join(f'&{v}' for v in self.value)}>;" + + +class PhandleArrayProperty(Property): + def __init__(self, name, values, comment=None): + super().__init__(name, values, comment) + self.type = PropertyType.PHANDLE_ARRAY + + def __str__(self): + if not isinstance(self.value[0], list): + self.value = [self.value] + + values = [f"<&{' '.join(map(str, v))}>" for v in self.value] + return self._str_format_list(values) + + +class ArrayProperty(Property): + def __init__(self, name, values, comment=None): + super().__init__(name, values, comment) + self.type = PropertyType.ARRAY + self.fmt = ["{}", "{}", "{}", "{}", "{}"] + + def __str__(self): + if not isinstance(self.value[0], list): + self.value = [self.value] + + values = [ + f"<{' '.join(self.fmt[((e.bit_length() - 1) // 8) if e.bit_length() else -1].format(e) if isinstance(e, int) else e for e in v)}>" + for v in self.value + ] + return self._str_format_list(values) + + +class HexArrayProperty(ArrayProperty): + def __init__(self, name, values, comment=None): + super().__init__(name, values, comment) + self.fmt = ["0x{:02x}", "0x{:04x}", "0x{:08x}", "0x{:08x}", "0x{:x}"] + + +class Uint8ArrayProperty(Property): + def __init__(self, name, values, comment=None): + super().__init__(name, values, comment) + self.type = PropertyType.UINT8_ARRAY + + def __str__(self): + c = self._str_format_comment() + values_str = f"[{' '.join(f'{v:02x}' for v in self.value)}]" + return f"{c}{self.name} = {values_str};" + + +class StringProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.STRING + + def __str__(self): + c = self._str_format_comment() + return f'{c}{self.name} = "{self.value}";' + + +class StringArrayProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.STRING_ARRAY + + def __str__(self): + return self._str_format_list(f'"{v}"' for v in self.value) + + +class IntProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.INTEGER + + def __str__(self): + c = self._str_format_comment() + return f"{c}{self.name} = <{self.value}>;" + + +class BoolProperty(Property): + def __init__(self, name, value, comment=None): + super().__init__(name, value, comment) + self.type = PropertyType.BOOLEAN + + def __str__(self): + c = self._str_format_comment() + return f"{c}{self.name};" if self.value else "" + + +class DeferredValue: + def __init__(self, value): + self.value = value + + def __str__(self): + return f"{self.value}" + + +def property_from_string(spec_type, prop, val): + if spec_type == "boolean": + return BoolProperty(prop, val) + elif spec_type == "int": + return IntProperty(prop, val) + elif spec_type == "array": + return ArrayProperty(prop, val) + elif spec_type == "uint8-array": + return Uint8ArrayProperty(prop, val) + elif spec_type == "string": + return StringProperty(prop, val) + elif spec_type == "string-array": + return StringArrayProperty(prop, val) + elif spec_type == "phandle": + return PhandleProperty(prop, val) + elif spec_type == "phandles": + return PhandlesProperty(prop, val) + elif spec_type == "phandle-array": + return PhandleArrayProperty(prop, val) + else: + raise ValueError(f"Unsupported property type {spec_type} for {prop}") diff --git a/scripts/dts/soc/__init__.py b/scripts/dts/soc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/dts/soc/series2.py b/scripts/dts/soc/series2.py new file mode 100644 index 00000000..045f6d90 --- /dev/null +++ b/scripts/dts/soc/series2.py @@ -0,0 +1,826 @@ +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import glob +import logging +import os +import re +import sys +from pathlib import Path + +import cmsis_svd.parser + +import util.sdk +import util.soc +from dts.node import Node, ClockNode, DeleteNode, ChosenNode +from dts.prop import DeferredValue, DeleteProperty, property_from_string + + +ZEPHYR_BASE = Path(os.getenv("ZEPHYR_BASE")) +sys.path.insert(0, str(ZEPHYR_BASE / "scripts" / "dts" / "python-devicetree" / "src")) +from devicetree import edtlib # noqa: E402 + +logger = logging.getLogger(__name__) + + +BT_FEATURES = { + "ble-2mbps-supported": ["device_supports_bluetooth"], + "ble-coded-phy-supported": ["device_supports_bluetooth_coded_phy"], + "ble-cte-tx-supported": [ + "device_supports_bluetooth_cte", + ], + "ble-cte-rx-supported": [ + "device_supports_bluetooth_cte", + "device_supports_bluetooth_iq_sampling", + "device_supports_bluetooth_antenna_switching", + ], + "ble-cs-supported": ["device_generic_family_efr32xg24"], +} + + +def create_device_tree(family, device, gen_config: util.soc.GenConfig, svd_dir: Path): + """ + Create base device tree for a generic device family based on a representative device. + """ + svd = cmsis_svd.parser.SVDParser.for_xml_file(svd_dir / f"{device}.svd") + + dt = Node("/") + dt.add_include("freq.h") + + chosen = ChosenNode("chosen") + dt.add_node(chosen) + + clocks = Node("clocks") + dt.add_node(clocks) + + create_clock_nodes(svd, clocks, gen_config.clocks) + + cpus = Node("cpus") + dt.add_node(cpus) + cpus.add_int("#address-cells", 1) + cpus.add_int("#size-cells", 0) + + cpu_compat = { + "cm3": {"binding": "arm,cortex-m3", "include": "arm/armv7-m.dtsi"}, + "cm4": {"binding": "arm,cortex-m4f", "include": "arm/armv7-m.dtsi"}, + "cm33": {"binding": "arm,cortex-m33", "include": "arm/armv8-m.dtsi"}, + "cm55": {"binding": "arm,cortex-m55", "include": "arm/armv8.1-m.dtsi"}, + } + cpu = Node( + "cpu", + labels=["cpu0"], + compatible=cpu_compat.get(gen_config.config.cmsis.get("cpu.core"), {}).get( + "binding" + ), + reg=[0], + reg_is_hex=False, + ) + cpus.add_node(cpu) + cpu.get_root().add_include( + cpu_compat.get(gen_config.config.cmsis.get("cpu.core"), {}).get("include") + ) + cpu.add_string("device_type", "cpu") + cpu.add_phandles( + "cpu-power-states", + ["pstate_em1", "pstate_em2", "pstate_em4"], + comment="The minimum residency and exit latency is managed by sl_power_manager on S2 devices.", + ) + cpu.add_int("#address-cells", 1) + cpu.add_int("#size-cells", 1) + cpu.add_node( + Node( + "itm", labels=["itm"], compatible="arm,armv8m-itm", reg=[0xE0000000, 0x1000] + ) + ) + cpu.add_node( + Node("mpu", labels=["mpu"], compatible="arm,armv8m-mpu", reg=[0xE000ED90, 0x40]) + ) + + power = Node("power-states") + cpus.add_node(power) + em1 = Node("em1", labels=["pstate_em1"], compatible="zephyr,power-state") + em1.add_string("power-state-name", "runtime-idle") + power.add_node(em1) + em2 = Node("em2", labels=["pstate_em2"], compatible="zephyr,power-state") + em2.add_string("power-state-name", "suspend-to-idle") + power.add_node(em2) + em4 = Node("em4", labels=["pstate_em4"], compatible="zephyr,power-state") + em4.add_string("power-state-name", "soft-off") + em4.status("disabled") + power.add_node(em4) + + sram = Node("memory", labels=["sram0"], compatible="mmio-sram") + sram.address = DeferredValue("memory.sram.base") + sram.add_string("device_type", "memory") + dt.add_node(sram) + + soc = Node("soc") + dt.add_node(soc) + + create_peripheral_nodes(svd, soc, gen_config.config, gen_config.peripherals) + dt.resolve_deferred_values(gen_config.config.cmsis) + + hwinfo = Node("hwinfo", labels=["hwinfo"], compatible="silabs,series2-hwinfo") + hwinfo.status("disabled") + dt.add_node(hwinfo) + + nvic = Node("&nvic") + nvic.add_int("arm,num-irq-priority-bits", DeferredValue("cpu.nvic_prio_bits")) + nvic.resolve_deferred_values(gen_config.config.cmsis) + + return [dt, nvic] + + +def get_max_output_power(cmsis: util.sdk.CmsisDeviceConfig) -> int: + """ + Get the maximum output power of the highest-power PA of a given device. + """ + try: + return cmsis.get("device.efr32_subghz_hp_pa_max_output") + except KeyError: + pass + + try: + return cmsis.get("device.efr32_2g4hz_hp_pa_max_output") + except KeyError: + pass + + try: + return cmsis.get("device.efr32_2g4hz_mp_pa_max_output") + except KeyError: + pass + + try: + return cmsis.get("device.efr32_2g4hz_lp_pa_max_output") + except KeyError: + return 0 + + +def create_radio_device_tree(shared_config: util.soc.SharedConfig): + """ + Create device tree for the radio part of a generic device family based on a + representative device. + """ + dt = Node("/") + dt.add_include(f"silabs/{shared_config.name}/{shared_config.name}.dtsi") + soc = Node("soc") + dt.add_node(soc) + + radio = Node( + "radio", + labels=["radio"], + compatible="silabs,series2-radio", + reg=[0xB0000000, 0x1000000], + ) + soc.add_node(radio) + + radio_interrupts = [ + "agc", + "bufc", + "frc_pri", + "frc", + "modem", + "protimer", + "rac_rsm", + "rac_seq", + "hostmailbox", + "rdmailbox", + "rfsense", + "prortc", + "synth", + "rfeca0", + "rfeca1", + ] + add_interrupts_to_node( + radio, None, radio_interrupts, shared_config.cmsis.interrupts, prio=1 + ) + + radio.add_int("pa-initial-power-dbm", 10) + radio.add_int("pa-max-power-dbm", get_max_output_power(shared_config.cmsis)) + radio.add_int( + "pa-ramp-time-us", + 2 if shared_config.name in ["xg22", "xg27", "xg29"] else 10, + ) + radio.add_int("pa-voltage-mv", 3300) + # TODO: Make OPN dependent? + radio.add_bool("radio-tx-high-power-supported", True) + + if shared_config.any("device_has_radio_2g4hz"): + radio.add_string("pa-2p4ghz", "auto") + if shared_config.any("device_has_radio_subghz"): + radio.add_string("pa-subghz", "highest") + + for prop, feature in BT_FEATURES.items(): + if shared_config.all(feature, radio_only=True): + radio.add_bool(prop, True) + + if shared_config.all("device_supports_bluetooth"): + bt = Node( + "bt_hci_silabs", labels=["bt_hci_silabs"], compatible="silabs,bt-hci-efr32" + ) + bt.status("disabled") + radio.add_node(bt) + + protimer = Node("protimer", labels=["protimer"], compatible="silabs,protimer") + protimer.status("disabled") + radio.add_node(protimer) + + pti = Node("pti", labels=["pti"], compatible="silabs,pti") + pti.add_int("clock-frequency", "DT_FREQ_K(1600)") + pti.add_string("mode", "uart") + pti.status("disabled") + radio.add_node(pti) + + return dt + + +def create_clock_nodes( + svd: cmsis_svd.parser.SVDParser, + clocks: ClockNode, + cfg: util.soc.ClockConfig, +): + """ + Parse clock muxes from SVD and add them to devicetree + """ + cmu: cmsis_svd.parser.SVDPeripheral = next( + filter(lambda p: p.name == "CMU_NS", svd.get_device().peripherals) + ) + clock_names = set() + + for reg in cmu.registers: + if reg.name.endswith("CTRL") and reg.name not in ["DPLLREFCLKCTRL"]: + for field in reg.fields: + clock_name = reg.name.rstrip("CTRL").lower() + + # Add clock muxes from CLKSEL registers + if field.name == "CLKSEL": + clock_names.add(clock_name) + + mask = ((1 << field.bit_width) - 1) << field.bit_offset + default = (reg.reset_value & mask) >> field.bit_offset + parent = None + for val in field.enumerated_values: + clksel_name = val.name.lower() + if clksel_name == "disabled": + continue + clock_names.add(clksel_name) + + if val.value == default: + parent = clksel_name + clocks.add_node( + ClockNode(clock_name, cfg.selection.get(clock_name, parent)) + ) + + # Add clock dividers from PRESC registers + if field.name == "PRESC": + if clock_name in ["exportclk"]: + continue + + # Single prescaler associated with the mux itself, or a child of the mux. + # Try to update the mux node. + node = clocks.nodes[-1] + if node.name != clock_name: + # Create a child if not found + node = ClockNode(clock_name, cfg.selection.get(clock_name)) + clocks.add_node(node) + + mask = ((1 << field.bit_width) - 1) << field.bit_offset + default = (reg.reset_value & mask) >> field.bit_offset + node.add_int("clock-div", default + 1) + elif field.name.endswith("PRESC"): + # Multiple prescalers that are children of the mux. Add new nodes. + clock_name = field.name.rstrip("PRESC").lower() + if clock_name in cfg.skip: + continue + for name in [clock_name] + cfg.extra_children.get(clock_name, []): + clock_node = ClockNode(name, cfg.selection.get(name)) + clock_node.add_int("clock-div", cfg.divider.get(name, 1)) + clocks.add_node(clock_node) + + # Additional clock nodes that are not directly derived from registers + clocks.add_node(ClockNode("systickclk", "hclk")) + if "hfxort" in clock_names: + clocks.add_node(ClockNode("hfxort", "hfxo")) + if "hfrcodpllrt" in clock_names: + clocks.add_node(ClockNode("hfrcodpllrt", "hfrcodpll")) + if "hclkdiv1024" in clock_names: + node = ClockNode("hclkdiv1024", "hclk") + node.add_int("clock-div", 1024) + clocks.add_node(node) + + +def create_peripheral_nodes(svd, soc, config, peripheral_config): + """ + Create devicetree node for a peripheral. + """ + binding_paths = glob.glob(f"{ZEPHYR_BASE}/dts/bindings/**/*.y*ml", recursive=True) + bindings = edtlib.bindings_from_paths(binding_paths, ignore_errors=True) + + for peripheral in svd.get_device().peripherals: + if not peripheral.name.endswith(("_NS", "_NS_HOST")): + continue + if peripheral.name.endswith("_NS_HOST"): + name = peripheral.name[:-8].lower() + else: + name = peripheral.name[:-3].lower() + + for cfg in peripheral_config: + if re.match(cfg.get("svd"), name): + break + else: + raise ValueError(f"No matching peripheral found for {peripheral.name}") + + if cfg.get("skip", False): + continue + + p = create_node_from_config(soc, config, cfg, bindings, name, peripheral) + + if (clock := config.clocks.get(name)) and name not in ["hfrco0"]: + p.add_phandle_array("clocks", ["cmu", clock["clock"], clock["branch"]]) + + extra_interrupts = [ + int_name.replace("{peripheral}", name) + for int_name in cfg.get("interrupts", []) + ] + add_interrupts_to_node(p, peripheral, extra_interrupts, config.cmsis.interrupts) + + +def create_node_from_config( + parent, output_config, cfg, bindings, name=None, peripheral=None, reg=None +): + """ + Construct a devicetree node from YAML configuration + """ + if "binding" in cfg: + binding = next(filter(lambda b: b.compatible == cfg.get("binding"), bindings)) + else: + binding = None + family = output_config.name + + if name is None and "labels" in cfg: + name = cfg["labels"][0] + + if not reg: + reg = cfg.get("reg") + + if cfg.get("reg_format", "hex") != "hex": + reg_is_hex = False + else: + reg_is_hex = True + + if peripheral and not reg: + start = peripheral.base_address + if cfg.get("reg_mode") == "narrow": + size = peripheral.registers[-1].address_offset + 4 + else: + size = peripheral.address_blocks[0].size * 4 + reg = [start, size] + + p = Node( + cfg.get("node", name), + labels=cfg.get("labels", [name] if name else None), + compatible=binding.compatible if binding else None, + reg=reg, + peripheral_name=name, + reg_is_hex=reg_is_hex, + ) + + if "address" in cfg: + p.address = cfg["address"] + + if binding: + # Add required constants from binding + if not cfg.get("skip_default_properties", False): + for prop, spec in binding.prop2specs.items(): + if spec.required and spec.const is not None: + p.add_prop(property_from_string(spec.type, prop, spec.const)) + + # Add values from config yaml + for prop, val in cfg.get("properties", {}).items(): + if prop == "ranges": + t = "boolean" if isinstance(val, bool) else "array" + p.add_prop(property_from_string(t, prop, val)) + else: + spec = binding.prop2specs.get(prop) + assert spec, f"No spec found for {prop} in {binding.compatible}" + p.add_prop(property_from_string(spec.type, prop, val)) + + # Add peripheral-id if required by the binding + # This should disappear once all drivers use clock control + if pid := binding.prop2specs.get("peripheral-id"): + if pid.required: + peripheral_id = name.removeprefix(p.name) + p.add_int("peripheral-id", peripheral_id) + + # Add child nodes + for child in cfg.get("children", []): + if h := child.get("handler"): + if h == "gpio": + em4_pins = {} + for wu, (port, pin) in output_config.cmsis.get("routes.gpio").items(): + if not wu.startswith("em4wu"): + continue + wu = int(wu[5:], 10) + if port not in em4_pins: + em4_pins[port] = [] + em4_pins[port].append((wu, pin)) + + for reg in peripheral.registers: + if m := re.match("PORT(.)_CTRL", reg.name): + port = m.group(1).lower() + child["properties"]["silabs,wakeup-ints"] = [ + [v[0]] for v in em4_pins.get(port, []) + ] + child["properties"]["silabs,wakeup-pins"] = [ + [v[1]] for v in em4_pins.get(port, []) + ] + create_node_from_config( + p, + output_config, + child, + bindings, + f"gpio{port}", + None, + [peripheral.base_address + reg.address_offset, 0x30], + ) + else: + create_node_from_config(p, output_config, child, bindings) + + if s := cfg.get("status"): + p.status(s) + + parent.add_node(p) + + if inc := cfg.get("include"): + p.get_root().add_include(inc.replace("{config}", family)) + + if c := cfg.get("chosen"): + p.get_root().find("chosen").select(c, p) + + for sibling in cfg.get("siblings", []): + sibling_name = None + sibling_reg = None + if h := sibling.get("handler"): + if h == "pinctrl": + dbus = next( + filter(lambda r: r.name == "DBGROUTEPEN", peripheral.registers) + ).address_offset + abus = next( + filter(lambda r: r.name == "ABUSALLOC", peripheral.registers) + ).address_offset + sibling_name = "pinctrl" + sibling_reg = { + "dbus": [ + peripheral.base_address + dbus, + peripheral.address_blocks[0].size - dbus, + ], + "abus": [peripheral.base_address + abus, 0x40], + } + elif h == "clkin0": + for reg in peripheral.registers: + if reg.name == "CMU_CLKIN0ROUTE": + sibling_name = "clkin0" + sibling_reg = [ + peripheral.base_address + reg.address_offset, + 0x4, + ] + else: + raise ValueError(f"No sibling handler '{h}' for {name}") + create_node_from_config( + parent, output_config, sibling, bindings, sibling_name, None, sibling_reg + ) + + return p + + +def add_interrupts_to_node(node, svd, extra_interrupt_names, interrupt_numbers, prio=2): + """ + Add interrupts to a devicetree node. + """ + interrupts = {} + + if svd: + for i in svd.interrupts: + if i.name.endswith(("_RX", "_TX")): + peripheral, dts_name = i.name.lower().split("_") + if not svd.name.lower().startswith(peripheral): + logger.warning( + "Skip interrupt with mismatched name: %s for %s", + i.name, + svd.name, + ) + continue + else: + dts_name = i.name.lower() + interrupts[dts_name] = [i.value, prio] + + for int_name in extra_interrupt_names: + if int_name.endswith(("_rx", "_tx")): + dts_name = int_name.split("_")[-1] + else: + dts_name = int_name + if dts_name not in interrupts and int_name in interrupt_numbers: + interrupts[dts_name] = [interrupt_numbers[int_name], prio] + + if interrupts: + interrupts = dict(sorted(interrupts.items(), key=lambda item: item[1])) + node.add_array("interrupts", list(interrupts.values())) + node.add_string_array("interrupt-names", list(interrupts.keys())) + + +def create_family_device_tree(family, shared_config): + """ + Create family-specific devicetree including the base devicetree and setting any + family-specific properties. + """ + generic_family_name = shared_config.name + family_dt = Node("/") + if family.any("device_has_radio"): + family_dt.add_include( + f"silabs/{generic_family_name}/efr32{generic_family_name}.dtsi" + ) + else: + family_dt.add_include( + f"silabs/{generic_family_name}/{generic_family_name}.dtsi" + ) + + nodes = [family_dt] + radio = Node(labels=["radio"]) + + generate_bt = not shared_config.all("device_supports_bluetooth") + if ( + generate_bt + and family.any("device_has_radio") + and family.any("device_supports_bluetooth") + ): + bt = Node( + "bt_hci_silabs", + labels=["bt_hci_silabs"], + compatible="silabs,bt-hci-efr32", + ) + bt.status("disabled") + radio.add_node(bt) + + if family.any("device_is_module"): + radio.add_string("pa-2p4ghz", "highest") + + if radio.props or radio.nodes: + nodes.append(radio) + + return nodes + + +def create_soc_device_tree( + soc: str, + family: util.soc.FamilyConfig, + shared_config: util.soc.SharedConfig, + cmsis: util.sdk.CmsisDeviceConfig, + module_config: dict, +): + """ + Create SoC-specific devicetree including the family devicettree and setting any + SoC-specific properties. + """ + generic_family_name = shared_config.name + soc_dt = Node("/") + soc_dt.add_include("mem.h") + soc_dt.add_include(f"silabs/{generic_family_name}/{family.name}.dtsi") + compatibles = [ + f"silabs,{soc}", + f"silabs,{family.name}", + f"silabs,{generic_family_name}", + ] + + if family.any("device_has_radio"): + compatibles.append("silabs,efr32") + else: + compatibles.append("silabs,efm32") + compatibles.append("simple-bus") + soc_dt.add_node(Node("soc", compatible=compatibles)) + + nodes = [soc_dt] + + if family.any("device_supports_bluetooth") and not family.soc_has( + soc, "device_supports_bluetooth" + ): + nodes.append(DeleteNode(label="bt_hci_silabs")) + + dcdc = Node(labels=["dcdc"]) + if module_config.get("SL_DEVICE_INIT_DCDC_PFMX_IPKVAL_OVERRIDE") == "1": + ipkvals = { + "3": 50, + "4": 65, + "5": 73, + "6": 80, + "7": 86, + "8": 93, + "9": 100, + "10": 106, + "11": 113, + "12": 120, + } + ipkval = module_config.get("SL_DEVICE_INIT_DCDC_PFMX_IPKVAL", 120) + dcdc.add_int("silabs,pfmx-peak-current-milliamp", ipkvals[ipkval]) + if "device_dcdc_boost" in family.provides.get( + soc + ) and "device_dcdc_buck" not in family.provides.get(soc): + dcdc.add_bool("regulator-boot-on", True) + dcdc.add_int("regulator-initial-mode", "SILABS_DCDC_MODE_BOOST") + dcdc.add_int("regulator-init-microvolt", 1800000) + dcdc.status("okay") + if dcdc.props: + nodes.append(dcdc) + soc_dt.add_include("zephyr/dt-bindings/regulator/silabs_dcdc.h") + + flash = Node(labels=["flash0"]) + flash.add_hex_array( + "reg", + [ + cmsis.get("memory.flash.base"), + f"DT_SIZE_K({cmsis.get('memory.flash.size') // 1024})", + ], + ) + flash.add_hex_array( + "ranges", + [ + 0, + cmsis.get("memory.flash.base"), + f"DT_SIZE_K({cmsis.get('memory.flash.size') // 1024})", + ], + ) + nodes.append(flash) + + if hfxo_en := module_config.get("SL_CLOCK_MANAGER_HFXO_EN"): + if hfxo_en == "SL_CLOCK_MANAGER_HFXO_EN_ENABLE": + hfxo = Node(labels=["hfxo"]) + + freq = int(module_config.get("SL_CLOCK_MANAGER_HFXO_FREQ"), 10) + if freq not in [38400000, 39000000]: + if (freq % 1000000) == 0: + freq = f"DT_FREQ_M({freq // 1000000})" + else: + freq = f"DT_FREQ_K({freq // 1000})" + hfxo.add_int("clock-frequency", freq) + + hfxo.add_array("ctune", [int(module_config["SL_CLOCK_MANAGER_HFXO_CTUNE"])]) + hfxo.add_int( + "precision", + module_config["SL_CLOCK_MANAGER_HFXO_PRECISION"], + ) + hfxo.status("okay") + nodes.append(hfxo) + + if lfxo_en := module_config.get("SL_CLOCK_MANAGER_LFXO_EN"): + if lfxo_en == "1": + lfxo = Node(labels=["lfxo"]) + lfxo.add_int("ctune", module_config["SL_CLOCK_MANAGER_LFXO_CTUNE"]) + lfxo.add_int( + "precision", + module_config["SL_CLOCK_MANAGER_LFXO_PRECISION"], + ) + lfxo.status("okay") + nodes.append(lfxo) + soc_dt.add_include(f"silabs/{generic_family_name}/clock-lfxo.dtsi") + else: + lfrco = Node(labels=["lfrco"]) + lfrco.add_bool("precision-mode", True) + nodes.append(lfrco) + + radio = Node(labels=["radio"]) + if ( + family.any("device_has_radio") + and shared_config.any("device_has_radio_2g4hz") + and not family.soc_has(soc, "device_has_radio_2g4hz") + ): + radio.add_prop(DeleteProperty("pa-2p4ghz")) + if ( + family.any("device_has_radio") + and shared_config.any("device_has_radio_subghz") + and not family.soc_has(soc, "device_has_radio_subghz") + ): + radio.add_prop(DeleteProperty("pa-subghz")) + + if family.soc_has(soc, "device_has_radio"): + family_max_power = get_max_output_power(shared_config.cmsis) + soc_max_power = get_max_output_power(cmsis) + if soc_max_power != family_max_power: + radio.add_int("pa-max-power-dbm", soc_max_power) + + if voltage := module_config.get("SL_RAIL_UTIL_PA_VOLTAGE_MV"): + if int(voltage) != 3300: + radio.add_int("pa-voltage-mv", module_config["SL_RAIL_UTIL_PA_VOLTAGE_MV"]) + + for prop, feature in BT_FEATURES.items(): + if family.soc_has(soc, feature) and not shared_config.all( + feature, radio_only=True + ): + radio.add_bool(prop, True) + + if radio.props: + nodes.append(radio) + + sram = Node(labels=["sram0"]) + sram.add_hex_array( + "reg", + [ + cmsis.get("memory.sram.base"), + f"DT_SIZE_K({cmsis.get('memory.sram.size') // 1024})", + ], + ) + nodes.append(sram) + + for g in ["a", "b", "c", "d"]: + mask = cmsis.get(f"P{g.upper()}_MASK", "gpio") + ngpios = mask.bit_length() + reserved = [] + in_reserved = False + for i in range(ngpios): + if ~mask & 2**i: + if not in_reserved: + reserved.append([i]) + in_reserved = True + else: + if in_reserved: + reserved[-1].append(i - reserved[-1][0]) + in_reserved = False + + gpio = Node(labels=[f"gpio{g}"]) + gpio.add_int("ngpios", ngpios) + if reserved: + gpio.add_array("gpio-reserved-ranges", reserved) + nodes.append(gpio) + + nodes.sort(key=lambda n: (n.labels, n.name)) + return nodes + + +def create_clock_device_trees(dt: Node) -> dict[str, list[Node]]: + """ + Create clock configuration devicetree fragments, configuring the clock tree to + use a given oscillator. + """ + output = {} + + freq = {} + freq["hfxo"] = dt.find("/soc/hfxo", address=False).prop("clock-frequency").value + start = freq["hfxo"].find("(") + 1 + end = freq["hfxo"].find(")") + hfxo_freq = int(freq["hfxo"][start:end]) + freq["hfrcodpll"] = freq["hfxo"][:start] + str(hfxo_freq * 2) + freq["hfxo"][end:] + + for clock_name in ["hfrcodpll", "hfxo"]: + nodes = [] + for mux_node in dt.find("clocks").nodes: + if p := mux_node.prop("clocks"): + if ( + mux_node.name not in ["hfrcodpllrt", "hfxort"] + and p.value == "hfrcodpll" + ): + new_node = Node(labels=[mux_node.labels[0]]) + new_node.add_phandle("clocks", clock_name) + nodes.append(new_node) + + cpu = Node(labels=["cpu0"]) + cpu.add_int("clock-frequency", freq[clock_name]) + nodes.append(cpu) + + cpu = Node(labels=["itm"]) + cpu.add_int("swo-ref-frequency", freq[clock_name]) + nodes.append(cpu) + + hfxo = Node(labels=["hfxo"]) + hfxo.status("okay") + nodes.append(hfxo) + + dpll = Node(labels=["hfrcodpll"]) + if clock_name == "hfxo": + dpll.status("disabled") + else: + dpll.add_int("clock-frequency", freq[clock_name]) + dpll.add_phandle("clocks", "hfxo") + dpll.add_bool("dpll-autorecover", True) + dpll.add_string("dpll-edge", "fall") + dpll.add_string("dpll-lock", "phase") + dpll.add_int("dpll-m", 1919) + dpll.add_int("dpll-n", 3839) + nodes.append(dpll) + + nodes.sort(key=lambda n: (n.labels, n.name)) + output[clock_name] = nodes + + nodes = [] + for mux_node in dt.find("clocks").nodes: + if p := mux_node.prop("clocks"): + if mux_node.name not in ["hfrcodpllrt", "hfxort"] and p.value == "lfrco": + new_node = Node(labels=[mux_node.labels[0]]) + new_node.add_phandle("clocks", "lfxo") + nodes.append(new_node) + + lfxo = Node(labels=["lfxo"]) + lfxo.status("okay") + nodes.append(lfxo) + + nodes.sort(key=lambda n: (n.labels, n.name)) + output["lfxo"] = nodes + + return output diff --git a/scripts/dts/soc/series2.yml b/scripts/dts/soc/series2.yml new file mode 100644 index 00000000..8b8de143 --- /dev/null +++ b/scripts/dts/soc/series2.yml @@ -0,0 +1,359 @@ +configs: + - name: xg21 + families: + - name: efr32mg21 + representative_device: EFR32MG21B020F1024IM32 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg21.c + - name: xg22 + families: + - name: efr32bg22 + representative_device: EFR32BG22C224F512IM40 + - name: efr32mg22 + - name: efr32fg22 + - name: bgm22 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg22.c + - name: xg23 + families: + - name: efr32zg23 + representative_device: EFR32ZG23B021F512IM40 + - name: efr32fg23 + - name: efm32pg23 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg23.c + - name: xg24 + families: + - name: efr32mg24 + representative_device: EFR32MG24B220F1536IM48 + - name: efr32bg24 + - name: mgm24 + - name: bgm24 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg24.c + - name: xg26 + families: + - name: efr32mg26 + representative_device: EFR32MG26B521F3200IM68 + - name: efr32bg26 + - name: efm32pg26 + - name: mgm26 + - name: bgm26 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg26.c + - name: xg27 + families: + - name: efr32bg27 + representative_device: EFR32BG27C140F768IM40 + - name: efr32mg27 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg27.c + - name: xg28 + families: + - name: efr32zg28 + representative_device: EFR32ZG28B322F1024IM68 + - name: efr32fg28 + - name: efm32pg28 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg28.c + - name: xg29 + families: + - name: efr32mg29 + representative_device: EFR32MG29B140F1024IM40 + - name: efr32bg29 + clocks: platform/service/device_manager/devices/sl_device_peripheral_hal_efr32xg29.c + + +clocks: + skip: + - rhclk + selection: + sysclk: hfrcodpll + hclk: sysclk + pclk: hclk + lspclk: pclk + traceclk: # on devices where no clksel field is present + - when: [xg21] + value: hclk + - value: sysclk + divider: + pclk: 2 + lspclk: 2 + extra_children: + pclk: [lspclk] + +peripherals: + - svd: cmu + node: clock + binding: "silabs,series-clock" + include: "zephyr/dt-bindings/clock/silabs/{config}-clock.h" + status: okay + - svd: fsrco + binding: "fixed-clock" + properties: + clock-frequency: DT_FREQ_M(20) + - svd: hfxo\d + node: hfxo + binding: "silabs,hfxo" + labels: ["clk_hfxo", hfxo] + properties: + clock-frequency: + - when: [xg23, xg24, xg25, xg26, xg28] + value: DT_FREQ_M(39) + - when: [xg21, xg22, xg27, xg29] + value: DT_FREQ_K(38400) + ctune: [140] + precision: 50 + status: disabled + - svd: lfxo + binding: "silabs,series2-lfxo" + properties: + clock-frequency: 32768 + ctune: 63 + precision: 50 + timeout: 4096 + status: disabled + - svd: hfrco0 + node: hfrcodpll + labels: [hfrcodpll] + binding: "silabs,series2-hfrcodpll" + properties: + clock-frequency: DT_FREQ_M(19) + - svd: hfrcoem23 + binding: "silabs,series2-hfrcoem23" + properties: + clock-frequency: DT_FREQ_M(19) + - svd: lfrco + binding: "silabs,series2-lfrco" + properties: + clock-frequency: 32768 + - svd: ulfrco + binding: "fixed-clock" + properties: + clock-frequency: 1000 + - svd: msc + node: flash-controller + binding: "silabs,series2-flash-controller" + chosen: "zephyr,flash-controller" + properties: + ranges: true + '#address-cells': 1 + '#size-cells': 1 + children: + - node: flash + labels: [flash0] + binding: "soc-nv-flash" + address: + cmsis_symbol: memory.flash.base + properties: + '#address-cells': 1 + '#size-cells': 1 + write-block-size: 4 + erase-block-size: + cmsis_symbol: memory.flash.page_size + - svd: usart0 + node: usart + binding: + - when: [xg22, xg27] + value: "silabs,usart-spi" + - value: "silabs,usart-uart" + status: disabled + skip_default_properties: true # Don't emit default properties since user can swap to spi binding + # Fallback: use USARTs for UART by default + - svd: usart + node: usart + binding: "silabs,usart-uart" + status: disabled + skip_default_properties: true # Don't emit default properties since user can swap to spi binding + - svd: eusart + node: eusart + binding: "silabs,eusart-spi" + interrupts: ["{peripheral}_rx", "{peripheral}_tx"] + status: disabled + skip_default_properties: true # Don't emit default properties since user can swap to uart binding + - svd: euart + node: eusart + binding: "silabs,eusart-uart" + interrupts: ["{peripheral}_rx", "{peripheral}_tx"] + status: disabled + - svd: burtc + labels: [burtc0] + binding: "silabs,burtc-counter" + status: disabled + - svd: semailbox + labels: [se] + binding: "silabs,gecko-semailbox" + chosen: "zephyr,entropy" + interrupts: [setamperhost, "sembrx", "sembtx"] + status: disabled + - svd: i2c + node: i2c + binding: "silabs,i2c" + include: "zephyr/dt-bindings/i2c/i2c.h" + properties: + clock-frequency: I2C_BITRATE_STANDARD + status: disabled + - svd: sysrtc + node: sysrtc + binding: "silabs,sysrtc" + chosen: "silabs,sleeptimer" + labels: [sysrtc0] + interrupts: [sysrtc_app, sysrtc_seq] + status: disabled + properties: + clock-frequency: 32768 + prescaler: 1 + - svd: rtcc + node: rtcc + binding: "silabs,rtcc" + chosen: "silabs,sleeptimer" + labels: [rtcc0] + status: disabled + properties: + clock-frequency: 32768 + prescaler: 1 + - svd: gpio + binding: "silabs,gpio" + include: "zephyr/dt-bindings/gpio/gpio.h" + properties: + ranges: true + '#address-cells': 1 + '#size-cells': 1 + children: + - handler: gpio + node: gpio + binding: "silabs,gpio-port" + properties: + gpio-controller: true + status: disabled + siblings: + - handler: pinctrl + node: pin-controller + binding: "silabs,dbus-pinctrl" + - handler: clkin0 + node: clkin0 + binding: "fixed-clock" + properties: + clock-frequency: DT_FREQ_M(38) + - svd: ldma$ + node: dma + labels: [dma0] + binding: "silabs,ldma" + include: "zephyr/dt-bindings/dma/silabs/{config}-dma.h" + properties: + dma-channels: + cmsis_symbol: CH_NUM + status: disabled + - svd: wdog + node: wdog + binding: "silabs,gecko-wdog" + # TODO: peripheral-id is required since the driver doesn't use clock control + status: disabled + - svd: iadc + node: adc + labels: [adc0] + binding: "silabs,iadc" + include: "zephyr/dt-bindings/adc/adc.h" + status: disabled + - svd: dcdc + binding: "silabs,series2-dcdc" + interrupts: [dcdc] + status: disabled + - svd: acmp + node: acmp + binding: "silabs,acmp" + status: disabled + - svd: timer + node: timer + binding: "silabs,series2-timer" + properties: + channels: + cmsis_symbol: CC_NUM + counter-size: + cmsis_symbol: CNTWIDTH + children: + - node: counter + binding: "silabs,timer-counter" + status: disabled + - node: pwm + binding: "silabs,timer-pwm" + status: disabled + status: disabled + - svd: letimer + node: letimer + binding: "silabs,series2-letimer" + children: + - node: pwm + binding: "silabs,letimer-pwm" + status: disabled + status: disabled + - svd: cryptoacc + labels: [trng] + chosen: "zephyr,entropy" + binding: "silabs,gecko-trng" + status: disabled + - svd: vdac + node: vdac + binding: "silabs,vdac" + status: disabled + interrupts: ["{peripheral}"] + properties: + "#address-cells": 1 + "#size-cells": 0 + children: + - node: channel + reg: [0] + reg_format: dec + - node: channel + reg: [1] + reg_format: dec + - svd: buram + node: retained-memory + binding: "silabs,buram" + reg_mode: narrow + status: disabled + - svd: gpcrc + labels: [gpcrc] + binding: "silabs,gpcrc" + chosen: "zephyr,crc" + status: okay + + +# skipped peripherals + - svd: scratchpad + skip: true # unused + - svd: emu + skip: true # handled by PM subsystem + - svd: dpll0 + skip: true # handled by the HFRCODPLL peripheral + - svd: icache0 + skip: true # configuration not yet exposed in Zephyr + - svd: prs + skip: true # PRS config is distributed + - svd: ldmaxbar + skip: true # handled by the LDMA peripheral + - svd: syscfg + skip: true # system config is handled by the SoC + - svd: hostmailbox + skip: true # handled by the radio driver + - svd: keyscan + skip: true # missing peripheral driver + - svd: dmem + skip: true # configuration not yet exposed in Zephyr + - svd: radioaes + skip: true # handled by the radio driver + - svd: bufc + skip: true # handled by the radio driver + - svd: smu + skip: true # configuration not yet exposed in Zephyr + - svd: pcnt + skip: true # missing peripheral driver + - svd: mvp + skip: true # missing peripheral driver + - svd: pdm + skip: true # missing peripheral driver + - svd: prortc + skip: true # handled by the radio driver + - svd: lesense + skip: true # missing peripheral driver + - svd: pfmxpprf + skip: true # handled by the DCDC peripheral + - svd: lcd + skip: true # missing peripheral driver + - svd: etampdet + skip: true # missing peripheral driver + - svd: amuxcp + skip: true # infrastructure diff --git a/scripts/gen_dts_board.py b/scripts/gen_dts_board.py new file mode 100755 index 00000000..a6df56b3 --- /dev/null +++ b/scripts/gen_dts_board.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import importlib +import logging +import os +import pkgutil +from pathlib import Path + +import dts.board +from util import copyright_header, board +from dts.node import Node, ChosenNode + +logger = logging.getLogger(__name__) + +ZEPHYR_BASE = Path(os.getenv("ZEPHYR_BASE")) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate .dts files for Series 2 boards." + ) + + parser.add_argument( + "--out", + "-o", + type=Path, + default=Path(__file__).parent / "out", + help="Output directory. Defaults to the directory ./out/ relative to the script. " + "Set to a directory in $ZEPHYR_BASE/boards/silabs/ to directly generate output " + "into the expected location within the Zephyr main tree.", + ) + + parser.add_argument( + "--sdk", + "-s", + type=Path, + default=Path(__file__).parent.parent / "simplicity_sdk", + help="Path to Simplicity SDK to extract data from. Defaults to the directory " + "../simplicity_sdk relative to the script.", + ) + + parser.add_argument( + "--board", + "-b", + default="xg24_dk2601b", + help="Device family to generate .dtsi for. Defaults to xg24_dk2601b if not set.", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable debug logging.", + ) + + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(message)s", + ) + + args.out.mkdir(exist_ok=True) + + kit_pn = args.board.replace("_", "-") + + if (args.sdk / "devices").exists(): + soc_dir = args.sdk / "devices" / "platform" / "Device" + board_dir = args.sdk / "boards" / "hardware" / "board" + elif (args.sdk / "platform_core").exists(): + soc_dir = args.sdk / "platform_core" / "platform" / "Device" + board_dir = args.sdk / "boards" / "hardware" / "board" + else: + soc_dir = args.sdk / "platform" / "Device" + board_dir = args.sdk / "hardware" / "board" + + dts_root = ZEPHYR_BASE / "dts" / "arm" + + boards = board.BoardDb(board_dir) + b = board.Board(args.board, boards, soc_dir, dts_root) + + nodes = [] + dt = Node("/") + dt.root = True + dt.add_include(b.soc_dtsi, priority=-100) + + dt.add_node(ChosenNode("aliases")) + dt.add_node(ChosenNode("chosen")) + + dt.add_string("model", f"Silicon Labs {b.name}") + dt.add_string("compatible", f"silabs,{b.id}") + + pinctrl = Node(labels=["pinctrl"]) + + for _loader, module_name, _is_pkg in pkgutil.iter_modules(dts.board.__path__): + full_name = f"{dts.board.__name__}.{module_name}" + module = importlib.import_module(full_name) + + n = module.generate(b, dt, pinctrl) + if n: + nodes += n + nodes.sort(key=lambda n: n.labels) + + for node in nodes: + dt.update_includes(node) + node.remove_includes() + + pinctrl_file = f"{args.board}-pinctrl.dtsi" + dt.add_include(pinctrl_file, local=True) + + # Write the output to a .dts file + nodes = [dt] + nodes + out_path = args.out / f"{args.board}.dts" + out_path.parent.mkdir(parents=True, exist_ok=True) + logger.info("Creating %s", out_path.name) + copyright_string = copyright_header.from_path(out_path) + out_path.write_text( + copyright_string + "\n".join([str(node) for node in nodes]), encoding="utf-8" + ) + + pinctrl_path = args.out / pinctrl_file + pinctrl_path.parent.mkdir(parents=True, exist_ok=True) + logger.info("Creating %s", pinctrl_path.name) + copyright_string = copyright_header.from_path(pinctrl_path) + pinctrl_path.write_text( + copyright_string + "\n".join([str(pinctrl)]), encoding="utf-8" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/gen_dts_soc_series2.py b/scripts/gen_dts_soc_series2.py new file mode 100755 index 00000000..5acd0ad5 --- /dev/null +++ b/scripts/gen_dts_soc_series2.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import logging +from pathlib import Path + +import yaml + +import util.copyright_header +import util.download +import util.sdk +import util.soc +from dts.soc.series2 import ( + create_device_tree, + create_clock_device_trees, + create_radio_device_tree, + create_family_device_tree, + create_soc_device_tree, +) +from dts.node import Node +from dts.prop import DeferredValue + +logger = logging.getLogger(__name__) + + +def main(): + gen_config = util.soc.GenConfig( + Path(__file__).parent / "dts" / "soc" / "series2.yml" + ) + + parser = argparse.ArgumentParser( + description="Generate .dtsi files for Series 2 SoCs." + ) + parser.add_argument( + "--out", + "-o", + type=Path, + default=Path(__file__).parent / "out", + help="Output directory. Defaults to the directory ./out/ relative to the " + "script. Set to $ZEPHYR_BASE/dts/arm/silabs/ to directly generate output " + "into the expected location within the Zephyr main tree.", + ) + parser.add_argument( + "--sdk", + "-s", + type=Path, + default=Path(__file__).parent.parent / "simplicity_sdk", + help="Path to Simplicity SDK to extract data from. Defaults to the directory " + "../simplicity_sdk relative to the script.", + ) + parser.add_argument( + "--family", + "-f", + default="xg24", + choices=gen_config.config_names, + help="Device family to generate .dtsi for. Defaults to xg24 if not set.", + ) + parser.add_argument( + "--soc-yml", + "-y", + type=Path, + help="Path to soc.yml to use for OPN filtering. Set to " + "$ZEPHYR_BASE/soc/silabs/soc.yml to use the file in the main tree.", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable debug logging.", + ) + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(message)s", + ) + + args.out.mkdir(exist_ok=True) + + if (args.sdk / "devices").exists(): + devices_dir = args.sdk / "devices" / "platform" / "Device" + modules_dir = args.sdk / "devices" / "hardware" / "module" / "config" + elif (args.sdk / "platform_core").exists(): + devices_dir = args.sdk / "platform_core" / "platform" / "Device" + modules_dir = args.sdk / "platform_core" / "hardware" / "module" / "config" + else: + devices_dir = args.sdk / "platform" / "Device" + modules_dir = args.sdk / "hardware" / "module" / "config" + + soc_filter = {} + if args.soc_yml and args.soc_yml.exists(): + soc_yml = yaml.safe_load(args.soc_yml.read_text(encoding="utf-8")) + for family in soc_yml.get("family", []): + for series in family.get("series", []): + soc_filter[series["name"]] = [] + for soc in series.get("socs", []): + soc_filter[series["name"]].append(soc["name"]) + + gen_config.select_config(args.family, devices_dir) + logger.info("Creating %s.dtsi", gen_config.config.name) + + packs = {} + for f in gen_config.config.families: + packs[f.name] = util.download.cmsis_pack( + Path(__file__).parent.absolute() / "cache", f.name + ) + + # Create .dtsi file for generic soc using first family + family = gen_config.config.families[0] + svd_dir = packs[family.name] / "SVD" / family.name.upper() + nodes = create_device_tree( + family.name, family.representative_device, gen_config, svd_dir + ) + + # Write the output to a .dtsi file + out_path = args.out / gen_config.config.name / f"{gen_config.config.name}.dtsi" + out_path.parent.mkdir(parents=True, exist_ok=True) + copyright_string = util.copyright_header.from_path(out_path) + out_path.write_text( + copyright_string + "\n".join([str(node) for node in nodes]), encoding="utf-8" + ) + + # Create .dtsi files for default clock configurations + for clock, tree in create_clock_device_trees(nodes[0]).items(): + out_path = args.out / gen_config.config.name / f"clock-{clock}.dtsi" + copyright_string = util.copyright_header.from_path(out_path) + out_path.write_text( + copyright_string + "\n".join([str(node) for node in tree]), encoding="utf-8" + ) + + # Create .dtsi file for the radio-enabled soc + logger.info("Creating efr32%s.dtsi", gen_config.config.name) + + radio_dt = create_radio_device_tree(gen_config.config) + + out_path = args.out / gen_config.config.name / f"efr32{gen_config.config.name}.dtsi" + out_path.parent.mkdir(parents=True, exist_ok=True) + copyright_string = util.copyright_header.from_path(out_path) + + out_path.write_text(copyright_string + str(radio_dt), encoding="utf-8") + + for family in gen_config.config.families: + if soc_filter: + if family.name not in soc_filter: + logger.info("Skipping %s, not in soc.yml", family.name) + continue + + logger.info("Creating %s.dtsi", family.name) + + nodes = create_family_device_tree(family, gen_config.config) + + out_path = args.out / gen_config.config.name / f"{family.name}.dtsi" + copyright_string = util.copyright_header.from_path(out_path) + out_path.write_text( + copyright_string + "\n".join(str(n) for n in nodes), encoding="utf-8" + ) + + svd_dir = packs[family.name] / "SVD" / family.name.upper() + + if not soc_filter: + logger.info("Including all SoCs in %s, no soc.yml given", family.name) + + for soc in svd_dir.glob("*.svd"): + soc = soc.stem.lower() + + if soc_filter and soc not in soc_filter.get(family.name, []): + logger.info("Skipping %s, not in soc.yml", soc) + continue + + logger.info("Creating %s.dtsi", soc) + + cmsis_path = ( + devices_dir + / "SiliconLabs" + / family.name.upper() + / "Include" + / f"{soc}.h" + ) + if not cmsis_path.exists(): + raise ValueError(f"Invalid SDK path {devices_dir}") + cmsis_config = util.sdk.CmsisDeviceConfig(cmsis_path) + module_config = util.sdk.defines_from_dir(modules_dir / soc.upper()) + + nodes = create_soc_device_tree( + soc, family, gen_config.config, cmsis_config, module_config + ) + + out_path = args.out / gen_config.config.name / f"{soc}.dtsi" + copyright_string = util.copyright_header.from_path(out_path) + out_path.write_text( + copyright_string + "\n".join(str(n) for n in nodes), encoding="utf-8" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/util/board.py b/scripts/util/board.py new file mode 100644 index 00000000..c22dd157 --- /dev/null +++ b/scripts/util/board.py @@ -0,0 +1,183 @@ +# Copyright (c) 2026 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +import yaml + +from dts.node import Node +from dts.prop import PhandleArrayProperty, PhandleProperty, StringArrayProperty + +from . import sdk + + +class BoardDb: + def __init__(self, board_dir: Path): + self._dir = board_dir + self._boards = {} + for f in (board_dir / "component").glob("*.slcc"): + slcc = dict(yaml.safe_load(Path(f).read_text(encoding="utf-8"))) + self._boards[slcc["id"]] = slcc + + def get(self, board: str): + return self._boards.get(board) + + def config(self, board: str): + f = self._dir / "config" / "component" / f"{board}_config.slcc" + slcc = dict(yaml.safe_load(f.read_text(encoding="utf-8"))) + return slcc + + def providing(self, req: str): + for board in self._boards.values(): + if any(p["name"] == req for p in board.get("provides")): + return board + + +class Board: + def __init__( + self, board_name: str, boards: BoardDb, soc_root: Path, dts_root: Path + ): + # Get kit data for board name + if not (kit := boards.get(board_name)): + raise ValueError(f"Unknown board: {board_name}") + + # Get board data for kit + for r in kit.get("requires"): + req = r["name"] + if req.startswith("hardware_board_from_"): + board = boards.providing(req) + if not board: + raise ValueError(f"No board provides {req}") + + # Resolve board revisions if board has multiple revisions + for r in board.get("requires"): + if r["name"].endswith("_revision"): + for rec in board.get("recommends"): + if board := boards.get(rec["id"]): + break + else: + raise ValueError(f"No board revision provides {r['name']}") + + self._kit = kit + self._board = board + + pn = self._tag("board:pn:") + self._tag("board:variant:") + config = boards.config(pn.lower()) + config_dir = list( + set( + Path(c["path"]).parent + for c in config.get("config_file") + if c.get("condition") in [None, ["brd4002a"]] + ) + )[0] + self.config = sdk.defines_from_dir(boards._dir / "config" / config_dir) + + soc_component = list(soc_root.rglob(f"{self.soc}.slcc"))[0] + soc_header = list(soc_root.rglob(f"{self.soc}.h"))[0] + _, self.soc_features = sdk.get_device_features(soc_component) + self.soc_config = sdk.CmsisDeviceConfig(soc_header) + + self.soc_dtsi = list(dts_root.rglob(f"{self.soc}.dtsi"))[0].relative_to( + dts_root + ) + self.soc_dir = self.soc_dtsi.parent + self.soc_family = self.soc_dir.name + + @property + def id(self) -> str: + return self._kit["id"] + + def _tag(self, key, multiple=False) -> str: + vals = [] + for t in self._board["tag"]: + if t.startswith(key): + val = t[len(key) :] + if multiple: + vals.append(val) + else: + return val + for t in self._kit["tag"]: + if t.startswith(key): + val = t[len(key) :] + if multiple: + vals.append(val) + else: + return val + if multiple: + return vals + else: + raise KeyError(f"Tag not found: {key}") + + @property + def name(self) -> str: + return self._tag("kit:opn:") + + @property + def soc(self) -> str: + return self._tag("board:device:") + + @property + def sensors(self) -> list[str]: + return self._tag("hardware:has:sensor:", multiple=True) + + @property + def spi_flash(self) -> str | None: + flash = None + try: + flash = self._tag("hardware:has:memory:spi:") + except: + pass + return flash + + @property + def display(self) -> str | None: + flash = None + try: + flash = self._tag("hardware:has:display:") + except: + pass + return flash + + def gpios(self, keys, prop_name, polarity=None, raw=False): + value = [] + for key in keys: + port = self.config[f"{key}_PORT"] + pin = self.config[f"{key}_PIN"] + if polarity is None: + polarity = "HIGH" in self.config.get(f"{key}_POLARITY", "HIGH") + + value.append( + [ + f"gpio{port[-1].lower()}", + pin, + "GPIO_ACTIVE_HIGH" if polarity else "GPIO_ACTIVE_LOW", + ] + ) + + if raw: + return value + else: + return PhandleArrayProperty(prop_name, value) + + def signals(self, peripheral_name, prefix, signals): + out = [] + for signal in signals: + port = self.config[f"{prefix}_{signal}_PORT"] + pin = self.config[f"{prefix}_{signal}_PIN"] + + out.append(f"{peripheral_name}_{signal}_P{port[-1]}{pin}") + + return out + + def pinctrl(self, name, mode, *groups): + pins = Node(f"{name}_{mode}", labels=[f"{name}_{mode}"]) + + for group in groups: + pins.add_node(group) + + props = [ + PhandleProperty("pinctrl-0", f"{name}_{mode}"), + StringArrayProperty("pinctrl-names", [mode]), + ] + + return pins, props diff --git a/scripts/util/sdk.py b/scripts/util/sdk.py new file mode 100644 index 00000000..19695eb2 --- /dev/null +++ b/scripts/util/sdk.py @@ -0,0 +1,206 @@ +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import re +import yaml +from pathlib import Path + + +class CmsisDeviceConfig: + """Represents the device configuration parsed from a CMSIS-Device header file.""" + + def __init__(self, hal_file): + text = hal_file.read_text() + self.symbols = {} + self.interrupts = {} + + self._load_peripheral_options(text) + self._load_cpu_options(text) + self._load_device_options(text) + self._load_memory_options(text) + self._load_interrupts(text) + self._load_fixed_routes(text) + + def get(self, key, parent=None): + """ + Get a configuration value from the CMSIS-Device configuration + """ + if "." in key: + parts = key.split(".") + c = self.symbols + for part in parts: + c = c[part] + elif parent: + c = self.symbols[parent][key] + else: + raise ValueError(f"Invalid config {key}") + return c + + def _load_interrupts(self, text): + matches = re.findall(r" (.*?)_IRQn\s+=\s+(\d+)", text) + self.interrupts = {name.lower(): int(num) for name, num in matches} + + def _load_peripheral_options(self, text): + cmsis_opts = re.findall( + r"^#define ([A-Z0-9_]+)\s+\(?0x([0-9A-F]+)UL\)?", text, flags=re.MULTILINE + ) + for name, value in cmsis_opts: + peripheral, symbol = name.split("_", 1) + if peripheral.lower() not in self.symbols: + self.symbols[peripheral.lower()] = {} + self.symbols[peripheral.lower()][symbol] = int(value, 16) + + def _load_cpu_options(self, text): + cpu_opts = re.findall( + r"^#define __([A-Z0-9_]+)_PRESENT\s+([0-9A-F]+)U", text, flags=re.MULTILINE + ) + self.symbols["cpu"] = { + "core": re.findall(r"^#define __([A-Z0-9]+)_REV", text, flags=re.MULTILINE)[ + 0 + ].lower(), + "nvic_prio_bits": int( + re.findall( + r"^#define __NVIC_PRIO_BITS\s+([0-9]+)U", text, flags=re.MULTILINE + )[0] + ), + } + for name, value in cpu_opts: + self.symbols["cpu"][name.lower()] = bool(value) + + def _load_device_options(self, text): + device_opts = re.findall( + r"^#define _SILICON_LABS_(?:(?:32B|GECKO_INTERNAL)_([A-Z0-9_]+)|" + r"([A-Z0-9_]+)_(?:TYPE|FEATURE|DBM))\s+([0-9A-Z_]+)", + text, + flags=re.MULTILINE, + ) + self.symbols["device"] = {} + for name1, name2, value in device_opts: + name = name1.lower() if name1 else name2.lower() + value = value.rsplit("_", 1)[-1] if "_" in value else int(value) + self.symbols["device"][name] = value + + def _load_memory_options(self, text): + memory_opts = re.findall( + r"^#define (FLASH|SRAM)_(BASE|(?:PAGE_)?SIZE)\s+\(0x([0-9A-F]+)UL\)", + text, + flags=re.MULTILINE, + ) + self.symbols["memory"] = {} + for region, prop, value in memory_opts: + if region.lower() not in self.symbols["memory"]: + self.symbols["memory"][region.lower()] = {} + self.symbols["memory"][region.lower()][prop.lower()] = int(value, 16) + + def _load_fixed_routes(self, text): + routes = {} + for per, sig, port_pin, value in re.findall( + r"^#define ([A-Z0-9]+)_([A-Z0-9_]+)_(PORT|PIN)\s+([0-9A-Z_]+)", + text, + flags=re.MULTILINE, + ): + per = per.lower() + sig = sig.lower() + if port_pin == "PORT": + port = value[6].lower() + if per not in routes: + routes[per] = {} + if sig not in routes[per]: + routes[per][sig] = (port, None) + else: + routes[per][sig] = (port, routes[per][sig][1]) + else: + pin = int(value.rstrip("U"), 10) + if per not in routes: + routes[per] = {} + if sig not in routes[per]: + routes[per][sig] = (None, pin) + else: + routes[per][sig] = (routes[per][sig][0], pin) + + self.symbols["routes"] = routes + + +def get_device_features(component_path: Path): + """ + Returns all provided features from a given SLC component as a list + """ + slcc = dict(yaml.safe_load(component_path.read_text())) + return (slcc["id"].lower(), [p["name"] for p in slcc["provides"]]) + + +def get_device_provides(sdk_path): + """ + Returns a dict mapping from device family to provided features for every soc + in the SDK. + """ + data = {} + component_path = sdk_path / "component" + if not component_path.exists(): + raise ValueError(f"Invalid SDK path: {sdk_path}") + + for f in component_path.glob("*.slcc"): + slcc_id, provides = get_device_features(f) + family = None + generic_family = "mcu" + for provide in provides: + if provide.startswith("device_family_"): + family = provide.split("_", 2)[-1] + if provide.startswith("device_generic_family_"): + generic_family = provide.split("_", 3)[-1] + + if generic_family not in data: + data[generic_family] = {} + if family not in data[generic_family]: + data[generic_family][family] = {} + + data[generic_family][family][slcc_id] = provides + + return data + + +def get_clock_config(hal_file): + """ + Returns a dict with the configuration of every clock branch in the given + SL Device Manager clock file from the HAL + """ + text = (Path(__file__).parents[2] / "simplicity_sdk" / hal_file).read_text() + matches = re.findall( + r"const sl_peripheral_(?:[a-z0-9]+_)?val_t.*?" + r"base = ([A-Z0-9_]+)_BASE.*?" + r"clk_branch = SL_([A-Z0-9_]+).*?" + r"bus_clock = SL_BUS_([A-Z0-9_]+)", + text, + flags=re.MULTILINE | re.DOTALL, + ) + + clocks = {} + for base, branch, clock in matches: + if ( + branch != "CLOCK_BRANCH_INVALID" + or clock != "CLOCK_INVALID" + or base in ["ACMP0", "ACMP1"] + ): + clocks[base.lower()] = { + "branch": branch, + "clock": clock if clock != "CLOCK_INVALID" else "CLOCK_AUTO", + } + + return clocks + + +def defines_from_dir(dir): + """ + Returns a dict with the configuration defines parsed from every file in the given directory. + """ + config_defines = {} + for f in sorted(dir.glob("*.h")): + text = f.read_text() + matches = re.findall( + r"^#define ([A-Z0-9_]+)(?:[\t ]+)(.+)", text, flags=re.MULTILINE + ) + for name, value in matches: + if name in config_defines: + continue + config_defines[name] = value + return config_defines diff --git a/scripts/util/soc.py b/scripts/util/soc.py new file mode 100644 index 00000000..1684e652 --- /dev/null +++ b/scripts/util/soc.py @@ -0,0 +1,174 @@ +# Copyright (c) 2025 Silicon Laboratories Inc. +# SPDX-License-Identifier: Apache-2.0 + +import copy +from pathlib import Path + +import yaml + +import util.sdk +from dts.prop import DeferredValue + + +class GenConfig: + def __init__(self, path: Path): + cfg = yaml.safe_load(path.read_text()) + self._configs = cfg.get("configs", []) + self._config = None + self.peripherals = cfg.get("peripherals", []) + self.clocks = ClockConfig(cfg.get("clocks", {})) + self.sdk = None + self.provides = None + + @property + def config_names(self): + return list(map(lambda c: c["name"], self._configs)) + + def select_config(self, name, sdk): + self.sdk = sdk + provides = util.sdk.get_device_provides(self.sdk) + for config in self._configs: + if config["name"] == name: + self._config = SharedConfig(config, self.sdk, provides) + break + else: + raise ValueError( + f"Configuration '{name}' not found in device configuration." + ) + + self.peripherals = self._resolve_values(copy.deepcopy(self.peripherals), name) + for k, v in self.clocks.selection.items(): + self.clocks.selection[k] = self._resolve_value( + f"clocks.selection.{k}", v, name + ) + + @property + def config(self): + return self._config + + def _resolve_value(self, prop, val, family): + if isinstance(val, list) and isinstance(val[0], dict) and "when" in val[0]: + for opt in val: + if family in opt.get("when", [family]) or "when" not in opt: + val = opt.get("value") + break + else: + raise ValueError( + f"No matching option found for {prop} (family {family})" + ) + elif isinstance(val, dict) and "cmsis_symbol" in val: + val = DeferredValue(val["cmsis_symbol"]) + + return val + + def _resolve_values(self, cfgs, family): + for cfg in cfgs: + for prop, val in cfg.get("properties", {}).items(): + cfg["properties"][prop] = self._resolve_value(prop, val, family) + if "address" in cfg: + cfg["address"] = self._resolve_value("address", cfg["address"], family) + if "children" in cfg: + cfg["children"] = self._resolve_values(cfg.get("children", []), family) + if "binding" in cfg: + cfg["binding"] = self._resolve_value("binding", cfg["binding"], family) + + return cfgs + + +class FamilyConfig: + """ + Configuration for a SoC family, e.g. efr32mg24 + """ + + def __init__(self, cfg): + self.name = cfg["name"] + self.representative_device = cfg.get("representative_device") + self.provides = None + + def soc_has(self, soc, needle): + """ + Returns true if the given ``soc`` provides every feature in ``needle`` + """ + if not isinstance(needle, list): + needle = [needle] + + return all(n in self.provides.get(soc) for n in needle) + + def any(self, needle): + """ + Returns true if any soc in the family provides every feature in ``needle`` + """ + if not isinstance(needle, list): + needle = [needle] + + return any( + all(n in provides for n in needle) for _, provides in self.provides.items() + ) + + def all(self, needle): + """ + Returns true if all socs in the family provide every feature in ``needle`` + """ + if not isinstance(needle, list): + needle = [needle] + + return all( + all(n in provides for n in needle) for _, provides in self.provides.items() + ) + + +class SharedConfig: + """ + Configuration for a generic family, e.g. xg24 + """ + + def __init__(self, config, sdk, provides): + self.name = config["name"] + self.clocks = util.sdk.get_clock_config(config["clocks"]) + self.families = [FamilyConfig(family) for family in config["families"]] + for family in self.families: + if family.name in provides[f"efr32{self.name}"]: + family.provides = provides[f"efr32{self.name}"][family.name] + else: + family.provides = provides["mcu"][family.name] + + representative_cmsis_path = ( + sdk + / "SiliconLabs" + / self.families[0].name.upper() + / "Include" + / f"{self.families[0].representative_device.lower()}.h" + ) + self.cmsis = util.sdk.CmsisDeviceConfig(representative_cmsis_path) + + def any(self, provides): + """ + Returns true if any soc in any family provides every feature in ``provides`` + """ + return any(f.any(provides) for f in self.families) + + def all(self, provides, radio_only=False): + """ + Returns true if all socs in all families provide every feature in ``provides``. + If ``radio_only`` is true, only socs with a radio are checked. + """ + + if radio_only: + return all( + f.all(provides) or not f.any("device_has_radio") for f in self.families + ) + else: + return all(f.all(provides) for f in self.families) + + +class ClockConfig: + extra_children: dict[str, list[str]] + selection: dict[str, str] + skip: list[str] + divider: dict[str, int] + + def __init__(self, cfg: dict): + self.extra_children = cfg.get("extra_children", {}) + self.selection = cfg.get("selection", {}) + self.skip = cfg.get("skip", []) + self.divider = cfg.get("divider", {})