1#!/usr/bin/env python3
2
3# Copyright (c) 2021 Nordic Semiconductor ASA
4# SPDX-License-Identifier: Apache-2.0
5
6"""
7Pinctrl Migration Utility Script for nRF Boards
8###############################################
9
10This script can be used to automatically migrate the Devicetree files of
11nRF-based boards using the old <signal>-pin properties to select peripheral
12pins. The script will parse a board Devicetree file and will first adjust that
13file by removing old pin-related properties replacing them with pinctrl states.
14A board-pinctrl.dtsi file will be generated containing the configuration for
15all pinctrl states. Note that script will also work on files that have been
16partially ported.
17
18.. warning::
19    This script uses a basic line based parser, therefore not all valid
20    Devicetree files will be converted correctly. **ADJUSTED/GENERATED FILES
21    MUST BE MANUALLY REVIEWED**.
22
23Known limitations: All SPI nodes will be assumed to be a master device.
24
25Usage::
26
27    python3 pinctrl_nrf_migrate.py
28        -i path/to/board.dts
29        [--no-backup]
30        [--skip-nrf-check]
31        [--header ""]
32
33Example:
34
35.. code-block:: devicetree
36
37    /* Old board.dts */
38    ...
39    &uart0 {
40        ...
41        tx-pin = <5>;
42        rx-pin = <33>;
43        rx-pull-up;
44        ...
45    };
46
47    /* Adjusted board.dts */
48    ...
49    #include "board-pinctrl.dtsi"
50    ...
51    &uart0 {
52        ...
53        pinctrl-0 = <&uart0_default>;
54        pinctrl-1 = <&uart0_sleep>;
55        pinctrl-names = "default", "sleep";
56        ...
57    };
58
59    /* Generated board-pinctrl.dtsi */
60    &pinctrl {
61        uart0_default: uart0_default {
62            group1 {
63                psels = <NRF_PSEL(UART_TX, 0, 5);
64            };
65            group2 {
66                psels = <NRF_PSEL(UART_RX, 1, 1)>;
67                bias-pull-up;
68            };
69        };
70
71        uart0_sleep: uart0_sleep {
72            group1 {
73                psels = <NRF_PSEL(UART_TX, 0, 5)>,
74                        <NRF_PSEL(UART_RX, 1, 1)>;
75                low-power-enable;
76            };
77        };
78    };
79"""
80
81import argparse
82import enum
83from pathlib import Path
84import re
85import shutil
86from typing import Callable, Optional, Dict, List
87
88
89#
90# Data types and containers
91#
92
93
94class PIN_CONFIG(enum.Enum):
95    """Pin configuration attributes"""
96
97    PULL_UP = "bias-pull-up"
98    PULL_DOWN = "bias-pull-down"
99    LOW_POWER = "low-power-enable"
100    NORDIC_INVERT = "nordic,invert"
101
102
103class Device(object):
104    """Device configuration class"""
105
106    def __init__(
107        self,
108        pattern: str,
109        callback: Callable,
110        signals: Dict[str, str],
111        needs_sleep: bool,
112    ) -> None:
113        self.pattern = pattern
114        self.callback = callback
115        self.signals = signals
116        self.needs_sleep = needs_sleep
117        self.attrs = {}
118
119
120class SignalMapping(object):
121    """Signal mapping (signal<>pin)"""
122
123    def __init__(self, signal: str, pin: int) -> None:
124        self.signal = signal
125        self.pin = pin
126
127
128class PinGroup(object):
129    """Pin group"""
130
131    def __init__(self, pins: List[SignalMapping], config: List[PIN_CONFIG]) -> None:
132        self.pins = pins
133        self.config = config
134
135
136class PinConfiguration(object):
137    """Pin configuration (mapping and configuration)"""
138
139    def __init__(self, mapping: SignalMapping, config: List[PIN_CONFIG]) -> None:
140        self.mapping = mapping
141        self.config = config
142
143
144class DeviceConfiguration(object):
145    """Device configuration"""
146
147    def __init__(self, name: str, pins: List[PinConfiguration]) -> None:
148        self.name = name
149        self.pins = pins
150
151    def add_signal_config(self, signal: str, config: PIN_CONFIG) -> None:
152        """Add configuration to signal"""
153        for pin in self.pins:
154            if signal == pin.mapping.signal:
155                pin.config.append(config)
156                return
157
158        self.pins.append(PinConfiguration(SignalMapping(signal, -1), [config]))
159
160    def set_signal_pin(self, signal: str, pin: int) -> None:
161        """Set signal pin"""
162        for pin_ in self.pins:
163            if signal == pin_.mapping.signal:
164                pin_.mapping.pin = pin
165                return
166
167        self.pins.append(PinConfiguration(SignalMapping(signal, pin), []))
168
169
170#
171# Content formatters and writers
172#
173
174
175def gen_pinctrl(
176    configs: List[DeviceConfiguration], input_file: Path, header: str
177) -> None:
178    """Generate board-pinctrl.dtsi file
179
180    Args:
181        configs: Board configs.
182        input_file: Board DTS file.
183    """
184
185    last_line = 0
186
187    pinctrl_file = input_file.parent / (input_file.stem + "-pinctrl.dtsi")
188    # append content before last node closing
189    if pinctrl_file.exists():
190        content = open(pinctrl_file).readlines()
191        for i, line in enumerate(content[::-1]):
192            if re.match(r"\s*};.*", line):
193                last_line = len(content) - (i + 1)
194                break
195
196    out = open(pinctrl_file, "w")
197
198    if not last_line:
199        out.write(header)
200        out.write("&pinctrl {\n")
201    else:
202        for line in content[:last_line]:
203            out.write(line)
204
205    for config in configs:
206        # create pin groups with common configuration (default state)
207        default_groups: List[PinGroup] = []
208        for pin in config.pins:
209            merged = False
210            for group in default_groups:
211                if group.config == pin.config:
212                    group.pins.append(pin.mapping)
213                    merged = True
214                    break
215            if not merged:
216                default_groups.append(PinGroup([pin.mapping], pin.config))
217
218        # create pin group for low power state
219        group = PinGroup([], [PIN_CONFIG.LOW_POWER])
220        for pin in config.pins:
221            group.pins.append(pin.mapping)
222        sleep_groups = [group]
223
224        # generate default and sleep state entries
225        out.write(f"\t{config.name}_default: {config.name}_default {{\n")
226        out.write(fmt_pinctrl_groups(default_groups))
227        out.write("\t};\n\n")
228
229        out.write(f"\t{config.name}_sleep: {config.name}_sleep {{\n")
230        out.write(fmt_pinctrl_groups(sleep_groups))
231        out.write("\t};\n\n")
232
233    if not last_line:
234        out.write("};\n")
235    else:
236        for line in content[last_line:]:
237            out.write(line)
238
239    out.close()
240
241
242def board_is_nrf(content: List[str]) -> bool:
243    """Check if board is nRF based.
244
245    Args:
246        content: DT file content as list of lines.
247
248    Returns:
249        True if board is nRF based, False otherwise.
250    """
251
252    for line in content:
253        m = re.match(r'^#include\s+(?:"|<).*nrf.*(?:>|").*', line)
254        if m:
255            return True
256
257    return False
258
259
260def fmt_pinctrl_groups(groups: List[PinGroup]) -> str:
261    """Format pinctrl groups.
262
263    Example generated content::
264
265        group1 {
266            psels = <NRF_PSEL(UART_TX, 0, 5)>;
267        };
268        group2 {
269            psels = <NRF_PSEL(UART_RX, 1, 1)>;
270            bias-pull-up;
271        };
272
273    Returns:
274        Generated groups.
275    """
276
277    content = ""
278
279    for i, group in enumerate(groups):
280        content += f"\t\tgroup{i + 1} {{\n"
281
282        # write psels entries
283        for i, mapping in enumerate(group.pins):
284            prefix = "psels = " if i == 0 else "\t"
285            suffix = ";" if i == len(group.pins) - 1 else ","
286            pin = mapping.pin
287            port = 0 if pin < 32 else 1
288            if port == 1:
289                pin -= 32
290            content += (
291                f"\t\t\t{prefix}<NRF_PSEL({mapping.signal}, {port}, {pin})>{suffix}\n"
292            )
293
294        # write all pin configuration (bias, low-power, etc.)
295        for entry in group.config:
296            content += f"\t\t\t{entry.value};\n"
297
298        content += "\t\t};\n"
299
300    return content
301
302
303def fmt_states(device: str, indent: str, needs_sleep: bool) -> str:
304    """Format state entries for the given device.
305
306    Args:
307        device: Device name.
308        indent: Indentation.
309        needs_sleep: If sleep entry is needed.
310
311    Returns:
312        State entries to be appended to the device.
313    """
314
315    if needs_sleep:
316        return "\n".join(
317            (
318                f"{indent}pinctrl-0 = <&{device}_default>;",
319                f"{indent}pinctrl-1 = <&{device}_sleep>;",
320                f'{indent}pinctrl-names = "default", "sleep";\n',
321            )
322        )
323    else:
324        return "\n".join(
325            (
326                f"{indent}pinctrl-0 = <&{device}_default>;",
327                f'{indent}pinctrl-names = "default";\n',
328            )
329        )
330
331
332def insert_pinctrl_include(content: List[str], board: str) -> None:
333    """Insert board pinctrl include if not present.
334
335    Args:
336        content: DT file content as list of lines.
337        board: Board name
338    """
339
340    already = False
341    include_last_line = -1
342    root_line = -1
343
344    for i, line in enumerate(content):
345        # check if file already includes a board pinctrl file
346        m = re.match(r'^#include\s+".*-pinctrl\.dtsi".*', line)
347        if m:
348            already = True
349            continue
350
351        # check if including
352        m = re.match(r'^#include\s+(?:"|<)(.*)(?:>|").*', line)
353        if m:
354            include_last_line = i
355            continue
356
357        # check for root entry
358        m = re.match(r"^\s*/\s*{.*", line)
359        if m:
360            root_line = i
361            break
362
363    if include_last_line < 0 and root_line < 0:
364        raise ValueError("Unexpected DT file content")
365
366    if not already:
367        if include_last_line >= 0:
368            line = include_last_line + 1
369        else:
370            line = max(0, root_line - 1)
371
372        content.insert(line, f'#include "{board}-pinctrl.dtsi"\n')
373
374
375def adjust_content(content: List[str], board: str) -> List[DeviceConfiguration]:
376    """Adjust content
377
378    Args:
379        content: File content to be adjusted.
380        board: Board name.
381    """
382
383    configs: List[DeviceConfiguration] = []
384    level = 0
385    in_device = False
386    states_written = False
387
388    new_content = []
389
390    for line in content:
391        # look for a device reference node (e.g. &uart0)
392        if not in_device:
393            m = re.match(r"^[^&]*&([a-z0-9]+)\s*{[^}]*$", line)
394            if m:
395                # check if device requires processing
396                current_device = None
397                for device in DEVICES:
398                    if re.match(device.pattern, m.group(1)):
399                        current_device = device
400                        indent = ""
401                        config = DeviceConfiguration(m.group(1), [])
402                        configs.append(config)
403                        break
404
405                # we are now inside a device node
406                level = 1
407                in_device = True
408                states_written = False
409        else:
410            # entering subnode (must come after all properties)
411            if re.match(r"[^\/\*]*{.*", line):
412                level += 1
413            # exiting subnode (or device node)
414            elif re.match(r"[^\/\*]*}.*", line):
415                level -= 1
416                in_device = level > 0
417            elif current_device:
418                # device already ported, drop
419                if re.match(r"[^\/\*]*pinctrl-\d+.*", line):
420                    current_device = None
421                    configs.pop()
422                # determine indentation
423                elif not indent:
424                    m = re.match(r"(\s+).*", line)
425                    if m:
426                        indent = m.group(1)
427
428            # process each device line, append states at the end
429            if current_device:
430                if level == 1:
431                    line = current_device.callback(config, current_device.signals, line)
432                if (level == 2 or not in_device) and not states_written:
433                    line = (
434                        fmt_states(config.name, indent, current_device.needs_sleep)
435                        + line
436                    )
437                    states_written = True
438                    current_device = None
439
440        if line:
441            new_content.append(line)
442
443    if configs:
444        insert_pinctrl_include(new_content, board)
445
446    content[:] = new_content
447
448    return configs
449
450
451#
452# Processing utilities
453#
454
455
456def match_and_store_pin(
457    config: DeviceConfiguration, signals: Dict[str, str], line: str
458) -> Optional[str]:
459    """Match and store a pin mapping.
460
461    Args:
462        config: Device configuration.
463        signals: Signals name mapping.
464        line: Line containing potential pin mapping.
465
466    Returns:
467        Line if found a pin mapping, None otherwise.
468    """
469
470    # handle qspi special case for io-pins (array case)
471    m = re.match(r"\s*io-pins\s*=\s*([\s<>,0-9]+).*", line)
472    if m:
473        pins = re.sub(r"[<>,]", "", m.group(1)).split()
474        for i, pin in enumerate(pins):
475            config.set_signal_pin(signals[f"io{i}"], int(pin))
476        return
477
478    m = re.match(r"\s*([a-z]+\d?)-pins?\s*=\s*<(\d+)>.*", line)
479    if m:
480        config.set_signal_pin(signals[m.group(1)], int(m.group(2)))
481        return
482
483    return line
484
485
486#
487# Device processing callbacks
488#
489
490
491def process_uart(config: DeviceConfiguration, signals, line: str) -> Optional[str]:
492    """Process UART/UARTE devices."""
493
494    # check if line specifies a pin
495    if not match_and_store_pin(config, signals, line):
496        return
497
498    # check if pull-up is specified
499    m = re.match(r"\s*([a-z]+)-pull-up.*", line)
500    if m:
501        config.add_signal_config(signals[m.group(1)], PIN_CONFIG.PULL_UP)
502        return
503
504    return line
505
506
507def process_spi(config: DeviceConfiguration, signals, line: str) -> Optional[str]:
508    """Process SPI devices."""
509
510    # check if line specifies a pin
511    if not match_and_store_pin(config, signals, line):
512        return
513
514    # check if pull-up is specified
515    m = re.match(r"\s*miso-pull-up.*", line)
516    if m:
517        config.add_signal_config(signals["miso"], PIN_CONFIG.PULL_UP)
518        return
519
520    # check if pull-down is specified
521    m = re.match(r"\s*miso-pull-down.*", line)
522    if m:
523        config.add_signal_config(signals["miso"], PIN_CONFIG.PULL_DOWN)
524        return
525
526    return line
527
528
529def process_pwm(config: DeviceConfiguration, signals, line: str) -> Optional[str]:
530    """Process PWM devices."""
531
532    # check if line specifies a pin
533    if not match_and_store_pin(config, signals, line):
534        return
535
536    # check if channel inversion is specified
537    m = re.match(r"\s*([a-z0-9]+)-inverted.*", line)
538    if m:
539        config.add_signal_config(signals[m.group(1)], PIN_CONFIG.NORDIC_INVERT)
540        return
541
542    return line
543
544
545DEVICES = [
546    Device(
547        r"uart\d",
548        process_uart,
549        {
550            "tx": "UART_TX",
551            "rx": "UART_RX",
552            "rts": "UART_RTS",
553            "cts": "UART_CTS",
554        },
555        needs_sleep=True,
556    ),
557    Device(
558        r"i2c\d",
559        match_and_store_pin,
560        {
561            "sda": "TWIM_SDA",
562            "scl": "TWIM_SCL",
563        },
564        needs_sleep=True,
565    ),
566    Device(
567        r"spi\d",
568        process_spi,
569        {
570            "sck": "SPIM_SCK",
571            "miso": "SPIM_MISO",
572            "mosi": "SPIM_MOSI",
573        },
574        needs_sleep=True,
575    ),
576    Device(
577        r"pdm\d",
578        match_and_store_pin,
579        {
580            "clk": "PDM_CLK",
581            "din": "PDM_DIN",
582        },
583        needs_sleep=False,
584    ),
585    Device(
586        r"qdec",
587        match_and_store_pin,
588        {
589            "a": "QDEC_A",
590            "b": "QDEC_B",
591            "led": "QDEC_LED",
592        },
593        needs_sleep=True,
594    ),
595    Device(
596        r"qspi",
597        match_and_store_pin,
598        {
599            "sck": "QSPI_SCK",
600            "io0": "QSPI_IO0",
601            "io1": "QSPI_IO1",
602            "io2": "QSPI_IO2",
603            "io3": "QSPI_IO3",
604            "csn": "QSPI_CSN",
605        },
606        needs_sleep=True,
607    ),
608    Device(
609        r"pwm\d",
610        process_pwm,
611        {
612            "ch0": "PWM_OUT0",
613            "ch1": "PWM_OUT1",
614            "ch2": "PWM_OUT2",
615            "ch3": "PWM_OUT3",
616        },
617        needs_sleep=True,
618    ),
619    Device(
620        r"i2s\d",
621        match_and_store_pin,
622        {
623            "sck": "I2S_SCK_M",
624            "lrck": "I2S_LRCK_M",
625            "sdout": "I2S_SDOUT",
626            "sdin": "I2S_SDIN",
627            "mck": "I2S_MCK",
628        },
629        needs_sleep=False,
630    ),
631]
632"""Supported devices and associated configuration"""
633
634
635def main(input_file: Path, no_backup: bool, skip_nrf_check: bool, header: str) -> None:
636    """Entry point
637
638    Args:
639        input_file: Input DTS file.
640        no_backup: Do not create backup files.
641    """
642
643    board_name = input_file.stem
644    content = open(input_file).readlines()
645
646    if not skip_nrf_check and not board_is_nrf(content):
647        print(f"Board {board_name} is not nRF based, terminating")
648        return
649
650    if not no_backup:
651        backup_file = input_file.parent / (board_name + ".bck" + input_file.suffix)
652        shutil.copy(input_file, backup_file)
653
654    configs = adjust_content(content, board_name)
655
656    if configs:
657        with open(input_file, "w") as f:
658            f.writelines(content)
659
660        gen_pinctrl(configs, input_file, header)
661
662        print(f"Board {board_name} Devicetree file has been converted")
663    else:
664        print(f"Nothing to be converted for {board_name}")
665
666
667if __name__ == "__main__":
668    parser = argparse.ArgumentParser("pinctrl migration utility for nRF", allow_abbrev=False)
669    parser.add_argument(
670        "-i", "--input", type=Path, required=True, help="Board DTS file"
671    )
672    parser.add_argument(
673        "--no-backup", action="store_true", help="Do not create backup files"
674    )
675    parser.add_argument(
676        "--skip-nrf-check",
677        action="store_true",
678        help="Skip checking if board is nRF-based",
679    )
680    parser.add_argument(
681        "--header", default="", type=str, help="Header to be prepended to pinctrl files"
682    )
683    args = parser.parse_args()
684
685    main(args.input, args.no_backup, args.skip_nrf_check, args.header)
686