blob: 1b48a0390afe9929911bb90072bd465154a0524d [file] [log] [blame]
"""
Utility to autogenerate Zephyr DT pinctrl files for all STM32 microcontrollers.
Usage::
python3 genpinctrl.py [-p /path/to/STM32CubeMX] [-o /path/to/output_dir]
Copyright (c) 2020 Teslabs Engineering S.L.
SPDX-License-Identifier: Apache-2.0
"""
import argparse
from collections import OrderedDict
import logging
from pathlib import Path
import re
import shutil
import xml.etree.ElementTree as ET
from jinja2 import Environment, FileSystemLoader
import yaml
logger = logging.getLogger(__name__)
SCRIPT_DIR = Path(__file__).absolute().parent
"""Script directory."""
REPO_ROOT = SCRIPT_DIR / ".." / ".."
"""Repository root (used for defaults)."""
CONFIG_FILE = SCRIPT_DIR / "stm32-pinctrl-config.yaml"
"""Configuration file."""
CONFIG_F1_FILE = SCRIPT_DIR / "stm32f1-pinctrl-config.yaml"
"""Configuration file for F1 series."""
TEMPLATE_FILE = "pinctrl-template.j2"
"""pinctrl template file."""
NS = "{http://mcd.rou.st.com/modules.php?name=mcu}"
"""MCU XML namespace."""
PINCTRL_ADDRESSES = {
"stm32f0": 0x48000000,
"stm32f1": 0x40010800,
"stm32f2": 0x40020000,
"stm32f3": 0x48000000,
"stm32f4": 0x40020000,
"stm32f7": 0x40020000,
"stm32g0": 0x50000000,
"stm32g4": 0x48000000,
"stm32h7": 0x58020000,
"stm32l0": 0x50000000,
"stm32l1": 0x40020000,
"stm32l4": 0x48000000,
"stm32l5": 0x42020000,
"stm32mp1": 0x50002000,
"stm32wb": 0x48000000,
}
"""pinctrl peripheral addresses for each family."""
def validate_config_entry(entry, family):
"""Validates pin configuration entry.
Args:
entry: Pin configuration entry.
family: STM32 family, e.g. "STM32F1".
Raises:
ValueError: If entry is not valid.
"""
if not entry.get("name"):
raise ValueError("Missing entry name")
if not entry.get("match"):
raise ValueError(f"Missing entry match for {entry['name']}")
if family == "STM32F1":
if not entry.get("mode"):
raise ValueError(f"Missing entry mode for {entry['name']}")
if entry["mode"] not in ("analog", "input", "alternate"):
raise ValueError(f"Invalid mode for {entry['name']}: {entry['mode']}")
else:
if entry.get("mode"):
if entry["mode"] not in ("analog", "alternate"):
raise ValueError(f"Invalid mode for {entry['name']}: {entry['mode']}")
if entry.get("bias"):
if entry["bias"] not in ("disable", "pull-up", "pull-down"):
raise ValueError(f"Invalid bias for {entry['name']}: {entry['bias']}")
if (
family == "STM32F1"
and entry["mode"] != "input"
and entry["bias"] != "disable"
):
raise ValueError(
f"Bias can only be set for input mode on F1 (entry: {entry['name']})"
)
if entry.get("drive"):
if entry["drive"] not in ("push-pull", "open-drain"):
raise ValueError(f"Invalid drive for {entry['name']}: {entry['drive']}")
if entry.get("slew-rate"):
if family == "STM32F1":
if entry["slew-rate"] not in (
"max-speed-10mhz",
"max-speed-2mhz",
"max-speed-50mhz",
):
raise ValueError(
f"Invalid slew rate for {entry['name']}: {entry['slew-rate']}"
)
else:
if entry["slew-rate"] not in (
"low-speed",
"medium-speed",
"high-speed",
"very-high-speed",
):
raise ValueError(
f"Invalid slew rate for {entry['name']}: {entry['slew-rate']}"
)
def format_mode(mode, af):
"""Format mode for FT (non-F1 series).
Args:
mode: Operation mode (analog, alternate).
af: Alternate function ("analog" or AF number).
Returns:
DT AF definition.
"""
if mode == "analog":
return "ANALOG"
elif mode == "alternate":
return f"AF{af:d}"
raise ValueError(f"Unsupported mode: {mode}")
def format_mode_f1(mode):
"""Format mode for DT (F1 series).
Args:
mode: Mode (analog, input, alternate).
Returns:
DT mode definition.
"""
if mode == "analog":
return "ANALOG"
elif mode == "input":
return "GPIO_IN"
elif mode == "alternate":
return "ALTERNATE"
raise ValueError(f"Unsupported mode: {mode}")
def format_remap(remap):
"""Format remap number for DT.
Args:
remap: Remap number (0..3).
Returns:
DT remap definition.
"""
if remap is None or remap == 0:
return "NO_REMAP"
elif remap == 1:
return "REMAP_1"
elif remap == 2:
return "REMAP_2"
elif remap == 3:
return "REMAP_FULL"
raise ValueError(f"Unsupported remap: {remap}")
def get_gpio_ip_afs(cube_path):
"""Obtain all GPIO IP alternate functions.
Example output::
{
"STM32L4P_gpio_v1_0": {
"PA2": {
"ADC1_IN2": "analog",
"EVENTOUT": 15,
"LPUART1_TX": 8,
...
},
...
},
"STM32F103x4_gpio_v1_0": {
"PB3": {
"ADC1_IN2": "analog",
"EVENTOUT": [0],
"LPUART1_TX": [0, 1],
...
},
...
},
...
}
Notes:
F1 series AF number corresponds to remap numbers.
Args:
cube_path: Path to CubeMX package.
Returns:
Dictionary of alternate functions.
"""
ip_path = cube_path / "db" / "mcu" / "IP"
if not ip_path.exists():
raise FileNotFoundError(f"IP DB folder '{ip_path}' does not exist")
results = dict()
for gpio_file in ip_path.glob("GPIO-*_Modes.xml"):
m = re.search(r"GPIO-(.*)_Modes.xml", gpio_file.name)
gpio_ip = m.group(1)
gpio_ip_entries = dict()
results[gpio_ip] = gpio_ip_entries
gpio_tree = ET.parse(gpio_file)
gpio_root = gpio_tree.getroot()
for pin in gpio_root.findall(NS + "GPIO_Pin"):
pin_name = pin.get("Name")
pin_entries = dict()
gpio_ip_entries[pin_name] = pin_entries
for signal in pin.findall(NS + "PinSignal"):
signal_name = signal.get("Name")
if "STM32F1" in gpio_ip:
remap_blocks = signal.findall(NS + "RemapBlock")
if remap_blocks is None:
logger.error(
f"Missing remaps for {signal_name} (ip: {gpio_ip})"
)
continue
for remap_block in remap_blocks:
name = remap_block.get("Name")
m = re.search(r"^[A-Z0-9]+_REMAP(\d+)", name)
if not m:
logger.error(
f"Unexpected remap format: {name} (ip: {gpio_ip})"
)
continue
remap_num = int(m.group(1))
if signal_name not in pin_entries:
pin_entries[signal_name] = list()
pin_entries[signal_name].append(remap_num)
else:
param = signal.find(NS + "SpecificParameter")
if param is None:
logger.error(
f"Missing parameters for {signal_name} (ip: {gpio_ip})"
)
continue
value = param.find(NS + "PossibleValue")
if value is None:
logger.error(
f"Missing signal value for {signal_name} (ip: {gpio_ip})"
)
continue
m = re.search(r"^GPIO_AF(\d+)_[A-Z0-9]+", value.text)
if not m:
logger.error(
f"Unexpected AF format: {value.text} (ip: {gpio_ip})"
)
continue
af_n = int(m.group(1))
pin_entries[signal_name] = af_n
return results
def get_mcu_signals(cube_path, gpio_ip_afs):
"""Obtain all MCU signals.
Example output::
{
"STM32WB": [
{
"name": "STM32WB30CEUx"
"pins: [
{
"port": "a",
"number": 0,
"signals" : [
{
"name": "ADC1_IN5",
"af": None,
},
{
"name": "UART1_TX",
"af": 3,
},
...
]
},
...
]
},
...
]
}
Args:
cube_path: Path to CubeMX.
gpio_ip_afs: GPIO IP alternate functions.
Returns:
Dictionary with all MCU signals.
"""
mcus_path = cube_path / "db" / "mcu"
if not mcus_path.exists():
raise FileNotFoundError(f"MCU DB folder '{mcus_path}' does not exist")
results = dict()
for mcu_file in mcus_path.glob("STM32*.xml"):
mcu_tree = ET.parse(mcu_file)
mcu_root = mcu_tree.getroot()
# obtain family, reference and GPIO IP
family = mcu_root.get("Family").replace("+", "")
ref = mcu_root.get("RefName")
gpio_ip_version = None
for ip in mcu_root.findall(NS + "IP"):
if ip.get("Name") == "GPIO":
gpio_ip_version = ip.get("Version")
break
if not gpio_ip_version:
logger.error(f"GPIO IP version not specified (mcu: {mcu_file})")
continue
if gpio_ip_version not in gpio_ip_afs:
logger.error(f"GPIO IP version {gpio_ip_version} not available")
continue
gpio_ip = gpio_ip_afs[gpio_ip_version]
# create reference entry on its family
if family not in results:
family_entries = list()
results[family] = family_entries
else:
family_entries = results[family]
pin_entries = list()
family_entries.append({"name": ref, "pins": pin_entries})
# process all pins
for pin in mcu_root.findall(NS + "Pin"):
if pin.get("Type") != "I/O":
continue
# obtain pin port (A, B, ...) and number (0, 1, ...)
pin_name = pin.get("Name")
m = re.search(r"^P([A-Z])(\d+).*$", pin_name)
if not m:
continue
pin_port = m.group(1).lower()
pin_number = int(m.group(2))
if pin_name not in gpio_ip:
continue
pin_afs = gpio_ip[pin_name]
pin_signals = list()
pin_entries.append(
{
"port": pin_port,
"pin": pin_number,
"signals": pin_signals,
}
)
# process all pin signals
for signal in pin.findall(NS + "Signal"):
if signal.get("Name") == "GPIO":
continue
signal_name = signal.get("Name")
if signal_name is None:
continue
if signal_name in pin_afs:
pin_af = pin_afs[signal_name]
if not isinstance(pin_af, list):
pin_af = [pin_af]
found_afs = pin_af
# STM32F1: assume NO_REMAP (af=0) if signal is not listed in pin_afs
elif family == "STM32F1":
found_afs = [0]
# Non STM32F1: No alternate function found, mode is analog
else:
found_afs = [None]
for af in found_afs:
pin_signals.append({"name": signal_name, "af": af})
return results
def main(cube_path, output):
"""Entry point.
Args:
cube_path: CubeMX path.
output: Output directory.
"""
with open(CONFIG_FILE) as f:
config = yaml.load(f, Loader=yaml.Loader)
with open(CONFIG_F1_FILE) as f:
config_f1 = yaml.load(f, Loader=yaml.Loader)
env = Environment(
trim_blocks=True, lstrip_blocks=True, loader=FileSystemLoader(SCRIPT_DIR)
)
env.filters["format_mode"] = format_mode
env.filters["format_mode_f1"] = format_mode_f1
env.filters["format_remap"] = format_remap
template = env.get_template(TEMPLATE_FILE)
gpio_ip_afs = get_gpio_ip_afs(cube_path)
mcu_signals = get_mcu_signals(cube_path, gpio_ip_afs)
if output.exists():
for entry in output.iterdir():
# Remove directories, ignore files (README)
if entry.is_dir():
shutil.rmtree(entry)
else:
output.mkdir(parents=True)
for family, refs in mcu_signals.items():
# obtain family pinctrl address
pinctrl_addr = PINCTRL_ADDRESSES.get(family.lower())
if not pinctrl_addr:
logger.error(f"Unsupported family: {family}")
continue
# create directory for each family
family_dir = output / family.lower()[5:]
if not family_dir.exists():
family_dir.mkdir()
# process each reference
for ref in refs:
entries = dict()
# process each pin in the current reference
for pin in ref["pins"]:
# process each pin available signal (matched against regex)
for signal in pin["signals"]:
if family == "STM32F1":
selected_config = config_f1
else:
selected_config = config
for af in selected_config:
validate_config_entry(af, family)
m = re.search(af["match"], signal["name"])
if not m:
continue
if af["name"] not in entries:
entries[af["name"]] = list()
# Define the signal mode using, by priority order:
# 1- the config mode (ie: "af["mode"]")
# 2- the inferred mode (an alternate function was found)
if af.get("mode") or family == "STM32F1":
signal["mode"] = af["mode"]
else:
if signal["af"] is not None:
signal["mode"] = "alternate"
else:
signal["mode"] = "analog"
entries[af["name"]].append(
{
"port": pin["port"],
"pin": pin["pin"],
"signal": signal["name"].lower(),
"af": signal["af"],
"mode": signal["mode"],
"drive": af.get("drive"),
"bias": af.get("bias"),
"slew-rate": af.get("slew-rate"),
"variant": af.get("variant"),
}
)
if not entries:
continue
# sort entries by group name
entries = OrderedDict(sorted(entries.items(), key=lambda kv: kv[0]))
# sort entries in each group by signal, port, pin
for group in entries:
entries[group] = sorted(
entries[group],
key=lambda entry: (
str(entry["signal"]).split("_")[0][-1],
entry["port"],
entry["pin"],
),
)
# write pinctrl file
ref_file = family_dir / (ref["name"].lower() + "-pinctrl.dtsi")
with open(ref_file, "w") as f:
f.write(
template.render(
family=family, pinctrl_addr=pinctrl_addr, entries=entries
)
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-p",
"--cube-path",
type=Path,
default=Path.home() / "STM32CubeMX",
help="CubeMX path",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=REPO_ROOT / "dts" / "st",
help="Output directory",
)
args = parser.parse_args()
main(args.cube_path, args.output)