blob: 1618522d0aa0938ef75748f0909708df925db880 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2021 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
"""
Pinctrl Migration Utility Script for nRF Boards
###############################################
This script can be used to automatically migrate the Devicetree files of
nRF-based boards using the old <signal>-pin properties to select peripheral
pins. The script will parse a board Devicetree file and will first adjust that
file by removing old pin-related properties replacing them with pinctrl states.
A board-pinctrl.dtsi file will be generated containing the configuration for
all pinctrl states. Note that script will also work on files that have been
partially ported.
.. warning::
This script uses a basic line based parser, therefore not all valid
Devicetree files will be converted correctly. **ADJUSTED/GENERATED FILES
MUST BE MANUALLY REVIEWED**.
Usage::
python3 pinctrl_nrf_migrate.py
-i path/to/board.dts
[--no-backup]
[--skip-nrf-check]
[--header ""]
Example:
.. code-block:: devicetree
/* Old board.dts */
...
&uart0 {
...
tx-pin = <5>;
rx-pin = <33>;
rx-pull-up;
...
};
/* Adjusted board.dts */
...
#include "board-pinctrl.dtsi"
...
&uart0 {
...
pinctrl-0 = <&uart0_default>;
pinctrl-1 = <&uart0_sleep>;
pinctrl-names = "default", "sleep";
...
};
/* Generated board-pinctrl.dtsi */
&pinctrl {
uart0_default: uart0_default {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 5);
};
group2 {
psels = <NRF_PSEL(UART_RX, 1, 1)>;
bias-pull-up;
};
};
uart0_sleep: uart0_sleep {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 5)>,
<NRF_PSEL(UART_RX, 1, 1)>;
low-power-enable;
};
};
};
"""
import argparse
import enum
from pathlib import Path
import re
import shutil
from typing import Callable, Optional, Dict, List
#
# Data types and containers
#
class PIN_CONFIG(enum.Enum):
"""Pin configuration attributes"""
PULL_UP = "bias-pull-up"
PULL_DOWN = "bias-pull-down"
LOW_POWER = "low-power-enable"
class Device(object):
"""Device configuration class"""
def __init__(
self, pattern: str, callback: Callable, signals: Dict[str, str]
) -> None:
self.pattern = pattern
self.callback = callback
self.signals = signals
class SignalMapping(object):
"""Signal mapping (signal<>pin)"""
def __init__(self, signal: str, pin: int) -> None:
self.signal = signal
self.pin = pin
class PinGroup(object):
"""Pin group"""
def __init__(self, pins: List[SignalMapping], config: List[PIN_CONFIG]) -> None:
self.pins = pins
self.config = config
class PinConfiguration(object):
"""Pin configuration (mapping and configuration)"""
def __init__(self, mapping: SignalMapping, config: List[PIN_CONFIG]) -> None:
self.mapping = mapping
self.config = config
class DeviceConfiguration(object):
"""Device configuration"""
def __init__(self, name: str, pins: List[PinConfiguration]) -> None:
self.name = name
self.pins = pins
def add_signal_config(self, signal: str, config: PIN_CONFIG) -> None:
"""Add configuration to signal"""
for pin in self.pins:
if signal == pin.mapping.signal:
pin.config.append(config)
return
self.pins.append(PinConfiguration(SignalMapping(signal, -1), [config]))
def set_signal_pin(self, signal: str, pin: int) -> None:
"""Set signal pin"""
for pin_ in self.pins:
if signal == pin_.mapping.signal:
pin_.mapping.pin = pin
return
self.pins.append(PinConfiguration(SignalMapping(signal, pin), []))
#
# Content formatters and writers
#
def gen_pinctrl(
configs: List[DeviceConfiguration], input_file: Path, header: str
) -> None:
"""Generate board-pinctrl.dtsi file
Args:
configs: Board configs.
input_file: Board DTS file.
"""
last_line = 0
pinctrl_file = input_file.parent / (input_file.stem + "-pinctrl.dtsi")
# append content before last node closing
if pinctrl_file.exists():
content = open(pinctrl_file).readlines()
for i, line in enumerate(content[::-1]):
if re.match(r"\s*};.*", line):
last_line = len(content) - (i + 1)
break
out = open(pinctrl_file, "w")
if not last_line:
out.write(header)
out.write("&pinctrl {\n")
else:
for line in content[:last_line]:
out.write(line)
for config in configs:
# create pin groups with common configuration (default state)
default_groups: List[PinGroup] = []
for pin in config.pins:
merged = False
for group in default_groups:
if group.config == pin.config:
group.pins.append(pin.mapping)
merged = True
break
if not merged:
default_groups.append(PinGroup([pin.mapping], pin.config))
# create pin group for low power state
group = PinGroup([], [PIN_CONFIG.LOW_POWER])
for pin in config.pins:
group.pins.append(pin.mapping)
sleep_groups = [group]
# generate default and sleep state entries
out.write(f"\t{config.name}_default: {config.name}_default {{\n")
out.write(fmt_pinctrl_groups(default_groups))
out.write("\t};\n\n")
out.write(f"\t{config.name}_sleep: {config.name}_sleep {{\n")
out.write(fmt_pinctrl_groups(sleep_groups))
out.write("\t};\n\n")
if not last_line:
out.write("};\n")
else:
for line in content[last_line:]:
out.write(line)
out.close()
def board_is_nrf(content: List[str]) -> bool:
"""Check if board is nRF based.
Args:
content: DT file content as list of lines.
Returns:
True if board is nRF based, False otherwise.
"""
for line in content:
m = re.match(r'^#include\s+(?:"|<).*nrf.*(?:>|").*', line)
if m:
return True
return False
def fmt_pinctrl_groups(groups: List[PinGroup]) -> str:
"""Format pinctrl groups.
Example generated content::
group1 {
psels = <NRF_PSEL(UART_TX, 0, 5)>;
};
group2 {
psels = <NRF_PSEL(UART_RX, 1, 1)>;
bias-pull-up;
};
Returns:
Generated groups.
"""
content = ""
for i, group in enumerate(groups):
content += f"\t\tgroup{i + 1} {{\n"
# write psels entries
for i, mapping in enumerate(group.pins):
prefix = "psels = " if i == 0 else " "
suffix = ";" if i == len(group.pins) - 1 else ","
pin = mapping.pin
port = 0 if pin < 32 else 1
if port == 1:
pin -= 32
content += (
f"\t\t\t{prefix}<NRF_PSEL({mapping.signal}, {port}, {pin})>{suffix}\n"
)
# write all pin configuration (bias, low-power, etc.)
for entry in group.config:
content += f"\t\t\t{entry.value};\n"
content += "\t\t};\n"
return content
def fmt_states(device: str, indent: str) -> str:
"""Format state entries for the given device.
Args:
device: Device name.
indent: Intentation.
Returns:
State entries to be appended to the device.
"""
return "\n".join(
(
f"{indent}pinctrl-0 = <&{device}_default>;",
f"{indent}pinctrl-1 = <&{device}_sleep>;",
f'{indent}pinctrl-names = "default", "sleep";\n',
)
)
def insert_pinctrl_include(content: List[str], board: str) -> None:
"""Insert board pinctrl include if not present.
Args:
content: DT file content as list of lines.
board: Board name
"""
already = False
include_last_line = -1
root_line = -1
for i, line in enumerate(content):
# check if file already includes a board pinctrl file
m = re.match(r'^#include\s+".*-pinctrl\.dtsi".*', line)
if m:
already = True
continue
# check if including
m = re.match(r'^#include\s+(?:"|<)(.*)(?:>|").*', line)
if m:
include_last_line = i
continue
# check for root entry
m = re.match(r"^\s*/\s*{.*", line)
if m:
root_line = i
break
if include_last_line < 0 and root_line < 0:
raise ValueError("Unexpected DT file content")
if not already:
if include_last_line >= 0:
line = include_last_line + 1
else:
line = max(0, root_line - 1)
content.insert(line, f'#include "{board}-pinctrl.dtsi"\n')
def adjust_content(content: List[str], board: str) -> List[DeviceConfiguration]:
"""Adjust content
Args:
content: File content to be adjusted.
board: Board name.
"""
configs: List[DeviceConfiguration] = []
level = 0
in_device = False
new_content = []
for line in content:
# look for a device reference node (e.g. &uart0)
if not in_device:
m = re.match(r"^[^&]*&([a-z0-9]+)\s*{[^}]*$", line)
if m:
# check if device requires processing
current_device = None
for device in DEVICES:
if re.match(device.pattern, m.group(1)):
current_device = device
indent = ""
config = DeviceConfiguration(m.group(1), [])
configs.append(config)
break
# we are now inside a device node
in_device = True
level = 1
else:
# entering subnode
if re.match(r"[^\/\*]*{.*", line):
level += 1
# exiting subnode (or device node)
elif re.match(r"[^\/\*]*}.*", line):
level -= 1
in_device = level > 0
elif current_device:
# device already ported, drop
if re.match(r"[^\/\*]*pinctrl-\d+.*", line):
current_device = None
configs.pop()
# determine indentation
elif not indent:
m = re.match(r"(\s+).*", line)
if m:
indent = m.group(1)
# process each device line, append states at the end
if current_device:
if in_device:
line = current_device.callback(config, current_device.signals, line)
else:
line = fmt_states(config.name, indent) + line
current_device = None
if line:
new_content.append(line)
if configs:
insert_pinctrl_include(new_content, board)
content[:] = new_content
return configs
#
# Processing utilities
#
def match_and_store_pin(
config: DeviceConfiguration, signals: Dict[str, str], line: str
) -> bool:
"""Match and store a pin mapping.
Args:
config: Device configuration.
signals: Signals name mapping.
line: Line containing potential pin mapping.
Returns:
True if line contains a pin mapping, False otherwise.
"""
m = re.match(r"\s*([a-z]+)-pin\s*=\s*<(\d+)>.*", line)
if m:
config.set_signal_pin(signals[m.group(1)], int(m.group(2)))
return True
return False
#
# Device processing callbacks
#
def process_uart(config: DeviceConfiguration, signals, line: str) -> Optional[str]:
"""Process UART/UARTE devices."""
# check if line specifies a pin
if match_and_store_pin(config, signals, line):
return None
# check if pull-up is specified
m = re.match(r"\s*([a-z]+)-pull-up.*", line)
if m:
config.add_signal_config(signals[m.group(1)], PIN_CONFIG.PULL_UP)
return None
return line
DEVICES = [
Device(
r"uart\d",
process_uart,
{
"tx": "UART_TX",
"rx": "UART_RX",
"rts": "UART_RTS",
"cts": "UART_CTS",
},
),
]
"""Supported devices and associated configuration"""
def main(input_file: Path, no_backup: bool, skip_nrf_check: bool, header: str) -> None:
"""Entry point
Args:
input_file: Input DTS file.
no_backup: Do not create backup files.
"""
board_name = input_file.stem
content = open(input_file).readlines()
if not skip_nrf_check and not board_is_nrf(content):
print(f"Board {board_name} is not nRF based, terminating")
return
if not no_backup:
backup_file = input_file.parent / (board_name + ".bck" + input_file.suffix)
shutil.copy(input_file, backup_file)
configs = adjust_content(content, board_name)
if configs:
with open(input_file, "w") as f:
f.writelines(content)
gen_pinctrl(configs, input_file, header)
print(f"Board {board_name} Devicetree file has been converted")
else:
print(f"Nothing to be converted for {board_name}")
if __name__ == "__main__":
parser = argparse.ArgumentParser("pinctrl migration utility for nRF")
parser.add_argument(
"-i", "--input", type=Path, required=True, help="Board DTS file"
)
parser.add_argument(
"--no-backup", action="store_true", help="Do not create backup files"
)
parser.add_argument(
"--skip-nrf-check",
action="store_true",
help="Skip checking if board is nRF-based",
)
parser.add_argument(
"--header",
default="",
type=str,
help="Header to be prepended to pinctrl files"
)
args = parser.parse_args()
main(args.input, args.no_backup, args.skip_nrf_check, args.header)