#!/usr/bin/env python3 # # Copyright (c) 2022, NXP # # SPDX-License-Identifier: Apache-2.0 """ Implements a configuration file parser for LPC and RT6xx/5xx MCUs, which can generate pinctrl definitions for Zephyr """ import xml.etree.ElementTree as ET import re import collections import logging import os import pathlib import __main__ 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, imx_rt = False): """ 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._offset = -1 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') self._mux_overrides = {} if imx_rt: # RT 6xx/5xx series has different function register func_name = 'FSEL' pio_regex = re.compile(r'IOPCTL_PIO(\d+)_(\d+)') else: func_name = 'FUNC' pio_regex = re.compile(r'IOCON_PIO(\d)_*(\d+)') for assign in connection.iter('assign'): reg = assign.attrib.get('register') field = assign.attrib.get('bit_field') val = assign.attrib.get('bit_field_value') logging.debug('\t\t\t [ASSIGN] %s %s', reg, val) # Only process PIO register FUNC setting match = pio_regex.match(reg) if match and (field == func_name): if self._channel: # For mux options with channels, format pin name as: # {Peripheral}_{Signal}{Channel}_{Pin} self._name = f"{self._periph}_{self._signal}{self._channel}" # Append name of pin self._name += f"_PIO{match.group(1)}_{match.group(2)}" port = int(match.group(1)) pin = int(match.group(2)) self._mux = int(val, 16) self._offset = (port * 32) + pin elif match and field == 'MODE': # MUX overrides pullup/pulldown mode if val == '0': self._mux_overrides['mode'] = 'inactive' elif val == '1': self._mux_overrides['mode'] = 'pullDown' elif val == '2': self._mux_overrides['mode'] = 'pullUp' elif val == '3': self._mux_overrides['mode'] = 'repeater' elif match and field == 'ASW' and not imx_rt: # MUX override analog switch setting if val == '0x1': self._mux_overrides['asw'] = 'enabled' self._mux_overrides['digimode'] = 'disabled' elif match and field == 'ASW0' and not imx_rt: # LPC553x has two ASW bits if val == '0x1': self._mux_overrides['asw0'] = 'enabled' self._mux_overrides['digimode'] = 'disabled' elif match and field == 'ASW1' and not imx_rt: # LPC553x has two ASW bits if val == '0x1': self._mux_overrides['asw1'] = 'enabled' self._mux_overrides['digimode'] = 'disabled' elif match and field == 'AMENA' and imx_rt: # MUX override analog switch setting if val == '0x1': self._mux_overrides['amena'] = 'enabled' if self._name == 'PMIC_I2C_SCL' and imx_rt: # RT600/500 have special pmic I2C pins self._offset = 0x100 self._mux = 0 elif self._name == 'PMIC_I2C_SDA' and imx_rt: self._offset = 0x101 self._mux = 0 if re.match(r'^\d', self._name): # If string starts with a digit, it will not be a valid C name self._name = f"PIN_{self._name}" if self._offset == -1: # 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_mux_overrides(self): """ Some MUX options define specific pin property overrides. Get them here if they exist """ return self._mux_overrides def get_port(self): """ Get mux port """ return self._port def get_signal(self): """ Get mux signal name """ return self._signal def get_offset(self): """ Get mux register offset """ return self._offset 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, MUXOption) and self._name == obj._name def __lt__(self, obj): """ Compare objects based on name """ if not isinstance(obj, MUXOption): return True return self._name < obj._name class SignalPin: """ Internal class representing a signal on the SOC """ def __init__(self, pin, imx_rt = False): """ Initializes a SignalPin object @param pin: pin XML object from signal_configuration.xml """ # lpc pin names are formatted as PIOx_y pin_regex = re.search(r'PIO(\d+)_(\d+)', pin.attrib['name']) if (imx_rt and (pin.attrib['name'] == 'PMIC_I2C_SCL' or pin.attrib['name'] == 'PMIC_I2C_SDA')): # iMX RT has special pins without a mux setting self._name = pin.attrib['name'] self._port = 0 self._pin = 0 elif pin_regex is None: logging.debug('Could not match pin name %s', pin.attrib['name']) self._name = '' return else: self._name = pin.attrib['name'] self._port = int(pin_regex.group(1)) self._pin = int(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, imx_rt = imx_rt) # 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 and port """ return isinstance(obj, SignalPin) and self._name == obj._name def __lt__(self, obj): """ Compare objects based on port and pin """ if not isinstance(obj, SignalPin): return True if self._port == obj._port: return self._pin < obj._pin return self._port < obj._port 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, imx_rt = False): """ 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') if not signal_name in signal_map: logging.warning('Signal name %s not present in mapping', signal_name) # No way to find mux option continue # Get mux option for this signal signal = signal_map[signal_name] 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') # Get pin mux option overrides for (override, value) in mux.get_mux_overrides().items(): pin_overrides[override] = value if imx_rt: pin_props = self._imx_rt_props_to_dts(pin_overrides, defaults) else: pin_props = self._lpc_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 _imx_rt_props_to_dts(self, props, defaults): """ Remap dictionary of property names from NXP defined values to Zephyr ones (applies to RT600/RT500 properties) @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 = { # Slew rate property mappings 'normal': 'normal', 'slow': 'slow', # Drive strength property mappings 'normal': 'normal', 'full': '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] # For each property, append the provided override or the default # Check pull settings pull_enable = prop_val('pupdena') == 'enabled' if pull_enable: if prop_val('pupdsel') == 'pullDown': zephyr_props.append('bias-pull-down') else: zephyr_props.append('bias-pull-up') # input buffer if prop_val('ibena') == 'enabled': zephyr_props.append('input-enable') # Slew rate settings zephyr_props.append(f"slew-rate = {sanitize(prop_val('slew_rate'))}") # Drive strength zephyr_props.append(f"drive-strength = {sanitize(prop_val('drive'))}") # analog switch if prop_val('amena') == 'enabled': zephyr_props.append('nxp,analog-mode') # open drain if prop_val('odena') == 'enabled': zephyr_props.append('drive-open-drain') # Pin invert settings if prop_val('iiena') == 'enabled': zephyr_props.append('nxp,invert') return tuple(zephyr_props) def _lpc_props_to_dts(self, props, defaults): """ Remap dictionary of property names from NXP defined values to Zephyr ones (applies to LPC properties only) @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 = { # Slew rate property mappings 'standard': 'standard', 'fast': 'fast', # power source property mappings 'signal3v3': '3v3', 'signal1v8': '1v8', # i2cfilter property mappings 'nonhighspeedmode': 'slow', 'highspeedmode': 'fast' } # 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] if x in defaults else "" # For each property, append the provided override or the default # Check pull settings if prop_val('mode') == 'pullUp': zephyr_props.append('bias-pull-up') elif prop_val('mode') == 'pullDown': zephyr_props.append('bias-pull-down') elif prop_val('mode') == 'repeater': # Repeater latches the pin to the last input, to keep it from floating zephyr_props.append('drive-push-pull') # Slew rate settings if 'slew_rate' in defaults: zephyr_props.append(f"slew-rate = {sanitize(prop_val('slew_rate'))}") # Pin invert settings if prop_val('invert') == 'enabled': zephyr_props.append('nxp,invert') # open drain settings if prop_val('open_drain') == 'enabled': zephyr_props.append('drive-open-drain') if 'asw' in defaults: # analog switch setting (ASW bit for type A pins) if prop_val('asw') == 'enabled' and prop_val('digimode') == 'disabled': # Note we only respect the ASW setting if digimode is false, # This condition can only occur when a mux specific override sets # DIGIMODE=0, ASW=1. zephyr_props.append('nxp,analog-mode') if prop_val('asw0') == 'enabled' and prop_val('digimode') == 'disabled': # analog switch setting 0 (LPC553x has two ASW bits) zephyr_props.append('nxp,analog-mode') if prop_val('asw1') == 'enabled' and prop_val('digimode') == 'disabled': # analog switch setting 0 (LPC553x has two ASW bits) zephyr_props.append('nxp,analog-alt-mode') if 'ssel' in defaults: # i2c power source select (SSEL bit for type I pins) zephyr_props.append(f"power-source = {sanitize(prop_val('ssel'))}") # i2c filter (FILTEROFF bit for type I pins) # Note that when filter_off == 'enabled', the filter is actually on if prop_val('filter_off') == 'enabled': # Check i2c filter speed bit (I2CFILTER bit for type I pins) zephyr_props.append(f"nxp,i2c-filter = {sanitize(prop_val('i2cfilter'))}") # i2c pullup (ECS bit for type I pins) if prop_val('ecs') == 'enabled': zephyr_props.append('nxp,i2c-pullup') # i2c mode (EGP bit for type I pins) if prop_val('egp') == 'i2c': zephyr_props.append('nxp,i2c-mode') return tuple(zephyr_props) class NXPSdkUtil: """ Class for lpc 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') if 'MIMXRT' in self._part_num: # IMX RT600/500 series part. Different register layout and pin names self._imx_rt = True else: self._imx_rt = False logging.info("Loaded XML for %s", self._part_num) pins_node = signal_root.find("pins") for pin in pins_node: signal = SignalPin(pin, self._imx_rt) # Only add valid signal pins to list if signal.get_name() != '': self._pins[signal.get_name()] = signal 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 header file. Board level pin groups can include this pinctrl header file to access pin control defintions. @param outputfile: file to write output pinctrl defs to """ file_header = ("/*\n" f" * NOTE: File generated by {os.path.basename(__main__.__file__)}\n" f" * from {self._part_num}/signal_configuration.xml\n" " *\n" f" * {self._copyright}\n" " */\n" "\n") if self._imx_rt: # Notes on the below macro: # We store the pin and port values as an offset, because some pins # do not follow a consistent offset. We use 12 bits to store this # offset. # Mux values range from 0-15, so we give 4 bits # shift the offset 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 IOPCTL_MUX(offset, mux)\t\t\\\n" "\t((((offset) & 0xFFF) << 20) |\t\t\\\n" "\t(((mux) & 0xF) << 0))\n\n") else: # Notes on the below macro: # We store the pin and port values as an offset, because some pins # do not follow a consistent offset. We use 12 bits to store this # offset. # Mux values range from 0-15, so we give 4 bits # type values range from 0-2, so we give 3 bits # shift the offset 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 IOCON_MUX(offset, type, mux)\t\t\\\n" "\t(((offset & 0xFFF) << 20) |\t\t\\\n" "\t(((type) & 0x3) << 18) |\t\t\\\n" "\t(((mux) & 0xF) << 0))\n\n" "#define IOCON_TYPE_D 0x0\n" "#define IOCON_TYPE_I 0x1\n" "#define IOCON_TYPE_A 0x2\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) # Write pins for pin in sorted(self._pins.values()): if not self._imx_rt: # LPC IOCON has analog and I2C type pins, iMX RT does not if 'asw' in pin.get_pin_defaults(): pin_type = 'IOCON_TYPE_A' # Analog pin type elif 'asw0' in pin.get_pin_defaults(): pin_type = 'IOCON_TYPE_A' # LPC553x has ASW0 and ASW1 bits elif 'ssel' in pin.get_pin_defaults(): pin_type = 'IOCON_TYPE_I' else: pin_type = 'IOCON_TYPE_D' sig_port = pin.get_port() sig_pin = pin.get_pin() for mux in sorted(pin.get_mux_options()): offset = mux.get_offset() label = mux.get_name() mux = mux.get_mux() if self._imx_rt: file.write(f"#define {label} IOPCTL_MUX({offset}, {mux}) " f"/* PIO{sig_port}_{sig_pin} */\n") else: file.write(f"#define {label} IOCON_MUX({offset}, {pin_type}, {mux}) " f"/* PIO{sig_port}_{sig_pin} */\n") file.write("\n#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, self._imx_rt) 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: File generated by {os.path.basename(__main__.__file__)}\n" f" * from {os.path.basename(mexfile)}\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) if self._imx_rt: file.write(f"\n#include \n\n") else: 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() description = group.get_description() # if a description is present, write it 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") logging.info("Writing pin group %s to disk", group.get_name()) 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 if pin_prop is None: logging.error("No pin properties present") 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) if config_tree.getroot().find('mex:common/mex:board', NAMESPACES) is None: return get_processor_name(mexfile) + '-board' 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