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