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