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 26chip_interfaces = { 27 'RP2040': "src/rp2040/rp2040_interface_pins.json", 28 'RP2350A': "src/rp2350/rp2350a_interface_pins.json", 29 'RP2350B': "src/rp2350/rp2350b_interface_pins.json", 30} 31 32compulsory_cmake_settings = set(['PICO_PLATFORM']) 33compulsory_cmake_default_settings = set(['PICO_FLASH_SIZE_BYTES']) 34matching_cmake_default_settings = set(['PICO_FLASH_SIZE_BYTES', 'PICO_RP2350_A2_SUPPORTED']) 35compulsory_defines = set(['PICO_FLASH_SIZE_BYTES']) 36 37DefineType = namedtuple("DefineType", ["name", "value", "resolved_value", "lineno"]) 38 39def list_to_string_with(lst, joiner): 40 elems = len(lst) 41 if elems == 0: 42 return "" 43 elif elems == 1: 44 return str(lst[0]) 45 else: 46 return "{} {} {}".format(", ".join(str(l) for l in lst[:-1]), joiner, lst[-1]) 47 48 49board_header = sys.argv[1] 50if not os.path.isfile(board_header): 51 raise Exception("{} doesn't exist".format(board_header)) 52board_header_basename = os.path.basename(board_header) 53 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 58defines = dict() 59cmake_settings = dict() 60cmake_default_settings = dict() 61 62has_include_guard = False 63has_board_detection = False 64has_include_suggestion = False 65 66 67def read_defines_from(header_file, defines_dict): 68 with open(header_file) as fh: 69 last_ifndef = None 70 last_ifndef_lineno = -1 71 validity_stack = [True] 72 board_detection_is_next = False 73 for lineno, line in enumerate(fh.readlines()): 74 lineno += 1 75 # strip trailing comments 76 line = re.sub(r"(?<=\S)\s*//.*$", "", line) 77 78 # look for "// pico_cmake_set BLAH_BLAH=42" 79 m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line) 80 if m: 81 #print(m.groups()) 82 name = m.group(1) 83 value = m.group(2) 84 # check all uppercase 85 if name != name.upper(): 86 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 87 # check for multiply-defined values 88 if name in cmake_settings: 89 if cmake_settings[name].value != value: 90 raise Exception("{}:{} Conflicting values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value)) 91 else: 92 if show_warnings: 93 warnings.warn("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value)) 94 else: 95 cmake_settings[name] = DefineType(name, value, None, lineno) 96 continue 97 98 # look for "// pico_cmake_set_default BLAH_BLAH=42" 99 m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line) 100 if m: 101 #print(m.groups()) 102 name = m.group(1) 103 value = m.group(2) 104 # check all uppercase 105 if name != name.upper(): 106 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 107 if name not in cmake_default_settings: 108 cmake_default_settings[name] = DefineType(name, value, None, lineno) 109 continue 110 111 # look for "#else" 112 m = re.match(r"^\s*#else\s*$", line) 113 if m: 114 validity_stack[-1] = not validity_stack[-1] 115 continue 116 117 # look for #endif 118 m = re.match(r"^\s*#endif\s*$", line) 119 if m: 120 validity_stack.pop() 121 continue 122 123 if validity_stack[-1]: 124 # look for "#include "foo.h" 125 m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line) 126 if m: 127 include = m.group(1) 128 #print("Found nested include \"{}\" in {}".format(include, header_file)) 129 assert include.endswith(".h") 130 # assume that the include is also in the boards directory 131 assert "/" not in include or include.startswith("boards/") 132 read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines) 133 continue 134 135 # look for "#if BLAH_BLAH" 136 m = re.match(r"^\s*#if\s+(\w+)\s*$", line) 137 if m: 138 last_if = m.group(1) 139 last_if_lineno = lineno 140 validity_stack.append(bool(defines[last_if].resolved_value)) 141 continue 142 143 # look for "#ifdef BLAH_BLAH" 144 m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line) 145 if m: 146 last_ifdef = m.group(1) 147 last_ifdef_lineno = lineno 148 validity_stack.append(last_ifdef in defines) 149 continue 150 151 # look for "#ifndef BLAH_BLAH" 152 m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line) 153 if m: 154 last_ifndef = m.group(1) 155 last_ifndef_lineno = lineno 156 validity_stack.append(last_ifndef not in defines) 157 continue 158 159 # look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42" 160 m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line) 161 if m: 162 #print(m.groups()) 163 name = m.group(1) 164 value = m.group(2) 165 # check all uppercase 166 if name != name.upper(): 167 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 168 # check that adjacent #ifndef and #define lines match up 169 if last_ifndef_lineno + 1 == lineno: 170 if last_ifndef != name: 171 raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name)) 172 if value: 173 try: 174 # most board-defines are integer values 175 value = int(value, 0) 176 except ValueError: 177 pass 178 179 # resolve nested defines 180 resolved_value = value 181 while resolved_value in defines_dict: 182 resolved_value = defines_dict[resolved_value].resolved_value 183 else: 184 resolved_value = None 185 186 # check for multiply-defined values 187 if name in defines_dict: 188 if defines_dict[name].value != value: 189 raise Exception("{}:{} Conflicting definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value)) 190 else: 191 if show_warnings: 192 warnings.warn("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value)) 193 else: 194 defines_dict[name] = DefineType(name, value, resolved_value, lineno) 195 196 197if board_header_basename == "amethyst_fpga.h": 198 defines['PICO_RP2350'] = DefineType('PICO_RP2350', 1, 1, -1) 199 200with open(board_header) as header_fh: 201 last_ifndef = None 202 last_ifndef_lineno = -1 203 validity_stack = [True] 204 board_detection_is_next = False 205 for lineno, line in enumerate(header_fh.readlines()): 206 lineno += 1 207 # strip trailing comments 208 line = re.sub(r"(?<=\S)\s*//.*$", "", line) 209 210 # look for board-detection comment 211 if re.match("^\s*// For board detection", line): 212 board_detection_is_next = True 213 continue 214 215 # check include-suggestion 216 m = re.match("^\s*// This header may be included by other board headers as \"(.+?)\"", line) 217 if m: 218 include_suggestion = m.group(1) 219 if include_suggestion == expected_include_suggestion: 220 has_include_suggestion = True 221 else: 222 raise Exception("{}:{} Suggests including \"{}\" but file is named \"{}\"".format(board_header, lineno, include_suggestion, expected_include_suggestion)) 223 continue 224 225 # look for "// pico_cmake_set BLAH_BLAH=42" 226 m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line) 227 if m: 228 #print(m.groups()) 229 name = m.group(1) 230 value = m.group(2) 231 # check all uppercase 232 if name != name.upper(): 233 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 234 # check for multiply-defined values 235 if name in cmake_settings: 236 raise Exception("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value)) 237 else: 238 if value: 239 try: 240 # most cmake settings are integer values 241 value = int(value, 0) 242 except ValueError: 243 pass 244 cmake_settings[name] = DefineType(name, value, None, lineno) 245 continue 246 247 # look for "// pico_cmake_set_default BLAH_BLAH=42" 248 m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line) 249 if m: 250 #print(m.groups()) 251 name = m.group(1) 252 value = m.group(2) 253 # check all uppercase 254 if name != name.upper(): 255 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 256 # check for multiply-defined values 257 if name in cmake_default_settings: 258 raise Exception("{}:{} Multiple values for pico_cmake_set_default {} ({} and {})".format(board_header, lineno, name, cmake_default_settings[name].value, value)) 259 else: 260 if value: 261 try: 262 # most cmake settings are integer values 263 value = int(value, 0) 264 except ValueError: 265 pass 266 cmake_default_settings[name] = DefineType(name, value, None, lineno) 267 continue 268 269 # look for "#else" 270 m = re.match(r"^\s*#else\s*$", line) 271 if m: 272 validity_stack[-1] = not validity_stack[-1] 273 continue 274 275 # look for #endif 276 m = re.match(r"^\s*#endif\s*$", line) 277 if m: 278 validity_stack.pop() 279 continue 280 281 if validity_stack[-1]: 282 # look for "#include "foo.h" 283 m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line) 284 if m: 285 include = m.group(1) 286 #print("Found include \"{}\" in {}".format(include, board_header)) 287 assert include.endswith(".h") 288 # assume that the include is also in the boards directory 289 assert "/" not in include or include.startswith("boards/") 290 read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines) 291 continue 292 293 # look for "#if BLAH_BLAH" 294 m = re.match(r"^\s*#if\s+(!)?\s*(\w+)\s*$", line) 295 if m: 296 valid = bool(defines[m.group(2)].resolved_value) 297 if m.group(1): 298 valid = not valid 299 validity_stack.append(valid) 300 continue 301 302 # look for "#ifdef BLAH_BLAH" 303 m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line) 304 if m: 305 validity_stack.append(m.group(1) in defines) 306 continue 307 308 # look for "#ifndef BLAH_BLAH" 309 m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line) 310 if m: 311 last_ifndef = m.group(1) 312 last_ifndef_lineno = lineno 313 validity_stack.append(last_ifndef not in defines) 314 continue 315 316 # look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42" 317 m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line) 318 if m: 319 #print(m.groups()) 320 name = m.group(1) 321 value = m.group(2) 322 # check all uppercase 323 if name != name.upper(): 324 raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name)) 325 # check that adjacent #ifndef and #define lines match up 326 if last_ifndef_lineno + 1 == lineno: 327 if last_ifndef != name: 328 raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name)) 329 if value: 330 try: 331 # most board-defines are integer values 332 value = int(value, 0) 333 except ValueError: 334 pass 335 336 # resolve nested defines 337 resolved_value = value 338 while resolved_value in defines: 339 resolved_value = defines[resolved_value].resolved_value 340 else: 341 resolved_value = None 342 343 # check the include-guard define 344 if re.match(r"^_BOARDS_(\w+)_H$", name): 345 # check it has an #ifndef 346 if last_ifndef_lineno +1 != lineno: 347 raise Exception("{}:{} Include-guard #define {} is missing an #ifndef".format(board_header, lineno, name)) 348 if value: 349 raise Exception("{}:{} Include-guard #define {} shouldn't have a value".format(board_header, lineno, name)) 350 if len(defines) and not (len(defines) == 1 and defines[list(defines.keys())[0]].lineno < 0): 351 raise Exception("{}:{} Include-guard #define {} should be the first define".format(board_header, lineno, name)) 352 if name == expected_include_guard: 353 has_include_guard = True 354 else: 355 raise Exception("{}:{} Found include-guard #define {} but expected {}".format(board_header, lineno, name, expected_include_guard)) 356 # check board-detection define 357 if board_detection_is_next: 358 board_detection_is_next = False 359 if value: 360 raise Exception("{}:{} Board-detection #define {} shouldn't have a value".format(board_header, lineno, name)) 361 # this is a bit messy because pico.h does "#define RASPBERRYPI_PICO" and metrotech_xerxes_rp2040.h does "#define XERXES_RP2040" 362 if name.endswith(expected_board_detection) or expected_board_detection.endswith(name): 363 has_board_detection = True 364 else: 365 raise Exception("{}:{} Board-detection #define {} should end with {}".format(board_header, lineno, name, expected_board_detection)) 366 # check for multiply-defined values 367 if name in defines: 368 raise Exception("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines[name].value, value)) 369 else: 370 defines[name] = DefineType(name, value, resolved_value, lineno) 371 continue 372 373 374#import pprint; pprint.pprint(dict(sorted(defines.items(), key=lambda x: x[1].lineno))) 375#import pprint; pprint.pprint(dict(sorted(cmake_settings.items(), key=lambda x: x[1].lineno))) 376#import pprint; pprint.pprint(dict(sorted(cmake_default_settings.items(), key=lambda x: x[1].lineno))) 377chip = None 378if board_header_basename == "none.h": 379 chip = 'RP2040' 380 other_chip = 'RP2350' 381else: 382 for setting in compulsory_cmake_settings: 383 if setting not in cmake_settings: 384 raise Exception("{} is missing a pico_cmake_set {} comment".format(board_header, setting)) 385 if cmake_settings['PICO_PLATFORM'].value == "rp2040": 386 chip = 'RP2040' 387 other_chip = 'RP2350' 388 elif cmake_settings['PICO_PLATFORM'].value == "rp2350": 389 other_chip = 'RP2040' 390 if 'PICO_RP2350A' in defines and defines['PICO_RP2350A'].resolved_value == 1: 391 chip = 'RP2350A' 392 else: 393 chip = 'RP2350B' 394 if not board_header.endswith("amethyst_fpga.h"): 395 if 'PICO_RP2350_A2_SUPPORTED' not in cmake_default_settings: 396 raise Exception("{} uses chip {} but is missing a pico_cmake_set_default {} comment".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED')) 397 if 'PICO_RP2350_A2_SUPPORTED' not in defines: 398 raise Exception("{} uses chip {} but is missing a #define {}".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED')) 399 if defines['PICO_RP2350_A2_SUPPORTED'].resolved_value != 1: 400 raise Exception("{} sets #define {} {} (should be 1)".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED', defines['PICO_RP2350_A2_SUPPORTED'].resolved_value)) 401 for setting in compulsory_cmake_default_settings: 402 if setting not in cmake_default_settings: 403 raise Exception("{} is missing a pico_cmake_set_default {} comment".format(board_header, setting)) 404 for setting in matching_cmake_default_settings: 405 if setting in cmake_default_settings and setting not in defines: 406 raise Exception("{} has pico_cmake_set_default {} but is missing a matching #define".format(board_header, setting)) 407 elif setting in defines and setting not in cmake_default_settings: 408 raise Exception("{} has #define {} but is missing a matching pico_cmake_set_default comment".format(board_header, setting)) 409 elif setting in defines and setting in cmake_default_settings: 410 if cmake_default_settings[setting].value != defines[setting].resolved_value: 411 raise Exception("{} has mismatched pico_cmake_set_default and #define values for {}".format(board_header, setting)) 412 for setting in compulsory_defines: 413 if setting not in defines: 414 raise Exception("{} is missing a #define {}".format(board_header, setting)) 415 416if chip is None: 417 raise Exception("Couldn't determine chip for {}".format(board_header)) 418interfaces_json = chip_interfaces[chip] 419if not os.path.isfile(interfaces_json): 420 raise Exception("{} doesn't exist".format(interfaces_json)) 421 422with open(interfaces_json) as interfaces_fh: 423 interface_pins = json.load(interfaces_fh) 424 allowed_interfaces = interface_pins["interfaces"] 425 allowed_pins = set(interface_pins["pins"]) 426 # convert instance-keys to integers (allowed by Python but not by JSON) 427 for interface in allowed_interfaces: 428 instances = allowed_interfaces[interface]["instances"] 429 # can't modify a list that we're iterating over, so iterate over a copy 430 instances_copy = list(instances) 431 for instance in instances_copy: 432 instance_num = int(instance) 433 instances[instance_num] = instances.pop(instance) 434 435pins = dict() # dict of lists 436for name, define in defines.items(): 437 438 # check for other-chip defines 439 if other_chip in name: 440 raise Exception("{}:{} Header is for {} and so shouldn't have settings for {} ({})".format(board_header, define.lineno, chip, other_chip, name)) 441 442 # check for pin-conflicts 443 if name.endswith("_PIN"): 444 if define.resolved_value is None: 445 raise Exception("{}:{} {} is set to an undefined value".format(board_header, define.lineno, name)) 446 elif not isinstance(define.resolved_value, int): 447 raise Exception("{}:{} {} resolves to a non-integer value {}".format(board_header, define.lineno, name, define.resolved_value)) 448 else: 449 if define.resolved_value in pins and define.resolved_value == define.value: 450 if show_warnings: 451 warnings.warn("{}:{} Both {} and {} claim to be pin {}".format(board_header, define.lineno, pins[define.resolved_value][0].name, name, define.resolved_value)) 452 pins[define.resolved_value].append(define) 453 else: 454 if define.resolved_value not in allowed_pins: 455 raise Exception("{}:{} Pin {} for {} isn't a valid pin-number".format(board_header, define.lineno, define.resolved_value, name)) 456 pins[define.resolved_value] = [define] 457 458 # check for invalid DEFAULT mappings 459 m = re.match("^(PICO_DEFAULT_([A-Z0-9]+))_([A-Z0-9]+)_PIN$", name) 460 if m: 461 instance_name = m.group(1) 462 interface = m.group(2) 463 function = m.group(3) 464 if interface == "WS2812": 465 continue 466 if interface not in allowed_interfaces: 467 raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json)) 468 if instance_name not in defines: 469 raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, instance_name)) 470 instance_define = defines[instance_name] 471 instance_num = instance_define.resolved_value 472 if instance_num not in allowed_interfaces[interface]["instances"]: 473 raise Exception("{}:{} {} is set to an invalid instance {}".format(board_header, instance_define.lineno, instance_define, instance_num)) 474 interface_instance = allowed_interfaces[interface]["instances"][instance_num] 475 if function not in interface_instance: 476 raise Exception("{}:{} {} is defined but {} isn't a valid function for {}".format(board_header, define.lineno, name, function, instance_define)) 477 if define.resolved_value not in interface_instance[function]: 478 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)) 479 480 # check that each used DEFAULT interface includes (at least) the expected pin-functions 481 m = re.match("^PICO_DEFAULT_([A-Z0-9]+)$", name) 482 if m: 483 interface = m.group(1) 484 if interface not in allowed_interfaces: 485 raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json)) 486 if "expected_functions" in allowed_interfaces[interface]: 487 expected_functions = allowed_interfaces[interface]["expected_functions"] 488 if "required" in expected_functions: 489 for function in expected_functions["required"]: 490 expected_function_pin = "{}_{}_PIN".format(name, function) 491 if expected_function_pin not in defines: 492 raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, expected_function_pin)) 493 if "one_of" in expected_functions: 494 expected_function_pins = list("{}_{}_PIN".format(name, function) for function in expected_functions["one_of"]) 495 if not any(func_pin in defines for func_pin in expected_function_pins): 496 raise Exception("{}:{} {} is defined but none of {} are defined".format(board_header, define.lineno, name, list_to_string_with(expected_function_pins, "or"))) 497 498if not has_include_guard: 499 raise Exception("{} has no include-guard (expected {})".format(board_header, expected_include_guard)) 500if not has_board_detection and expected_board_detection != "NONE": 501 raise Exception("{} has no board-detection #define (expected {})".format(board_header, expected_board_detection)) 502# lots of headers don't have this 503#if not has_include_suggestion: 504# raise Exception("{} has no include-suggestion (expected {})".format(board_header, expected_include_suggestion)) 505 506# Check that #if / #ifdef / #ifndef / #else / #endif are correctly balanced 507assert len(validity_stack) == 1 and validity_stack[0] 508