blob: ef1878a4221f4379cfdd7e92ef9e4f9f8573ef73 [file] [log] [blame]
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +02001#!/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
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +010023Known limitations: All SPI nodes will be assumed to be a master device.
24
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +020025Usage::
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"
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100100 NORDIC_INVERT = "nordic,invert"
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200101
102
103class Device(object):
104 """Device configuration class"""
105
106 def __init__(
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100107 self,
108 pattern: str,
109 callback: Callable,
110 signals: Dict[str, str],
111 needs_sleep: bool,
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200112 ) -> None:
113 self.pattern = pattern
114 self.callback = callback
115 self.signals = signals
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100116 self.needs_sleep = needs_sleep
117 self.attrs = {}
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200118
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):
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100284 prefix = "psels = " if i == 0 else "\t"
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200285 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
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100303def fmt_states(device: str, indent: str, needs_sleep: bool) -> str:
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200304 """Format state entries for the given device.
305
306 Args:
307 device: Device name.
Nazar Kazakovf483b1b2022-03-16 21:07:43 +0000308 indent: Indentation.
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100309 needs_sleep: If sleep entry is needed.
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200310
311 Returns:
312 State entries to be appended to the device.
313 """
314
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100315 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 )
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200322 )
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100323 else:
324 return "\n".join(
325 (
326 f"{indent}pinctrl-0 = <&{device}_default>;",
327 f'{indent}pinctrl-names = "default";\n',
328 )
329 )
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200330
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
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100386 states_written = False
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200387
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
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200406 level = 1
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100407 in_device = True
408 states_written = False
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200409 else:
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100410 # entering subnode (must come after all properties)
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200411 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:
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100430 if level == 1:
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200431 line = current_device.callback(config, current_device.signals, line)
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100432 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
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200438 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
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100458) -> Optional[str]:
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200459 """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:
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100467 Line if found a pin mapping, None otherwise.
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200468 """
469
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100470 # 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)
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200479 if m:
480 config.set_signal_pin(signals[m.group(1)], int(m.group(2)))
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100481 return
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200482
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100483 return line
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200484
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
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100495 if not match_and_store_pin(config, signals, line):
496 return
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200497
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)
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100502 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
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200541
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 },
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100555 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,
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200630 ),
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__":
Jamie McCraeec704442023-01-04 16:08:36 +0000668 parser = argparse.ArgumentParser("pinctrl migration utility for nRF", allow_abbrev=False)
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200669 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(
Gerard Marull-Paretas8b91cb02022-03-16 16:04:49 +0100681 "--header", default="", type=str, help="Header to be prepended to pinctrl files"
Gerard Marull-Paretasd4770012021-09-30 13:10:24 +0200682 )
683 args = parser.parse_args()
684
685 main(args.input, args.no_backup, args.skip_nrf_check, args.header)