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