1#!/usr/bin/env python
2
3"""
4Copyright (c) 2024 Silicon Laboratories Inc.
5
6SPDX-License-Identifier: Apache-2.0
7"""
8
9import argparse
10import cmsis_svd
11import datetime
12import lxml
13import re
14import shutil
15import tempfile
16import urllib.request
17import zipfile
18
19from pathlib import Path
20
21import cmsis_svd.parser
22
23PIN_TOOL_URL = "https://github.com/SiliconLabs/simplicity_sdk/releases/download/v2024.6.2/pintool.zip"
24CMSIS_PACK_URL = "https://www.silabs.com/documents/public/cmsis-packs/SiliconLabs.GeckoPlatform_FAMILY_DFP.2024.6.0.pack"
25
26# Families to parse to produce generic pinout header
27FAMILIES = {
28  "xg21": ["efr32mg21", "efr32bg21", "mgm21", "bgm21"],
29  "xg22": ["efr32mg22", "efr32bg22", "efr32fg22", "mgm22", "bgm22", "efm32pg22"],
30  "xg23": ["efr32fg23", "efr32sg23", "efr32zg23", "zgm23", "efm32pg23"], # "fgm23",
31  "xg24": ["efr32mg24", "efr32bg24", "mgm24", "bgm24"],
32  "xg25": ["efr32fg25"],
33  "xg26": ["efr32mg26", "efr32bg26"],
34  "xg27": ["efr32mg27", "efr32bg27"],
35  "xg28": ["efr32fg28", "efr32sg28", "efr32zg28", "efm32pg28"],
36  "xg29": ["efr32bg29"],
37}
38ABUSES = {
39  "xg21": "platform/Device/SiliconLabs/EFR32MG21/Include/efr32mg21_gpio.h",
40  "xg22": "platform/Device/SiliconLabs/EFR32BG22/Include/efr32bg22_gpio.h",
41  "xg23": "platform/Device/SiliconLabs/EFR32FG23/Include/efr32fg23_gpio.h",
42  "xg24": "platform/Device/SiliconLabs/EFR32MG24/Include/efr32mg24_gpio.h",
43  "xg25": "platform/Device/SiliconLabs/EFR32FG25/Include/efr32fg25_gpio.h",
44  "xg26": "platform/Device/SiliconLabs/EFR32MG26/Include/efr32mg26_gpio.h",
45  "xg27": "platform/Device/SiliconLabs/EFR32BG27/Include/efr32bg27_gpio.h",
46  "xg28": "platform/Device/SiliconLabs/EFR32FG28/Include/efr32fg28_gpio.h",
47  "xg29": "platform/Device/SiliconLabs/EFR32BG29/Include/efr32bg29_gpio.h",
48}
49
50# Certain peripherals have different names in SVD and Pin Tool data; rename the SVD peripheral
51PERIPHERAL_ALIAS = {
52  "FRC": "PTI",
53  "LETIMER": "LETIMER0",
54  "SYXO0": "HFXO0",
55}
56
57# Certain signals have different names in SVD and Pin Tool data; rename the SVD signal
58SIGNAL_ALIAS = {
59  "CCC0": "CDTI0",
60  "CCC1": "CDTI1",
61  "CCC2": "CDTI2",
62  "CCC3": "CDTI3",
63  "USART1::SCLK": "CLK",
64}
65
66# Certain signals have different names in SVD and Pin Tool data; rename the Pin Tool signal
67PT_SIGNAL_ALIAS = {
68  "ACMPOUT": "DIGOUT",
69  "COLOUT0": "COL_OUT_0",
70  "COLOUT1": "COL_OUT_1",
71  "COLOUT2": "COL_OUT_2",
72  "COLOUT3": "COL_OUT_3",
73  "COLOUT4": "COL_OUT_4",
74  "COLOUT5": "COL_OUT_5",
75  "COLOUT6": "COL_OUT_6",
76  "COLOUT7": "COL_OUT_7",
77  "ROWSENSE0": "ROW_SENSE_0",
78  "ROWSENSE1": "ROW_SENSE_1",
79  "ROWSENSE2": "ROW_SENSE_2",
80  "ROWSENSE3": "ROW_SENSE_3",
81  "ROWSENSE4": "ROW_SENSE_4",
82  "ROWSENSE5": "ROW_SENSE_5",
83  "ANTROLLOVER": "ANT_ROLL_OVER",
84  "ANTRR0": "ANT_RR0",
85  "ANTRR1": "ANT_RR1",
86  "ANTRR2": "ANT_RR2",
87  "ANTRR3": "ANT_RR3",
88  "ANTRR4": "ANT_RR4",
89  "ANTRR5": "ANT_RR5",
90  "ANTSWEN": "ANT_SW_EN",
91  "ANTSWUS": "ANT_SW_US",
92  "ANTTRIG": "ANT_TRIG",
93  "ANTTRIGSTOP": "ANT_TRIG_STOP",
94  "BUFOUTREQINASYNC": "BUFOUT_REQ_IN_ASYNC",
95  "USBVBUSSENSE": "USB_VBUS_SENSE",
96}
97
98# Expected offset of DBGROUTEPEN register across all of Series 2.
99# Used as base address of pinctrl device tree node.
100PINCTRL_GPIO_OFFSET = 1088
101
102
103class Peripheral:
104  def __init__(self, name, offset):
105    self.name = name
106    self.offset = offset
107    self.signals = []
108
109  def max_signal_len(self):
110    return max(len(s.name) for s in self.signals)
111
112  def set_signal_enable(self, name, bit):
113    for signal in self.signals:
114      if signal.name == name:
115        break
116    else:
117      signal = Signal(name, self)
118      self.signals.append(signal)
119
120    signal.have_enable = True
121    signal.enable = bit
122
123  def set_signal_route(self, name, offset):
124    for signal in self.signals:
125      if signal.name == name:
126        break
127    else:
128      signal = Signal(name, self)
129      self.signals.append(signal)
130
131    signal.route = offset - self.offset
132
133
134class Signal:
135  def __init__(self, name, peripheral):
136    self.peripheral = peripheral
137    self.name = name
138    self.route = None
139    self.have_enable = False
140    self.enable = 0
141    self.pinout = {}
142
143  def display_name(self):
144    return f"{self.peripheral.name}_{self.name}"
145
146
147def download_pin_tool_data(path: Path) -> None:
148  """
149  Download Pin Tool zip file from SiSDK release artifact
150  """
151  dst = path / "pin_tool"
152  if dst.exists():
153    print("Skipping download of Pin Tool data, already exists")
154    return
155  print("Downloading Pin Tool data")
156  with urllib.request.urlopen(PIN_TOOL_URL) as response:
157    with tempfile.NamedTemporaryFile() as tmp_file:
158      shutil.copyfileobj(response, tmp_file)
159
160      with zipfile.ZipFile(tmp_file, 'r') as zip:
161        zip.extractall(dst)
162
163
164def download_cmsis_pack(path: Path, family: str) -> None:
165  """
166  Download CMSIS Pack containing SVD files for a given family
167  """
168  dst = path / "pack" / family
169  if dst.exists():
170    print(f"Skipping download of CMSIS Pack for {family}, already exists")
171    return
172  print(f"Downloading CMSIS Pack for {family}")
173  with urllib.request.urlopen(CMSIS_PACK_URL.replace("FAMILY", family.upper())) as response:
174    with tempfile.NamedTemporaryFile() as tmp_file:
175      shutil.copyfileobj(response, tmp_file)
176
177      with zipfile.ZipFile(tmp_file, 'r') as zip:
178        zip.extractall(dst)
179
180
181def parse_svd(peripherals, path: Path, family: str) -> None:
182  for svd_path in (path / "pack" / family / "SVD" / family.upper()).glob("*.svd"):
183    print(f"Parsing SVD for {svd_path.stem}")
184    parser = cmsis_svd.parser.SVDParser.for_xml_file(svd_path)
185    gpio: cmsis_svd.parser.SVDPeripheral = next(filter(lambda p: p.name == "GPIO_NS", parser.get_device().peripherals))
186    for reg in gpio.registers:
187      if reg.name == "DBGROUTEPEN":
188        assert PINCTRL_GPIO_OFFSET == reg.address_offset
189
190      reg_offset_word = (reg.address_offset - PINCTRL_GPIO_OFFSET) // 4
191
192      if reg.name.endswith("_ROUTEEN"):
193        peripheral = reg.name[:-8]
194        peripheral = PERIPHERAL_ALIAS.get(peripheral, peripheral)
195        if peripheral not in peripherals:
196          peripherals[peripheral] = Peripheral(peripheral, reg_offset_word)
197
198        for field in reg.fields:
199          if field.name.endswith("PEN"):
200            signal = field.name[:-3]
201            signal = SIGNAL_ALIAS.get(signal, signal)
202            signal = SIGNAL_ALIAS.get(f"{peripheral}::{signal}", signal)
203            peripherals[peripheral].set_signal_enable(signal, field.bit_offset)
204
205      if reg.name.endswith("ROUTE"):
206        peripheral, signal = reg.name.split("_", 1)
207        peripheral = PERIPHERAL_ALIAS.get(peripheral, peripheral)
208        signal = signal[:-5]
209        signal = SIGNAL_ALIAS.get(signal, signal)
210        signal = SIGNAL_ALIAS.get(f"{peripheral}::{signal}", signal)
211
212        if peripheral not in peripherals:
213          peripherals[peripheral] = Peripheral(peripheral, reg_offset_word)
214
215        peripherals[peripheral].set_signal_route(signal, reg_offset_word)
216
217
218def parse_pin_tool(peripherals, path: Path, family: str):
219  for pin_tool in (path / "pin_tool" / "platform" / "hwconf_data" / "pin_tool" / family).glob("*/PORTIO.portio"):
220    print(f"Parsing Pin Tool for {pin_tool.parent.stem}")
221    with open(pin_tool, 'r') as f:
222      tree = lxml.etree.parse(f)
223
224    for peripheral in peripherals.values():
225      for signal in peripheral.signals:
226        pt_signal = PT_SIGNAL_ALIAS.get(signal.name, signal.name)
227
228        if peripheral.name == "PRS0":
229          pt_peripheral = f"PRS.{signal.name}"
230          pt_signal_prefix = "PRS"
231        else:
232          pt_peripheral = peripheral.name
233          pt_signal_prefix = peripheral.name
234
235        for node in tree.getroot().xpath(f'portIo/pinRoutes/module[@name="{pt_peripheral}"]/selector[@name="{pt_signal_prefix}_{pt_signal}"]'):
236          for loc in node.xpath(f'route[@name="{pt_signal}"]/location'):
237            port = int(loc.attrib["portBankIndex"])
238            pin = int(loc.attrib["pinIndex"])
239            if port not in signal.pinout:
240              signal.pinout[port] = set()
241            signal.pinout[port].add(pin)
242
243          break
244        else:
245          print(f"WARN: No Pin Tool match for {signal.display_name()} for {pin_tool.parent.stem}")
246
247
248def write_header(path: Path, family, peripherals: dict, abuses: list) -> None:
249  """
250  Write DT binding header containing DBUS routing data for pinctrl use
251  """
252  lines = [
253    "/*",
254    f" * Copyright (c) {datetime.date.today().year} Silicon Laboratories Inc.",
255    " * SPDX-License-Identifier: Apache-2.0",
256    " *",
257    f" * Pin Control for Silicon Labs {family.upper()} devices",
258    " *",
259    f" * This file was generated by the script {Path(__file__).name} in the hal_silabs module.",
260    " * Do not manually edit.",
261    " */",
262    "",
263    f"#ifndef ZEPHYR_DT_BINDINGS_PINCTRL_SILABS_{family.upper()}_PINCTRL_H_",
264    f"#define ZEPHYR_DT_BINDINGS_PINCTRL_SILABS_{family.upper()}_PINCTRL_H_",
265    "",
266    "#include <dt-bindings/pinctrl/silabs-pinctrl-dbus.h>",
267    "",
268  ]
269
270  # Emit generic peripheral macros
271  for peripheral in peripherals.values():
272    have_content = False
273    for signal in peripheral.signals:
274      if signal.route is not None:
275        pad = peripheral.max_signal_len() - len(signal.name) + 1
276        lines.append(f"#define SILABS_DBUS_{signal.display_name()}(port, pin){' ' * pad}"
277                     f"SILABS_DBUS(port, pin, {peripheral.offset}, {int(signal.have_enable)}, "
278                     f"{signal.enable}, {signal.route})")
279        have_content = True
280      else:
281        print(f"WARN: No route register for {signal.display_name()}")
282    if have_content:
283      lines.append("")
284
285  # Emit pin-specific macros using peripheral macros
286  for peripheral in peripherals.values():
287    have_content = False
288    for signal in peripheral.signals:
289      for port, pins in signal.pinout.items():
290        for pin in sorted(pins):
291          pad = peripheral.max_signal_len() - len(signal.name) + 1
292          lines.append(f"#define {signal.display_name()}_P{chr(65 + port)}{pin}{' ' * pad}"
293                       f"SILABS_DBUS_{signal.display_name()}(0x{port:x}, 0x{pin:x})")
294          have_content = True
295    if have_content:
296      lines.append("")
297
298  # Emit analog buses
299  max_len = 0
300  for abus in abuses:
301    curr_len = len(abus["bus_name"]) + len(abus["peripheral"])
302    if curr_len > max_len:
303      max_len = curr_len
304  for abus in abuses:
305    curr_len = len(abus["bus_name"]) + len(abus["peripheral"])
306    lines.append(f"#define ABUS_{abus["bus_name"]}_{abus["peripheral"]}{' ' * (max_len - curr_len + 1)}"
307                  f"SILABS_ABUS(0x{abus["base_offset"]:x}, 0x{abus["parity"]:x}, 0x{abus["value"]:x})")
308  lines.append("")
309
310  lines.append(f"#endif /* ZEPHYR_DT_BINDINGS_PINCTRL_SILABS_{family.upper()}_PINCTRL_H_ */")
311  lines.append("")
312  path.mkdir(parents=True, exist_ok=True)
313  (path / f"{family}-pinctrl.h").write_text("\n".join(lines))
314
315def parse_abus(file: Path) -> list:
316  offset_map = {
317    "EVEN0": 0,
318    "EVEN1": 1,
319    "ODD0": 2,
320    "ODD1": 3,
321  }
322  peripheral_map = {
323    "ADC0": "IADC0",
324  }
325  abuses = []
326  with file.open() as f:
327    for line in f:
328      if m := re.match(r"#define _GPIO_([A-Z])[A-Z]?BUSALLOC_([A-Z]+(EVEN\d|ODD\d))_([^\s]+)\s+0x(.+)UL", line):
329        if m.group(4) not in ["DEFAULT", "TRISTATE", "MASK"]:
330          abuses.append({
331            "base_offset": ord(m.group(1)) - 65,
332            "bus_name": m.group(2),
333            "parity": offset_map[m.group(3)],
334            "peripheral": peripheral_map.get(m.group(4), m.group(4)),
335            "value": int(m.group(5), base=16),
336          })
337
338  return abuses
339
340if __name__ == "__main__":
341  parser = argparse.ArgumentParser(description="Generate headers for Pinctrl for Series 2 devices. "
342                                   "The headers are used from DeviceTree, and represent every "
343                                   "allowed pin selection for every digital bus signal as a DT "
344                                   "compatible macro.")
345  parser.add_argument("--workdir", "-w", default=Path(__file__).parent.absolute() / "cache",
346                      type=Path, help="Working directory to store downloaded Pin Tool and "
347                      "CMSIS-Pack artifacts.")
348  parser.add_argument("--sdk", "-s", default=Path(__file__).parent.parent.absolute() / "simplicity_sdk",
349                      type=Path, help="SDK directory.")
350  parser.add_argument("--out", "-o", default=(Path(__file__).parent.absolute() / "out"), type=Path,
351                      help="Output directory for generated bindings. Defaults to the directory "
352                      "./out relative to the script. Set to $ZEPHYR_BASE/include/zephyr/"
353                      "dt-bindings/pinctrl/silabs/ to directly generate output into the expected "
354                      "location within the Zephyr main tree.")
355  parser.add_argument("--family", "-f", default="xg24", choices=FAMILIES.keys(),
356                      help="Device family to generate pinctrl bindings for. Defaults to xg24 if "
357                      "not set.")
358  args = parser.parse_args()
359
360  download_pin_tool_data(args.workdir)
361
362  peripherals = {}
363
364  for family in FAMILIES[args.family]:
365    download_cmsis_pack(args.workdir, family)
366    # Find DBUS register offsets for all peripheral signals from SVD
367    parse_svd(peripherals, args.workdir, family)
368    # Add available pins for all peripheral signals from Pin Tool data
369    parse_pin_tool(peripherals, args.workdir, family)
370
371  abuses = parse_abus(args.sdk / ABUSES[args.family])
372
373  write_header(args.out, args.family, peripherals, abuses)
374