scripts: genpinctrl: script for DT pinctrl generation

Initial version of the genpinctrl script. This script can be used to
automatically generate DT files with pinctrl configuration for all STM32
families using CubeMX database.

Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
This commit is contained in:
Gerard Marull-Paretas 2020-09-04 18:41:57 +02:00 committed by Kumar Gala
parent cea57f86d3
commit bd2e89c49b
5 changed files with 862 additions and 0 deletions

View File

@ -0,0 +1,146 @@
# Configuration file for pinctrl generation (F1 series only).
#
# This file contains a list of pin configuration templates used to generate the
# pinctrl files. Each entry can have the following fields:
#
# - name (mandatory): This is the pin function name, e.g. UART_TX. It is used
# to group pin configurations alphabetically in the generated pinctrl files.
#
# - match (mandatory): This is a regular expression used to match against
# STM32 xml database pin configuration names. The regular expression should
# be as precise as possible. Note that it needs to be escaped here in the
# configuration file.
#
# - mode (mandatory): Mode setting (analog, alternate, input). Mode needs to
# be set according to the following rules:
# * Pin operates in analog configuration: analog
# * Pin operates in alternate function input configuration: input
# * Pin operates in alternate function output/bidirectional configuration:
# alternate
#
# - bias (optional): Bias setting (disable, pull-up, pull-down). This setting
# only applies to "input" mode. Equivalent to "disable" (a.k.a floating) if
# not set.
#
# - drive (optional): Drive setting (push-pull, open-drain). Equivalent to
# "push-pull" if not set.
#
# - slew-rate (optional): Slew rate setting (max-speed-10mhz, max-speed-2mhz,
# max-speed-50mhz). Equivalent to "max-speed-10mhz" if not set.
#
# - variant (optional): Defines an alternative pin configuration. This is used
# to provide multiple configurations of a pin function (slave, master,
# low-power, ...).
#
---
- name: ADC_IN
match: "^ADC\\d+_IN\\d+$"
mode: analog
- name: CAN_RX
match: "^CAN\\d+_RX$"
mode: input
- name: CAN_TX
match: "^CAN\\d+_TX$"
mode: alternate
- name: DAC_OUT
match: "^DAC_OUT\\d+$"
mode: analog
- name: I2C_SCL
match: "^I2C\\d+_SCL$"
drive: open-drain
mode: alternate
- name: I2C_SDA
match: "^I2C\\d+_SDA$"
drive: open-drain
mode: alternate
- name: I2S_CK
match: "^I2S\\d+_CK$"
mode: alternate
- name: I2S_WS
match: "^I2S\\d+_WS$"
mode: alternate
- name: I2S_SD
match: "^I2S\\d+_SD$"
mode: alternate
- name: SPI_MASTER_MISO
match: "^SPI\\d+_MISO$"
bias: pull-down
mode: input
variant: master
- name: SPI_MASTER_MOSI
match: "^SPI\\d+_MOSI$"
mode: alternate
variant: master
- name: SPI_MASTER_SCK
match: "^SPI\\d+_SCK$"
mode: alternate
variant: master
- name: SPI_MASTER_NSS
match: "^SPI\\d+_NSS$"
mode: alternate
variant: master
- name: SPI_SLAVE_MISO
match: "^SPI\\d+_MISO$"
mode: alternate
variant: slave
- name: SPI_SLAVE_MOSI
match: "^SPI\\d+_MOSI$"
mode: input
variant: slave
- name: SPI_SLAVE_SCK
match: "^SPI\\d+_SCK$"
mode: input
variant: slave
- name: SPI_SLAVE_NSS
match: "^SPI\\d+_NSS$"
bias: pull-up
mode: input
variant: slave
- name: TIM_CH_PWM / TIM_CHN_PWM
match: "^TIM\\d+_CH\\d+N?$"
mode: alternate
variant: pwm
- name: UART_CTS / USART_CTS
match: "^US?ART\\d+_CTS$"
drive: open-drain
bias: pull-up
mode: input
- name: UART_RTS / USART_RTS
match: "^US?ART\\d+_RTS$"
mode: alternate
- name: UART_TX / USART_TX
match: "^US?ART\\d+_TX$"
mode: alternate
- name: UART_RX / USART_RX
match: "^US?ART\\d+_RX$"
mode: input
- name: USB_OTG_FS_DM
match: "^USB_OTG_FS_DM$"
mode: alternate
- name: USB_OTG_FS_DP
match: "^USB_OTG_FS_DP$"
mode: alternate

View File

@ -0,0 +1,138 @@
# Configuration file for pinctrl generation (except F1 series).
#
# This file contains a list of pin configuration templates used to generate the
# pinctrl files. Each entry can have the following fields:
#
# - name (mandatory): This is the pin function name, e.g. UART_TX. It is used
# to group pin configurations alphabetically in the generated pinctrl files.
#
# - match (mandatory): This is a regular expression used to match against
# STM32 xml database pin configuration names. The regular expression should
# be as precise as possible. Note that it needs to be escaped here in the
# configuration file.
#
# - mode (mandatory): Mode setting (analog, alternate). Mode needs to
# be set according to the following rules:
# * Pin operates in analog configuration: analog
# * Pin operates in alternate function configuration: alternate
#
# - bias (optional): Bias setting (disable, pull-up, pull-down). Equivalent to
# "disable" (a.k.a floating) if not set.
#
# - drive (optional): Drive setting (push-pull, open-drain). Equivalent to
# "push-pull" if not set.
#
# - slew-rate (optional): Slew rate setting (low-speed, medium-speed,
# high-speed, very-high-speed). Equivalent to "low-speed" if not set.
#
# - variant (optional): Defines an alternative pin configuration. This is used
# to provide multiple configurations of a pin function (slave, master,
# low-power, ...).
#
---
- name: ADC_IN / ADC_INN / ADC_INP
match: "^ADC(?:\\d+)?_IN[NP]?\\d+$"
mode: analog
- name: CAN_RX
match: "^CAN\\d+_RX$"
mode: alternate
bias: pull-up
- name: CAN_TX
match: "^CAN\\d+_TX$"
mode: alternate
- name: DAC_OUT
match: "^DAC(?:\\d+)?_OUT\\d+$"
mode: analog
- name: FDCAN_RX
match: "^FDCAN\\d+_RX$"
mode: alternate
- name: FDCAN_TX
match: "^FDCAN\\d+_TX$"
mode: alternate
- name: I2C_SCL
match: "^I2C\\d+_SCL$"
mode: alternate
drive: open-drain
bias: pull-up
- name: I2C_SDA
match: "^I2C\\d+_SDA$"
mode: alternate
drive: open-drain
bias: pull-up
- name: I2S_CK
match: "^I2S\\d+_CK$"
mode: alternate
- name: I2S_WS
match: "^I2S\\d+_WS$"
mode: alternate
- name: I2S_SD
match: "^I2S\\d+_SD$"
mode: alternate
- name: SPI_MISO
match: "^SPI\\d+_MISO$"
mode: alternate
bias: pull-down
- name: SPI_MOSI
match: "^SPI\\d+_MOSI$"
mode: alternate
bias: pull-down
# NOTE: The SPI_SCK pins speed must be set to very-high-speed to avoid last data
# bit corruption which is a known issue on multiple STM32F4 series SPI
# peripheral (ref. ES0182 Rev 12, 2.5.12, p. 22).
- name: SPI_SCK
match: "^SPI\\d+_SCK$"
mode: alternate
slew-rate: very-high-speed
- name: SPI_NSS
match: "^SPI\\d+_NSS$"
mode: alternate
bias: pull-up
- name: TIM_CH_PWM / TIM_CHN_PWM
match: "^TIM\\d+_CH\\d+N?$"
mode: alternate
variant: pwm
- name: UART_CTS / USART_CTS / LPUART_CTS
match: "^(?:LP)?US?ART\\d+_CTS$"
mode: alternate
drive: open-drain
bias: pull-up
- name: UART_RTS / USART_RTS / LPUART_RTS
match: "^(?:LP)?US?ART\\d+_RTS$"
mode: alternate
drive: open-drain
bias: pull-up
- name: UART_TX / USART_TX / LPUART_TX
match: "^(?:LP)?US?ART\\d+_TX$"
mode: alternate
bias: pull-up
- name: UART_RX / USART_RX / LPUART_RX
match: "^(?:LP)?US?ART\\d+_RX$"
mode: alternate
- name: USB_OTG_FS_DM
match: "^USB_OTG_FS_DM$"
mode: alternate
- name: USB_OTG_FS_DP
match: "^USB_OTG_FS_DP$"
mode: alternate

View File

@ -0,0 +1,532 @@
"""
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 glob
import logging
import os
import re
import shutil
import xml.etree.ElementTree as ET
from jinja2 import Environment, FileSystemLoader
import yaml
logger = logging.getLogger(__name__)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
"""Script directory."""
CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.yaml")
"""Configuration file."""
CONFIG_F1_FILE = os.path.join(SCRIPT_DIR, "config-f1.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 not entry.get("mode"):
raise ValueError(f"Missing entry mode for {entry['name']}")
if family == "STM32F1":
if entry["mode"] not in ("analog", "input", "alternate"):
raise ValueError(f"Invalid mode for {entry['name']}: {entry['mode']}")
else:
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 "FULL_REMAP"
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,
...
},
...
},
...
}
Notes:
F1 series AF number corresponds to remap number.
Args:
cube_path: Path to CubeMX package.
Returns:
Dictionary of alternate functions.
"""
ip_path = os.path.join(cube_path, "db", "mcu", "IP")
if not os.path.exists(ip_path):
raise FileNotFoundError(f"IP DB folder '{ip_path}' does not exist")
results = dict()
for gpio_file in glob.glob(os.path.join(ip_path, "GPIO-*_Modes.xml")):
m = re.search(r"GPIO-(.*)_Modes.xml", gpio_file)
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:
# NOTE: only first remap is taken as in case of multiple remaps
# they all map to the same pin
remap_block = signal.find(NS + "RemapBlock")
if remap_block is None:
logger.error(
f"Missing remaps for {signal_name} (ip: {gpio_ip})"
)
continue
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))
pin_entries[signal_name] = 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 = os.path.join(cube_path, "db", "mcu")
if not os.path.exists(mcus_path):
raise FileNotFoundError(f"MCU DB folder '{mcus_path}' does not exist")
results = dict()
for mcu_file in glob.glob(os.path.join(mcus_path, "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 re.match(r"^ADC(?:\d+)?_IN[NP]?\d+$", signal_name) or re.match(
r"^DAC(?:\d+)?_OUT\d+$", signal_name
):
pin_af = None
elif signal_name in pin_afs:
pin_af = pin_afs[signal_name]
else:
continue
pin_signals.append({"name": signal_name, "af": pin_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 os.path.exists(output):
shutil.rmtree(output)
os.makedirs(output)
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 = os.path.join(output, family.lower()[5:])
if not os.path.exists(family_dir):
os.makedirs(family_dir)
# 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()
entries[af["name"]].append(
{
"port": pin["port"],
"pin": pin["pin"],
"signal": signal["name"].lower(),
"af": signal["af"],
"mode": af["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 = os.path.join(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", required=True, help="CubeMX path")
parser.add_argument("-o", "--output", required=True, help="Output directory")
args = parser.parse_args()
main(args.cube_path, args.output)

View File

@ -0,0 +1,44 @@
/*
* NOTE: Autogenerated file using genpinctrl.py
*
* SPDX-License-Identifier: Apache-2.0
*/
{% if family == "STM32F1" %}
#include <dt-bindings/pinctrl/stm32f1-pinctrl.h>
{% else %}
#include <dt-bindings/pinctrl/stm32-pinctrl.h>
{% endif %}
/ {
soc {
pinctrl: pin-controller@{{ "%08X" | format(pinctrl_addr) }} {
{{ newline }}
{% for group, group_entries in entries.items() %}
/* {{ group }} */
{{ newline }}
{% for entry in group_entries %}
{% set variant = "_" + entry["variant"] if entry["variant"] else "" %}
{% set name = "%s%s_p%s%d" | format(entry["signal"], variant, entry["port"], entry["pin"]) %}
{{ name }}: {{ name }} {
{% if family == "STM32F1" %}
pinmux = <STM32F1_PINMUX('{{ entry["port"] | upper }}', {{ entry["pin"] }}, {{ entry["mode"] | format_mode_f1 }}, {{ entry["af"] | format_remap }})>;
{% else %}
pinmux = <STM32_PINMUX('{{ entry["port"] | upper }}', {{ entry["pin"] }}, {{ entry["mode"] | format_mode(entry["af"]) }})>;
{% endif %}
{% if entry["bias"] %}
bias-{{ entry["bias"] }};
{% endif %}
{% if entry["drive"] %}
drive-{{ entry["drive"] }};
{% endif %}
{% if entry["slew-rate"] %}
slew-rate = "{{ entry["slew-rate"] }}";
{% endif %}
};
{{ newline }}
{% endfor %}
{% endfor %}
};
};
};

2
scripts/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Jinja2~=2.11
pyyaml~=5.3