1
0
mirror of https://github.com/raspberrypi/pico-sdk.git synced 2025-08-09 04:22:44 +03:00

More board header checks (#1814)

* Modify check_board_header.py to work with both RP2040- and RP2350-based boards

* Tweaks to the board-header files so that they pass check_board_header.py

* Update tools/check_board_header.py
so that it handles pico_cmake_set_default the same way as cmake/generic_board.cmake does
This commit is contained in:
Andrew Scheller
2024-08-20 18:47:20 +01:00
committed by GitHub
parent 1bdd006c8d
commit 9a41722dd3
29 changed files with 643 additions and 192 deletions

View File

@@ -23,13 +23,373 @@ from collections import namedtuple
# warnings off by default, because some boards use the same pin for multiple purposes
show_warnings = False
interfaces_json = "src/rp2040/rp2040_interface_pins.json"
if not os.path.isfile(interfaces_json):
raise Exception("{} doesn't exist".format(interfaces_json))
chip_interfaces = {
'RP2040': "src/rp2040/rp2040_interface_pins.json",
'RP2350A': "src/rp2350/rp2350a_interface_pins.json",
'RP2350B': "src/rp2350/rp2350b_interface_pins.json",
}
DefineType = namedtuple("DefineType", ["name", "value", "resolved_value", "lineno"])
def list_to_string_with(lst, joiner):
elems = len(lst)
if elems == 0:
return ""
elif elems == 1:
return str(lst[0])
else:
return "{} {} {}".format(", ".join(str(l) for l in lst[:-1]), joiner, lst[-1])
board_header = sys.argv[1]
if not os.path.isfile(board_header):
raise Exception("{} doesn't exist".format(board_header))
board_header_basename = os.path.basename(board_header)
expected_include_suggestion = "/".join(board_header.split("/")[-2:])
expected_include_guard = "_" + re.sub(r"\W", "_", expected_include_suggestion.upper())
expected_board_detection = re.sub(r"\W", "_", expected_include_suggestion.split("/")[-1].upper()[:-2])
defines = dict()
cmake_settings = dict()
cmake_default_settings = dict()
has_include_guard = False
has_board_detection = False
has_include_suggestion = False
def read_defines_from(header_file, defines_dict):
with open(header_file) as fh:
last_ifndef = None
last_ifndef_lineno = -1
validity_stack = [True]
board_detection_is_next = False
for lineno, line in enumerate(fh.readlines()):
lineno += 1
# strip trailing comments
line = re.sub(r"(?<=\S)\s*//.*$", "", line)
# look for "// pico_cmake_set BLAH_BLAH=42"
m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check for multiply-defined values
if name in cmake_settings:
if cmake_settings[name].value != value:
raise Exception("{}:{} Conflicting values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
else:
if show_warnings:
warnings.warn("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
else:
cmake_settings[name] = DefineType(name, value, None, lineno)
continue
# look for "// pico_cmake_set_default BLAH_BLAH=42"
m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
if name not in cmake_default_settings:
cmake_default_settings[name] = DefineType(name, value, None, lineno)
continue
# look for "#else"
m = re.match(r"^\s*#else\s*$", line)
if m:
validity_stack[-1] = not validity_stack[-1]
continue
# look for #endif
m = re.match(r"^\s*#endif\s*$", line)
if m:
validity_stack.pop()
continue
if validity_stack[-1]:
# look for "#include "foo.h"
m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line)
if m:
include = m.group(1)
#print("Found nested include \"{}\" in {}".format(include, header_file))
assert include.endswith(".h")
# assume that the include is also in the boards directory
assert "/" not in include or include.startswith("boards/")
read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines)
continue
# look for "#if BLAH_BLAH"
m = re.match(r"^\s*#if\s+(\w+)\s*$", line)
if m:
last_if = m.group(1)
last_if_lineno = lineno
validity_stack.append(bool(defines[last_if].resolved_value))
continue
# look for "#ifdef BLAH_BLAH"
m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line)
if m:
last_ifdef = m.group(1)
last_ifdef_lineno = lineno
validity_stack.append(last_ifdef in defines)
continue
# look for "#ifndef BLAH_BLAH"
m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line)
if m:
last_ifndef = m.group(1)
last_ifndef_lineno = lineno
validity_stack.append(last_ifndef not in defines)
continue
# look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42"
m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check that adjacent #ifndef and #define lines match up
if last_ifndef_lineno + 1 == lineno:
if last_ifndef != name:
raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name))
if value:
try:
# most board-defines are integer values
value = int(value, 0)
except ValueError:
pass
# resolve nested defines
resolved_value = value
while resolved_value in defines_dict:
resolved_value = defines_dict[resolved_value].resolved_value
else:
resolved_value = None
# check for multiply-defined values
if name in defines_dict:
if defines_dict[name].value != value:
raise Exception("{}:{} Conflicting definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value))
else:
if show_warnings:
warnings.warn("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value))
else:
defines_dict[name] = DefineType(name, value, resolved_value, lineno)
if board_header_basename == "amethyst_fpga.h":
defines['PICO_RP2350'] = DefineType('PICO_RP2350', 1, 1, -1)
with open(board_header) as header_fh:
last_ifndef = None
last_ifndef_lineno = -1
validity_stack = [True]
board_detection_is_next = False
for lineno, line in enumerate(header_fh.readlines()):
lineno += 1
# strip trailing comments
line = re.sub(r"(?<=\S)\s*//.*$", "", line)
# look for board-detection comment
if re.match("^\s*// For board detection", line):
board_detection_is_next = True
continue
# check include-suggestion
m = re.match("^\s*// This header may be included by other board headers as \"(.+?)\"", line)
if m:
include_suggestion = m.group(1)
if include_suggestion == expected_include_suggestion:
has_include_suggestion = True
else:
raise Exception("{}:{} Suggests including \"{}\" but file is named \"{}\"".format(board_header, lineno, include_suggestion, expected_include_suggestion))
continue
# look for "// pico_cmake_set BLAH_BLAH=42"
m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check for multiply-defined values
if name in cmake_settings:
raise Exception("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
else:
cmake_settings[name] = DefineType(name, value, None, lineno)
continue
# look for "// pico_cmake_set_default BLAH_BLAH=42"
m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check for multiply-defined values
if name in cmake_default_settings:
raise Exception("{}:{} Multiple values for pico_cmake_set_default {} ({} and {})".format(board_header, lineno, name, cmake_default_settings[name].value, value))
else:
cmake_default_settings[name] = DefineType(name, value, None, lineno)
continue
# look for "#else"
m = re.match(r"^\s*#else\s*$", line)
if m:
validity_stack[-1] = not validity_stack[-1]
continue
# look for #endif
m = re.match(r"^\s*#endif\s*$", line)
if m:
validity_stack.pop()
continue
if validity_stack[-1]:
# look for "#include "foo.h"
m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line)
if m:
include = m.group(1)
#print("Found include \"{}\" in {}".format(include, board_header))
assert include.endswith(".h")
# assume that the include is also in the boards directory
assert "/" not in include or include.startswith("boards/")
read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines)
continue
# look for "#if BLAH_BLAH"
m = re.match(r"^\s*#if\s+(!)?\s*(\w+)\s*$", line)
if m:
valid = bool(defines[m.group(2)].resolved_value)
if m.group(1):
valid = not valid
validity_stack.append(valid)
continue
# look for "#ifdef BLAH_BLAH"
m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line)
if m:
validity_stack.append(m.group(1) in defines)
continue
# look for "#ifndef BLAH_BLAH"
m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line)
if m:
last_ifndef = m.group(1)
last_ifndef_lineno = lineno
validity_stack.append(last_ifndef not in defines)
continue
# look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42"
m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check that adjacent #ifndef and #define lines match up
if last_ifndef_lineno + 1 == lineno:
if last_ifndef != name:
raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name))
if value:
try:
# most board-defines are integer values
value = int(value, 0)
except ValueError:
pass
# resolve nested defines
resolved_value = value
while resolved_value in defines:
resolved_value = defines[resolved_value].resolved_value
else:
resolved_value = None
# check the include-guard define
if re.match(r"^_BOARDS_(\w+)_H$", name):
# check it has an #ifndef
if last_ifndef_lineno +1 != lineno:
raise Exception("{}:{} Include-guard #define {} is missing an #ifndef".format(board_header, lineno, name))
if value:
raise Exception("{}:{} Include-guard #define {} shouldn't have a value".format(board_header, lineno, name))
if len(defines) and not (len(defines) == 1 and defines[list(defines.keys())[0]].lineno < 0):
raise Exception("{}:{} Include-guard #define {} should be the first define".format(board_header, lineno, name))
if name == expected_include_guard:
has_include_guard = True
else:
raise Exception("{}:{} Found include-guard #define {} but expected {}".format(board_header, lineno, name, expected_include_guard))
# check board-detection define
if board_detection_is_next:
board_detection_is_next = False
if value:
raise Exception("{}:{} Board-detection #define {} shouldn't have a value".format(board_header, lineno, name))
# this is a bit messy because pico.h does "#define RASPBERRYPI_PICO" and metrotech_xerxes_rp2040.h does "#define XERXES_RP2040"
if name.endswith(expected_board_detection) or expected_board_detection.endswith(name):
has_board_detection = True
else:
raise Exception("{}:{} Board-detection #define {} should end with {}".format(board_header, lineno, name, expected_board_detection))
# check for multiply-defined values
if name in defines:
raise Exception("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines[name].value, value))
else:
defines[name] = DefineType(name, value, resolved_value, lineno)
continue
#import pprint; pprint.pprint(dict(sorted(defines.items(), key=lambda x: x[1].lineno)))
#import pprint; pprint.pprint(dict(sorted(cmake_settings.items(), key=lambda x: x[1].lineno)))
#import pprint; pprint.pprint(dict(sorted(cmake_default_settings.items(), key=lambda x: x[1].lineno)))
chip = None
if board_header_basename == "none.h":
chip = 'RP2040'
other_chip = 'RP2350'
else:
if 'PICO_PLATFORM' not in cmake_settings:
raise Exception("{} is missing a pico_cmake_set {} comment".format(board_header, 'PICO_PLATFORM'))
if cmake_settings['PICO_PLATFORM'].value == "rp2040":
chip = 'RP2040'
other_chip = 'RP2350'
elif cmake_settings['PICO_PLATFORM'].value == "rp2350":
other_chip = 'RP2040'
if 'PICO_RP2350A' in defines and defines['PICO_RP2350A'].resolved_value == 1:
chip = 'RP2350A'
else:
chip = 'RP2350B'
if not board_header.endswith("amethyst_fpga.h"):
if 'PICO_RP2350_A2_SUPPORTED' not in defines:
raise Exception("{} uses chip {} but is missing a #define {}".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED'))
if defines['PICO_RP2350_A2_SUPPORTED'].resolved_value != 1:
raise Exception("{} sets #define {} {} (should be 1)".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED', defines['PICO_RP2350_A2_SUPPORTED'].resolved_value))
if 'PICO_FLASH_SIZE_BYTES' not in cmake_default_settings:
raise Exception("{} is missing a pico_cmake_set_default {} comment".format(board_header, 'PICO_FLASH_SIZE_BYTES'))
if 'PICO_FLASH_SIZE_BYTES' not in defines:
raise Exception("{} is missing a #define {}".format(board_header, 'PICO_FLASH_SIZE_BYTES'))
if cmake_default_settings['PICO_FLASH_SIZE_BYTES'].value != defines['PICO_FLASH_SIZE_BYTES'].resolved_value:
raise Exception("{} has mismatched pico_cmake_set_default and #define values for {}".format(board_header, 'PICO_FLASH_SIZE_BYTES'))
if chip is None:
raise Exception("Couldn't determine chip for {}".format(board_header))
interfaces_json = chip_interfaces[chip]
if not os.path.isfile(interfaces_json):
raise Exception("{} doesn't exist".format(interfaces_json))
with open(interfaces_json) as interfaces_fh:
interface_pins = json.load(interfaces_fh)
@@ -44,121 +404,30 @@ with open(interfaces_json) as interfaces_fh:
instance_num = int(instance)
instances[instance_num] = instances.pop(instance)
DefineType = namedtuple("DefineType", ["name", "value", "resolved_value", "lineno"])
defines = dict()
pins = dict() # dict of lists
has_include_guard = False
has_board_detection = False
has_include_suggestion = False
expected_include_suggestion = "/".join(board_header.split("/")[-2:])
expected_include_guard = "_" + re.sub(r"\W", "_", expected_include_suggestion.upper())
expected_board_detection = re.sub(r"\W", "_", expected_include_suggestion.split("/")[-1].upper()[:-2])
with open(board_header) as header_fh:
last_ifndef = None
last_ifndef_lineno = -1
board_detection_is_next = False
for lineno, line in enumerate(header_fh.readlines()):
lineno += 1
# strip trailing comments
line = re.sub(r"(?<=\S)\s*//.*$", "", line)
# look for board-detection comment
if re.match("// For board detection", line):
board_detection_is_next = True
continue
# check include-suggestion
m = re.match(r"^// This header may be included by other board headers as \"(.+?)\"", line)
if m:
include_suggestion = m.group(1)
if include_suggestion == expected_include_suggestion:
has_include_suggestion = True
else:
raise Exception(r"{}:{} Suggests including \"{}\" but file is named \"{}\"".format(board_header, lineno, include_suggestion, expected_include_suggestion))
# look for "#ifndef BLAH_BLAH"
m = re.match(r"^#ifndef (\w+)\s*$", line)
if m:
last_ifndef = m.group(1)
last_ifndef_lineno = lineno
# look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42"
m = re.match(r"^#define (\w+)(?:\s+(.+?))?\s*$", line)
if m:
#print(m.groups())
name = m.group(1)
value = m.group(2)
# check all uppercase
if name != name.upper():
raise Exception(r"{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
# check that adjacent #ifndef and #define lines match up
if last_ifndef_lineno + 1 == lineno:
if last_ifndef != name:
raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name))
if value:
try:
# most board-defines are integer values
value = int(value, 0)
except ValueError:
pass
# resolve nested defines
resolved_value = value
while resolved_value in defines:
resolved_value = defines[resolved_value].resolved_value
else:
resolved_value = None
define = DefineType(name, value, resolved_value, lineno)
# check the include-guard define
if re.match(r"^_BOARDS_(\w+)_H$", name):
# check it has an #ifndef
if last_ifndef_lineno +1 != lineno:
raise Exception("{}:{} Include-guard #define {} is missing an #ifndef".format(board_header, lineno, name))
if value:
raise Exception("{}:{} Include-guard #define {} shouldn't have a value".format(board_header, lineno, name))
if len(defines):
raise Exception("{}:{} Include-guard #define {} should be the first define".format(board_header, lineno, name))
if name == expected_include_guard:
has_include_guard = True
else:
raise Exception("{}:{} Found include-guard #define {} but expected {}".format(board_header, lineno, name, expected_include_guard))
# check board-detection define
if board_detection_is_next:
board_detection_is_next = False
if value:
raise Exception("{}:{} Board-detection #define {} shouldn't have a value".format(board_header, lineno, name))
# this is a bit messy because pico.h does "#define RASPBERRYPI_PICO" and metrotech_xerxes_rp2040.h does "#define XERXES_RP2040"
if name.endswith(expected_board_detection) or expected_board_detection.endswith(name):
has_board_detection = True
else:
raise Exception("{}:{} Board-detection #define {} should end with {}".format(board_header, lineno, name, expected_board_detection))
# check for multiply-defined values
if name in defines:
raise Exception("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines[name].value, value))
else:
defines[name] = define
# check for pin-conflicts
if name.endswith("_PIN"):
if resolved_value is None:
raise Exception("{}:{} {} is set to an undefined value".format(board_header, lineno, name))
elif not isinstance(resolved_value, int):
raise Exception("{}:{} {} resolves to a non-integer value {}".format(board_header, lineno, name, resolved_value))
else:
if resolved_value in pins and resolved_value == value:
if show_warnings:
warnings.warn("{}:{} Both {} and {} claim to be pin {}".format(board_header, lineno, pins[resolved_value][0].name, name, resolved_value))
pins[resolved_value].append(define)
else:
if resolved_value not in allowed_pins:
raise Exception("{}:{} Pin {} for {} isn't a valid pin-number".format(board_header, lineno, resolved_value, name))
pins[resolved_value] = [define]
#import pprint; pprint.pprint(dict(sorted(defines.items(), key=lambda x: x[1].lineno)))
# check for invalid DEFAULT mappings
for name, define in defines.items():
# check for other-chip defines
if other_chip in name:
raise Exception("{}:{} Header is for {} and so shouldn't have settings for {} ({})".format(board_header, define.lineno, chip, other_chip, name))
# check for pin-conflicts
if name.endswith("_PIN"):
if define.resolved_value is None:
raise Exception("{}:{} {} is set to an undefined value".format(board_header, define.lineno, name))
elif not isinstance(define.resolved_value, int):
raise Exception("{}:{} {} resolves to a non-integer value {}".format(board_header, define.lineno, name, define.resolved_value))
else:
if define.resolved_value in pins and define.resolved_value == define.value:
if show_warnings:
warnings.warn("{}:{} Both {} and {} claim to be pin {}".format(board_header, define.lineno, pins[define.resolved_value][0].name, name, define.resolved_value))
pins[define.resolved_value].append(define)
else:
if define.resolved_value not in allowed_pins:
raise Exception("{}:{} Pin {} for {} isn't a valid pin-number".format(board_header, define.lineno, define.resolved_value, name))
pins[define.resolved_value] = [define]
# check for invalid DEFAULT mappings
m = re.match("^(PICO_DEFAULT_([A-Z0-9]+))_([A-Z0-9]+)_PIN$", name)
if m:
instance_name = m.group(1)
@@ -180,17 +449,7 @@ for name, define in defines.items():
if define.resolved_value not in interface_instance[function]:
raise Exception("{}:{} {} is set to {} which isn't a valid pin for {} on {} {}".format(board_header, define.lineno, name, define.resolved_value, function, interface, instance_num))
def list_to_string_with(lst, joiner):
elems = len(lst)
if elems == 0:
return ""
elif elems == 1:
return str(lst[0])
else:
return "{} {} {}".format(", ".join(str(l) for l in lst[:-1]), joiner, lst[-1])
# check that each used DEFAULT interface includes (at least) the expected pin-functions
for name, define in defines.items():
# check that each used DEFAULT interface includes (at least) the expected pin-functions
m = re.match("^PICO_DEFAULT_([A-Z0-9]+)$", name)
if m:
interface = m.group(1)
@@ -216,3 +475,5 @@ if not has_board_detection and expected_board_detection != "NONE":
#if not has_include_suggestion:
# raise Exception("{} has no include-suggestion (expected {})".format(board_header, expected_include_suggestion))
# Check that #if / #ifdef / #ifndef / #else / #endif are correctly balanced
assert len(validity_stack) == 1 and validity_stack[0]