Browse Source

binman: Support generating FITs with multiple dtbs

In some cases it is useful to generate a FIT which has a number of DTB
images, selectable by configuration. Add support for this in binman,
using a simple iterator and string substitution.

Signed-off-by: Simon Glass <sjg@chromium.org>
Simon Glass 3 years ago
parent
commit
6cf9953bfb

+ 47 - 1
tools/binman/README.entries

@@ -339,6 +339,7 @@ For example, this creates an image containing a FIT with U-Boot SPL:
     binman {
     binman {
         fit {
         fit {
             description = "Test FIT";
             description = "Test FIT";
+            fit,fdt-list = "of-list";
 
 
             images {
             images {
                 kernel@1 {
                 kernel@1 {
@@ -357,7 +358,52 @@ For example, this creates an image containing a FIT with U-Boot SPL:
         };
         };
     };
     };
 
 
-Properties:
+U-Boot supports creating fdt and config nodes automatically. To do this,
+pass an of-list property (e.g. -a of-list=file1 file2). This tells binman
+that you want to generates nodes for two files: file1.dtb and file2.dtb
+The fit,fdt-list property (see above) indicates that of-list should be used.
+If the property is missing you will get an error.
+
+Then add a 'generator node', a node with a name starting with '@':
+
+    images {
+        @fdt-SEQ {
+            description = "fdt-NAME";
+            type = "flat_dt";
+            compression = "none";
+        };
+    };
+
+This tells binman to create nodes fdt-1 and fdt-2 for each of your two
+files. All the properties you specify will be included in the node. This
+node acts like a template to generate the nodes. The generator node itself
+does not appear in the output - it is replaced with what binman generates.
+
+You can create config nodes in a similar way:
+
+    configurations {
+        default = "@config-DEFAULT-SEQ";
+        @config-SEQ {
+            description = "NAME";
+            firmware = "uboot";
+            loadables = "atf";
+            fdt = "fdt-SEQ";
+        };
+    };
+
+This tells binman to create nodes config-1 and config-2, i.e. a config for
+each of your two files.
+
+Available substitutions for '@' nodes are:
+
+    SEQ    Sequence number of the generated fdt (1, 2, ...)
+    NAME   Name of the dtb as provided (i.e. without adding '.dtb')
+
+Note that if no devicetree files are provided (with '-a of-list' as above)
+then no nodes will be generated.
+
+
+Properties (in the 'fit' node itself):
     fit,external-offset: Indicates that the contents of the FIT are external
     fit,external-offset: Indicates that the contents of the FIT are external
         and provides the external offset. This is passsed to mkimage via
         and provides the external offset. This is passsed to mkimage via
         the -E and -p flags.
         the -E and -p flags.

+ 88 - 6
tools/binman/etype/fit.py

@@ -8,7 +8,7 @@
 from collections import defaultdict, OrderedDict
 from collections import defaultdict, OrderedDict
 import libfdt
 import libfdt
 
 
-from binman.entry import Entry
+from binman.entry import Entry, EntryArg
 from dtoc import fdt_util
 from dtoc import fdt_util
 from dtoc.fdt import Fdt
 from dtoc.fdt import Fdt
 from patman import tools
 from patman import tools
@@ -27,6 +27,7 @@ class Entry_fit(Entry):
         binman {
         binman {
             fit {
             fit {
                 description = "Test FIT";
                 description = "Test FIT";
+                fit,fdt-list = "of-list";
 
 
                 images {
                 images {
                     kernel@1 {
                     kernel@1 {
@@ -45,7 +46,52 @@ class Entry_fit(Entry):
             };
             };
         };
         };
 
 
-    Properties:
+    U-Boot supports creating fdt and config nodes automatically. To do this,
+    pass an of-list property (e.g. -a of-list=file1 file2). This tells binman
+    that you want to generates nodes for two files: file1.dtb and file2.dtb
+    The fit,fdt-list property (see above) indicates that of-list should be used.
+    If the property is missing you will get an error.
+
+    Then add a 'generator node', a node with a name starting with '@':
+
+        images {
+            @fdt-SEQ {
+                description = "fdt-NAME";
+                type = "flat_dt";
+                compression = "none";
+            };
+        };
+
+    This tells binman to create nodes fdt-1 and fdt-2 for each of your two
+    files. All the properties you specify will be included in the node. This
+    node acts like a template to generate the nodes. The generator node itself
+    does not appear in the output - it is replaced with what binman generates.
+
+    You can create config nodes in a similar way:
+
+        configurations {
+            default = "@config-DEFAULT-SEQ";
+            @config-SEQ {
+                description = "NAME";
+                firmware = "uboot";
+                loadables = "atf";
+                fdt = "fdt-SEQ";
+            };
+        };
+
+    This tells binman to create nodes config-1 and config-2, i.e. a config for
+    each of your two files.
+
+    Available substitutions for '@' nodes are:
+
+        SEQ    Sequence number of the generated fdt (1, 2, ...)
+        NAME   Name of the dtb as provided (i.e. without adding '.dtb')
+
+    Note that if no devicetree files are provided (with '-a of-list' as above)
+    then no nodes will be generated.
+
+
+    Properties (in the 'fit' node itself):
         fit,external-offset: Indicates that the contents of the FIT are external
         fit,external-offset: Indicates that the contents of the FIT are external
             and provides the external offset. This is passsed to mkimage via
             and provides the external offset. This is passsed to mkimage via
             the -E and -p flags.
             the -E and -p flags.
@@ -64,6 +110,17 @@ class Entry_fit(Entry):
         self._fit = None
         self._fit = None
         self._fit_sections = {}
         self._fit_sections = {}
         self._fit_props = {}
         self._fit_props = {}
+        for pname, prop in self._node.props.items():
+            if pname.startswith('fit,'):
+                self._fit_props[pname] = prop
+
+        self._fdts = None
+        self._fit_list_prop = self._fit_props.get('fit,fdt-list')
+        if self._fit_list_prop:
+            fdts, = self.GetEntryArgsOrProps(
+                [EntryArg(self._fit_list_prop.value, str)])
+            if fdts is not None:
+                self._fdts = fdts.split()
 
 
     def ReadNode(self):
     def ReadNode(self):
         self._ReadSubnodes()
         self._ReadSubnodes()
@@ -84,13 +141,12 @@ class Entry_fit(Entry):
                   image
                   image
             """
             """
             for pname, prop in node.props.items():
             for pname, prop in node.props.items():
-                if pname.startswith('fit,'):
-                    self._fit_props[pname] = prop
-                else:
+                if not pname.startswith('fit,'):
                     fsw.property(pname, prop.bytes)
                     fsw.property(pname, prop.bytes)
 
 
             rel_path = node.path[len(base_node.path):]
             rel_path = node.path[len(base_node.path):]
-            has_images = depth == 2 and rel_path.startswith('/images/')
+            in_images = rel_path.startswith('/images')
+            has_images = depth == 2 and in_images
             if has_images:
             if has_images:
                 # This node is a FIT subimage node (e.g. "/images/kernel")
                 # This node is a FIT subimage node (e.g. "/images/kernel")
                 # containing content nodes. We collect the subimage nodes and
                 # containing content nodes. We collect the subimage nodes and
@@ -108,6 +164,32 @@ class Entry_fit(Entry):
                     # the FIT (e.g. "/images/kernel/u-boot"), so don't call
                     # the FIT (e.g. "/images/kernel/u-boot"), so don't call
                     # fsw.add_node() or _AddNode() for it.
                     # fsw.add_node() or _AddNode() for it.
                     pass
                     pass
+                elif subnode.name.startswith('@'):
+                    if self._fdts:
+                        # Generate notes for each FDT
+                        for seq, fdt_fname in enumerate(self._fdts):
+                            node_name = subnode.name[1:].replace('SEQ',
+                                                                 str(seq + 1))
+                            fname = tools.GetInputFilename(fdt_fname + '.dtb')
+                            with fsw.add_node(node_name):
+                                for pname, prop in subnode.props.items():
+                                    val = prop.bytes.replace(
+                                        b'NAME', tools.ToBytes(fdt_fname))
+                                    val = val.replace(
+                                        b'SEQ', tools.ToBytes(str(seq + 1)))
+                                    fsw.property(pname, val)
+
+                                    # Add data for 'fdt' nodes (but not 'config')
+                                    if depth == 1 and in_images:
+                                        fsw.property('data',
+                                                     tools.ReadFile(fname))
+                    else:
+                        if self._fdts is None:
+                            if self._fit_list_prop:
+                                self.Raise("Generator node requires '%s' entry argument" %
+                                           self._fit_list_prop.value)
+                            else:
+                                self.Raise("Generator node requires 'fit,fdt-list' property")
                 else:
                 else:
                     with fsw.add_node(subnode.name):
                     with fsw.add_node(subnode.name):
                         _AddNode(base_node, depth + 1, subnode)
                         _AddNode(base_node, depth + 1, subnode)

+ 102 - 4
tools/binman/ftest.py

@@ -75,6 +75,11 @@ FSP_M_DATA            = b'fsp_m'
 FSP_S_DATA            = b'fsp_s'
 FSP_S_DATA            = b'fsp_s'
 FSP_T_DATA            = b'fsp_t'
 FSP_T_DATA            = b'fsp_t'
 ATF_BL31_DATA         = b'bl31'
 ATF_BL31_DATA         = b'bl31'
+TEST_FDT1_DATA        = b'fdt1'
+TEST_FDT2_DATA        = b'test-fdt2'
+
+# Subdirectory of the input dir to use to put test FDTs
+TEST_FDT_SUBDIR       = 'fdts'
 
 
 # The expected size for the device tree in some tests
 # The expected size for the device tree in some tests
 EXTRACT_DTB_SIZE = 0x3c9
 EXTRACT_DTB_SIZE = 0x3c9
@@ -170,6 +175,12 @@ class TestFunctional(unittest.TestCase):
         TestFunctional._MakeInputFile('compress', COMPRESS_DATA)
         TestFunctional._MakeInputFile('compress', COMPRESS_DATA)
         TestFunctional._MakeInputFile('bl31.bin', ATF_BL31_DATA)
         TestFunctional._MakeInputFile('bl31.bin', ATF_BL31_DATA)
 
 
+        # Add a few .dtb files for testing
+        TestFunctional._MakeInputFile('%s/test-fdt1.dtb' % TEST_FDT_SUBDIR,
+                                      TEST_FDT1_DATA)
+        TestFunctional._MakeInputFile('%s/test-fdt2.dtb' % TEST_FDT_SUBDIR,
+                                      TEST_FDT2_DATA)
+
         # Travis-CI may have an old lz4
         # Travis-CI may have an old lz4
         cls.have_lz4 = True
         cls.have_lz4 = True
         try:
         try:
@@ -287,7 +298,7 @@ class TestFunctional(unittest.TestCase):
 
 
     def _DoTestFile(self, fname, debug=False, map=False, update_dtb=False,
     def _DoTestFile(self, fname, debug=False, map=False, update_dtb=False,
                     entry_args=None, images=None, use_real_dtb=False,
                     entry_args=None, images=None, use_real_dtb=False,
-                    verbosity=None, allow_missing=False):
+                    verbosity=None, allow_missing=False, extra_indirs=None):
         """Run binman with a given test file
         """Run binman with a given test file
 
 
         Args:
         Args:
@@ -307,6 +318,7 @@ class TestFunctional(unittest.TestCase):
             verbosity: Verbosity level to use (0-3, None=don't set it)
             verbosity: Verbosity level to use (0-3, None=don't set it)
             allow_missing: Set the '--allow-missing' flag so that missing
             allow_missing: Set the '--allow-missing' flag so that missing
                 external binaries just produce a warning instead of an error
                 external binaries just produce a warning instead of an error
+            extra_indirs: Extra input directories to add using -I
         """
         """
         args = []
         args = []
         if debug:
         if debug:
@@ -333,6 +345,9 @@ class TestFunctional(unittest.TestCase):
         if images:
         if images:
             for image in images:
             for image in images:
                 args += ['-i', image]
                 args += ['-i', image]
+        if extra_indirs:
+            for indir in extra_indirs:
+                args += ['-I', indir]
         return self._DoBinman(*args)
         return self._DoBinman(*args)
 
 
     def _SetupDtb(self, fname, outfile='u-boot.dtb'):
     def _SetupDtb(self, fname, outfile='u-boot.dtb'):
@@ -382,7 +397,8 @@ class TestFunctional(unittest.TestCase):
         return dtb.GetContents()
         return dtb.GetContents()
 
 
     def _DoReadFileDtb(self, fname, use_real_dtb=False, map=False,
     def _DoReadFileDtb(self, fname, use_real_dtb=False, map=False,
-                       update_dtb=False, entry_args=None, reset_dtbs=True):
+                       update_dtb=False, entry_args=None, reset_dtbs=True,
+                       extra_indirs=None):
         """Run binman and return the resulting image
         """Run binman and return the resulting image
 
 
         This runs binman with a given test file and then reads the resulting
         This runs binman with a given test file and then reads the resulting
@@ -406,6 +422,7 @@ class TestFunctional(unittest.TestCase):
             reset_dtbs: With use_real_dtb the test dtb is overwritten by this
             reset_dtbs: With use_real_dtb the test dtb is overwritten by this
                 function. If reset_dtbs is True, then the original test dtb
                 function. If reset_dtbs is True, then the original test dtb
                 is written back before this function finishes
                 is written back before this function finishes
+            extra_indirs: Extra input directories to add using -I
 
 
         Returns:
         Returns:
             Tuple:
             Tuple:
@@ -429,7 +446,8 @@ class TestFunctional(unittest.TestCase):
 
 
         try:
         try:
             retcode = self._DoTestFile(fname, map=map, update_dtb=update_dtb,
             retcode = self._DoTestFile(fname, map=map, update_dtb=update_dtb,
-                    entry_args=entry_args, use_real_dtb=use_real_dtb)
+                    entry_args=entry_args, use_real_dtb=use_real_dtb,
+                    extra_indirs=extra_indirs)
             self.assertEqual(0, retcode)
             self.assertEqual(0, retcode)
             out_dtb_fname = tools.GetOutputFilename('u-boot.dtb.out')
             out_dtb_fname = tools.GetOutputFilename('u-boot.dtb.out')
 
 
@@ -3557,7 +3575,87 @@ class TestFunctional(unittest.TestCase):
         data = self._DoReadFile('169_atf_bl31.dts')
         data = self._DoReadFile('169_atf_bl31.dts')
         self.assertEqual(ATF_BL31_DATA, data[:len(ATF_BL31_DATA)])
         self.assertEqual(ATF_BL31_DATA, data[:len(ATF_BL31_DATA)])
 
 
-ATF_BL31_DATA
+    def testFitFdt(self):
+        """Test an image with an FIT with multiple FDT images"""
+        def _CheckFdt(seq, expected_data):
+            """Check the FDT nodes
+
+            Args:
+                seq: Sequence number to check (0 or 1)
+                expected_data: Expected contents of 'data' property
+            """
+            name = 'fdt-%d' % seq
+            fnode = dtb.GetNode('/images/%s' % name)
+            self.assertIsNotNone(fnode)
+            self.assertEqual({'description','type', 'compression', 'data'},
+                             set(fnode.props.keys()))
+            self.assertEqual(expected_data, fnode.props['data'].bytes)
+            self.assertEqual('fdt-test-fdt%d.dtb' % seq,
+                             fnode.props['description'].value)
+
+        def _CheckConfig(seq, expected_data):
+            """Check the configuration nodes
+
+            Args:
+                seq: Sequence number to check (0 or 1)
+                expected_data: Expected contents of 'data' property
+            """
+            cnode = dtb.GetNode('/configurations')
+            self.assertIn('default', cnode.props)
+            self.assertEqual('config-1', cnode.props['default'].value)
+
+            name = 'config-%d' % seq
+            fnode = dtb.GetNode('/configurations/%s' % name)
+            self.assertIsNotNone(fnode)
+            self.assertEqual({'description','firmware', 'loadables', 'fdt'},
+                             set(fnode.props.keys()))
+            self.assertEqual('conf-test-fdt%d.dtb' % seq,
+                             fnode.props['description'].value)
+            self.assertEqual('fdt-%d' % seq, fnode.props['fdt'].value)
+
+        entry_args = {
+            'of-list': 'test-fdt1 test-fdt2',
+        }
+        data = self._DoReadFileDtb(
+            '170_fit_fdt.dts',
+            entry_args=entry_args,
+            extra_indirs=[os.path.join(self._indir, TEST_FDT_SUBDIR)])[0]
+        self.assertEqual(U_BOOT_NODTB_DATA, data[-len(U_BOOT_NODTB_DATA):])
+        fit_data = data[len(U_BOOT_DATA):-len(U_BOOT_NODTB_DATA)]
+
+        dtb = fdt.Fdt.FromData(fit_data)
+        dtb.Scan()
+        fnode = dtb.GetNode('/images/kernel')
+        self.assertIn('data', fnode.props)
+
+        # Check all the properties in fdt-1 and fdt-2
+        _CheckFdt(1, TEST_FDT1_DATA)
+        _CheckFdt(2, TEST_FDT2_DATA)
+
+        # Check configurations
+        _CheckConfig(1, TEST_FDT1_DATA)
+        _CheckConfig(2, TEST_FDT2_DATA)
+
+    def testFitFdtMissingList(self):
+        """Test handling of a missing 'of-list' entry arg"""
+        with self.assertRaises(ValueError) as e:
+            self._DoReadFile('170_fit_fdt.dts')
+        self.assertIn("Generator node requires 'of-list' entry argument",
+                      str(e.exception))
+
+    def testFitFdtEmptyList(self):
+        """Test handling of an empty 'of-list' entry arg"""
+        entry_args = {
+            'of-list': '',
+        }
+        data = self._DoReadFileDtb('170_fit_fdt.dts', entry_args=entry_args)[0]
+
+    def testFitFdtMissingProp(self):
+        """Test handling of a missing 'fit,fdt-list' property"""
+        with self.assertRaises(ValueError) as e:
+            self._DoReadFile('171_fit_fdt_missing_prop.dts')
+        self.assertIn("Generator node requires 'fit,fdt-list' property",
+                      str(e.exception))
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     unittest.main()
     unittest.main()

+ 55 - 0
tools/binman/test/170_fit_fdt.dts

@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot {
+		};
+		fit {
+			description = "test-desc";
+			#address-cells = <1>;
+			fit,fdt-list = "of-list";
+
+			images {
+				kernel {
+					description = "Vanilla Linux kernel";
+					type = "kernel";
+					arch = "ppc";
+					os = "linux";
+					compression = "gzip";
+					load = <00000000>;
+					entry = <00000000>;
+					hash-1 {
+						algo = "crc32";
+					};
+					hash-2 {
+						algo = "sha1";
+					};
+					u-boot {
+					};
+				};
+				@fdt-SEQ {
+					description = "fdt-NAME.dtb";
+					type = "flat_dt";
+					compression = "none";
+				};
+			};
+
+			configurations {
+				default = "config-1";
+				@config-SEQ {
+					description = "conf-NAME.dtb";
+					firmware = "uboot";
+					loadables = "atf";
+					fdt = "fdt-SEQ";
+				};
+			};
+		};
+		u-boot-nodtb {
+		};
+	};
+};

+ 54 - 0
tools/binman/test/171_fit_fdt_missing_prop.dts

@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot {
+		};
+		fit {
+			description = "test-desc";
+			#address-cells = <1>;
+
+			images {
+				kernel {
+					description = "Vanilla Linux kernel";
+					type = "kernel";
+					arch = "ppc";
+					os = "linux";
+					compression = "gzip";
+					load = <00000000>;
+					entry = <00000000>;
+					hash-1 {
+						algo = "crc32";
+					};
+					hash-2 {
+						algo = "sha1";
+					};
+					u-boot {
+					};
+				};
+				@fdt-SEQ {
+					description = "fdt-NAME.dtb";
+					type = "flat_dt";
+					compression = "none";
+				};
+			};
+
+			configurations {
+				default = "config-1";
+				@config-SEQ {
+					description = "conf-NAME.dtb";
+					firmware = "uboot";
+					loadables = "atf";
+					fdt = "fdt-SEQ";
+				};
+			};
+		};
+		u-boot-nodtb {
+		};
+	};
+};