modm_data.nrfx.pinout
Nordic Product Specification Pinouts
The package pinouts and special pin functions of the nRF devices are not part
of the nrfx MDK, but only described in the pin assignment chapters of the
Nordic product specifications. Since the Nordic documentation is protected by a
bot challenge, it cannot be downloaded automatically, so the data is extracted
manually and stored in this package as data/pinout.json. The pipeline only
reads this JSON file and does not require any network access.
Updating the Pinout Data
The JSON file only needs to be updated for new products or data corrections:
- Open the pin assignment chapter of the product specification in a browser, for example, https://docs.nordicsemi.com/r/bundle/ps_nrf52840/page/pin.html. All products are listed in the product overview, for example, https://docs.nordicsemi.com/r/bundle/additionalresources/page/additionalresources/nrf53-series/nrf5340.
- The page content is rendered with JavaScript, so saving the page source only
stores an empty shell. Instead, copy the rendered page: In Safari, open
Develop → Show Web Inspector, right-click the
<html>element in the Elements tab, and select Copy → HTML. - Paste the HTML into
ext/nordic/pinout/ps_{product}-pin.html, for example,ext/nordic/pinout/ps_nrf52840-pin.html. Convert the saved chapters and merge them into the JSON file:
make convert-nordic-pinoutAdd the package codes of new products to
package_code_map.- Review the changes of the JSON file and commit only the JSON file, since the HTML files are subject to Nordic's copyright.
The parser in pinout_from_html() expects tables with a caption containing
"Pin assignments" or "Ball assignments" and the columns "Pin" and "Name", and
a table captioned "Special GPIO considerations". If the documentation layout
changes, the parser needs to be adapted and the JSON output compared with the
previous version.
1# Copyright 2020, Hannes Ellinger 2# Copyright 2026, Niklas Hauser 3# SPDX-License-Identifier: MPL-2.0 4 5""" 6# Nordic Product Specification Pinouts 7 8The package pinouts and special pin functions of the nRF devices are not part 9of the nrfx MDK, but only described in the pin assignment chapters of the 10Nordic product specifications. Since the Nordic documentation is protected by a 11bot challenge, it cannot be downloaded automatically, so the data is extracted 12manually and stored in this package as `data/pinout.json`. The pipeline only 13reads this JSON file and does not require any network access. 14 15## Updating the Pinout Data 16 17The JSON file only needs to be updated for new products or data corrections: 18 191. Open the pin assignment chapter of the product specification in a browser, 20 for example, <https://docs.nordicsemi.com/r/bundle/ps_nrf52840/page/pin.html>. 21 All products are listed in the product overview, for example, 22 <https://docs.nordicsemi.com/r/bundle/additionalresources/page/additionalresources/nrf53-series/nrf5340>. 232. The page content is rendered with JavaScript, so saving the page source only 24 stores an empty shell. Instead, copy the rendered page: In Safari, open 25 *Develop → Show Web Inspector*, right-click the `<html>` element in the 26 *Elements* tab, and select *Copy → HTML*. 273. Paste the HTML into `ext/nordic/pinout/ps_{product}-pin.html`, for example, 28 `ext/nordic/pinout/ps_nrf52840-pin.html`. 294. Convert the saved chapters and merge them into the JSON file: 30 ```sh 31 make convert-nordic-pinout 32 ``` 335. Add the package codes of new products to `package_code_map`. 346. Review the changes of the JSON file and commit only the JSON file, since the 35 HTML files are subject to Nordic's copyright. 36 37The parser in `pinout_from_html()` expects tables with a caption containing 38"Pin assignments" or "Ball assignments" and the columns "Pin" and "Name", and 39a table captioned "Special GPIO considerations". If the documentation layout 40changes, the parser needs to be adapted and the JSON output compared with the 41previous version. 42""" 43 44import re 45import json 46import logging 47from pathlib import Path 48from functools import cache 49from importlib.resources import files 50 51from lxml import etree 52 53LOGGER = logging.getLogger(__name__) 54 55PRODUCT_SPECIFICATION_URL = "https://docs.nordicsemi.com/r/bundle/ps_{product}/page/pin.html" 56 57# Maps the package names of the product specification to the package codes of the device names 58package_code_map = { 59 "nrf52805": {"WLCSP": ["ca"]}, 60 "nrf52810": {"QFN48": ["qf"], "QFN32": ["qc"], "WLCSP": ["ca"]}, 61 "nrf52811": {"QFN48": ["qf"], "QFN32": ["qc"], "WLCSP": ["ca"]}, 62 "nrf52820": {"QFN40": ["qd"], "WLCSP": ["cf"]}, 63 "nrf52832": {"QFN48": ["qf"], "WLCSP": ["ci"]}, 64 "nrf52833": {"aQFN73": ["qi"], "QFN40": ["qd"], "WLCSP": ["cj"]}, 65 "nrf52840": {"aQFN73": ["qi"], "QFN48": ["qf"], "WLCSP": ["ck"]}, 66 "nrf5340": {"aQFN94": ["qk"], "WLCSP": ["cl", "cm"]}, 67} 68 69 70@cache 71def _pinout_data() -> dict: 72 return json.loads((files(__package__) / "data" / "pinout.json").read_text()) 73 74 75def pinout(product: str) -> dict: 76 """ 77 Returns the package pinouts and the special pin functions of a product. 78 The data is extracted from the pin assignment chapter of the product 79 specification HTML and stored as JSON in this package. 80 81 :param product: The product name, for example, `nrf52840`. 82 :return: Dictionary with `packages` list (each with `name`, `pins`, and `codes`) 83 and `specials` mapping of `port.pin` to a sorted list of tags. 84 """ 85 data = _pinout_data().get(product, {"packages": [], "specials": {}}) 86 codes = package_code_map.get(product, {}) 87 packages = [package | {"codes": codes.get(package["name"], [])} for package in data["packages"]] 88 return {"packages": packages, "specials": data["specials"]} 89 90 91def _text_lines(element): 92 lines = [line.strip() for line in element.xpath('.//p[contains(@class, "lines")]/text()') if line.strip()] 93 if lines: 94 return lines 95 return [line.strip() for line in element.xpath(".//text()") if line.strip()] 96 97 98def _parse_gpio_ref(token): 99 if token is None: 100 return None 101 cleaned = token.strip().upper().rstrip(".,;:") 102 if not cleaned.startswith("P") or "." not in cleaned: 103 return None 104 port_text, pin_text = cleaned[1:].split(".", 1) 105 if not port_text.isdigit(): 106 return None 107 pin_digits = [] 108 for character in pin_text: 109 if not character.isdigit(): 110 break 111 pin_digits.append(character) 112 if not pin_digits: 113 return None 114 return str(int(port_text)), str(int("".join(pin_digits))) 115 116 117def _extract_package_name(caption): 118 compact = re.sub(r"\s+", " ", caption or "").strip() 119 if match := re.search(r"(aQFN\d+|QFN\d+|WLCSP\d+|WLCSP)", compact, re.IGNORECASE): 120 return match.group(1).replace("aqfn", "aQFN").replace("wlcsp", "WLCSP") 121 return None 122 123 124def _extract_pin_refs(pin_text): 125 tokenized = pin_text or "" 126 for separator in ("-", "/", ",", ";", ":", "(", ")", "[", "]"): 127 tokenized = tokenized.replace(separator, f" {separator} ") 128 refs = [ref for token in tokenized.split() if (ref := _parse_gpio_ref(token)) is not None] 129 if not refs: 130 return [] 131 if len(refs) == 2 and "-" in (pin_text or ""): 132 first_port, first_pin = refs[0][0], int(refs[0][1]) 133 second_port, second_pin = refs[1][0], int(refs[1][1]) 134 if first_port == second_port and first_pin <= second_pin: 135 return [(first_port, str(pin)) for pin in range(first_pin, second_pin + 1)] 136 unique = [] 137 for value in refs: 138 if value not in unique: 139 unique.append(value) 140 return unique 141 142 143def _extract_special_tags(text): 144 lower = text.lower() 145 tags = set() 146 for ain in re.findall(r"ain\s*(\d+)", lower): 147 tags.add(f"ain{ain}") 148 if "trace" in lower: 149 tags.add("trace") 150 if "traceclk" in lower: 151 tags.add("traceclk") 152 for tracedata in re.findall(r"tracedata\s*\[?(\d+)\]?", lower): 153 tags.add(f"tracedata{tracedata}") 154 if "serial wire output" in lower or re.search(r"\bswo\b", lower): 155 tags.add("swo") 156 if "qspi" in lower: 157 tags.add("qspi") 158 for signal in re.findall(r"\b(io[0-3]|sck|csn|dcx)\s+for\s+qspi\b", lower): 159 tags.add(f"qspi_{signal}") 160 for signal in re.findall(r"qspi\s*/\s*(csn|sck)\b", lower): 161 tags.add(f"qspi_{signal}") 162 if "spim4" in lower: 163 tags.add("spim4") 164 for signal in re.findall(r"\b(sck|mosi|miso|csn|dcx)\s+for\s+spim4\b", lower): 165 tags.add(f"spim4_{signal}") 166 if "twim" in lower: 167 tags.add("twim") 168 if "twis" in lower: 169 tags.add("twis") 170 if re.search(r"\btwi\b", lower): 171 tags.add("twi") 172 return tags 173 174 175def pinout_from_html(path: Path) -> dict: 176 """ 177 Parses the pin assignment chapter of a Nordic product specification. 178 179 :param path: Path to the HTML file of the pin assignment chapter. 180 :return: Dictionary with `packages` and `specials` as described in `pinout()`. 181 """ 182 tree = etree.parse(str(path), parser=etree.HTMLParser(recover=True)) 183 package_pinouts = [] 184 special_tags = {} 185 186 for table in tree.xpath("//table[caption]"): 187 caption_text = " ".join(text.strip() for text in table.xpath("./caption//text()") if text.strip()) 188 caption_lower = caption_text.lower() 189 190 if "special gpio considerations" in caption_lower: 191 for row in table.xpath("./tbody/tr"): 192 cells = row.xpath("./td") 193 if len(cells) < 2: 194 continue 195 refs = _extract_pin_refs(" ".join(_text_lines(cells[0]))) 196 tags = _extract_special_tags(" ".join(_text_lines(cells[1]))) 197 for ref in refs: 198 special_tags.setdefault(ref, set()).update(tags) 199 continue 200 201 if "pin assignment" not in caption_lower and "ball assignment" not in caption_lower: 202 continue 203 204 package_name = _extract_package_name(caption_text) 205 if package_name is None: 206 context = table.xpath("ancestor::article[1]/@id | ancestor::article[1]/h2[1]//text()") 207 package_name = _extract_package_name(" ".join(text.strip() for text in context if text.strip())) 208 if package_name is None: 209 continue 210 211 headers = [] 212 for header_cell in table.xpath("./thead//th"): 213 if header_text := " ".join(text.strip() for text in header_cell.xpath(".//text()") if text.strip()).lower(): 214 headers.append(header_text) 215 if not headers: 216 continue 217 218 pin_idx = name_idx = function_idx = description_idx = recommended_idx = None 219 for index, header in enumerate(headers): 220 if pin_idx is None and header == "pin": 221 pin_idx = index 222 elif name_idx is None and header == "name": 223 name_idx = index 224 elif function_idx is None and "function" in header: 225 function_idx = index 226 elif description_idx is None and "description" in header: 227 description_idx = index 228 elif recommended_idx is None and "recommended" in header: 229 recommended_idx = index 230 if pin_idx is None or name_idx is None: 231 continue 232 233 def cell_text(cells, index): 234 return " ".join(_text_lines(cells[index])) if index is not None and index < len(cells) else "" 235 236 package_pins = [] 237 for row in table.xpath("./tbody/tr"): 238 cells = row.xpath("./td") 239 if len(cells) <= max(pin_idx, name_idx): 240 continue 241 pin_position = " ".join(_text_lines(cells[pin_idx])) 242 name_lines = _text_lines(cells[name_idx]) 243 if not pin_position or not name_lines: 244 continue 245 246 gpio_match = None 247 for line in name_lines: 248 if (ref := _parse_gpio_ref(line)) is not None: 249 gpio_match = (ref[0], ref[1], f"P{int(ref[0])}.{int(ref[1]):02d}") 250 break 251 252 function_text = cell_text(cells, function_idx) 253 pin_entry = {"position": pin_position, "name": gpio_match[2] if gpio_match else name_lines[0]} 254 if gpio_match is None and "power" in function_text.lower(): 255 pin_entry["type"] = "power" 256 package_pins.append(pin_entry) 257 258 if gpio_match is not None: 259 row_text = " ".join(name_lines + [function_text, cell_text(cells, description_idx)]) 260 row_text += " " + cell_text(cells, recommended_idx) 261 if tags := _extract_special_tags(row_text): 262 special_tags.setdefault((gpio_match[0], gpio_match[1]), set()).update(tags) 263 264 if package_pins: 265 package_pinouts.append({"name": package_name, "pins": package_pins}) 266 267 return { 268 "packages": package_pinouts, 269 "specials": {f"{port}.{pin}": sorted(tags) for (port, pin), tags in special_tags.items() if tags}, 270 } 271 272 273def write_pinout_json(html_folder: Path, json_path: Path = None) -> Path: 274 """ 275 Converts all `ps_{product}-pin.html` files in a folder and merges them into 276 the pinout JSON file of this package, so that only the chapters of new or 277 changed products need to be saved. The files are the pin assignment chapters 278 saved from `PRODUCT_SPECIFICATION_URL`, see the module documentation. 279 280 :param html_folder: Folder containing the product specification pin chapters. 281 :param json_path: Optional output path, defaults to the package data file. 282 :return: Path to the written JSON file. 283 """ 284 json_path = Path(json_path or (files(__package__) / "data" / "pinout.json")) 285 data = json.loads(json_path.read_text()) if json_path.exists() else {} 286 for path in sorted(Path(html_folder).glob("ps_*-pin.html")): 287 product = path.name[3:].split("-")[0] 288 data[product] = pinout_from_html(path) 289 LOGGER.info("%s: %d packages", product, len(data[product]["packages"])) 290 json_path.write_text(json.dumps(dict(sorted(data.items())), indent=2) + "\n") 291 return json_path
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.