# This file is part of the MicroPython project, http://micropython.org/ # # The MIT License (MIT) # # Copyright (c) 2023 Jim Mussared # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # This script contains common functionality to assist a port in implementing # make-pins.py, which is used to emit compile-time definitions of pin, AF, and # ADC objects based on inputs from the vendor HAL/SDK and the board # definition's pins.csv. # The pins.csv file can contain empty lines, comments (a line beginning with "#") # or pin definition lines. Pin definition lines must be of the form: # # board,cpu # # Where "board" is the user-facing name of the pin as specified by the particular # board layout and markings, and "cpu" is the corresponding name of the CPU/MCU # pin. # # The "board" entry may be absent if the CPU pin has no additional name, and both # entries may start with "-" to hide them from the corresponding Python dict of # pins, and hence hide them from the user (but they are still accessible in C). # # For example, take the following pins.csv file: # # X1,PA0 # -X2,PA1 # X3,-PA2 # -X4,-PA3 # ,PA4 # ,-PA5 # # The first row here configures: # - The CPU pin PA0 is labelled X1. # - The Python user can access both by the names Pin("X1") and Pin("A0"). # - The Python user can access both by the members Pin.board.X1 and Pin.cpu.A0. # - In C code they are available as pyb_pin_X1 and pin_A0. # # Prefixing the names with "-" hides them from the user. The following table # summarises the various possibilities: # # pins.csv entry | board name | cpu name | C board name | C cpu name # ---------------+------------+----------+--------------+----------- # X1,PA0 "X1" "A0" pyb_pin_X1 pin_A0 # -X2,PA1 - "A1" pyb_pin_X2 pin_A1 # X3,-PA2 "X3" - pyb_pin_X3 pin_A2 # -X4,-PA3 - - pyb_pin_X4 pin_A3 # ,PA4 - "A4" - pin_A4 # ,-PA5 - - - pin_A5 import argparse import csv import os import sys class PinGeneratorError(Exception): pass # A port should define a subclass of Pin that knows how to validate cpu/board # names and emits the required structures. class Pin: def __init__(self, cpu_pin_name): self._cpu_pin_name = cpu_pin_name # Optional aliases for the board from pins.csv. Each entry is a tuple # of (name, hidden). Hidden board pins are in pins.csv with with a "-" # prefix and will available to C but not Python. self._board_pin_names = set() # An unavailable pin is one that is not explicitly mentioned at all in # pins.csv (or added explicitly with PinGenerator.add_cpu_pin). self._available = False # A hidden pin is one that is in pins.csv with a "-" prefix and will # be still available to C but not Python. self._hidden = False # Reference to the PinGenerator instance. self._generator = None # The name of the pin to use in MP_QSTR_{} or pin_{}. Defaults to the cpu name. def name(self): return self._cpu_pin_name # Add a board alias (e.g. from pins.csv). def add_board_pin_name(self, board_pin_name, hidden=False): self._board_pin_names.add( ( board_pin_name, hidden, ) ) # Override this to handle an af specified in af.csv. def add_af(self, af_idx, af_name, af): raise NotImplementedError # Override this to verify that naming matches the MCU standard (e.g. "GPIOn" or "PXn"). @staticmethod def validate_cpu_pin_name(cpu_pin_name): if not cpu_pin_name.strip(): raise PinGeneratorError("Missing cpu pin name") # Override this to provide additional validation of board names. @staticmethod def validate_board_pin_name(board_pin_name): # TODO: ensure this is a valid Python/C identifier and can be used as MP_QSTR_foo. pass # Must be implemented when using NumericPinGenerator. # Returns the integer index, or None to exclude this pin from the table. def index(self): raise NotImplementedError # Can be overridden when using NumericPinGenerator. # Returns a string which is a C expression that evaluates to the index # e.g. `GPIO_NUM_7`. # This is used whenever the index is emitted in source code and defaults # to just returning the pin index as a literal. # Return None to exclude this pin from the table. def index_name(self): i = self.index() return str(i) if i is not None else None # Returns an expression that defines the pin. e.g. `{ .base { ... }, .x }`. # This is used as the RHS of the `const machine_pin_obj_t # pin_EXT_GPIO0_obj =` statements for named pins, and elements of # `machine_pin_obj_table` for numeric pins. # This will typically might be implemented as an invocation of a macro # defined in the port-specific prefix. def definition(self): raise NotImplementedError # Whether the pin object should be declared as "const". This should be True # for most pins, but some special cases (e.g. external pins on rp2) might # need mutable pin objects (e.g. to track current pin state). def is_const(self): return True # Optionally return a preprocessor expression that can be used to suppress # this pin (e.g. `MICROPY_HW_ENABLE_GPIOn`). def enable_macro(self): return None # Override this to output any additional per-pin definitions or other # content that should appear at the start of the source output. # This could be used to define additional objects such as IRQs or AFs. def print_source(self, out_source): pass # A port should define a subclass of PinGenerator (or NumericPinGenerator). class PinGenerator: def __init__(self, pin_type, enable_af=False): self._pins = [] self._pin_type = pin_type self._enable_af = enable_af # Allows a port to define a known cpu pin (without relying on it being in the # csv file). def add_cpu_pin(self, cpu_pin_name, available=True): pin = self._pin_type(cpu_pin_name) pin._available = available self._pins.append(pin) pin._generator = self return pin # Iterate just the available pins (i.e. ones in pins.csv). def available_pins(self, exclude_hidden=False): for pin in self._pins: if not pin._available: continue if exclude_hidden and pin._hidden: continue yield pin # Allows a port to add additional command-line arguments to be handled. def extra_args(self, parser): pass # Load board->cpu mapping from csv. def parse_board_csv(self, filename): with open(filename, "r") as csvfile: rows = csv.reader(csvfile) for linenum, row in enumerate(rows): try: # Skip empty lines, and lines starting with "#". if len(row) == 0 or row[0].startswith("#"): continue # Lines must be pairs of names. if len(row) != 2: raise PinGeneratorError("Expecting two entries in each row") board_pin_name, cpu_pin_name = (x.strip() for x in row) # All rows must include a cpu name. cpu_hidden = False if cpu_pin_name.startswith("-"): cpu_hidden = True cpu_pin_name = cpu_pin_name[1:] self._pin_type.validate_cpu_pin_name(cpu_pin_name) pin = self.find_pin_by_cpu_pin_name(cpu_pin_name, create=True) pin._available = True # It's in pins.csv so must be available. pin._hidden = cpu_hidden # Optionally don't make available to Python. # Rows can optionally alias to a board name. if board_pin_name: board_hidden = False if board_pin_name.startswith("-"): board_hidden = True board_pin_name = board_pin_name[1:] self._pin_type.validate_board_pin_name(board_pin_name) pin.add_board_pin_name(board_pin_name, board_hidden) # Inject "file:line: " into the exception. except PinGeneratorError as er: raise PinGeneratorError("{}:{}: {}".format(filename, linenum, er)) def parse_af_csv(self, filename, header_rows=1, pin_col=0, af_col=1): headings = {} with open(filename, "r") as csvfile: rows = csv.reader(csvfile) for linenum, row in enumerate(rows): try: # Skip empty lines, and lines starting with "#". if len(row) == 0 or row[0].startswith("#"): continue # Consume `header_rows` non-blank/comment rows at the start. if header_rows: if not headings: # If this is the first header row then initialise # the headings dict. for af_idx, header in enumerate(row[af_col:]): headings[af_idx] = header.strip() header_rows -= 1 continue # Lines must be pairs of names. if len(row) <= max(pin_col, af_col): raise PinGeneratorError( "Expecting {} entries in each row".format(max(pin_col, af_col)) ) cpu_pin_name = row[pin_col].strip() if cpu_pin_name == "-": continue self._pin_type.validate_cpu_pin_name(cpu_pin_name) pin = self.find_pin_by_cpu_pin_name(cpu_pin_name, create=True) for af_idx, af in enumerate(row[af_col:]): af = af.strip() if not af: continue pin.add_af(af_idx, headings.get(af_idx, ""), af) # Inject "file:line: " into the exception. except PinGeneratorError as er: raise PinGeneratorError("{}:{}: {}".format(filename, linenum, er)) # Find an existing pin. def find_pin_by_cpu_pin_name(self, cpu_pin_name, create=True): for pin in self._pins: if pin._cpu_pin_name == cpu_pin_name: return pin if create: return self.add_cpu_pin(cpu_pin_name, available=False) else: raise PinGeneratorError("Unknown cpu pin {}".format(cpu_pin_name)) # Print the locals dict for Pin.board. def print_board_locals_dict(self, out_source): print(file=out_source) print( "static const mp_rom_map_elem_t machine_pin_board_pins_locals_dict_table[] = {", file=out_source, ) for pin in self.available_pins(): for board_pin_name, board_hidden in pin._board_pin_names: if board_hidden: # Don't include hidden pins in Pins.board. continue # We don't use the enable macro for board pins, because they # shouldn't be referenced in pins.csv unless they're # available. print( " {{ MP_ROM_QSTR(MP_QSTR_{:s}), MP_ROM_PTR(pin_{:s}) }},".format( board_pin_name, pin.name(), ), file=out_source, ) print("};", file=out_source) print( "MP_DEFINE_CONST_DICT(machine_pin_board_pins_locals_dict, machine_pin_board_pins_locals_dict_table);", file=out_source, ) # Print the locals dict for Pin.cpu. def print_cpu_locals_dict(self, out_source): print(file=out_source) print( "static const mp_rom_map_elem_t machine_pin_cpu_pins_locals_dict_table[] = {", file=out_source, ) for pin in self.available_pins(exclude_hidden=True): m = pin.enable_macro() if m: print(" #if {}".format(m), file=out_source) print( " {{ MP_ROM_QSTR(MP_QSTR_{:s}), MP_ROM_PTR(pin_{:s}) }},".format( pin.name(), pin.name(), ), file=out_source, ) if m: print(" #endif", file=out_source) print("};", file=out_source) print( "MP_DEFINE_CONST_DICT(machine_pin_cpu_pins_locals_dict, machine_pin_cpu_pins_locals_dict_table);", file=out_source, ) # NumericPinGenerator can override this to use an entry in machine_pin_obj_table. def _cpu_pin_pointer(self, pin): return "&pin_{:s}_obj".format(pin.name()) # Allow a port to prefix the board pin macro names with something. # e.g. STM32 does pyb_pin_NAME whereas other ports using pin_NAME. def board_name_define_prefix(self): return "" # Print the pin_CPUNAME and pin_BOARDNAME macros. def print_defines(self, out_header, cpu=True, board=True): # Provide #defines for each cpu pin. for pin in self.available_pins(): print(file=out_header) m = pin.enable_macro() if m: print("#if {}".format(m), file=out_header) # #define pin_CPUNAME (...) if cpu: print( "#define pin_{:s} ({:s})".format(pin.name(), self._cpu_pin_pointer(pin)), file=out_header, ) # #define pin_BOARDNAME (pin_CPUNAME) if board: for board_pin_name, _board_hidden in pin._board_pin_names: # Note: Hidden board pins are still available to C via the macro. # Note: The RHS isn't wrapped in (), which is necessary to make the # STATIC_AF_ macro work on STM32. print( "#define {:s}pin_{:s} pin_{:s}".format( self.board_name_define_prefix(), board_pin_name, pin.name(), ), file=out_header, ) if m: print("#endif", file=out_header) def print_pin_objects(self, out_source): print(file=out_source) for pin in self.available_pins(): m = pin.enable_macro() if m: print("#if {}".format(m), file=out_source) print( "{:s}machine_pin_obj_t pin_{:s}_obj = {:s};".format( "const " if pin.is_const() else "", pin.name(), pin.definition(), ), file=out_source, ) if m: print("#endif", file=out_source) def print_pin_object_externs(self, out_header): print(file=out_header) for pin in self.available_pins(): m = pin.enable_macro() if m: print("#if {}".format(m), file=out_header) print( "extern {:s}machine_pin_obj_t pin_{:s}_obj;".format( "const " if pin.is_const() else "", pin.name(), ), file=out_header, ) if m: print("#endif", file=out_header) def print_source(self, out_source): self.print_pin_objects(out_source) self.print_cpu_locals_dict(out_source) self.print_board_locals_dict(out_source) def print_header(self, out_header): self.print_pin_object_externs(out_header) self.print_defines(out_header) # A port can override this if it has extra input files (e.g. af.csv) to load. def load_inputs(self, out_source): # Optionally load pins.csv to get cpu->board name mappings. if self._enable_af and self.args.af_csv: print("// --af-csv {:s}".format(self.args.af_csv), file=out_source) self.parse_af_csv(self.args.af_csv) # Optionally load pins.csv to get cpu->board name mappings. if self.args.board_csv: print("// --board-csv {:s}".format(self.args.board_csv), file=out_source) self.parse_board_csv(self.args.board_csv) # Prepend the prefix file to the start of the output. if self.args.prefix: print("// --prefix {:s}".format(self.args.prefix), file=out_source) print(file=out_source) with open(self.args.prefix, "r") as prefix_file: print(prefix_file.read(), end="", file=out_source) # A port can override this to do extra work after the main source+header # have been written, such as generating additional header files. def generate_extra_files(self): pass def main(self): parser = argparse.ArgumentParser(description="Generate board specific pin file") parser.add_argument("--board-csv") if self._enable_af: parser.add_argument("--af-csv") parser.add_argument("--prefix") parser.add_argument("--output-source") parser.add_argument("--output-header") self.extra_args(parser) self.args = parser.parse_args() try: with open(self.args.output_source, "w") as out_source: print("// This file was automatically generated by make-pins.py", file=out_source) print("//", file=out_source) # Load additional files (including port-specific ones). self.load_inputs(out_source) # Allow a port to print arbitrary per-pin content. for pin in self.available_pins(): pin.print_source(out_source) # Print the tables and dictionaries. self.print_source(out_source) with open(self.args.output_header, "w") as out_header: self.print_header(out_header) self.generate_extra_files() except PinGeneratorError as er: print(er) sys.exit(1) # For ports that use numeric pin identifiers (e.g. ESP32, rp2). # This emits the machine_pin_obj_t instances as an array (machine_pin_obj_table). class NumericPinGenerator(PinGenerator): # This should return a const expression that is the number of GPIO pins # for this board. def cpu_table_size(self): raise NotImplementedError def print_cpu_table(self, out_source): # Print machine_pin_obj_table, where each element is `[n] = {obj}`. print(file=out_source) print( "const machine_pin_obj_t machine_pin_obj_table[{}] = {{".format(self.cpu_table_size()), file=out_source, ) for pin in self.available_pins(): n = pin.index_name() if n is None: continue m = pin.enable_macro() if m: print(" #if {}".format(m), file=out_source) print( " [{:s}] = {:s},".format( pin.index_name(), pin.definition(), ), file=out_source, ) if m: print(" #endif", file=out_source) print("};", file=out_source) # For pins that do not have an index, print them out in the same style as PinGenerator. print(file=out_source) for pin in self.available_pins(): n = pin.index_name() if n is not None: continue m = pin.enable_macro() if m: print("#if {}".format(m), file=out_source) print( "{:s}machine_pin_obj_t pin_{:s}_obj = {:s};".format( "const " if pin.is_const() else "", pin.name(), pin.definition(), ), file=out_source, ) if m: print("#endif", file=out_source) # Replace PinGenerator's implementation to print the numeric table. def print_source(self, out_source): self.print_cpu_table(out_source) self.print_board_locals_dict(out_source) # Replace PinGenerator's implementation to only print the defines. def print_header(self, out_header): self.print_defines(out_header) def _cpu_pin_pointer(self, pin): n = pin.index_name() if n is not None: return "&machine_pin_obj_table[{:s}]".format(pin.index_name()) else: return super()._cpu_pin_pointer(pin)