summaryrefslogtreecommitdiff
path: root/zephyr/scripts/named_gpios.py
blob: 2eaf344e7fbbca6e3d5b70a56b5195b10d04f170 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Configure-time checks for the named-gpios node."""

import argparse
import inspect
import logging
from pathlib import Path
import pickle
import re
import site
import sys
from typing import List, Optional


def _load_edt(zephyr_base, edt_pickle):
    """Load an EDT object from a pickle file source.

    Args:
        zephyr_base: pathlib.Path pointing to the Zephyr OS repository.
        edt_pickle: pathlib.Path pointing to the EDT object, stored as a pickle
            file.

    Returns:
        A 3-field tuple: (edtlib, edt, project_name)
            edtlib: module object for the edtlib
            edt: EDT object of the devicetree
            project_name: string containing the name of the project or test.

        Returns None if the edtlib pickle file doesn't exist.
    """
    zephyr_devicetree_path = (
        zephyr_base / "scripts" / "dts" / "python-devicetree" / "src"
    )

    # Add Zephyr's python-devicetree into the source path.
    site.addsitedir(zephyr_devicetree_path)

    try:
        with open(edt_pickle, "rb") as edt_file:
            edt = pickle.load(edt_file)
    except FileNotFoundError:
        # Skip the all EC specific checks if the edt_pickle file doesn't exist.
        # UnpicklingErrors will raise an exception and fail the build.
        return None, None, None

    is_test = re.compile(r"twister-out")

    if is_test.search(edt_pickle.as_posix()):
        # For tests built with twister, the edt.pickle file is located in a
        # path ending <test_name>/zephyr/.
        project_name = edt_pickle.parents[1].name
    else:
        # For Zephyr EC project, the edt.pickle file is located in a path
        # ending <project>/build-[ro|rw|single-image]/zephyr/.
        project_name = edt_pickle.parents[2].name

    edtlib = inspect.getmodule(edt)

    return edtlib, edt, project_name


def _detect_gpios_mismatches(node_name, prop_name, prop_gpios, board_gpios):
    """Verify that all GPIO entries in a -gpios style property match the flags
    specified in the named-gpios node.

    Args:
        node_name   - The name of the node containing the -gpios style property.
        prop_name   - The name of the -gpios style property.
        prop_gpios  - An edtlib.ControllerAndData class object containing
                      the GPIO tuples found in the property.
        board_gpios - A dictionary that maps gpio port/pin tuples to the GPIO
                      flags. This dictionary is initialized from the children
                      found in the named-gpios devicetree node.

    Returns:
        A tuple indicating how many GPIOs were checked, and how many GPIOs
        had a mismatch.
    """
    errors = 0
    count = 0

    # The -gpios property may be an array of GPIO tuples
    for gpio in prop_gpios:
        count += 1
        gpio_pin = gpio.data["pin"]

        # The "flags" cell should be only 16-bits
        dt_flags = gpio.data["flags"] & 0xFFFF

        if gpio.controller.labels[0] is not None:
            nodelabel = gpio.controller.labels[0]
        else:
            nodelabel = gpio.controller.name

        if (nodelabel, gpio_pin) not in board_gpios.keys():
            # GPIO not specified in named-gpios
            logging.debug(
                "Warning: property %s/%s = <%s %s 0x%x> not found in named-gpios",
                node_name,
                prop_name,
                nodelabel,
                gpio_pin,
                dt_flags,
            )
            continue

        if dt_flags != board_gpios[(nodelabel, gpio_pin)]:
            errors += 1
            logging.error(
                "ERROR: property %s/%s = <%s %s 0x%x>. Flags don't match named-gpios 0x%x",
                node_name,
                prop_name,
                nodelabel,
                gpio_pin,
                dt_flags,
                board_gpios[(nodelabel, gpio_pin)],
            )

    return count, errors


def verify_gpios_flags_match(edtlib, edt, project_name):
    """Check that GPIO flags used across devices matches.

    Until all drivers are upstream, the Zephyr EC devicetrees specify GPIOs
    in multiple locations: the "named-gpios" node performs the configuration
    of all GPIOs on the board, and individual drivers may also configure GPIOs.

    This routine finds all "*-gpios" style properties in the devicetree and
    cross-checks the flags set by "named-gpios" and ensures they match.

    Args:
        edtlib: Module object for the edtlib library.
        edt: EDT object representation of a devicetree
        project_name: A string containing the project/test name

    Returns:
        True if no GPIO flag mismatches found.  Returns False otherwise.
    """
    try:
        named_gpios = edt.get_node("/named-gpios")
    except edtlib.EDTError:
        # If the named-gpios node doesn't exist, return success.
        return True

    # Dictionary using the gpio,pin tuple as the key. Value set to the flags
    board_gpios = dict()
    for node in named_gpios.children.values():
        if "gpios" not in node.props:
            continue

        gpios = node.props["gpios"].val

        # edtlib converts a "-gpios" style property to a list of of
        # ControllerAndData objects.  However, the named-gpios node only
        # supports a single GPIO per child, so no need to iterate over
        # the list.
        gpio = gpios[0]
        gpio_pin = gpio.data["pin"]

        if gpio.controller.labels[0] is not None:
            nodelabel = gpio.controller.labels[0]
        else:
            nodelabel = gpio.controller.name

        # The named-gpios stores a 32-bit flags, but Zephyr GPIO flags
        # are limited to the lower 16-bits
        board_gpios[(nodelabel, gpio_pin)] = gpio.data["flags"] & 0xFFFF

    errors = 0
    count = 0
    for node in edt.nodes:
        for prop_name in node.props.keys():
            if prop_name.endswith("-gpios"):
                gpios = node.props[prop_name].val

                prop_gpio_count, prop_gpio_errors = _detect_gpios_mismatches(
                    node.name, prop_name, gpios, board_gpios
                )
                count += prop_gpio_count
                errors += prop_gpio_errors

    if errors:
        logging.error("%d GPIO mismatches found in %s.", errors, project_name)
        return False

    logging.info("Verified %d '*-gpios' properties, all flags match", count)
    return True


def verify_no_duplicates(edtlib, edt, project_name):
    """Verify there are no duplicate GPIOs in the named-gpios node.

    Args:
        edtlib: Module object for the edtlib library.
        edt: EDT object representation of a devicetree
        project_name: A string containing the project/test name

    Returns:
        True if no duplicates found.  Returns False otherwise.
    """
    # Dictionary of GPIO controllers, indexed by the GPIO controller nodelabel
    gpio_ctrls = dict()
    duplicates = 0
    count = 0

    try:
        named_gpios = edt.get_node("/named-gpios")
    except edtlib.EDTError:
        # If the named-gpios node doesn't exist, return success.
        return True

    for node in named_gpios.children.values():
        if "gpios" not in node.props:
            continue

        gpios = node.props["gpios"].val
        count += 1

        # edtlib converts a "-gpios" style property to a list of of
        # ControllerAndData objects.  However, the named-gpios node only
        # supports a single GPIO per child, so no need to iterate over
        # the list.
        gpio = gpios[0]
        gpio_pin = gpio.data["pin"]

        # Note that EDT stores the node name (not a nodelabel) in the
        # Node.name property.  Use the nodelabel, if available as the
        # key for gpio_ctrls.
        if gpio.controller.labels[0] is not None:
            nodelabel = gpio.controller.labels[0]
        else:
            nodelabel = gpio.controller.name

        if not nodelabel in gpio_ctrls:
            # Create a dictionary at each GPIO controller
            gpio_ctrls[nodelabel] = dict()

        if gpio_pin in gpio_ctrls[nodelabel]:
            logging.error(
                "Duplicate GPIOs found at nodes: %s and %s",
                gpio_ctrls[nodelabel][gpio_pin],
                node.name,
            )
            duplicates += 1
        else:
            # Store the node name for the new pin
            gpio_ctrls[nodelabel][gpio_pin] = node.name

    if duplicates:
        logging.error(
            "%d duplicate GPIOs found in %s", duplicates, project_name
        )
        return False

    logging.info("Verified %d GPIOs, no duplicates found", count)
    return True


# Dictionary used to map log level strings to their corresponding int values.
log_level_map = {
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL,
}


def parse_args(argv: Optional[List[str]] = None):
    """Returns parsed command-line arguments"""
    parser = argparse.ArgumentParser(
        prog="named_gpios",
        description="Zephyr EC specific devicetree checks",
    )

    parser.add_argument(
        "--zephyr-base",
        type=Path,
        help="Path to Zephyr OS repository",
        required=True,
    )

    parser.add_argument(
        "--edt-pickle",
        type=Path,
        help="EDT object file, in pickle format",
        required=True,
    )

    parser.add_argument(
        "-l",
        "--log-level",
        choices=log_level_map.values(),
        metavar=f"{{{','.join(log_level_map)}}}",
        type=lambda x: log_level_map[x],
        default=logging.INFO,
        help="Set the logging level (default=INFO)",
    )

    return parser.parse_args(argv)


def main(argv: Optional[List[str]] = None) -> Optional[int]:
    """The main function.

    Args:
        argv: Optionally, the command-line to parse, not including argv[0].

    Returns:
        Zero upon success, or non-zero upon failure.
    """
    args = parse_args(argv)

    log_format = "%(levelname)s: %(message)s"

    logging.basicConfig(format=log_format, level=args.log_level)

    edtlib, edt, project_name = _load_edt(args.zephyr_base, args.edt_pickle)

    if edtlib is None:
        return 0

    if not verify_no_duplicates(edtlib, edt, project_name):
        return 1

    if not verify_gpios_flags_match(edtlib, edt, project_name):
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))