modm_data.nrfx

Nordic Semiconductor nrfx MDK

Parses the linker scripts and CMSIS-SVD files of the nRF51, nRF52, and nRF53 devices from the nrfx MDK. The package pinouts are extracted from the pin assignment chapters of the product specifications and stored as JSON, since the Nordic documentation cannot be downloaded automatically. See modm_data.nrfx.pinout for how to update the pinout data.

 1# Copyright 2026, Niklas Hauser
 2# SPDX-License-Identifier: MPL-2.0
 3
 4"""
 5# Nordic Semiconductor nrfx MDK
 6
 7Parses the linker scripts and CMSIS-SVD files of the nRF51, nRF52, and nRF53
 8devices from the nrfx MDK. The package pinouts are extracted from the pin
 9assignment chapters of the product specifications and stored as JSON, since
10the Nordic documentation cannot be downloaded automatically. See
11`modm_data.nrfx.pinout` for how to update the pinout data.
12"""
13
14from .device_data import device_files, device_from_file, did_from_string
15from .pinout import pinout, pinout_from_html, write_pinout_json
16
17__all__ = [
18    "device_files",
19    "device_from_file",
20    "did_from_string",
21    "pinout",
22    "pinout_from_html",
23    "write_pinout_json",
24]
def device_files(prefix: str) -> list[pathlib.Path]:
40def device_files(prefix: str) -> list[Path]:
41    """
42    :param prefix: A device prefix, for example, `nrf52`.
43    :return: A sorted list of linker scripts of the devices in the nrfx MDK.
44    """
45    return sorted(_MDK_PATH.rglob(f"{prefix.lower()}[0-9]*_*.ld"), key=lambda p: p.name)
Parameters
  • prefix: A device prefix, for example, nrf52.
Returns

A sorted list of linker scripts of the devices in the nrfx MDK.

def device_from_file(ld_path: pathlib.Path) -> dict:
 48def device_from_file(ld_path: Path) -> dict:
 49    """
 50    Extracts the device data from the nrfx MDK linker script and SVD file.
 51
 52    :param ld_path: Path to the linker script of the device.
 53    :return: A dictionary of device properties.
 54    """
 55    ld_path = Path(ld_path)
 56    p = {"id": (did := did_from_string(ld_path.stem.replace("_", "-")))}
 57
 58    svd_path = ld_path.with_name(re.sub(r"\_\w{4}(\_\w+)?.ld", r"\1.svd", ld_path.name))
 59    if not svd_path.exists():
 60        fallback = {"51": "nrf51.svd", "52": "nrf52.svd"}.get(did.family)
 61        for candidate in [ld_path.with_name(fallback), ld_path.parent.with_name(fallback)] if fallback else []:
 62            if candidate.exists():
 63                svd_path = candidate
 64                break
 65    device_file = XmlReader(svd_path)
 66    LOGGER.info("Parsing '%s'", did.string)
 67
 68    # information about the core and architecture
 69    core = device_file.query("//device/cpu/name")[0].text.lower().replace("cm", "cortex-m")
 70    if device_file.query("//device/cpu/fpuPresent")[0].text in ("1", "true"):
 71        p["fpu"] = "fpv4-sp-d16" if "m4" in core else "fpv5-sp-d16"
 72    p["core"] = core
 73    p["revision"] = device_file.query("//device/cpu/revision")[0].text
 74
 75    # find the values for flash and ram
 76    memories = []
 77    if did.family == "53":
 78        if did.core == "app":
 79            memories = [{"name": "flash", "access": "rx", "size": str(1024 * 1024), "start": "0x00000000"}]
 80            for idx in range(8):
 81                start = hex(0x20000000 + idx * 64 * 1024)
 82                memories.append({"name": f"ram{idx}", "access": "rwx", "size": str(64 * 1024), "start": start})
 83        else:
 84            memories = [{"name": "flash", "access": "rx", "size": str(256 * 1024), "start": "0x01000000"}]
 85            for idx in range(4):
 86                start = hex(0x21000000 + idx * 16 * 1024)
 87                memories.append({"name": f"ram{idx}", "access": "rwx", "size": str(16 * 1024), "start": start})
 88    else:
 89        memory = re.search(r"MEMORY\s*\{(.*?)\}", ld_path.read_text(), flags=re.DOTALL).group(1)
 90        pattern = (
 91            r"  (?P<name>\w+) \((?P<access>\w+)\) : ORIGIN = (?P<start>0x[\da-fA-F]+), LENGTH = (?P<size>0x[\da-fA-F]+)"
 92        )
 93        for match in re.finditer(pattern, memory):
 94            name = match.group("name").lower()
 95            if "ext" in name:
 96                continue
 97            memories.append(
 98                {
 99                    "name": name.replace("code_ram", "code"),
100                    "access": match.group("access").lower(),
101                    "size": str(int(match.group("size").lower(), 16)),
102                    "start": match.group("start").lower(),
103                }
104            )
105    p["memories"] = memories
106
107    # Signals
108    signals = {}
109    for s in device_file.query("//peripherals/peripheral/registers/cluster"):
110        if s.find("name").text == "PSEL":
111            instance = s.getparent().getparent().find("name").text.lower().split("_")[0]
112            signals[instance] = [element.text.lower() for element in s.findall("register/name")]
113
114    # nRF51 and older SVD files may use PSEL* registers directly instead of a PSEL cluster
115    for peripheral in device_file.query("//peripherals/peripheral"):
116        instance = peripheral.find("name").text.lower().split("_")[0]
117        for register_name in peripheral.findall("registers/register/name"):
118            if register_name.text is None or not register_name.text.startswith("PSEL"):
119                continue
120            if (signal_name := register_name.text[4:].lower()) == "":
121                continue
122            signals.setdefault(instance, [])
123            if signal_name not in signals[instance]:
124                signals[instance].append(signal_name)
125
126    # drivers and gpios
127    modules = []
128    ports = {}
129    gpios = []
130    fixed_signals = {}
131    for m in device_file.query("//peripherals/peripheral"):
132        modulename = m.find("name").text
133        if modulename.endswith("_S"):
134            continue
135        modulename = modulename.split("_")[0]
136
137        if "GPIO Port" in m.find("description").text or modulename == "GPIO":
138            # omit the leading P of the port names, also of the derived ports
139            portnumber = "0" if modulename == "GPIO" else modulename[1:]
140            if m.get("derivedFrom") is not None:
141                portsize = ports[m.get("derivedFrom")[1:].split("_")[0]]
142            else:
143                portsize = int(m.find("size").text, base=0)
144            ports[portnumber] = portsize
145            gpios.extend((portnumber, str(i)) for i in range(portsize))
146            continue
147
148        module = re.search(r"(?P<module>.*\D)(?P<instance>\d*$)", modulename).group("module").lower()
149        modules.append((module, modulename.lower()))
150
151        # copy available signals to all derived peripherals
152        if m.get("derivedFrom") is not None and m.get("derivedFrom").lower() in signals:
153            signals[modulename.lower()] = signals[m.get("derivedFrom").lower()]
154
155        # extract fixed analog channel capabilities from enum descriptions (AIN0..AINx)
156        for field in m.findall("registers//field"):
157            field_name = field.find("name")
158            if (
159                field_name is None
160                or field_name.text is None
161                or field_name.text.upper() not in ("PSEL", "PSELP", "PSELN")
162            ):
163                continue
164            for enum_value in field.findall("enumeratedValues/enumeratedValue"):
165                enum_name = enum_value.find("name").text if enum_value.find("name") is not None else ""
166                enum_desc = enum_value.find("description").text if enum_value.find("description") is not None else ""
167                for token in (enum_name, enum_desc):
168                    if token is not None and (match := re.search(r"AIN(?P<index>\d+)", token.upper())):
169                        fixed_signals.setdefault(module, set()).add(f"ain{match.group('index')}")
170
171    p["modules"] = sorted(list(set(modules)))
172    p["gpios"] = gpios
173    p["fixed_signals"] = {module: sorted(names) for module, names in fixed_signals.items()}
174    p["signals"] = []
175    for instance, names in signals.items():
176        driver = re.search(r"(?P<module>.*\D)(?P<instance>\d*$)", instance).group("module").lower()
177        # TODO take care of multichannel signals like OUT[%s] of PWM peripheral
178        p["signals"].extend({"driver": driver, "instance": instance, "name": n} for n in names if "[%s]" not in n)
179
180    pin_data = pinout(f"nrf{did.family}{did.series}")
181    p["pin_packages"] = pin_data["packages"]
182    p["pin_specials"] = pin_data["specials"]
183
184    # Unique interrupts
185    p["interrupts"] = []
186    for i in device_file.query("//peripherals/peripheral/interrupt"):
187        interrupt = {"position": i.find("value").text, "name": i.find("name").text}
188        if interrupt not in p["interrupts"]:
189            p["interrupts"].append(interrupt)
190    return p

Extracts the device data from the nrfx MDK linker script and SVD file.

Parameters
  • ld_path: Path to the linker script of the device.
Returns

A dictionary of device properties.

def did_from_string(string: str) -> modm_data.kg.DeviceIdentifier:
17def did_from_string(string: str) -> DeviceIdentifier:
18    """
19    Parses NRF device strings, for example, `nrf52840-qiaa`, organized as
20    `{platform}{family}{series}-{package}{function}` and optionally `@{core}`.
21    """
22    string = string.lower()
23    match_string = r"nrf(?P<family>[0-9]{2})(?P<series>[0-9]{2,3})-(?P<package>\w{2})(?P<function>\w{2})"
24    if "nrf53" in string:
25        match_string += r"-(?P<core>\w+)"
26    if string.startswith("nrf") and (match := re.search(match_string, string)):
27        i = DeviceIdentifier("{platform}{family}{series}-{package}{function}")
28        i.set("platform", "nrf")
29        i.set("family", match.group("family").lower())
30        i.set("series", match.group("series").lower())
31        i.set("package", match.group("package").lower())
32        i.set("function", match.group("function").lower())
33        if "nrf53" in string:
34            i.naming_schema += "@{core}"
35            i.set("core", match.group("core").lower()[:3])
36        return i
37    raise ValueError(f"Unknown identifier '{string}'!")

Parses NRF device strings, for example, nrf52840-qiaa, organized as {platform}{family}{series}-{package}{function} and optionally @{core}.

def pinout(product: str) -> dict:
76def pinout(product: str) -> dict:
77    """
78    Returns the package pinouts and the special pin functions of a product.
79    The data is extracted from the pin assignment chapter of the product
80    specification HTML and stored as JSON in this package.
81
82    :param product: The product name, for example, `nrf52840`.
83    :return: Dictionary with `packages` list (each with `name`, `pins`, and `codes`)
84             and `specials` mapping of `port.pin` to a sorted list of tags.
85    """
86    data = _pinout_data().get(product, {"packages": [], "specials": {}})
87    codes = package_code_map.get(product, {})
88    packages = [package | {"codes": codes.get(package["name"], [])} for package in data["packages"]]
89    return {"packages": packages, "specials": data["specials"]}

Returns the package pinouts and the special pin functions of a product. The data is extracted from the pin assignment chapter of the product specification HTML and stored as JSON in this package.

Parameters
  • product: The product name, for example, nrf52840.
Returns

Dictionary with packages list (each with name, pins, and codes) and specials mapping of port.pin to a sorted list of tags.

def pinout_from_html(path: pathlib.Path) -> dict:
176def pinout_from_html(path: Path) -> dict:
177    """
178    Parses the pin assignment chapter of a Nordic product specification.
179
180    :param path: Path to the HTML file of the pin assignment chapter.
181    :return: Dictionary with `packages` and `specials` as described in `pinout()`.
182    """
183    tree = etree.parse(str(path), parser=etree.HTMLParser(recover=True))
184    package_pinouts = []
185    special_tags = {}
186
187    for table in tree.xpath("//table[caption]"):
188        caption_text = " ".join(text.strip() for text in table.xpath("./caption//text()") if text.strip())
189        caption_lower = caption_text.lower()
190
191        if "special gpio considerations" in caption_lower:
192            for row in table.xpath("./tbody/tr"):
193                cells = row.xpath("./td")
194                if len(cells) < 2:
195                    continue
196                refs = _extract_pin_refs(" ".join(_text_lines(cells[0])))
197                tags = _extract_special_tags(" ".join(_text_lines(cells[1])))
198                for ref in refs:
199                    special_tags.setdefault(ref, set()).update(tags)
200            continue
201
202        if "pin assignment" not in caption_lower and "ball assignment" not in caption_lower:
203            continue
204
205        package_name = _extract_package_name(caption_text)
206        if package_name is None:
207            context = table.xpath("ancestor::article[1]/@id | ancestor::article[1]/h2[1]//text()")
208            package_name = _extract_package_name(" ".join(text.strip() for text in context if text.strip()))
209        if package_name is None:
210            continue
211
212        headers = []
213        for header_cell in table.xpath("./thead//th"):
214            if header_text := " ".join(text.strip() for text in header_cell.xpath(".//text()") if text.strip()).lower():
215                headers.append(header_text)
216        if not headers:
217            continue
218
219        pin_idx = name_idx = function_idx = description_idx = recommended_idx = None
220        for index, header in enumerate(headers):
221            if pin_idx is None and header == "pin":
222                pin_idx = index
223            elif name_idx is None and header == "name":
224                name_idx = index
225            elif function_idx is None and "function" in header:
226                function_idx = index
227            elif description_idx is None and "description" in header:
228                description_idx = index
229            elif recommended_idx is None and "recommended" in header:
230                recommended_idx = index
231        if pin_idx is None or name_idx is None:
232            continue
233
234        def cell_text(cells, index):
235            return " ".join(_text_lines(cells[index])) if index is not None and index < len(cells) else ""
236
237        package_pins = []
238        for row in table.xpath("./tbody/tr"):
239            cells = row.xpath("./td")
240            if len(cells) <= max(pin_idx, name_idx):
241                continue
242            pin_position = " ".join(_text_lines(cells[pin_idx]))
243            name_lines = _text_lines(cells[name_idx])
244            if not pin_position or not name_lines:
245                continue
246
247            gpio_match = None
248            for line in name_lines:
249                if (ref := _parse_gpio_ref(line)) is not None:
250                    gpio_match = (ref[0], ref[1], f"P{int(ref[0])}.{int(ref[1]):02d}")
251                    break
252
253            function_text = cell_text(cells, function_idx)
254            pin_entry = {"position": pin_position, "name": gpio_match[2] if gpio_match else name_lines[0]}
255            if gpio_match is None and "power" in function_text.lower():
256                pin_entry["type"] = "power"
257            package_pins.append(pin_entry)
258
259            if gpio_match is not None:
260                row_text = " ".join(name_lines + [function_text, cell_text(cells, description_idx)])
261                row_text += " " + cell_text(cells, recommended_idx)
262                if tags := _extract_special_tags(row_text):
263                    special_tags.setdefault((gpio_match[0], gpio_match[1]), set()).update(tags)
264
265        if package_pins:
266            package_pinouts.append({"name": package_name, "pins": package_pins})
267
268    return {
269        "packages": package_pinouts,
270        "specials": {f"{port}.{pin}": sorted(tags) for (port, pin), tags in special_tags.items() if tags},
271    }

Parses the pin assignment chapter of a Nordic product specification.

Parameters
  • path: Path to the HTML file of the pin assignment chapter.
Returns

Dictionary with packages and specials as described in pinout().

def write_pinout_json( html_folder: pathlib.Path, json_path: pathlib.Path = None) -> pathlib.Path:
274def write_pinout_json(html_folder: Path, json_path: Path = None) -> Path:
275    """
276    Converts all `ps_{product}-pin.html` files in a folder and merges them into
277    the pinout JSON file of this package, so that only the chapters of new or
278    changed products need to be saved. The files are the pin assignment chapters
279    saved from `PRODUCT_SPECIFICATION_URL`, see the module documentation.
280
281    :param html_folder: Folder containing the product specification pin chapters.
282    :param json_path: Optional output path, defaults to the package data file.
283    :return: Path to the written JSON file.
284    """
285    json_path = Path(json_path or (files(__package__) / "data" / "pinout.json"))
286    data = json.loads(json_path.read_text()) if json_path.exists() else {}
287    for path in sorted(Path(html_folder).glob("ps_*-pin.html")):
288        product = path.name[3:].split("-")[0]
289        data[product] = pinout_from_html(path)
290        LOGGER.info("%s: %d packages", product, len(data[product]["packages"]))
291    json_path.write_text(json.dumps(dict(sorted(data.items())), indent=2) + "\n")
292    return json_path

Converts all ps_{product}-pin.html files in a folder and merges them into the pinout JSON file of this package, so that only the chapters of new or changed products need to be saved. The files are the pin assignment chapters saved from PRODUCT_SPECIFICATION_URL, see the module documentation.

Parameters
  • html_folder: Folder containing the product specification pin chapters.
  • json_path: Optional output path, defaults to the package data file.
Returns

Path to the written JSON file.