#!/usr/bin/env python3 # # Copyright (c) 2017, Linaro Limited # Copyright (c) 2018, Bobby Noelte # # SPDX-License-Identifier: Apache-2.0 # # vim: ai:ts=4:sw=4 import os, fnmatch import re import yaml import argparse from collections import defaultdict from devicetree import parse_file from extract.globals import * import extract.globals from extract.clocks import clocks from extract.compatible import compatible from extract.interrupts import interrupts from extract.reg import reg from extract.flash import flash from extract.default import default def extract_bus_name(node_path, def_label): label = def_label + '_BUS_NAME' prop_alias = {} add_compat_alias(node_path, 'BUS_NAME', label, prop_alias) # Generate defines for node aliases if node_path in aliases: add_prop_aliases( node_path, lambda alias: str_to_label(alias) + '_BUS_NAME', label, prop_alias) insert_defs(node_path, {label: '"' + find_parent_prop(node_path, 'label') + '"'}, prop_alias) def extract_string_prop(node_path, key, label): if node_path not in defs: # Make all defs have the special 'aliases' key, to remove existence # checks elsewhere defs[node_path] = {'aliases': {}} defs[node_path][label] = '"' + reduced[node_path]['props'][key] + '"' def generate_prop_defines(node_path, prop): # Generates #defines (and .conf file values) from the prop # named 'prop' on the device tree node at 'node_path' binding = get_binding(node_path) if 'parent' in binding and 'bus' in binding['parent']: # If the binding specifies a parent for the node, then include the # parent in the #define's generated for the properties parent_path = get_parent_path(node_path) def_label = 'DT_' + node_label(parent_path) + '_' \ + node_label(node_path) else: def_label = 'DT_' + node_label(node_path) names = prop_names(reduced[node_path], prop) if prop == 'reg': reg.extract(node_path, names, def_label, 1) elif prop == 'interrupts' or prop == 'interrupts-extended': interrupts.extract(node_path, prop, names, def_label) elif prop == 'compatible': compatible.extract(node_path, prop, def_label) elif 'clocks' in prop: clocks.extract(node_path, prop, def_label) elif 'pwms' in prop or 'gpios' in prop: prop_values = reduced[node_path]['props'][prop] generic = prop[:-1] # Drop the 's' from the prop # Deprecated the non-'S' form extract_controller(node_path, prop, prop_values, 0, def_label, generic, deprecate=True) extract_controller(node_path, prop, prop_values, 0, def_label, prop) # Deprecated the non-'S' form extract_cells(node_path, prop, prop_values, names, 0, def_label, generic, deprecate=True) extract_cells(node_path, prop, prop_values, names, 0, def_label, prop) else: default.extract(node_path, prop, binding['properties'][prop]['type'], def_label) def generate_node_defines(node_path): # Generates #defines (and .conf file values) from the device # tree node at 'node_path' if get_compat(node_path) not in get_binding_compats(): return # We extract a few different #defines for a flash partition, so it's easier # to handle it in one step if 'partition@' in node_path: flash.extract_partition(node_path) return if get_binding(node_path) is None: return generate_bus_defines(node_path) # Generate per-property ('foo = <1 2 3>', etc.) #defines for yaml_prop, yaml_val in get_binding(node_path)['properties'].items(): if yaml_prop.startswith("#") or yaml_prop.endswith("-map"): continue match = False # Handle each property individually, this ends up handling common # patterns for things like reg, interrupts, etc that we don't need # any special case handling at a node level for prop in reduced[node_path]['props']: if re.fullmatch(yaml_prop, prop): match = True generate_prop_defines(node_path, prop) # Handle the case that we have a boolean property, but its not # in the dts if not match and yaml_val['type'] == 'boolean': generate_prop_defines(node_path, yaml_prop) def generate_bus_defines(node_path): # Generates any node-level #defines related to # # parent: # bus: ... binding = get_binding(node_path) if not ('parent' in binding and 'bus' in binding['parent']): return parent_path = get_parent_path(node_path) # Check that parent has matching child bus value try: parent_binding = get_binding(parent_path) parent_bus = parent_binding['child']['bus'] except (KeyError, TypeError): raise Exception("{0} defines parent {1} as bus master, but {1} is not " "configured as bus master in binding" .format(node_path, parent_path)) if parent_bus != binding['parent']['bus']: raise Exception("{0} defines parent {1} as {2} bus master, but {1} is " "configured as {3} bus master" .format(node_path, parent_path, binding['parent']['bus'], parent_bus)) # Generate *_BUS_NAME #define extract_bus_name( node_path, 'DT_' + node_label(parent_path) + '_' + node_label(node_path)) def prop_names(node, prop_name): # Returns a list with the *-names for the property (reg-names, # interrupt-names, etc.) The list is copied so that it can be modified # in-place later without stomping on the device tree data. # The first case turns 'interrupts' into 'interrupt-names' names = node['props'].get(prop_name[:-1] + '-names', []) or \ node['props'].get(prop_name + '-names', []) if isinstance(names, list): # Allow the list of names to be modified in-place without # stomping on the property return names.copy() return [names] def merge_properties(parent, fname, to_dict, from_dict): # Recursively merges the 'from_dict' dictionary into 'to_dict', to # implement !include. 'parent' is the current parent key being looked at. # 'fname' is the top-level .yaml file. for k in from_dict: if (k in to_dict and isinstance(to_dict[k], dict) and isinstance(from_dict[k], dict)): merge_properties(k, fname, to_dict[k], from_dict[k]) else: to_dict[k] = from_dict[k] # Warn when overriding a property and changing its value... if (k in to_dict and to_dict[k] != from_dict[k] and # ...unless it's the 'title', 'description', or 'version' # property. These are overridden deliberately. not k in {'title', 'version', 'description'} and # Also allow the category to be changed from 'optional' to # 'required' without a warning not (k == "category" and to_dict[k] == "optional" and from_dict[k] == "required")): print("extract_dts_includes.py: {}('{}') merge of property " "'{}': '{}' overwrites '{}'" .format(fname, parent, k, from_dict[k], to_dict[k])) def merge_included_bindings(fname, node): # Recursively merges properties from files !include'd from the 'inherits' # section of the binding. 'fname' is the path to the top-level binding # file, and 'node' the current top-level YAML node being processed. check_binding_properties(node) if 'inherits' in node: for inherited in node.pop('inherits'): inherited = merge_included_bindings(fname, inherited) merge_properties(None, fname, inherited, node) node = inherited return node def check_binding_properties(node): # Checks that the top-level YAML node 'node' has the expected properties. # Prints warnings and substitutes defaults otherwise. if 'title' not in node: print("extract_dts_includes.py: node without 'title' -", node) for prop in 'title', 'description': if prop not in node: node[prop] = "".format(prop) print("extract_dts_includes.py: '{}' property missing " "in '{}' binding. Using '{}'." .format(prop, node['title'], node[prop])) if 'id' in node: print("extract_dts_includes.py: WARNING: id field set " "in '{}', should be removed.".format(node['title'])) def define_str(name, value, value_tabs, is_deprecated=False): line = "#define " + name if is_deprecated: line += " __DEPRECATED_MACRO " return line + (value_tabs - len(line)//8)*'\t' + str(value) + '\n' def write_conf(f): for node in sorted(defs): f.write('# ' + node.split('/')[-1] + '\n') for prop in sorted(defs[node]): if prop != 'aliases' and prop.startswith("DT_"): f.write('%s=%s\n' % (prop, defs[node][prop])) for alias in sorted(defs[node]['aliases']): alias_target = defs[node]['aliases'][alias] if alias_target not in defs[node]: alias_target = defs[node]['aliases'][alias_target] if alias.startswith("DT_"): f.write('%s=%s\n' % (alias, defs[node].get(alias_target))) f.write('\n') def write_header(f, deprecated_only): f.write('''\ /********************************************** * Generated include file * DO NOT MODIFY */ #ifndef GENERATED_DTS_BOARD_UNFIXED_H #define GENERATED_DTS_BOARD_UNFIXED_H ''') def max_dict_key(dct): return max(len(key) for key in dct) for node in sorted(defs): f.write('/* ' + node.split('/')[-1] + ' */\n') maxlen = max_dict_key(defs[node]) if defs[node]['aliases']: maxlen = max(maxlen, max_dict_key(defs[node]['aliases'])) maxlen += len('#define ') value_tabs = (maxlen + 8)//8 # Tabstop index for value if 8*value_tabs - maxlen <= 2: # Add some minimum room between the macro name and the value value_tabs += 1 for prop in sorted(defs[node]): if prop != 'aliases': deprecated_warn = False if prop in deprecated_main: deprecated_warn = True if not prop.startswith('DT_'): deprecated_warn = True if deprecated_only and not deprecated_warn: continue f.write(define_str(prop, defs[node][prop], value_tabs, deprecated_warn)) for alias in sorted(defs[node]['aliases']): alias_target = defs[node]['aliases'][alias] deprecated_warn = False # Mark any non-DT_ prefixed define as deprecated except # for now we special case LED, SW, and *PWM_LED* if not alias.startswith('DT_'): deprecated_warn = True if alias in deprecated: deprecated_warn = True if deprecated_only and not deprecated_warn: continue f.write(define_str(alias, alias_target, value_tabs, deprecated_warn)) f.write('\n') f.write('#endif\n') def load_bindings(root, binding_dirs): find_binding_files(binding_dirs) dts_compats = all_compats(root) compat_to_binding = {} # Maps buses to dictionaries that map compats to YAML nodes bus_to_binding = defaultdict(dict) compats = [] # Add '!include foo.yaml' handling yaml.Loader.add_constructor('!include', yaml_include) loaded_yamls = set() for file in binding_files: # Extract compat from 'constraint:' line for line in open(file, 'r', encoding='utf-8'): match = re.match(r'\s+constraint:\s*"([^"]*)"', line) if match: break else: # No 'constraint:' line found. Move on to next yaml file. continue compat = match.group(1) if compat not in dts_compats or file in loaded_yamls: # The compat does not appear in the device tree, or the yaml # file has already been loaded continue # Found a binding (.yaml file) for a 'compatible' value that # appears in DTS. Load it. loaded_yamls.add(file) if compat not in compats: compats.append(compat) with open(file, 'r', encoding='utf-8') as yf: binding = merge_included_bindings(file, yaml.load(yf, Loader=yaml.Loader)) if 'parent' in binding: bus_to_binding[binding['parent']['bus']][compat] = binding compat_to_binding[compat] = binding if not compat_to_binding: raise Exception("No bindings found in '{}'".format(binding_dirs)) extract.globals.bindings = compat_to_binding extract.globals.bus_bindings = bus_to_binding extract.globals.binding_compats = compats def find_binding_files(binding_dirs): # Initializes the global 'binding_files' variable with a list of paths to # binding (.yaml) files global binding_files binding_files = [] for binding_dir in binding_dirs: for root, _, filenames in os.walk(binding_dir): for filename in fnmatch.filter(filenames, '*.yaml'): binding_files.append(os.path.join(root, filename)) def yaml_include(loader, node): # Implements !include. Returns a list with the top-level YAML structures # for the included files (a single-element list if there's just one file). if isinstance(node, yaml.ScalarNode): # !include foo.yaml return [load_binding_file(loader.construct_scalar(node))] if isinstance(node, yaml.SequenceNode): # !include [foo.yaml, bar.yaml] return [load_binding_file(fname) for fname in loader.construct_sequence(node)] yaml_inc_error("Error: unrecognised node type in !include statement") def load_binding_file(fname): # yaml_include() helper for loading an !include'd file. !include takes just # the basename of the file, so we need to make sure that there aren't # multiple candidates. filepaths = [filepath for filepath in binding_files if os.path.basename(filepath) == os.path.basename(fname)] if not filepaths: yaml_inc_error("Error: unknown file name '{}' in !include statement" .format(fname)) if len(filepaths) > 1: yaml_inc_error("Error: multiple candidates for file name '{}' in " "!include statement: {}".format(fname, filepaths)) with open(filepaths[0], 'r', encoding='utf-8') as f: return yaml.load(f, Loader=yaml.Loader) def yaml_inc_error(msg): # Helper for reporting errors in the !include implementation raise yaml.constructor.ConstructorError(None, None, msg) def generate_defines(): # Generates #defines (and .conf file values) from DTS # sorted() otherwise Python < 3.6 randomizes the order of the flash # partition table for node_path in sorted(reduced.keys()): generate_node_defines(node_path) if not defs: raise Exception("No information parsed from dts file.") for k, v in regs_config.items(): if k in chosen: reg.extract(chosen[k], None, v, 1024) for k, v in name_config.items(): if k in chosen: extract_string_prop(chosen[k], "label", v) flash.extract_flash() flash.extract_code_partition() def parse_arguments(): rdh = argparse.RawDescriptionHelpFormatter parser = argparse.ArgumentParser(description=__doc__, formatter_class=rdh) parser.add_argument("-d", "--dts", required=True, help="DTS file") parser.add_argument("-y", "--yaml", nargs='+', required=True, help="YAML file directories, we allow multiple") parser.add_argument("-i", "--include", help="Generate include file for the build system") parser.add_argument("-k", "--keyvalue", help="Generate config file for the build system") parser.add_argument("--old-alias-names", action='store_true', help="Generate aliases also in the old way, without " "compatibility information in their labels") parser.add_argument("--deprecated-only", action='store_true', help="Generate only the deprecated defines") return parser.parse_args() def main(): args = parse_arguments() enable_old_alias_names(args.old_alias_names) # Parse DTS and fetch the root node with open(args.dts, 'r', encoding='utf-8') as f: root = parse_file(f)['/'] # Create some global data structures from the parsed DTS create_reduced(root, '/') create_phandles(root, '/') create_aliases(root) create_chosen(root) # Re-sort instance_id by reg addr # # Note: this is a short term fix and should be removed when # generate defines for instance with a prefix like 'DT_INST' # # Build a dict of dicts, first level is index by compat # second level is index by reg addr compat_reg_dict = defaultdict(dict) for node in reduced.values(): instance = node.get('instance_id') if instance and node['addr'] is not None: for compat in instance: reg = node['addr'] compat_reg_dict[compat][reg] = node # Walk the reg addr in sorted order to re-index 'instance_id' for compat in compat_reg_dict: # only update if we have more than one instance if len(compat_reg_dict[compat]) > 1: for idx, reg_addr in enumerate(sorted(compat_reg_dict[compat])): compat_reg_dict[compat][reg_addr]['instance_id'][compat] = idx # Load any bindings (.yaml files) that match 'compatible' values from the # DTS load_bindings(root, args.yaml) # Generate keys and values for the configuration file and the header file generate_defines() # Write the configuration file and the header file if args.keyvalue is not None: with open(args.keyvalue, 'w', encoding='utf-8') as f: write_conf(f) if args.include is not None: with open(args.include, 'w', encoding='utf-8') as f: write_header(f, args.deprecated_only) if __name__ == '__main__': main()