summaryrefslogtreecommitdiff
path: root/ports/stm32/boards/make-pins.py
blob: 6f8a0a659d2ebae65df6d83466f100d02470b1a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#!/usr/bin/env python


from collections import defaultdict, namedtuple
import os
import re
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../../tools"))
import boardgen


# Must have matching entries in AF_FN_* enum in ../pin_defs_stm32.h
SUPPORTED_AF = {
    "TIM": ["CH1", "CH2", "CH3", "CH4", "CH1N", "CH2N", "CH3N", "CH1_ETR", "ETR", "BKIN"],
    "I2C": ["SDA", "SCL"],
    "I2S": ["CK", "MCK", "SD", "WS", "EXTSD"],
    "USART": ["RX", "TX", "CTS", "RTS", "CK"],
    "UART": ["RX", "TX", "CTS", "RTS"],
    "LPUART": ["RX", "TX", "CTS", "RTS"],
    "SPI": ["NSS", "SCK", "MISO", "MOSI"],
    "SDMMC": ["CK", "CMD", "D0", "D1", "D2", "D3"],
    "CAN": ["TX", "RX"],
}

# Only make these AFs available if we actually enable those peripherals (by
# assigning default pins in mpconfigboard.h).
CONDITIONAL_VAR = {
    "I2C": "MICROPY_HW_I2C{num}_SCL",
    "I2S": "MICROPY_HW_I2S{num}",
    "SPI": "MICROPY_HW_SPI{num}_SCK",
    "UART": "MICROPY_HW_UART{num}_TX",
    "LPUART": "MICROPY_HW_LPUART{num}_TX",
    "USART": "MICROPY_HW_UART{num}_TX",
    "SDMMC": "MICROPY_HW_SDMMC{num}_CK",
    "CAN": "MICROPY_HW_CAN{num}_TX",
}

# Contains the result of parsing an cell from af.csv.
PinAf = namedtuple(
    "PinAf",
    [
        "af_idx",  # int, 0-15
        "af_fn",  # e.g. "I2C"
        "af_unit",  # int, e.g. 1 (for I2C1) or None (for OTG_HS_ULPI_CK)
        "af_pin",  # e.g. "SDA"
        "af_supported",  # bool, see table above
        "af_name",  # e.g. "I2C1_SDA"
    ],
)

# Only support ADC1, ADC2, ADC3 for now (e.g. cannot support ADC4 & ADC5 on
# STM32G4).
MIN_ADC_UNIT = 1
MAX_ADC_UNIT = 3


class Stm32Pin(boardgen.Pin):
    def __init__(self, cpu_pin_name):
        super().__init__(cpu_pin_name)

        # Pins ending in "_C" correspond to the analog-only pad of a pair
        # of dual (analog) pads found on H7 MCUs (eg PA0 and PA0_C pair).
        self._analog_only = cpu_pin_name.endswith("_C")
        if self._analog_only:
            cpu_pin_name = cpu_pin_name[:-2]

        # P<port><num> (already verified by validate_cpu_pin_name).
        self._port = cpu_pin_name[1]
        self._pin = int(cpu_pin_name[2:])

        # List of PinAf instances.
        self._afs = []

        # The channel that this pin uses on each of the listed units.
        self._adc_channel = 0
        # e.g. ADC123 -> [1,2,3]
        self._adc_units = []

    # Called for each AF defined in the csv file for this pin. e.g. "SPI2_NSS/I2S2_WS"
    def add_af(self, af_idx, af_name, af):
        if af_idx > 16:
            # The AF csv files should not have more than 16 AFs + 1 ADC list.
            return

        # AF 16 is the ADC, which is special cased below.
        if af_idx == 16:
            if af_name != "ADC":
                raise boardgen.PinGeneratorError(
                    "Invalid AF column name '{:s}' for ADC column with index {:d}.".format(
                        af_name, af_idx
                    )
                )
            return self.add_adc(af)

        if af_name != "AF{:d}".format(af_idx):
            raise boardgen.PinGeneratorError(
                "Invalid AF column name '{:s}' for AF index {:d}.".format(af_name, af_idx)
            )

        # If there is a slash, then the slash separates multiple aliases for
        # the same alternate function.
        for af_name in af.split("/"):
            if not af_name.strip():
                continue
            # This matches <fn><unit>_<pin>, with consideration for:
            #  - fn may contain a digit (e.g. I2C)
            #  - there may be an "ext" after the unit
            m = re.match("([A-Z0-9]+[A-Z])(([0-9]+)(ext)?)?(_(.*))?", af_name)
            if not m:
                raise boardgen.PinGeneratorError(
                    "Invalid af '{:s}' for pin '{:s}'".format(af_name, self.name())
                )
            else:
                af_fn = m.group(1)
                af_unit = int(m.group(3)) if m.group(3) is not None else None
                af_ext = m.group(4) == "ext"
                af_pin = m.group(6)

                # Special case. We change I2S2ext_SD into I2S2_EXTSD so that it parses
                # the same way the other peripherals do.
                if af_ext:
                    af_pin = "EXT" + af_pin

                # Special case: FDCAN peripheral is named CAN in MicroPython, same as bxCAN
                af_fn = af_fn.replace("FDCAN", "CAN")

                af_supported = af_fn in SUPPORTED_AF and af_pin in SUPPORTED_AF[af_fn]

                self._afs.append(PinAf(af_idx, af_fn, af_unit, af_pin, af_supported, af_name))

    # ADCs are slash separated "ADC<list of units>_<mode><channel>", where unit=1,2,3,4,5 and mode=IN,INN,INP
    def add_adc(self, adc):
        if not adc.strip():
            return

        # TODO: This needs to be improved to support the case where a pin can
        # be the P for one channel, and the N for a different channel.
        # e.g. "ADC123_INP12/ADC123_INN11".
        for adc_name in adc.split("/"):
            m = re.match("ADC([1-5]+)_(IN[NP]?)([0-9]+)$", adc_name)
            if not m:
                raise boardgen.PinGeneratorError(
                    "Invalid adc '{:s}' for pin '{:s}'".format(adc_name, self.name())
                )
            adc_units = [int(x) for x in m.group(1) if MIN_ADC_UNIT <= int(x) <= MAX_ADC_UNIT]
            adc_mode = m.group(2)
            if adc_mode == "INN":
                # On H7 we have INN/INP, all other parts use IN only. Only use
                # IN or INP channels.
                continue
            adc_channel = int(m.group(3))

            # Pick the entry with the most ADC units, e.g. "ADC1_INP16/ADC12_INN1/ADC12_INP0" --> "ADC12_INN1".
            if len(adc_units) > len(self._adc_units):
                self._adc_units = adc_units
                self._adc_channel = adc_channel

    # STM32-specific behavior, strip the "P" from the start of the name when emitting this pin.
    # e.g. the #define is pin_A11 not pin_PA11. Fortunately we don't have to special case this
    # as there are no places where emit the full "PA11" name.
    def name(self):
        return self._cpu_pin_name[1:]

    # Use the PIN() macro defined in stm32f4xx_prefix.c for defining the pin
    # objects.
    def definition(self):
        # Generate bitfield of supported ADC units where lsb is unit 1 (e.g. [1,3] --> 0b101).
        adc_units_bitfield = (
            " | ".join("PIN_ADC{}".format(unit) for unit in self._adc_units) or "0"
        )

        # PIN(p_port, p_pin, p_af, p_adc_num, p_adc_channel)
        pin_macro = "PIN_ANALOG" if self._analog_only else "PIN"
        return "{:s}({:s}, {:d}, pin_{:s}_af, {:s}, {:d})".format(
            pin_macro, self._port, self._pin, self.name(), adc_units_bitfield, self._adc_channel
        )

    # This will be called at the start of the output (after the prefix). Use
    # it to emit the af objects (via the AF() macro).
    def print_source(self, out_source):
        print(file=out_source)
        print("const pin_af_obj_t pin_{:s}_af[] = {{".format(self.name()), file=out_source)
        for af in self._afs:
            if af.af_fn in CONDITIONAL_VAR:
                print(
                    "    #if defined({:s})".format(
                        CONDITIONAL_VAR[af.af_fn].format(num=af.af_unit)
                    ),
                    file=out_source,
                )
            if af.af_supported:
                print("    ", end="", file=out_source)
            else:
                print("    // ", end="", file=out_source)
            # AF(af_idx, af_fn, af_unit, af_type, af_ptr)
            print(
                "AF({:d}, {:s}, {:d}, {:s}, {:s}{:s}),  // {:s}".format(
                    af.af_idx,
                    af.af_fn,
                    af.af_unit or 0,
                    af.af_pin or "NONE",
                    af.af_fn,
                    "" if af.af_unit is None else str(af.af_unit),
                    af.af_name,
                ),
                file=out_source,
            )
            if af.af_fn in CONDITIONAL_VAR:
                print("    #endif", file=out_source)
        print("};", file=out_source)

    # STM32 cpu names must be "P<port><num>".
    @staticmethod
    def validate_cpu_pin_name(cpu_pin_name):
        boardgen.Pin.validate_cpu_pin_name(cpu_pin_name)

        if not re.match("P[A-O][0-9]+(_C)?$", cpu_pin_name):
            raise boardgen.PinGeneratorError("Invalid cpu pin name '{}'".format(cpu_pin_name))


class Stm32PinGenerator(boardgen.PinGenerator):
    def __init__(self):
        # Use custom pin type above, and also enable the --af-csv argument so
        # that add_af gets called on each pin.
        super().__init__(
            pin_type=Stm32Pin,
            enable_af=True,
        )

    # STM32-specific behavior, we use pin_A0 for the cpu names, but
    # pyb_pin_X11 for the board names.
    def board_name_define_prefix(self):
        return "pyb_"

    # Override the default implementation just to change the default arguments
    # (extra header row, skip first column).
    def parse_af_csv(self, filename):
        return super().parse_af_csv(filename, header_rows=2, pin_col=1, af_col=2)

    # Find which ADCs are used on this chip and on how many pins and the
    # maximum channel number for each.
    def count_adc_pins(self):
        adc_units = defaultdict(lambda: (0, 0))
        for pin in self._pins:  # All pins
            for unit in pin._adc_units:
                num, max_channel = adc_units[unit]
                if pin._available:
                    adc_units[unit] = num + 1, max(max_channel, pin._adc_channel)
        return adc_units.items()

    # Print table of pins for each ADC (indexed by channel).
    def print_adcs(self, out_source):
        for adc_unit, (num_pins, max_channel) in self.count_adc_pins():
            print(file=out_source)
            print(
                "const machine_pin_obj_t * const pin_adc{:d}[{:d}] = {{".format(
                    adc_unit, max_channel + 1
                ),
                file=out_source,
            )
            # Don't include pins that weren't in pins.csv.
            for pin in self.available_pins():
                if pin._hidden:
                    continue
                if adc_unit in pin._adc_units:
                    print(
                        "    [{:d}] = {:s},".format(pin._adc_channel, self._cpu_pin_pointer(pin)),
                        file=out_source,
                    )
            print("};", file=out_source)

    # Print externs for the adc pin tables.
    def print_adc_externs(self, out_source):
        print(file=out_source)
        for adc_unit, (num_pins, max_channel) in self.count_adc_pins():
            print(
                "extern const machine_pin_obj_t * const pin_adc{:d}[{:d}];".format(
                    adc_unit, max_channel + 1
                ),
                file=out_source,
            )

    # Append ADC definitions to the end of the source output.
    def print_source(self, out_source):
        super().print_source(out_source)
        self.print_adcs(out_source)

    # Append ADC externs to the end of the header output, and don't include
    # externs in mboot mode.
    def print_header(self, out_header):
        if self.args.mboot_mode:
            self.print_defines(out_header, cpu=False)
        else:
            super().print_header(out_header)
            self.print_adc_externs(out_header)

    # This is a set of map entries `MP_QSTR_AF<num>_<fn>` -> `GPIO_AF<num>_<fn>`
    # that become part of the locals dict of machine.Pin.
    def print_af_const(self, out_af_const):
        # Extract all unique "AF<num>_<fn>" values.
        names = set()
        for pin in self.available_pins():
            for af in pin._afs:
                if not af.af_supported:
                    continue
                key = (
                    "AF{:d}_{:s}{:d}".format(af.af_idx, af.af_fn, af.af_unit),
                    af.af_fn,
                    af.af_unit,
                )
                names.add(key)

        # Generate the table.
        for key in sorted(names):
            name, af_fn, af_unit = key
            if af_fn in CONDITIONAL_VAR:
                print(
                    "    #if defined({:s})".format(CONDITIONAL_VAR[af_fn].format(num=af_unit)),
                    file=out_af_const,
                )
            print(
                "    {{ MP_ROM_QSTR(MP_QSTR_{:s}), MP_ROM_INT(GPIO_{:s}) }},".format(name, name),
                file=out_af_const,
            )
            if af_fn in CONDITIONAL_VAR:
                print("    #endif", file=out_af_const)

    # Output macros to compile-time match a pin object to its AF. See
    # mp_hal_pin_config_alt_static and mp_hal_pin_config_alt_static_speed in
    # pin_static_af.h.
    def print_af_defs(self, out_af_defs):
        # Get the set of unique "<fn><unit>_<pinfn>" (e.g. I2C1_SDA) and which
        # pins can be used for each.
        af_defs = defaultdict(list)
        for pin in self._pins:
            for af in pin._afs:
                key = af.af_fn, af.af_unit, af.af_pin
                af_defs[key].append((pin, af.af_idx))

        # Emit a macro for each that will match a pin object to the
        # corresponding af index for that pin doing that function.
        for key, pins in af_defs.items():
            af_fn, af_unit, af_pin = key
            print(file=out_af_defs)
            print(
                "#define STATIC_AF_{:s}{:s}_{:s}(pin_obj) ( \\".format(
                    af_fn, "" if af_unit is None else str(af_unit), af_pin or "NULL"
                ),
                file=out_af_defs,
            )
            for pin, af_idx in pins:
                if self.args.mboot_mode:
                    print(
                        "    ((pin_obj) == (pin_{:s})) ? ({:d}) : \\".format(pin.name(), af_idx),
                        file=out_af_defs,
                    )
                else:
                    # Match either "(pin_A11_obj)" (if using pin_A11 or
                    # pyb_pin_X11) or "((pin_A11_obj))" (if going via another
                    # macro e.g. MICROPY_HW_QSPIFLASH_CS).
                    # TODO: Why do we need do do string matching? (i.e. why can't the mboot behavior be used always?).
                    print(
                        '    ((strcmp( #pin_obj , "(&pin_{:s}_obj)") & strcmp( #pin_obj , "((&pin_{:s}_obj))")) == 0) ? ({:d}) : \\'.format(
                            pin.name(), pin.name(), af_idx
                        ),
                        file=out_af_defs,
                    )
            print("    (0xffffffffffffffffULL))", file=out_af_defs)

    # Additional stm32-specific outputs that will be written in
    # generate_extra_files().
    def extra_args(self, parser):
        parser.add_argument("--output-af-const")
        parser.add_argument("--output-af-defs")

        # In mboot mode the af-defs use object rather than string comparison,
        # and we don't include externs in the header file.
        parser.add_argument("--mboot-mode", action="store_true")

    # Called in main() after everything else is done to write additional files.
    def generate_extra_files(self):
        if self.args.output_af_const:
            with open(self.args.output_af_const, "w") as out_af_const:
                self.print_af_const(out_af_const)

        if self.args.output_af_defs:
            with open(self.args.output_af_defs, "w") as out_af_defs:
                self.print_af_defs(out_af_defs)


if __name__ == "__main__":
    Stm32PinGenerator().main()