# Copyright (c) 2021, Linaro Limited. # Copyright (c) 2022, NXP # # SPDX-License-Identifier: Apache-2.0 """ Implements a configuration file parser for kinetis MCUs, which can generate pinctrl definitions for Zephyr """ import xml.etree.ElementTree as ET import re import os import collections import logging import pathlib import __main__ # layout/index of pins tuple PIN = collections.namedtuple('PIN', ['PERIPH', 'NAME_PART', 'SIGNAL', 'PORT', 'PIN', 'CH', 'MUX_FUNC']) NAMESPACES = {'mex': 'http://mcuxpresso.nxp.com/XSD/mex_configuration_14'} class MUXOption: """ Internal class representing a mux option on the SOC """ def __init__(self, connection): """ Initializes a mux option @param connection XML connection option from signal_configuration.xml """ self._name = connection.attrib.get('name_part') logging.debug("\t\t %s", self._name) if self._name is None: self._name = '' return # Get MUX settings self._port = None for periph in connection.iter('peripheral_signal_ref'): self._periph = periph.attrib.get('peripheral') self._signal = periph.attrib.get('signal') self._channel = periph.attrib.get('channel') for assign in connection.iter('assign'): reg = assign.attrib.get('register') val = assign.attrib.get('bit_field_value') logging.debug('\t\t\t [ASSIGN] %s %s', reg, val) # Only process PCR registers match = re.match(r'PORT([A-Z])_PCR(\d+)', reg) if match: # For muxes like PTC5, do not append peripheral name if re.match(r'PT[A-Z]\d+', self._name) is None: self._name += f"_PT{match.group(1)}{match.group(2)}" self._port = match.group(1) self._pin = int(match.group(2)) self._mux = int(val, 16) if self._port is None: # Not a valid port mapping. Clear name self._name = '' def __repr__(self): """ String representation of object """ return "MUXOption(%s)" % (self._name) def get_name(self): """ Get mux option name """ return self._name def get_mux_name(self): """ Get name of the mux option, without pin name """ if self._channel: return f"{self._periph}_{self._signal}, {self._channel}" return f"{self._periph}_{self._signal}" def get_port(self): """ Get mux port """ return self._port def get_signal(self): """ Get mux signal name """ return self._signal def get_pin(self): """ Get mux pin """ return self._pin def get_mux(self): """ Get mux register write value """ return self._mux def get_periph(self): """ Get peripheral name """ return self._periph def get_channel(self): """ Get channel number """ return self._channel def __hash__(self): """ Override hash method to return pin name as hash """ return hash(self._name) def __eq__(self, obj): """ Like the hash method, we override the eq method to return true if two objects have the same pin name """ return isinstance(obj, SignalPin) and self._name == obj._name def __lt__(self, obj): """ Compare objects based on name """ if not isinstance(obj, SignalPin): return True return self._name < obj._name class SignalPin: """ Internal class representing a signal on the SOC """ def __init__(self, pin): """ Initializes a SignalPin object @param pin: pin XML object from signal_configuration.xml """ # Kinetis pin names are formatted as [PT[Port][Pin]] pin_regex = re.search(r'PT([A-Z])(\d+)', pin.attrib['name']) if pin_regex is None: logging.debug('Could not match pin name %s', pin.attrib['name']) self._name = '' return self._name = pin.attrib['name'] self._port = pin_regex.group(1) self._pin = pin_regex.group(2) self._properties = self._get_pin_properties(pin.find('functional_properties')) self._mux_options = {} for connections in pin.findall('connections'): mux_opt = MUXOption(connections) # Only append mux options with a valid name if mux_opt.get_name() != '': self._mux_options[mux_opt.get_mux_name()] = mux_opt def __repr__(self): """ String representation of object """ return "SignalPin(%s)" % (self._name) def __hash__(self): """ Override hash method to return pin name as hash """ return hash(self._name) def __eq__(self, obj): """ Like the hash method, we override the eq method to return true if two objects have the same pin name """ return isinstance(obj, SignalPin) and self._name == obj._name def __lt__(self, obj): """ Compare objects based on name """ if not isinstance(obj, SignalPin): return True return self._name < obj._name def get_name(self): """ Get name of pin """ return self._name def get_port(self): """ Get PORT this signal is defined for """ return self._port def get_pin(self): """ Get pin this signal is defined for """ return self._pin def get_mux_connection(self, signal): """ Gets an MUXOption object for the relevant signal name @param signal: Signal name on pin to get mux option for """ if signal in self._mux_options: return self._mux_options[signal] return None def get_mux_options(self): """ Gets all unique settings for IOMUX on the specific pin """ return set(self._mux_options.values()) def get_pin_properties(self): """ Gets array of pin property names """ return self._properties.keys() def get_pin_property_default(self, prop): """ Gets name of default pin property @param prop: name of pin property """ return self._properties[prop]['default'] def get_pin_defaults(self): """ Gets mapping of all pin property names to default value names """ pin_defaults = {} for prop in self.get_pin_properties(): pin_default = self.get_pin_property_default(prop) pin_defaults[prop] = pin_default return pin_defaults def get_pin_property_value(self, prop, selection): """ Gets bit value for pin property @param prop: name of pin property @param selection: name of option selected for property """ return self._properties[prop][selection] def _get_pin_properties(self, props): """ Builds dictionary with all pin properties @param props: pin function_properties XML object in signal_configuration.xml """ prop_mapping = {} for prop in props.findall('functional_property'): prop_id = prop.attrib['id'] if not 'default' in prop.attrib: # No default property. Skip continue prop_mapping[prop_id] = {} prop_mapping[prop_id]['default'] = prop.attrib['default'] for state in prop.findall('state'): reg_assign = state.find('configuration/assign') if reg_assign: bit_value = int(reg_assign.attrib['bit_field_value'], 0) else: # Assume writing zero to register will select default bit_value = 0 prop_mapping[prop_id][state.attrib['id']] = bit_value return prop_mapping class PinGroup: """ Internal class representing pin group """ def __init__(self, function, signal_map): """ Creates a pin group @param function: function xml structure from MEX configuration file @param signal_map: Signal mapping, maps signal names to signal pins """ self._name = function.attrib.get('name') pins = function.find('mex:pins', NAMESPACES) description = function.find('mex:description', NAMESPACES) if description is not None and description.text is not None: # Replace
html tag with newline self._description = description.text.replace("<br/>", "\n") else: self._description = "" # Build dictionary mapping pin properties to pins. This allows us to # group pins based on shared configuration self._pin_groups = collections.defaultdict(lambda: []) for pin in pins: # find signal defintion for this pin signal_name = pin.attrib.get('pin_signal') signal = signal_map[signal_name] if not signal: logging.warning('Signal name %s not present in mapping', signal_name) # No way to find mux option continue # Get mux option for this signal mux_option = f"{pin.attrib.get('peripheral')}_{pin.attrib.get('signal')}" mux = signal.get_mux_connection(mux_option) if mux is None: logging.warning('Signal name %s has no mux', mux_option) # Do not add pinmux option to group continue # Get pin defaults for this pin defaults = signal.get_pin_defaults() # Get pin overrides features = pin.find('mex:pin_features', NAMESPACES) pin_overrides = {} if features is not None: for feature in pin.find('mex:pin_features', NAMESPACES): pin_overrides[feature.attrib.get('name')] = feature.attrib.get('value') pin_props = self._props_to_dts(pin_overrides, defaults) self._pin_groups[pin_props].append(mux) def __repr__(self): """ Get string representation of the object """ return "PinGroup(%s)" % (self._name) def __eq__(self, obj): """ return true if two objects have the same pin group name """ return isinstance(obj, PinGroup) and self._name == obj._name def __lt__(self, obj): """ Compare objects based on name """ if not isinstance(obj, PinGroup): return True return self._name < obj._name def get_pin_props(self): """ Get all unique pin properties """ return self._pin_groups.keys() def get_pins(self, props): """ Get all pins with a provided set of properties @param props: property set """ return self._pin_groups[props] def get_description(self): """ Get description of the pin group, if present. If no description present, description will be "" """ return self._description def get_name(self): """ Get pin group name """ return self._name def _props_to_dts(self, props, defaults): """ Remap dictionary of property names from NXP defined values to Zephyr ones @param props: Dictionary of NXP property names and values @param defaults: Dictionary of NXP property names and default pin values @return array of strings suitable for writing to DTS """ zephyr_props = [] prop_mapping = { 'fast': 'fast', 'slow': 'slow', 'low': 'low', 'high': 'high', } # Lambda to convert property names to zephyr formatted strings sanitize = lambda x: "\"" + prop_mapping[x] + "\"" if (x in prop_mapping) else "" # Lambda to get property value or fallback on default prop_val = lambda x: props[x] if x in props else defaults[x] # Check pin defaults and overrides to see if the pin will have a pull pull_enable = prop_val('pull_enable') == 'enable' # For each property, append the provided override or the default zephyr_props.append(f"drive-strength = {sanitize(prop_val('drive_strength'))}") if prop_val('open_drain') == 'enable': zephyr_props.append('drive-open-drain') if pull_enable: # If pull is enabled, select pull up or pull down if prop_val('pull_select') == 'up': zephyr_props.append('bias-pull-up') else: zephyr_props.append('bias-pull-down') zephyr_props.append(f"slew-rate = {sanitize(prop_val('slew_rate'))}") if prop_val('passive_filter') == 'enable': zephyr_props.append("nxp,passive-filter") return tuple(zephyr_props) class NXPSdkUtil: """ Class for kinetis configuration file parser """ def __init__(self, cfg_root, copyright_header = "", log_level = logging.ERROR): """ Initialize SDK utilities. Providing a signal file will enable this class to parse MEX files, and generate output DTS @param cfg_root processor configuration folder root @param copyright_header: copyright string to add to any generated file header @param log_level: log level for SDK utility """ # Load the signal XML data self._logger = logging.getLogger('') self._logger.setLevel(log_level) self._parse_signal_xml(pathlib.Path(cfg_root)/'signal_configuration.xml') self._copyright = copyright_header logging.info("Loaded %d configurable pin defs", len(self._pins)) def _parse_signal_xml(self, signal_fn): """ Parses signal XML configuration file. Builds a list of pins, which can be used to generate soc level DTSI file. @param signal_fn: signal_configuration.xml file to parse """ self._pins = {} try: signal_tree = ET.parse(signal_fn) except ET.ParseError: logging.error("Could not parse provided signal file: %s", signal_fn) return signal_root = signal_tree.getroot() self._part_num = signal_root.find("./part_information/part_number").get('id') logging.info("Loaded XML for %s", self._part_num) periphs_node = signal_root.find("peripherals") periphs = [] for pin in periphs_node: pin_id = pin.attrib.get("id") name = pin.attrib.get("name") if pin_id != name: logging.warning("id and name don't match") periphs.append(pin_id) pins_node = signal_root.find("pins") for pin in pins_node: signal = SignalPin(pin) # Only add valid signal pins to list if signal.get_name() != '': self._pins[signal.get_name()] = signal def _write_pins(self, which_port, pins, file): """ Writes all pin mux nodes for a specific pin port to soc pinctrl dtsi file. @param which_port: pin port to define @param pins: list of pin mux options to write @param file: output file to write to """ port_pins = list(filter(lambda p: (p.get_port().lower() == which_port), pins)) if (len(port_pins)) == 0: return port_pins.sort(key=lambda p: (p.get_pin(), p.get_mux())) seen_nodes = [] for pin_data in port_pins: label = pin_data.get_name() port = pin_data.get_port() pin = pin_data.get_pin() mux = pin_data.get_mux() if label in seen_nodes: continue seen_nodes.append(label) file.write(f"#define {label} KINETIS_MUX('{port}',{pin},{mux}) /* PT{port}{pin} */\n") def get_part_num(self): """ Return the part number this class is instantiated for """ return self._part_num def write_pinctrl_defs(self, outputfile): """ Writes all pin mux options into pinctrl DTSI file. Board level pin groups can include this pinctrl dtsi file to access pin control defintions. @param outputfile: file to write output pinctrl defs to """ # Create list of all pin mux options pinmux_opts = [] for pin in self._pins.values(): pinmux_opts.extend(pin.get_mux_options()) pcr_pins = list(filter(lambda p: (p.get_periph() not in ["FB", "EZPORT"]), pinmux_opts)) file_header = ("/*\n" f" * NOTE: Autogenerated file by {os.path.basename(__main__.__file__)}\n" f" * for {self._part_num}/signal_configuration.xml\n" " *\n" f" * {self._copyright}\n" " */\n" "\n") # Notes on the below macro: # Port values range from 'A'-'E', so we store them with 4 bits, # with port A being 0, B=1,... # Pin values range from 0-31, so we give 6 bits for future expansion # Mux values range from 0-8, so we give 3 bits # shift the port and pin values to the MSBs of the mux value, so they # don't conflict with pin configuration settings # Store the mux value at the offset it will actually be written to the # configuration register mux_macro = ("#define KINETIS_MUX(port, pin, mux)\t\t\\\n" "\t(((((port) - 'A') & 0xF) << 28) |\t\\\n" "\t(((pin) & 0x3F) << 22) |\t\t\\\n" "\t(((mux) & 0x7) << 8))\n\n") with open(outputfile, "w", encoding="utf8") as file: file.write(file_header) # ifdef guard file.write(f"#ifndef _ZEPHYR_DTS_BINDING_{self._part_num.upper()}_\n") file.write(f"#define _ZEPHYR_DTS_BINDING_{self._part_num.upper()}_\n\n") # Write macro to make port name file.write(mux_macro) self._write_pins('a', pcr_pins, file) self._write_pins('b', pcr_pins, file) self._write_pins('c', pcr_pins, file) self._write_pins('d', pcr_pins, file) self._write_pins('e', pcr_pins, file) file.write("#endif\n") def _parse_mex_cfg(self, mexfile): """ Parses mex configuration into pin groups. @param mexfile: mex configuration file to parse @return parsed pin groups """ pin_groups = {} try: mex_xml = ET.parse(mexfile) for function in mex_xml.findall( 'mex:tools/mex:pins/mex:functions_list/mex:function', NAMESPACES): group = PinGroup(function, self._pins) pin_groups[group.get_name()] = group return pin_groups except ET.ParseError: logging.error("Could not parse mex file %s", mex_xml) return None def write_pinctrl_groups(self, mexfile, outputfile): """ Write pinctrl groups to disk as a parsed DTS file. Intended for use with the output of @ref write_pinctrl_defs @param mexfile: mex file to parse @param outputfile: DTS pinctrl file to write pin groups to """ file_header = ("/*\n" f" * NOTE: Autogenerated file by {os.path.basename(__main__.__file__)}\n" f" * for {self._part_num}/signal_configuration.xml\n" " *\n" f" * {self._copyright}\n" " */\n" "\n") pin_groups = self._parse_mex_cfg(mexfile) with open(outputfile, "w", encoding="utf8") as file: file.write(file_header) file.write(f"\n#include \n\n") file.write("&pinctrl {\n") # Write pin groups back out to disk for group in pin_groups.values(): pin_props = group.get_pin_props() if len(pin_props) == 0: # Do not write to disk continue logging.info("Writing pin group %s to disk", group.get_name()) # Write description as comment if group has one description = group.get_description() if description != "": description_lines = description.split("\n") if len(description_lines) == 1: file.write(f"\t/* {description} */\n") else: file.write("\t/*\n") for line in description_lines: file.write(f"\t * {line}\n") file.write("\t */\n") file.write(f"\t{group.get_name().lower()}: {group.get_name().lower()} {{\n") idx = 0 for pin_prop in sorted(pin_props): group_str = f"\t\tgroup{idx} {{\n" # Write all pin names group_str += "\t\t\tpinmux = " for pin in group.get_pins(pin_prop): group_str += f"<{pin.get_name()}>,\n\t\t\t\t" # Strip out last 3 tabs and close pin name list group_str = re.sub(r',\n\t\t\t\t$', ';\n', group_str) idx += 1 # Write all pin props for prop in pin_prop: group_str += f"\t\t\t{prop};\n" group_str += "\t\t};\n" file.write(group_str) file.write("\t};\n\n") file.write("};\n") """ Utility functions used to get details about board/processor from MEX file """ def get_board_name(mexfile): """ Extracts board name from a mex file @param mexfile: mex file to parse for board name """ try: config_tree = ET.parse(mexfile) return config_tree.getroot().find('mex:common/mex:board', NAMESPACES).text except ET.ParseError: print(f"Malformed XML tree {mexfile}") return None except IOError: print(f"File {mexfile} could not be opened") return None def get_processor_name(mexfile): """ Extracts processor name from a mex file @param mexfile: mex file to parse for processor name """ try: config_tree = ET.parse(mexfile) processor = config_tree.getroot().find('mex:common/mex:processor', NAMESPACES) if processor is None: raise RuntimeError("Cannot locate processor name in MEX file. " "Are you using v12 of the MCUXpresso configuration tools?") return processor.text except ET.ParseError: print(f"Malformed XML tree {mexfile}") return None except IOError: print(f"File {mexfile} could not be opened") return None def get_package_name(mexfile): """ Extracts package name from a mex file @param mexfile: mex file to parse for package name """ try: config_tree = ET.parse(mexfile) return config_tree.getroot().find('mex:common/mex:package', NAMESPACES).text except ET.ParseError: print(f"Malformed XML tree {mexfile}") return None except IOError: print(f"File {mexfile} could not be opened") return None