#!/usr/bin/env python3 # Copyright (c) 2019 Nordic Semiconductor ASA # Copyright (c) 2019 Linaro Limited # SPDX-License-Identifier: BSD-3-Clause # This script uses edtlib to generate a header file and a .conf file (both # containing the same values) from a devicetree (.dts) file. Information from # binding files in YAML format is used as well. # # Bindings are files that describe devicetree nodes. Devicetree nodes are # usually mapped to bindings via their 'compatible = "..."' property. # # See the docstring/comments at the top of edtlib.py for more information. # # Note: Do not access private (_-prefixed) identifiers from edtlib here (and # also note that edtlib is not meant to expose the dtlib API directly). # Instead, think of what API you need, and add it as a public documented API in # edtlib. This will keep this script simple. import argparse import os import pathlib import sys import edtlib def main(): global conf_file global header_file args = parse_args() try: edt = edtlib.EDT(args.dts, args.bindings_dirs) except edtlib.EDTError as e: sys.exit("devicetree error: " + str(e)) conf_file = open(args.conf_out, "w", encoding="utf-8") header_file = open(args.header_out, "w", encoding="utf-8") write_top_comment(edt) active_compats = set() for node in edt.nodes: if node.enabled and node.matching_compat: # Skip 'fixed-partitions' devices since they are handled by # write_flash() and would generate extra spurious #defines if node.matching_compat == "fixed-partitions": continue write_node_comment(node) write_regs(node) write_irqs(node) write_props(node) write_clocks(node) write_spi_dev(node) write_bus(node) write_existence_flags(node) active_compats.update(node.compats) out_comment("Active compatibles (mentioned in DTS + binding found)") for compat in sorted(active_compats): #define DT_COMPAT_ 1 out("COMPAT_{}".format(str2ident(compat)), 1) # Derived from /chosen write_addr_size(edt, "zephyr,sram", "SRAM") write_addr_size(edt, "zephyr,ccm", "CCM") write_addr_size(edt, "zephyr,dtcm", "DTCM") write_addr_size(edt, "zephyr,ipc_shm", "IPC_SHM") write_flash(edt) print("Devicetree configuration written to " + args.conf_out) def parse_args(): # Returns parsed command-line arguments parser = argparse.ArgumentParser() parser.add_argument("--dts", required=True, help="DTS file") parser.add_argument("--bindings-dirs", nargs='+', required=True, help="directory with bindings in YAML format, " "we allow multiple") parser.add_argument("--header-out", required=True, help="path to write header to") parser.add_argument("--conf-out", required=True, help="path to write configuration file to") return parser.parse_args() def write_top_comment(edt): # Writes an overview comment with misc. info at the top of the header and # configuration file s = """\ Generated by gen_defines.py DTS input file: {} Directories with bindings: {} Nodes in dependency order (ordinal and path): """.format(edt.dts_path, ", ".join(map(relativize, edt.bindings_dirs))) for scc in edt.scc_order(): if len(scc) > 1: err("cycle in devicetree involving " + ", ".join(node.path for node in scc)) s += " {0.dep_ordinal:<3} {0.path}\n".format(scc[0]) out_comment(s, blank_before=False) def write_node_comment(node): # Writes a comment describing 'node' to the header and configuration file s = """\ Devicetree node: {} Binding (compatible = {}): {} Dependency Ordinal: {} """.format(node.path, node.matching_compat, relativize(node.binding_path), node.dep_ordinal) if node.depends_on: s += "\nRequires:\n" for dep in node.depends_on: s += " {:<3} {}\n".format(dep.dep_ordinal, dep.path) if node.required_by: s += "\nSupports:\n" for req in node.required_by: s += " {:<3} {}\n".format(req.dep_ordinal, req.path) # Indent description by two spaces s += "\nDescription:\n" + \ "\n".join(" " + line for line in node.description.splitlines()) out_comment(s) def relativize(path): # If 'path' is within $ZEPHYR_BASE, returns it relative to $ZEPHYR_BASE, # with a "$ZEPHYR_BASE/..." hint at the start of the string. Otherwise, # returns 'path' unchanged. zbase = os.getenv("ZEPHYR_BASE") if zbase is None: return path try: return str("$ZEPHYR_BASE" / pathlib.Path(path).relative_to(zbase)) except ValueError: # Not within ZEPHYR_BASE return path def write_regs(node): # Writes address/size output for the registers in the node's 'reg' property def reg_addr_name_alias(reg): return str2ident(reg.name) + "_BASE_ADDRESS" if reg.name else None def reg_size_name_alias(reg): return str2ident(reg.name) + "_SIZE" if reg.name else None for reg in node.regs: out_dev(node, reg_addr_ident(reg), hex(reg.addr), name_alias=reg_addr_name_alias(reg)) if reg.size: out_dev(node, reg_size_ident(reg), reg.size, name_alias=reg_size_name_alias(reg)) def write_props(node): # Writes any properties defined in the "properties" section of the binding # for the node for prop in node.props.values(): # Skip #size-cell and other property starting with #. Also skip mapping # properties like 'gpio-map'. if prop.name[0] == "#" or prop.name.endswith("-map"): continue # See write_clocks() if prop.name == "clocks": continue # edtlib provides these as well (Property.val becomes an edtlib.Node # and a list of edtlib.Nodes, respectively). Nothing is generated for # them currently though. if prop.type in {"phandle", "phandles"}: continue # Skip properties that we handle elsewhere if prop.name in { "reg", "compatible", "status", "interrupts", "interrupt-controller", "gpio-controller" }: continue if prop.description is not None: out_comment(prop.description, blank_before=False) ident = str2ident(prop.name) if prop.type == "boolean": out_dev(node, ident, 1 if prop.val else 0) elif prop.type == "string": out_dev_s(node, ident, prop.val) elif prop.type == "int": out_dev(node, ident, prop.val) elif prop.type == "array": for i, val in enumerate(prop.val): out_dev(node, "{}_{}".format(ident, i), val) out_dev(node, ident, "{" + ", ".join(map(str, prop.val)) + "}") elif prop.type == "string-array": for i, val in enumerate(prop.val): out_dev_s(node, "{}_{}".format(ident, i), val) elif prop.type == "uint8-array": out_dev(node, ident, "{ " + ", ".join("0x{:02x}".format(b) for b in prop.val) + " }") elif prop.type == "phandle-array": write_phandle_val_list(prop) # Generate DT_..._ENUM if there's an 'enum:' key in the binding if prop.enum_index is not None: out_dev(node, ident + "_ENUM", prop.enum_index) def write_bus(node): # Generate bus-related #defines if not node.bus: return if node.parent.label is None: err("missing 'label' property on {!r}".format(node.parent)) # #define DT__BUS_NAME out_dev_s(node, "BUS_NAME", str2ident(node.parent.label)) for compat in node.compats: # #define DT__BUS_ 1 out("{}_BUS_{}".format(str2ident(compat), str2ident(node.bus)), 1) def write_existence_flags(node): # Generate #defines of the form # # #define DT_INST__ 1 # # These are flags for which devices exist. for compat in node.compats: out("INST_{}_{}".format(node.instance_no[compat], str2ident(compat)), 1) def reg_addr_ident(reg): # Returns the identifier (e.g., macro name) to be used for the address of # 'reg' in the output node = reg.node # NOTE: to maintain compat wit the old script we special case if there's # only a single register (we drop the '_0'). if len(node.regs) > 1: return "BASE_ADDRESS_{}".format(node.regs.index(reg)) else: return "BASE_ADDRESS" def reg_size_ident(reg): # Returns the identifier (e.g., macro name) to be used for the size of # 'reg' in the output node = reg.node # NOTE: to maintain compat wit the old script we special case if there's # only a single register (we drop the '_0'). if len(node.regs) > 1: return "SIZE_{}".format(node.regs.index(reg)) else: return "SIZE" def dev_ident(node): # Returns an identifier for the device given by 'node'. Used when building # e.g. macro names. # TODO: Handle PWM on STM # TODO: Better document the rules of how we generate things ident = "" if node.bus: ident += "{}_{:X}_".format( str2ident(node.parent.matching_compat), node.parent.unit_addr) ident += "{}_".format(str2ident(node.matching_compat)) if node.unit_addr is not None: ident += "{:X}".format(node.unit_addr) elif node.parent.unit_addr is not None: ident += "{:X}_{}".format(node.parent.unit_addr, str2ident(node.name)) else: # This is a bit of a hack ident += "{}".format(str2ident(node.name)) return ident def dev_aliases(node): # Returns a list of aliases for the device given by 'node', used e.g. when # building macro names return dev_path_aliases(node) + dev_instance_aliases(node) def dev_path_aliases(node): # Returns a list of aliases for the device given by 'node', based on the # aliases registered for it, in the /aliases node. Used when building e.g. # macro names. if node.matching_compat is None: return [] compat_s = str2ident(node.matching_compat) aliases = [] for alias in node.aliases: aliases.append("ALIAS_{}".format(str2ident(alias))) # TODO: See if we can remove or deprecate this form aliases.append("{}_{}".format(compat_s, str2ident(alias))) return aliases def dev_instance_aliases(node): # Returns a list of aliases for the device given by 'node', based on the # instance number of the device (based on how many instances of that # particular device there are). # # This is a list since a device can have multiple 'compatible' strings, # each with their own instance number. return ["INST_{}_{}".format(node.instance_no[compat], str2ident(compat)) for compat in node.compats] def write_addr_size(edt, prop_name, prefix): # Writes _BASE_ADDRESS and _SIZE for the node pointed at by # the /chosen property named 'prop_name', if it exists node = edt.chosen_node(prop_name) if not node: return if not node.regs: err("missing 'reg' property in node pointed at by /chosen/{} ({!r})" .format(prop_name, node)) out_comment("/chosen/{} ({})".format(prop_name, node.path)) out("{}_BASE_ADDRESS".format(prefix), hex(node.regs[0].addr)) out("{}_SIZE".format(prefix), node.regs[0].size//1024) def write_flash(edt): # Writes flash-related output write_flash_node(edt) write_code_partition(edt) flash_index = 0 for node in edt.nodes: if node.name.startswith("partition@"): write_flash_partition(node, flash_index) flash_index += 1 if flash_index != 0: out_comment("Number of flash partitions") out("FLASH_AREA_NUM", flash_index) def write_flash_node(edt): # Writes output for the top-level flash node pointed at by # zephyr,flash in /chosen node = edt.chosen_node("zephyr,flash") out_comment("/chosen/zephyr,flash ({})" .format(node.path if node else "missing")) if not node: # No flash node. Write dummy values. out("FLASH_BASE_ADDRESS", 0) out("FLASH_SIZE", 0) return if len(node.regs) != 1: err("expected zephyr,flash to have a single register, has {}" .format(len(node.regs))) if node.bus == "spi" and len(node.parent.regs) == 2: reg = node.parent.regs[1] # QSPI flash else: reg = node.regs[0] out("FLASH_BASE_ADDRESS", hex(reg.addr)) if reg.size: out("FLASH_SIZE", reg.size//1024) if "erase-block-size" in node.props: out("FLASH_ERASE_BLOCK_SIZE", node.props["erase-block-size"].val) if "write-block-size" in node.props: out("FLASH_WRITE_BLOCK_SIZE", node.props["write-block-size"].val) def write_code_partition(edt): # Writes output for the node pointed at by zephyr,code-partition in /chosen node = edt.chosen_node("zephyr,code-partition") out_comment("/chosen/zephyr,code-partition ({})" .format(node.path if node else "missing")) if not node: # No code partition. Write dummy values. out("CODE_PARTITION_OFFSET", 0) out("CODE_PARTITION_SIZE", 0) return if not node.regs: err("missing 'regs' property on {!r}".format(node)) out("CODE_PARTITION_OFFSET", node.regs[0].addr) out("CODE_PARTITION_SIZE", node.regs[0].size) def write_flash_partition(partition_node, index): out_comment("Flash partition at " + partition_node.path) if partition_node.label is None: err("missing 'label' property on {!r}".format(partition_node)) # Generate label-based identifiers write_flash_partition_prefix( "FLASH_AREA_" + str2ident(partition_node.label), partition_node, index) # Generate index-based identifiers write_flash_partition_prefix( "FLASH_AREA_{}".format(index), partition_node, index) def write_flash_partition_prefix(prefix, partition_node, index): # write_flash_partition() helper. Generates identifiers starting with # 'prefix'. out("{}_ID".format(prefix), index) out("{}_READ_ONLY".format(prefix), 1 if partition_node.read_only else 0) for i, reg in enumerate(partition_node.regs): # Also add aliases that point to the first sector (TODO: get rid of the # aliases?) out("{}_OFFSET_{}".format(prefix, i), reg.addr, aliases=["{}_OFFSET".format(prefix)] if i == 0 else []) out("{}_SIZE_{}".format(prefix, i), reg.size, aliases=["{}_SIZE".format(prefix)] if i == 0 else []) controller = partition_node.flash_controller if controller.label is not None: out_s("{}_DEV".format(prefix), controller.label) def write_irqs(node): # Writes IRQ num and data for the interrupts in the node's 'interrupt' # property def irq_name_alias(irq, cell_name): if not irq.name: return None alias = "IRQ_{}".format(str2ident(irq.name)) if cell_name != "irq": alias += "_" + str2ident(cell_name) return alias def map_arm_gic_irq_type(irq, irq_num): # Maps ARM GIC IRQ (type)+(index) combo to linear IRQ number if "type" not in irq.data: err("Expected binding for {!r} to have 'type' in interrupt-cells" .format(irq.controller)) irq_type = irq.data["type"] if irq_type == 0: # GIC_SPI return irq_num + 32 if irq_type == 1: # GIC_PPI return irq_num + 16 err("Invalid interrupt type specified for {!r}" .format(irq)) def encode_zephyr_multi_level_irq(irq, irq_num): # See doc/reference/kernel/other/interrupts.rst for details # on how this encoding works irq_ctrl = irq.controller # Look for interrupt controller parent until we have none while irq_ctrl.interrupts: irq_num = (irq_num + 1) << 8 if "irq" not in irq_ctrl.interrupts[0].data: err("Expected binding for {!r} to have 'irq' in interrupt-cells" .format(irq_ctrl)) irq_num |= irq_ctrl.interrupts[0].data["irq"] irq_ctrl = irq_ctrl.interrupts[0].controller return irq_num for irq_i, irq in enumerate(node.interrupts): for cell_name, cell_value in irq.data.items(): ident = "IRQ_{}".format(irq_i) if cell_name == "irq": if "arm,gic" in irq.controller.compats: cell_value = map_arm_gic_irq_type(irq, cell_value) cell_value = encode_zephyr_multi_level_irq(irq, cell_value) else: ident += "_" + str2ident(cell_name) out_dev(node, ident, cell_value, name_alias=irq_name_alias(irq, cell_name)) def write_spi_dev(node): # Writes SPI device GPIO chip select data if there is any cs_gpio = edtlib.spi_dev_cs_gpio(node) if cs_gpio is not None: write_phandle_val_list_entry(node, cs_gpio, None, "CS_GPIOS") def write_phandle_val_list(prop): # Writes output for a phandle/value list, e.g. # # pwms = <&pwm-ctrl-1 10 20 # &pwm-ctrl-2 30 40>; # # prop: # phandle/value Property instance. # # If only one entry appears in 'prop' (the example above has two), the # generated identifier won't get a '_0' suffix, and the '_COUNT' and # group initializer are skipped too. # # The base identifier is derived from the property name. For example, 'pwms = ...' # generates output like this: # # #define _PWMS_CONTROLLER_0 "PWM_0" (name taken from 'label = ...') # #define _PWMS_CHANNEL_0 123 (name taken from *-cells in binding) # #define _PWMS_0 {"PWM_0", 123} # #define _PWMS_CONTROLLER_1 "PWM_1" # #define _PWMS_CHANNEL_1 456 # #define _PWMS_1 {"PWM_1", 456} # #define _PWMS_COUNT 2 # #define _PWMS {_PWMS_0, _PWMS_1} # ... # pwms -> PWMS # foo-gpios -> FOO_GPIOS ident = str2ident(prop.name) initializer_vals = [] for i, entry in enumerate(prop.val): initializer_vals.append(write_phandle_val_list_entry( prop.node, entry, i if len(prop.val) > 1 else None, ident)) if len(prop.val) > 1: out_dev(prop.node, ident + "_COUNT", len(initializer_vals)) out_dev(prop.node, ident, "{" + ", ".join(initializer_vals) + "}") def write_phandle_val_list_entry(node, entry, i, ident): # write_phandle_val_list() helper. We could get rid of it if it wasn't for # write_spi_dev(). Adds 'i' as an index to identifiers unless it's None. # # 'entry' is an edtlib.ControllerAndData instance. # # Returns the identifier for the macro that provides the # initializer for the entire entry. initializer_vals = [] if entry.controller.label is not None: ctrl_ident = ident + "_CONTROLLER" # e.g. PWMS_CONTROLLER if entry.name: name_alias = str2ident(entry.name) + "_" + ctrl_ident else: name_alias = None # Ugly backwards compatibility hack. Only add the index if there's # more than one entry. if i is not None: ctrl_ident += "_{}".format(i) initializer_vals.append(quote_str(entry.controller.label)) out_dev_s(node, ctrl_ident, entry.controller.label, name_alias) for cell, val in entry.data.items(): cell_ident = ident + "_" + str2ident(cell) # e.g. PWMS_CHANNEL if entry.name: # From e.g. 'pwm-names = ...' name_alias = str2ident(entry.name) + "_" + cell_ident else: name_alias = None # Backwards compatibility (see above) if i is not None: cell_ident += "_{}".format(i) out_dev(node, cell_ident, val, name_alias) initializer_vals += entry.data.values() initializer_ident = ident if entry.name: name_alias = initializer_ident + "_" + str2ident(entry.name) else: name_alias = None if i is not None: initializer_ident += "_{}".format(i) return out_dev(node, initializer_ident, "{" + ", ".join(map(str, initializer_vals)) + "}", name_alias) def write_clocks(node): # Writes clock information. # # Most of this ought to be handled in write_props(), but the identifiers # that get generated for 'clocks' are inconsistent with the with other # 'phandle-array' properties. # # See https://github.com/zephyrproject-rtos/zephyr/pull/19327#issuecomment-534081845. if "clocks" not in node.props: return for clock_i, clock in enumerate(node.props["clocks"].val): controller = clock.controller if controller.label is not None: out_dev_s(node, "CLOCK_CONTROLLER", controller.label) for name, val in clock.data.items(): if clock_i == 0: clk_name_alias = "CLOCK_" + str2ident(name) else: clk_name_alias = None out_dev(node, "CLOCK_{}_{}".format(str2ident(name), clock_i), val, name_alias=clk_name_alias) if "fixed-clock" not in controller.compats: continue if "clock-frequency" not in controller.props: err("{!r} is a 'fixed-clock' but lacks a 'clock-frequency' " "property".format(controller)) out_dev(node, "CLOCKS_CLOCK_FREQUENCY", controller.props["clock-frequency"].val) def str2ident(s): # Converts 's' to a form suitable for (part of) an identifier return s.replace("-", "_") \ .replace(",", "_") \ .replace("@", "_") \ .replace("/", "_") \ .replace(".", "_") \ .replace("+", "PLUS") \ .upper() def out_dev(node, ident, val, name_alias=None): # Writes an # # _ = # # assignment, along with a set of # # _ # # aliases, for each device alias. If 'name_alias' (a string) is passed, # then these additional aliases are generated: # # _ # _ (for each device alias) # # 'name_alias' is used for reg-names and the like. # # Returns the identifier used for the macro that provides the value # for 'ident' within 'node', e.g. DT_MFG_MODEL_CTL_GPIOS_PIN. dev_prefix = dev_ident(node) aliases = [alias + "_" + ident for alias in dev_aliases(node)] if name_alias is not None: aliases.append(dev_prefix + "_" + name_alias) aliases += [alias + "_" + name_alias for alias in dev_aliases(node)] return out(dev_prefix + "_" + ident, val, aliases) def out_dev_s(node, ident, s, name_alias=None): # Like out_dev(), but emits 's' as a string literal # # Returns the generated macro name for 'ident'. return out_dev(node, ident, quote_str(s), name_alias) def out_s(ident, val): # Like out(), but puts quotes around 'val' and escapes any double # quotes and backslashes within it # # Returns the generated macro name for 'ident'. return out(ident, quote_str(val)) def out(ident, val, aliases=()): # Writes '#define ' to the header and '=' to the # the configuration file. # # Also writes any aliases listed in 'aliases' (an iterable). For the # header, these look like '#define '. For the configuration # file, the value is just repeated as '=' for each alias. # # Returns the generated macro name for 'ident'. print("#define DT_{:40} {}".format(ident, val), file=header_file) primary_ident = "DT_{}".format(ident) # Exclude things that aren't single token values from .conf. At # the moment the only such items are unquoted string # representations of initializer lists, which begin with a curly # brace. output_to_conf = not (isinstance(val, str) and val.startswith("{")) if output_to_conf: print("{}={}".format(primary_ident, val), file=conf_file) for alias in aliases: if alias != ident: print("#define DT_{:40} DT_{}".format(alias, ident), file=header_file) if output_to_conf: # For the configuration file, the value is just repeated for all # the aliases print("DT_{}={}".format(alias, val), file=conf_file) return primary_ident def out_comment(s, blank_before=True): # Writes 's' as a comment to the header and configuration file. 's' is # allowed to have multiple lines. blank_before=True adds a blank line # before the comment. if blank_before: print(file=header_file) print(file=conf_file) if "\n" in s: # Format multi-line comments like # # /* # * first line # * second line # * # * empty line before this line # */ res = ["/*"] for line in s.splitlines(): # Avoid an extra space after '*' for empty lines. They turn red in # Vim if space error checking is on, which is annoying. res.append(" *" if not line.strip() else " * " + line) res.append(" */") print("\n".join(res), file=header_file) else: # Format single-line comments like # # /* foo bar */ print("/* " + s + " */", file=header_file) print("\n".join("# " + line for line in s.splitlines()), file=conf_file) def escape(s): # Backslash-escapes any double quotes and backslashes in 's' # \ must be escaped before " to avoid double escaping return s.replace("\\", "\\\\").replace('"', '\\"') def quote_str(s): # Puts quotes around 's' and escapes any double quotes and # backslashes within it return '"{}"'.format(escape(s)) def err(s): raise Exception(s) if __name__ == "__main__": main()