binman: Add support for alternative FDTs

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

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

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

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

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

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

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass
2024-07-20 11:49:45 +01:00
parent daed9b42b4
commit 7081a94ea4
14 changed files with 507 additions and 2 deletions

View File

@@ -84,7 +84,7 @@ class Bintoolfdtgrep(bintool.Bintool):
elif phase == 'spl':
tag = 'bootph-pre-ram'
else:
raise(f"Invalid U-Boot phase '{phase}': Use tpl/vpl/spl")
raise ValueError(f"Invalid U-Boot phase '{phase}': Use tpl/vpl/spl")
# These args mirror those in cmd_fdtgrep in scripts/Makefile.lib
# First do the first stage

View File

@@ -734,6 +734,9 @@ def ProcessImage(image, update_fdt, write_map, get_contents=True,
image.WriteMap()
has_problems = CheckForProblems(image)
image.WriteAlternates()
return has_problems
def Binman(args):

View File

@@ -11,6 +11,48 @@ features to produce new behaviours.
.. _etype_alternates_fdt:
Entry: alternates-fdt: Entry that generates alternative sections for each devicetree provided
---------------------------------------------------------------------------------------------
When creating an image designed to boot on multiple models, each model
requires its own devicetree. This entry deals with selecting the correct
devicetree from a directory containing them. Each one is read in turn, then
used to produce section contents which are written to a file. This results
in a number of images, one for each model.
For example this produces images for each .dtb file in the 'dtb' directory::
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "tpl";
section {
u-boot-tpl {
};
};
};
Each output file is named based on its input file, so an input file of
`model1.dtb` results in an output file of `model1.bin` (i.e. the `NAME` in
the `filename-pattern` property is replaced with the .dtb basename).
Note that this entry type still produces contents for the 'main' image, in
that case using the normal dtb provided to Binman, e.g. `u-boot-tpl.dtb`.
But that image is unlikely to be useful, since it relates to whatever dtb
happened to be the default when U-Boot builds
(i.e. `CONFIG_DEFAULT_DEVICE_TREE`). However, Binman ensures that the size
of each of the alternates is the same as the 'default' one, so they can in
principle be 'slotted in' to the appropriate place in the main image.
The optional `fdt-phase` property indicates the phase to build. In this
case, it etype runs fdtgrep to obtain the devicetree subset for that phase,
respecting the `bootph-xxx` tags in the devicetree.
.. _etype_atf_bl31:
Entry: atf-bl31: ARM Trusted Firmware (ATF) BL31 blob

View File

@@ -1395,6 +1395,8 @@ features to produce new behaviours.
'u-boot-tpl-dtb'
Returns:
bytes: Contents of requested FDT
tuple:
fname (str): Filename of .dtb
bytes: Contents of FDT (possibly run through fdtgrep)
"""
return self.section.FdtContents(fdt_etype)

View File

@@ -0,0 +1,132 @@
# SPDX-License-Identifier: GPL-2.0+
# Copyright 2024 Google LLC
# Written by Simon Glass <sjg@chromium.org>
"""Entry-type module for producing multiple alternate sections"""
import glob
import os
from binman.entry import EntryArg
from binman.etype.section import Entry_section
from dtoc import fdt_util
from u_boot_pylib import tools
class Entry_alternates_fdt(Entry_section):
"""Entry that generates alternative sections for each devicetree provided
When creating an image designed to boot on multiple models, each model
requires its own devicetree. This entry deals with selecting the correct
devicetree from a directory containing them. Each one is read in turn, then
used to produce section contents which are written to a file. This results
in a number of images, one for each model.
For example this produces images for each .dtb file in the 'dtb' directory::
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "tpl";
section {
u-boot-tpl {
};
};
};
Each output file is named based on its input file, so an input file of
`model1.dtb` results in an output file of `model1.bin` (i.e. the `NAME` in
the `filename-pattern` property is replaced with the .dtb basename).
Note that this entry type still produces contents for the 'main' image, in
that case using the normal dtb provided to Binman, e.g. `u-boot-tpl.dtb`.
But that image is unlikely to be useful, since it relates to whatever dtb
happened to be the default when U-Boot builds
(i.e. `CONFIG_DEFAULT_DEVICE_TREE`). However, Binman ensures that the size
of each of the alternates is the same as the 'default' one, so they can in
principle be 'slotted in' to the appropriate place in the main image.
The optional `fdt-phase` property indicates the phase to build. In this
case, it etype runs fdtgrep to obtain the devicetree subset for that phase,
respecting the `bootph-xxx` tags in the devicetree.
"""
def __init__(self, section, etype, node):
super().__init__(section, etype, node)
self.fdt_list_dir = None
self.filename_pattern = None
self.required_props = ['fdt-list-dir']
self._cur_fdt = None
self._fdt_phase = None
self.fdtgrep = None
self._fdt_dir = None
self._fdts = None
self._fname_pattern = None
self._remove_props = None
self.alternates = None
def ReadNode(self):
"""Read properties from the node"""
super().ReadNode()
self._fdt_dir = fdt_util.GetString(self._node, 'fdt-list-dir')
fname = tools.get_input_filename(self._fdt_dir)
fdts = glob.glob('*.dtb', root_dir=fname)
self._fdts = [os.path.splitext(f)[0] for f in fdts]
self._fdt_phase = fdt_util.GetString(self._node, 'fdt-phase')
# This is used by Image.WriteAlternates()
self.alternates = self._fdts
self._fname_pattern = fdt_util.GetString(self._node, 'filename-pattern')
self._remove_props = []
props, = self.GetEntryArgsOrProps(
[EntryArg('of-spl-remove-props', str)], required=False)
if props:
self._remove_props = props.split()
def FdtContents(self, fdt_etype):
# If there is no current FDT, just use the normal one
if not self._cur_fdt:
return self.section.FdtContents(fdt_etype)
# Find the file to use
fname = os.path.join(self._fdt_dir, f'{self._cur_fdt}.dtb')
infile = tools.get_input_filename(fname)
# Run fdtgrep if needed, to remove unwanted nodes and properties
if self._fdt_phase:
uniq = self.GetUniqueName()
outfile = tools.get_output_filename(
f'{uniq}.{self._cur_fdt}-{self._fdt_phase}.dtb')
self.fdtgrep.create_for_phase(infile, self._fdt_phase, outfile,
self._remove_props)
return outfile, tools.read_file(outfile)
return fname, tools.read_file(infile)
def ProcessWithFdt(self, alt):
"""Produce the contents of this entry, using a particular FDT blob
Args:
alt (str): Name of the alternate
Returns:
tuple:
str: Filename to use for the alternate's .bin file
bytes: Contents of this entry's section, using the selected FDT
"""
pattern = self._fname_pattern or 'NAME.bin'
fname = pattern.replace('NAME', alt)
data = b''
try:
self._cur_fdt = alt
self.ProcessContents()
data = self.GetPaddedData()
finally:
self._cur_fdt = None
return fname, data
def AddBintools(self, btools):
super().AddBintools(btools)
self.fdtgrep = self.AddBintool(btools, 'fdtgrep')

View File

@@ -7,6 +7,7 @@
# python -m unittest func_test.TestFunctional.testHelp
import collections
import glob
import gzip
import hashlib
from optparse import OptionParser
@@ -7484,6 +7485,126 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
err,
"Image '.*' is missing external blobs and is non-functional: .*")
def CheckAlternates(self, dts, phase, xpl_data):
"""Run the test for the alterative-fdt etype
Args:
dts (str): Devicetree file to process
phase (str): Phase to process ('spl', 'tpl' or 'vpl')
xpl_data (bytes): Expected data for the phase's binary
Returns:
dict of .dtb files produced
key: str filename
value: Fdt object
"""
testdir = TestFunctional._MakeInputDir('dtb')
dtb_list = []
for fname in glob.glob(f'{self.TestFile("alt_dts")}/*.dts'):
tmp_fname = fdt_util.EnsureCompiled(fname, testdir)
base = os.path.splitext(os.path.basename(fname))[0]
dtb_list.append(base + '.bin')
shutil.move(tmp_fname, os.path.join(testdir, base + '.dtb'))
entry_args = {
f'{phase}-dtb': '1',
f'{phase}-bss-pad': 'y',
'of-spl-remove-props': 'prop-to-remove another-prop-to-get-rid-of',
}
data = self._DoReadFileDtb(dts, use_real_dtb=True, update_dtb=True,
use_expanded=True, entry_args=entry_args)[0]
self.assertEqual(xpl_data, data[:len(xpl_data)])
rest = data[len(xpl_data):]
pad_len = 10
self.assertEqual(tools.get_bytes(0, pad_len), rest[:pad_len])
# Check the dtb is using the test file
dtb_data = rest[pad_len:]
dtb = fdt.Fdt.FromData(dtb_data)
dtb.Scan()
fdt_size = dtb.GetFdtObj().totalsize()
self.assertEqual('model-not-set',
fdt_util.GetString(dtb.GetRoot(), 'compatible'))
pad_len = 10
# Check the other output files
dtbs = {}
for fname in dtb_list:
pathname = tools.get_output_filename(fname)
self.assertTrue(os.path.exists(pathname))
data = tools.read_file(pathname)
self.assertEqual(xpl_data, data[:len(xpl_data)])
rest = data[len(xpl_data):]
self.assertEqual(tools.get_bytes(0, pad_len), rest[:pad_len])
rest = rest[pad_len:]
dtb = fdt.Fdt.FromData(rest)
dtb.Scan()
dtbs[fname] = dtb
expected = 'one' if '1' in fname else 'two'
self.assertEqual(f'u-boot,model-{expected}',
fdt_util.GetString(dtb.GetRoot(), 'compatible'))
# Make sure the FDT is the same size as the 'main' one
rest = rest[fdt_size:]
self.assertEqual(b'', rest)
return dtbs
def testAlternatesFdt(self):
"""Test handling of alternates-fdt etype"""
self._SetupTplElf()
dtbs = self.CheckAlternates('328_alternates_fdt.dts', 'tpl',
U_BOOT_TPL_NODTB_DATA)
for dtb in dtbs.values():
# Check for the node with the tag
node = dtb.GetNode('/node')
self.assertIsNotNone(node)
self.assertEqual(5, len(node.props.keys()))
# Make sure the other node is still there
self.assertIsNotNone(dtb.GetNode('/node/other-node'))
def testAlternatesFdtgrep(self):
"""Test handling of alternates-fdt etype using fdtgrep"""
self._SetupTplElf()
dtbs = self.CheckAlternates('329_alternates_fdtgrep.dts', 'tpl',
U_BOOT_TPL_NODTB_DATA)
for dtb in dtbs.values():
# Check for the node with the tag
node = dtb.GetNode('/node')
self.assertIsNotNone(node)
self.assertEqual({'some-prop', 'not-a-prop-to-remove'},
node.props.keys())
# Make sure the other node is gone
self.assertIsNone(dtb.GetNode('/node/other-node'))
def testAlternatesFdtgrepVpl(self):
"""Test handling of alternates-fdt etype using fdtgrep with vpl"""
self._SetupVplElf()
dtbs = self.CheckAlternates('330_alternates_vpl.dts', 'vpl',
U_BOOT_VPL_NODTB_DATA)
def testAlternatesFdtgrepSpl(self):
"""Test handling of alternates-fdt etype using fdtgrep with spl"""
self._SetupSplElf()
dtbs = self.CheckAlternates('331_alternates_spl.dts', 'spl',
U_BOOT_SPL_NODTB_DATA)
def testAlternatesFdtgrepInval(self):
"""Test alternates-fdt etype using fdtgrep with invalid phase"""
self._SetupSplElf()
with self.assertRaises(ValueError) as e:
dtbs = self.CheckAlternates('332_alternates_inval.dts', 'spl',
U_BOOT_SPL_NODTB_DATA)
self.assertIn("Invalid U-Boot phase 'bad-phase': Use tpl/vpl/spl",
str(e.exception))
if __name__ == "__main__":
unittest.main()

View File

@@ -193,6 +193,19 @@ class Image(section.Entry_section):
os.remove(sname)
os.symlink(fname, sname)
def WriteAlternates(self):
"""Write out alternative devicetree blobs, each in its own file"""
alt_entry = self.FindEntryType('alternates-fdt')
if not alt_entry:
return
for alt in alt_entry.alternates:
fname, data = alt_entry.ProcessWithFdt(alt)
pathname = tools.get_output_filename(fname)
tout.info(f"Writing alternate '{alt}' to '{pathname}'")
tools.write_file(pathname, data)
tout.info("Wrote %#x bytes" % len(data))
def WriteMap(self):
"""Write a map of the image to a .map file

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "model-not-set";
binman {
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
section {
u-boot-tpl {
};
};
};
blob {
filename = "blobfile";
};
};
};

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "model-not-set";
binman {
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "tpl";
section {
u-boot-tpl {
};
};
};
blob {
filename = "blobfile";
};
};
};

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "model-not-set";
binman {
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "vpl";
section {
u-boot-vpl {
};
};
};
blob {
filename = "blobfile";
};
};
};

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "model-not-set";
binman {
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "spl";
section {
u-boot-spl {
};
};
};
blob {
filename = "blobfile";
};
};
};

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "model-not-set";
binman {
alternates-fdt {
fdt-list-dir = "dtb";
filename-pattern = "NAME.bin";
fdt-phase = "bad-phase";
section {
u-boot-spl {
};
};
};
blob {
filename = "blobfile";
};
};
};

View File

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
model = "Model One";
compatible = "u-boot,model-one";
/* this node remains due to bootph-pre-sram tag */
node {
some-prop;
prop-to-remove;
another-prop-to-get-rid-of;
not-a-prop-to-remove;
bootph-pre-sram;
/* this node get removed by fdtgrep */
other-node {
another-prop;
};
};
};

View File

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0+
// Copyright 2024 Google LLC
// Written by Simon Glass <sjg@chromium.org>
/dts-v1/;
/ {
model = "Model Two";
compatible = "u-boot,model-two";
/* this node remains due to bootph-pre-sram tag */
node {
some-prop;
prop-to-remove;
another-prop-to-get-rid-of;
not-a-prop-to-remove;
bootph-pre-sram;
/* this node get removed by fdtgrep */
other-node {
another-prop;
};
};
};