binman: ti-board-config: Add support for TI board config binaries

The ti-board-config entry loads and validates a given YAML config file
against a given schema, and generates the board config binary. K3
devices require these binaries to be packed into the final system
firmware images.

Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Neha Malcom Francis <n-francis@ti.com>
diff --git a/tools/binman/entries.rst b/tools/binman/entries.rst
index b55f424..8a9e778 100644
--- a/tools/binman/entries.rst
+++ b/tools/binman/entries.rst
@@ -1664,6 +1664,54 @@
 
 
 
+.. _etype_ti_board_config:
+
+Entry: ti-board-config: An entry containing a TI schema validated board config binary
+-------------------------------------------------------------------------------------
+
+This etype supports generation of two kinds of board configuration
+binaries: singular board config binary as well as combined board config
+binary.
+
+Properties / Entry arguments:
+    - config-file: File containing board configuration data in YAML
+    - schema-file: File containing board configuration YAML schema against
+      which the config file is validated
+
+Output files:
+    - board config binary: File containing board configuration binary
+
+These above parameters are used only when the generated binary is
+intended to be a single board configuration binary. Example::
+
+    my-ti-board-config {
+        ti-board-config {
+            config = "board-config.yaml";
+            schema = "schema.yaml";
+        };
+    };
+
+To generate a combined board configuration binary, we pack the
+needed individual binaries into a ti-board-config binary. In this case,
+the available supported subnode names are board-cfg, pm-cfg, sec-cfg and
+rm-cfg. The final binary is prepended with a header containing details about
+the included board config binaries. Example::
+
+    my-combined-ti-board-config {
+        ti-board-config {
+            board-cfg {
+                config = "board-cfg.yaml";
+                schema = "schema.yaml";
+            };
+            sec-cfg {
+                config = "sec-cfg.yaml";
+                schema = "schema.yaml";
+            };
+        }
+    }
+
+
+
 .. _etype_u_boot:
 
 Entry: u-boot: U-Boot flat binary
diff --git a/tools/binman/etype/ti_board_config.py b/tools/binman/etype/ti_board_config.py
new file mode 100644
index 0000000..94f894c
--- /dev/null
+++ b/tools/binman/etype/ti_board_config.py
@@ -0,0 +1,259 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2022-2023 Texas Instruments Incorporated - https://www.ti.com/
+# Written by Neha Malcom Francis <n-francis@ti.com>
+#
+# Entry-type module for generating schema validated TI board
+# configuration binary
+#
+
+import os
+import struct
+import yaml
+
+from collections import OrderedDict
+from jsonschema import validate
+from shutil import copyfileobj
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+from u_boot_pylib import tools
+
+BOARDCFG = 0xB
+BOARDCFG_SEC = 0xD
+BOARDCFG_PM = 0xE
+BOARDCFG_RM = 0xC
+BOARDCFG_NUM_ELEMS = 4
+
+class Entry_ti_board_config(Entry_section):
+    """An entry containing a TI schema validated board config binary
+
+    This etype supports generation of two kinds of board configuration
+    binaries: singular board config binary as well as combined board config
+    binary.
+
+    Properties / Entry arguments:
+        - config-file: File containing board configuration data in YAML
+        - schema-file: File containing board configuration YAML schema against
+          which the config file is validated
+
+    Output files:
+        - board config binary: File containing board configuration binary
+
+    These above parameters are used only when the generated binary is
+    intended to be a single board configuration binary. Example::
+
+        my-ti-board-config {
+            ti-board-config {
+                config = "board-config.yaml";
+                schema = "schema.yaml";
+            };
+        };
+
+    To generate a combined board configuration binary, we pack the
+    needed individual binaries into a ti-board-config binary. In this case,
+    the available supported subnode names are board-cfg, pm-cfg, sec-cfg and
+    rm-cfg. The final binary is prepended with a header containing details about
+    the included board config binaries. Example::
+
+        my-combined-ti-board-config {
+            ti-board-config {
+                board-cfg {
+                    config = "board-cfg.yaml";
+                    schema = "schema.yaml";
+                };
+                sec-cfg {
+                    config = "sec-cfg.yaml";
+                    schema = "schema.yaml";
+                };
+            }
+        }
+    """
+    def __init__(self, section, etype, node):
+        super().__init__(section, etype, node)
+        self._config = None
+        self._schema = None
+        self._entries = OrderedDict()
+        self._num_elems = BOARDCFG_NUM_ELEMS
+        self._fmt = '<HHHBB'
+        self._index = 0
+        self._binary_offset = 0
+        self._sw_rev = 1
+        self._devgrp = 0
+
+    def ReadNode(self):
+        super().ReadNode()
+        self._config = fdt_util.GetString(self._node, 'config')
+        self._schema = fdt_util.GetString(self._node, 'schema')
+        # Depending on whether config file is present in node, we determine
+        # whether it is a combined board config binary or not
+        if self._config is None:
+            self.ReadEntries()
+        else:
+            self._config_file = tools.get_input_filename(self._config)
+            self._schema_file = tools.get_input_filename(self._schema)
+
+    def ReadEntries(self):
+        """Read the subnodes to find out what should go in this image
+        """
+        for node in self._node.subnodes:
+            if 'type' not in node.props:
+                entry = Entry.Create(self, node, 'ti-board-config')
+                entry.ReadNode()
+                cfg_data = entry.BuildSectionData(True)
+                entry._cfg_data = cfg_data
+                self._entries[entry.name] = entry
+        self._num_elems = len(self._node.subnodes)
+
+    def _convert_to_byte_chunk(self, val, data_type):
+        """Convert value into byte array
+
+        Args:
+            val: value to convert into byte array
+            data_type: data type used in schema, supported data types are u8,
+                u16 and u32
+
+        Returns:
+            array of bytes representing value
+        """
+        size = 0
+        if (data_type == '#/definitions/u8'):
+            size = 1
+        elif (data_type == '#/definitions/u16'):
+            size = 2
+        else:
+            size = 4
+        if type(val) == int:
+            br = val.to_bytes(size, byteorder='little')
+        return br
+
+    def _compile_yaml(self, schema_yaml, file_yaml):
+        """Convert YAML file into byte array based on YAML schema
+
+        Args:
+            schema_yaml: file containing YAML schema
+            file_yaml: file containing config to compile
+
+        Returns:
+            array of bytes repesenting YAML file against YAML schema
+        """
+        br = bytearray()
+        for key, node in file_yaml.items():
+            node_schema = schema_yaml['properties'][key]
+            node_type = node_schema.get('type')
+            if not 'type' in node_schema:
+                br += self._convert_to_byte_chunk(node,
+                                                node_schema.get('$ref'))
+            elif node_type == 'object':
+                br += self._compile_yaml(node_schema, node)
+            elif node_type == 'array':
+                for item in node:
+                    if not isinstance(item, dict):
+                        br += self._convert_to_byte_chunk(
+                            item, schema_yaml['properties'][key]['items']['$ref'])
+                    else:
+                        br += self._compile_yaml(node_schema.get('items'), item)
+        return br
+
+    def _generate_binaries(self):
+        """Generate config binary artifacts from the loaded YAML configuration file
+
+        Returns:
+            byte array containing config binary artifacts
+            or None if generation fails
+        """
+        cfg_binary = bytearray()
+        for key, node in self.file_yaml.items():
+            node_schema = self.schema_yaml['properties'][key]
+            br = self._compile_yaml(node_schema, node)
+            cfg_binary += br
+        return cfg_binary
+
+    def _add_boardcfg(self, bcfgtype, bcfgdata):
+        """Add board config to combined board config binary
+
+        Args:
+            bcfgtype (int): board config type
+            bcfgdata (byte array): board config data
+        """
+        size = len(bcfgdata)
+        desc = struct.pack(self._fmt, bcfgtype,
+                            self._binary_offset, size, self._devgrp, 0)
+        with open(self.descfile, 'ab+') as desc_fh:
+            desc_fh.write(desc)
+        with open(self.bcfgfile, 'ab+') as bcfg_fh:
+            bcfg_fh.write(bcfgdata)
+        self._binary_offset += size
+        self._index += 1
+
+    def _finalize(self):
+        """Generate final combined board config binary
+
+        Returns:
+            byte array containing combined board config data
+            or None if unable to generate
+        """
+        with open(self.descfile, 'rb') as desc_fh:
+            with open(self.bcfgfile, 'rb') as bcfg_fh:
+                with open(self.fh_file, 'ab+') as fh:
+                    copyfileobj(desc_fh, fh)
+                    copyfileobj(bcfg_fh, fh)
+        data = tools.read_file(self.fh_file)
+        return data
+
+    def BuildSectionData(self, required):
+        if self._config is None:
+            self._binary_offset = 0
+            uniq = self.GetUniqueName()
+            self.fh_file = tools.get_output_filename('fh.%s' % uniq)
+            self.descfile = tools.get_output_filename('desc.%s' % uniq)
+            self.bcfgfile = tools.get_output_filename('bcfg.%s' % uniq)
+
+            # when binman runs again make sure we start clean
+            if os.path.exists(self.fh_file):
+                os.remove(self.fh_file)
+            if os.path.exists(self.descfile):
+                os.remove(self.descfile)
+            if os.path.exists(self.bcfgfile):
+                os.remove(self.bcfgfile)
+
+            with open(self.fh_file, 'wb') as f:
+                t_bytes = f.write(struct.pack(
+                    '<BB', self._num_elems, self._sw_rev))
+            self._binary_offset += t_bytes
+            self._binary_offset += self._num_elems * struct.calcsize(self._fmt)
+
+            if 'board-cfg' in self._entries:
+                self._add_boardcfg(BOARDCFG, self._entries['board-cfg']._cfg_data)
+
+            if 'sec-cfg' in self._entries:
+                self._add_boardcfg(BOARDCFG_SEC, self._entries['sec-cfg']._cfg_data)
+
+            if 'pm-cfg' in self._entries:
+                self._add_boardcfg(BOARDCFG_PM, self._entries['pm-cfg']._cfg_data)
+
+            if 'rm-cfg' in self._entries:
+                self._add_boardcfg(BOARDCFG_RM, self._entries['rm-cfg']._cfg_data)
+
+            data = self._finalize()
+            return data
+
+        else:
+            with open(self._config_file, 'r') as f:
+                self.file_yaml = yaml.safe_load(f)
+            with open(self._schema_file, 'r') as sch:
+                self.schema_yaml = yaml.safe_load(sch)
+
+            try:
+                validate(self.file_yaml, self.schema_yaml)
+            except Exception as e:
+                self.Raise(f"Schema validation error: {e}")
+
+            data = self._generate_binaries()
+            return data
+
+    def SetImagePos(self, image_pos):
+        Entry.SetImagePos(self, image_pos)
+
+    def CheckEntries(self):
+        Entry.CheckEntries(self)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index e53181a..5a3226e 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -97,6 +97,7 @@
 PRE_LOAD_MAGIC        = b'UBSH'
 PRE_LOAD_VERSION      = 0x11223344.to_bytes(4, 'big')
 PRE_LOAD_HDR_SIZE     = 0x00001000.to_bytes(4, 'big')
+TI_BOARD_CONFIG_DATA  = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
 
 # Subdirectory of the input dir to use to put test FDTs
 TEST_FDT_SUBDIR       = 'fdts'
@@ -199,6 +200,9 @@
         shutil.copytree(cls.TestFile('files'),
                         os.path.join(cls._indir, 'files'))
 
+        shutil.copytree(cls.TestFile('yaml'),
+                        os.path.join(cls._indir, 'yaml'))
+
         TestFunctional._MakeInputFile('compress', COMPRESS_DATA)
         TestFunctional._MakeInputFile('compress_big', COMPRESS_DATA_BIG)
         TestFunctional._MakeInputFile('bl31.bin', ATF_BL31_DATA)
@@ -6884,6 +6888,22 @@
             # Move to next
             spl_data = content[:0x18]
 
+    def testTIBoardConfig(self):
+        """Test that a schema validated board config file can be generated"""
+        data = self._DoReadFile('277_ti_board_cfg.dts')
+        self.assertEqual(TI_BOARD_CONFIG_DATA, data)
+
+    def testTIBoardConfigCombined(self):
+        """Test that a schema validated combined board config file can be generated"""
+        data = self._DoReadFile('278_ti_board_cfg_combined.dts')
+        configlen_noheader = TI_BOARD_CONFIG_DATA * 4
+        self.assertGreater(data, configlen_noheader)
+
+    def testTIBoardConfigNoDataType(self):
+        """Test that error is thrown when data type is not supported"""
+        with self.assertRaises(ValueError) as e:
+            data = self._DoReadFile('279_ti_board_cfg_no_type.dts')
+        self.assertIn("Schema validation error", str(e.exception))
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/test/277_ti_board_cfg.dts b/tools/binman/test/277_ti_board_cfg.dts
new file mode 100644
index 0000000..cda024c
--- /dev/null
+++ b/tools/binman/test/277_ti_board_cfg.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		ti-board-config {
+			config = "yaml/config.yaml";
+			schema = "yaml/schema.yaml";
+		};
+	};
+};
diff --git a/tools/binman/test/278_ti_board_cfg_combined.dts b/tools/binman/test/278_ti_board_cfg_combined.dts
new file mode 100644
index 0000000..95ef449
--- /dev/null
+++ b/tools/binman/test/278_ti_board_cfg_combined.dts
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		ti-board-config {
+			board-cfg {
+				config = "yaml/config.yaml";
+				schema = "yaml/schema.yaml";
+			};
+			sec-cfg {
+				config = "yaml/config.yaml";
+				schema = "yaml/schema.yaml";
+			};
+			rm-cfg {
+				config = "yaml/config.yaml";
+				schema = "yaml/schema.yaml";
+			};
+			pm-cfg {
+				config = "yaml/config.yaml";
+				schema = "yaml/schema.yaml";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/279_ti_board_cfg_no_type.dts b/tools/binman/test/279_ti_board_cfg_no_type.dts
new file mode 100644
index 0000000..584b7ac
--- /dev/null
+++ b/tools/binman/test/279_ti_board_cfg_no_type.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		ti-board-config {
+			config = "yaml/config.yaml";
+			schema = "yaml/schema_notype.yaml";
+		};
+	};
+};
diff --git a/tools/binman/test/yaml/config.yaml b/tools/binman/test/yaml/config.yaml
new file mode 100644
index 0000000..5f799a6
--- /dev/null
+++ b/tools/binman/test/yaml/config.yaml
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test config
+#
+---
+
+main-branch:
+  obj:
+    a: 0x0
+    b: 0
+  arr: [0, 0, 0, 0]
+  another-arr:
+    - #1
+      c: 0
+      d: 0
+    - #2
+      c: 0
+      d: 0
diff --git a/tools/binman/test/yaml/schema.yaml b/tools/binman/test/yaml/schema.yaml
new file mode 100644
index 0000000..8aa03f3
--- /dev/null
+++ b/tools/binman/test/yaml/schema.yaml
@@ -0,0 +1,49 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test schema
+#
+---
+
+definitions:
+    u8:
+        type: integer
+        minimum: 0
+        maximum: 0xff
+    u16:
+        type: integer
+        minimum: 0
+        maximum: 0xffff
+    u32:
+        type: integer
+        minimum: 0
+        maximum: 0xffffffff
+
+type: object
+properties:
+    main-branch:
+        type: object
+        properties:
+            obj:
+                type: object
+                properties:
+                    a:
+                        $ref: "#/definitions/u32"
+                    b:
+                        $ref: "#/definitions/u16"
+            arr:
+                type: array
+                minItems: 4
+                maxItems: 4
+                items:
+                    $ref: "#/definitions/u8"
+            another-arr:
+                type: array
+                minItems: 2
+                maxItems: 2
+                items:
+                    type: object
+                    properties:
+                        c:
+                            $ref: "#/definitions/u8"
+                        d:
+                            $ref: "#/definitions/u8"
diff --git a/tools/binman/test/yaml/schema_notype.yaml b/tools/binman/test/yaml/schema_notype.yaml
new file mode 100644
index 0000000..6b4d98f
--- /dev/null
+++ b/tools/binman/test/yaml/schema_notype.yaml
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test schema
+#
+---
+
+definitions:
+    u8:
+        type: integer
+        minimum: 0
+        maximum: 0xff
+    u16:
+        type: integer
+        minimum: 0
+        maximum: 0xffff
+    u32:
+        type: integer
+        minimum: 0
+        maximum: 0xffffffff
+
+type: object
+properties:
+    main-branch:
+        type: object
+        properties:
+            obj:
+                type: object
+                properties:
+                    a:
+                        $ref: "#/definitions/u4"
+                    b:
+                        $ref: "#/definitions/u16"
+            arr:
+                type: array
+                minItems: 4
+                maxItems: 4
+                items:
+                    $ref: "#/definitions/u8"