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]
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.
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.
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}.
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
packageslist (each withname,pins, andcodes) andspecialsmapping ofport.pinto a sorted list of tags.
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
packagesandspecialsas described inpinout().
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.