#!/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 -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**. Known limitations: All SPI nodes will be assumed to be a master device. 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 = ; bias-pull-up; }; }; uart0_sleep: uart0_sleep { group1 { psels = , ; 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" NORDIC_INVERT = "nordic,invert" class Device(object): """Device configuration class""" def __init__( self, pattern: str, callback: Callable, signals: Dict[str, str], needs_sleep: bool, ) -> None: self.pattern = pattern self.callback = callback self.signals = signals self.needs_sleep = needs_sleep self.attrs = {} 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 = ; }; group2 { psels = ; 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 "\t" 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}{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, needs_sleep: bool) -> str: """Format state entries for the given device. Args: device: Device name. indent: Indentation. needs_sleep: If sleep entry is needed. Returns: State entries to be appended to the device. """ if needs_sleep: return "\n".join( ( f"{indent}pinctrl-0 = <&{device}_default>;", f"{indent}pinctrl-1 = <&{device}_sleep>;", f'{indent}pinctrl-names = "default", "sleep";\n', ) ) else: return "\n".join( ( f"{indent}pinctrl-0 = <&{device}_default>;", f'{indent}pinctrl-names = "default";\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 states_written = 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 level = 1 in_device = True states_written = False else: # entering subnode (must come after all properties) 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 level == 1: line = current_device.callback(config, current_device.signals, line) if (level == 2 or not in_device) and not states_written: line = ( fmt_states(config.name, indent, current_device.needs_sleep) + line ) states_written = True 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 ) -> Optional[str]: """Match and store a pin mapping. Args: config: Device configuration. signals: Signals name mapping. line: Line containing potential pin mapping. Returns: Line if found a pin mapping, None otherwise. """ # handle qspi special case for io-pins (array case) m = re.match(r"\s*io-pins\s*=\s*([\s<>,0-9]+).*", line) if m: pins = re.sub(r"[<>,]", "", m.group(1)).split() for i, pin in enumerate(pins): config.set_signal_pin(signals[f"io{i}"], int(pin)) return m = re.match(r"\s*([a-z]+\d?)-pins?\s*=\s*<(\d+)>.*", line) if m: config.set_signal_pin(signals[m.group(1)], int(m.group(2))) return return line # # Device processing callbacks # def process_uart(config: DeviceConfiguration, signals, line: str) -> Optional[str]: """Process UART/UARTE devices.""" # check if line specifies a pin if not match_and_store_pin(config, signals, line): return # 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 return line def process_spi(config: DeviceConfiguration, signals, line: str) -> Optional[str]: """Process SPI devices.""" # check if line specifies a pin if not match_and_store_pin(config, signals, line): return # check if pull-up is specified m = re.match(r"\s*miso-pull-up.*", line) if m: config.add_signal_config(signals["miso"], PIN_CONFIG.PULL_UP) return # check if pull-down is specified m = re.match(r"\s*miso-pull-down.*", line) if m: config.add_signal_config(signals["miso"], PIN_CONFIG.PULL_DOWN) return return line def process_pwm(config: DeviceConfiguration, signals, line: str) -> Optional[str]: """Process PWM devices.""" # check if line specifies a pin if not match_and_store_pin(config, signals, line): return # check if channel inversion is specified m = re.match(r"\s*([a-z0-9]+)-inverted.*", line) if m: config.add_signal_config(signals[m.group(1)], PIN_CONFIG.NORDIC_INVERT) return return line DEVICES = [ Device( r"uart\d", process_uart, { "tx": "UART_TX", "rx": "UART_RX", "rts": "UART_RTS", "cts": "UART_CTS", }, needs_sleep=True, ), Device( r"i2c\d", match_and_store_pin, { "sda": "TWIM_SDA", "scl": "TWIM_SCL", }, needs_sleep=True, ), Device( r"spi\d", process_spi, { "sck": "SPIM_SCK", "miso": "SPIM_MISO", "mosi": "SPIM_MOSI", }, needs_sleep=True, ), Device( r"pdm\d", match_and_store_pin, { "clk": "PDM_CLK", "din": "PDM_DIN", }, needs_sleep=False, ), Device( r"qdec", match_and_store_pin, { "a": "QDEC_A", "b": "QDEC_B", "led": "QDEC_LED", }, needs_sleep=True, ), Device( r"qspi", match_and_store_pin, { "sck": "QSPI_SCK", "io0": "QSPI_IO0", "io1": "QSPI_IO1", "io2": "QSPI_IO2", "io3": "QSPI_IO3", "csn": "QSPI_CSN", }, needs_sleep=True, ), Device( r"pwm\d", process_pwm, { "ch0": "PWM_OUT0", "ch1": "PWM_OUT1", "ch2": "PWM_OUT2", "ch3": "PWM_OUT3", }, needs_sleep=True, ), Device( r"i2s\d", match_and_store_pin, { "sck": "I2S_SCK_M", "lrck": "I2S_LRCK_M", "sdout": "I2S_SDOUT", "sdin": "I2S_SDIN", "mck": "I2S_MCK", }, needs_sleep=False, ), ] """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", allow_abbrev=False) 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)