modm_data.html.stmicro.reference

  1# Copyright 2022, Niklas Hauser
  2# SPDX-License-Identifier: MPL-2.0
  3
  4import re
  5from functools import cached_property, cache
  6from collections import defaultdict
  7import modm_data.html as html
  8from .helper import split_device_filter, device_filter_from
  9
 10
 11class ReferenceManual(html.Document):
 12    def __init__(self, path: str):
 13        super().__init__(path)
 14
 15    def __repr__(self) -> str:
 16        return f"RM({self.fullname})"
 17
 18    @cached_property
 19    def devices(self) -> list[str]:
 20        # Find all occurrences of STM32* strings in the first chapter
 21        chapter = self.chapter("chapter 0")
 22        for heading, texts in chapter.heading_texts():
 23            all_text = ""
 24            for text in texts:
 25                all_text += text.text(br=" ")
 26            # print(heading, all_text)
 27            # Must also match "STM32L4+" !!!
 28            rdevices = re.findall(r"STM32[\w/\+]+", all_text)
 29            if rdevices:
 30                break
 31
 32        # Split combo strings into individual devices
 33        devices = []
 34        for device in list(set(rdevices)):
 35            if len(parts := device.split("/")) >= 2:
 36                base = parts[0]
 37                devices.append(base)
 38                base = base[:-len(parts[1])]
 39                for part in parts[1:]:
 40                    devices.append(base + part)
 41            else:
 42                devices.append(device)
 43
 44        # Remove non-specific mentions: shortest subset
 45        return list(sorted(set(devices)))
 46
 47    @property
 48    def device_filters(self) -> list[str]:
 49        # Match STM32L4+ with STM32[SRQP], since STM32L4Axx is STM32L4 (without +)
 50        return [d.replace("x", ".").replace("+", r"[SRQP]").lower() for d in self.devices]
 51
 52    def filter_devices(self, devices):
 53        dids = set()
 54        for did in devices:
 55            for device in self.device_filters:
 56                if re.match(device, did.string):
 57                    dids.add(did)
 58                    break
 59        return list(dids)
 60
 61    @cached_property
 62    def flash_latencies(self):
 63        # FLASH peripheral is not described in the reference manual because... reasons?
 64        if "STM32F100xx" in self.devices:
 65            return {"": {1800: [24]}}
 66        if any(d.startswith("STM32F1") for d in self.devices):
 67            return {"": {1800: [24, 48, 72]}}
 68        # Tables too complicated to parse
 69        if any(d.startswith("STM32L1") for d in self.devices):
 70            # See Table 13 and Table 26
 71            return {
 72                "stm32l1...[68b]": { # Cat 1
 73                    1800: [16, 32],
 74                    1500: [8, 16],
 75                    1200: [2.1, 4.2]
 76                },
 77                "": { # Cat 2,3,4,5,6
 78                    1800: [16, 32],
 79                    1500: [8, 16],
 80                    1200: [4.2, 8]
 81                }
 82            }
 83
 84        if (any(d.startswith("STM32F0") for d in self.devices) or
 85            any(d.startswith("STM32F3") for d in self.devices)):
 86            # Attempt to find the FLASH_ACR register and look at the LATENCY descriptions
 87            chapter = self.chapter(r"flash")
 88            headings = chapter.heading_tables(r"\(FLASH_ACR\)")
 89            if not headings:
 90                raise KeyError(f"Cannot find FLASH_ACR section in '{chapter._relpath}'")
 91            bit_descr_table = headings[0][1][1]
 92            cell = bit_descr_table.cell(1, -1)
 93            matches = list(map(int, re.findall(r"CLK +≤ +(\d+) +MHz", cell.text())))
 94            return {1800: matches}
 95
 96        # Derive from the wait states table
 97        if any(d.startswith("STM32F2") for d in self.devices):
 98            chapter = self.chapter(r"memory and bus")
 99        else:
100            chapter = self.chapters(r"flash")[0]
101
102        if tables := chapter.tables("power +range"):
103            table_data = defaultdict(list)
104            for row in tables[0].cell_rows():
105                min_voltage = re.search(r"(\d.\d+).+?(\d.\d+)", row.match_value("power +range")[0].text()).group(1)
106                min_voltage = int(float(min_voltage) * 1000)
107                for wait_state in row.match_keys("wait +state"):
108                    max_frequency = float(re.search(r"(.*?) +MHz", row[wait_state][0].text()).group(1))
109                    table_data[min_voltage].append(max_frequency)
110            return {"": {k:sorted(v) for k,v in table_data.items()}}
111
112        ws_tables = {}
113        for table in chapter.tables(r"wait states"):
114            table_data = defaultdict(list)
115            for row in table.cell_rows():
116                for vkey in row.match_keys("voltage|range"):
117                    if (vrange := re.search(r"(\d.\d+).+?(\d.\d+) *?V", vkey)) is not None:
118                        min_voltage = int(float(vrange.group(1)) * 1000)
119                    else:
120                        vrange = re.search(r"Range(\d[bn]?)", vkey.replace(" ", ""))
121                        min_voltage = {"0": 1280, "1b": 1280,
122                                       "1n": 1200, "1": 1200,
123                                       "2": 1000}[vrange.group(1)]
124                    max_frequency = row[vkey][0].text(
125                            **{r"-": "", r".*?CLK.*?(\d+).*": r"\1",
126                               r".*?;(\d+) *MHz.*": r"\1", r".*?(\d+).*": r"\1"})
127                    if max_frequency:
128                        table_data[min_voltage].append(float(max_frequency))
129            dfilter = device_filter_from(table.caption())
130            assert table_data
131            ws_tables[dfilter] = {v: list(sorted(set(f))) for v, f in table_data.items()}
132
133        # print(ws_tables)
134        assert ws_tables
135        return ws_tables
136
137
138
139    @cached_property
140    def vector_tables(self):
141        name_replace = {"p": r"\1,", r" +": "", r"\(.*?\)|_IRQn|_$|^-$|[Rr]eserved": "",
142                        r"\+|and": ",", r"_(\w+)(LSE|BLE|I2C\d)_": r"_\1,\2_",
143                        r"EXTI\[(\d+):(\d+)\]": r"EXTI\1_\2", r"\[(\d+):(\d+)\]": r"\2_\1"}
144        capt_replace = {"br": " ", r" +": " ", r"\((Cat\.\d.*?devices)\)": r"\1", r"\(.*?\)": "",
145                        r"for +connectivity +line +devices": "for STM32F105/7",
146                        r"for +XL\-density +devices": "for STM32F10xxF/G",
147                        r"for +other +STM32F10xxx +devices": "for the rest",}
148
149        chapter = next(c for c in self.chapters(r"nvic|interrupt") if "exti" not in c.name)
150        tables = chapter.tables(r"vector +table|list +of +vector|NVIC|CPU")
151        assert tables
152
153        vtables = {}
154        for table in tables:
155            caption = table.caption(**capt_replace)
156            table_name = "VectorTable"
157            if len(tables) > 1:
158                # Create the right table filter
159                if (core := re.search(r"CPU(\d)|NVIC(\d)", caption)) is not None:
160                    table_name += f":Core={core.group(1)}" # Multi-core device
161                elif devices := device_filter_from(caption):
162                    table_name += f":Device={devices}"
163                elif categories := re.findall(r"Cat\.(\d)", caption):
164                    # Size category filter
165                    categories = "|".join(categories)
166                    table_name += f":Categories={categories}"
167
168            vtable = defaultdict(set)
169            for pos, values in table.domains("position").cells("acro").items():
170                if pos.isnumeric() and (name := values.match_value("acro")[0].text(**name_replace)):
171                    vtable[int(pos)].update(html.listify(name))
172            vtables[table_name] = dict(vtable)
173
174        return vtables
175
176    @cache
177    def peripheral_maps(self, chapter, assert_table=True):
178        off_replace = {r" +": "", "0x000x00": "0x00", "to": "-", "×": "*", r"\(\d+\)": ""}
179        dom_replace = {r"Register +size": "Bit position"}
180        reg_replace = {
181            r" +|\.+": "", r"\(COM(\d)\)": r"_COM\1",
182            r"^[Rr]es$||0x[\da-fA-FXx]+|\(.*?\)|-": "",
183            r"(?i)reserved|resetvalue.*": "", "enabled": "_EN", "disabled": "_DIS",
184            r"(?i)Outputcomparemode": "_Output", "(?i)Inputcapturemode": "_Input", "mode": "",
185            r"^TG_FS_": "OTG_FS_", "toRTC": "RTC", "SPI2S_": "SPI_",
186            r"andTIM\d+_.*": "", r"x=[\d,]+": ""}
187        fld_replace = {
188            r"\] +\d+(th|rd|nd|st)": "]", r" +|\.+|\[.*?\]|\[?\d+:\d+\]?|\(.*?\)|-|^[\dXx]+$|%|__|:0\]": "",
189            r"Dataregister|Independentdataregister": "DATA",
190            r"Framefilterreg0.*": "FRAME_FILTER_REG",
191            r"[Rr]es(erved)?|[Rr]egular|x_x(bits)?|NotAvailable|RefertoSection\d+:Comparator": "",
192            r"Sampletimebits|Injectedchannelsequence|channelsequence|conversioninregularsequencebits": "",
193            r"conversioninsequencebits|conversionininjectedsequencebits|or|first|second|third|fourth": ""}
194        bit_replace = {r".*:": ""}
195        glo_replace = {r"[Rr]eserved": ""}
196
197        print(chapter._relpath)
198        tables = chapter.tables(r"register +map")
199        if assert_table: assert tables
200
201        peripheral_data = {}
202        for table in tables:
203            caption = table.caption()
204            if any(n in caption for n in ["DFIFO", "global", "EXTI register map section", "vs swapping option"]):
205                continue
206            heading = table._heading.text(**{r"((\d+\.)+(\d+)?).*": r"\1"})
207            print(table, caption)
208
209            register_data = {}
210            for row in table.cell_rows():
211                rkey = next(k for k in row.match_keys("register") if not "size" in k)
212                register = row[rkey][0].text(**reg_replace).strip()
213                if not register: continue
214                offset = row.match_value(r"off-?set|addr")[0].text(**off_replace)
215                if not offset: continue
216                field_data = {}
217                for bits in row.match_keys(r"^[\d-]+$|.*?:[\d-]+$"):
218                    field = row[bits][0].text(**fld_replace).strip()
219                    if not field: continue
220                    bits = sorted(html.listify(html.replace(bits, **bit_replace), r"-"))
221                    if len(bits) == 2: bits = range(int(bits[0]), int(bits[1]))
222                    for bit in bits:
223                        bit = int(bit)
224                        field_data[bit] = field
225                        # print(f"{offset:>10} {register:60} {bit:>2} {field}")
226                register_data[register] = (offset, field_data)
227            assert register_data
228            peripheral_data[caption] = (heading, register_data)
229
230        if peripheral_data and all("HRTIM" in ca for ca in peripheral_data):
231            caption, heading = next((c,p) for c, (p, _) in peripheral_data.items())
232            all_registers = {k:v for (_, values) in peripheral_data.values() for k,v in values.items()}
233            peripheral_data = {caption: (heading, all_registers)}
234
235        instance_offsets = {}
236        if tables := chapter.tables(r"ADC +global +register +map"):
237            for row in tables[0].cell_rows():
238                if ifilter := row.match_value("register")[0].text(**glo_replace):
239                    offset = int(row.match_value("offset")[0].text().split("-")[0], 16)
240                    for instance in re.findall(r"ADC(\d+)", ifilter):
241                        instance_offsets[f"ADC[{instance}]"] = offset
242
243        return peripheral_data, instance_offsets
244
245    @cached_property
246    def peripherals(self):
247        per_replace = {
248            r" +": "", r".*?\(([A-Z]+|DMA2D)\).*?": r"\1",
249            r"Reserved|Port|Power|Registers|Reset|\(.*?\)|_REG": "",
250            r"(\d)/I2S\d": r"\1", r"/I2S|CANMessageRAM|Cortex-M4|I2S\dext|^GPV$": "",
251            r"Ethernet": "ETH", r"Flash": "FLASH", r"(?i).*ETHERNET.*": "ETH",
252            r"(?i)Firewall": "FW", r"HDMI-|": "", "SPDIF-RX": "SPDIFRX",
253            r"SPI2S2": "SPI2", "Tamper": "TAMP", "TT-FDCAN": "FDCAN",
254            r"USBOTG([FH])S": r"USB_OTG_\1S", "LCD-TFT": "LTDC", "DSIHOST": "DSI",
255            "TIMER": "TIM", r"^VREF$": "VREFBUF", "DelayBlock": "DLYB",
256            "I/O": "M", "I/O": "", "DAC1/2": "DAC12",
257            r"[a-z]": ""}
258        adr_replace = {r" |\(\d\)": ""}
259        sct_replace = {r"-": ""}
260        hdr_replace = {r"Peripheral +register +map": "map"}
261        bus_replace = {r".*(A\wB\d).*": r"\1", "-": ""}
262
263        # RM0431 has a bug where the peripheral table is in chapter 1
264        if "RM0431" in self.name:
265            chapters = self.chapters(r"chapter 1 ")
266        else:
267            chapters = self.chapters(r"memory +and +bus|memory +overview")
268        assert chapters
269        print(chapters[0]._relpath)
270
271        tables = chapters[0].tables(r"register +boundary")
272        assert tables
273
274        peripherals = defaultdict(list)
275        for table in tables:
276            print(table.caption())
277            for row in table.cell_rows(**hdr_replace):
278                regmaps = row.match_value("map")
279                if regmaps:
280                    regmap = regmaps[0].text(**sct_replace).strip()
281                    sections = re.findall(r"Section +(\d+\.\d+(\.\d+)?)", regmap)
282                    if not sections: continue
283                    sections = [s[0] for s in sections]
284                else:
285                    sections = []
286                names = html.listify(row.match_value("peri")[0].text(**per_replace), r"[-\&\+/,]")
287                if not names: continue
288                address = row.match_value("address")[0].text(**adr_replace)
289                address_min = int(address.split("-")[0], 16)
290                address_max = int(address.split("-")[1], 16)
291                bus = row.match_value("bus")[0].text(**bus_replace).strip()
292                peripherals[table.caption()].append( (names, address_min, address_max, bus, sections) )
293                print(f"{','.join(names):20} @[{hex(address_min)}, {hex(address_max)}] {bus:4} -> {', '.join(sections)}")
294
295        return peripherals
class ReferenceManual(modm_data.html.document.Document):
 12class ReferenceManual(html.Document):
 13    def __init__(self, path: str):
 14        super().__init__(path)
 15
 16    def __repr__(self) -> str:
 17        return f"RM({self.fullname})"
 18
 19    @cached_property
 20    def devices(self) -> list[str]:
 21        # Find all occurrences of STM32* strings in the first chapter
 22        chapter = self.chapter("chapter 0")
 23        for heading, texts in chapter.heading_texts():
 24            all_text = ""
 25            for text in texts:
 26                all_text += text.text(br=" ")
 27            # print(heading, all_text)
 28            # Must also match "STM32L4+" !!!
 29            rdevices = re.findall(r"STM32[\w/\+]+", all_text)
 30            if rdevices:
 31                break
 32
 33        # Split combo strings into individual devices
 34        devices = []
 35        for device in list(set(rdevices)):
 36            if len(parts := device.split("/")) >= 2:
 37                base = parts[0]
 38                devices.append(base)
 39                base = base[:-len(parts[1])]
 40                for part in parts[1:]:
 41                    devices.append(base + part)
 42            else:
 43                devices.append(device)
 44
 45        # Remove non-specific mentions: shortest subset
 46        return list(sorted(set(devices)))
 47
 48    @property
 49    def device_filters(self) -> list[str]:
 50        # Match STM32L4+ with STM32[SRQP], since STM32L4Axx is STM32L4 (without +)
 51        return [d.replace("x", ".").replace("+", r"[SRQP]").lower() for d in self.devices]
 52
 53    def filter_devices(self, devices):
 54        dids = set()
 55        for did in devices:
 56            for device in self.device_filters:
 57                if re.match(device, did.string):
 58                    dids.add(did)
 59                    break
 60        return list(dids)
 61
 62    @cached_property
 63    def flash_latencies(self):
 64        # FLASH peripheral is not described in the reference manual because... reasons?
 65        if "STM32F100xx" in self.devices:
 66            return {"": {1800: [24]}}
 67        if any(d.startswith("STM32F1") for d in self.devices):
 68            return {"": {1800: [24, 48, 72]}}
 69        # Tables too complicated to parse
 70        if any(d.startswith("STM32L1") for d in self.devices):
 71            # See Table 13 and Table 26
 72            return {
 73                "stm32l1...[68b]": { # Cat 1
 74                    1800: [16, 32],
 75                    1500: [8, 16],
 76                    1200: [2.1, 4.2]
 77                },
 78                "": { # Cat 2,3,4,5,6
 79                    1800: [16, 32],
 80                    1500: [8, 16],
 81                    1200: [4.2, 8]
 82                }
 83            }
 84
 85        if (any(d.startswith("STM32F0") for d in self.devices) or
 86            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,
123                                       "1n": 1200, "1": 1200,
124                                       "2": 1000}[vrange.group(1)]
125                    max_frequency = row[vkey][0].text(
126                            **{r"-": "", r".*?CLK.*?(\d+).*": r"\1",
127                               r".*?;(\d+) *MHz.*": r"\1", r".*?(\d+).*": r"\1"})
128                    if max_frequency:
129                        table_data[min_voltage].append(float(max_frequency))
130            dfilter = device_filter_from(table.caption())
131            assert table_data
132            ws_tables[dfilter] = {v: list(sorted(set(f))) for v, f in table_data.items()}
133
134        # print(ws_tables)
135        assert ws_tables
136        return ws_tables
137
138
139
140    @cached_property
141    def vector_tables(self):
142        name_replace = {"p": r"\1,", r" +": "", r"\(.*?\)|_IRQn|_$|^-$|[Rr]eserved": "",
143                        r"\+|and": ",", r"_(\w+)(LSE|BLE|I2C\d)_": r"_\1,\2_",
144                        r"EXTI\[(\d+):(\d+)\]": r"EXTI\1_\2", r"\[(\d+):(\d+)\]": r"\2_\1"}
145        capt_replace = {"br": " ", r" +": " ", r"\((Cat\.\d.*?devices)\)": r"\1", r"\(.*?\)": "",
146                        r"for +connectivity +line +devices": "for STM32F105/7",
147                        r"for +XL\-density +devices": "for STM32F10xxF/G",
148                        r"for +other +STM32F10xxx +devices": "for the rest",}
149
150        chapter = next(c for c in self.chapters(r"nvic|interrupt") if "exti" not in c.name)
151        tables = chapter.tables(r"vector +table|list +of +vector|NVIC|CPU")
152        assert tables
153
154        vtables = {}
155        for table in tables:
156            caption = table.caption(**capt_replace)
157            table_name = "VectorTable"
158            if len(tables) > 1:
159                # Create the right table filter
160                if (core := re.search(r"CPU(\d)|NVIC(\d)", caption)) is not None:
161                    table_name += f":Core={core.group(1)}" # Multi-core device
162                elif devices := device_filter_from(caption):
163                    table_name += f":Device={devices}"
164                elif categories := re.findall(r"Cat\.(\d)", caption):
165                    # Size category filter
166                    categories = "|".join(categories)
167                    table_name += f":Categories={categories}"
168
169            vtable = defaultdict(set)
170            for pos, values in table.domains("position").cells("acro").items():
171                if pos.isnumeric() and (name := values.match_value("acro")[0].text(**name_replace)):
172                    vtable[int(pos)].update(html.listify(name))
173            vtables[table_name] = dict(vtable)
174
175        return vtables
176
177    @cache
178    def peripheral_maps(self, chapter, assert_table=True):
179        off_replace = {r" +": "", "0x000x00": "0x00", "to": "-", "×": "*", r"\(\d+\)": ""}
180        dom_replace = {r"Register +size": "Bit position"}
181        reg_replace = {
182            r" +|\.+": "", r"\(COM(\d)\)": r"_COM\1",
183            r"^[Rr]es$||0x[\da-fA-FXx]+|\(.*?\)|-": "",
184            r"(?i)reserved|resetvalue.*": "", "enabled": "_EN", "disabled": "_DIS",
185            r"(?i)Outputcomparemode": "_Output", "(?i)Inputcapturemode": "_Input", "mode": "",
186            r"^TG_FS_": "OTG_FS_", "toRTC": "RTC", "SPI2S_": "SPI_",
187            r"andTIM\d+_.*": "", r"x=[\d,]+": ""}
188        fld_replace = {
189            r"\] +\d+(th|rd|nd|st)": "]", r" +|\.+|\[.*?\]|\[?\d+:\d+\]?|\(.*?\)|-|^[\dXx]+$|%|__|:0\]": "",
190            r"Dataregister|Independentdataregister": "DATA",
191            r"Framefilterreg0.*": "FRAME_FILTER_REG",
192            r"[Rr]es(erved)?|[Rr]egular|x_x(bits)?|NotAvailable|RefertoSection\d+:Comparator": "",
193            r"Sampletimebits|Injectedchannelsequence|channelsequence|conversioninregularsequencebits": "",
194            r"conversioninsequencebits|conversionininjectedsequencebits|or|first|second|third|fourth": ""}
195        bit_replace = {r".*:": ""}
196        glo_replace = {r"[Rr]eserved": ""}
197
198        print(chapter._relpath)
199        tables = chapter.tables(r"register +map")
200        if assert_table: assert tables
201
202        peripheral_data = {}
203        for table in tables:
204            caption = table.caption()
205            if any(n in caption for n in ["DFIFO", "global", "EXTI register map section", "vs swapping option"]):
206                continue
207            heading = table._heading.text(**{r"((\d+\.)+(\d+)?).*": r"\1"})
208            print(table, caption)
209
210            register_data = {}
211            for row in table.cell_rows():
212                rkey = next(k for k in row.match_keys("register") if not "size" in k)
213                register = row[rkey][0].text(**reg_replace).strip()
214                if not register: continue
215                offset = row.match_value(r"off-?set|addr")[0].text(**off_replace)
216                if not offset: continue
217                field_data = {}
218                for bits in row.match_keys(r"^[\d-]+$|.*?:[\d-]+$"):
219                    field = row[bits][0].text(**fld_replace).strip()
220                    if not field: continue
221                    bits = sorted(html.listify(html.replace(bits, **bit_replace), r"-"))
222                    if len(bits) == 2: bits = range(int(bits[0]), int(bits[1]))
223                    for bit in bits:
224                        bit = int(bit)
225                        field_data[bit] = field
226                        # print(f"{offset:>10} {register:60} {bit:>2} {field}")
227                register_data[register] = (offset, field_data)
228            assert register_data
229            peripheral_data[caption] = (heading, register_data)
230
231        if peripheral_data and all("HRTIM" in ca for ca in peripheral_data):
232            caption, heading = next((c,p) for c, (p, _) in peripheral_data.items())
233            all_registers = {k:v for (_, values) in peripheral_data.values() for k,v in values.items()}
234            peripheral_data = {caption: (heading, all_registers)}
235
236        instance_offsets = {}
237        if tables := chapter.tables(r"ADC +global +register +map"):
238            for row in tables[0].cell_rows():
239                if ifilter := row.match_value("register")[0].text(**glo_replace):
240                    offset = int(row.match_value("offset")[0].text().split("-")[0], 16)
241                    for instance in re.findall(r"ADC(\d+)", ifilter):
242                        instance_offsets[f"ADC[{instance}]"] = offset
243
244        return peripheral_data, instance_offsets
245
246    @cached_property
247    def peripherals(self):
248        per_replace = {
249            r" +": "", r".*?\(([A-Z]+|DMA2D)\).*?": r"\1",
250            r"Reserved|Port|Power|Registers|Reset|\(.*?\)|_REG": "",
251            r"(\d)/I2S\d": r"\1", r"/I2S|CANMessageRAM|Cortex-M4|I2S\dext|^GPV$": "",
252            r"Ethernet": "ETH", r"Flash": "FLASH", r"(?i).*ETHERNET.*": "ETH",
253            r"(?i)Firewall": "FW", r"HDMI-|": "", "SPDIF-RX": "SPDIFRX",
254            r"SPI2S2": "SPI2", "Tamper": "TAMP", "TT-FDCAN": "FDCAN",
255            r"USBOTG([FH])S": r"USB_OTG_\1S", "LCD-TFT": "LTDC", "DSIHOST": "DSI",
256            "TIMER": "TIM", r"^VREF$": "VREFBUF", "DelayBlock": "DLYB",
257            "I/O": "M", "I/O": "", "DAC1/2": "DAC12",
258            r"[a-z]": ""}
259        adr_replace = {r" |\(\d\)": ""}
260        sct_replace = {r"-": ""}
261        hdr_replace = {r"Peripheral +register +map": "map"}
262        bus_replace = {r".*(A\wB\d).*": r"\1", "-": ""}
263
264        # RM0431 has a bug where the peripheral table is in chapter 1
265        if "RM0431" in self.name:
266            chapters = self.chapters(r"chapter 1 ")
267        else:
268            chapters = self.chapters(r"memory +and +bus|memory +overview")
269        assert chapters
270        print(chapters[0]._relpath)
271
272        tables = chapters[0].tables(r"register +boundary")
273        assert tables
274
275        peripherals = defaultdict(list)
276        for table in tables:
277            print(table.caption())
278            for row in table.cell_rows(**hdr_replace):
279                regmaps = row.match_value("map")
280                if regmaps:
281                    regmap = regmaps[0].text(**sct_replace).strip()
282                    sections = re.findall(r"Section +(\d+\.\d+(\.\d+)?)", regmap)
283                    if not sections: continue
284                    sections = [s[0] for s in sections]
285                else:
286                    sections = []
287                names = html.listify(row.match_value("peri")[0].text(**per_replace), r"[-\&\+/,]")
288                if not names: continue
289                address = row.match_value("address")[0].text(**adr_replace)
290                address_min = int(address.split("-")[0], 16)
291                address_max = int(address.split("-")[1], 16)
292                bus = row.match_value("bus")[0].text(**bus_replace).strip()
293                peripherals[table.caption()].append( (names, address_min, address_max, bus, sections) )
294                print(f"{','.join(names):20} @[{hex(address_min)}, {hex(address_max)}] {bus:4} -> {', '.join(sections)}")
295
296        return peripherals
ReferenceManual(path: str)
13    def __init__(self, path: str):
14        super().__init__(path)
devices: list[str]
19    @cached_property
20    def devices(self) -> list[str]:
21        # Find all occurrences of STM32* strings in the first chapter
22        chapter = self.chapter("chapter 0")
23        for heading, texts in chapter.heading_texts():
24            all_text = ""
25            for text in texts:
26                all_text += text.text(br=" ")
27            # print(heading, all_text)
28            # Must also match "STM32L4+" !!!
29            rdevices = re.findall(r"STM32[\w/\+]+", all_text)
30            if rdevices:
31                break
32
33        # Split combo strings into individual devices
34        devices = []
35        for device in list(set(rdevices)):
36            if len(parts := device.split("/")) >= 2:
37                base = parts[0]
38                devices.append(base)
39                base = base[:-len(parts[1])]
40                for part in parts[1:]:
41                    devices.append(base + part)
42            else:
43                devices.append(device)
44
45        # Remove non-specific mentions: shortest subset
46        return list(sorted(set(devices)))
device_filters: list[str]
48    @property
49    def device_filters(self) -> list[str]:
50        # Match STM32L4+ with STM32[SRQP], since STM32L4Axx is STM32L4 (without +)
51        return [d.replace("x", ".").replace("+", r"[SRQP]").lower() for d in self.devices]
def filter_devices(self, devices):
53    def filter_devices(self, devices):
54        dids = set()
55        for did in devices:
56            for device in self.device_filters:
57                if re.match(device, did.string):
58                    dids.add(did)
59                    break
60        return list(dids)
flash_latencies
 62    @cached_property
 63    def flash_latencies(self):
 64        # FLASH peripheral is not described in the reference manual because... reasons?
 65        if "STM32F100xx" in self.devices:
 66            return {"": {1800: [24]}}
 67        if any(d.startswith("STM32F1") for d in self.devices):
 68            return {"": {1800: [24, 48, 72]}}
 69        # Tables too complicated to parse
 70        if any(d.startswith("STM32L1") for d in self.devices):
 71            # See Table 13 and Table 26
 72            return {
 73                "stm32l1...[68b]": { # Cat 1
 74                    1800: [16, 32],
 75                    1500: [8, 16],
 76                    1200: [2.1, 4.2]
 77                },
 78                "": { # Cat 2,3,4,5,6
 79                    1800: [16, 32],
 80                    1500: [8, 16],
 81                    1200: [4.2, 8]
 82                }
 83            }
 84
 85        if (any(d.startswith("STM32F0") for d in self.devices) or
 86            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,
123                                       "1n": 1200, "1": 1200,
124                                       "2": 1000}[vrange.group(1)]
125                    max_frequency = row[vkey][0].text(
126                            **{r"-": "", r".*?CLK.*?(\d+).*": r"\1",
127                               r".*?;(\d+) *MHz.*": r"\1", r".*?(\d+).*": r"\1"})
128                    if max_frequency:
129                        table_data[min_voltage].append(float(max_frequency))
130            dfilter = device_filter_from(table.caption())
131            assert table_data
132            ws_tables[dfilter] = {v: list(sorted(set(f))) for v, f in table_data.items()}
133
134        # print(ws_tables)
135        assert ws_tables
136        return ws_tables
vector_tables
140    @cached_property
141    def vector_tables(self):
142        name_replace = {"p": r"\1,", r" +": "", r"\(.*?\)|_IRQn|_$|^-$|[Rr]eserved": "",
143                        r"\+|and": ",", r"_(\w+)(LSE|BLE|I2C\d)_": r"_\1,\2_",
144                        r"EXTI\[(\d+):(\d+)\]": r"EXTI\1_\2", r"\[(\d+):(\d+)\]": r"\2_\1"}
145        capt_replace = {"br": " ", r" +": " ", r"\((Cat\.\d.*?devices)\)": r"\1", r"\(.*?\)": "",
146                        r"for +connectivity +line +devices": "for STM32F105/7",
147                        r"for +XL\-density +devices": "for STM32F10xxF/G",
148                        r"for +other +STM32F10xxx +devices": "for the rest",}
149
150        chapter = next(c for c in self.chapters(r"nvic|interrupt") if "exti" not in c.name)
151        tables = chapter.tables(r"vector +table|list +of +vector|NVIC|CPU")
152        assert tables
153
154        vtables = {}
155        for table in tables:
156            caption = table.caption(**capt_replace)
157            table_name = "VectorTable"
158            if len(tables) > 1:
159                # Create the right table filter
160                if (core := re.search(r"CPU(\d)|NVIC(\d)", caption)) is not None:
161                    table_name += f":Core={core.group(1)}" # Multi-core device
162                elif devices := device_filter_from(caption):
163                    table_name += f":Device={devices}"
164                elif categories := re.findall(r"Cat\.(\d)", caption):
165                    # Size category filter
166                    categories = "|".join(categories)
167                    table_name += f":Categories={categories}"
168
169            vtable = defaultdict(set)
170            for pos, values in table.domains("position").cells("acro").items():
171                if pos.isnumeric() and (name := values.match_value("acro")[0].text(**name_replace)):
172                    vtable[int(pos)].update(html.listify(name))
173            vtables[table_name] = dict(vtable)
174
175        return vtables
@cache
def peripheral_maps(self, chapter, assert_table=True):
177    @cache
178    def peripheral_maps(self, chapter, assert_table=True):
179        off_replace = {r" +": "", "0x000x00": "0x00", "to": "-", "×": "*", r"\(\d+\)": ""}
180        dom_replace = {r"Register +size": "Bit position"}
181        reg_replace = {
182            r" +|\.+": "", r"\(COM(\d)\)": r"_COM\1",
183            r"^[Rr]es$||0x[\da-fA-FXx]+|\(.*?\)|-": "",
184            r"(?i)reserved|resetvalue.*": "", "enabled": "_EN", "disabled": "_DIS",
185            r"(?i)Outputcomparemode": "_Output", "(?i)Inputcapturemode": "_Input", "mode": "",
186            r"^TG_FS_": "OTG_FS_", "toRTC": "RTC", "SPI2S_": "SPI_",
187            r"andTIM\d+_.*": "", r"x=[\d,]+": ""}
188        fld_replace = {
189            r"\] +\d+(th|rd|nd|st)": "]", r" +|\.+|\[.*?\]|\[?\d+:\d+\]?|\(.*?\)|-|^[\dXx]+$|%|__|:0\]": "",
190            r"Dataregister|Independentdataregister": "DATA",
191            r"Framefilterreg0.*": "FRAME_FILTER_REG",
192            r"[Rr]es(erved)?|[Rr]egular|x_x(bits)?|NotAvailable|RefertoSection\d+:Comparator": "",
193            r"Sampletimebits|Injectedchannelsequence|channelsequence|conversioninregularsequencebits": "",
194            r"conversioninsequencebits|conversionininjectedsequencebits|or|first|second|third|fourth": ""}
195        bit_replace = {r".*:": ""}
196        glo_replace = {r"[Rr]eserved": ""}
197
198        print(chapter._relpath)
199        tables = chapter.tables(r"register +map")
200        if assert_table: assert tables
201
202        peripheral_data = {}
203        for table in tables:
204            caption = table.caption()
205            if any(n in caption for n in ["DFIFO", "global", "EXTI register map section", "vs swapping option"]):
206                continue
207            heading = table._heading.text(**{r"((\d+\.)+(\d+)?).*": r"\1"})
208            print(table, caption)
209
210            register_data = {}
211            for row in table.cell_rows():
212                rkey = next(k for k in row.match_keys("register") if not "size" in k)
213                register = row[rkey][0].text(**reg_replace).strip()
214                if not register: continue
215                offset = row.match_value(r"off-?set|addr")[0].text(**off_replace)
216                if not offset: continue
217                field_data = {}
218                for bits in row.match_keys(r"^[\d-]+$|.*?:[\d-]+$"):
219                    field = row[bits][0].text(**fld_replace).strip()
220                    if not field: continue
221                    bits = sorted(html.listify(html.replace(bits, **bit_replace), r"-"))
222                    if len(bits) == 2: bits = range(int(bits[0]), int(bits[1]))
223                    for bit in bits:
224                        bit = int(bit)
225                        field_data[bit] = field
226                        # print(f"{offset:>10} {register:60} {bit:>2} {field}")
227                register_data[register] = (offset, field_data)
228            assert register_data
229            peripheral_data[caption] = (heading, register_data)
230
231        if peripheral_data and all("HRTIM" in ca for ca in peripheral_data):
232            caption, heading = next((c,p) for c, (p, _) in peripheral_data.items())
233            all_registers = {k:v for (_, values) in peripheral_data.values() for k,v in values.items()}
234            peripheral_data = {caption: (heading, all_registers)}
235
236        instance_offsets = {}
237        if tables := chapter.tables(r"ADC +global +register +map"):
238            for row in tables[0].cell_rows():
239                if ifilter := row.match_value("register")[0].text(**glo_replace):
240                    offset = int(row.match_value("offset")[0].text().split("-")[0], 16)
241                    for instance in re.findall(r"ADC(\d+)", ifilter):
242                        instance_offsets[f"ADC[{instance}]"] = offset
243
244        return peripheral_data, instance_offsets
peripherals
246    @cached_property
247    def peripherals(self):
248        per_replace = {
249            r" +": "", r".*?\(([A-Z]+|DMA2D)\).*?": r"\1",
250            r"Reserved|Port|Power|Registers|Reset|\(.*?\)|_REG": "",
251            r"(\d)/I2S\d": r"\1", r"/I2S|CANMessageRAM|Cortex-M4|I2S\dext|^GPV$": "",
252            r"Ethernet": "ETH", r"Flash": "FLASH", r"(?i).*ETHERNET.*": "ETH",
253            r"(?i)Firewall": "FW", r"HDMI-|": "", "SPDIF-RX": "SPDIFRX",
254            r"SPI2S2": "SPI2", "Tamper": "TAMP", "TT-FDCAN": "FDCAN",
255            r"USBOTG([FH])S": r"USB_OTG_\1S", "LCD-TFT": "LTDC", "DSIHOST": "DSI",
256            "TIMER": "TIM", r"^VREF$": "VREFBUF", "DelayBlock": "DLYB",
257            "I/O": "M", "I/O": "", "DAC1/2": "DAC12",
258            r"[a-z]": ""}
259        adr_replace = {r" |\(\d\)": ""}
260        sct_replace = {r"-": ""}
261        hdr_replace = {r"Peripheral +register +map": "map"}
262        bus_replace = {r".*(A\wB\d).*": r"\1", "-": ""}
263
264        # RM0431 has a bug where the peripheral table is in chapter 1
265        if "RM0431" in self.name:
266            chapters = self.chapters(r"chapter 1 ")
267        else:
268            chapters = self.chapters(r"memory +and +bus|memory +overview")
269        assert chapters
270        print(chapters[0]._relpath)
271
272        tables = chapters[0].tables(r"register +boundary")
273        assert tables
274
275        peripherals = defaultdict(list)
276        for table in tables:
277            print(table.caption())
278            for row in table.cell_rows(**hdr_replace):
279                regmaps = row.match_value("map")
280                if regmaps:
281                    regmap = regmaps[0].text(**sct_replace).strip()
282                    sections = re.findall(r"Section +(\d+\.\d+(\.\d+)?)", regmap)
283                    if not sections: continue
284                    sections = [s[0] for s in sections]
285                else:
286                    sections = []
287                names = html.listify(row.match_value("peri")[0].text(**per_replace), r"[-\&\+/,]")
288                if not names: continue
289                address = row.match_value("address")[0].text(**adr_replace)
290                address_min = int(address.split("-")[0], 16)
291                address_max = int(address.split("-")[1], 16)
292                bus = row.match_value("bus")[0].text(**bus_replace).strip()
293                peripherals[table.caption()].append( (names, address_min, address_max, bus, sections) )
294                print(f"{','.join(names):20} @[{hex(address_min)}, {hex(address_max)}] {bus:4} -> {', '.join(sections)}")
295
296        return peripherals