Merge tag 'dm-pull-20jul23' of https://source.denx.de/u-boot/custodians/u-boot-dm

binman mkimage and template enhancements
misc fixes
diff --git a/arch/arm/dts/stm32mp15-u-boot.dtsi b/arch/arm/dts/stm32mp15-u-boot.dtsi
index d872c6f..573dd4d 100644
--- a/arch/arm/dts/stm32mp15-u-boot.dtsi
+++ b/arch/arm/dts/stm32mp15-u-boot.dtsi
@@ -226,6 +226,7 @@
 		mkimage {
 			args = "-T stm32image -a 0x2ffc2500 -e 0x2ffc2500";
 			u-boot-spl {
+				no-write-symbols;
 			};
 		};
 	};
diff --git a/arch/sandbox/include/asm/sdl.h b/arch/sandbox/include/asm/sdl.h
index 56dcb84..ee4991f 100644
--- a/arch/sandbox/include/asm/sdl.h
+++ b/arch/sandbox/include/asm/sdl.h
@@ -7,6 +7,7 @@
 #define __SANDBOX_SDL_H
 
 #include <errno.h>
+#include <video.h>
 
 #ifdef CONFIG_SANDBOX_SDL
 
@@ -87,6 +88,22 @@
  */
 int sandbox_sdl_sound_init(int rate, int channels);
 
+/**
+ * sandbox_sdl_set_bpp() - Set the depth of the sandbox display
+ *
+ * The device must not be active when this function is called. It activiates it
+ * before returning.
+ *
+ * This updates the depth value and adjusts a few other settings accordingly.
+ * It must be called before the display is probed.
+ *
+ * @dev: Device to adjust
+ * @l2bpp: depth to set
+ * Return: 0 if the device was already active, other error if it fails to probe
+ * after the change
+ */
+int sandbox_sdl_set_bpp(struct udevice *dev, enum video_log2_bpp l2bpp);
+
 #else
 static inline int sandbox_sdl_init_display(int width, int height, int log2_bpp,
 					   bool double_size)
@@ -134,6 +151,12 @@
 	return -ENODEV;
 }
 
+static inline int sandbox_sdl_set_bpp(struct udevice *dev,
+				      enum video_log2_bpp l2bpp)
+{
+	return -ENOSYS;
+}
+
 #endif
 
 #endif
diff --git a/arch/sandbox/include/asm/test.h b/arch/sandbox/include/asm/test.h
index e482271..17159f8 100644
--- a/arch/sandbox/include/asm/test.h
+++ b/arch/sandbox/include/asm/test.h
@@ -8,7 +8,6 @@
 #ifndef __ASM_TEST_H
 #define __ASM_TEST_H
 
-#include <video.h>
 #include <pci_ids.h>
 
 struct unit_test_state;
@@ -300,30 +299,6 @@
  */
 int sandbox_cros_ec_get_pwm_duty(struct udevice *dev, uint index, uint *duty);
 
-#if IS_ENABLED(CONFIG_SANDBOX_SDL)
-/**
- * sandbox_sdl_set_bpp() - Set the depth of the sandbox display
- *
- * The device must not be active when this function is called. It activiates it
- * before returning.
- *
- * This updates the depth value and adjusts a few other settings accordingly.
- * It must be called before the display is probed.
- *
- * @dev: Device to adjust
- * @l2bpp: depth to set
- * Return: 0 if the device was already active, other error if it fails to probe
- * after the change
- */
-int sandbox_sdl_set_bpp(struct udevice *dev, enum video_log2_bpp l2bpp);
-#else
-static inline int sandbox_sdl_set_bpp(struct udevice *dev,
-				      enum video_log2_bpp l2bpp)
-{
-	return -ENOSYS;
-}
-#endif
-
 /**
  * sandbox_set_fake_efi_mgr_dev() - Control EFI bootmgr producing valid bootflow
  *
diff --git a/boot/bootmeth_script.c b/boot/bootmeth_script.c
index 225eb18..a4050c3 100644
--- a/boot/bootmeth_script.c
+++ b/boot/bootmeth_script.c
@@ -190,7 +190,10 @@
 	ulong addr;
 	int ret;
 
-	ret = env_set("devtype", blk_get_devtype(bflow->blk));
+	if (desc->uclass_id == UCLASS_USB)
+		ret = env_set("devtype", "usb");
+	else
+		ret = env_set("devtype", blk_get_devtype(bflow->blk));
 	if (!ret)
 		ret = env_set_hex("devnum", desc->devnum);
 	if (!ret)
diff --git a/cmd/load.c b/cmd/load.c
index 5c4f347..2715cf5 100644
--- a/cmd/load.c
+++ b/cmd/load.c
@@ -181,13 +181,17 @@
 		    } else
 #endif
 		    {
+			void *dst;
+
 			ret = lmb_reserve(&lmb, store_addr, binlen);
 			if (ret) {
 				printf("\nCannot overwrite reserved area (%08lx..%08lx)\n",
 					store_addr, store_addr + binlen);
 				return ret;
 			}
-			memcpy((char *)(store_addr), binbuf, binlen);
+			dst = map_sysmem(store_addr, binlen);
+			memcpy(dst, binbuf, binlen);
+			unmap_sysmem(dst);
 			lmb_free(&lmb, store_addr, binlen);
 		    }
 		    if ((store_addr) < start_addr)
@@ -350,15 +354,19 @@
 	if(write_record(SREC3_START))			/* write the header */
 		return (-1);
 	do {
-		if(count) {						/* collect hex data in the buffer  */
-			c = *(volatile uchar*)(address + reclen);	/* get one byte    */
-			checksum += c;							/* accumulate checksum */
+		volatile uchar *src;
+
+		src = map_sysmem(address, count);
+		if (count) {				/* collect hex data in the buffer */
+			c = src[reclen];		/* get one byte */
+			checksum += c;			/* accumulate checksum */
 			data[2*reclen]   = hex[(c>>4)&0x0f];
 			data[2*reclen+1] = hex[c & 0x0f];
 			data[2*reclen+2] = '\0';
 			++reclen;
 			--count;
 		}
+		unmap_sysmem((void *)src);
 		if(reclen == SREC_BYTES_PER_RECORD || count == 0) {
 			/* enough data collected for one record: dump it */
 			if(reclen) {	/* build & write a data record: */
diff --git a/drivers/core/of_access.c b/drivers/core/of_access.c
index 81a3079..57f1044 100644
--- a/drivers/core/of_access.c
+++ b/drivers/core/of_access.c
@@ -593,11 +593,14 @@
 int of_property_match_string(const struct device_node *np, const char *propname,
 			     const char *string)
 {
-	const struct property *prop = of_find_property(np, propname, NULL);
+	int len = 0;
+	const struct property *prop = of_find_property(np, propname, &len);
 	size_t l;
 	int i;
 	const char *p, *end;
 
+	if (!prop && len == -FDT_ERR_NOTFOUND)
+		return -ENOENT;
 	if (!prop)
 		return -EINVAL;
 	if (!prop->value)
diff --git a/drivers/core/read.c b/drivers/core/read.c
index 0289a2e..5749473 100644
--- a/drivers/core/read.c
+++ b/drivers/core/read.c
@@ -211,10 +211,9 @@
 	return dev_remap_addr_index(dev, 0);
 }
 
-fdt_addr_t dev_read_addr_size(const struct udevice *dev, const char *property,
-			      fdt_size_t *sizep)
+fdt_addr_t dev_read_addr_size(const struct udevice *dev, fdt_size_t *sizep)
 {
-	return ofnode_get_addr_size(dev_ofnode(dev), property, sizep);
+	return dev_read_addr_size_index(dev, 0, sizep);
 }
 
 const char *dev_read_name(const struct udevice *dev)
diff --git a/drivers/reset/reset-rockchip.c b/drivers/reset/reset-rockchip.c
index 2ebe338..6cabaa1 100644
--- a/drivers/reset/reset-rockchip.c
+++ b/drivers/reset/reset-rockchip.c
@@ -97,7 +97,7 @@
 	fdt_addr_t addr;
 	fdt_size_t size;
 
-	addr = dev_read_addr_size(dev, "reg", &size);
+	addr = dev_read_addr_size(dev, &size);
 	if (addr == FDT_ADDR_T_NONE)
 		return -EINVAL;
 
diff --git a/include/dm/read.h b/include/dm/read.h
index 56ac076..137f2a5 100644
--- a/include/dm/read.h
+++ b/include/dm/read.h
@@ -347,18 +347,13 @@
 void *dev_remap_addr(const struct udevice *dev);
 
 /**
- * dev_read_addr_size() - get address and size from a device property
- *
- * This does no address translation. It simply reads an property that contains
- * an address and a size value, one after the other.
+ * dev_read_addr_size() - Get the reg property of a device
  *
  * @dev: Device to read from
- * @propname: property to read
  * @sizep: place to put size value (on success)
  * Return: address value, or FDT_ADDR_T_NONE on error
  */
-fdt_addr_t dev_read_addr_size(const struct udevice *dev, const char *propname,
-			      fdt_size_t *sizep);
+fdt_addr_t dev_read_addr_size(const struct udevice *dev, fdt_size_t *sizep);
 
 /**
  * dev_read_name() - get the name of a device's node
@@ -1002,10 +997,9 @@
 }
 
 static inline fdt_addr_t dev_read_addr_size(const struct udevice *dev,
-					    const char *propname,
 					    fdt_size_t *sizep)
 {
-	return ofnode_get_addr_size(dev_ofnode(dev), propname, sizep);
+	return dev_read_addr_size_index(dev, 0, sizep);
 }
 
 static inline const char *dev_read_name(const struct udevice *dev)
diff --git a/test/dm/video.c b/test/dm/video.c
index 0534ee9..d907f68 100644
--- a/test/dm/video.c
+++ b/test/dm/video.c
@@ -15,6 +15,7 @@
 #include <video.h>
 #include <video_console.h>
 #include <asm/test.h>
+#include <asm/sdl.h>
 #include <dm/test.h>
 #include <dm/uclass-internal.h>
 #include <test/test.h>
diff --git a/tools/binman/binman.rst b/tools/binman/binman.rst
index 23cbb99..8f57b6c 100644
--- a/tools/binman/binman.rst
+++ b/tools/binman/binman.rst
@@ -727,6 +727,13 @@
     Note that missing, optional blobs do not produce a non-zero exit code from
     binman, although it does show a warning about the missing external blob.
 
+insert-template:
+    This is not strictly speaking an entry property, since it is processed early
+    in Binman before the entries are read. It is a list of phandles of nodes to
+    include in the current (target) node. For each node, its subnodes and their
+    properties are brought into the target node. See Templates_ below for
+    more information.
+
 The attributes supported for images and sections are described below. Several
 are similar to those for entries.
 
@@ -831,6 +838,13 @@
     binman. This is automatic for certain entry types, e.g. `u-boot-spl`. See
     binman_syms_ for more information.
 
+no-write-symbols:
+    Disables symbol writing for this entry. This can be used in entry types
+    where symbol writing is automatic. For example, if `u-boot-spl` refers to
+    the `u_boot_any_image_pos` symbol but U-Boot is not available in the image
+    containing SPL, this can be used to disable the writing. Quite likely this
+    indicates a bug in your setup.
+
 elf-filename:
     Sets the file name of a blob's associated ELF file. For example, if the
     blob is `zephyr.bin` then the ELF file may be `zephyr.elf`. This allows
@@ -1165,6 +1179,86 @@
      arch/arm/dts/u-boot.dtsi ... found: "arch/arm/dts/juno-r2-u-boot.dtsi"
 
 
+Templates
+=========
+
+Sometimes multiple images need to be created which have all have a common
+part. For example, a board may generate SPI and eMMC images which both include
+a FIT. Since the FIT includes many entries, it is tedious to repeat them twice
+in the image description.
+
+Templates provide a simple way to handle this::
+
+    binman {
+        multiple-images;
+        common_part: template-1 {
+            some-property;
+            fit {
+                ... lots of entries in here
+            };
+
+            text {
+                text = "base image";
+            };
+        };
+
+        spi-image {
+            filename = "image-spi.bin";
+            insert-template = <&fit>;
+
+            /* things specific to SPI follow */
+            footer {
+            ];
+
+            text {
+                text = "SPI image";
+            };
+        };
+
+        mmc-image {
+            filename = "image-mmc.bin";
+            insert-template = <&fit>;
+
+            /* things specific to MMC follow */
+            footer {
+            ];
+
+            text {
+                text = "MMC image";
+            };
+        };
+    };
+
+The template node name must start with 'template', so it is not considered to be
+an image itself.
+
+The mechanism is very simple. For each phandle in the 'insert-templates'
+property, the source node is looked up. Then the subnodes of that source node
+are copied into the target node, i.e. the one containing the `insert-template`
+property.
+
+If the target node has a node with the same name as a template, its properties
+override corresponding properties in the template. This allows the template to
+be uses as a base, with the node providing updates to the properties as needed.
+The overriding happens recursively.
+
+Template nodes appear first in each node that they are inserted into and
+ordering of template nodes is preserved. Other nodes come afterwards. If a
+template node also appears in the target node, then the template node sets the
+order. Thus the template can be used to set the ordering, even if the target
+node provides all the properties. In the above example, `fit` and `text` appear
+first in the `spi-image` and `mmc-image` images, followed by `footer`.
+
+Where there are multiple template nodes, they are inserted in that order. so
+the first template node appears first, then the second.
+
+Properties in the template node are inserted into the destination node if they
+do not exist there. In the example above, `some-property` is added to each of
+`spi-image` and `mmc-image`.
+
+Note that template nodes are not removed from the binman description at present.
+
+
 Updating an ELF file
 ====================
 
diff --git a/tools/binman/bintool.py b/tools/binman/bintool.py
index 8162968..0b0f56d 100644
--- a/tools/binman/bintool.py
+++ b/tools/binman/bintool.py
@@ -288,7 +288,7 @@
         name = os.path.expanduser(self.name)  # Expand paths containing ~
         all_args = (name,) + args
         env = tools.get_env_with_path()
-        tout.detail(f"bintool: {' '.join(all_args)}")
+        tout.debug(f"bintool: {' '.join(all_args)}")
         result = command.run_pipe(
             [all_args], capture=True, capture_stderr=True, env=env,
             raise_on_error=False, binary=binary)
diff --git a/tools/binman/control.py b/tools/binman/control.py
index 7e2dd35..25e6681 100644
--- a/tools/binman/control.py
+++ b/tools/binman/control.py
@@ -22,6 +22,7 @@
 from binman import cbfs_util
 from binman import elf
 from binman import entry
+from dtoc import fdt_util
 from u_boot_pylib import command
 from u_boot_pylib import tools
 from u_boot_pylib import tout
@@ -56,8 +57,9 @@
     images = OrderedDict()
     if 'multiple-images' in binman_node.props:
         for node in binman_node.subnodes:
-            images[node.name] = Image(node.name, node,
-                                      use_expanded=use_expanded)
+            if 'template' not in node.name:
+                images[node.name] = Image(node.name, node,
+                                          use_expanded=use_expanded)
     else:
         images['image'] = Image('image', binman_node, use_expanded=use_expanded)
     return images
@@ -478,6 +480,30 @@
 
     AfterReplace(image, allow_resize=True, write_map=write_map)
 
+def _ProcessTemplates(parent):
+    """Handle any templates in the binman description
+
+    Args:
+        parent: Binman node to process (typically /binman)
+
+    Search though each target node looking for those with an 'insert-template'
+    property. Use that as a list of references to template nodes to use to
+    adjust the target node.
+
+    Processing involves copying each subnode of the template node into the
+    target node.
+
+    This is done recursively, so templates can be at any level of the binman
+    image, e.g. inside a section.
+
+    See 'Templates' in the Binman documnentation for details.
+    """
+    for node in parent.subnodes:
+        tmpl = fdt_util.GetPhandleList(node, 'insert-template')
+        if tmpl:
+            node.copy_subnodes_from_phandles(tmpl)
+        _ProcessTemplates(node)
+
 def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt, use_expanded):
     """Prepare the images to be processed and select the device tree
 
@@ -520,6 +546,8 @@
         raise ValueError("Device tree '%s' does not have a 'binman' "
                             "node" % dtb_fname)
 
+    _ProcessTemplates(node)
+
     images = _ReadImageDesc(node, use_expanded)
 
     if select_images:
diff --git a/tools/binman/elf.py b/tools/binman/elf.py
index 5816284..4219001 100644
--- a/tools/binman/elf.py
+++ b/tools/binman/elf.py
@@ -248,6 +248,9 @@
         entry: Entry to process
         section: Section which can be used to lookup symbol values
         base_sym: Base symbol marking the start of the image
+
+    Returns:
+        int: Number of symbols written
     """
     if not base_sym:
         base_sym = '__image_copy_start'
@@ -269,12 +272,13 @@
 
     if not syms:
         tout.debug('LookupAndWriteSymbols: no syms')
-        return
+        return 0
     base = syms.get(base_sym)
     if not base and not is_elf:
         tout.debug('LookupAndWriteSymbols: no base')
-        return
+        return 0
     base_addr = 0 if is_elf else base.address
+    count = 0
     for name, sym in syms.items():
         if name.startswith('_binman'):
             msg = ("Section '%s': Symbol '%s'\n   in entry '%s'" %
@@ -307,6 +311,11 @@
                        (msg, name, offset, value, len(value_bytes)))
             entry.data = (entry.data[:offset] + value_bytes +
                         entry.data[offset + sym.size:])
+            count += 1
+    if count:
+        tout.detail(
+            f"Section '{section.GetPath()}': entry '{entry.GetPath()}' : {count} symbols")
+    return count
 
 def GetSymbolValue(sym, data, msg):
     """Get the value of a symbol
diff --git a/tools/binman/elf_test.py b/tools/binman/elf_test.py
index c980839..cc95b42 100644
--- a/tools/binman/elf_test.py
+++ b/tools/binman/elf_test.py
@@ -141,7 +141,8 @@
         entry = FakeEntry(10)
         section = FakeSection()
         elf_fname = self.ElfTestFile('u_boot_binman_syms_bad')
-        elf.LookupAndWriteSymbols(elf_fname, entry, section)
+        count = elf.LookupAndWriteSymbols(elf_fname, entry, section)
+        self.assertEqual(0, count)
 
     def testBadSymbolSize(self):
         """Test that an attempt to use an 8-bit symbol are detected
@@ -162,7 +163,7 @@
     def testNoValue(self):
         """Test the case where we have no value for the symbol
 
-        This should produce -1 values for all thress symbols, taking up the
+        This should produce -1 values for all three symbols, taking up the
         first 16 bytes of the image.
         """
         if not elf.ELF_TOOLS:
@@ -170,7 +171,8 @@
         entry = FakeEntry(28)
         section = FakeSection(sym_value=None)
         elf_fname = self.ElfTestFile('u_boot_binman_syms')
-        elf.LookupAndWriteSymbols(elf_fname, entry, section)
+        count = elf.LookupAndWriteSymbols(elf_fname, entry, section)
+        self.assertEqual(5, count)
         expected = (struct.pack('<L', elf.BINMAN_SYM_MAGIC_VALUE) +
                     tools.get_bytes(255, 20) +
                     tools.get_bytes(ord('a'), 4))
@@ -369,6 +371,11 @@
             elf.GetSymbolOffset(fname, 'embed')
         self.assertIn('__image_copy_start', str(e.exception))
 
+    def test_get_symbol_address(self):
+        fname = self.ElfTestFile('embed_data')
+        addr = elf.GetSymbolAddress(fname, 'region_size')
+        self.assertEqual(0, addr)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tools/binman/entries.rst b/tools/binman/entries.rst
index b71af80..b55f424 100644
--- a/tools/binman/entries.rst
+++ b/tools/binman/entries.rst
@@ -615,6 +615,12 @@
         `of-list` meaning that `-a of-list="dtb1 dtb2..."` should be passed
         to binman.
 
+    fit,fdt-list-val
+        As an alternative to fit,fdt-list the list of device tree files
+        can be provided in this property as a string list, e.g.::
+
+            fit,fdt-list-val = "dtb1", "dtb2";
+
 Substitutions
 ~~~~~~~~~~~~~
 
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 3945690..42e0b7b 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -158,6 +158,7 @@
         self.offset_from_elf = None
         self.preserve = False
         self.build_done = False
+        self.no_write_symbols = False
 
     @staticmethod
     def FindEntryClass(etype, expanded):
@@ -321,6 +322,7 @@
                                                              'offset-from-elf')
 
         self.preserve = fdt_util.GetBool(self._node, 'preserve')
+        self.no_write_symbols = fdt_util.GetBool(self._node, 'no-write-symbols')
 
     def GetDefaultFilename(self):
         return None
@@ -472,6 +474,9 @@
     def ObtainContents(self, skip_entry=None, fake_size=0):
         """Figure out the contents of an entry.
 
+        For missing blobs (where allow-missing is enabled), the contents are set
+        to b'' and self.missing is set to True.
+
         Args:
             skip_entry (Entry): Entry to skip when obtaining section contents
             fake_size (int): Size of fake file to create if needed
@@ -695,7 +700,7 @@
         Args:
           section: Section containing the entry
         """
-        if self.auto_write_symbols:
+        if self.auto_write_symbols and not self.no_write_symbols:
             # Check if we are writing symbols into an ELF file
             is_elf = self.GetDefaultFilename() == self.elf_fname
             elf.LookupAndWriteSymbols(self.elf_fname, self, section.GetImage(),
@@ -1309,10 +1314,6 @@
         """
         data = b''
         for entry in entries:
-            # First get the input data and put it in a file. If not available,
-            # try later.
-            if not entry.ObtainContents(fake_size=fake_size):
-                return None, None, None
             data += entry.GetData()
         uniq = self.GetUniqueName()
         fname = tools.get_output_filename(f'{prefix}.{uniq}')
diff --git a/tools/binman/etype/blob_phase.py b/tools/binman/etype/blob_phase.py
index b937158..951d993 100644
--- a/tools/binman/etype/blob_phase.py
+++ b/tools/binman/etype/blob_phase.py
@@ -52,3 +52,8 @@
 
         # Read entries again, now that we have some
         self.ReadEntries()
+
+        # Propagate the no-write-symbols property
+        if self.no_write_symbols:
+            for entry in self._entries.values():
+                entry.no_write_symbols = True
diff --git a/tools/binman/etype/fit.py b/tools/binman/etype/fit.py
index c395706..ef4d066 100644
--- a/tools/binman/etype/fit.py
+++ b/tools/binman/etype/fit.py
@@ -81,6 +81,12 @@
             `of-list` meaning that `-a of-list="dtb1 dtb2..."` should be passed
             to binman.
 
+        fit,fdt-list-val
+            As an alternative to fit,fdt-list the list of device tree files
+            can be provided in this property as a string list, e.g.::
+
+                fit,fdt-list-val = "dtb1", "dtb2";
+
     Substitutions
     ~~~~~~~~~~~~~
 
@@ -361,6 +367,9 @@
                 [EntryArg(self._fit_list_prop.value, str)])
             if fdts is not None:
                 self._fdts = fdts.split()
+        else:
+            self._fdts = fdt_util.GetStringList(self._node, 'fit,fdt-list-val')
+
         self._fit_default_dt = self.GetEntryArgsOrProps([EntryArg('default-dt',
                                                                   str)])[0]
 
diff --git a/tools/binman/etype/mkimage.py b/tools/binman/etype/mkimage.py
index e028c44..6ae5d0c 100644
--- a/tools/binman/etype/mkimage.py
+++ b/tools/binman/etype/mkimage.py
@@ -8,10 +8,11 @@
 from collections import OrderedDict
 
 from binman.entry import Entry
+from binman.etype.section import Entry_section
 from dtoc import fdt_util
 from u_boot_pylib import tools
 
-class Entry_mkimage(Entry):
+class Entry_mkimage(Entry_section):
     """Binary produced by mkimage
 
     Properties / Entry arguments:
@@ -121,54 +122,67 @@
     """
     def __init__(self, section, etype, node):
         super().__init__(section, etype, node)
-        self._multiple_data_files = fdt_util.GetBool(self._node, 'multiple-data-files')
-        self._mkimage_entries = OrderedDict()
         self._imagename = None
-        self._filename = fdt_util.GetString(self._node, 'filename')
-        self.align_default = None
+        self._multiple_data_files = False
 
     def ReadNode(self):
         super().ReadNode()
+        self._multiple_data_files = fdt_util.GetBool(self._node,
+                                                     'multiple-data-files')
         self._args = fdt_util.GetArgs(self._node, 'args')
         self._data_to_imagename = fdt_util.GetBool(self._node,
                                                    'data-to-imagename')
         if self._data_to_imagename and self._node.FindNode('imagename'):
             self.Raise('Cannot use both imagename node and data-to-imagename')
-        self.ReadEntries()
 
     def ReadEntries(self):
         """Read the subnodes to find out what should go in this image"""
         for node in self._node.subnodes:
-            entry = Entry.Create(self, node)
+            if self.IsSpecialSubnode(node):
+                continue
+            entry = Entry.Create(self, node,
+                                 expanded=self.GetImage().use_expanded,
+                                 missing_etype=self.GetImage().missing_etype)
             entry.ReadNode()
+            entry.SetPrefix(self._name_prefix)
             if entry.name == 'imagename':
                 self._imagename = entry
             else:
-                self._mkimage_entries[entry.name] = entry
+                self._entries[entry.name] = entry
 
-    def ObtainContents(self):
+    def BuildSectionData(self, required):
+        """Build mkimage entry contents
+
+        Runs mkimage to build the entry contents
+
+        Args:
+            required (bool): True if the data must be present, False if it is OK
+                to return None
+
+        Returns:
+            bytes: Contents of the section
+        """
         # Use a non-zero size for any fake files to keep mkimage happy
         # Note that testMkimageImagename() relies on this 'mkimage' parameter
         fake_size = 1024
         if self._multiple_data_files:
             fnames = []
             uniq = self.GetUniqueName()
-            for entry in self._mkimage_entries.values():
-                if not entry.ObtainContents(fake_size=fake_size):
-                    return False
-                if entry._pathname:
-                    fnames.append(entry._pathname)
+            for entry in self._entries.values():
+                # Put the contents in a temporary file
+                ename = f'mkimage-in-{uniq}-{entry.name}'
+                fname = tools.get_output_filename(ename)
+                data = entry.GetData(required)
+                tools.write_file(fname, data)
+                fnames.append(fname)
             input_fname = ":".join(fnames)
+            data = b''
         else:
             data, input_fname, uniq = self.collect_contents_to_file(
-                self._mkimage_entries.values(), 'mkimage', fake_size)
-            if data is None:
-                return False
+                self._entries.values(), 'mkimage', fake_size)
         if self._imagename:
             image_data, imagename_fname, _ = self.collect_contents_to_file(
                 [self._imagename], 'mkimage-n', 1024)
-            if image_data is None:
-                return False
         outfile = self._filename if self._filename else 'mkimage-out.%s' % uniq
         output_fname = tools.get_output_filename(outfile)
 
@@ -176,8 +190,7 @@
         self.CheckMissing(missing_list)
         self.missing = bool(missing_list)
         if self.missing:
-            self.SetContents(b'')
-            return self.allow_missing
+            return b''
 
         args = ['-d', input_fname]
         if self._data_to_imagename:
@@ -186,71 +199,58 @@
             args += ['-n', imagename_fname]
         args += self._args + [output_fname]
         if self.mkimage.run_cmd(*args) is not None:
-            self.SetContents(tools.read_file(output_fname))
+            return tools.read_file(output_fname)
         else:
             # Bintool is missing; just use the input data as the output
             self.record_missing_bintool(self.mkimage)
-            self.SetContents(data)
-
-        return True
+            return data
 
     def GetEntries(self):
         # Make a copy so we don't change the original
-        entries = OrderedDict(self._mkimage_entries)
+        entries = OrderedDict(self._entries)
         if self._imagename:
             entries['imagename'] = self._imagename
         return entries
 
-    def SetAllowMissing(self, allow_missing):
-        """Set whether a section allows missing external blobs
+    def AddBintools(self, btools):
+        super().AddBintools(btools)
+        self.mkimage = self.AddBintool(btools, 'mkimage')
 
-        Args:
-            allow_missing: True if allowed, False if not allowed
-        """
-        self.allow_missing = allow_missing
-        for entry in self._mkimage_entries.values():
-            entry.SetAllowMissing(allow_missing)
-        if self._imagename:
-            self._imagename.SetAllowMissing(allow_missing)
+    def CheckEntries(self):
+        pass
 
-    def SetAllowFakeBlob(self, allow_fake):
-        """Set whether the sub nodes allows to create a fake blob
+    def ProcessContents(self):
+        # The blob may have changed due to WriteSymbols()
+        ok = super().ProcessContents()
+        data = self.BuildSectionData(True)
+        ok2 = self.ProcessContentsUpdate(data)
+        return ok and ok2
 
-        Args:
-            allow_fake: True if allowed, False if not allowed
-        """
-        for entry in self._mkimage_entries.values():
-            entry.SetAllowFakeBlob(allow_fake)
-        if self._imagename:
-            self._imagename.SetAllowFakeBlob(allow_fake)
+    def SetImagePos(self, image_pos):
+        """Set the position in the image
 
-    def CheckMissing(self, missing_list):
-        """Check if any entries in this section have missing external blobs
+        This sets each subentry's offsets, sizes and positions-in-image
+        according to where they ended up in the packed mkimage file.
 
-        If there are missing (non-optional) blobs, the entries are added to the
-        list
+        NOTE: This assumes a legacy mkimage and assumes that the images are
+        written to the output in order. SoC-specific mkimage handling may not
+        conform to this, in which case these values may be wrong.
 
         Args:
-            missing_list: List of Entry objects to be added to
+            image_pos (int): Position of this entry in the image
         """
-        for entry in self._mkimage_entries.values():
-            entry.CheckMissing(missing_list)
-        if self._imagename:
-            self._imagename.CheckMissing(missing_list)
+        # The mkimage header consists of 0x40 bytes, following by a table of
+        # offsets for each file
+        upto = 0x40
 
-    def CheckFakedBlobs(self, faked_blobs_list):
-        """Check if any entries in this section have faked external blobs
+        # Skip the 0-terminated list of offsets (assume a single image)
+        upto += 4 + 4
+        for entry in self.GetEntries().values():
+            entry.SetOffsetSize(upto, None)
 
-        If there are faked blobs, the entries are added to the list
+            # Give up if any entries lack a size
+            if entry.size is None:
+                return
+            upto += entry.size
 
-        Args:
-            faked_blobs_list: List of Entry objects to be added to
-        """
-        for entry in self._mkimage_entries.values():
-            entry.CheckFakedBlobs(faked_blobs_list)
-        if self._imagename:
-            self._imagename.CheckFakedBlobs(faked_blobs_list)
-
-    def AddBintools(self, btools):
-        super().AddBintools(btools)
-        self.mkimage = self.AddBintool(btools, 'mkimage')
+        super().SetImagePos(image_pos)
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index c36edd1..7c4d312 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -168,6 +168,7 @@
         self._end_4gb = False
         self._ignore_missing = False
         self._filename = None
+        self.align_default = 0
 
     def IsSpecialSubnode(self, node):
         """Check if a node is a special one used by the section itself
@@ -178,7 +179,8 @@
         Returns:
             bool: True if the node is a special one, else False
         """
-        return node.name.startswith('hash') or node.name.startswith('signature')
+        start_list = ('hash', 'signature', 'template')
+        return any(node.name.startswith(name) for name in start_list)
 
     def ReadNode(self):
         """Read properties from the section node"""
@@ -315,12 +317,15 @@
         This should be overridden by subclasses which want to build their own
         data structure for the section.
 
+        Missing entries will have be given empty (or fake) data, so are
+        processed normally here.
+
         Args:
             required: True if the data must be present, False if it is OK to
                 return None
 
         Returns:
-            Contents of the section (bytes)
+            Contents of the section (bytes), None if not available
         """
         section_data = bytearray()
 
@@ -710,6 +715,33 @@
     def GetEntryContents(self, skip_entry=None):
         """Call ObtainContents() for each entry in the section
 
+        The overall goal of this function is to read in any available data in
+        this entry and any subentries. This includes reading in blobs, setting
+        up objects which have predefined contents, etc.
+
+        Since entry types which contain entries call ObtainContents() on all
+        those entries too, the result is that ObtainContents() is called
+        recursively for the whole tree below this one.
+
+        Entries with subentries are generally not *themselves& processed here,
+        i.e. their ObtainContents() implementation simply obtains contents of
+        their subentries, skipping their own contents. For example, the
+        implementation here (for entry_Section) does not attempt to pack the
+        entries into a final result. That is handled later.
+
+        Generally, calling this results in SetContents() being called for each
+        entry, so that the 'data' and 'contents_size; properties are set, and
+        subsequent calls to GetData() will return value data.
+
+        Where 'allow_missing' is set, this can result in the 'missing' property
+        being set to True if there is no data. This is handled by setting the
+        data to b''. This function will still return success. Future calls to
+        GetData() for this entry will return b'', or in the case where the data
+        is faked, GetData() will return that fake data.
+
+        Args:
+            skip_entry: (single) Entry to skip, or None to process all entries
+
         Note that this may set entry.absent to True if the entry is not
         actually needed
         """
@@ -719,7 +751,7 @@
                     next_todo.append(entry)
             return entry
 
-        todo = self._entries.values()
+        todo = self.GetEntries().values()
         for passnum in range(3):
             threads = state.GetThreads()
             next_todo = []
@@ -892,7 +924,7 @@
             allow_missing: True if allowed, False if not allowed
         """
         self.allow_missing = allow_missing
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.SetAllowMissing(allow_missing)
 
     def SetAllowFakeBlob(self, allow_fake):
@@ -902,7 +934,7 @@
             allow_fake: True if allowed, False if not allowed
         """
         super().SetAllowFakeBlob(allow_fake)
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.SetAllowFakeBlob(allow_fake)
 
     def CheckMissing(self, missing_list):
@@ -914,7 +946,7 @@
         Args:
             missing_list: List of Entry objects to be added to
         """
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.CheckMissing(missing_list)
 
     def CheckFakedBlobs(self, faked_blobs_list):
@@ -925,7 +957,7 @@
         Args:
             faked_blobs_list: List of Entry objects to be added to
         """
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.CheckFakedBlobs(faked_blobs_list)
 
     def CheckOptional(self, optional_list):
@@ -936,7 +968,7 @@
         Args:
             optional_list (list): List of Entry objects to be added to
         """
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.CheckOptional(optional_list)
 
     def check_missing_bintools(self, missing_list):
@@ -948,7 +980,7 @@
             missing_list: List of Bintool objects to be added to
         """
         super().check_missing_bintools(missing_list)
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.check_missing_bintools(missing_list)
 
     def _CollectEntries(self, entries, entries_by_name, add_entry):
@@ -998,12 +1030,12 @@
             entry.Raise(f'Missing required properties/entry args: {missing}')
 
     def CheckAltFormats(self, alt_formats):
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.CheckAltFormats(alt_formats)
 
     def AddBintools(self, btools):
         super().AddBintools(btools)
-        for entry in self._entries.values():
+        for entry in self.GetEntries().values():
             entry.AddBintools(btools)
 
     def read_elf_segments(self):
diff --git a/tools/binman/etype/u_boot_spl_bss_pad.py b/tools/binman/etype/u_boot_spl_bss_pad.py
index 1ffeb39..4af4045 100644
--- a/tools/binman/etype/u_boot_spl_bss_pad.py
+++ b/tools/binman/etype/u_boot_spl_bss_pad.py
@@ -38,7 +38,7 @@
     def ObtainContents(self):
         fname = tools.get_input_filename('spl/u-boot-spl')
         bss_size = elf.GetSymbolAddress(fname, '__bss_size')
-        if not bss_size:
+        if bss_size is None:
             self.Raise('Expected __bss_size symbol in spl/u-boot-spl')
         self.SetContents(tools.get_bytes(0, bss_size))
         return True
diff --git a/tools/binman/etype/u_boot_tpl_bss_pad.py b/tools/binman/etype/u_boot_tpl_bss_pad.py
index 29c6a95..46d2cd5 100644
--- a/tools/binman/etype/u_boot_tpl_bss_pad.py
+++ b/tools/binman/etype/u_boot_tpl_bss_pad.py
@@ -38,7 +38,7 @@
     def ObtainContents(self):
         fname = tools.get_input_filename('tpl/u-boot-tpl')
         bss_size = elf.GetSymbolAddress(fname, '__bss_size')
-        if not bss_size:
+        if bss_size is None:
             self.Raise('Expected __bss_size symbol in tpl/u-boot-tpl')
         self.SetContents(tools.get_bytes(0, bss_size))
         return True
diff --git a/tools/binman/etype/u_boot_vpl_bss_pad.py b/tools/binman/etype/u_boot_vpl_bss_pad.py
index bba38cc..12b286a 100644
--- a/tools/binman/etype/u_boot_vpl_bss_pad.py
+++ b/tools/binman/etype/u_boot_vpl_bss_pad.py
@@ -38,7 +38,7 @@
     def ObtainContents(self):
         fname = tools.get_input_filename('vpl/u-boot-vpl')
         bss_size = elf.GetSymbolAddress(fname, '__bss_size')
-        if not bss_size:
+        if bss_size is None:
             self.Raise('Expected __bss_size symbol in vpl/u-boot-vpl')
         self.SetContents(tools.get_bytes(0, bss_size))
         return True
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 43b4f85..e53181a 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -1103,6 +1103,7 @@
 
     def testPackZeroOffset(self):
         """Test that an entry at offset 0 is not given a new offset"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as e:
             self._DoTestFile('025_pack_zero_size.dts')
         self.assertIn("Node '/binman/u-boot-spl': Offset 0x0 (0) overlaps "
@@ -1116,6 +1117,7 @@
 
     def testPackX86RomNoSize(self):
         """Test that the end-at-4gb property requires a size property"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as e:
             self._DoTestFile('027_pack_4gb_no_size.dts')
         self.assertIn("Image '/binman': Section size must be provided when "
@@ -1124,6 +1126,7 @@
     def test4gbAndSkipAtStartTogether(self):
         """Test that the end-at-4gb and skip-at-size property can't be used
         together"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as e:
             self._DoTestFile('098_4gb_and_skip_at_start_together.dts')
         self.assertIn("Image '/binman': Provide either 'end-at-4gb' or "
@@ -1131,6 +1134,7 @@
 
     def testPackX86RomOutside(self):
         """Test that the end-at-4gb property checks for offset boundaries"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as e:
             self._DoTestFile('028_pack_4gb_outside.dts')
         self.assertIn("Node '/binman/u-boot': Offset 0x0 (0) size 0x4 (4) "
@@ -1423,6 +1427,7 @@
 
     def testPackUbootSplMicrocode(self):
         """Test that x86 microcode can be handled correctly in SPL"""
+        self._SetupSplElf()
         self._PackUbootSplMicrocode('049_x86_ucode_spl.dts')
 
     def testPackUbootSplMicrocodeReorder(self):
@@ -1442,6 +1447,7 @@
 
     def testSplDtb(self):
         """Test that an image with spl/u-boot-spl.dtb can be created"""
+        self._SetupSplElf()
         data = self._DoReadFile('051_u_boot_spl_dtb.dts')
         self.assertEqual(U_BOOT_SPL_DTB_DATA, data[:len(U_BOOT_SPL_DTB_DATA)])
 
@@ -1452,7 +1458,7 @@
         self.assertEqual(U_BOOT_SPL_NODTB_DATA, data[:len(U_BOOT_SPL_NODTB_DATA)])
 
     def checkSymbols(self, dts, base_data, u_boot_offset, entry_args=None,
-                     use_expanded=False):
+                     use_expanded=False, no_write_symbols=False):
         """Check the image contains the expected symbol values
 
         Args:
@@ -1481,9 +1487,14 @@
         sym_values = struct.pack('<LLQLL', elf.BINMAN_SYM_MAGIC_VALUE,
                                  0x00, u_boot_offset + len(U_BOOT_DATA),
                                  0x10 + u_boot_offset, 0x04)
-        expected = (sym_values + base_data[24:] +
-                    tools.get_bytes(0xff, 1) + U_BOOT_DATA + sym_values +
-                    base_data[24:])
+        if no_write_symbols:
+            expected = (base_data +
+                        tools.get_bytes(0xff, 0x38 - len(base_data)) +
+                        U_BOOT_DATA + base_data)
+        else:
+            expected = (sym_values + base_data[24:] +
+                        tools.get_bytes(0xff, 1) + U_BOOT_DATA + sym_values +
+                        base_data[24:])
         self.assertEqual(expected, data)
 
     def testSymbols(self):
@@ -1957,6 +1968,8 @@
 
     def testUpdateFdtAll(self):
         """Test that all device trees are updated with offset/size info"""
+        self._SetupSplElf()
+        self._SetupTplElf()
         data = self._DoReadFileRealDtb('082_fdt_update_all.dts')
 
         base_expected = {
@@ -3279,6 +3292,8 @@
 
     def testUpdateFdtAllRepack(self):
         """Test that all device trees are updated with offset/size info"""
+        self._SetupSplElf()
+        self._SetupTplElf()
         data = self._DoReadFileRealDtb('134_fdt_update_all_repack.dts')
         SECTION_SIZE = 0x300
         DTB_SIZE = 602
@@ -3732,6 +3747,7 @@
 
     def testMkimage(self):
         """Test using mkimage to build an image"""
+        self._SetupSplElf()
         data = self._DoReadFile('156_mkimage.dts')
 
         # Just check that the data appears in the file somewhere
@@ -3739,6 +3755,7 @@
 
     def testMkimageMissing(self):
         """Test that binman still produces an image if mkimage is missing"""
+        self._SetupSplElf()
         with test_util.capture_sys_output() as (_, stderr):
             self._DoTestFile('156_mkimage.dts',
                              force_missing_bintools='mkimage')
@@ -3851,6 +3868,7 @@
 
     def testSimpleFit(self):
         """Test an image with a FIT inside"""
+        self._SetupSplElf()
         data = self._DoReadFile('161_fit.dts')
         self.assertEqual(U_BOOT_DATA, data[:len(U_BOOT_DATA)])
         self.assertEqual(U_BOOT_NODTB_DATA, data[-len(U_BOOT_NODTB_DATA):])
@@ -5370,6 +5388,7 @@
 
     def testFitSubentryHashSubnode(self):
         """Test an image with a FIT inside"""
+        self._SetupSplElf()
         data, _, _, out_dtb_name = self._DoReadFileDtb(
             '221_fit_subentry_hash.dts', use_real_dtb=True, update_dtb=True)
 
@@ -5888,6 +5907,7 @@
 
     def testMkimageImagename(self):
         """Test using mkimage with -n holding the data too"""
+        self._SetupSplElf()
         data = self._DoReadFile('242_mkimage_name.dts')
 
         # Check that the data appears in the file somewhere
@@ -5905,6 +5925,7 @@
 
     def testMkimageImage(self):
         """Test using mkimage with -n holding the data too"""
+        self._SetupSplElf()
         data = self._DoReadFile('243_mkimage_image.dts')
 
         # Check that the data appears in the file somewhere
@@ -5925,6 +5946,7 @@
 
     def testMkimageImageNoContent(self):
         """Test using mkimage with -n and no data"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as exc:
             self._DoReadFile('244_mkimage_image_no_content.dts')
         self.assertIn('Could not complete processing of contents',
@@ -5932,6 +5954,7 @@
 
     def testMkimageImageBad(self):
         """Test using mkimage with imagename node and data-to-imagename"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as exc:
             self._DoReadFile('245_mkimage_image_bad.dts')
         self.assertIn('Cannot use both imagename node and data-to-imagename',
@@ -5947,6 +5970,7 @@
 
     def testMkimageCollection(self):
         """Test using a collection referring to an entry in a mkimage entry"""
+        self._SetupSplElf()
         data = self._DoReadFile('247_mkimage_coll.dts')
         expect = U_BOOT_SPL_DATA + U_BOOT_DATA
         self.assertEqual(expect, data[:len(expect)])
@@ -6032,6 +6056,8 @@
 
     def testMkimageMultipleDataFiles(self):
         """Test passing multiple files to mkimage in a mkimage entry"""
+        self._SetupSplElf()
+        self._SetupTplElf()
         data = self._DoReadFile('252_mkimage_mult_data.dts')
         # Size of files are packed in their 4B big-endian format
         expect = struct.pack('>I', len(U_BOOT_TPL_DATA))
@@ -6046,8 +6072,42 @@
         expect += U_BOOT_SPL_DATA
         self.assertEqual(expect, data[-len(expect):])
 
+    def testMkimageMultipleExpanded(self):
+        """Test passing multiple files to mkimage in a mkimage entry"""
+        self._SetupSplElf()
+        self._SetupTplElf()
+        entry_args = {
+            'spl-bss-pad': 'y',
+            'spl-dtb': 'y',
+        }
+        data = self._DoReadFileDtb('252_mkimage_mult_data.dts',
+                                   use_expanded=True, entry_args=entry_args)[0]
+        pad_len = 10
+        tpl_expect = U_BOOT_TPL_DATA
+        spl_expect = U_BOOT_SPL_NODTB_DATA + tools.get_bytes(0, pad_len)
+        spl_expect += U_BOOT_SPL_DTB_DATA
+
+        content = data[0x40:]
+        lens = struct.unpack('>III', content[:12])
+
+        # Size of files are packed in their 4B big-endian format
+        # Size info is always followed by a 4B zero value.
+        self.assertEqual(len(tpl_expect), lens[0])
+        self.assertEqual(len(spl_expect), lens[1])
+        self.assertEqual(0, lens[2])
+
+        rest = content[12:]
+        self.assertEqual(tpl_expect, rest[:len(tpl_expect)])
+
+        rest = rest[len(tpl_expect):]
+        align_pad = len(tpl_expect) % 4
+        self.assertEqual(tools.get_bytes(0, align_pad), rest[:align_pad])
+        rest = rest[align_pad:]
+        self.assertEqual(spl_expect, rest)
+
     def testMkimageMultipleNoContent(self):
         """Test passing multiple data files to mkimage with one data file having no content"""
+        self._SetupSplElf()
         with self.assertRaises(ValueError) as exc:
             self._DoReadFile('253_mkimage_mult_no_content.dts')
         self.assertIn('Could not complete processing of contents',
@@ -6055,6 +6115,7 @@
 
     def testMkimageFilename(self):
         """Test using mkimage to build a binary with a filename"""
+        self._SetupSplElf()
         retcode = self._DoTestFile('254_mkimage_filename.dts')
         self.assertEqual(0, retcode)
         fname = tools.get_output_filename('mkimage-test.bin')
@@ -6529,6 +6590,7 @@
 
     def testReplaceFitSibling(self):
         """Test an image with a FIT inside where we replace its sibling"""
+        self._SetupSplElf()
         fname = TestFunctional._MakeInputFile('once', b'available once')
         self._DoReadFileRealDtb('277_replace_fit_sibling.dts')
         os.remove(fname)
@@ -6603,7 +6665,7 @@
                 Private key
                 DTB
         """
-
+        self._SetupSplElf()
         data = self._DoReadFileRealDtb(dts)
         updated_fname = tools.get_output_filename('image-updated.bin')
         tools.write_file(updated_fname, data)
@@ -6676,6 +6738,152 @@
                                 ['fit'])
         self.assertIn("Node '/fit': Missing tool: 'mkimage'", str(e.exception))
 
+    def testSymbolNoWrite(self):
+        """Test disabling of symbol writing"""
+        self._SetupSplElf()
+        self.checkSymbols('282_symbols_disable.dts', U_BOOT_SPL_DATA, 0x1c,
+                          no_write_symbols=True)
+
+    def testSymbolNoWriteExpanded(self):
+        """Test disabling of symbol writing in expanded entries"""
+        entry_args = {
+            'spl-dtb': '1',
+        }
+        self.checkSymbols('282_symbols_disable.dts', U_BOOT_SPL_NODTB_DATA +
+                          U_BOOT_SPL_DTB_DATA, 0x38,
+                          entry_args=entry_args, use_expanded=True,
+                          no_write_symbols=True)
+
+    def testMkimageSpecial(self):
+        """Test mkimage ignores special hash-1 node"""
+        data = self._DoReadFile('283_mkimage_special.dts')
+
+        # Just check that the data appears in the file somewhere
+        self.assertIn(U_BOOT_DATA, data)
+
+    def testFitFdtList(self):
+        """Test an image with an FIT with the fit,fdt-list-val option"""
+        entry_args = {
+            'default-dt': 'test-fdt2',
+        }
+        data = self._DoReadFileDtb(
+            '284_fit_fdt_list.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)]
+
+    def testSplEmptyBss(self):
+        """Test an expanded SPL with a zero-size BSS"""
+        # ELF file with a '__bss_size' symbol
+        self._SetupSplElf(src_fname='bss_data_zero')
+
+        entry_args = {
+            'spl-bss-pad': 'y',
+            'spl-dtb': 'y',
+        }
+        data = self._DoReadFileDtb('285_spl_expand.dts',
+                                   use_expanded=True, entry_args=entry_args)[0]
+
+    def testTemplate(self):
+        """Test using a template"""
+        TestFunctional._MakeInputFile('vga2.bin', b'#' + VGA_DATA)
+        data = self._DoReadFile('286_template.dts')
+        first = U_BOOT_DATA + VGA_DATA + U_BOOT_DTB_DATA
+        second = U_BOOT_DATA + b'#' + VGA_DATA + U_BOOT_DTB_DATA
+        self.assertEqual(U_BOOT_IMG_DATA + first + second, data)
+
+    def testTemplateBlobMulti(self):
+        """Test using a template with 'multiple-images' enabled"""
+        TestFunctional._MakeInputFile('my-blob.bin', b'blob')
+        TestFunctional._MakeInputFile('my-blob2.bin', b'other')
+        retcode = self._DoTestFile('287_template_multi.dts')
+
+        self.assertEqual(0, retcode)
+        image = control.images['image']
+        image_fname = tools.get_output_filename('my-image.bin')
+        data = tools.read_file(image_fname)
+        self.assertEqual(b'blob@@@@other', data)
+
+    def testTemplateFit(self):
+        """Test using a template in a FIT"""
+        fit_data = self._DoReadFile('288_template_fit.dts')
+        fname = os.path.join(self._indir, 'fit_data.fit')
+        tools.write_file(fname, fit_data)
+        out = tools.run('dumpimage', '-l', fname)
+
+    def testTemplateSection(self):
+        """Test using a template in a section (not at top level)"""
+        TestFunctional._MakeInputFile('vga2.bin', b'#' + VGA_DATA)
+        data = self._DoReadFile('289_template_section.dts')
+        first = U_BOOT_DATA + VGA_DATA + U_BOOT_DTB_DATA
+        second = U_BOOT_DATA + b'#' + VGA_DATA + U_BOOT_DTB_DATA
+        self.assertEqual(U_BOOT_IMG_DATA + first + second + first, data)
+
+    def testMkimageSymbols(self):
+        """Test using mkimage to build an image with symbols in it"""
+        self._SetupSplElf('u_boot_binman_syms')
+        data = self._DoReadFile('290_mkimage_sym.dts')
+
+        image = control.images['image']
+        entries = image.GetEntries()
+        self.assertIn('u-boot', entries)
+        u_boot = entries['u-boot']
+
+        mkim = entries['mkimage']
+        mkim_entries = mkim.GetEntries()
+        self.assertIn('u-boot-spl', mkim_entries)
+        spl = mkim_entries['u-boot-spl']
+        self.assertIn('u-boot-spl2', mkim_entries)
+        spl2 = mkim_entries['u-boot-spl2']
+
+        # skip the mkimage header and the area sizes
+        mk_data = data[mkim.offset + 0x40:]
+        size, term = struct.unpack('>LL', mk_data[:8])
+
+        # There should be only one image, so check that the zero terminator is
+        # present
+        self.assertEqual(0, term)
+
+        content = mk_data[8:8 + size]
+
+        # The image should contain the symbols from u_boot_binman_syms.c
+        # Note that image_pos is adjusted by the base address of the image,
+        # which is 0x10 in our test image
+        spl_data = content[:0x18]
+        content = content[0x1b:]
+
+        # After the header is a table of offsets for each image. There should
+        # only be one image, then a 0 terminator, so figure out the real start
+        # of the image data
+        base = 0x40 + 8
+
+        # Check symbols in both u-boot-spl and u-boot-spl2
+        for i in range(2):
+            vals = struct.unpack('<LLQLL', spl_data)
+
+            # The image should contain the symbols from u_boot_binman_syms.c
+            # Note that image_pos is adjusted by the base address of the image,
+            # which is 0x10 in our 'u_boot_binman_syms' test image
+            self.assertEqual(elf.BINMAN_SYM_MAGIC_VALUE, vals[0])
+            self.assertEqual(base, vals[1])
+            self.assertEqual(spl2.offset, vals[2])
+            # figure out the internal positions of its components
+            self.assertEqual(0x10 + u_boot.image_pos, vals[3])
+
+            # Check that spl and spl2 are actually at the indicated positions
+            self.assertEqual(
+                elf.BINMAN_SYM_MAGIC_VALUE,
+                struct.unpack('<I', data[spl.image_pos:spl.image_pos + 4])[0])
+            self.assertEqual(
+                elf.BINMAN_SYM_MAGIC_VALUE,
+                struct.unpack('<I', data[spl2.image_pos:spl2.image_pos + 4])[0])
+
+            self.assertEqual(len(U_BOOT_DATA), vals[4])
+
+            # Move to next
+            spl_data = content[:0x18]
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/state.py b/tools/binman/state.py
index 3e78cf3..45bae40 100644
--- a/tools/binman/state.py
+++ b/tools/binman/state.py
@@ -385,8 +385,8 @@
         for_repack: True is this property is only needed for repacking
     """
     for n in GetUpdateNodes(node, for_repack):
-        tout.detail("File %s: Update node '%s' prop '%s' to %#x" %
-                    (n.GetFdt().name, n.path, prop, value))
+        tout.debug("File %s: Update node '%s' prop '%s' to %#x" %
+                   (n.GetFdt().name, n.path, prop, value))
         n.SetInt(prop, value)
 
 def CheckAddHashProp(node):
diff --git a/tools/binman/test/282_symbols_disable.dts b/tools/binman/test/282_symbols_disable.dts
new file mode 100644
index 0000000..6efa933
--- /dev/null
+++ b/tools/binman/test/282_symbols_disable.dts
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		pad-byte = <0xff>;
+		u-boot-spl {
+			no-write-symbols;
+		};
+
+		u-boot {
+			offset = <0x38>;
+			no-expanded;
+		};
+
+		u-boot-spl2 {
+			type = "u-boot-spl";
+			no-write-symbols;
+		};
+	};
+};
diff --git a/tools/binman/test/283_mkimage_special.dts b/tools/binman/test/283_mkimage_special.dts
new file mode 100644
index 0000000..c234093
--- /dev/null
+++ b/tools/binman/test/283_mkimage_special.dts
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		mkimage {
+			args = "-T script";
+
+			u-boot {
+			};
+
+			hash {
+			};
+
+			imagename {
+				type = "u-boot";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/284_fit_fdt_list.dts b/tools/binman/test/284_fit_fdt_list.dts
new file mode 100644
index 0000000..8885313
--- /dev/null
+++ b/tools/binman/test/284_fit_fdt_list.dts
@@ -0,0 +1,58 @@
+// 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-val = "test-fdt1", "test-fdt2";
+
+			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";
+					hash {
+						algo = "sha256";
+					};
+				};
+			};
+
+			configurations {
+				default = "@config-DEFAULT-SEQ";
+				@config-SEQ {
+					description = "conf-NAME.dtb";
+					firmware = "uboot";
+					loadables = "atf";
+					fdt = "fdt-SEQ";
+				};
+			};
+		};
+		u-boot-nodtb {
+		};
+	};
+};
diff --git a/tools/binman/test/285_spl_expand.dts b/tools/binman/test/285_spl_expand.dts
new file mode 100644
index 0000000..9c88ccb
--- /dev/null
+++ b/tools/binman/test/285_spl_expand.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot-spl {
+		};
+	};
+};
diff --git a/tools/binman/test/286_template.dts b/tools/binman/test/286_template.dts
new file mode 100644
index 0000000..6980dbf
--- /dev/null
+++ b/tools/binman/test/286_template.dts
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot-img {
+		};
+
+		common_part: template {
+			u-boot {
+			};
+
+			intel-vga {
+				filename = "vga.bin";
+			};
+		};
+
+		first {
+			type = "section";
+			insert-template = <&common_part>;
+
+			u-boot-dtb {
+			};
+		};
+
+		second {
+			type = "section";
+			insert-template = <&common_part>;
+
+			u-boot-dtb {
+			};
+
+			intel-vga {
+				filename = "vga2.bin";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/287_template_multi.dts b/tools/binman/test/287_template_multi.dts
new file mode 100644
index 0000000..122bfcc
--- /dev/null
+++ b/tools/binman/test/287_template_multi.dts
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+/ {
+	binman: binman {
+		multiple-images;
+
+		my_template: template {
+			blob-ext@0 {
+				filename = "my-blob.bin";
+				offset = <0>;
+			};
+			blob-ext@8 {
+				offset = <8>;
+			};
+		};
+
+		image {
+			pad-byte = <0x40>;
+			filename = "my-image.bin";
+			insert-template = <&my_template>;
+			blob-ext@8 {
+				filename = "my-blob2.bin";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/288_template_fit.dts b/tools/binman/test/288_template_fit.dts
new file mode 100644
index 0000000..d84dca4
--- /dev/null
+++ b/tools/binman/test/288_template_fit.dts
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	binman: binman {
+		multiple-images;
+
+		my_template: template {
+			fit@0 {
+				images {
+					kernel-1 {
+					};
+					kernel-2 {
+					};
+				};
+			};
+		};
+
+		image {
+			filename = "image.bin";
+			insert-template = <&my_template>;
+
+			fit@0 {
+				description = "desc";
+				configurations {
+				};
+				images {
+					kernel-3 {
+					};
+					kernel-4 {
+					};
+				};
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/289_template_section.dts b/tools/binman/test/289_template_section.dts
new file mode 100644
index 0000000..8a744a0
--- /dev/null
+++ b/tools/binman/test/289_template_section.dts
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot-img {
+		};
+
+		common_part: template {
+			u-boot {
+			};
+
+			intel-vga {
+				filename = "vga.bin";
+			};
+		};
+
+		first {
+			type = "section";
+			insert-template = <&common_part>;
+
+			u-boot-dtb {
+			};
+		};
+
+		section {
+			second {
+				type = "section";
+				insert-template = <&common_part>;
+
+				u-boot-dtb {
+				};
+
+				intel-vga {
+					filename = "vga2.bin";
+				};
+			};
+		};
+
+		second {
+			type = "section";
+			insert-template = <&common_part>;
+
+			u-boot-dtb {
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/290_mkimage_sym.dts b/tools/binman/test/290_mkimage_sym.dts
new file mode 100644
index 0000000..2dfd286
--- /dev/null
+++ b/tools/binman/test/290_mkimage_sym.dts
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		u-boot-dtb {
+		};
+
+		mkimage {
+			args = "-n test -T script";
+
+			u-boot-spl {
+			};
+
+			u-boot-spl2 {
+				type = "u-boot-spl";
+			};
+		};
+
+		u-boot {
+		};
+	};
+};
diff --git a/tools/binman/test/Makefile b/tools/binman/test/Makefile
index cd66a30..4d152ee 100644
--- a/tools/binman/test/Makefile
+++ b/tools/binman/test/Makefile
@@ -32,7 +32,7 @@
 LDS_EFL_SECTIONS := -T $(SRC)elf_sections.lds
 LDS_BLOB := -T $(SRC)blob_syms.lds
 
-TARGETS = u_boot_ucode_ptr u_boot_no_ucode_ptr bss_data \
+TARGETS = u_boot_ucode_ptr u_boot_no_ucode_ptr bss_data bss_data_zero \
 	u_boot_binman_syms u_boot_binman_syms.bin u_boot_binman_syms_bad \
 	u_boot_binman_syms_size u_boot_binman_syms_x86 embed_data \
 	u_boot_binman_embed u_boot_binman_embed_sm elf_sections blob_syms.bin
@@ -48,6 +48,9 @@
 bss_data: CFLAGS += $(SRC)bss_data.lds
 bss_data: bss_data.c
 
+bss_data_zero: CFLAGS += $(SRC)bss_data_zero.lds
+bss_data_zero: bss_data_zero.c
+
 embed_data: CFLAGS += $(SRC)embed_data.lds
 embed_data: embed_data.c
 
diff --git a/tools/binman/test/bss_data.c b/tools/binman/test/bss_data.c
index 4f9b64c..7047a3b 100644
--- a/tools/binman/test/bss_data.c
+++ b/tools/binman/test/bss_data.c
@@ -7,9 +7,8 @@
  */
 
 int bss_data[10];
-int __bss_size = sizeof(bss_data);
 
-int main()
+int main(void)
 {
 	bss_data[2] = 2;
 
diff --git a/tools/binman/test/bss_data_zero.c b/tools/binman/test/bss_data_zero.c
new file mode 100644
index 0000000..7047a3b
--- /dev/null
+++ b/tools/binman/test/bss_data_zero.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2016 Google, Inc
+ *
+ * Simple program to create a bss_data region so the symbol can be read
+ * by binutils. This is used by binman tests.
+ */
+
+int bss_data[10];
+
+int main(void)
+{
+	bss_data[2] = 2;
+
+	return 0;
+}
diff --git a/tools/binman/test/bss_data_zero.lds b/tools/binman/test/bss_data_zero.lds
new file mode 100644
index 0000000..8fa0210
--- /dev/null
+++ b/tools/binman/test/bss_data_zero.lds
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2016 Google, Inc
+ */
+
+OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
+OUTPUT_ARCH(i386)
+ENTRY(_start)
+
+SECTIONS
+{
+	. = 0xfffffdf0;
+	_start = .;
+	__bss_size = 0;
+}
diff --git a/tools/binman/test/embed_data.lds b/tools/binman/test/embed_data.lds
index 908bf66..d416cb2 100644
--- a/tools/binman/test/embed_data.lds
+++ b/tools/binman/test/embed_data.lds
@@ -17,6 +17,7 @@
 		embed_start = .;
 		*(.embed*)
 		embed_end = .;
+		region_size = 0;
 		. = ALIGN(32);
 		*(.data*)
 	}
diff --git a/tools/dtoc/fdt.py b/tools/dtoc/fdt.py
index a8e0534..fd0f3e9 100644
--- a/tools/dtoc/fdt.py
+++ b/tools/dtoc/fdt.py
@@ -13,6 +13,7 @@
 import libfdt
 from libfdt import QUIET_NOTFOUND
 from u_boot_pylib import tools
+from u_boot_pylib import tout
 
 # This deals with a device tree, presenting it as an assortment of Node and
 # Prop objects, representing nodes and properties, respectively. This file
@@ -264,6 +265,13 @@
                 fdt_obj.setprop(node.Offset(), self.name, self.bytes)
             self.dirty = False
 
+    def purge(self):
+        """Set a property offset to None
+
+        The property remains in the tree structure and will be recreated when
+        the FDT is synced
+        """
+        self._offset = None
 
 class Node:
     """A device tree node
@@ -534,8 +542,8 @@
         """
         return self.AddData(prop_name, struct.pack('>I', val))
 
-    def AddSubnode(self, name):
-        """Add a new subnode to the node
+    def Subnode(self, name):
+        """Create new subnode for the node
 
         Args:
             name: name of node to add
@@ -544,10 +552,72 @@
             New subnode that was created
         """
         path = self.path + '/' + name
-        subnode = Node(self._fdt, self, None, name, path)
+        return Node(self._fdt, self, None, name, path)
+
+    def AddSubnode(self, name):
+        """Add a new subnode to the node, after all other subnodes
+
+        Args:
+            name: name of node to add
+
+        Returns:
+            New subnode that was created
+        """
+        subnode = self.Subnode(name)
         self.subnodes.append(subnode)
         return subnode
 
+    def insert_subnode(self, name):
+        """Add a new subnode to the node, before all other subnodes
+
+        This deletes other subnodes and sets their offset to None, so that they
+        will be recreated after this one.
+
+        Args:
+            name: name of node to add
+
+        Returns:
+            New subnode that was created
+        """
+        # Deleting a node invalidates the offsets of all following nodes, so
+        # process in reverse order so that the offset of each node remains valid
+        # until deletion.
+        for subnode in reversed(self.subnodes):
+            subnode.purge(True)
+        subnode = self.Subnode(name)
+        self.subnodes.insert(0, subnode)
+        return subnode
+
+    def purge(self, delete_it=False):
+        """Purge this node, setting offset to None and deleting from FDT"""
+        if self._offset is not None:
+            if delete_it:
+                CheckErr(self._fdt._fdt_obj.del_node(self.Offset()),
+                     "Node '%s': delete" % self.path)
+            self._offset = None
+            self._fdt.Invalidate()
+
+        for prop in self.props.values():
+            prop.purge()
+
+        for subnode in self.subnodes:
+            subnode.purge(False)
+
+    def move_to_first(self):
+        """Move the current node to first in its parent's node list"""
+        parent = self.parent
+        if parent.subnodes and parent.subnodes[0] == self:
+            return
+        for subnode in reversed(parent.subnodes):
+            subnode.purge(True)
+
+        new_subnodes = [self]
+        for subnode in parent.subnodes:
+            #subnode.purge(False)
+            if subnode != self:
+                new_subnodes.append(subnode)
+        parent.subnodes = new_subnodes
+
     def Delete(self):
         """Delete a node
 
@@ -635,6 +705,71 @@
             prop.Sync(auto_resize)
         return added
 
+    def merge_props(self, src):
+        """Copy missing properties (except 'phandle') from another node
+
+        Args:
+            src (Node): Node containing properties to copy
+
+        Adds properties which are present in src but not in this node. Any
+        'phandle' property is not copied since this might result in two nodes
+        with the same phandle, thus making phandle references ambiguous.
+        """
+        for name, src_prop in src.props.items():
+            if name != 'phandle' and name not in self.props:
+                self.props[name] = Prop(self, None, name, src_prop.bytes)
+
+    def copy_node(self, src):
+        """Copy a node and all its subnodes into this node
+
+        Args:
+            src (Node): Node to copy
+
+        Returns:
+            Node: Resulting destination node
+
+        This works recursively.
+
+        The new node is put before all other nodes. If the node already
+        exists, just its subnodes and properties are copied, placing them before
+        any existing subnodes. Properties which exist in the destination node
+        already are not copied.
+        """
+        dst = self.FindNode(src.name)
+        if dst:
+            dst.move_to_first()
+        else:
+            dst = self.insert_subnode(src.name)
+        dst.merge_props(src)
+
+        # Process in reverse order so that they appear correctly in the result,
+        # since copy_node() puts the node first in the list
+        for node in reversed(src.subnodes):
+            dst.copy_node(node)
+        return dst
+
+    def copy_subnodes_from_phandles(self, phandle_list):
+        """Copy subnodes of a list of nodes into another node
+
+        Args:
+            phandle_list (list of int): List of phandles of nodes to copy
+
+        For each node in the phandle list, its subnodes and their properties are
+        copied recursively. Note that it does not copy the node itself, nor its
+        properties.
+        """
+        # Process in reverse order, since new nodes are inserted at the start of
+        # the destination's node list. We want them to appear in order of the
+        # phandle list
+        for phandle in phandle_list.__reversed__():
+            parent = self.GetFdt().LookupPhandle(phandle)
+            tout.debug(f'adding template {parent.path} to node {self.path}')
+            for node in parent.subnodes.__reversed__():
+                dst = self.copy_node(node)
+
+            tout.debug(f'merge props from {parent.path} to {dst.path}')
+            self.merge_props(parent)
+
 
 class Fdt:
     """Provides simple access to a flat device tree blob using libfdts.
diff --git a/tools/dtoc/test/dtoc_test_copy.dts b/tools/dtoc/test/dtoc_test_copy.dts
new file mode 100644
index 0000000..36faa9b
--- /dev/null
+++ b/tools/dtoc/test/dtoc_test_copy.dts
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Test device tree file for dtoc
+ *
+ * Copyright 2017 Google, Inc
+ */
+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+	reference = <&over>;	/* nake sure that the 'over' phandle exists */
+	copy-list = <&another &base>;
+
+	dest {
+		bootph-all;
+		compatible = "sandbox,spl-test";
+		stringarray = "one";
+		longbytearray = [09 0a 0b 0c 0d 0e 0f 10];
+		maybe-empty-int = <1>;
+
+		first@0 {
+			a-prop = <456>;
+			b-prop = <1>;
+		};
+
+		existing {
+		};
+
+		base {
+			second {
+				second3 {
+				};
+
+				second2 {
+					new-prop;
+				};
+
+				second1 {
+					new-prop;
+				};
+
+				second4 {
+				};
+			};
+		};
+	};
+
+	base: base {
+		compatible = "sandbox,i2c";
+		bootph-all;
+		#address-cells = <1>;
+		#size-cells = <0>;
+		over: over {
+			compatible = "sandbox,pmic";
+			bootph-all;
+			reg = <9>;
+			low-power;
+		};
+
+		first@0 {
+			reg = <0>;
+			a-prop = <123>;
+		};
+
+		second: second {
+			second1 {
+				some-prop;
+			};
+
+			second2 {
+				some-prop;
+			};
+		};
+	};
+
+	another: another {
+		new-prop = "hello";
+		earlier {
+			wibble = <2>;
+		};
+
+		later {
+			fibble = <3>;
+		};
+	};
+};
diff --git a/tools/dtoc/test_fdt.py b/tools/dtoc/test_fdt.py
index 4fe8d12..3e54694 100755
--- a/tools/dtoc/test_fdt.py
+++ b/tools/dtoc/test_fdt.py
@@ -306,6 +306,119 @@
         self.assertIn("Internal error, node '/spl-test' name mismatch 'i2c@0'",
                       str(exc.exception))
 
+    def test_copy_node(self):
+        """Test copy_node() function"""
+        def do_copy_checks(dtb, dst, expect_none):
+            self.assertEqual(
+                ['/dest/base', '/dest/first@0', '/dest/existing'],
+                [n.path for n in dst.subnodes])
+
+            chk = dtb.GetNode('/dest/base')
+            self.assertTrue(chk)
+            self.assertEqual(
+                {'compatible', 'bootph-all', '#address-cells', '#size-cells'},
+                chk.props.keys())
+
+            # Check the first property
+            prop = chk.props['bootph-all']
+            self.assertEqual('bootph-all', prop.name)
+            self.assertEqual(True, prop.value)
+            self.assertEqual(chk.path, prop._node.path)
+
+            # Check the second property
+            prop2 = chk.props['compatible']
+            self.assertEqual('compatible', prop2.name)
+            self.assertEqual('sandbox,i2c', prop2.value)
+            self.assertEqual(chk.path, prop2._node.path)
+
+            base = chk.FindNode('base')
+            self.assertTrue(chk)
+
+            first = dtb.GetNode('/dest/base/first@0')
+            self.assertTrue(first)
+            over = dtb.GetNode('/dest/base/over')
+            self.assertTrue(over)
+
+            # Make sure that the phandle for 'over' is not copied
+            self.assertNotIn('phandle', over.props.keys())
+
+            second = dtb.GetNode('/dest/base/second')
+            self.assertTrue(second)
+            self.assertEqual([over.name, first.name, second.name],
+                             [n.name for n in chk.subnodes])
+            self.assertEqual(chk, over.parent)
+            self.assertEqual(
+                {'bootph-all', 'compatible', 'reg', 'low-power'},
+                over.props.keys())
+
+            if expect_none:
+                self.assertIsNone(prop._offset)
+                self.assertIsNone(prop2._offset)
+                self.assertIsNone(over._offset)
+            else:
+                self.assertTrue(prop._offset)
+                self.assertTrue(prop2._offset)
+                self.assertTrue(over._offset)
+
+            # Now check ordering of the subnodes
+            self.assertEqual(
+                ['second1', 'second2', 'second3', 'second4'],
+                [n.name for n in second.subnodes])
+
+        dtb = fdt.FdtScan(find_dtb_file('dtoc_test_copy.dts'))
+        tmpl = dtb.GetNode('/base')
+        dst = dtb.GetNode('/dest')
+        dst.copy_node(tmpl)
+
+        do_copy_checks(dtb, dst, expect_none=True)
+
+        dtb.Sync(auto_resize=True)
+
+        # Now check that the FDT looks correct
+        new_dtb = fdt.Fdt.FromData(dtb.GetContents())
+        new_dtb.Scan()
+        dst = new_dtb.GetNode('/dest')
+        do_copy_checks(new_dtb, dst, expect_none=False)
+
+    def test_copy_subnodes_from_phandles(self):
+        """Test copy_node() function"""
+        dtb = fdt.FdtScan(find_dtb_file('dtoc_test_copy.dts'))
+
+        orig = dtb.GetNode('/')
+        node_list = fdt_util.GetPhandleList(orig, 'copy-list')
+
+        dst = dtb.GetNode('/dest')
+        dst.copy_subnodes_from_phandles(node_list)
+
+        pmic = dtb.GetNode('/dest/over')
+        self.assertTrue(pmic)
+
+        subn = dtb.GetNode('/dest/first@0')
+        self.assertTrue(subn)
+        self.assertEqual({'a-prop', 'b-prop', 'reg'}, subn.props.keys())
+
+        self.assertEqual(
+            ['/dest/earlier', '/dest/later', '/dest/over', '/dest/first@0',
+             '/dest/second', '/dest/existing', '/dest/base'],
+            [n.path for n in dst.subnodes])
+
+        # Make sure that the phandle for 'over' is not copied
+        over = dst.FindNode('over')
+        print('keys', over.props.keys())
+        self.assertNotIn('phandle', over.props.keys())
+
+        # Check the merged properties, first the base ones in '/dest'
+        expect = {'bootph-all', 'compatible', 'stringarray', 'longbytearray',
+                  'maybe-empty-int'}
+
+        # Properties from 'base'
+        expect.update({'#address-cells', '#size-cells'})
+
+        # Properties from 'another'
+        expect.add('new-prop')
+
+        self.assertEqual(expect, set(dst.props.keys()))
+
 
 class TestProp(unittest.TestCase):
     """Test operation of the Prop class"""
diff --git a/tools/u_boot_pylib/pyproject.toml b/tools/u_boot_pylib/pyproject.toml
index 3f33caf..037c5d6 100644
--- a/tools/u_boot_pylib/pyproject.toml
+++ b/tools/u_boot_pylib/pyproject.toml
@@ -9,7 +9,7 @@
   { name="Simon Glass", email="sjg@chromium.org" },
 ]
 description = "U-Boot python library"
-readme = "README.md"
+readme = "README.rst"
 requires-python = ">=3.7"
 classifiers = [
     "Programming Language :: Python :: 3",
@@ -20,3 +20,7 @@
 [project.urls]
 "Homepage" = "https://u-boot.readthedocs.io"
 "Bug Tracker" = "https://source.denx.de/groups/u-boot/-/issues"
+
+[tool.setuptools.packages.find]
+where = [".."]
+include = ["u_boot_pylib*"]