1#!/usr/bin/env python3 2# 3# Copyright (c) 2024 Raspberry Pi Ltd. 4# 5# SPDX-License-Identifier: BSD-3-Clause 6# 7# 8# Simple script to check basic validity of a board-header-file 9# 10# Usage: 11# 12# tools/check_board_header.py src/boards/include/boards/<board.h> 13 14 15import re 16import sys 17import os.path 18import json 19import warnings 20 21from collections import namedtuple 22 23# warnings off by default, because some boards use the same pin for multiple purposes 24show_warnings = False 25 26interfaces_json = "src/rp2040/rp2040_interface_pins.json" 27if not os.path.isfile(interfaces_json): 28 raise Exception("{} doesn't exist".format(interfaces_json)) 29 30board_header = sys.argv[1] 31if not os.path.isfile(board_header): 32 raise Exception("{} doesn't exist".format(board_header)) 33 34with open(interfaces_json) as interfaces_fh: 35 interface_pins = json.load(interfaces_fh) 36 allowed_interfaces = interface_pins["interfaces"] 37 allowed_pins = set(interface_pins["pins"]) 38 # convert instance-keys to integers (allowed by Python but not by JSON) 39 for interface in allowed_interfaces: 40 instances = allowed_interfaces[interface]["instances"] 41 # can't modify a list that we're iterating over, so iterate over a copy 42 instances_copy = list(instances) 43 for instance in instances_copy: 44 instance_num = int(instance) 45 instances[instance_num] = instances.pop(instance) 46 47DefineType = namedtuple("DefineType", ["name", "value", "resolved_value", "lineno"]) 48 49defines = dict() 50pins = dict() # dict of lists 51has_include_guard = False 52has_board_detection = False 53has_include_suggestion = False 54expected_include_suggestion = "/".join(board_header.split("/")[-2:]) 55expected_include_guard = "_" + re.sub(r"\W", "_", expected_include_suggestion.upper()) 56expected_board_detection = re.sub(r"\W", "_", expected_include_suggestion.split("/")[-1].upper()[:-2]) 57 58with open(board_header) as header_fh: 59 last_ifndef = None 60 last_ifndef_lineno = -1 61 board_detection_is_next = False 62 for lineno, line in enumerate(header_fh.readlines()): 63 lineno += 1 64 # strip trailing comments 65 line = re.sub(r"(?<=\S)\s*//.*$", "", line) 66 67 # look for board-detection comment 68 if re.match("// For board detection", line): 69 board_detection_is_next = True 70 continue 71 # check include-suggestion 72 m = re.match(r"^// This header may be included by other board headers as \"(.+?)\"", line) 73 if m: 74 include_suggestion = m.group(1) 75 if include_suggestion == expected_include_suggestion: 76 has_include_suggestion = True 77 else: 78 raise Exception(r"{}:{} Suggests including \"{}\" but file is named \"{}\"".format(board_header, lineno, include_suggestion, expected_include_suggestion)) 79 # look for "#ifndef BLAH_BLAH" 80 m = re.match(r"^#ifndef (\w+)\s*$", line) 81 if m: 82 last_ifndef = m.group(1) 83 last_ifndef_lineno = lineno 84 # look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42" 85 m = re.match(r"^#define (\w+)(?:\s+(.+?))?\s*$", line) 86 if m: 87 #print(m.groups()) 88 name = m.group(1) 89 value = m.group(2) 90 # check all uppercase 91 if name != name.upper(): 92 raise Exception(r"{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 93 # check that adjacent #ifndef and #define lines match up 94 if last_ifndef_lineno + 1 == lineno: 95 if last_ifndef != name: 96 raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name)) 97 if value: 98 try: 99 # most board-defines are integer values 100 value = int(value, 0) 101 except ValueError: 102 pass 103 104 # resolve nested defines 105 resolved_value = value 106 while resolved_value in defines: 107 resolved_value = defines[resolved_value].resolved_value 108 else: 109 resolved_value = None 110 111 define = DefineType(name, value, resolved_value, lineno) 112 113 # check the include-guard define 114 if re.match(r"^_BOARDS_(\w+)_H$", name): 115 # check it has an #ifndef 116 if last_ifndef_lineno +1 != lineno: 117 raise Exception("{}:{} Include-guard #define {} is missing an #ifndef".format(board_header, lineno, name)) 118 if value: 119 raise Exception("{}:{} Include-guard #define {} shouldn't have a value".format(board_header, lineno, name)) 120 if len(defines): 121 raise Exception("{}:{} Include-guard #define {} should be the first define".format(board_header, lineno, name)) 122 if name == expected_include_guard: 123 has_include_guard = True 124 else: 125 raise Exception("{}:{} Found include-guard #define {} but expected {}".format(board_header, lineno, name, expected_include_guard)) 126 # check board-detection define 127 if board_detection_is_next: 128 board_detection_is_next = False 129 if value: 130 raise Exception("{}:{} Board-detection #define {} shouldn't have a value".format(board_header, lineno, name)) 131 # this is a bit messy because pico.h does "#define RASPBERRYPI_PICO" and metrotech_xerxes_rp2040.h does "#define XERXES_RP2040" 132 if name.endswith(expected_board_detection) or expected_board_detection.endswith(name): 133 has_board_detection = True 134 else: 135 raise Exception("{}:{} Board-detection #define {} should end with {}".format(board_header, lineno, name, expected_board_detection)) 136 # check for multiply-defined values 137 if name in defines: 138 raise Exception("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines[name].value, value)) 139 else: 140 defines[name] = define 141 142 # check for pin-conflicts 143 if name.endswith("_PIN"): 144 if resolved_value is None: 145 raise Exception("{}:{} {} is set to an undefined value".format(board_header, lineno, name)) 146 elif not isinstance(resolved_value, int): 147 raise Exception("{}:{} {} resolves to a non-integer value {}".format(board_header, lineno, name, resolved_value)) 148 else: 149 if resolved_value in pins and resolved_value == value: 150 if show_warnings: 151 warnings.warn("{}:{} Both {} and {} claim to be pin {}".format(board_header, lineno, pins[resolved_value][0].name, name, resolved_value)) 152 pins[resolved_value].append(define) 153 else: 154 if resolved_value not in allowed_pins: 155 raise Exception("{}:{} Pin {} for {} isn't a valid pin-number".format(board_header, lineno, resolved_value, name)) 156 pins[resolved_value] = [define] 157 158#import pprint; pprint.pprint(dict(sorted(defines.items(), key=lambda x: x[1].lineno))) 159 160# check for invalid DEFAULT mappings 161for name, define in defines.items(): 162 m = re.match("^(PICO_DEFAULT_([A-Z0-9]+))_([A-Z0-9]+)_PIN$", name) 163 if m: 164 instance_name = m.group(1) 165 interface = m.group(2) 166 function = m.group(3) 167 if interface == "WS2812": 168 continue 169 if interface not in allowed_interfaces: 170 raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json)) 171 if instance_name not in defines: 172 raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, instance_name)) 173 instance_define = defines[instance_name] 174 instance_num = instance_define.resolved_value 175 if instance_num not in allowed_interfaces[interface]["instances"]: 176 raise Exception("{}:{} {} is set to an invalid instance {}".format(board_header, instance_define.lineno, instance_define, instance_num)) 177 interface_instance = allowed_interfaces[interface]["instances"][instance_num] 178 if function not in interface_instance: 179 raise Exception("{}:{} {} is defined but {} isn't a valid function for {}".format(board_header, define.lineno, name, function, instance_define)) 180 if define.resolved_value not in interface_instance[function]: 181 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)) 182 183def list_to_string_with(lst, joiner): 184 elems = len(lst) 185 if elems == 0: 186 return "" 187 elif elems == 1: 188 return str(lst[0]) 189 else: 190 return "{} {} {}".format(", ".join(str(l) for l in lst[:-1]), joiner, lst[-1]) 191 192# check that each used DEFAULT interface includes (at least) the expected pin-functions 193for name, define in defines.items(): 194 m = re.match("^PICO_DEFAULT_([A-Z0-9]+)$", name) 195 if m: 196 interface = m.group(1) 197 if interface not in allowed_interfaces: 198 raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json)) 199 if "expected_functions" in allowed_interfaces[interface]: 200 expected_functions = allowed_interfaces[interface]["expected_functions"] 201 if "required" in expected_functions: 202 for function in expected_functions["required"]: 203 expected_function_pin = "{}_{}_PIN".format(name, function) 204 if expected_function_pin not in defines: 205 raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, expected_function_pin)) 206 if "one_of" in expected_functions: 207 expected_function_pins = list("{}_{}_PIN".format(name, function) for function in expected_functions["one_of"]) 208 if not any(func_pin in defines for func_pin in expected_function_pins): 209 raise Exception("{}:{} {} is defined but none of {} are defined".format(board_header, define.lineno, name, list_to_string_with(expected_function_pins, "or"))) 210 211if not has_include_guard: 212 raise Exception("{} has no include-guard (expected {})".format(board_header, expected_include_guard)) 213if not has_board_detection and expected_board_detection != "NONE": 214 raise Exception("{} has no board-detection #define (expected {})".format(board_header, expected_board_detection)) 215# lots of headers don't have this 216#if not has_include_suggestion: 217# raise Exception("{} has no include-suggestion (expected {})".format(board_header, expected_include_suggestion)) 218 219