You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
685 lines
18 KiB
685 lines
18 KiB
#!/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**. |
|
|
|
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 = <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" |
|
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 = <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 "\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}<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, 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)
|
|
|