modm_data.html.stmicro

 1# Copyright 2022, Niklas Hauser
 2# SPDX-License-Identifier: MPL-2.0
 3
 4from .datasheet_sensor import DatasheetSensor
 5from .datasheet_stm32 import DatasheetStm32
 6from .reference import ReferenceManual
 7from .document import load_documents, load_document_devices, datasheet_for_device, reference_manual_for_device
 8from .helper import find_device_filter
 9
10__all__ = [
11    "DatasheetSensor",
12    "DatasheetStm32",
13    "ReferenceManual",
14    "load_documents",
15    "load_document_devices",
16    "datasheet_for_device",
17    "reference_manual_for_device",
18    "find_device_filter",
19]
class DatasheetSensor(modm_data.html.document.Document):
11class DatasheetSensor(Document):
12    def __init__(self, path: str):
13        super().__init__(path)
14
15    def __repr__(self) -> str:
16        return f"DSsensor({self.fullname})"
17
18    @cache
19    def register_map(self, assert_table=True):
20        pass
DatasheetSensor(path: str)
12    def __init__(self, path: str):
13        super().__init__(path)
@cache
def register_map(self, assert_table=True):
18    @cache
19    def register_map(self, assert_table=True):
20        pass
Inherited Members
modm_data.html.document.Document
path
relpath
fullname
name
version
path_pdf
chapters
chapter
class DatasheetStm32(modm_data.html.document.Document):
 16class DatasheetStm32(Document):
 17    def __init__(self, path: str):
 18        super().__init__(path)
 19        self._id = {}
 20        self._devices = {}
 21
 22    def __repr__(self) -> str:
 23        return f"DSstm32({self.fullname})"
 24
 25    @cached_property
 26    def device_family(self) -> str:
 27        return re.search(r"STM32\w\w", self.chapter("chapter 0").headings()[0].text(br=" ")).group(0)
 28
 29    @cached_property
 30    def devices(self) -> list[DeviceIdentifier]:
 31        # Find the device summary table or use the heading
 32        chapter = self.chapter("chapter 0")
 33        ndevices = []
 34        if tables := chapter.tables("Device summary"):
 35            for row in tables[0].cell_rows("number"):
 36                ndevices.extend(
 37                    c.text(br=" ", sup=" ").replace(",", " ").strip() for cells in row.values() for c in cells
 38                )
 39            ndevices = [d for dev in ndevices for d in dev.split(" ") if "STM32" in d]
 40        else:
 41            # Check all uncaptioned tables for "Product summary" domain
 42            for table in chapter.tables():
 43                if table.domains_x("Product +summary"):
 44                    for row in table.cell_rows():
 45                        for cells in row.values():
 46                            ndevices.extend(html_listify(cells[-1].text(br=" ", sup=" ")))
 47                    break
 48            else:
 49                # print(chapter._path.relative_to(Path().cwd()))
 50                ndevices = chapter.headings()[0].text(br=" ").replace(",", " ").strip()
 51                ndevices = [d.replace("-", "xx") for d in ndevices.split(" ") if "STM32" in d]
 52        # print(ndevices)
 53
 54        # Split combo strings into individual devices
 55        devices = []
 56        for device in ndevices:
 57            device = device.replace("STM32479", "STM32F479").replace("STM32469", "STM32F469")
 58            if not re.match(r"STM32[A-Z][0-57BL][0-9ABCERSPQ][0-9AB]", device):
 59                raise ValueError(f"Filter ID '{device}' does not match schema!")
 60            devices.extend(split_device_filter(device))
 61
 62        # Find the Ordering Information to complete the identifiers (x=?)
 63        did = defaultdict(dict)
 64        current = ""
 65        chapter = self.chapter("ordering|numbering")
 66        for heading, texts in chapter.heading_texts("ordering|numbering"):
 67            for text in texts:
 68                for para in text.text(br="\n").split("\n"):
 69                    if "=" in para or ":" in para:
 70                        key, value = para.split("=" if "=" in para else ":", 1)
 71                        did[current][key.strip()] = value.strip()
 72                    else:
 73                        current = para
 74        self._id = did = ReDict(did)
 75        # print(did)
 76        did_map = [
 77            ("count", "pin"),
 78            ("size", "size"),
 79            ("package", "package"),
 80            ("temperature", "temperature"),
 81            ("option|identification|dedicated +pinout|power pairs|specificity|packing", "variant"),
 82        ]
 83        sdids = [(dname, {}) for dname in set(dev[:9] for dev in devices)]
 84        for pattern, name in did_map:
 85            if keys := did.match_keys(pattern):
 86                new_ids = []
 87                if name == "variant":
 88                    for oid in sdids:
 89                        new_ids.append((oid[0], oid[1] | {f"{name}": "normal"}))
 90                for value, descr in did[keys[0]].items():
 91                    value = re.sub(r"\(\d\)", "", value)
 92                    if len(value) > 1:
 93                        if name == "variant" and (match := re.search(r"x += +([NA])", descr)):
 94                            # We need to specially deal with the -N variants
 95                            value = match.group(1)
 96                        else:
 97                            value = ""
 98                            if name == "variant":
 99                                continue
100                    for oid in sdids:
101                        new_ids.append((oid[0] + value, oid[1] | {f"{name}": descr}))
102                sdids = new_ids
103            elif name != "variant":
104                raise KeyError(f"Cannot find '{pattern}' in {did.keys()}!")
105
106        # Some datasheets have the specific table at the very end
107        if tables := chapter.tables("order codes"):
108            for row in tables[0].cell_rows("code"):
109                for cells in row.values():
110                    for cell in cells:
111                        devices.extend(html_listify(cell.text()))
112
113        # convert to proper device identifiers
114        dids = set()
115        for string, descr in sdids:
116            did = did_from_string(string)
117            # print(did)
118            # filter through the device strings:
119            for device in devices:
120                if re.match(device.replace("x", ".").lower(), did.string):
121                    dids.add(did)
122                    break
123
124        # print(devices)
125        if not dids:
126            raise ValueError(f"Could not find devices for {self.fullname}!")
127        return list(dids)
128
129    @property
130    def _chapter_pins(self):
131        return self.chapter("pin description|pinout")
132
133    @property
134    def _table_pinout(self):
135        tables = self._chapter_pins.tables(
136            "pin( +and +ball|/ball| +assignment +and)? +(definition|description)", br=" "
137        )
138        if not tables:
139            raise ValueError(f"Unable to find pin definition table in chapter! {self._chapter_pins._relpath}")
140        return tables[0]
141
142    @property
143    def _tables_alternate_functions(self):
144        tables = self._chapter_pins.tables("alternate +function", br=" ")
145        if not tables and "STM32F1" not in self.device_family:
146            raise ValueError(f"Unable to find alternate function table in chapter! {self._chapter_pins._relpath}")
147        return tables
148
149    @cached_property
150    def packages(self):
151        domains = self._table_pinout.domains_x(r"num(<br>)?ber|pins?:|pin/ball +name|:WLCSP.*?", br="<br>")
152        if not domains:
153            raise ValueError(f"Unable to find package domains in table! {self._chapter_pins._relpath}")
154        packages = []
155        for domain in domains:
156            if domain == "Pin (UFQFPN48):Number":
157                continue
158            ndpackage = domain.split(":")[-1].replace("UFBGA/TFBGA64", "UFBGA64/TFBGA64")
159            ndpackage = ndpackage.replace("LPQF", "LQFP").replace("TSSPOP", "TSSOP")
160            ndpackage = ndpackage.replace("UFBG100", "UFBGA100").replace("UBGA", "UFBGA")
161            ndpackage = ndpackage.replace("UQFN", "UFQFPN").replace("WLCSP20L", "WLCSP20")
162            ndpackage = ndpackage.replace("UFQFPN48E", "UFQFPN48+E").replace("UFQFN", "UFQFPN")
163            ndpackage = ndpackage.replace("LQFP48 SMPS<br>UFQFPN48 SMPS", "LQFP48/UFQFPN48+SMPS")
164            ndpackage = ndpackage.replace("LQFP<br>64", "LQFP64").replace("LQFP<br>48", "LQFP48")
165            ndpackage = ndpackage.replace("UFQFPN<br>32", "UFQFPN32").replace("UFQFP<br>N48", "UFQFPN48")
166            ndpackage = ndpackage.replace("WLCSP<br>25", "WLCSP25")
167            ndpackage = re.sub(r"^BGA", "UFBGA", ndpackage)
168            ndpackage = re.sub(r"\(\d\)| ", "", ndpackage)
169            ndpackage = re.sub(r"[Ee]xt-?/?", "", ndpackage)
170            ndpackage = re.sub(r"<br>SMPS", "SMPS", ndpackage)
171            ndpackage = re.sub(r"SMSP", "SMPS", ndpackage)
172            ndpackage = re.sub(r"with|-|_", "+", ndpackage)
173            ndpackage = re.sub(r"or", "/", ndpackage)
174            ndpackage = re.sub(r"\(STM32L0.*?UxSonly\)", "+S", ndpackage)
175            ndpackage = re.sub(r"(\d+)SMPS", r"\1+SMPS", ndpackage)
176            ndpackage = re.sub(r"\+GP", "", ndpackage)
177            if "DS13311" in self.name or "DS13312" in self.name:
178                ndpackage = ndpackage.replace("+SMPS", "")
179            if (match := re.search(r":(STM32.*?):", domain)) is not None:
180                devs = html_listify(match.group(1))
181                ndpackage += "+" + "|".join(d.replace("x", ".") for d in devs)
182            ndpackage, *filters = ndpackage.split("+")
183            filters = ("+" + "+".join(filters)) if filters else ""
184            spackage = [p for p in re.split(r"[/,]| or |<br>", ndpackage) if p]
185            # print(domain, spackage)
186            for package in spackage:
187                if (pack := split_package(package)) is not None:
188                    packages.append((domain, package + filters, *pack))
189                else:
190                    print(f"Unknown package {package}!")
191        if not packages:
192            if "DS13259" in self.name:
193                packages = [("Pin:Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
194            elif "DS13047" in self.name:
195                packages = [("Pin (UFQFPN48):Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
196            else:
197                raise ValueError(f"Unable to find packages! {self._chapter_pins._relpath} {domains}")
198        return packages
199
200    @property
201    def packages_pins(self):
202        # Add pinouts and signals
203        pin_replace = {r"sup| |D?NC|/$|\(.*?\)|.*?connected.*": "", "–": "-", r"VREF_\+": "VREF+"}
204        add_replace = {
205            r"[- ]|\(.*?\)|.*?selection.*|.*?reset.*": "",
206            r"OPAMP": ",OPAMP",
207            r"COMP": ",COMP",
208            r"OPAMP,1": "OPAMP1",
209        }
210        afs_replace = {r"[- ]|\(.*?\)": "", "LCD_SEG": ",LCD_SEG"}  # noqa: F841
211        pos_replace = {r'[“”\-"]|NC|\(.*?\)': ""}
212        sig_replace = {
213            r"[- ]|\(.*?\)": "",
214            r"(MOSI|SCK|NSS)I2S": r"\1,I2S",
215            "_µM": "_M",
216            r"(CH\d)TIM": r"\1,TIM",
217            r"_([RT]XD\d?|DV|EN|CLK)ETH_": r"_\1,ETH_",
218        }
219
220        data_packages = defaultdict(list)
221        data_pins = defaultdict(dict)
222
223        packages = set((d[0], d[1]) for d in self.packages)
224        # Import the pin definitions incl. additional function
225        for row in self._table_pinout.cell_rows(br="<br>"):
226            pin_name = row.match_value("pin +name|:name")[0].text(**pin_replace).strip()
227            if not pin_name:
228                continue
229            ios = row.match_value("I ?/ ?O")[0].text(**{"-": ""})
230            ptype = row.match_value("type")[0].text()
231            # Hack to make fix the PD0/PD1 pins
232            if pin_name.startswith("OSC") and (remap := row.match_value("remap")):
233                if (pin := remap[0].text()).startswith("P"):
234                    pin_name = f"{pin}-{pin_name}"
235
236            data_pin = data_pins[pin_name]
237            if ios:
238                data_pin["structure"] = ios
239            if ptype:
240                data_pin["type"] = ptype
241            if ptype == "I/O" and "STM32F1" not in self.device_family:
242                signals = html_listify(row.match_value("additional")[0].text(**add_replace))
243                data_pin["additional"] = set(signals)
244
245            for domain, package_name in packages:
246                if ppos := html_listify(row[domain][0].text(**pos_replace)):
247                    data_packages[package_name].append((pin_name, ppos))
248
249        # Import the alternate functions
250        for table in self._tables_alternate_functions:
251            cells = table.domains("port|pin +name").cells(r"AF(IO)?\d+")
252            for pin, afs in cells.items():
253                name = html_replace(pin.split(":")[-1], **pin_replace)
254                data_pin = data_pins[name]
255                if "alternate" not in data_pin:
256                    data_pin["alternate"] = defaultdict(list)
257                for af, csignals in afs.items():
258                    af = int(re.search(r"AF(IO)?(\d{1,2})", af).group(2))
259                    for csignal in csignals:
260                        signals = html_listify(csignal.text(**sig_replace))
261                        data_pin["alternate"][af].extend(signals)
262
263        return data_packages, data_pins
DatasheetStm32(path: str)
17    def __init__(self, path: str):
18        super().__init__(path)
19        self._id = {}
20        self._devices = {}
device_family: str
25    @cached_property
26    def device_family(self) -> str:
27        return re.search(r"STM32\w\w", self.chapter("chapter 0").headings()[0].text(br=" ")).group(0)
devices: list[modm_data.owl.DeviceIdentifier]
 29    @cached_property
 30    def devices(self) -> list[DeviceIdentifier]:
 31        # Find the device summary table or use the heading
 32        chapter = self.chapter("chapter 0")
 33        ndevices = []
 34        if tables := chapter.tables("Device summary"):
 35            for row in tables[0].cell_rows("number"):
 36                ndevices.extend(
 37                    c.text(br=" ", sup=" ").replace(",", " ").strip() for cells in row.values() for c in cells
 38                )
 39            ndevices = [d for dev in ndevices for d in dev.split(" ") if "STM32" in d]
 40        else:
 41            # Check all uncaptioned tables for "Product summary" domain
 42            for table in chapter.tables():
 43                if table.domains_x("Product +summary"):
 44                    for row in table.cell_rows():
 45                        for cells in row.values():
 46                            ndevices.extend(html_listify(cells[-1].text(br=" ", sup=" ")))
 47                    break
 48            else:
 49                # print(chapter._path.relative_to(Path().cwd()))
 50                ndevices = chapter.headings()[0].text(br=" ").replace(",", " ").strip()
 51                ndevices = [d.replace("-", "xx") for d in ndevices.split(" ") if "STM32" in d]
 52        # print(ndevices)
 53
 54        # Split combo strings into individual devices
 55        devices = []
 56        for device in ndevices:
 57            device = device.replace("STM32479", "STM32F479").replace("STM32469", "STM32F469")
 58            if not re.match(r"STM32[A-Z][0-57BL][0-9ABCERSPQ][0-9AB]", device):
 59                raise ValueError(f"Filter ID '{device}' does not match schema!")
 60            devices.extend(split_device_filter(device))
 61
 62        # Find the Ordering Information to complete the identifiers (x=?)
 63        did = defaultdict(dict)
 64        current = ""
 65        chapter = self.chapter("ordering|numbering")
 66        for heading, texts in chapter.heading_texts("ordering|numbering"):
 67            for text in texts:
 68                for para in text.text(br="\n").split("\n"):
 69                    if "=" in para or ":" in para:
 70                        key, value = para.split("=" if "=" in para else ":", 1)
 71                        did[current][key.strip()] = value.strip()
 72                    else:
 73                        current = para
 74        self._id = did = ReDict(did)
 75        # print(did)
 76        did_map = [
 77            ("count", "pin"),
 78            ("size", "size"),
 79            ("package", "package"),
 80            ("temperature", "temperature"),
 81            ("option|identification|dedicated +pinout|power pairs|specificity|packing", "variant"),
 82        ]
 83        sdids = [(dname, {}) for dname in set(dev[:9] for dev in devices)]
 84        for pattern, name in did_map:
 85            if keys := did.match_keys(pattern):
 86                new_ids = []
 87                if name == "variant":
 88                    for oid in sdids:
 89                        new_ids.append((oid[0], oid[1] | {f"{name}": "normal"}))
 90                for value, descr in did[keys[0]].items():
 91                    value = re.sub(r"\(\d\)", "", value)
 92                    if len(value) > 1:
 93                        if name == "variant" and (match := re.search(r"x += +([NA])", descr)):
 94                            # We need to specially deal with the -N variants
 95                            value = match.group(1)
 96                        else:
 97                            value = ""
 98                            if name == "variant":
 99                                continue
100                    for oid in sdids:
101                        new_ids.append((oid[0] + value, oid[1] | {f"{name}": descr}))
102                sdids = new_ids
103            elif name != "variant":
104                raise KeyError(f"Cannot find '{pattern}' in {did.keys()}!")
105
106        # Some datasheets have the specific table at the very end
107        if tables := chapter.tables("order codes"):
108            for row in tables[0].cell_rows("code"):
109                for cells in row.values():
110                    for cell in cells:
111                        devices.extend(html_listify(cell.text()))
112
113        # convert to proper device identifiers
114        dids = set()
115        for string, descr in sdids:
116            did = did_from_string(string)
117            # print(did)
118            # filter through the device strings:
119            for device in devices:
120                if re.match(device.replace("x", ".").lower(), did.string):
121                    dids.add(did)
122                    break
123
124        # print(devices)
125        if not dids:
126            raise ValueError(f"Could not find devices for {self.fullname}!")
127        return list(dids)
packages
149    @cached_property
150    def packages(self):
151        domains = self._table_pinout.domains_x(r"num(<br>)?ber|pins?:|pin/ball +name|:WLCSP.*?", br="<br>")
152        if not domains:
153            raise ValueError(f"Unable to find package domains in table! {self._chapter_pins._relpath}")
154        packages = []
155        for domain in domains:
156            if domain == "Pin (UFQFPN48):Number":
157                continue
158            ndpackage = domain.split(":")[-1].replace("UFBGA/TFBGA64", "UFBGA64/TFBGA64")
159            ndpackage = ndpackage.replace("LPQF", "LQFP").replace("TSSPOP", "TSSOP")
160            ndpackage = ndpackage.replace("UFBG100", "UFBGA100").replace("UBGA", "UFBGA")
161            ndpackage = ndpackage.replace("UQFN", "UFQFPN").replace("WLCSP20L", "WLCSP20")
162            ndpackage = ndpackage.replace("UFQFPN48E", "UFQFPN48+E").replace("UFQFN", "UFQFPN")
163            ndpackage = ndpackage.replace("LQFP48 SMPS<br>UFQFPN48 SMPS", "LQFP48/UFQFPN48+SMPS")
164            ndpackage = ndpackage.replace("LQFP<br>64", "LQFP64").replace("LQFP<br>48", "LQFP48")
165            ndpackage = ndpackage.replace("UFQFPN<br>32", "UFQFPN32").replace("UFQFP<br>N48", "UFQFPN48")
166            ndpackage = ndpackage.replace("WLCSP<br>25", "WLCSP25")
167            ndpackage = re.sub(r"^BGA", "UFBGA", ndpackage)
168            ndpackage = re.sub(r"\(\d\)| ", "", ndpackage)
169            ndpackage = re.sub(r"[Ee]xt-?/?", "", ndpackage)
170            ndpackage = re.sub(r"<br>SMPS", "SMPS", ndpackage)
171            ndpackage = re.sub(r"SMSP", "SMPS", ndpackage)
172            ndpackage = re.sub(r"with|-|_", "+", ndpackage)
173            ndpackage = re.sub(r"or", "/", ndpackage)
174            ndpackage = re.sub(r"\(STM32L0.*?UxSonly\)", "+S", ndpackage)
175            ndpackage = re.sub(r"(\d+)SMPS", r"\1+SMPS", ndpackage)
176            ndpackage = re.sub(r"\+GP", "", ndpackage)
177            if "DS13311" in self.name or "DS13312" in self.name:
178                ndpackage = ndpackage.replace("+SMPS", "")
179            if (match := re.search(r":(STM32.*?):", domain)) is not None:
180                devs = html_listify(match.group(1))
181                ndpackage += "+" + "|".join(d.replace("x", ".") for d in devs)
182            ndpackage, *filters = ndpackage.split("+")
183            filters = ("+" + "+".join(filters)) if filters else ""
184            spackage = [p for p in re.split(r"[/,]| or |<br>", ndpackage) if p]
185            # print(domain, spackage)
186            for package in spackage:
187                if (pack := split_package(package)) is not None:
188                    packages.append((domain, package + filters, *pack))
189                else:
190                    print(f"Unknown package {package}!")
191        if not packages:
192            if "DS13259" in self.name:
193                packages = [("Pin:Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
194            elif "DS13047" in self.name:
195                packages = [("Pin (UFQFPN48):Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
196            else:
197                raise ValueError(f"Unable to find packages! {self._chapter_pins._relpath} {domains}")
198        return packages
packages_pins
200    @property
201    def packages_pins(self):
202        # Add pinouts and signals
203        pin_replace = {r"sup| |D?NC|/$|\(.*?\)|.*?connected.*": "", "–": "-", r"VREF_\+": "VREF+"}
204        add_replace = {
205            r"[- ]|\(.*?\)|.*?selection.*|.*?reset.*": "",
206            r"OPAMP": ",OPAMP",
207            r"COMP": ",COMP",
208            r"OPAMP,1": "OPAMP1",
209        }
210        afs_replace = {r"[- ]|\(.*?\)": "", "LCD_SEG": ",LCD_SEG"}  # noqa: F841
211        pos_replace = {r'[“”\-"]|NC|\(.*?\)': ""}
212        sig_replace = {
213            r"[- ]|\(.*?\)": "",
214            r"(MOSI|SCK|NSS)I2S": r"\1,I2S",
215            "_µM": "_M",
216            r"(CH\d)TIM": r"\1,TIM",
217            r"_([RT]XD\d?|DV|EN|CLK)ETH_": r"_\1,ETH_",
218        }
219
220        data_packages = defaultdict(list)
221        data_pins = defaultdict(dict)
222
223        packages = set((d[0], d[1]) for d in self.packages)
224        # Import the pin definitions incl. additional function
225        for row in self._table_pinout.cell_rows(br="<br>"):
226            pin_name = row.match_value("pin +name|:name")[0].text(**pin_replace).strip()
227            if not pin_name:
228                continue
229            ios = row.match_value("I ?/ ?O")[0].text(**{"-": ""})
230            ptype = row.match_value("type")[0].text()
231            # Hack to make fix the PD0/PD1 pins
232            if pin_name.startswith("OSC") and (remap := row.match_value("remap")):
233                if (pin := remap[0].text()).startswith("P"):
234                    pin_name = f"{pin}-{pin_name}"
235
236            data_pin = data_pins[pin_name]
237            if ios:
238                data_pin["structure"] = ios
239            if ptype:
240                data_pin["type"] = ptype
241            if ptype == "I/O" and "STM32F1" not in self.device_family:
242                signals = html_listify(row.match_value("additional")[0].text(**add_replace))
243                data_pin["additional"] = set(signals)
244
245            for domain, package_name in packages:
246                if ppos := html_listify(row[domain][0].text(**pos_replace)):
247                    data_packages[package_name].append((pin_name, ppos))
248
249        # Import the alternate functions
250        for table in self._tables_alternate_functions:
251            cells = table.domains("port|pin +name").cells(r"AF(IO)?\d+")
252            for pin, afs in cells.items():
253                name = html_replace(pin.split(":")[-1], **pin_replace)
254                data_pin = data_pins[name]
255                if "alternate" not in data_pin:
256                    data_pin["alternate"] = defaultdict(list)
257                for af, csignals in afs.items():
258                    af = int(re.search(r"AF(IO)?(\d{1,2})", af).group(2))
259                    for csignal in csignals:
260                        signals = html_listify(csignal.text(**sig_replace))
261                        data_pin["alternate"][af].extend(signals)
262
263        return data_packages, data_pins
Inherited Members
modm_data.html.document.Document
path
relpath
fullname
name
version
path_pdf
chapters
chapter
class ReferenceManual(modm_data.html.document.Document):
 13class ReferenceManual(Document):
 14    def __init__(self, path: str):
 15        super().__init__(path)
 16
 17    def __repr__(self) -> str:
 18        return f"RM({self.fullname})"
 19
 20    @cached_property
 21    def devices(self) -> list[str]:
 22        # Find all occurrences of STM32* strings in the first chapter
 23        chapter = self.chapter("chapter 0")
 24        for heading, texts in chapter.heading_texts():
 25            all_text = ""
 26            for text in texts:
 27                all_text += text.text(br=" ")
 28            # print(heading, all_text)
 29            # Must also match "STM32L4+" !!!
 30            rdevices = re.findall(r"STM32[\w/\+]+", all_text)
 31            if rdevices:
 32                break
 33
 34        # Split combo strings into individual devices
 35        devices = []
 36        for device in list(set(rdevices)):
 37            if len(parts := device.split("/")) >= 2:
 38                base = parts[0]
 39                devices.append(base)
 40                base = base[: -len(parts[1])]
 41                for part in parts[1:]:
 42                    devices.append(base + part)
 43            else:
 44                devices.append(device)
 45
 46        # Remove non-specific mentions: shortest subset
 47        return list(sorted(set(devices)))
 48
 49    @property
 50    def device_filters(self) -> list[str]:
 51        # Match STM32L4+ with STM32[SRQP], since STM32L4Axx is STM32L4 (without +)
 52        return [d.replace("x", ".").replace("+", r"[SRQP]").lower() for d in self.devices]
 53
 54    def filter_devices(self, devices):
 55        dids = set()
 56        for did in devices:
 57            for device in self.device_filters:
 58                if re.match(device, did.string):
 59                    dids.add(did)
 60                    break
 61        return list(dids)
 62
 63    @cached_property
 64    def flash_latencies(self):
 65        # FLASH peripheral is not described in the reference manual because... reasons?
 66        if "STM32F100xx" in self.devices:
 67            return {"": {1800: [24]}}
 68        if any(d.startswith("STM32F1") for d in self.devices):
 69            return {"": {1800: [24, 48, 72]}}
 70        # Tables too complicated to parse
 71        if any(d.startswith("STM32L1") for d in self.devices):
 72            # See Table 13 and Table 26
 73            return {
 74                "stm32l1...[68b]": {  # Cat 1
 75                    1800: [16, 32],
 76                    1500: [8, 16],
 77                    1200: [2.1, 4.2],
 78                },
 79                "": {  # Cat 2,3,4,5,6
 80                    1800: [16, 32],
 81                    1500: [8, 16],
 82                    1200: [4.2, 8],
 83                },
 84            }
 85
 86        if any(d.startswith("STM32F0") for d in self.devices) or any(d.startswith("STM32F3") for d in self.devices):
 87            # Attempt to find the FLASH_ACR register and look at the LATENCY descriptions
 88            chapter = self.chapter(r"flash")
 89            headings = chapter.heading_tables(r"\(FLASH_ACR\)")
 90            if not headings:
 91                raise KeyError(f"Cannot find FLASH_ACR section in '{chapter._relpath}'")
 92            bit_descr_table = headings[0][1][1]
 93            cell = bit_descr_table.cell(1, -1)
 94            matches = list(map(int, re.findall(r"CLK +≤ +(\d+) +MHz", cell.text())))
 95            return {1800: matches}
 96
 97        # Derive from the wait states table
 98        if any(d.startswith("STM32F2") for d in self.devices):
 99            chapter = self.chapter(r"memory and bus")
100        else:
101            chapter = self.chapters(r"flash")[0]
102
103        if tables := chapter.tables("power +range"):
104            table_data = defaultdict(list)
105            for row in tables[0].cell_rows():
106                min_voltage = re.search(r"(\d.\d+).+?(\d.\d+)", row.match_value("power +range")[0].text()).group(1)
107                min_voltage = int(float(min_voltage) * 1000)
108                for wait_state in row.match_keys("wait +state"):
109                    max_frequency = float(re.search(r"(.*?) +MHz", row[wait_state][0].text()).group(1))
110                    table_data[min_voltage].append(max_frequency)
111            return {"": {k: sorted(v) for k, v in table_data.items()}}
112
113        ws_tables = {}
114        for table in chapter.tables(r"wait states"):
115            table_data = defaultdict(list)
116            for row in table.cell_rows():
117                for vkey in row.match_keys("voltage|range"):
118                    if (vrange := re.search(r"(\d.\d+).+?(\d.\d+) *?V", vkey)) is not None:
119                        min_voltage = int(float(vrange.group(1)) * 1000)
120                    else:
121                        vrange = re.search(r"Range(\d[bn]?)", vkey.replace(" ", ""))
122                        min_voltage = {"0": 1280, "1b": 1280, "1n": 1200, "1": 1200, "2": 1000}[vrange.group(1)]
123                    max_frequency = row[vkey][0].text(
124                        **{r"-": "", r".*?CLK.*?(\d+).*": r"\1", r".*?;(\d+) *MHz.*": r"\1", r".*?(\d+).*": r"\1"}
125                    )
126                    if max_frequency:
127                        table_data[min_voltage].append(float(max_frequency))
128            dfilter = device_filter_from(table.caption())
129            assert table_data
130            ws_tables[dfilter] = {v: list(sorted(set(f))) for v, f in table_data.items()}
131
132        # print(ws_tables)
133        assert ws_tables
134        return ws_tables
135
136    @cached_property
137    def vector_tables(self):
138        name_replace = {
139            "p": r"\1,",
140            r" +": "",
141            r"\(.*?\)|_IRQn|_$|^-$|[Rr]eserved": "",
142            r"\+|and": ",",
143            r"_(\w+)(LSE|BLE|I2C\d)_": r"_\1,\2_",
144            r"EXTI\[(\d+):(\d+)\]": r"EXTI\1_\2",
145            r"\[(\d+):(\d+)\]": r"\2_\1",
146        }
147        capt_replace = {
148            "br": " ",
149            r" +": " ",
150            r"\((Cat\.\d.*?devices)\)": r"\1",
151            r"\(.*?\)": "",
152            r"for +connectivity +line +devices": "for STM32F105/7",
153            r"for +XL\-density +devices": "for STM32F10xxF/G",
154            r"for +other +STM32F10xxx +devices": "for the rest",
155        }
156
157        chapter = next(c for c in self.chapters(r"nvic|interrupt") if "exti" not in c.name)
158        tables = chapter.tables(r"vector +table|list +of +vector|NVIC|CPU")
159        assert tables
160
161        vtables = {}
162        for table in tables:
163            caption = table.caption(**capt_replace)
164            table_name = "VectorTable"
165            if len(tables) > 1:
166                # Create the right table filter
167                if (core := re.search(r"CPU(\d)|NVIC(\d)", caption)) is not None:
168                    table_name += f":Core={core.group(1)}"  # Multi-core device
169                elif devices := device_filter_from(caption):
170                    table_name += f":Device={devices}"
171                elif categories := re.findall(r"Cat\.(\d)", caption):
172                    # Size category filter
173                    categories = "|".join(categories)
174                    table_name += f":Categories={categories}"
175
176            vtable = defaultdict(set)
177            for pos, values in table.domains("position").cells("acro").items():
178                if pos.isnumeric() and (name := values.match_value("acro")[0].text(**name_replace)):
179                    vtable[int(pos)].update(html.listify(name))
180            vtables[table_name] = dict(vtable)
181
182        return vtables
183
184    @cache
185    def peripheral_maps(self, chapter, assert_table=True):
186        off_replace = {r" +": "", "0x000x00": "0x00", "to": "-", "×": "*", r"\(\d+\)": ""}
187        dom_replace = {r"Register +size": "Bit position"}  # noqa: F841
188        reg_replace = {
189            r" +|\.+": "",
190            r"\(COM(\d)\)": r"_COM\1",
191            r"^[Rr]es$||0x[\da-fA-FXx]+|\(.*?\)|-": "",
192            r"(?i)reserved|resetvalue.*": "",
193            "enabled": "_EN",
194            "disabled": "_DIS",
195            r"(?i)Outputcomparemode": "_Output",
196            "(?i)Inputcapturemode": "_Input",
197            "mode": "",
198            r"^TG_FS_": "OTG_FS_",
199            "toRTC": "RTC",
200            "SPI2S_": "SPI_",
201            r"andTIM\d+_.*": "",
202            r"x=[\d,]+": "",
203        }
204        fld_replace = {
205            r"\] +\d+(th|rd|nd|st)": "]",
206            r" +|\.+|\[.*?\]|\[?\d+:\d+\]?|\(.*?\)|-|^[\dXx]+$|%|__|:0\]": "",
207            r"Dataregister|Independentdataregister": "DATA",
208            r"Framefilterreg0.*": "FRAME_FILTER_REG",
209            r"[Rr]es(erved)?|[Rr]egular|x_x(bits)?|NotAvailable|RefertoSection\d+:Comparator": "",
210            r"Sampletimebits|Injectedchannelsequence|channelsequence|conversioninregularsequencebits": "",
211            r"conversioninsequencebits|conversionininjectedsequencebits|or|first|second|third|fourth": "",
212        }
213        bit_replace = {r".*:": ""}
214        glo_replace = {r"[Rr]eserved": ""}
215
216        print(chapter._relpath)
217        tables = chapter.tables(r"register +map")
218        if assert_table:
219            assert tables
220
221        peripheral_data = {}
222        for table in tables:
223            caption = table.caption()
224            if any(n in caption for n in ["DFIFO", "global", "EXTI register map section", "vs swapping option"]):
225                continue
226            heading = table._heading.text(**{r"((\d+\.)+(\d+)?).*": r"\1"})
227            print(table, caption)
228
229            register_data = {}
230            for row in table.cell_rows():
231                rkey = next(k for k in row.match_keys("register") if "size" not in k)
232                register = row[rkey][0].text(**reg_replace).strip()
233                if not register:
234                    continue
235                offset = row.match_value(r"off-?set|addr")[0].text(**off_replace)
236                if not offset:
237                    continue
238                field_data = {}
239                for bits in row.match_keys(r"^[\d-]+$|.*?:[\d-]+$"):
240                    field = row[bits][0].text(**fld_replace).strip()
241                    if not field:
242                        continue
243                    bits = sorted(html.listify(html.replace(bits, **bit_replace), r"-"))
244                    if len(bits) == 2:
245                        bits = range(int(bits[0]), int(bits[1]))
246                    for bit in bits:
247                        bit = int(bit)
248                        field_data[bit] = field
249                        # print(f"{offset:>10} {register:60} {bit:>2} {field}")
250                register_data[register] = (offset, field_data)
251            assert register_data
252            peripheral_data[caption] = (heading, register_data)
253
254        if peripheral_data and all("HRTIM" in ca for ca in peripheral_data):
255            caption, heading = next((c, p) for c, (p, _) in peripheral_data.items())
256            all_registers = {k: v for (_, values) in peripheral_data.values() for k, v in values.items()}
257            peripheral_data = {caption: (heading, all_registers)}
258
259        instance_offsets = {}
260        if tables := chapter.tables(r"ADC +global +register +map"):
261            for row in tables[0].cell_rows():
262                if ifilter := row.match_value("register")[0].text(**glo_replace):
263                    offset = int(row.match_value("offset")[0].text().split("-")[0], 16)
264                    for instance in re.findall(r"ADC(\d+)", ifilter):
265                        instance_offsets[f"ADC[{instance}]"] = offset
266
267        return peripheral_data, instance_offsets
268
269    @cached_property
270    def peripherals(self):
271        per_replace = {
272            r" +": "",
273            r".*?\(([A-Z]+|DMA2D)\).*?": r"\1",
274            r"Reserved|Port|Power|Registers|Reset|\(.*?\)|_REG": "",
275            r"(\d)/I2S\d": r"\1",
276            r"/I2S|CANMessageRAM|Cortex-M4|I2S\dext|^GPV$": "",
277            r"Ethernet": "ETH",
278            r"Flash": "FLASH",
279            r"(?i).*ETHERNET.*": "ETH",
280            r"(?i)Firewall": "FW",
281            r"HDMI-|": "",
282            "SPDIF-RX": "SPDIFRX",
283            r"SPI2S2": "SPI2",
284            "Tamper": "TAMP",
285            "TT-FDCAN": "FDCAN",
286            r"USBOTG([FH])S": r"USB_OTG_\1S",
287            "LCD-TFT": "LTDC",
288            "DSIHOST": "DSI",
289            "TIMER": "TIM",
290            r"^VREF$": "VREFBUF",
291            "DelayBlock": "DLYB",
292            "I/O": "",
293            "DAC1/2": "DAC12",
294            r"[a-z]": "",
295        }
296        adr_replace = {r" |\(\d\)": ""}
297        sct_replace = {r"-": ""}
298        hdr_replace = {r"Peripheral +register +map": "map"}
299        bus_replace = {r".*(A\wB\d).*": r"\1", "-": ""}
300
301        # RM0431 has a bug where the peripheral table is in chapter 1
302        if "RM0431" in self.name:
303            chapters = self.chapters(r"chapter 1 ")
304        else:
305            chapters = self.chapters(r"memory +and +bus|memory +overview")
306        assert chapters
307        print(chapters[0]._relpath)
308
309        tables = chapters[0].tables(r"register +boundary")
310        assert tables
311
312        peripherals = defaultdict(list)
313        for table in tables:
314            print(table.caption())
315            for row in table.cell_rows(**hdr_replace):
316                regmaps = row.match_value("map")
317                if regmaps:
318                    regmap = regmaps[0].text(**sct_replace).strip()
319                    sections = re.findall(r"Section +(\d+\.\d+(\.\d+)?)", regmap)
320                    if not sections:
321                        continue
322                    sections = [s[0] for s in sections]
323                else:
324                    sections = []
325                names = html.listify(row.match_value("peri")[0].text(**per_replace), r"[-\&\+/,]")
326                if not names:
327                    continue
328                address = row.match_value("address")[0].text(**adr_replace)
329                address_min = int(address.split("-")[0], 16)
330                address_max = int(address.split("-")[1], 16)
331                bus = row.match_value("bus")[0].text(**bus_replace).strip()
332                peripherals[table.caption()].append((names, address_min, address_max, bus, sections))
333                print(
334                    f"{','.join(names):20} @[{hex(address_min)}, {hex(address_max)}] {bus:4} -> {', '.join(sections)}"
335                )
336
337        return peripherals
ReferenceManual(path: str)
14    def __init__(self, path: str):
15        super().__init__(path)
devices: list[str]
20    @cached_property
21    def devices(self) -> list[str]:
22        # Find all occurrences of STM32* strings in the first chapter
23        chapter = self.chapter("chapter 0")
24        for heading, texts in chapter.heading_texts():
25            all_text = ""
26            for text in texts:
27                all_text += text.text(br=" ")
28            # print(heading, all_text)
29            # Must also match "STM32L4+" !!!
30            rdevices = re.findall(r"STM32[\w/\+]+", all_text)
31            if rdevices:
32                break
33
34        # Split combo strings into individual devices
35        devices = []
36        for device in list(set(rdevices)):
37            if len(parts := device.split("/")) >= 2:
38                base = parts[0]
39                devices.append(base)
40                base = base[: -len(parts[1])]
41                for part in parts[1:]:
42                    devices.append(base + part)
43            else:
44                devices.append(device)
45
46        # Remove non-specific mentions: shortest subset
47        return list(sorted(set(devices)))
device_filters: list[str]
49    @property
50    def device_filters(self) -> list[str]:
51        # Match STM32L4+ with STM32[SRQP], since STM32L4Axx is STM32L4 (without +)
52        return [d.replace("x", ".").replace("+", r"[SRQP]").lower() for d in self.devices]
def filter_devices(self, devices):
54    def filter_devices(self, devices):
55        dids = set()
56        for did in devices:
57            for device in self.device_filters:
58                if re.match(device, did.string):
59                    dids.add(did)
60                    break
61        return list(dids)
flash_latencies
 63    @cached_property
 64    def flash_latencies(self):
 65        # FLASH peripheral is not described in the reference manual because... reasons?
 66        if "STM32F100xx" in self.devices:
 67            return {"": {1800: [24]}}
 68        if any(d.startswith("STM32F1") for d in self.devices):
 69            return {"": {1800: [24, 48, 72]}}
 70        # Tables too complicated to parse
 71        if any(d.startswith("STM32L1") for d in self.devices):
 72            # See Table 13 and Table 26
 73            return {
 74                "stm32l1...[68b]": {  # Cat 1
 75                    1800: [16, 32],
 76                    1500: [8, 16],
 77                    1200: [2.1, 4.2],
 78                },
 79                "": {  # Cat 2,3,4,5,6
 80                    1800: [16, 32],
 81                    1500: [8, 16],
 82                    1200: [4.2, 8],
 83                },
 84            }
 85
 86        if any(d.startswith("STM32F0") for d in self.devices) or any(d.startswith("STM32F3") for d in self.devices):
 87            # Attempt to find the FLASH_ACR register and look at the LATENCY descriptions
 88            chapter = self.chapter(r"flash")
 89            headings = chapter.heading_tables(r"\(FLASH_ACR\)")
 90            if not headings:
 91                raise KeyError(f"Cannot find FLASH_ACR section in '{chapter._relpath}'")
 92            bit_descr_table = headings[0][1][1]
 93            cell = bit_descr_table.cell(1, -1)
 94            matches = list(map(int, re.findall(r"CLK +≤ +(\d+) +MHz", cell.text())))
 95            return {1800: matches}
 96
 97        # Derive from the wait states table
 98        if any(d.startswith("STM32F2") for d in self.devices):
 99            chapter = self.chapter(r"memory and bus")
100        else:
101            chapter = self.chapters(r"flash")[0]
102
103        if tables := chapter.tables("power +range"):
104            table_data = defaultdict(list)
105            for row in tables[0].cell_rows():
106                min_voltage = re.search(r"(\d.\d+).+?(\d.\d+)", row.match_value("power +range")[0].text()).group(1)
107                min_voltage = int(float(min_voltage) * 1000)
108                for wait_state in row.match_keys("wait +state"):
109                    max_frequency = float(re.search(r"(.*?) +MHz", row[wait_state][0].text()).group(1))
110                    table_data[min_voltage].append(max_frequency)
111            return {"": {k: sorted(v) for k, v in table_data.items()}}
112
113        ws_tables = {}
114        for table in chapter.tables(r"wait states"):
115            table_data = defaultdict(list)
116            for row in table.cell_rows():
117                for vkey in row.match_keys("voltage|range"):
118                    if (vrange := re.search(r"(\d.\d+).+?(\d.\d+) *?V", vkey)) is not None:
119                        min_voltage = int(float(vrange.group(1)) * 1000)
120                    else:
121                        vrange = re.search(r"Range(\d[bn]?)", vkey.replace(" ", ""))
122                        min_voltage = {"0": 1280, "1b": 1280, "1n": 1200, "1": 1200, "2": 1000}[vrange.group(1)]
123                    max_frequency = row[vkey][0].text(
124                        **{r"-": "", r".*?CLK.*?(\d+).*": r"\1", r".*?;(\d+) *MHz.*": r"\1", r".*?(\d+).*": r"\1"}
125                    )
126                    if max_frequency:
127                        table_data[min_voltage].append(float(max_frequency))
128            dfilter = device_filter_from(table.caption())
129            assert table_data
130            ws_tables[dfilter] = {v: list(sorted(set(f))) for v, f in table_data.items()}
131
132        # print(ws_tables)
133        assert ws_tables
134        return ws_tables
vector_tables
136    @cached_property
137    def vector_tables(self):
138        name_replace = {
139            "p": r"\1,",
140            r" +": "",
141            r"\(.*?\)|_IRQn|_$|^-$|[Rr]eserved": "",
142            r"\+|and": ",",
143            r"_(\w+)(LSE|BLE|I2C\d)_": r"_\1,\2_",
144            r"EXTI\[(\d+):(\d+)\]": r"EXTI\1_\2",
145            r"\[(\d+):(\d+)\]": r"\2_\1",
146        }
147        capt_replace = {
148            "br": " ",
149            r" +": " ",
150            r"\((Cat\.\d.*?devices)\)": r"\1",
151            r"\(.*?\)": "",
152            r"for +connectivity +line +devices": "for STM32F105/7",
153            r"for +XL\-density +devices": "for STM32F10xxF/G",
154            r"for +other +STM32F10xxx +devices": "for the rest",
155        }
156
157        chapter = next(c for c in self.chapters(r"nvic|interrupt") if "exti" not in c.name)
158        tables = chapter.tables(r"vector +table|list +of +vector|NVIC|CPU")
159        assert tables
160
161        vtables = {}
162        for table in tables:
163            caption = table.caption(**capt_replace)
164            table_name = "VectorTable"
165            if len(tables) > 1:
166                # Create the right table filter
167                if (core := re.search(r"CPU(\d)|NVIC(\d)", caption)) is not None:
168                    table_name += f":Core={core.group(1)}"  # Multi-core device
169                elif devices := device_filter_from(caption):
170                    table_name += f":Device={devices}"
171                elif categories := re.findall(r"Cat\.(\d)", caption):
172                    # Size category filter
173                    categories = "|".join(categories)
174                    table_name += f":Categories={categories}"
175
176            vtable = defaultdict(set)
177            for pos, values in table.domains("position").cells("acro").items():
178                if pos.isnumeric() and (name := values.match_value("acro")[0].text(**name_replace)):
179                    vtable[int(pos)].update(html.listify(name))
180            vtables[table_name] = dict(vtable)
181
182        return vtables
@cache
def peripheral_maps(self, chapter, assert_table=True):
184    @cache
185    def peripheral_maps(self, chapter, assert_table=True):
186        off_replace = {r" +": "", "0x000x00": "0x00", "to": "-", "×": "*", r"\(\d+\)": ""}
187        dom_replace = {r"Register +size": "Bit position"}  # noqa: F841
188        reg_replace = {
189            r" +|\.+": "",
190            r"\(COM(\d)\)": r"_COM\1",
191            r"^[Rr]es$||0x[\da-fA-FXx]+|\(.*?\)|-": "",
192            r"(?i)reserved|resetvalue.*": "",
193            "enabled": "_EN",
194            "disabled": "_DIS",
195            r"(?i)Outputcomparemode": "_Output",
196            "(?i)Inputcapturemode": "_Input",
197            "mode": "",
198            r"^TG_FS_": "OTG_FS_",
199            "toRTC": "RTC",
200            "SPI2S_": "SPI_",
201            r"andTIM\d+_.*": "",
202            r"x=[\d,]+": "",
203        }
204        fld_replace = {
205            r"\] +\d+(th|rd|nd|st)": "]",
206            r" +|\.+|\[.*?\]|\[?\d+:\d+\]?|\(.*?\)|-|^[\dXx]+$|%|__|:0\]": "",
207            r"Dataregister|Independentdataregister": "DATA",
208            r"Framefilterreg0.*": "FRAME_FILTER_REG",
209            r"[Rr]es(erved)?|[Rr]egular|x_x(bits)?|NotAvailable|RefertoSection\d+:Comparator": "",
210            r"Sampletimebits|Injectedchannelsequence|channelsequence|conversioninregularsequencebits": "",
211            r"conversioninsequencebits|conversionininjectedsequencebits|or|first|second|third|fourth": "",
212        }
213        bit_replace = {r".*:": ""}
214        glo_replace = {r"[Rr]eserved": ""}
215
216        print(chapter._relpath)
217        tables = chapter.tables(r"register +map")
218        if assert_table:
219            assert tables
220
221        peripheral_data = {}
222        for table in tables:
223            caption = table.caption()
224            if any(n in caption for n in ["DFIFO", "global", "EXTI register map section", "vs swapping option"]):
225                continue
226            heading = table._heading.text(**{r"((\d+\.)+(\d+)?).*": r"\1"})
227            print(table, caption)
228
229            register_data = {}
230            for row in table.cell_rows():
231                rkey = next(k for k in row.match_keys("register") if "size" not in k)
232                register = row[rkey][0].text(**reg_replace).strip()
233                if not register:
234                    continue
235                offset = row.match_value(r"off-?set|addr")[0].text(**off_replace)
236                if not offset:
237                    continue
238                field_data = {}
239                for bits in row.match_keys(r"^[\d-]+$|.*?:[\d-]+$"):
240                    field = row[bits][0].text(**fld_replace).strip()
241                    if not field:
242                        continue
243                    bits = sorted(html.listify(html.replace(bits, **bit_replace), r"-"))
244                    if len(bits) == 2:
245                        bits = range(int(bits[0]), int(bits[1]))
246                    for bit in bits:
247                        bit = int(bit)
248                        field_data[bit] = field
249                        # print(f"{offset:>10} {register:60} {bit:>2} {field}")
250                register_data[register] = (offset, field_data)
251            assert register_data
252            peripheral_data[caption] = (heading, register_data)
253
254        if peripheral_data and all("HRTIM" in ca for ca in peripheral_data):
255            caption, heading = next((c, p) for c, (p, _) in peripheral_data.items())
256            all_registers = {k: v for (_, values) in peripheral_data.values() for k, v in values.items()}
257            peripheral_data = {caption: (heading, all_registers)}
258
259        instance_offsets = {}
260        if tables := chapter.tables(r"ADC +global +register +map"):
261            for row in tables[0].cell_rows():
262                if ifilter := row.match_value("register")[0].text(**glo_replace):
263                    offset = int(row.match_value("offset")[0].text().split("-")[0], 16)
264                    for instance in re.findall(r"ADC(\d+)", ifilter):
265                        instance_offsets[f"ADC[{instance}]"] = offset
266
267        return peripheral_data, instance_offsets
peripherals
269    @cached_property
270    def peripherals(self):
271        per_replace = {
272            r" +": "",
273            r".*?\(([A-Z]+|DMA2D)\).*?": r"\1",
274            r"Reserved|Port|Power|Registers|Reset|\(.*?\)|_REG": "",
275            r"(\d)/I2S\d": r"\1",
276            r"/I2S|CANMessageRAM|Cortex-M4|I2S\dext|^GPV$": "",
277            r"Ethernet": "ETH",
278            r"Flash": "FLASH",
279            r"(?i).*ETHERNET.*": "ETH",
280            r"(?i)Firewall": "FW",
281            r"HDMI-|": "",
282            "SPDIF-RX": "SPDIFRX",
283            r"SPI2S2": "SPI2",
284            "Tamper": "TAMP",
285            "TT-FDCAN": "FDCAN",
286            r"USBOTG([FH])S": r"USB_OTG_\1S",
287            "LCD-TFT": "LTDC",
288            "DSIHOST": "DSI",
289            "TIMER": "TIM",
290            r"^VREF$": "VREFBUF",
291            "DelayBlock": "DLYB",
292            "I/O": "",
293            "DAC1/2": "DAC12",
294            r"[a-z]": "",
295        }
296        adr_replace = {r" |\(\d\)": ""}
297        sct_replace = {r"-": ""}
298        hdr_replace = {r"Peripheral +register +map": "map"}
299        bus_replace = {r".*(A\wB\d).*": r"\1", "-": ""}
300
301        # RM0431 has a bug where the peripheral table is in chapter 1
302        if "RM0431" in self.name:
303            chapters = self.chapters(r"chapter 1 ")
304        else:
305            chapters = self.chapters(r"memory +and +bus|memory +overview")
306        assert chapters
307        print(chapters[0]._relpath)
308
309        tables = chapters[0].tables(r"register +boundary")
310        assert tables
311
312        peripherals = defaultdict(list)
313        for table in tables:
314            print(table.caption())
315            for row in table.cell_rows(**hdr_replace):
316                regmaps = row.match_value("map")
317                if regmaps:
318                    regmap = regmaps[0].text(**sct_replace).strip()
319                    sections = re.findall(r"Section +(\d+\.\d+(\.\d+)?)", regmap)
320                    if not sections:
321                        continue
322                    sections = [s[0] for s in sections]
323                else:
324                    sections = []
325                names = html.listify(row.match_value("peri")[0].text(**per_replace), r"[-\&\+/,]")
326                if not names:
327                    continue
328                address = row.match_value("address")[0].text(**adr_replace)
329                address_min = int(address.split("-")[0], 16)
330                address_max = int(address.split("-")[1], 16)
331                bus = row.match_value("bus")[0].text(**bus_replace).strip()
332                peripherals[table.caption()].append((names, address_min, address_max, bus, sections))
333                print(
334                    f"{','.join(names):20} @[{hex(address_min)}, {hex(address_max)}] {bus:4} -> {', '.join(sections)}"
335                )
336
337        return peripherals
Inherited Members
modm_data.html.document.Document
path
relpath
fullname
name
version
path_pdf
chapters
chapter
def load_documents() -> list:
20def load_documents() -> list:
21    documents = defaultdict(dict)
22    for path in sorted(ext_path("stmicro/html").glob("*-v*")):
23        # This doc is parsed wrongly since it has a DRAFT background
24        if "DS12960-v5" in path.stem:
25            continue
26        # This doc has a preliminary ordering information STM32WBA52CGU6TR
27        if "DS14127" in path.stem:
28            continue
29        doc = Document(path)
30        if "DS" in doc.name and (chap := doc.chapters("chapter 0")):
31            # FIXME: Better detection that DS13252 is a STM32WB55 module, not a chip!
32            if (
33                any("STM32" in h.html for h in chap[0].headings())
34                and "DS13252" not in doc.name
35                and "DS14096" not in doc.name
36            ):
37                documents[doc.name][doc.version] = DatasheetStm32(path)
38            else:
39                documents[doc.name][doc.version] = DatasheetSensor(path)
40        elif "RM" in doc.name:
41            documents[doc.name][doc.version] = ReferenceManual(path)
42    return documents
def load_document_devices( use_cached=True) -> tuple[dict[modm_data.owl.DeviceIdentifier, DatasheetStm32], dict[modm_data.owl.DeviceIdentifier, ReferenceManual]]:
 45def load_document_devices(
 46    use_cached=True,
 47) -> tuple[dict[DeviceIdentifier, DatasheetStm32], dict[DeviceIdentifier, ReferenceManual]]:
 48    global DOCUMENT_CACHE
 49    if DOCUMENT_CACHE is not None:
 50        return DOCUMENT_CACHE
 51
 52    global MAP_DEVICE_DOC_FILE
 53    if MAP_DEVICE_DOC_FILE.exists() and use_cached:
 54        with MAP_DEVICE_DOC_FILE.open("r", encoding="utf-8") as fh:
 55            json_data = json.load(fh)
 56
 57        docs = {}
 58        for path in set(json_data["ds"].values()):
 59            docs[path] = DatasheetStm32(path)
 60        for path in set(json_data["rm"].values()):
 61            docs[path] = ReferenceManual(path)
 62        datasheets = {did_from_string(did): docs[path] for did, path in json_data["ds"].items()}
 63        reference_manuals = {did_from_string(did): docs[path] for did, path in json_data["rm"].items()}
 64    else:
 65        dss = defaultdict(set)
 66        rms = defaultdict(set)
 67        for name, versions in load_documents().items():
 68            # Always choose the latest version
 69            doc = list(versions.values())[-1]
 70            # print(doc.path_pdf.relative_to(Path().cwd()), doc.path.relative_to(Path().cwd()))
 71            # print(doc.devices)
 72            if isinstance(doc, DatasheetStm32):
 73                if not doc.devices:
 74                    raise ValueError(f"{doc} has no associated devices!")
 75                for dev in doc.devices:
 76                    dss[dev].add(doc)
 77            elif isinstance(doc, ReferenceManual):
 78                if not doc.devices:
 79                    raise ValueError(f"{doc} has no associated devices!")
 80                for dev in doc.devices:
 81                    rms[dev].add(doc)
 82
 83        for dev, docs in dss.items():
 84            if len(docs) != 1:
 85                raise ValueError(f"One device points to multiple datasheets! {dev} -> {docs}")
 86        datasheets = {did: list(ds)[0] for did, ds in dss.items()}
 87        # print(len(datasheets.keys()), sorted(list(d.string for d in datasheets.keys())))
 88
 89        manuals = defaultdict(set)
 90        for dev, docs in rms.items():
 91            if len(docs) != 1:
 92                raise ValueError(f"One device points to multiple reference manuals! {dev} -> {docs}")
 93            for dev in list(docs)[0].filter_devices(datasheets.keys()):
 94                manuals[dev].add(list(docs)[0])
 95
 96        for dev, docs in manuals.items():
 97            if len(docs) != 1:
 98                raise ValueError(f"One device points to multiple reference manuals! {dev} -> {docs}")
 99
100        reference_manuals = {did: list(rm)[0] for did, rm in manuals.items()}
101
102        # Cache the results for the next call
103        json_data = {
104            "ds": {did.string: str(doc.path) for did, doc in datasheets.items()},
105            "rm": {did.string: str(doc.path) for did, doc in reference_manuals.items()},
106        }
107        MAP_DEVICE_DOC_FILE.parent.mkdir(parents=True, exist_ok=True)
108        with MAP_DEVICE_DOC_FILE.open("w", encoding="utf-8") as fh:
109            json.dump(json_data, fh, indent=4)
110
111    DOCUMENT_CACHE = (datasheets, reference_manuals)
112    return datasheets, reference_manuals
def datasheet_for_device( did: modm_data.owl.DeviceIdentifier) -> DatasheetStm32:
129def datasheet_for_device(did: DeviceIdentifier) -> DatasheetStm32:
130    datasheets, _ = load_document_devices()
131    return _document_for_device(did, datasheets)
def reference_manual_for_device( did: modm_data.owl.DeviceIdentifier) -> ReferenceManual:
134def reference_manual_for_device(did: DeviceIdentifier) -> ReferenceManual:
135    _, reference_manual = load_document_devices()
136    return _document_for_device(did, reference_manual)
def find_device_filter(did, device_filters):
35def find_device_filter(did, device_filters):
36    for dfilter in list(sorted(device_filters, key=lambda d: -len(d))):
37        if re.match(dfilter, did.string) or dfilter == "":
38            return dfilter
39    return None