FIT provides a way to select between different devicetree blobs
depending on the model. This works fine for U-Boot proper and allows SPL
to select the correct blob for the current board at runtime. The boot
sequence (SPL->U-Boot proper) is therefore covered by the existing
feature set.

The first boot phase (typically TPL) cannot use FIT since SoC boot ROMs
don't currently support it. Therefore the TPL image must be specific to
each model it boots on.

To support booting on mulitple models, binman must therefore produce a
separate TPL image for each model, even if the images for the rest of
the phases are identical.

TPL needs to be packaged as an executable binary along with a reduced
devicetree. When multiple models are supported, a reduced devicetree
must be provided for each model.

U-Boot's build system is designed to build a single devicetree for SPL
builds, so does not support this requirement.

Add a new 'alternatives' feature to Binman, allowing it to automatically
subset a devicetree to produce the reduced devicetree for a particular
phase for each supported model. With this it is possible to produce a
separate TPL image for each of the models. The correct one can then be
loaded onto a board, along with the common FIT image(s).

Signed-off-by: Simon Glass <s...@chromium.org>
---

 tools/binman/btool/fdtgrep.py                |   2 +-
 tools/binman/control.py                      |   3 +
 tools/binman/entries.rst                     |  42 ++++++
 tools/binman/entry.py                        |   4 +-
 tools/binman/etype/alternates_fdt.py         | 132 +++++++++++++++++++
 tools/binman/ftest.py                        | 121 +++++++++++++++++
 tools/binman/image.py                        |  13 ++
 tools/binman/test/328_alternates_fdt.dts     |  28 ++++
 tools/binman/test/329_alternates_fdtgrep.dts |  29 ++++
 tools/binman/test/330_alternates_vpl.dts     |  29 ++++
 tools/binman/test/331_alternates_spl.dts     |  29 ++++
 tools/binman/test/332_alternates_inval.dts   |  29 ++++
 tools/binman/test/alt_dts/model1.dts         |  24 ++++
 tools/binman/test/alt_dts/model2.dts         |  24 ++++
 14 files changed, 507 insertions(+), 2 deletions(-)
 create mode 100644 tools/binman/etype/alternates_fdt.py
 create mode 100644 tools/binman/test/328_alternates_fdt.dts
 create mode 100644 tools/binman/test/329_alternates_fdtgrep.dts
 create mode 100644 tools/binman/test/330_alternates_vpl.dts
 create mode 100644 tools/binman/test/331_alternates_spl.dts
 create mode 100644 tools/binman/test/332_alternates_inval.dts
 create mode 100644 tools/binman/test/alt_dts/model1.dts
 create mode 100644 tools/binman/test/alt_dts/model2.dts

diff --git a/tools/binman/btool/fdtgrep.py b/tools/binman/btool/fdtgrep.py
index c34d8d8943b..da1f8c7bf4e 100644
--- a/tools/binman/btool/fdtgrep.py
+++ b/tools/binman/btool/fdtgrep.py
@@ -84,7 +84,7 @@ class Bintoolfdtgrep(bintool.Bintool):
         elif phase == 'spl':
             tag = 'bootph-pre-ram'
         else:
-            raise(f"Invalid U-Boot phase '{phase}': Use tpl/vpl/spl")
+            raise ValueError(f"Invalid U-Boot phase '{phase}': Use 
tpl/vpl/spl")
 
         # These args mirror those in cmd_fdtgrep in scripts/Makefile.lib
         # First do the first stage
diff --git a/tools/binman/control.py b/tools/binman/control.py
index a233c778d5e..542c2b45644 100644
--- a/tools/binman/control.py
+++ b/tools/binman/control.py
@@ -734,6 +734,9 @@ def ProcessImage(image, update_fdt, write_map, 
get_contents=True,
         image.WriteMap()
 
     has_problems = CheckForProblems(image)
+
+    image.WriteAlternates()
+
     return has_problems
 
 def Binman(args):
diff --git a/tools/binman/entries.rst b/tools/binman/entries.rst
index 38dfe2c7db9..8bfec8b434e 100644
--- a/tools/binman/entries.rst
+++ b/tools/binman/entries.rst
@@ -11,6 +11,48 @@ features to produce new behaviours.
 
 
 
+.. _etype_alternates_fdt:
+
+Entry: alternates-fdt: Entry that generates alternative sections for each 
devicetree provided
+---------------------------------------------------------------------------------------------
+
+When creating an image designed to boot on multiple models, each model
+requires its own devicetree. This entry deals with selecting the correct
+devicetree from a directory containing them. Each one is read in turn, then
+used to produce section contents which are written to a file. This results
+in a number of images, one for each model.
+
+For example this produces images for each .dtb file in the 'dtb' directory::
+
+    alternates-fdt {
+        fdt-list-dir = "dtb";
+        filename-pattern = "NAME.bin";
+        fdt-phase = "tpl";
+
+        section {
+            u-boot-tpl {
+            };
+        };
+    };
+
+Each output file is named based on its input file, so an input file of
+`model1.dtb` results in an output file of `model1.bin` (i.e. the `NAME` in
+the `filename-pattern` property is replaced with the .dtb basename).
+
+Note that this entry type still produces contents for the 'main' image, in
+that case using the normal dtb provided to Binman, e.g. `u-boot-tpl.dtb`.
+But that image is unlikely to be useful, since it relates to whatever dtb
+happened to be the default when U-Boot builds
+(i.e. `CONFIG_DEFAULT_DEVICE_TREE`). However, Binman ensures that the size
+of each of the alternates is the same as the 'default' one, so they can in
+principle be 'slotted in' to the appropriate place in the main image.
+
+The optional `fdt-phase` property indicates the phase to build. In this
+case, it etype runs fdtgrep to obtain the devicetree subset for that phase,
+respecting the `bootph-xxx` tags in the devicetree.
+
+
+
 .. _etype_atf_bl31:
 
 Entry: atf-bl31: ARM Trusted Firmware (ATF) BL31 blob
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 494b1b1278d..6d2f3789940 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -1395,6 +1395,8 @@ features to produce new behaviours.
                 'u-boot-tpl-dtb'
 
         Returns:
-            bytes: Contents of requested FDT
+            tuple:
+                fname (str): Filename of .dtb
+                bytes: Contents of FDT (possibly run through fdtgrep)
         """
         return self.section.FdtContents(fdt_etype)
diff --git a/tools/binman/etype/alternates_fdt.py 
b/tools/binman/etype/alternates_fdt.py
new file mode 100644
index 00000000000..808f535aa1b
--- /dev/null
+++ b/tools/binman/etype/alternates_fdt.py
@@ -0,0 +1,132 @@
+# SPDX-License-Identifier:      GPL-2.0+
+# Copyright 2024 Google LLC
+# Written by Simon Glass <s...@chromium.org>
+
+"""Entry-type module for producing multiple alternate sections"""
+
+import glob
+import os
+
+from binman.entry import EntryArg
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+from u_boot_pylib import tools
+
+class Entry_alternates_fdt(Entry_section):
+    """Entry that generates alternative sections for each devicetree provided
+
+    When creating an image designed to boot on multiple models, each model
+    requires its own devicetree. This entry deals with selecting the correct
+    devicetree from a directory containing them. Each one is read in turn, then
+    used to produce section contents which are written to a file. This results
+    in a number of images, one for each model.
+
+    For example this produces images for each .dtb file in the 'dtb' 
directory::
+
+        alternates-fdt {
+            fdt-list-dir = "dtb";
+            filename-pattern = "NAME.bin";
+            fdt-phase = "tpl";
+
+            section {
+                u-boot-tpl {
+                };
+            };
+        };
+
+    Each output file is named based on its input file, so an input file of
+    `model1.dtb` results in an output file of `model1.bin` (i.e. the `NAME` in
+    the `filename-pattern` property is replaced with the .dtb basename).
+
+    Note that this entry type still produces contents for the 'main' image, in
+    that case using the normal dtb provided to Binman, e.g. `u-boot-tpl.dtb`.
+    But that image is unlikely to be useful, since it relates to whatever dtb
+    happened to be the default when U-Boot builds
+    (i.e. `CONFIG_DEFAULT_DEVICE_TREE`). However, Binman ensures that the size
+    of each of the alternates is the same as the 'default' one, so they can in
+    principle be 'slotted in' to the appropriate place in the main image.
+
+    The optional `fdt-phase` property indicates the phase to build. In this
+    case, it etype runs fdtgrep to obtain the devicetree subset for that phase,
+    respecting the `bootph-xxx` tags in the devicetree.
+    """
+    def __init__(self, section, etype, node):
+        super().__init__(section, etype, node)
+        self.fdt_list_dir = None
+        self.filename_pattern = None
+        self.required_props = ['fdt-list-dir']
+        self._cur_fdt = None
+        self._fdt_phase = None
+        self.fdtgrep = None
+        self._fdt_dir = None
+        self._fdts = None
+        self._fname_pattern = None
+        self._remove_props = None
+        self.alternates = None
+
+    def ReadNode(self):
+        """Read properties from the node"""
+        super().ReadNode()
+        self._fdt_dir = fdt_util.GetString(self._node, 'fdt-list-dir')
+        fname = tools.get_input_filename(self._fdt_dir)
+        fdts = glob.glob('*.dtb', root_dir=fname)
+        self._fdts = [os.path.splitext(f)[0] for f in fdts]
+
+        self._fdt_phase = fdt_util.GetString(self._node, 'fdt-phase')
+
+        # This is used by Image.WriteAlternates()
+        self.alternates = self._fdts
+
+        self._fname_pattern = fdt_util.GetString(self._node, 
'filename-pattern')
+
+        self._remove_props = []
+        props, = self.GetEntryArgsOrProps(
+            [EntryArg('of-spl-remove-props', str)], required=False)
+        if props:
+            self._remove_props = props.split()
+
+    def FdtContents(self, fdt_etype):
+        # If there is no current FDT, just use the normal one
+        if not self._cur_fdt:
+            return self.section.FdtContents(fdt_etype)
+
+        # Find the file to use
+        fname = os.path.join(self._fdt_dir, f'{self._cur_fdt}.dtb')
+        infile = tools.get_input_filename(fname)
+
+        # Run fdtgrep if needed, to remove unwanted nodes and properties
+        if self._fdt_phase:
+            uniq = self.GetUniqueName()
+            outfile = tools.get_output_filename(
+                f'{uniq}.{self._cur_fdt}-{self._fdt_phase}.dtb')
+            self.fdtgrep.create_for_phase(infile, self._fdt_phase, outfile,
+                                          self._remove_props)
+            return outfile, tools.read_file(outfile)
+        return fname, tools.read_file(infile)
+
+    def ProcessWithFdt(self, alt):
+        """Produce the contents of this entry, using a particular FDT blob
+
+        Args:
+            alt (str): Name of the alternate
+
+        Returns:
+            tuple:
+                str: Filename to use for the alternate's .bin file
+                bytes: Contents of this entry's section, using the selected FDT
+        """
+        pattern = self._fname_pattern or 'NAME.bin'
+        fname = pattern.replace('NAME', alt)
+
+        data = b''
+        try:
+            self._cur_fdt = alt
+            self.ProcessContents()
+            data = self.GetPaddedData()
+        finally:
+            self._cur_fdt = None
+        return fname, data
+
+    def AddBintools(self, btools):
+        super().AddBintools(btools)
+        self.fdtgrep = self.AddBintool(btools, 'fdtgrep')
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index d091855b8e3..684e960b582 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -7,6 +7,7 @@
 #    python -m unittest func_test.TestFunctional.testHelp
 
 import collections
+import glob
 import gzip
 import hashlib
 from optparse import OptionParser
@@ -7484,6 +7485,126 @@ fdt         fdtmap                Extract the 
devicetree blob from the fdtmap
             err,
             "Image '.*' is missing external blobs and is non-functional: .*")
 
+    def CheckAlternates(self, dts, phase, xpl_data):
+        """Run the test for the alterative-fdt etype
+
+        Args:
+            dts (str): Devicetree file to process
+            phase (str): Phase to process ('spl', 'tpl' or 'vpl')
+            xpl_data (bytes): Expected data for the phase's binary
+
+        Returns:
+            dict of .dtb files produced
+                key: str filename
+                value: Fdt object
+        """
+        testdir = TestFunctional._MakeInputDir('dtb')
+        dtb_list = []
+        for fname in glob.glob(f'{self.TestFile("alt_dts")}/*.dts'):
+            tmp_fname = fdt_util.EnsureCompiled(fname, testdir)
+            base = os.path.splitext(os.path.basename(fname))[0]
+            dtb_list.append(base + '.bin')
+            shutil.move(tmp_fname, os.path.join(testdir, base + '.dtb'))
+
+        entry_args = {
+            f'{phase}-dtb': '1',
+            f'{phase}-bss-pad': 'y',
+            'of-spl-remove-props': 'prop-to-remove another-prop-to-get-rid-of',
+        }
+        data = self._DoReadFileDtb(dts, use_real_dtb=True, update_dtb=True,
+                                   use_expanded=True, entry_args=entry_args)[0]
+        self.assertEqual(xpl_data, data[:len(xpl_data)])
+        rest = data[len(xpl_data):]
+        pad_len = 10
+        self.assertEqual(tools.get_bytes(0, pad_len), rest[:pad_len])
+
+        # Check the dtb is using the test file
+        dtb_data = rest[pad_len:]
+        dtb = fdt.Fdt.FromData(dtb_data)
+        dtb.Scan()
+        fdt_size = dtb.GetFdtObj().totalsize()
+        self.assertEqual('model-not-set',
+                         fdt_util.GetString(dtb.GetRoot(), 'compatible'))
+
+        pad_len = 10
+
+        # Check the other output files
+        dtbs = {}
+        for fname in dtb_list:
+            pathname = tools.get_output_filename(fname)
+            self.assertTrue(os.path.exists(pathname))
+
+            data = tools.read_file(pathname)
+            self.assertEqual(xpl_data, data[:len(xpl_data)])
+            rest = data[len(xpl_data):]
+
+            self.assertEqual(tools.get_bytes(0, pad_len), rest[:pad_len])
+            rest = rest[pad_len:]
+
+            dtb = fdt.Fdt.FromData(rest)
+            dtb.Scan()
+            dtbs[fname] = dtb
+
+            expected = 'one' if '1' in fname else 'two'
+            self.assertEqual(f'u-boot,model-{expected}',
+                             fdt_util.GetString(dtb.GetRoot(), 'compatible'))
+
+            # Make sure the FDT is the same size as the 'main' one
+            rest = rest[fdt_size:]
+
+            self.assertEqual(b'', rest)
+        return dtbs
+
+    def testAlternatesFdt(self):
+        """Test handling of alternates-fdt etype"""
+        self._SetupTplElf()
+        dtbs = self.CheckAlternates('328_alternates_fdt.dts', 'tpl',
+                                    U_BOOT_TPL_NODTB_DATA)
+        for dtb in dtbs.values():
+            # Check for the node with the tag
+            node = dtb.GetNode('/node')
+            self.assertIsNotNone(node)
+            self.assertEqual(5, len(node.props.keys()))
+
+            # Make sure the other node is still there
+            self.assertIsNotNone(dtb.GetNode('/node/other-node'))
+
+    def testAlternatesFdtgrep(self):
+        """Test handling of alternates-fdt etype using fdtgrep"""
+        self._SetupTplElf()
+        dtbs = self.CheckAlternates('329_alternates_fdtgrep.dts', 'tpl',
+                                    U_BOOT_TPL_NODTB_DATA)
+        for dtb in dtbs.values():
+            # Check for the node with the tag
+            node = dtb.GetNode('/node')
+            self.assertIsNotNone(node)
+            self.assertEqual({'some-prop', 'not-a-prop-to-remove'},
+                             node.props.keys())
+
+            # Make sure the other node is gone
+            self.assertIsNone(dtb.GetNode('/node/other-node'))
+
+    def testAlternatesFdtgrepVpl(self):
+        """Test handling of alternates-fdt etype using fdtgrep with vpl"""
+        self._SetupVplElf()
+        dtbs = self.CheckAlternates('330_alternates_vpl.dts', 'vpl',
+                                    U_BOOT_VPL_NODTB_DATA)
+
+    def testAlternatesFdtgrepSpl(self):
+        """Test handling of alternates-fdt etype using fdtgrep with spl"""
+        self._SetupSplElf()
+        dtbs = self.CheckAlternates('331_alternates_spl.dts', 'spl',
+                                    U_BOOT_SPL_NODTB_DATA)
+
+    def testAlternatesFdtgrepInval(self):
+        """Test alternates-fdt etype using fdtgrep with invalid phase"""
+        self._SetupSplElf()
+        with self.assertRaises(ValueError) as e:
+            dtbs = self.CheckAlternates('332_alternates_inval.dts', 'spl',
+                                        U_BOOT_SPL_NODTB_DATA)
+        self.assertIn("Invalid U-Boot phase 'bad-phase': Use tpl/vpl/spl",
+                      str(e.exception))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/image.py b/tools/binman/image.py
index c1be5cc23a2..702c9055585 100644
--- a/tools/binman/image.py
+++ b/tools/binman/image.py
@@ -193,6 +193,19 @@ class Image(section.Entry_section):
                 os.remove(sname)
             os.symlink(fname, sname)
 
+    def WriteAlternates(self):
+        """Write out alternative devicetree blobs, each in its own file"""
+        alt_entry = self.FindEntryType('alternates-fdt')
+        if not alt_entry:
+            return
+
+        for alt in alt_entry.alternates:
+            fname, data = alt_entry.ProcessWithFdt(alt)
+            pathname = tools.get_output_filename(fname)
+            tout.info(f"Writing alternate '{alt}' to '{pathname}'")
+            tools.write_file(pathname, data)
+            tout.info("Wrote %#x bytes" % len(data))
+
     def WriteMap(self):
         """Write a map of the image to a .map file
 
diff --git a/tools/binman/test/328_alternates_fdt.dts 
b/tools/binman/test/328_alternates_fdt.dts
new file mode 100644
index 00000000000..c913c8e4745
--- /dev/null
+++ b/tools/binman/test/328_alternates_fdt.dts
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       compatible = "model-not-set";
+
+       binman {
+               alternates-fdt {
+                       fdt-list-dir = "dtb";
+                       filename-pattern = "NAME.bin";
+
+                       section {
+                               u-boot-tpl {
+                               };
+                       };
+               };
+
+               blob {
+                       filename = "blobfile";
+               };
+       };
+};
diff --git a/tools/binman/test/329_alternates_fdtgrep.dts 
b/tools/binman/test/329_alternates_fdtgrep.dts
new file mode 100644
index 00000000000..41695281456
--- /dev/null
+++ b/tools/binman/test/329_alternates_fdtgrep.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       compatible = "model-not-set";
+
+       binman {
+               alternates-fdt {
+                       fdt-list-dir = "dtb";
+                       filename-pattern = "NAME.bin";
+                       fdt-phase = "tpl";
+
+                       section {
+                               u-boot-tpl {
+                               };
+                       };
+               };
+
+               blob {
+                       filename = "blobfile";
+               };
+       };
+};
diff --git a/tools/binman/test/330_alternates_vpl.dts 
b/tools/binman/test/330_alternates_vpl.dts
new file mode 100644
index 00000000000..5b57069e2ab
--- /dev/null
+++ b/tools/binman/test/330_alternates_vpl.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       compatible = "model-not-set";
+
+       binman {
+               alternates-fdt {
+                       fdt-list-dir = "dtb";
+                       filename-pattern = "NAME.bin";
+                       fdt-phase = "vpl";
+
+                       section {
+                               u-boot-vpl {
+                               };
+                       };
+               };
+
+               blob {
+                       filename = "blobfile";
+               };
+       };
+};
diff --git a/tools/binman/test/331_alternates_spl.dts 
b/tools/binman/test/331_alternates_spl.dts
new file mode 100644
index 00000000000..882fefce34a
--- /dev/null
+++ b/tools/binman/test/331_alternates_spl.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       compatible = "model-not-set";
+
+       binman {
+               alternates-fdt {
+                       fdt-list-dir = "dtb";
+                       filename-pattern = "NAME.bin";
+                       fdt-phase = "spl";
+
+                       section {
+                               u-boot-spl {
+                               };
+                       };
+               };
+
+               blob {
+                       filename = "blobfile";
+               };
+       };
+};
diff --git a/tools/binman/test/332_alternates_inval.dts 
b/tools/binman/test/332_alternates_inval.dts
new file mode 100644
index 00000000000..8c145dd2449
--- /dev/null
+++ b/tools/binman/test/332_alternates_inval.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       compatible = "model-not-set";
+
+       binman {
+               alternates-fdt {
+                       fdt-list-dir = "dtb";
+                       filename-pattern = "NAME.bin";
+                       fdt-phase = "bad-phase";
+
+                       section {
+                               u-boot-spl {
+                               };
+                       };
+               };
+
+               blob {
+                       filename = "blobfile";
+               };
+       };
+};
diff --git a/tools/binman/test/alt_dts/model1.dts 
b/tools/binman/test/alt_dts/model1.dts
new file mode 100644
index 00000000000..01e95e8fabe
--- /dev/null
+++ b/tools/binman/test/alt_dts/model1.dts
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       model = "Model One";
+       compatible = "u-boot,model-one";
+
+       /* this node remains due to bootph-pre-sram tag */
+       node {
+               some-prop;
+               prop-to-remove;
+               another-prop-to-get-rid-of;
+               not-a-prop-to-remove;
+               bootph-pre-sram;
+
+               /* this node get removed by fdtgrep */
+               other-node {
+                       another-prop;
+               };
+       };
+};
diff --git a/tools/binman/test/alt_dts/model2.dts 
b/tools/binman/test/alt_dts/model2.dts
new file mode 100644
index 00000000000..7829c519772
--- /dev/null
+++ b/tools/binman/test/alt_dts/model2.dts
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright 2024 Google LLC
+// Written by Simon Glass <s...@chromium.org>
+
+/dts-v1/;
+
+/ {
+       model = "Model Two";
+       compatible = "u-boot,model-two";
+
+       /* this node remains due to bootph-pre-sram tag */
+       node {
+               some-prop;
+               prop-to-remove;
+               another-prop-to-get-rid-of;
+               not-a-prop-to-remove;
+               bootph-pre-sram;
+
+               /* this node get removed by fdtgrep */
+               other-node {
+                       another-prop;
+               };
+       };
+};
-- 
2.34.1

Reply via email to