micropython/tools/boardgen.py

557 lines
22 KiB
Python

# 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)