* [PATCH v3 00/10] Generate Android boot images with binman
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
U-Boot is seeing increasing adoption on pocket computers, many of which
(sadly) have fused bootloader chains. Many of these bootloaders have a
very rigid definition of what a "valid" downstream payload looks like.
Sometimes it's just a boring old v0/v2 Android boot image. Sometimes
it's something decidedly more grotesque.
To date, this last-mile packaging step has been trapped in downstream CI
pipelines, blogposts, documentation, etc. This patch series aims to
gather up all that esoterica and institutional knowledge and codify it
in U-Boot's build system, using binman.
Put differently: an overwhelming majority of these pocket computer
devices have a "canonical" payload format that U-Boot currently has no
support for. Let's fix that.
The first patches in this series introduce an `android_boot` etype. To
begin with, this is a "typical" abootimg, as defined by canonical AOSP
sources and reference `fastboot`/`mkbootimg` implementation. There's
plenty of devices out there with a sane(-ish, nothing in Android-land is
ever truly sane) bootloader that will happily chain a U-Boot rolled into
the kernel section of an abootimg.
With that out of the way, the cursed bootloaders are next to be
supported. Binman etypes for Qualcomm's "QCDT" and Samsung's "DTBH"
formats are implemented. These are non-standard vendor-specific
devicetree containers, which the previous bootloader uses to pick a FDT
to boot the downstream with.
In both cases, these vendor-specific formats are tacked on to the end of
a v0 abootimg, with the header_version being hijacked to encode the
length of this following payload. Thus, the android_boot etype is
retrofitted to allow these shenanigans.
Binman configs that produce flashable boot artifacts are introduced for
the following devices:
* google-sargo (vanilla v2)
* oneplus-fajita (vanilla v2)
* samsung-a5u-eur (QCDT v0)
* samsung-gt510 (QCDT v0)
* samsung-j7xelte (DTBH v0)
I also successfully tested a vanilla v0 on my samsung-expressltexx, in
conjunction with the qcom-armv7 series already on the list. However,
since that work is still in-flight and the expressltexx DTS is
downstream, I opted not to include it here.
Note that, I dropped the commits that introduce this support for
Qualcomm devices, as it was NAK'd by "Qualcomm". I still think this is
worth inclusion upstream for a couple of reasons:
* It's still useful for Exynos devices.
* If/when other non-qcom devices with fused Android bootloaders come
along, it will likely be useful there as well.
* Downstreams can easily carry DTSI overlays for qcom devices to take
advantage of this new native U-Boot support there.
* Maybe the hard position against binman will change/soften on the
Qualcomm side, in which case we can simply start dropping in DTSIs to
add support for those devices.
Signed-off-by: Sam Day <me@samcday.com>
---
Changes in v3:
- android_boot etype:
- collapse _GetOptionalEntryData into _GetEntryData
- use self.Raise consistently for sane errors
- clean up and expand tests for 100% coverage
- fix docstring examples formatting
- remove unnecessary restriction on os-version in conjunction with
vendor-dt
- introduce Entry_Android_vendor_dt_table shared base class for
QCDT+DTBH.
- qcdt+dtbh etypes:
- formalize header/record format/size constants
- rework tests and hit 100% coverage
- binman:
- fix support for listing multiple names to `binman test`
- add fnmatch globbing support to `binman test`
- Link to v2: https://lore.kernel.org/r/20260608-android-binman-v2-0-b8203682507f@samcday.com
Changes in v2:
- Rebase on next
- Drop all qcom-related work as it has been NAK'd from mach-snapdragon
- Link to v1: https://lore.kernel.org/r/20260606-android-binman-v1-0-c7a3c14b8a3d@samcday.com
---
Sam Day (10):
binman: support running multiple tests
binman: test name globbing support
binman: section: add AlignUp+PadToAlignment helpers
binman: Android boot image support
.gitignore: ignore binman-generated blobs
binman: android_boot: vendor-dt support
binman: Add QCDT support
binman: Add DTBH support
arch: arm: exynos: add j7xelte binman config
configs: exynos-mobile: pull in binman
.gitignore | 1 +
arch/arm/dts/exynos-mobile.dtsi | 5 +
arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi | 26 ++
arch/arm/mach-exynos/Kconfig | 1 +
configs/exynos-mobile_defconfig | 1 +
tools/binman/android_vendor_dt_table.py | 104 ++++++
tools/binman/cmdline.py | 2 +-
tools/binman/etype/android_boot.py | 376 +++++++++++++++++++++
tools/binman/etype/dtbh.py | 108 ++++++
tools/binman/etype/qcdt.py | 80 +++++
tools/binman/etype/section.py | 9 +
tools/binman/ftest.py | 365 ++++++++++++++++++++
tools/binman/main.py | 8 +-
tools/binman/test/android_boot_chonky_cells.dts | 13 +
tools/binman/test/android_boot_dtb_in_v0.dts | 12 +
tools/binman/test/android_boot_invalid_addr.dts | 13 +
.../binman/test/android_boot_invalid_pagesize.dts | 11 +
tools/binman/test/android_boot_invalid_subnode.dts | 12 +
tools/binman/test/android_boot_missing_kernel.dts | 9 +
.../test/android_boot_oversized_bootname.dts | 12 +
.../test/android_boot_unsupported_version.dts | 11 +
tools/binman/test/android_boot_v0.dts | 34 ++
.../test/android_boot_v0_pagesize_too_smol.dts | 12 +
tools/binman/test/android_boot_v2.dts | 50 +++
tools/binman/test/android_boot_v2_missing_dtb.dts | 12 +
.../test/android_boot_v2_pagesize_too_smol.dts | 13 +
tools/binman/test/android_boot_v2_vendor_dt.dts | 14 +
tools/binman/test/android_boot_vendor_dt.dts | 27 ++
tools/binman/test/dtbh.dts | 22 ++
tools/binman/test/dtbh_bad_model_info.dts | 20 ++
tools/binman/test/dtbh_invalid_pagesize.dts | 12 +
tools/binman/test/dtbh_missing_model_info.dts | 16 +
tools/binman/test/dtbh_missing_payload.dts | 15 +
tools/binman/test/dtbh_missing_subnodes.dts | 10 +
tools/binman/test/dtbh_multi.dts | 29 ++
tools/binman/test/dtbh_multiple_dtbs.dts | 22 ++
tools/binman/test/dtbh_page_size_from_abootimg.dts | 30 ++
tools/binman/test/dtbh_special_subnodes.dts | 11 +
tools/binman/test/qcdt.dts | 36 ++
tools/binman/test/qcdt_bad_msm_id.dts | 17 +
tools/binman/test/qcdt_invalid_pagesize.dts | 12 +
tools/binman/test/qcdt_missing_msm_id.dts | 12 +
tools/binman/test/qcdt_missing_payload.dts | 14 +
tools/binman/test/qcdt_missing_subnodes.dts | 13 +
tools/binman/test/qcdt_multiple_dtbs.dts | 34 ++
tools/binman/test/qcdt_page_size_from_abootimg.dts | 33 ++
tools/binman/test/qcdt_zero_pagesize.dts | 12 +
tools/u_boot_pylib/test_util.py | 25 +-
48 files changed, 1724 insertions(+), 12 deletions(-)
---
base-commit: e91911169bc737ee4a79963a1cba8db2aab7c1c0
change-id: 20260604-android-binman-ad7a43f4e99d
Best regards,
--
Sam Day <me@samcday.com>
^ permalink raw reply [flat|nested] 25+ messages in thread* [PATCH v3 01/10] binman: support running multiple tests
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
The helptext for `binman test` suggests that this was already supported,
but it wasn't properly wired up.
---
tools/binman/main.py | 8 ++++----
tools/u_boot_pylib/test_util.py | 23 ++++++++++++++++-------
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/tools/binman/main.py b/tools/binman/main.py
index a0c775fd629..5473789361d 100755
--- a/tools/binman/main.py
+++ b/tools/binman/main.py
@@ -58,8 +58,8 @@ def RunTests(debug, verbosity, processes, test_preserve_dirs, args, toolpath):
the output directory for this test. Both directories are displayed
on the command line.
processes: Number of processes to use to run tests (None=same as #CPUs)
- args: List of positional args provided to binman. This can hold a test
- name to execute (as in 'binman test testSections', for example)
+ args: List of positional args provided to binman. This can hold test
+ names to execute (as in 'binman test testSections', for example)
toolpath: List of paths to use for tools
"""
from binman import bintool_test
@@ -72,13 +72,13 @@ def RunTests(debug, verbosity, processes, test_preserve_dirs, args, toolpath):
from binman import image_test
import doctest
- test_name = args and args[0] or None
+ test_names = args or None
# Run the entry tests first ,since these need to be the first to import the
# 'entry' module.
result = test_util.run_test_suites(
'binman', debug, verbosity, False, test_preserve_dirs, processes,
- test_name, toolpath,
+ test_names, toolpath,
[bintool_test.TestBintool, entry_test.TestEntry, ftest.TestFunctional,
fdt_test.TestFdt, elf_test.TestElf, image_test.TestImage,
cbfs_util_test.TestCbfs, fip_util_test.TestFip])
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index 364596d7a0c..ff275a7b7c0 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -157,7 +157,7 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
the output directory for this test. Both directories are displayed
on the command line.
processes: Number of processes to use to run tests (None=same as #CPUs)
- test_name: Name of test to run, or None for all
+ test_name: Name of test, or list of test names, to run; None for all
toolpath: List of paths to use for tools
class_and_module_list: List of test classes (type class) and module
names (type str) to run
@@ -182,12 +182,19 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
resultclass=FullTextTestResult,
)
- if use_concurrent and processes != 1 and not test_name:
+ if isinstance(test_name, str):
+ test_names = [test_name]
+ else:
+ test_names = list(test_name or [])
+ test_name_set = set(test_names)
+
+ if use_concurrent and processes != 1 and not test_names:
suite = ConcurrentTestSuite(suite,
fork_for_tests(processes or multiprocessing.cpu_count()))
for module in class_and_module_list:
- if isinstance(module, str) and (not test_name or test_name == module):
+ if (isinstance(module, str) and
+ (not test_names or module in test_name_set)):
suite.addTests(doctest.DocTestSuite(module))
for module in class_and_module_list:
@@ -197,15 +204,17 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
if hasattr(module, 'setup_test_args'):
setup_test_args = getattr(module, 'setup_test_args')
setup_test_args(preserve_indir=test_preserve_dirs,
- preserve_outdirs=test_preserve_dirs and test_name is not None,
+ preserve_outdirs=test_preserve_dirs and bool(test_names),
toolpath=toolpath, verbosity=verbosity, no_capture=no_capture)
- if test_name:
+ if test_names:
# Since Python v3.5 If an ImportError or AttributeError occurs
# while traversing a name then a synthetic test that raises that
# error when run will be returned. Check that the requested test
# exists, otherwise these errors are included in the results.
- if test_name in loader.getTestCaseNames(module):
- suite.addTests(loader.loadTestsFromName(test_name, module))
+ module_test_names = loader.getTestCaseNames(module)
+ for name in test_names:
+ if name in module_test_names:
+ suite.addTests(loader.loadTestsFromName(name, module))
else:
suite.addTests(loader.loadTestsFromTestCase(module))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 01/10] binman: support running multiple tests
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
The helptext for `binman test` suggests that this was already supported,
but it wasn't properly wired up.
---
tools/binman/main.py | 8 ++++----
tools/u_boot_pylib/test_util.py | 23 ++++++++++++++++-------
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/tools/binman/main.py b/tools/binman/main.py
index a0c775fd629..5473789361d 100755
--- a/tools/binman/main.py
+++ b/tools/binman/main.py
@@ -58,8 +58,8 @@ def RunTests(debug, verbosity, processes, test_preserve_dirs, args, toolpath):
the output directory for this test. Both directories are displayed
on the command line.
processes: Number of processes to use to run tests (None=same as #CPUs)
- args: List of positional args provided to binman. This can hold a test
- name to execute (as in 'binman test testSections', for example)
+ args: List of positional args provided to binman. This can hold test
+ names to execute (as in 'binman test testSections', for example)
toolpath: List of paths to use for tools
"""
from binman import bintool_test
@@ -72,13 +72,13 @@ def RunTests(debug, verbosity, processes, test_preserve_dirs, args, toolpath):
from binman import image_test
import doctest
- test_name = args and args[0] or None
+ test_names = args or None
# Run the entry tests first ,since these need to be the first to import the
# 'entry' module.
result = test_util.run_test_suites(
'binman', debug, verbosity, False, test_preserve_dirs, processes,
- test_name, toolpath,
+ test_names, toolpath,
[bintool_test.TestBintool, entry_test.TestEntry, ftest.TestFunctional,
fdt_test.TestFdt, elf_test.TestElf, image_test.TestImage,
cbfs_util_test.TestCbfs, fip_util_test.TestFip])
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index 364596d7a0c..ff275a7b7c0 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -157,7 +157,7 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
the output directory for this test. Both directories are displayed
on the command line.
processes: Number of processes to use to run tests (None=same as #CPUs)
- test_name: Name of test to run, or None for all
+ test_name: Name of test, or list of test names, to run; None for all
toolpath: List of paths to use for tools
class_and_module_list: List of test classes (type class) and module
names (type str) to run
@@ -182,12 +182,19 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
resultclass=FullTextTestResult,
)
- if use_concurrent and processes != 1 and not test_name:
+ if isinstance(test_name, str):
+ test_names = [test_name]
+ else:
+ test_names = list(test_name or [])
+ test_name_set = set(test_names)
+
+ if use_concurrent and processes != 1 and not test_names:
suite = ConcurrentTestSuite(suite,
fork_for_tests(processes or multiprocessing.cpu_count()))
for module in class_and_module_list:
- if isinstance(module, str) and (not test_name or test_name == module):
+ if (isinstance(module, str) and
+ (not test_names or module in test_name_set)):
suite.addTests(doctest.DocTestSuite(module))
for module in class_and_module_list:
@@ -197,15 +204,17 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
if hasattr(module, 'setup_test_args'):
setup_test_args = getattr(module, 'setup_test_args')
setup_test_args(preserve_indir=test_preserve_dirs,
- preserve_outdirs=test_preserve_dirs and test_name is not None,
+ preserve_outdirs=test_preserve_dirs and bool(test_names),
toolpath=toolpath, verbosity=verbosity, no_capture=no_capture)
- if test_name:
+ if test_names:
# Since Python v3.5 If an ImportError or AttributeError occurs
# while traversing a name then a synthetic test that raises that
# error when run will be returned. Check that the requested test
# exists, otherwise these errors are included in the results.
- if test_name in loader.getTestCaseNames(module):
- suite.addTests(loader.loadTestsFromName(test_name, module))
+ module_test_names = loader.getTestCaseNames(module)
+ for name in test_names:
+ if name in module_test_names:
+ suite.addTests(loader.loadTestsFromName(name, module))
else:
suite.addTests(loader.loadTestsFromTestCase(module))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 02/10] binman: test name globbing support
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
This makes invocations like `binman test testAndroid*` possible. It also
composes well with the previous commit that adds support for multiple
test names: `binman test 'testAndroid*' 'testQcdt*'` also works as you'd
expect.
---
tools/binman/cmdline.py | 2 +-
tools/u_boot_pylib/test_util.py | 12 +++++++-----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/tools/binman/cmdline.py b/tools/binman/cmdline.py
index 9632ec115e5..b3be9f79b20 100644
--- a/tools/binman/cmdline.py
+++ b/tools/binman/cmdline.py
@@ -201,7 +201,7 @@ controlled by a description in the board device tree.'''
'preserve the output directory if a single test is run (pass '
'test name at the end of the command line')
test_parser.add_argument('tests', nargs='*',
- help='Test names to run (omit for all)')
+ help='Test names/patterns to run (omit for all)')
tool_parser = subparsers.add_parser('tool', help='Check bintools')
tool_parser.add_argument('-l', '--list', action='store_true',
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index ff275a7b7c0..2089b3574ca 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -4,6 +4,7 @@
#
import doctest
+import fnmatch
import glob
import multiprocessing
import os
@@ -186,7 +187,9 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
test_names = [test_name]
else:
test_names = list(test_name or [])
- test_name_set = set(test_names)
+
+ def _match_name(name):
+ return any(fnmatch.fnmatchcase(name, pattern) for pattern in test_names)
if use_concurrent and processes != 1 and not test_names:
suite = ConcurrentTestSuite(suite,
@@ -194,7 +197,7 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
for module in class_and_module_list:
if (isinstance(module, str) and
- (not test_names or module in test_name_set)):
+ (not test_names or _match_name(module))):
suite.addTests(doctest.DocTestSuite(module))
for module in class_and_module_list:
@@ -211,9 +214,8 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
# while traversing a name then a synthetic test that raises that
# error when run will be returned. Check that the requested test
# exists, otherwise these errors are included in the results.
- module_test_names = loader.getTestCaseNames(module)
- for name in test_names:
- if name in module_test_names:
+ for name in loader.getTestCaseNames(module):
+ if _match_name(name):
suite.addTests(loader.loadTestsFromName(name, module))
else:
suite.addTests(loader.loadTestsFromTestCase(module))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 02/10] binman: test name globbing support
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
This makes invocations like `binman test testAndroid*` possible. It also
composes well with the previous commit that adds support for multiple
test names: `binman test 'testAndroid*' 'testQcdt*'` also works as you'd
expect.
---
tools/binman/cmdline.py | 2 +-
tools/u_boot_pylib/test_util.py | 12 +++++++-----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/tools/binman/cmdline.py b/tools/binman/cmdline.py
index 9632ec115e5..b3be9f79b20 100644
--- a/tools/binman/cmdline.py
+++ b/tools/binman/cmdline.py
@@ -201,7 +201,7 @@ controlled by a description in the board device tree.'''
'preserve the output directory if a single test is run (pass '
'test name at the end of the command line')
test_parser.add_argument('tests', nargs='*',
- help='Test names to run (omit for all)')
+ help='Test names/patterns to run (omit for all)')
tool_parser = subparsers.add_parser('tool', help='Check bintools')
tool_parser.add_argument('-l', '--list', action='store_true',
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index ff275a7b7c0..2089b3574ca 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -4,6 +4,7 @@
#
import doctest
+import fnmatch
import glob
import multiprocessing
import os
@@ -186,7 +187,9 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
test_names = [test_name]
else:
test_names = list(test_name or [])
- test_name_set = set(test_names)
+
+ def _match_name(name):
+ return any(fnmatch.fnmatchcase(name, pattern) for pattern in test_names)
if use_concurrent and processes != 1 and not test_names:
suite = ConcurrentTestSuite(suite,
@@ -194,7 +197,7 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
for module in class_and_module_list:
if (isinstance(module, str) and
- (not test_names or module in test_name_set)):
+ (not test_names or _match_name(module))):
suite.addTests(doctest.DocTestSuite(module))
for module in class_and_module_list:
@@ -211,9 +214,8 @@ def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
# while traversing a name then a synthetic test that raises that
# error when run will be returned. Check that the requested test
# exists, otherwise these errors are included in the results.
- module_test_names = loader.getTestCaseNames(module)
- for name in test_names:
- if name in module_test_names:
+ for name in loader.getTestCaseNames(module):
+ if _match_name(name):
suite.addTests(loader.loadTestsFromName(name, module))
else:
suite.addTests(loader.loadTestsFromTestCase(module))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 03/10] binman: section: add AlignUp+PadToAlignment helpers
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
In the following commits we will be introducing new etypes derived from
Entry_section which need to align values and pad data. To avoid
duplication we introduce the shared helpers here.
---
tools/binman/etype/section.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index 8530b7ee17f..a8eca3eaf5f 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -1040,3 +1040,12 @@ class Entry_section(Entry):
for entry in entries.values():
return entry.read_elf_segments()
return None
+
+ def AlignUp(self, value, align):
+ """Return value aligned to given power of 2"""
+ return (value + align - 1) & ~(align - 1)
+
+
+ def PadToAlignment(self, data, align):
+ """Null-pads given data to given power of 2"""
+ return data + b'\0' * (self.AlignUp(len(data), align) - len(data))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 03/10] binman: section: add AlignUp+PadToAlignment helpers
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
In the following commits we will be introducing new etypes derived from
Entry_section which need to align values and pad data. To avoid
duplication we introduce the shared helpers here.
---
tools/binman/etype/section.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index 8530b7ee17f..a8eca3eaf5f 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -1040,3 +1040,12 @@ class Entry_section(Entry):
for entry in entries.values():
return entry.read_elf_segments()
return None
+
+ def AlignUp(self, value, align):
+ """Return value aligned to given power of 2"""
+ return (value + align - 1) & ~(align - 1)
+
+
+ def PadToAlignment(self, data, align):
+ """Null-pads given data to given power of 2"""
+ return data + b'\0' * (self.AlignUp(len(data), align) - len(data))
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 04/10] binman: Android boot image support
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
Introduce initial support for Android boot images (abootimgs).
The AOSP implementation was used as a reference for this work.
We start with just v0 and v2 support. v1/v3/v4 are deliberately left
unimplemented for now as no use-case has been found for them yet.
Since we're targeting U-Boot use cases here, a couple of things were
omitted from this impl, namely "second" and recovery_dtbo support.
Link: https://android.googlesource.com/platform/system/tools/mkbootimg/
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 285 +++++++++++++++++++++
tools/binman/ftest.py | 148 +++++++++++
tools/binman/test/android_boot_chonky_cells.dts | 13 +
tools/binman/test/android_boot_dtb_in_v0.dts | 12 +
tools/binman/test/android_boot_invalid_addr.dts | 13 +
.../binman/test/android_boot_invalid_pagesize.dts | 11 +
tools/binman/test/android_boot_invalid_subnode.dts | 12 +
tools/binman/test/android_boot_missing_kernel.dts | 9 +
.../test/android_boot_oversized_bootname.dts | 12 +
.../test/android_boot_unsupported_version.dts | 11 +
tools/binman/test/android_boot_v0.dts | 34 +++
.../test/android_boot_v0_pagesize_too_smol.dts | 12 +
tools/binman/test/android_boot_v2.dts | 50 ++++
tools/binman/test/android_boot_v2_missing_dtb.dts | 12 +
.../test/android_boot_v2_pagesize_too_smol.dts | 13 +
tools/binman/test/android_boot_v2_vendor_dt.dts | 14 +
16 files changed, 661 insertions(+)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
new file mode 100644
index 00000000000..8b72d90acc5
--- /dev/null
+++ b/tools/binman/etype/android_boot.py
@@ -0,0 +1,285 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Android boot images
+
+import hashlib
+import struct
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+
+
+BOOT_MAGIC = b'ANDROID!'
+BOOT_NAME_SIZE = 16
+BOOT_ARGS_SIZE = 512
+IMAGE_ID_SIZE = 32
+BOOT_EXTRA_ARGS_SIZE = 1024
+
+BOOT_IMAGE_HEADER_V0 = '<{}s10I{}s{}s{}s'.format(len(BOOT_MAGIC),
+ BOOT_NAME_SIZE,
+ BOOT_ARGS_SIZE,
+ IMAGE_ID_SIZE)
+BOOT_IMAGE_HEADER_V0_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V0)
+BOOT_IMAGE_HEADER_V2 = (BOOT_IMAGE_HEADER_V0 +
+ '{}sIQIIQ'.format(BOOT_EXTRA_ARGS_SIZE))
+BOOT_IMAGE_HEADER_V2_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V2)
+
+
+class Entry_android_boot(Entry_section):
+ """Android boot image
+
+ This creates an Android v0 or v2 boot image.
+
+ A kernel payload, optional ramdisk payload can be supplied. A DTB payload
+ can also be provided when header_version == v2.
+
+ Properties / Entry arguments:
+ - header-version: Android boot image header version, must be 0 or 2,
+ defaults to 0
+ - page-size: Image page size, defaults to 2048
+ - base: Base address added to the offsets below, defaults to 0x10000000
+ - kernel-offset: Kernel load offset from base, defaults to 0x00008000
+ - ramdisk-offset: Ramdisk load offset from base, defaults to 0x01000000
+ - tags-offset: ATAGS/FDT offset from base, defaults to 0x00000100
+ - dtb-offset: DTB load offset from base, defaults to 0x01f00000
+ - os-version: Encoded Android OS version and patch level, defaults to 0
+ - boot-name: Android boot image board name
+ - cmdline: Android boot command line
+
+ This entry uses the following subnodes:
+ - kernel: section containing the executable payload
+ - dtb: section containing the DTB payload, used by header version 2 only
+ - ramdisk: optional section containing a ramdisk payload
+
+ Example::
+ A v2 abootimg with control FDT placed in the DTB section:
+
+ android-boot {
+ header-version = <2>;
+ page-size = <4096>;
+ base = <0x12345678>;
+ kernel-offset = <0xCAFED00D>;
+ ramdisk-offset = <0xBEEFBABE>;
+ tags-offset = <0xFEEDDEAD>;
+ dtb-offset = <0x06660666>;
+ cmdline = "foo bar";
+
+ kernel {
+ u-boot-nodtb {
+ # Many Android bootloaders support gzipped kernels
+ compress = "gzip";
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+
+ Example::
+ A v0 abootimg with embedded control FDT (v0 doesn't support DTBs) and
+ an empty ramdisk (some bootloaders insist on a ramdisk being present):
+
+ android-boot {
+ header-version = <0>;
+ page-size = <2048>;
+ base = <0x80200000>;
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ """
+
+ def ReadNode(self):
+ super().ReadNode()
+ self.header_version = fdt_util.GetInt(self._node, 'header-version', 0)
+ self.page_size = fdt_util.GetInt(self._node, 'page-size', 2048)
+ self.base = self._GetIntCells('base', 0x10000000)
+ self.kernel_offset = self._GetIntCells('kernel-offset', 0x00008000)
+ self.ramdisk_offset = self._GetIntCells('ramdisk-offset', 0x01000000)
+ self.tags_offset = self._GetIntCells('tags-offset', 0x00000100)
+ self.dtb_offset = self._GetIntCells('dtb-offset', 0x01f00000)
+ self.os_version = fdt_util.GetInt(self._node, 'os-version', 0)
+ self.boot_name = fdt_util.GetString(self._node, 'boot-name', '')
+ self.cmdline = fdt_util.GetString(self._node, 'cmdline', '')
+
+ if self.header_version not in (0, 2):
+ self.Raise('Only Android boot image header versions 0 and 2 are '
+ 'supported')
+ if self.page_size <= 0 or self.page_size & (self.page_size - 1):
+ self.Raise('page-size must be a power of two')
+ if 'kernel' not in self._entries:
+ self.Raise("Missing required subnode 'kernel'")
+
+ if self.header_version == 0:
+ if self.page_size < BOOT_IMAGE_HEADER_V0_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' in self._entries:
+ self.Raise("Subnode 'dtb' requires header-version 2")
+ else:
+ # v2
+ if self.page_size < BOOT_IMAGE_HEADER_V2_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' not in self._entries:
+ self.Raise("Missing required subnode 'dtb'")
+
+ def ReadEntries(self):
+ for node in self._node.subnodes:
+ if node.name not in ('kernel', 'ramdisk', 'dtb'):
+ self.Raise("Unexpected subnode '%s'" % node.name)
+
+ entry = Entry.Create(self, node, etype='section',
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[node.name] = entry
+
+ def _GetIntCells(self, propname, default):
+ prop = self._node.props.get(propname)
+ if not prop:
+ return default
+
+ values = prop.value if isinstance(prop.value, list) else [prop.value]
+ if len(values) > 2:
+ self.Raise("Property '%s' must contain one or two cells" %
+ propname)
+
+ value = 0
+ for cell in values:
+ value = value << 32 | fdt_util.fdt32_to_cpu(cell)
+
+ return value
+
+ def _GetAddr(self, offset, name, size=32):
+ addr = self.base + offset
+ if addr >= 1 << size:
+ self.Raise('%s address %#x does not fit in %d bits' %
+ (name, addr, size))
+
+ return addr
+
+ def _CheckFit(self, name, data, size):
+ if len(data) > size:
+ self.Raise('%s is %d bytes, maximum is %d' %
+ (name, len(data), size))
+
+ return data + b'\0' * (size - len(data))
+
+ @staticmethod
+ def _BootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def _SplitCmdline(self):
+ cmdline = self.cmdline.encode('ascii') + b'\0'
+ return (self._CheckFit('cmdline', cmdline[:BOOT_ARGS_SIZE],
+ BOOT_ARGS_SIZE),
+ self._CheckFit('extra-cmdline', cmdline[BOOT_ARGS_SIZE:],
+ BOOT_EXTRA_ARGS_SIZE))
+
+ def _GetEntryData(self, name, required, default=None):
+ entry = self._entries.get(name)
+ if not entry and default is not None:
+ return default
+ return entry.GetData(required)
+
+ def _BuildV0SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ ramdisk = self._GetEntryData('ramdisk', required, b'')
+ if not required and (kernel is None or vendor_dt is None or
+ ramdisk is None):
+ return None
+
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline = self._CheckFit('cmdline', self.cmdline.encode('ascii'),
+ BOOT_ARGS_SIZE)
+
+ boot_id_payloads = [kernel, ramdisk, b'']
+ image_id = self._BootId(*boot_id_payloads)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V0,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id)
+
+ image = bytearray()
+ image += self.PadToAlignment(header, self.page_size)
+ image += self.PadToAlignment(kernel, self.page_size)
+ image += self.PadToAlignment(ramdisk, self.page_size)
+
+ return bytes(image)
+
+ def _BuildV2SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ dtb = self._GetEntryData('dtb', required)
+ ramdisk = self._GetEntryData('ramdisk', required, b'')
+ if not required and (kernel is None or dtb is None):
+ return None
+
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline, extra_cmdline = self._SplitCmdline()
+ image_id = self._BootId(kernel, ramdisk, b'', b'', dtb)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V2,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id,
+ extra_cmdline,
+ 0, # recovery_dtbo_len
+ 0, # recovery_dtbo_offset
+ BOOT_IMAGE_HEADER_V2_SIZE,
+ len(dtb),
+ self._GetAddr(self.dtb_offset, 'dtb', size=64))
+
+ image = bytearray()
+ image += self.PadToAlignment(header, self.page_size)
+ image += self.PadToAlignment(kernel, self.page_size)
+ image += self.PadToAlignment(ramdisk, self.page_size)
+ image += self.PadToAlignment(dtb, self.page_size)
+
+ return bytes(image)
+
+ def BuildSectionData(self, required):
+ if self.header_version == 0:
+ return self._BuildV0SectionData(required)
+
+ return self._BuildV2SectionData(required)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index bf98b268ac1..71740205c72 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5598,6 +5598,154 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Node '/binman/renesas-rcar4-sa0': SRAM data longer than 966656 Bytes",
str(exc.exception))
+ @staticmethod
+ def _AndroidBootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def testAndroidBootUnsupportedVersion(self):
+ """Test that binman rejects versions other than v0 and v2"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_unsupported_version.dts')
+ self.assertIn("Only Android boot image header versions 0 and 2 are supported",
+ str(exc.exception))
+
+ def testAndroidBootInvalidPageSize(self):
+ """Test that binman rejects page sizes that are not a power of 2"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testAndroidBootV0PageSizeTooSmol(self):
+ """Test that binman rejects page sizes that are smaller than header size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v0_pagesize_too_smol.dts')
+ self.assertIn("page-size must fit the Android boot image header",
+ str(exc.exception))
+
+ def testAndroidBootMissingKernel(self):
+ """Test that binman rejects configurations missing a kernel{} subnode"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_missing_kernel.dts')
+ self.assertIn("Missing required subnode 'kernel'",
+ str(exc.exception))
+
+ def testAndroidBootInvalidSubnode(self):
+ """Test that binman rejects invalid subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_subnode.dts')
+ self.assertIn("Unexpected subnode 'bacon'",
+ str(exc.exception))
+
+ def testAndroidBootInvalidAddr(self):
+ """Test that binman rejects invalid addresses"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_addr.dts')
+ self.assertIn("kernel address 0xdeadbeefdafed00d does not fit in 32 bits",
+ str(exc.exception))
+
+ def testAndroidBootOversizedBootName(self):
+ """Test that binman rejects boot-name exceeding 16 chars"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_oversized_bootname.dts')
+ self.assertIn("boot-name is 38 bytes, maximum is 16",
+ str(exc.exception))
+
+ def testAndroidBootChonkyCells(self):
+ """Test that binman rejects >2 cell addresses"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_chonky_cells.dts')
+ self.assertIn("Property 'base' must contain one or two cells",
+ str(exc.exception))
+
+ def testAndroidBootV0(self):
+ """Test that binman can produce a plain legacy Android boot image"""
+ data = self._DoReadFile('android_boot_v0.dts')
+ header = struct.unpack_from('<8s10I16s512s32s', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80208000, header[2])
+ self.assertEqual(1, header[3])
+ self.assertEqual(0x81200000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80200100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(0, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'foo', header[12].split(b'\0', 1)[0])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'\0', b''),
+ header[13])
+
+ def testAndroidBootV0WithDTB(self):
+ """Test that binman rejects v0 abootimgs containing a dtb section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_dtb_in_v0.dts')
+ self.assertIn("Subnode 'dtb' requires header-version 2",
+ str(exc.exception))
+
+ def testAndroidBootV2(self):
+ """Test that binman can produce an Android boot image"""
+ data = self._DoReadFile('android_boot_v2.dts')
+ header = struct.unpack_from('<8s10I16s512s32s1024sIQIIQ', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80008000, header[2])
+ self.assertEqual(0, header[3])
+ self.assertEqual(0x81000000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80000100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(2, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'test-board', header[11].split(b'\0', 1)[0])
+ self.assertEqual(0, header[15])
+ self.assertEqual(0, header[16])
+ self.assertEqual(1660, header[17])
+ self.assertEqual(len(U_BOOT_DTB_DATA), header[18])
+ self.assertEqual(0x81f00000, header[19])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'', b'', b'',
+ U_BOOT_DTB_DATA), header[13])
+
+ cmdline = header[12].split(b'\0', 1)[0]
+ extra_cmdline = header[14].split(b'\0', 1)[0]
+ self.assertEqual(b"tests.. ", cmdline[-8:])
+ self.assertEqual(512, len(cmdline))
+ self.assertEqual(b'sup', extra_cmdline)
+
+ self.assertEqual(U_BOOT_DATA, data[0x800:0x800 + len(U_BOOT_DATA)])
+ self.assertEqual(U_BOOT_DTB_DATA,
+ data[0x1000:0x1000 + len(U_BOOT_DTB_DATA)])
+
+ def testAndroidBootV2PageSizeTooSmol(self):
+ """Test that binman rejects page sizes that are smaller than header size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_pagesize_too_smol.dts')
+ self.assertIn("page-size must fit the Android boot image header",
+ str(exc.exception))
+
+ def testAndroidBootV2MissingDTB(self):
+ """Test that binman rejects v2 abootimgs missing a DTB section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_missing_dtb.dts')
+ self.assertIn("Missing required subnode 'dtb'",
+ str(exc.exception))
+
+ def testAndroidBootV2VendorDt(self):
+ """Test that binman rejects v2 abootimgs with a vendor-dt section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_vendor_dt.dts')
+ self.assertIn("Subnode 'vendor-dt' requires header-version 0",
+ str(exc.exception))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/android_boot_chonky_cells.dts b/tools/binman/test/android_boot_chonky_cells.dts
new file mode 100644
index 00000000000..7fdc1c86f6b
--- /dev/null
+++ b/tools/binman/test/android_boot_chonky_cells.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ base = <0xDEADBEEF 0xCAFED00D 0xDECAF>;
+
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_dtb_in_v0.dts b/tools/binman/test/android_boot_dtb_in_v0.dts
new file mode 100644
index 00000000000..24b91f9a33c
--- /dev/null
+++ b/tools/binman/test/android_boot_dtb_in_v0.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel {};
+ dtb {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_addr.dts b/tools/binman/test/android_boot_invalid_addr.dts
new file mode 100644
index 00000000000..0d7cb051921
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_addr.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel-offset = <0xDEADBEEF 0xCAFED00D>;
+
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_pagesize.dts b/tools/binman/test/android_boot_invalid_pagesize.dts
new file mode 100644
index 00000000000..01925187475
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_pagesize.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <2049>;
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_subnode.dts b/tools/binman/test/android_boot_invalid_subnode.dts
new file mode 100644
index 00000000000..747f95068be
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_subnode.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel {};
+ bacon {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_missing_kernel.dts b/tools/binman/test/android_boot_missing_kernel.dts
new file mode 100644
index 00000000000..fe30eb5cbb3
--- /dev/null
+++ b/tools/binman/test/android_boot_missing_kernel.dts
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {};
+ };
+};
diff --git a/tools/binman/test/android_boot_oversized_bootname.dts b/tools/binman/test/android_boot_oversized_bootname.dts
new file mode 100644
index 00000000000..5f5564840f8
--- /dev/null
+++ b/tools/binman/test/android_boot_oversized_bootname.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ boot-name = "this is decidedly longer than 16 bytes";
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_unsupported_version.dts b/tools/binman/test/android_boot_unsupported_version.dts
new file mode 100644
index 00000000000..9843b368b3a
--- /dev/null
+++ b/tools/binman/test/android_boot_unsupported_version.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <1>;
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v0.dts b/tools/binman/test/android_boot_v0.dts
new file mode 100644
index 00000000000..18813ff3613
--- /dev/null
+++ b/tools/binman/test/android_boot_v0.dts
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that android-boot can be referenced before it's built */
+ collection {
+ content = <&abootimg>;
+ };
+
+ abootimg: android-boot {
+ header-version = <0>;
+ page-size = <0x800>;
+ base = <0x80200000>;
+ cmdline = "foo";
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v0_pagesize_too_smol.dts b/tools/binman/test/android_boot_v0_pagesize_too_smol.dts
new file mode 100644
index 00000000000..2c617f12a1e
--- /dev/null
+++ b/tools/binman/test/android_boot_v0_pagesize_too_smol.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <32>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2.dts b/tools/binman/test/android_boot_v2.dts
new file mode 100644
index 00000000000..55fab329443
--- /dev/null
+++ b/tools/binman/test/android_boot_v2.dts
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+#define CMDLINE(...) #__VA_ARGS__
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that android-boot can be referenced before it's built */
+ collection {
+ content = <&abootimg>;
+ };
+
+ abootimg: android-boot {
+ header-version = <2>;
+ page-size = <0x800>;
+ base = <0x80000000>;
+ kernel-offset = <0x00008000>;
+ ramdisk-offset = <0x01000000>;
+ tags-offset = <0x00000100>;
+ dtb-offset = <0x01f00000>;
+ boot-name = "test-board";
+ cmdline = CMDLINE(
+ This is a very long commandline that is sure to exceed the
+ 512 chars that is allotted to the cmdline and this should
+ spillover into extra_cmdline which is useful from a
+ function testing standpoint. Gosh, it sure it hard to come
+ up with enough filler text here to get over the 512 char
+ limit though, huh? Even for someone as loquacious as
+ myself. So anyway. How's your day going? I wrote a binman
+ functional test today. It was fun. Did you know that
+ binman is great. I like binman. I also like functional
+ tests.. sup);
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_missing_dtb.dts b/tools/binman/test/android_boot_v2_missing_dtb.dts
new file mode 100644
index 00000000000..bf7bee622c4
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_missing_dtb.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_pagesize_too_smol.dts b/tools/binman/test/android_boot_v2_pagesize_too_smol.dts
new file mode 100644
index 00000000000..0761ff20543
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_pagesize_too_smol.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ page-size = <32>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_vendor_dt.dts b/tools/binman/test/android_boot_v2_vendor_dt.dts
new file mode 100644
index 00000000000..a7684d8492a
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_vendor_dt.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ kernel {};
+ dtb {};
+ vendor-dt {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 04/10] binman: Android boot image support
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
Introduce initial support for Android boot images (abootimgs).
The AOSP implementation was used as a reference for this work.
We start with just v0 and v2 support. v1/v3/v4 are deliberately left
unimplemented for now as no use-case has been found for them yet.
Since we're targeting U-Boot use cases here, a couple of things were
omitted from this impl, namely "second" and recovery_dtbo support.
Link: https://android.googlesource.com/platform/system/tools/mkbootimg/
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 285 +++++++++++++++++++++
tools/binman/ftest.py | 148 +++++++++++
tools/binman/test/android_boot_chonky_cells.dts | 13 +
tools/binman/test/android_boot_dtb_in_v0.dts | 12 +
tools/binman/test/android_boot_invalid_addr.dts | 13 +
.../binman/test/android_boot_invalid_pagesize.dts | 11 +
tools/binman/test/android_boot_invalid_subnode.dts | 12 +
tools/binman/test/android_boot_missing_kernel.dts | 9 +
.../test/android_boot_oversized_bootname.dts | 12 +
.../test/android_boot_unsupported_version.dts | 11 +
tools/binman/test/android_boot_v0.dts | 34 +++
.../test/android_boot_v0_pagesize_too_smol.dts | 12 +
tools/binman/test/android_boot_v2.dts | 50 ++++
tools/binman/test/android_boot_v2_missing_dtb.dts | 12 +
.../test/android_boot_v2_pagesize_too_smol.dts | 13 +
tools/binman/test/android_boot_v2_vendor_dt.dts | 14 +
16 files changed, 661 insertions(+)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
new file mode 100644
index 00000000000..8b72d90acc5
--- /dev/null
+++ b/tools/binman/etype/android_boot.py
@@ -0,0 +1,285 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Android boot images
+
+import hashlib
+import struct
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+
+
+BOOT_MAGIC = b'ANDROID!'
+BOOT_NAME_SIZE = 16
+BOOT_ARGS_SIZE = 512
+IMAGE_ID_SIZE = 32
+BOOT_EXTRA_ARGS_SIZE = 1024
+
+BOOT_IMAGE_HEADER_V0 = '<{}s10I{}s{}s{}s'.format(len(BOOT_MAGIC),
+ BOOT_NAME_SIZE,
+ BOOT_ARGS_SIZE,
+ IMAGE_ID_SIZE)
+BOOT_IMAGE_HEADER_V0_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V0)
+BOOT_IMAGE_HEADER_V2 = (BOOT_IMAGE_HEADER_V0 +
+ '{}sIQIIQ'.format(BOOT_EXTRA_ARGS_SIZE))
+BOOT_IMAGE_HEADER_V2_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V2)
+
+
+class Entry_android_boot(Entry_section):
+ """Android boot image
+
+ This creates an Android v0 or v2 boot image.
+
+ A kernel payload, optional ramdisk payload can be supplied. A DTB payload
+ can also be provided when header_version == v2.
+
+ Properties / Entry arguments:
+ - header-version: Android boot image header version, must be 0 or 2,
+ defaults to 0
+ - page-size: Image page size, defaults to 2048
+ - base: Base address added to the offsets below, defaults to 0x10000000
+ - kernel-offset: Kernel load offset from base, defaults to 0x00008000
+ - ramdisk-offset: Ramdisk load offset from base, defaults to 0x01000000
+ - tags-offset: ATAGS/FDT offset from base, defaults to 0x00000100
+ - dtb-offset: DTB load offset from base, defaults to 0x01f00000
+ - os-version: Encoded Android OS version and patch level, defaults to 0
+ - boot-name: Android boot image board name
+ - cmdline: Android boot command line
+
+ This entry uses the following subnodes:
+ - kernel: section containing the executable payload
+ - dtb: section containing the DTB payload, used by header version 2 only
+ - ramdisk: optional section containing a ramdisk payload
+
+ Example::
+ A v2 abootimg with control FDT placed in the DTB section:
+
+ android-boot {
+ header-version = <2>;
+ page-size = <4096>;
+ base = <0x12345678>;
+ kernel-offset = <0xCAFED00D>;
+ ramdisk-offset = <0xBEEFBABE>;
+ tags-offset = <0xFEEDDEAD>;
+ dtb-offset = <0x06660666>;
+ cmdline = "foo bar";
+
+ kernel {
+ u-boot-nodtb {
+ # Many Android bootloaders support gzipped kernels
+ compress = "gzip";
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+
+ Example::
+ A v0 abootimg with embedded control FDT (v0 doesn't support DTBs) and
+ an empty ramdisk (some bootloaders insist on a ramdisk being present):
+
+ android-boot {
+ header-version = <0>;
+ page-size = <2048>;
+ base = <0x80200000>;
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ """
+
+ def ReadNode(self):
+ super().ReadNode()
+ self.header_version = fdt_util.GetInt(self._node, 'header-version', 0)
+ self.page_size = fdt_util.GetInt(self._node, 'page-size', 2048)
+ self.base = self._GetIntCells('base', 0x10000000)
+ self.kernel_offset = self._GetIntCells('kernel-offset', 0x00008000)
+ self.ramdisk_offset = self._GetIntCells('ramdisk-offset', 0x01000000)
+ self.tags_offset = self._GetIntCells('tags-offset', 0x00000100)
+ self.dtb_offset = self._GetIntCells('dtb-offset', 0x01f00000)
+ self.os_version = fdt_util.GetInt(self._node, 'os-version', 0)
+ self.boot_name = fdt_util.GetString(self._node, 'boot-name', '')
+ self.cmdline = fdt_util.GetString(self._node, 'cmdline', '')
+
+ if self.header_version not in (0, 2):
+ self.Raise('Only Android boot image header versions 0 and 2 are '
+ 'supported')
+ if self.page_size <= 0 or self.page_size & (self.page_size - 1):
+ self.Raise('page-size must be a power of two')
+ if 'kernel' not in self._entries:
+ self.Raise("Missing required subnode 'kernel'")
+
+ if self.header_version == 0:
+ if self.page_size < BOOT_IMAGE_HEADER_V0_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' in self._entries:
+ self.Raise("Subnode 'dtb' requires header-version 2")
+ else:
+ # v2
+ if self.page_size < BOOT_IMAGE_HEADER_V2_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' not in self._entries:
+ self.Raise("Missing required subnode 'dtb'")
+
+ def ReadEntries(self):
+ for node in self._node.subnodes:
+ if node.name not in ('kernel', 'ramdisk', 'dtb'):
+ self.Raise("Unexpected subnode '%s'" % node.name)
+
+ entry = Entry.Create(self, node, etype='section',
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[node.name] = entry
+
+ def _GetIntCells(self, propname, default):
+ prop = self._node.props.get(propname)
+ if not prop:
+ return default
+
+ values = prop.value if isinstance(prop.value, list) else [prop.value]
+ if len(values) > 2:
+ self.Raise("Property '%s' must contain one or two cells" %
+ propname)
+
+ value = 0
+ for cell in values:
+ value = value << 32 | fdt_util.fdt32_to_cpu(cell)
+
+ return value
+
+ def _GetAddr(self, offset, name, size=32):
+ addr = self.base + offset
+ if addr >= 1 << size:
+ self.Raise('%s address %#x does not fit in %d bits' %
+ (name, addr, size))
+
+ return addr
+
+ def _CheckFit(self, name, data, size):
+ if len(data) > size:
+ self.Raise('%s is %d bytes, maximum is %d' %
+ (name, len(data), size))
+
+ return data + b'\0' * (size - len(data))
+
+ @staticmethod
+ def _BootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def _SplitCmdline(self):
+ cmdline = self.cmdline.encode('ascii') + b'\0'
+ return (self._CheckFit('cmdline', cmdline[:BOOT_ARGS_SIZE],
+ BOOT_ARGS_SIZE),
+ self._CheckFit('extra-cmdline', cmdline[BOOT_ARGS_SIZE:],
+ BOOT_EXTRA_ARGS_SIZE))
+
+ def _GetEntryData(self, name, required, default=None):
+ entry = self._entries.get(name)
+ if not entry and default is not None:
+ return default
+ return entry.GetData(required)
+
+ def _BuildV0SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ ramdisk = self._GetEntryData('ramdisk', required, b'')
+ if not required and (kernel is None or vendor_dt is None or
+ ramdisk is None):
+ return None
+
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline = self._CheckFit('cmdline', self.cmdline.encode('ascii'),
+ BOOT_ARGS_SIZE)
+
+ boot_id_payloads = [kernel, ramdisk, b'']
+ image_id = self._BootId(*boot_id_payloads)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V0,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id)
+
+ image = bytearray()
+ image += self.PadToAlignment(header, self.page_size)
+ image += self.PadToAlignment(kernel, self.page_size)
+ image += self.PadToAlignment(ramdisk, self.page_size)
+
+ return bytes(image)
+
+ def _BuildV2SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ dtb = self._GetEntryData('dtb', required)
+ ramdisk = self._GetEntryData('ramdisk', required, b'')
+ if not required and (kernel is None or dtb is None):
+ return None
+
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline, extra_cmdline = self._SplitCmdline()
+ image_id = self._BootId(kernel, ramdisk, b'', b'', dtb)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V2,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id,
+ extra_cmdline,
+ 0, # recovery_dtbo_len
+ 0, # recovery_dtbo_offset
+ BOOT_IMAGE_HEADER_V2_SIZE,
+ len(dtb),
+ self._GetAddr(self.dtb_offset, 'dtb', size=64))
+
+ image = bytearray()
+ image += self.PadToAlignment(header, self.page_size)
+ image += self.PadToAlignment(kernel, self.page_size)
+ image += self.PadToAlignment(ramdisk, self.page_size)
+ image += self.PadToAlignment(dtb, self.page_size)
+
+ return bytes(image)
+
+ def BuildSectionData(self, required):
+ if self.header_version == 0:
+ return self._BuildV0SectionData(required)
+
+ return self._BuildV2SectionData(required)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index bf98b268ac1..71740205c72 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5598,6 +5598,154 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Node '/binman/renesas-rcar4-sa0': SRAM data longer than 966656 Bytes",
str(exc.exception))
+ @staticmethod
+ def _AndroidBootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def testAndroidBootUnsupportedVersion(self):
+ """Test that binman rejects versions other than v0 and v2"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_unsupported_version.dts')
+ self.assertIn("Only Android boot image header versions 0 and 2 are supported",
+ str(exc.exception))
+
+ def testAndroidBootInvalidPageSize(self):
+ """Test that binman rejects page sizes that are not a power of 2"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testAndroidBootV0PageSizeTooSmol(self):
+ """Test that binman rejects page sizes that are smaller than header size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v0_pagesize_too_smol.dts')
+ self.assertIn("page-size must fit the Android boot image header",
+ str(exc.exception))
+
+ def testAndroidBootMissingKernel(self):
+ """Test that binman rejects configurations missing a kernel{} subnode"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_missing_kernel.dts')
+ self.assertIn("Missing required subnode 'kernel'",
+ str(exc.exception))
+
+ def testAndroidBootInvalidSubnode(self):
+ """Test that binman rejects invalid subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_subnode.dts')
+ self.assertIn("Unexpected subnode 'bacon'",
+ str(exc.exception))
+
+ def testAndroidBootInvalidAddr(self):
+ """Test that binman rejects invalid addresses"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_invalid_addr.dts')
+ self.assertIn("kernel address 0xdeadbeefdafed00d does not fit in 32 bits",
+ str(exc.exception))
+
+ def testAndroidBootOversizedBootName(self):
+ """Test that binman rejects boot-name exceeding 16 chars"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_oversized_bootname.dts')
+ self.assertIn("boot-name is 38 bytes, maximum is 16",
+ str(exc.exception))
+
+ def testAndroidBootChonkyCells(self):
+ """Test that binman rejects >2 cell addresses"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_chonky_cells.dts')
+ self.assertIn("Property 'base' must contain one or two cells",
+ str(exc.exception))
+
+ def testAndroidBootV0(self):
+ """Test that binman can produce a plain legacy Android boot image"""
+ data = self._DoReadFile('android_boot_v0.dts')
+ header = struct.unpack_from('<8s10I16s512s32s', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80208000, header[2])
+ self.assertEqual(1, header[3])
+ self.assertEqual(0x81200000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80200100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(0, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'foo', header[12].split(b'\0', 1)[0])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'\0', b''),
+ header[13])
+
+ def testAndroidBootV0WithDTB(self):
+ """Test that binman rejects v0 abootimgs containing a dtb section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_dtb_in_v0.dts')
+ self.assertIn("Subnode 'dtb' requires header-version 2",
+ str(exc.exception))
+
+ def testAndroidBootV2(self):
+ """Test that binman can produce an Android boot image"""
+ data = self._DoReadFile('android_boot_v2.dts')
+ header = struct.unpack_from('<8s10I16s512s32s1024sIQIIQ', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80008000, header[2])
+ self.assertEqual(0, header[3])
+ self.assertEqual(0x81000000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80000100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(2, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'test-board', header[11].split(b'\0', 1)[0])
+ self.assertEqual(0, header[15])
+ self.assertEqual(0, header[16])
+ self.assertEqual(1660, header[17])
+ self.assertEqual(len(U_BOOT_DTB_DATA), header[18])
+ self.assertEqual(0x81f00000, header[19])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'', b'', b'',
+ U_BOOT_DTB_DATA), header[13])
+
+ cmdline = header[12].split(b'\0', 1)[0]
+ extra_cmdline = header[14].split(b'\0', 1)[0]
+ self.assertEqual(b"tests.. ", cmdline[-8:])
+ self.assertEqual(512, len(cmdline))
+ self.assertEqual(b'sup', extra_cmdline)
+
+ self.assertEqual(U_BOOT_DATA, data[0x800:0x800 + len(U_BOOT_DATA)])
+ self.assertEqual(U_BOOT_DTB_DATA,
+ data[0x1000:0x1000 + len(U_BOOT_DTB_DATA)])
+
+ def testAndroidBootV2PageSizeTooSmol(self):
+ """Test that binman rejects page sizes that are smaller than header size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_pagesize_too_smol.dts')
+ self.assertIn("page-size must fit the Android boot image header",
+ str(exc.exception))
+
+ def testAndroidBootV2MissingDTB(self):
+ """Test that binman rejects v2 abootimgs missing a DTB section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_missing_dtb.dts')
+ self.assertIn("Missing required subnode 'dtb'",
+ str(exc.exception))
+
+ def testAndroidBootV2VendorDt(self):
+ """Test that binman rejects v2 abootimgs with a vendor-dt section"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('android_boot_v2_vendor_dt.dts')
+ self.assertIn("Subnode 'vendor-dt' requires header-version 0",
+ str(exc.exception))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/android_boot_chonky_cells.dts b/tools/binman/test/android_boot_chonky_cells.dts
new file mode 100644
index 00000000000..7fdc1c86f6b
--- /dev/null
+++ b/tools/binman/test/android_boot_chonky_cells.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ base = <0xDEADBEEF 0xCAFED00D 0xDECAF>;
+
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_dtb_in_v0.dts b/tools/binman/test/android_boot_dtb_in_v0.dts
new file mode 100644
index 00000000000..24b91f9a33c
--- /dev/null
+++ b/tools/binman/test/android_boot_dtb_in_v0.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel {};
+ dtb {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_addr.dts b/tools/binman/test/android_boot_invalid_addr.dts
new file mode 100644
index 00000000000..0d7cb051921
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_addr.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel-offset = <0xDEADBEEF 0xCAFED00D>;
+
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_pagesize.dts b/tools/binman/test/android_boot_invalid_pagesize.dts
new file mode 100644
index 00000000000..01925187475
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_pagesize.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <2049>;
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_invalid_subnode.dts b/tools/binman/test/android_boot_invalid_subnode.dts
new file mode 100644
index 00000000000..747f95068be
--- /dev/null
+++ b/tools/binman/test/android_boot_invalid_subnode.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ kernel {};
+ bacon {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_missing_kernel.dts b/tools/binman/test/android_boot_missing_kernel.dts
new file mode 100644
index 00000000000..fe30eb5cbb3
--- /dev/null
+++ b/tools/binman/test/android_boot_missing_kernel.dts
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {};
+ };
+};
diff --git a/tools/binman/test/android_boot_oversized_bootname.dts b/tools/binman/test/android_boot_oversized_bootname.dts
new file mode 100644
index 00000000000..5f5564840f8
--- /dev/null
+++ b/tools/binman/test/android_boot_oversized_bootname.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ boot-name = "this is decidedly longer than 16 bytes";
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_unsupported_version.dts b/tools/binman/test/android_boot_unsupported_version.dts
new file mode 100644
index 00000000000..9843b368b3a
--- /dev/null
+++ b/tools/binman/test/android_boot_unsupported_version.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <1>;
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v0.dts b/tools/binman/test/android_boot_v0.dts
new file mode 100644
index 00000000000..18813ff3613
--- /dev/null
+++ b/tools/binman/test/android_boot_v0.dts
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that android-boot can be referenced before it's built */
+ collection {
+ content = <&abootimg>;
+ };
+
+ abootimg: android-boot {
+ header-version = <0>;
+ page-size = <0x800>;
+ base = <0x80200000>;
+ cmdline = "foo";
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v0_pagesize_too_smol.dts b/tools/binman/test/android_boot_v0_pagesize_too_smol.dts
new file mode 100644
index 00000000000..2c617f12a1e
--- /dev/null
+++ b/tools/binman/test/android_boot_v0_pagesize_too_smol.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <32>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2.dts b/tools/binman/test/android_boot_v2.dts
new file mode 100644
index 00000000000..55fab329443
--- /dev/null
+++ b/tools/binman/test/android_boot_v2.dts
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+#define CMDLINE(...) #__VA_ARGS__
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that android-boot can be referenced before it's built */
+ collection {
+ content = <&abootimg>;
+ };
+
+ abootimg: android-boot {
+ header-version = <2>;
+ page-size = <0x800>;
+ base = <0x80000000>;
+ kernel-offset = <0x00008000>;
+ ramdisk-offset = <0x01000000>;
+ tags-offset = <0x00000100>;
+ dtb-offset = <0x01f00000>;
+ boot-name = "test-board";
+ cmdline = CMDLINE(
+ This is a very long commandline that is sure to exceed the
+ 512 chars that is allotted to the cmdline and this should
+ spillover into extra_cmdline which is useful from a
+ function testing standpoint. Gosh, it sure it hard to come
+ up with enough filler text here to get over the 512 char
+ limit though, huh? Even for someone as loquacious as
+ myself. So anyway. How's your day going? I wrote a binman
+ functional test today. It was fun. Did you know that
+ binman is great. I like binman. I also like functional
+ tests.. sup);
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_missing_dtb.dts b/tools/binman/test/android_boot_v2_missing_dtb.dts
new file mode 100644
index 00000000000..bf7bee622c4
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_missing_dtb.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_pagesize_too_smol.dts b/tools/binman/test/android_boot_v2_pagesize_too_smol.dts
new file mode 100644
index 00000000000..0761ff20543
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_pagesize_too_smol.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ page-size = <32>;
+ kernel {};
+ };
+ };
+};
diff --git a/tools/binman/test/android_boot_v2_vendor_dt.dts b/tools/binman/test/android_boot_v2_vendor_dt.dts
new file mode 100644
index 00000000000..a7684d8492a
--- /dev/null
+++ b/tools/binman/test/android_boot_v2_vendor_dt.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ header-version = <2>;
+ kernel {};
+ dtb {};
+ vendor-dt {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 05/10] .gitignore: ignore binman-generated blobs
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
The new android_boot etype is causing these /comp*u-boot-nodtb artifacts
to be generated, which aren't currently picked up by any ignore rules.
This issue only occurs when doing in-tree builds, but it's still worth
fixing regardless.
Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Sam Day <me@samcday.com>
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index d57d3be0291..6e274de77ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,6 +84,7 @@ fit-dtb.blob*
/test/fdt_overlay/test-fdt-overlay-stacked.dtbo.S
/test/fdt_overlay/test-fdt-overlay.dtbo.S
capsule_esl_file
+/comp.*.u-boot-nodtb
#
# Generated include files
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 05/10] .gitignore: ignore binman-generated blobs
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
The new android_boot etype is causing these /comp*u-boot-nodtb artifacts
to be generated, which aren't currently picked up by any ignore rules.
This issue only occurs when doing in-tree builds, but it's still worth
fixing regardless.
Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Sam Day <me@samcday.com>
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index d57d3be0291..6e274de77ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,6 +84,7 @@ fit-dtb.blob*
/test/fdt_overlay/test-fdt-overlay-stacked.dtbo.S
/test/fdt_overlay/test-fdt-overlay.dtbo.S
capsule_esl_file
+/comp.*.u-boot-nodtb
#
# Generated include files
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 06/10] binman: android_boot: vendor-dt support
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
There's many Android bootloaders out there that expect non-standard
abootimgs. Samsung and Qualcomm, and likely many others. This quirk has
been codified in the widely used osm0sis fork/rewrite of mkbootimg.
Presumably, these vendor-specific abootimgs were conceived before the v2
format was widely available or specified. The evidence for this is their
hijacking of the header_version field to specify the length of a payload
that follows the main abootimg.
At least QCDT and DTBH (both of which will be implemented as binman
etypes in following commits) use this approach.
Link: https://github.com/osm0sis/mkbootimg
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 41 ++++++++++++++++++++++++++--
tools/binman/ftest.py | 15 ++++++++++
tools/binman/test/android_boot_vendor_dt.dts | 27 ++++++++++++++++++
3 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 8b72d90acc5..5cfa71ee981 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -33,6 +33,9 @@ class Entry_android_boot(Entry_section):
A kernel payload, optional ramdisk payload can be supplied. A DTB payload
can also be provided when header_version == v2.
+ Vendor-specific payloads are also supported. These are non-standard
+ v0 images with a special DT container format appended.
+
Properties / Entry arguments:
- header-version: Android boot image header version, must be 0 or 2,
defaults to 0
@@ -50,6 +53,7 @@ class Entry_android_boot(Entry_section):
- kernel: section containing the executable payload
- dtb: section containing the DTB payload, used by header version 2 only
- ramdisk: optional section containing a ramdisk payload
+ - vendor-dt: legacy vendor DT payload, used by header version 0 only
Example::
A v2 abootimg with control FDT placed in the DTB section:
@@ -112,6 +116,7 @@ class Entry_android_boot(Entry_section):
self.os_version = fdt_util.GetInt(self._node, 'os-version', 0)
self.boot_name = fdt_util.GetString(self._node, 'boot-name', '')
self.cmdline = fdt_util.GetString(self._node, 'cmdline', '')
+ self.vendor_dt_node = self._node.FindNode('vendor-dt')
if self.header_version not in (0, 2):
self.Raise('Only Android boot image header versions 0 and 2 are '
@@ -132,9 +137,15 @@ class Entry_android_boot(Entry_section):
self.Raise('page-size must fit the Android boot image header')
if 'dtb' not in self._entries:
self.Raise("Missing required subnode 'dtb'")
+ if self.vendor_dt_node:
+ self.Raise("Subnode 'vendor-dt' requires header-version 0")
def ReadEntries(self):
for node in self._node.subnodes:
+ if node.name == 'vendor-dt':
+ self._ReadVendorDtEntries(node)
+ continue
+
if node.name not in ('kernel', 'ramdisk', 'dtb'):
self.Raise("Unexpected subnode '%s'" % node.name)
@@ -145,6 +156,14 @@ class Entry_android_boot(Entry_section):
entry.SetPrefix(self._name_prefix)
self._entries[node.name] = entry
+ def _ReadVendorDtEntries(self, vendor_dt_node):
+ entry = Entry.Create(self, vendor_dt_node, etype='section',
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[vendor_dt_node.name] = entry
+
def _GetIntCells(self, propname, default):
prop = self._node.props.get(propname)
if not prop:
@@ -198,8 +217,14 @@ class Entry_android_boot(Entry_section):
return default
return entry.GetData(required)
+ def _BuildVendorDt(self, required):
+ if not self.vendor_dt_node:
+ return b''
+ return self._GetEntryData('vendor-dt', required)
+
def _BuildV0SectionData(self, required):
kernel = self._GetEntryData('kernel', required)
+ vendor_dt = self._BuildVendorDt(required)
ramdisk = self._GetEntryData('ramdisk', required, b'')
if not required and (kernel is None or vendor_dt is None or
ramdisk is None):
@@ -211,8 +236,18 @@ class Entry_android_boot(Entry_section):
BOOT_ARGS_SIZE)
boot_id_payloads = [kernel, ramdisk, b'']
+ if self.vendor_dt_node:
+ boot_id_payloads.append(vendor_dt)
image_id = self._BootId(*boot_id_payloads)
+ overloaded_header_version = self.header_version
+ if self.vendor_dt_node:
+ # vendor DTs overload the header_version field to store the length
+ # of the appended payload. Hopefully AOSP abootimg never progresses
+ # to v8192-ish or we might have some real specificity problems on
+ # our hands.
+ overloaded_header_version = len(vendor_dt)
+
header = struct.pack(BOOT_IMAGE_HEADER_V0,
BOOT_MAGIC,
len(kernel),
@@ -223,7 +258,7 @@ class Entry_android_boot(Entry_section):
0, # second_offset
self._GetAddr(self.tags_offset, 'tags'),
self.page_size,
- self.header_version,
+ overloaded_header_version,
self.os_version,
boot_name,
cmdline,
@@ -233,6 +268,7 @@ class Entry_android_boot(Entry_section):
image += self.PadToAlignment(header, self.page_size)
image += self.PadToAlignment(kernel, self.page_size)
image += self.PadToAlignment(ramdisk, self.page_size)
+ image += self.PadToAlignment(vendor_dt, self.page_size)
return bytes(image)
@@ -240,7 +276,8 @@ class Entry_android_boot(Entry_section):
kernel = self._GetEntryData('kernel', required)
dtb = self._GetEntryData('dtb', required)
ramdisk = self._GetEntryData('ramdisk', required, b'')
- if not required and (kernel is None or dtb is None):
+ if not required and (kernel is None or dtb is None or
+ ramdisk is None):
return None
boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 71740205c72..bbdcb721eca 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5746,6 +5746,21 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Subnode 'vendor-dt' requires header-version 0",
str(exc.exception))
+ def testAndroidBootVendorDt(self):
+ """Test that android-boot can embed an arbitrary vendor-dt section"""
+ data = self._DoReadFile('android_boot_vendor_dt.dts')
+ header = struct.unpack_from('<8s10I16s512s32s', data, 0)
+ page_size = 2048
+ vendor_dt_offset = page_size * 3
+ vendor_dt = b'howdy'
+ self.assertEqual(len(vendor_dt), header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'\0', b'',
+ vendor_dt), header[13])
+ self.assertEqual(vendor_dt + b'\0' * (page_size - len(vendor_dt)),
+ data[vendor_dt_offset:vendor_dt_offset + page_size])
+ self.assertEqual(vendor_dt_offset + page_size, len(data))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/android_boot_vendor_dt.dts b/tools/binman/test/android_boot_vendor_dt.dts
new file mode 100644
index 00000000000..194396a0880
--- /dev/null
+++ b/tools/binman/test/android_boot_vendor_dt.dts
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+ binman {
+ android-boot {
+ header-version = <0>;
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ vendor-dt {
+ text {
+ size = <5>;
+ text = "howdy";
+ };
+ };
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 06/10] binman: android_boot: vendor-dt support
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
There's many Android bootloaders out there that expect non-standard
abootimgs. Samsung and Qualcomm, and likely many others. This quirk has
been codified in the widely used osm0sis fork/rewrite of mkbootimg.
Presumably, these vendor-specific abootimgs were conceived before the v2
format was widely available or specified. The evidence for this is their
hijacking of the header_version field to specify the length of a payload
that follows the main abootimg.
At least QCDT and DTBH (both of which will be implemented as binman
etypes in following commits) use this approach.
Link: https://github.com/osm0sis/mkbootimg
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 41 ++++++++++++++++++++++++++--
tools/binman/ftest.py | 15 ++++++++++
tools/binman/test/android_boot_vendor_dt.dts | 27 ++++++++++++++++++
3 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 8b72d90acc5..5cfa71ee981 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -33,6 +33,9 @@ class Entry_android_boot(Entry_section):
A kernel payload, optional ramdisk payload can be supplied. A DTB payload
can also be provided when header_version == v2.
+ Vendor-specific payloads are also supported. These are non-standard
+ v0 images with a special DT container format appended.
+
Properties / Entry arguments:
- header-version: Android boot image header version, must be 0 or 2,
defaults to 0
@@ -50,6 +53,7 @@ class Entry_android_boot(Entry_section):
- kernel: section containing the executable payload
- dtb: section containing the DTB payload, used by header version 2 only
- ramdisk: optional section containing a ramdisk payload
+ - vendor-dt: legacy vendor DT payload, used by header version 0 only
Example::
A v2 abootimg with control FDT placed in the DTB section:
@@ -112,6 +116,7 @@ class Entry_android_boot(Entry_section):
self.os_version = fdt_util.GetInt(self._node, 'os-version', 0)
self.boot_name = fdt_util.GetString(self._node, 'boot-name', '')
self.cmdline = fdt_util.GetString(self._node, 'cmdline', '')
+ self.vendor_dt_node = self._node.FindNode('vendor-dt')
if self.header_version not in (0, 2):
self.Raise('Only Android boot image header versions 0 and 2 are '
@@ -132,9 +137,15 @@ class Entry_android_boot(Entry_section):
self.Raise('page-size must fit the Android boot image header')
if 'dtb' not in self._entries:
self.Raise("Missing required subnode 'dtb'")
+ if self.vendor_dt_node:
+ self.Raise("Subnode 'vendor-dt' requires header-version 0")
def ReadEntries(self):
for node in self._node.subnodes:
+ if node.name == 'vendor-dt':
+ self._ReadVendorDtEntries(node)
+ continue
+
if node.name not in ('kernel', 'ramdisk', 'dtb'):
self.Raise("Unexpected subnode '%s'" % node.name)
@@ -145,6 +156,14 @@ class Entry_android_boot(Entry_section):
entry.SetPrefix(self._name_prefix)
self._entries[node.name] = entry
+ def _ReadVendorDtEntries(self, vendor_dt_node):
+ entry = Entry.Create(self, vendor_dt_node, etype='section',
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[vendor_dt_node.name] = entry
+
def _GetIntCells(self, propname, default):
prop = self._node.props.get(propname)
if not prop:
@@ -198,8 +217,14 @@ class Entry_android_boot(Entry_section):
return default
return entry.GetData(required)
+ def _BuildVendorDt(self, required):
+ if not self.vendor_dt_node:
+ return b''
+ return self._GetEntryData('vendor-dt', required)
+
def _BuildV0SectionData(self, required):
kernel = self._GetEntryData('kernel', required)
+ vendor_dt = self._BuildVendorDt(required)
ramdisk = self._GetEntryData('ramdisk', required, b'')
if not required and (kernel is None or vendor_dt is None or
ramdisk is None):
@@ -211,8 +236,18 @@ class Entry_android_boot(Entry_section):
BOOT_ARGS_SIZE)
boot_id_payloads = [kernel, ramdisk, b'']
+ if self.vendor_dt_node:
+ boot_id_payloads.append(vendor_dt)
image_id = self._BootId(*boot_id_payloads)
+ overloaded_header_version = self.header_version
+ if self.vendor_dt_node:
+ # vendor DTs overload the header_version field to store the length
+ # of the appended payload. Hopefully AOSP abootimg never progresses
+ # to v8192-ish or we might have some real specificity problems on
+ # our hands.
+ overloaded_header_version = len(vendor_dt)
+
header = struct.pack(BOOT_IMAGE_HEADER_V0,
BOOT_MAGIC,
len(kernel),
@@ -223,7 +258,7 @@ class Entry_android_boot(Entry_section):
0, # second_offset
self._GetAddr(self.tags_offset, 'tags'),
self.page_size,
- self.header_version,
+ overloaded_header_version,
self.os_version,
boot_name,
cmdline,
@@ -233,6 +268,7 @@ class Entry_android_boot(Entry_section):
image += self.PadToAlignment(header, self.page_size)
image += self.PadToAlignment(kernel, self.page_size)
image += self.PadToAlignment(ramdisk, self.page_size)
+ image += self.PadToAlignment(vendor_dt, self.page_size)
return bytes(image)
@@ -240,7 +276,8 @@ class Entry_android_boot(Entry_section):
kernel = self._GetEntryData('kernel', required)
dtb = self._GetEntryData('dtb', required)
ramdisk = self._GetEntryData('ramdisk', required, b'')
- if not required and (kernel is None or dtb is None):
+ if not required and (kernel is None or dtb is None or
+ ramdisk is None):
return None
boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 71740205c72..bbdcb721eca 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5746,6 +5746,21 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Subnode 'vendor-dt' requires header-version 0",
str(exc.exception))
+ def testAndroidBootVendorDt(self):
+ """Test that android-boot can embed an arbitrary vendor-dt section"""
+ data = self._DoReadFile('android_boot_vendor_dt.dts')
+ header = struct.unpack_from('<8s10I16s512s32s', data, 0)
+ page_size = 2048
+ vendor_dt_offset = page_size * 3
+ vendor_dt = b'howdy'
+ self.assertEqual(len(vendor_dt), header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'\0', b'',
+ vendor_dt), header[13])
+ self.assertEqual(vendor_dt + b'\0' * (page_size - len(vendor_dt)),
+ data[vendor_dt_offset:vendor_dt_offset + page_size])
+ self.assertEqual(vendor_dt_offset + page_size, len(data))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/android_boot_vendor_dt.dts b/tools/binman/test/android_boot_vendor_dt.dts
new file mode 100644
index 00000000000..194396a0880
--- /dev/null
+++ b/tools/binman/test/android_boot_vendor_dt.dts
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+ binman {
+ android-boot {
+ header-version = <0>;
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ vendor-dt {
+ text {
+ size = <5>;
+ text = "howdy";
+ };
+ };
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 07/10] binman: Add QCDT support
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
This vendor-specific format is used by many bootloaders on older qcom
SoCs, such as msm8916. It's a container for N FDTs. Each one is
contained in a record that includes metadata about the platform/variant
it targets. The previous bootloader picks the "right" record based on
this metadata.
This initial impl targets a streamlined v2 path, with no support for
different versions or multiple qcom,msm-id/qcom,board-id tuples. If/when
that's needed it will be implemented in a follow-up.
In the following commit, support for DTBH will also be introduced.
Because QCDT and DTBH share a lot of common behaviour, this commit also
introduces a Entry_Android_vendor_dt_table base class.
This impl was based on the lk2nd/CAF LK dtbTool script.
Link: https://github.com/msm8916-mainline/lk2nd/blob/main/lk2nd/scripts/dtbTool
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/android_vendor_dt_table.py | 104 +++++++++++++++++++++
tools/binman/etype/android_boot.py | 31 ++++++
tools/binman/etype/qcdt.py | 80 ++++++++++++++++
tools/binman/ftest.py | 96 +++++++++++++++++++
tools/binman/test/qcdt.dts | 36 +++++++
tools/binman/test/qcdt_bad_msm_id.dts | 17 ++++
tools/binman/test/qcdt_invalid_pagesize.dts | 12 +++
tools/binman/test/qcdt_missing_msm_id.dts | 12 +++
tools/binman/test/qcdt_missing_payload.dts | 14 +++
tools/binman/test/qcdt_missing_subnodes.dts | 13 +++
tools/binman/test/qcdt_multiple_dtbs.dts | 34 +++++++
tools/binman/test/qcdt_page_size_from_abootimg.dts | 33 +++++++
tools/binman/test/qcdt_zero_pagesize.dts | 12 +++
13 files changed, 494 insertions(+)
diff --git a/tools/binman/android_vendor_dt_table.py b/tools/binman/android_vendor_dt_table.py
new file mode 100644
index 00000000000..91b785f274e
--- /dev/null
+++ b/tools/binman/android_vendor_dt_table.py
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: GPL-2.0+
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+
+
+class Entry_Android_vendor_dt_table(Entry_section):
+ """Base class for legacy Android vendor DT table entries"""
+
+ @staticmethod
+ def _DtbEntryName(node):
+ return '_dtb_%s' % node.name
+
+ def ReadNode(self):
+ super().ReadNode()
+ self._page_size = fdt_util.GetInt(self._node, 'page-size')
+ if (self._page_size is not None and
+ (self._page_size <= 0 or
+ self._page_size & (self._page_size - 1))):
+ self.Raise('page-size must be a power of two')
+
+ def _GetPayloadSubnodes(self, node):
+ return [subnode for subnode in node.subnodes
+ if not self.IsSpecialSubnode(subnode)]
+
+ def ReadEntries(self):
+ for node in self._node.subnodes:
+ if self.IsSpecialSubnode(node):
+ continue
+
+ payloads = self._GetPayloadSubnodes(node)
+ if len(payloads) > 1:
+ self.Raise("subnode '%s': must contain exactly one DTB "
+ "payload subnode" % node.name)
+ if not payloads:
+ continue
+
+ entry = Entry.Create(self, payloads[0],
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[self._DtbEntryName(node)] = entry
+
+ def _GetPageSize(self):
+ if self._page_size is not None:
+ return self._page_size
+
+ section = self.section
+ while section:
+ if section.etype == 'android-boot':
+ return section.page_size
+ section = section.section
+
+ return 2048
+
+ def _GetU32Cells(self, node, propname):
+ prop = node.props.get(propname)
+ if not prop:
+ self.Raise("subnode '%s': Missing required property '%s'" %
+ (node.name, propname))
+
+ values = prop.value if isinstance(prop.value, list) else [prop.value]
+ return [fdt_util.fdt32_to_cpu(value) for value in values]
+
+ def _GetU32Tuple(self, node, propname, width):
+ values = self._GetU32Cells(node, propname)
+ if len(values) != width:
+ self.Raise("subnode '%s': Property '%s' must contain exactly "
+ "%d cells" % (node.name, propname, width))
+
+ return tuple(values)
+
+ def _GetDtbData(self, node, required):
+ entry = self._entries.get(self._DtbEntryName(node))
+ if not entry:
+ self.Raise("subnode '%s': Missing required DTB payload subnode" %
+ node.name)
+
+ data = entry.GetData(required)
+ if data is None and not required:
+ return None
+
+ return data
+
+ def _GetDtbRecordData(self, node, required):
+ return self._GetDtbData(node, required)
+
+ def _ReadDtbRecords(self, required, read_record):
+ records = []
+ for node in self._node.subnodes:
+ if self.IsSpecialSubnode(node):
+ continue
+
+ data = self._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+ records.append(read_record(node, data))
+
+ if not records:
+ self.Raise('Missing required DTB subnodes')
+
+ return records
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 5cfa71ee981..57900e3d523 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -102,6 +102,37 @@ class Entry_android_boot(Entry_section):
};
};
};
+
+ Example::
+ A legacy QCDT abootimg, the kind msm8916 bootloaders expect:
+
+ android-boot {
+ base = <0x80000000>;
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <206 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
"""
def ReadNode(self):
diff --git a/tools/binman/etype/qcdt.py b/tools/binman/etype/qcdt.py
new file mode 100644
index 00000000000..ccf566af29f
--- /dev/null
+++ b/tools/binman/etype/qcdt.py
@@ -0,0 +1,80 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Qualcomm Android device tree tables
+
+import struct
+
+from binman.android_vendor_dt_table import Entry_Android_vendor_dt_table
+
+
+QCDT_MAGIC = b'QCDT'
+QCDT_VERSION = 2
+QCDT_HEADER = '<4sII'
+QCDT_HEADER_SIZE = struct.calcsize(QCDT_HEADER)
+QCDT_RECORD = '<IIIIII'
+QCDT_RECORD_SIZE = struct.calcsize(QCDT_RECORD)
+
+
+class Entry_qcdt(Entry_Android_vendor_dt_table):
+ """Qualcomm Android device tree table
+
+ This creates a QCDT table, the legacy device-tree table format used by
+ some Qualcomm Android bootloaders.
+
+ Properties / Entry arguments:
+ - page-size: QCDT page size, defaults to 2048, unless there's a parent
+ android-boot node with an explicit page-size
+
+ This entry uses the following subnodes:
+ - dtb-*: DTB records, each containing qcom,msm-id, qcom,board-id and
+ exactly one DTB payload entry
+
+ Example::
+
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <206 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ """
+
+ def _GetDtbRecordData(self, node, required):
+ msm_id = self._GetU32Tuple(node, 'qcom,msm-id', 2)
+ board_id = self._GetU32Tuple(node, 'qcom,board-id', 2)
+ data = super()._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+
+ return (msm_id, board_id, data)
+
+ def _ReadDtbRecord(self, node, data):
+ return data
+
+ def BuildSectionData(self, required):
+ page_size = self._GetPageSize()
+ dtbs = self._ReadDtbRecords(required, self._ReadDtbRecord)
+ if dtbs is None:
+ return None
+
+ size = QCDT_HEADER_SIZE + len(dtbs) * QCDT_RECORD_SIZE
+ dtb_offset = self.AlignUp(size, page_size)
+ records = []
+ payloads = bytearray()
+ for msm_id, board_id, dtb in dtbs:
+ platform_id, soc_rev = msm_id
+ variant_id, board_hw_subtype = board_id
+ dtb_size = self.AlignUp(len(dtb), page_size)
+ records.append((platform_id, variant_id, board_hw_subtype,
+ soc_rev, dtb_offset, dtb_size))
+ payloads += self.PadToAlignment(dtb, page_size)
+ dtb_offset += dtb_size
+
+ qcdt = bytearray(struct.pack(QCDT_HEADER, QCDT_MAGIC, QCDT_VERSION,
+ len(records)))
+ for record in records:
+ qcdt += struct.pack(QCDT_RECORD, *record)
+
+ return self.PadToAlignment(qcdt, page_size) + bytes(payloads)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index bbdcb721eca..b18d584c688 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5761,6 +5761,102 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
data[vendor_dt_offset:vendor_dt_offset + page_size])
self.assertEqual(vendor_dt_offset + page_size, len(data))
+ def testQcdt(self):
+ """Test that binman can produce a QCDT container"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'qcdt.dts', use_real_dtb=True)
+
+ dtb_size = tools.align(len(dtb_data), 0x800)
+
+ self.assertEqual(b'QCDT', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((0xce, 0xce08ff01, 1, 0, 0x800, dtb_size),
+ struct.unpack_from('<IIIIII', data, 12))
+ self.assertEqual((0xcf, 0xce08ff02, 2, 1, 0x800 + dtb_size,
+ dtb_size), struct.unpack_from('<IIIIII', data, 36))
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, 0x800)[0])
+ self.assertEqual(dtb_data, data[0x800:0x800 + len(dtb_data)])
+ self.assertEqual(dtb_data, data[0x800 + dtb_size:0x800 + dtb_size +
+ len(dtb_data)])
+
+ def testQcdtPageSizeFromParent(self):
+ """Test that QCDT inherits page-size from parent android-boot node"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'qcdt_page_size_from_abootimg.dts')
+
+ # header+kernel are aligned to 4096, vendor-dt follows after that.
+ vendor_dt_offset = 4096*2
+
+ self.assertEqual(b'QCDT', data[vendor_dt_offset:vendor_dt_offset + 4])
+ self.assertEqual((4096, 4096),
+ struct.unpack_from('<16xII', data,
+ vendor_dt_offset + 12))
+
+ def testQcdtBadMsmId(self):
+ """Test that QCDT rejects invalid msm-id properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_bad_msm_id.dts')
+ self.assertIn("Property 'qcom,msm-id' must contain exactly 2 cells",
+ str(exc.exception))
+
+ def testQcdtMissingMsmId(self):
+ """Test that QCDT rejects missing qcom,msm-id"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_msm_id.dts')
+ self.assertIn("Missing required property 'qcom,msm-id'",
+ str(exc.exception))
+
+ def testQcdtMissingDTBPayload(self):
+ """Test that QCDT rejects missing DTB payload"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_payload.dts')
+ self.assertIn("Missing required DTB payload subnode",
+ str(exc.exception))
+
+ def testQcdtMissingSubnodes(self):
+ """Test that QCDT rejects missing dtb subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
+ def testQcdtInvalidPageSize(self):
+ """Test that QCDT rejects invalid page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testQcdtZeroPageSize(self):
+ """Test that QCDT rejects zero page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_zero_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testQcdtMultipleDTBs(self):
+ """Test that QCDT handles multiple embedded DTBs"""
+ data = self._DoReadFile('qcdt_multiple_dtbs.dts')
+
+ page_size = 0x100
+ payload_size = page_size
+ payload_pad = tools.get_bytes(0, page_size - 1)
+
+ self.assertEqual(b'QCDT', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((0xce, 0xce08ff01, 1, 0, page_size,
+ payload_size),
+ struct.unpack_from('<IIIIII', data, 12))
+ self.assertEqual((0xcf, 0xce08ff02, 3, 2,
+ page_size + payload_size, payload_size),
+ struct.unpack_from('<IIIIII', data, 36))
+ self.assertEqual(tools.get_bytes(0x11, 1) + payload_pad,
+ data[page_size:page_size + payload_size])
+ self.assertEqual(tools.get_bytes(0x22, 1) + payload_pad,
+ data[page_size + payload_size:
+ page_size + payload_size * 2])
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/qcdt.dts b/tools/binman/test/qcdt.dts
new file mode 100644
index 00000000000..cdbd1a85379
--- /dev/null
+++ b/tools/binman/test/qcdt.dts
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that qcdt can be referenced before it's built */
+ collection {
+ content = <&qcdt>;
+ };
+
+ qcdt: qcdt {
+ hash {
+ };
+
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+
+ dtb-1 {
+ qcom,msm-id = <0xcf 1>;
+ qcom,board-id = <0xce08ff02 2>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_bad_msm_id.dts b/tools/binman/test/qcdt_bad_msm_id.dts
new file mode 100644
index 00000000000..1c3d4ec1a2e
--- /dev/null
+++ b/tools/binman/test/qcdt_bad_msm_id.dts
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0xce 0 1>;
+ qcom,board-id = <0xce08ff01 1>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_invalid_pagesize.dts b/tools/binman/test/qcdt_invalid_pagesize.dts
new file mode 100644
index 00000000000..d8eff98c7ac
--- /dev/null
+++ b/tools/binman/test/qcdt_invalid_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ page-size = <2049>;
+ dtb-0 {};
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_msm_id.dts b/tools/binman/test/qcdt_missing_msm_id.dts
new file mode 100644
index 00000000000..3eda1acb6c2
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_msm_id.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ dtb-0 {
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_payload.dts b/tools/binman/test/qcdt_missing_payload.dts
new file mode 100644
index 00000000000..ae2c41cbcf8
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_payload.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_subnodes.dts b/tools/binman/test/qcdt_missing_subnodes.dts
new file mode 100644
index 00000000000..4b1af9570b6
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_subnodes.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_multiple_dtbs.dts b/tools/binman/test/qcdt_multiple_dtbs.dts
new file mode 100644
index 00000000000..db04c122a6e
--- /dev/null
+++ b/tools/binman/test/qcdt_multiple_dtbs.dts
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ page-size = <0x100>;
+
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ fill {
+ size = <1>;
+ fill-byte = [11];
+ };
+ };
+
+ dtb-1 {
+ qcom,msm-id = <0xcf 2>;
+ qcom,board-id = <0xce08ff02 3>;
+
+ fill {
+ size = <1>;
+ fill-byte = [22];
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_page_size_from_abootimg.dts b/tools/binman/test/qcdt_page_size_from_abootimg.dts
new file mode 100644
index 00000000000..557863d8834
--- /dev/null
+++ b/tools/binman/test/qcdt_page_size_from_abootimg.dts
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ android-boot {
+ page-size = <4096>;
+
+ kernel {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0 0>;
+ qcom,board-id = <0 0>;
+
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_zero_pagesize.dts b/tools/binman/test/qcdt_zero_pagesize.dts
new file mode 100644
index 00000000000..8ca802719f0
--- /dev/null
+++ b/tools/binman/test/qcdt_zero_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ page-size = <0>;
+ dtb-0 {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 07/10] binman: Add QCDT support
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
This vendor-specific format is used by many bootloaders on older qcom
SoCs, such as msm8916. It's a container for N FDTs. Each one is
contained in a record that includes metadata about the platform/variant
it targets. The previous bootloader picks the "right" record based on
this metadata.
This initial impl targets a streamlined v2 path, with no support for
different versions or multiple qcom,msm-id/qcom,board-id tuples. If/when
that's needed it will be implemented in a follow-up.
In the following commit, support for DTBH will also be introduced.
Because QCDT and DTBH share a lot of common behaviour, this commit also
introduces a Entry_Android_vendor_dt_table base class.
This impl was based on the lk2nd/CAF LK dtbTool script.
Link: https://github.com/msm8916-mainline/lk2nd/blob/main/lk2nd/scripts/dtbTool
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/android_vendor_dt_table.py | 104 +++++++++++++++++++++
tools/binman/etype/android_boot.py | 31 ++++++
tools/binman/etype/qcdt.py | 80 ++++++++++++++++
tools/binman/ftest.py | 96 +++++++++++++++++++
tools/binman/test/qcdt.dts | 36 +++++++
tools/binman/test/qcdt_bad_msm_id.dts | 17 ++++
tools/binman/test/qcdt_invalid_pagesize.dts | 12 +++
tools/binman/test/qcdt_missing_msm_id.dts | 12 +++
tools/binman/test/qcdt_missing_payload.dts | 14 +++
tools/binman/test/qcdt_missing_subnodes.dts | 13 +++
tools/binman/test/qcdt_multiple_dtbs.dts | 34 +++++++
tools/binman/test/qcdt_page_size_from_abootimg.dts | 33 +++++++
tools/binman/test/qcdt_zero_pagesize.dts | 12 +++
13 files changed, 494 insertions(+)
diff --git a/tools/binman/android_vendor_dt_table.py b/tools/binman/android_vendor_dt_table.py
new file mode 100644
index 00000000000..91b785f274e
--- /dev/null
+++ b/tools/binman/android_vendor_dt_table.py
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: GPL-2.0+
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+
+
+class Entry_Android_vendor_dt_table(Entry_section):
+ """Base class for legacy Android vendor DT table entries"""
+
+ @staticmethod
+ def _DtbEntryName(node):
+ return '_dtb_%s' % node.name
+
+ def ReadNode(self):
+ super().ReadNode()
+ self._page_size = fdt_util.GetInt(self._node, 'page-size')
+ if (self._page_size is not None and
+ (self._page_size <= 0 or
+ self._page_size & (self._page_size - 1))):
+ self.Raise('page-size must be a power of two')
+
+ def _GetPayloadSubnodes(self, node):
+ return [subnode for subnode in node.subnodes
+ if not self.IsSpecialSubnode(subnode)]
+
+ def ReadEntries(self):
+ for node in self._node.subnodes:
+ if self.IsSpecialSubnode(node):
+ continue
+
+ payloads = self._GetPayloadSubnodes(node)
+ if len(payloads) > 1:
+ self.Raise("subnode '%s': must contain exactly one DTB "
+ "payload subnode" % node.name)
+ if not payloads:
+ continue
+
+ entry = Entry.Create(self, payloads[0],
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[self._DtbEntryName(node)] = entry
+
+ def _GetPageSize(self):
+ if self._page_size is not None:
+ return self._page_size
+
+ section = self.section
+ while section:
+ if section.etype == 'android-boot':
+ return section.page_size
+ section = section.section
+
+ return 2048
+
+ def _GetU32Cells(self, node, propname):
+ prop = node.props.get(propname)
+ if not prop:
+ self.Raise("subnode '%s': Missing required property '%s'" %
+ (node.name, propname))
+
+ values = prop.value if isinstance(prop.value, list) else [prop.value]
+ return [fdt_util.fdt32_to_cpu(value) for value in values]
+
+ def _GetU32Tuple(self, node, propname, width):
+ values = self._GetU32Cells(node, propname)
+ if len(values) != width:
+ self.Raise("subnode '%s': Property '%s' must contain exactly "
+ "%d cells" % (node.name, propname, width))
+
+ return tuple(values)
+
+ def _GetDtbData(self, node, required):
+ entry = self._entries.get(self._DtbEntryName(node))
+ if not entry:
+ self.Raise("subnode '%s': Missing required DTB payload subnode" %
+ node.name)
+
+ data = entry.GetData(required)
+ if data is None and not required:
+ return None
+
+ return data
+
+ def _GetDtbRecordData(self, node, required):
+ return self._GetDtbData(node, required)
+
+ def _ReadDtbRecords(self, required, read_record):
+ records = []
+ for node in self._node.subnodes:
+ if self.IsSpecialSubnode(node):
+ continue
+
+ data = self._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+ records.append(read_record(node, data))
+
+ if not records:
+ self.Raise('Missing required DTB subnodes')
+
+ return records
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 5cfa71ee981..57900e3d523 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -102,6 +102,37 @@ class Entry_android_boot(Entry_section):
};
};
};
+
+ Example::
+ A legacy QCDT abootimg, the kind msm8916 bootloaders expect:
+
+ android-boot {
+ base = <0x80000000>;
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <206 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
"""
def ReadNode(self):
diff --git a/tools/binman/etype/qcdt.py b/tools/binman/etype/qcdt.py
new file mode 100644
index 00000000000..ccf566af29f
--- /dev/null
+++ b/tools/binman/etype/qcdt.py
@@ -0,0 +1,80 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Qualcomm Android device tree tables
+
+import struct
+
+from binman.android_vendor_dt_table import Entry_Android_vendor_dt_table
+
+
+QCDT_MAGIC = b'QCDT'
+QCDT_VERSION = 2
+QCDT_HEADER = '<4sII'
+QCDT_HEADER_SIZE = struct.calcsize(QCDT_HEADER)
+QCDT_RECORD = '<IIIIII'
+QCDT_RECORD_SIZE = struct.calcsize(QCDT_RECORD)
+
+
+class Entry_qcdt(Entry_Android_vendor_dt_table):
+ """Qualcomm Android device tree table
+
+ This creates a QCDT table, the legacy device-tree table format used by
+ some Qualcomm Android bootloaders.
+
+ Properties / Entry arguments:
+ - page-size: QCDT page size, defaults to 2048, unless there's a parent
+ android-boot node with an explicit page-size
+
+ This entry uses the following subnodes:
+ - dtb-*: DTB records, each containing qcom,msm-id, qcom,board-id and
+ exactly one DTB payload entry
+
+ Example::
+
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <206 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ """
+
+ def _GetDtbRecordData(self, node, required):
+ msm_id = self._GetU32Tuple(node, 'qcom,msm-id', 2)
+ board_id = self._GetU32Tuple(node, 'qcom,board-id', 2)
+ data = super()._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+
+ return (msm_id, board_id, data)
+
+ def _ReadDtbRecord(self, node, data):
+ return data
+
+ def BuildSectionData(self, required):
+ page_size = self._GetPageSize()
+ dtbs = self._ReadDtbRecords(required, self._ReadDtbRecord)
+ if dtbs is None:
+ return None
+
+ size = QCDT_HEADER_SIZE + len(dtbs) * QCDT_RECORD_SIZE
+ dtb_offset = self.AlignUp(size, page_size)
+ records = []
+ payloads = bytearray()
+ for msm_id, board_id, dtb in dtbs:
+ platform_id, soc_rev = msm_id
+ variant_id, board_hw_subtype = board_id
+ dtb_size = self.AlignUp(len(dtb), page_size)
+ records.append((platform_id, variant_id, board_hw_subtype,
+ soc_rev, dtb_offset, dtb_size))
+ payloads += self.PadToAlignment(dtb, page_size)
+ dtb_offset += dtb_size
+
+ qcdt = bytearray(struct.pack(QCDT_HEADER, QCDT_MAGIC, QCDT_VERSION,
+ len(records)))
+ for record in records:
+ qcdt += struct.pack(QCDT_RECORD, *record)
+
+ return self.PadToAlignment(qcdt, page_size) + bytes(payloads)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index bbdcb721eca..b18d584c688 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5761,6 +5761,102 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
data[vendor_dt_offset:vendor_dt_offset + page_size])
self.assertEqual(vendor_dt_offset + page_size, len(data))
+ def testQcdt(self):
+ """Test that binman can produce a QCDT container"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'qcdt.dts', use_real_dtb=True)
+
+ dtb_size = tools.align(len(dtb_data), 0x800)
+
+ self.assertEqual(b'QCDT', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((0xce, 0xce08ff01, 1, 0, 0x800, dtb_size),
+ struct.unpack_from('<IIIIII', data, 12))
+ self.assertEqual((0xcf, 0xce08ff02, 2, 1, 0x800 + dtb_size,
+ dtb_size), struct.unpack_from('<IIIIII', data, 36))
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, 0x800)[0])
+ self.assertEqual(dtb_data, data[0x800:0x800 + len(dtb_data)])
+ self.assertEqual(dtb_data, data[0x800 + dtb_size:0x800 + dtb_size +
+ len(dtb_data)])
+
+ def testQcdtPageSizeFromParent(self):
+ """Test that QCDT inherits page-size from parent android-boot node"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'qcdt_page_size_from_abootimg.dts')
+
+ # header+kernel are aligned to 4096, vendor-dt follows after that.
+ vendor_dt_offset = 4096*2
+
+ self.assertEqual(b'QCDT', data[vendor_dt_offset:vendor_dt_offset + 4])
+ self.assertEqual((4096, 4096),
+ struct.unpack_from('<16xII', data,
+ vendor_dt_offset + 12))
+
+ def testQcdtBadMsmId(self):
+ """Test that QCDT rejects invalid msm-id properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_bad_msm_id.dts')
+ self.assertIn("Property 'qcom,msm-id' must contain exactly 2 cells",
+ str(exc.exception))
+
+ def testQcdtMissingMsmId(self):
+ """Test that QCDT rejects missing qcom,msm-id"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_msm_id.dts')
+ self.assertIn("Missing required property 'qcom,msm-id'",
+ str(exc.exception))
+
+ def testQcdtMissingDTBPayload(self):
+ """Test that QCDT rejects missing DTB payload"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_payload.dts')
+ self.assertIn("Missing required DTB payload subnode",
+ str(exc.exception))
+
+ def testQcdtMissingSubnodes(self):
+ """Test that QCDT rejects missing dtb subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_missing_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
+ def testQcdtInvalidPageSize(self):
+ """Test that QCDT rejects invalid page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testQcdtZeroPageSize(self):
+ """Test that QCDT rejects zero page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('qcdt_zero_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testQcdtMultipleDTBs(self):
+ """Test that QCDT handles multiple embedded DTBs"""
+ data = self._DoReadFile('qcdt_multiple_dtbs.dts')
+
+ page_size = 0x100
+ payload_size = page_size
+ payload_pad = tools.get_bytes(0, page_size - 1)
+
+ self.assertEqual(b'QCDT', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((0xce, 0xce08ff01, 1, 0, page_size,
+ payload_size),
+ struct.unpack_from('<IIIIII', data, 12))
+ self.assertEqual((0xcf, 0xce08ff02, 3, 2,
+ page_size + payload_size, payload_size),
+ struct.unpack_from('<IIIIII', data, 36))
+ self.assertEqual(tools.get_bytes(0x11, 1) + payload_pad,
+ data[page_size:page_size + payload_size])
+ self.assertEqual(tools.get_bytes(0x22, 1) + payload_pad,
+ data[page_size + payload_size:
+ page_size + payload_size * 2])
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/qcdt.dts b/tools/binman/test/qcdt.dts
new file mode 100644
index 00000000000..cdbd1a85379
--- /dev/null
+++ b/tools/binman/test/qcdt.dts
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ /* confirm that qcdt can be referenced before it's built */
+ collection {
+ content = <&qcdt>;
+ };
+
+ qcdt: qcdt {
+ hash {
+ };
+
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ u-boot-dtb {
+ };
+ };
+
+ dtb-1 {
+ qcom,msm-id = <0xcf 1>;
+ qcom,board-id = <0xce08ff02 2>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_bad_msm_id.dts b/tools/binman/test/qcdt_bad_msm_id.dts
new file mode 100644
index 00000000000..1c3d4ec1a2e
--- /dev/null
+++ b/tools/binman/test/qcdt_bad_msm_id.dts
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0xce 0 1>;
+ qcom,board-id = <0xce08ff01 1>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_invalid_pagesize.dts b/tools/binman/test/qcdt_invalid_pagesize.dts
new file mode 100644
index 00000000000..d8eff98c7ac
--- /dev/null
+++ b/tools/binman/test/qcdt_invalid_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ page-size = <2049>;
+ dtb-0 {};
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_msm_id.dts b/tools/binman/test/qcdt_missing_msm_id.dts
new file mode 100644
index 00000000000..3eda1acb6c2
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_msm_id.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ dtb-0 {
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_payload.dts b/tools/binman/test/qcdt_missing_payload.dts
new file mode 100644
index 00000000000..ae2c41cbcf8
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_payload.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_missing_subnodes.dts b/tools/binman/test/qcdt_missing_subnodes.dts
new file mode 100644
index 00000000000..4b1af9570b6
--- /dev/null
+++ b/tools/binman/test/qcdt_missing_subnodes.dts
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_multiple_dtbs.dts b/tools/binman/test/qcdt_multiple_dtbs.dts
new file mode 100644
index 00000000000..db04c122a6e
--- /dev/null
+++ b/tools/binman/test/qcdt_multiple_dtbs.dts
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ qcdt {
+ page-size = <0x100>;
+
+ dtb-0 {
+ qcom,msm-id = <0xce 0>;
+ qcom,board-id = <0xce08ff01 1>;
+
+ fill {
+ size = <1>;
+ fill-byte = [11];
+ };
+ };
+
+ dtb-1 {
+ qcom,msm-id = <0xcf 2>;
+ qcom,board-id = <0xce08ff02 3>;
+
+ fill {
+ size = <1>;
+ fill-byte = [22];
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_page_size_from_abootimg.dts b/tools/binman/test/qcdt_page_size_from_abootimg.dts
new file mode 100644
index 00000000000..557863d8834
--- /dev/null
+++ b/tools/binman/test/qcdt_page_size_from_abootimg.dts
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ android-boot {
+ page-size = <4096>;
+
+ kernel {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ qcdt {
+ dtb-0 {
+ qcom,msm-id = <0 0>;
+ qcom,board-id = <0 0>;
+
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/qcdt_zero_pagesize.dts b/tools/binman/test/qcdt_zero_pagesize.dts
new file mode 100644
index 00000000000..8ca802719f0
--- /dev/null
+++ b/tools/binman/test/qcdt_zero_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ qcdt {
+ page-size = <0>;
+ dtb-0 {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 08/10] binman: Add DTBH support
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
DTBH, used by Samsung S-BOOT bootloaders, is similar to QCDT. That is,
it's a multi-record container that carries vendor-specific magic values
to assist the previous bootloader in picking the appropriate FDT for the
booting device.
dtbTool-exynos was used as a reference for this implementation.
Link: https://github.com/dsankouski/dtbtool-exynos
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 23 +++++
tools/binman/etype/dtbh.py | 108 +++++++++++++++++++++
tools/binman/ftest.py | 106 ++++++++++++++++++++
tools/binman/test/dtbh.dts | 22 +++++
tools/binman/test/dtbh_bad_model_info.dts | 20 ++++
tools/binman/test/dtbh_invalid_pagesize.dts | 12 +++
tools/binman/test/dtbh_missing_model_info.dts | 16 +++
tools/binman/test/dtbh_missing_payload.dts | 15 +++
tools/binman/test/dtbh_missing_subnodes.dts | 10 ++
tools/binman/test/dtbh_multi.dts | 29 ++++++
tools/binman/test/dtbh_multiple_dtbs.dts | 22 +++++
tools/binman/test/dtbh_page_size_from_abootimg.dts | 30 ++++++
tools/binman/test/dtbh_special_subnodes.dts | 11 +++
13 files changed, 424 insertions(+)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 57900e3d523..d6bc4774dab 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -127,6 +127,29 @@ class Entry_android_boot(Entry_section):
qcom,msm-id = <206 0>;
qcom,board-id = <0xce08ff01 1>;
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
+
+ Example::
+ A legacy DTBH abootimg, the kind some Samsung bootloaders expect:
+
+ android-boot {
+ kernel {
+ u-boot-nodtb {
+ };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
u-boot-dtb {
};
};
diff --git a/tools/binman/etype/dtbh.py b/tools/binman/etype/dtbh.py
new file mode 100644
index 00000000000..29e7fbec0c9
--- /dev/null
+++ b/tools/binman/etype/dtbh.py
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Samsung Android DTBH tables
+
+import struct
+
+from binman.android_vendor_dt_table import Entry_Android_vendor_dt_table
+from dtoc import fdt_util
+
+
+DTBH_MAGIC = b'DTBH'
+DTBH_VERSION = 2
+DTBH_PLATFORM_CODE_DEF = 0x50a6
+DTBH_SUBTYPE_CODE_DEF = 0x217584da
+DTBH_HEADER = '<4sII'
+DTBH_HEADER_SIZE = struct.calcsize(DTBH_HEADER)
+DTBH_RECORD = '<8I'
+DTBH_RECORD_SIZE = struct.calcsize(DTBH_RECORD)
+# Fixed per-record "space delimiter" used by dtbTool-exynos.
+# It's unclear what the 0x20 magic actually means, if anything.
+DTBH_RECORD_SPACE = 0x20
+# Zero end-of-table marker emitted after all DTBH records.
+DTBH_EOT = '<I'
+DTBH_EOT_SIZE = struct.calcsize(DTBH_EOT)
+
+
+class Entry_dtbh(Entry_Android_vendor_dt_table):
+ """Samsung Android device tree table
+
+ This creates a DTBH table, the legacy device-tree table format used by
+ some Samsung Android bootloaders.
+
+ Properties / Entry arguments:
+ - page-size: DTBH page size, defaults to the parent android-boot page
+ size or 2048 when used elsewhere
+ - platform: DTBH platform code, defaults to 0x50a6
+ - subtype: DTBH subtype code, defaults to 0x217584da
+
+ This entry uses the following subnodes:
+ - dtb-*: DTB records, each containing model_info-chip,
+ model_info-hw_rev, model_info-hw_rev_end and exactly one DTB payload
+ entry
+
+ Each dtb-* subnode must contain these properties:
+ - model_info-chip
+ - model_info-hw_rev
+ - model_info-hw_rev_end
+
+ Example::
+
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ """
+
+ def ReadNode(self):
+ super().ReadNode()
+ self.platform = fdt_util.GetInt(self._node, 'platform',
+ DTBH_PLATFORM_CODE_DEF)
+ self.subtype = fdt_util.GetInt(self._node, 'subtype',
+ DTBH_SUBTYPE_CODE_DEF)
+
+ def _GetDtbRecordData(self, node, required):
+ chip = self._GetU32Tuple(node, 'model_info-chip', 1)[0]
+ hw_rev = self._GetU32Tuple(node, 'model_info-hw_rev', 1)[0]
+ hw_rev_end = self._GetU32Tuple(node, 'model_info-hw_rev_end', 1)[0]
+ data = super()._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+
+ return (chip, hw_rev, hw_rev_end, data)
+
+ def _ReadDtbRecord(self, node, data):
+ chip, hw_rev, hw_rev_end, data = data
+ return (chip, self.platform, self.subtype, hw_rev, hw_rev_end, data)
+
+ def BuildSectionData(self, required):
+ page_size = self._GetPageSize()
+ dtbs = self._ReadDtbRecords(required, self._ReadDtbRecord)
+ if dtbs is None:
+ return None
+
+ size = (DTBH_HEADER_SIZE + len(dtbs) * DTBH_RECORD_SIZE +
+ DTBH_EOT_SIZE)
+ header_size = self.AlignUp(size, page_size)
+ dtb_offset = header_size
+ records = []
+ payloads = bytearray()
+ for chip, platform, subtype, hw_rev, hw_rev_end, dtb in dtbs:
+ dtb_size = self.AlignUp(len(dtb), page_size)
+ records.append((chip, platform, subtype, hw_rev, hw_rev_end,
+ dtb_offset, dtb_size, DTBH_RECORD_SPACE))
+ payloads += self.PadToAlignment(dtb, page_size)
+ dtb_offset += dtb_size
+
+ dtbh = bytearray(struct.pack(DTBH_HEADER, DTBH_MAGIC, DTBH_VERSION,
+ len(records)))
+ for record in records:
+ dtbh += struct.pack(DTBH_RECORD, *record)
+ dtbh += struct.pack(DTBH_EOT, 0)
+
+ return self.PadToAlignment(dtbh, page_size) + bytes(payloads)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index b18d584c688..9e92a40ef07 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5857,6 +5857,112 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
data[page_size + payload_size:
page_size + payload_size * 2])
+ def testDtbh(self):
+ """Test that binman can produce a DTBH container"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh.dts', use_real_dtb=True)
+
+ dtb_size = tools.align(len(dtb_data), 0x800)
+
+ self.assertEqual(b'DTBH', data[:4])
+ self.assertEqual((2, 1), struct.unpack_from('<II', data, 4))
+ self.assertEqual((7870, 0x50a6, 0x217584da, 123, 321, 0x800,
+ dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 12))
+ self.assertEqual(0, struct.unpack_from('<I', data, 44)[0])
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, 0x800)[0])
+ self.assertEqual(dtb_data, data[0x800:0x800 + len(dtb_data)])
+
+ def testDtbhMulti(self):
+ """Test that DTBH handles explicit properties and multiple DTBs"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh_multi.dts', use_real_dtb=True)
+
+ page_size = 0x100
+ dtb_size = tools.align(len(dtb_data), page_size)
+
+ self.assertEqual(b'DTBH', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((7870, 0x1234, 0x56789abc, 123, 321, page_size,
+ dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 12))
+ self.assertEqual((7871, 0x1234, 0x56789abc, 124, 322,
+ page_size + dtb_size, dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 44))
+ self.assertEqual(0, struct.unpack_from('<I', data, 76)[0])
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, page_size)[0])
+ self.assertEqual(dtb_data, data[page_size:page_size + len(dtb_data)])
+ self.assertEqual(dtb_data,
+ data[page_size + dtb_size:page_size + dtb_size +
+ len(dtb_data)])
+
+ def testDtbhPageSizeFromParent(self):
+ """Test that DTBH inherits page-size from parent android-boot node"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh_page_size_from_abootimg.dts', use_real_dtb=True)
+
+ # header+kernel are aligned to 4096, vendor-dt follows after that.
+ vendor_dt_offset = 4096 * 2
+ dtb_size = tools.align(len(dtb_data), 4096)
+
+ self.assertEqual(b'DTBH', data[vendor_dt_offset:vendor_dt_offset+4])
+ self.assertEqual((4096, dtb_size),
+ struct.unpack_from('<20x2I', data,
+ vendor_dt_offset + 12))
+
+ def testDtbhBadModelInfo(self):
+ """Test that DTBH rejects invalid model_info properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFileDtb('dtbh_bad_model_info.dts',
+ use_real_dtb=True)
+ self.assertIn("subnode 'dtb-0': Property 'model_info-chip' must "
+ "contain exactly 1 cells", str(exc.exception))
+
+ def testDtbhMissingModelInfo(self):
+ """Test that DTBH rejects missing model_info properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFileDtb('dtbh_missing_model_info.dts',
+ use_real_dtb=True)
+ self.assertIn("subnode 'dtb-0': Missing required property "
+ "'model_info-chip'", str(exc.exception))
+
+ def testDtbhInvalidPageSize(self):
+ """Test that DTBH rejects invalid page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testDtbhMultipleDTBs(self):
+ """Test that DTBH rejects multiple embedded DTBs"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_multiple_dtbs.dts')
+ self.assertIn("must contain exactly one DTB",
+ str(exc.exception))
+
+ def testDtbhMissingDTBPayload(self):
+ """Test that DTBH rejects missing DTB payload"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_missing_payload.dts')
+ self.assertIn("Missing required DTB payload subnode",
+ str(exc.exception))
+
+ def testDtbhMissingSubnodes(self):
+ """Test that DTBH rejects missing dtb subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_missing_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
+ def testDtbhOnlySpecialSubnodes(self):
+ """Test that DTBH rejects special-only subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_special_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/dtbh.dts b/tools/binman/test/dtbh.dts
new file mode 100644
index 00000000000..911ca1954d8
--- /dev/null
+++ b/tools/binman/test/dtbh.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ /* confirm that dtbh can be referenced before it's built */
+ collection {
+ content = <&dtbh>;
+ };
+
+ dtbh: dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_bad_model_info.dts b/tools/binman/test/dtbh_bad_model_info.dts
new file mode 100644
index 00000000000..9179f7c3761
--- /dev/null
+++ b/tools/binman/test/dtbh_bad_model_info.dts
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <2>;
+ #size-cells = <1>;
+
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870 1>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_invalid_pagesize.dts b/tools/binman/test/dtbh_invalid_pagesize.dts
new file mode 100644
index 00000000000..ec0534dc6a8
--- /dev/null
+++ b/tools/binman/test/dtbh_invalid_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ page-size = <2049>;
+ dtb-0 {};
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_model_info.dts b/tools/binman/test/dtbh_missing_model_info.dts
new file mode 100644
index 00000000000..9c92d9cf39a
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_model_info.dts
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_payload.dts b/tools/binman/test/dtbh_missing_payload.dts
new file mode 100644
index 00000000000..1ea5014ad36
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_payload.dts
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_subnodes.dts b/tools/binman/test/dtbh_missing_subnodes.dts
new file mode 100644
index 00000000000..6d3eac173d5
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_subnodes.dts
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_multi.dts b/tools/binman/test/dtbh_multi.dts
new file mode 100644
index 00000000000..9a89bbbada4
--- /dev/null
+++ b/tools/binman/test/dtbh_multi.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ page-size = <0x100>;
+ platform = <0x1234>;
+ subtype = <0x56789abc>;
+
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {};
+ };
+
+ dtb-1 {
+ model_info-chip = <7871>;
+ model_info-hw_rev = <124>;
+ model_info-hw_rev_end = <322>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_multiple_dtbs.dts b/tools/binman/test/dtbh_multiple_dtbs.dts
new file mode 100644
index 00000000000..c991564fab0
--- /dev/null
+++ b/tools/binman/test/dtbh_multiple_dtbs.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ fill {
+ size = <1>;
+ };
+ text {
+ text = "x";
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_page_size_from_abootimg.dts b/tools/binman/test/dtbh_page_size_from_abootimg.dts
new file mode 100644
index 00000000000..1512b8b5e8b
--- /dev/null
+++ b/tools/binman/test/dtbh_page_size_from_abootimg.dts
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <4096>;
+
+ kernel {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_special_subnodes.dts b/tools/binman/test/dtbh_special_subnodes.dts
new file mode 100644
index 00000000000..ce698ce00f2
--- /dev/null
+++ b/tools/binman/test/dtbh_special_subnodes.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ hash {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 08/10] binman: Add DTBH support
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
DTBH, used by Samsung S-BOOT bootloaders, is similar to QCDT. That is,
it's a multi-record container that carries vendor-specific magic values
to assist the previous bootloader in picking the appropriate FDT for the
booting device.
dtbTool-exynos was used as a reference for this implementation.
Link: https://github.com/dsankouski/dtbtool-exynos
Signed-off-by: Sam Day <me@samcday.com>
---
tools/binman/etype/android_boot.py | 23 +++++
tools/binman/etype/dtbh.py | 108 +++++++++++++++++++++
tools/binman/ftest.py | 106 ++++++++++++++++++++
tools/binman/test/dtbh.dts | 22 +++++
tools/binman/test/dtbh_bad_model_info.dts | 20 ++++
tools/binman/test/dtbh_invalid_pagesize.dts | 12 +++
tools/binman/test/dtbh_missing_model_info.dts | 16 +++
tools/binman/test/dtbh_missing_payload.dts | 15 +++
tools/binman/test/dtbh_missing_subnodes.dts | 10 ++
tools/binman/test/dtbh_multi.dts | 29 ++++++
tools/binman/test/dtbh_multiple_dtbs.dts | 22 +++++
tools/binman/test/dtbh_page_size_from_abootimg.dts | 30 ++++++
tools/binman/test/dtbh_special_subnodes.dts | 11 +++
13 files changed, 424 insertions(+)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
index 57900e3d523..d6bc4774dab 100644
--- a/tools/binman/etype/android_boot.py
+++ b/tools/binman/etype/android_boot.py
@@ -127,6 +127,29 @@ class Entry_android_boot(Entry_section):
qcom,msm-id = <206 0>;
qcom,board-id = <0xce08ff01 1>;
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
+
+ Example::
+ A legacy DTBH abootimg, the kind some Samsung bootloaders expect:
+
+ android-boot {
+ kernel {
+ u-boot-nodtb {
+ };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
u-boot-dtb {
};
};
diff --git a/tools/binman/etype/dtbh.py b/tools/binman/etype/dtbh.py
new file mode 100644
index 00000000000..29e7fbec0c9
--- /dev/null
+++ b/tools/binman/etype/dtbh.py
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Samsung Android DTBH tables
+
+import struct
+
+from binman.android_vendor_dt_table import Entry_Android_vendor_dt_table
+from dtoc import fdt_util
+
+
+DTBH_MAGIC = b'DTBH'
+DTBH_VERSION = 2
+DTBH_PLATFORM_CODE_DEF = 0x50a6
+DTBH_SUBTYPE_CODE_DEF = 0x217584da
+DTBH_HEADER = '<4sII'
+DTBH_HEADER_SIZE = struct.calcsize(DTBH_HEADER)
+DTBH_RECORD = '<8I'
+DTBH_RECORD_SIZE = struct.calcsize(DTBH_RECORD)
+# Fixed per-record "space delimiter" used by dtbTool-exynos.
+# It's unclear what the 0x20 magic actually means, if anything.
+DTBH_RECORD_SPACE = 0x20
+# Zero end-of-table marker emitted after all DTBH records.
+DTBH_EOT = '<I'
+DTBH_EOT_SIZE = struct.calcsize(DTBH_EOT)
+
+
+class Entry_dtbh(Entry_Android_vendor_dt_table):
+ """Samsung Android device tree table
+
+ This creates a DTBH table, the legacy device-tree table format used by
+ some Samsung Android bootloaders.
+
+ Properties / Entry arguments:
+ - page-size: DTBH page size, defaults to the parent android-boot page
+ size or 2048 when used elsewhere
+ - platform: DTBH platform code, defaults to 0x50a6
+ - subtype: DTBH subtype code, defaults to 0x217584da
+
+ This entry uses the following subnodes:
+ - dtb-*: DTB records, each containing model_info-chip,
+ model_info-hw_rev, model_info-hw_rev_end and exactly one DTB payload
+ entry
+
+ Each dtb-* subnode must contain these properties:
+ - model_info-chip
+ - model_info-hw_rev
+ - model_info-hw_rev_end
+
+ Example::
+
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ """
+
+ def ReadNode(self):
+ super().ReadNode()
+ self.platform = fdt_util.GetInt(self._node, 'platform',
+ DTBH_PLATFORM_CODE_DEF)
+ self.subtype = fdt_util.GetInt(self._node, 'subtype',
+ DTBH_SUBTYPE_CODE_DEF)
+
+ def _GetDtbRecordData(self, node, required):
+ chip = self._GetU32Tuple(node, 'model_info-chip', 1)[0]
+ hw_rev = self._GetU32Tuple(node, 'model_info-hw_rev', 1)[0]
+ hw_rev_end = self._GetU32Tuple(node, 'model_info-hw_rev_end', 1)[0]
+ data = super()._GetDtbRecordData(node, required)
+ if data is None and not required:
+ return None
+
+ return (chip, hw_rev, hw_rev_end, data)
+
+ def _ReadDtbRecord(self, node, data):
+ chip, hw_rev, hw_rev_end, data = data
+ return (chip, self.platform, self.subtype, hw_rev, hw_rev_end, data)
+
+ def BuildSectionData(self, required):
+ page_size = self._GetPageSize()
+ dtbs = self._ReadDtbRecords(required, self._ReadDtbRecord)
+ if dtbs is None:
+ return None
+
+ size = (DTBH_HEADER_SIZE + len(dtbs) * DTBH_RECORD_SIZE +
+ DTBH_EOT_SIZE)
+ header_size = self.AlignUp(size, page_size)
+ dtb_offset = header_size
+ records = []
+ payloads = bytearray()
+ for chip, platform, subtype, hw_rev, hw_rev_end, dtb in dtbs:
+ dtb_size = self.AlignUp(len(dtb), page_size)
+ records.append((chip, platform, subtype, hw_rev, hw_rev_end,
+ dtb_offset, dtb_size, DTBH_RECORD_SPACE))
+ payloads += self.PadToAlignment(dtb, page_size)
+ dtb_offset += dtb_size
+
+ dtbh = bytearray(struct.pack(DTBH_HEADER, DTBH_MAGIC, DTBH_VERSION,
+ len(records)))
+ for record in records:
+ dtbh += struct.pack(DTBH_RECORD, *record)
+ dtbh += struct.pack(DTBH_EOT, 0)
+
+ return self.PadToAlignment(dtbh, page_size) + bytes(payloads)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index b18d584c688..9e92a40ef07 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5857,6 +5857,112 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
data[page_size + payload_size:
page_size + payload_size * 2])
+ def testDtbh(self):
+ """Test that binman can produce a DTBH container"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh.dts', use_real_dtb=True)
+
+ dtb_size = tools.align(len(dtb_data), 0x800)
+
+ self.assertEqual(b'DTBH', data[:4])
+ self.assertEqual((2, 1), struct.unpack_from('<II', data, 4))
+ self.assertEqual((7870, 0x50a6, 0x217584da, 123, 321, 0x800,
+ dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 12))
+ self.assertEqual(0, struct.unpack_from('<I', data, 44)[0])
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, 0x800)[0])
+ self.assertEqual(dtb_data, data[0x800:0x800 + len(dtb_data)])
+
+ def testDtbhMulti(self):
+ """Test that DTBH handles explicit properties and multiple DTBs"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh_multi.dts', use_real_dtb=True)
+
+ page_size = 0x100
+ dtb_size = tools.align(len(dtb_data), page_size)
+
+ self.assertEqual(b'DTBH', data[:4])
+ self.assertEqual((2, 2), struct.unpack_from('<II', data, 4))
+ self.assertEqual((7870, 0x1234, 0x56789abc, 123, 321, page_size,
+ dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 12))
+ self.assertEqual((7871, 0x1234, 0x56789abc, 124, 322,
+ page_size + dtb_size, dtb_size, 0x20),
+ struct.unpack_from('<8I', data, 44))
+ self.assertEqual(0, struct.unpack_from('<I', data, 76)[0])
+ self.assertEqual(0xd00dfeed,
+ struct.unpack_from('>I', data, page_size)[0])
+ self.assertEqual(dtb_data, data[page_size:page_size + len(dtb_data)])
+ self.assertEqual(dtb_data,
+ data[page_size + dtb_size:page_size + dtb_size +
+ len(dtb_data)])
+
+ def testDtbhPageSizeFromParent(self):
+ """Test that DTBH inherits page-size from parent android-boot node"""
+ data, dtb_data, _map, _dtb = self._DoReadFileDtb(
+ 'dtbh_page_size_from_abootimg.dts', use_real_dtb=True)
+
+ # header+kernel are aligned to 4096, vendor-dt follows after that.
+ vendor_dt_offset = 4096 * 2
+ dtb_size = tools.align(len(dtb_data), 4096)
+
+ self.assertEqual(b'DTBH', data[vendor_dt_offset:vendor_dt_offset+4])
+ self.assertEqual((4096, dtb_size),
+ struct.unpack_from('<20x2I', data,
+ vendor_dt_offset + 12))
+
+ def testDtbhBadModelInfo(self):
+ """Test that DTBH rejects invalid model_info properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFileDtb('dtbh_bad_model_info.dts',
+ use_real_dtb=True)
+ self.assertIn("subnode 'dtb-0': Property 'model_info-chip' must "
+ "contain exactly 1 cells", str(exc.exception))
+
+ def testDtbhMissingModelInfo(self):
+ """Test that DTBH rejects missing model_info properties"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFileDtb('dtbh_missing_model_info.dts',
+ use_real_dtb=True)
+ self.assertIn("subnode 'dtb-0': Missing required property "
+ "'model_info-chip'", str(exc.exception))
+
+ def testDtbhInvalidPageSize(self):
+ """Test that DTBH rejects invalid page-size"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_invalid_pagesize.dts')
+ self.assertIn("page-size must be a power of two",
+ str(exc.exception))
+
+ def testDtbhMultipleDTBs(self):
+ """Test that DTBH rejects multiple embedded DTBs"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_multiple_dtbs.dts')
+ self.assertIn("must contain exactly one DTB",
+ str(exc.exception))
+
+ def testDtbhMissingDTBPayload(self):
+ """Test that DTBH rejects missing DTB payload"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_missing_payload.dts')
+ self.assertIn("Missing required DTB payload subnode",
+ str(exc.exception))
+
+ def testDtbhMissingSubnodes(self):
+ """Test that DTBH rejects missing dtb subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_missing_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
+ def testDtbhOnlySpecialSubnodes(self):
+ """Test that DTBH rejects special-only subnodes"""
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('dtbh_special_subnodes.dts')
+ self.assertIn("Missing required DTB subnodes",
+ str(exc.exception))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/dtbh.dts b/tools/binman/test/dtbh.dts
new file mode 100644
index 00000000000..911ca1954d8
--- /dev/null
+++ b/tools/binman/test/dtbh.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ /* confirm that dtbh can be referenced before it's built */
+ collection {
+ content = <&dtbh>;
+ };
+
+ dtbh: dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_bad_model_info.dts b/tools/binman/test/dtbh_bad_model_info.dts
new file mode 100644
index 00000000000..9179f7c3761
--- /dev/null
+++ b/tools/binman/test/dtbh_bad_model_info.dts
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <2>;
+ #size-cells = <1>;
+
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870 1>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_invalid_pagesize.dts b/tools/binman/test/dtbh_invalid_pagesize.dts
new file mode 100644
index 00000000000..ec0534dc6a8
--- /dev/null
+++ b/tools/binman/test/dtbh_invalid_pagesize.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ page-size = <2049>;
+ dtb-0 {};
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_model_info.dts b/tools/binman/test/dtbh_missing_model_info.dts
new file mode 100644
index 00000000000..9c92d9cf39a
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_model_info.dts
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_payload.dts b/tools/binman/test/dtbh_missing_payload.dts
new file mode 100644
index 00000000000..1ea5014ad36
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_payload.dts
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_missing_subnodes.dts b/tools/binman/test/dtbh_missing_subnodes.dts
new file mode 100644
index 00000000000..6d3eac173d5
--- /dev/null
+++ b/tools/binman/test/dtbh_missing_subnodes.dts
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_multi.dts b/tools/binman/test/dtbh_multi.dts
new file mode 100644
index 00000000000..9a89bbbada4
--- /dev/null
+++ b/tools/binman/test/dtbh_multi.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ page-size = <0x100>;
+ platform = <0x1234>;
+ subtype = <0x56789abc>;
+
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <123>;
+ model_info-hw_rev_end = <321>;
+
+ u-boot-dtb {};
+ };
+
+ dtb-1 {
+ model_info-chip = <7871>;
+ model_info-hw_rev = <124>;
+ model_info-hw_rev_end = <322>;
+
+ u-boot-dtb {};
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_multiple_dtbs.dts b/tools/binman/test/dtbh_multiple_dtbs.dts
new file mode 100644
index 00000000000..c991564fab0
--- /dev/null
+++ b/tools/binman/test/dtbh_multiple_dtbs.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ fill {
+ size = <1>;
+ };
+ text {
+ text = "x";
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_page_size_from_abootimg.dts b/tools/binman/test/dtbh_page_size_from_abootimg.dts
new file mode 100644
index 00000000000..1512b8b5e8b
--- /dev/null
+++ b/tools/binman/test/dtbh_page_size_from_abootimg.dts
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ android-boot {
+ page-size = <4096>;
+
+ kernel {
+ fill {
+ size = <1>;
+ };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <6>;
+ model_info-hw_rev_end = <6>;
+
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/dtbh_special_subnodes.dts b/tools/binman/test/dtbh_special_subnodes.dts
new file mode 100644
index 00000000000..ce698ce00f2
--- /dev/null
+++ b/tools/binman/test/dtbh_special_subnodes.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ binman {
+ dtbh {
+ hash {};
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v3 09/10] arch: arm: exynos: add j7xelte binman config
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
Note that, as of this commit, j7xelte does not yet exist in U-Boot's
upstream DTS tree. It was accepted into next so it should appear here
eventually.
Link: https://lore.kernel.org/all/177209522223.26390.6219893536178441080.b4-ty@kernel.org/
Signed-off-by: Sam Day <me@samcday.com>
---
arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
new file mode 100644
index 00000000000..0cd4c12a2e4
--- /dev/null
+++ b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/ {
+ binman {
+ filename = "u-boot-samsung-j7xelte.img";
+
+ android-boot {
+ /* S-BOOT ignores offsets, so they are not provided here */
+ kernel {
+ u-boot-nodtb { };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <0>;
+ model_info-hw_rev_end = <255>;
+
+ u-boot-dtb { };
+ };
+ };
+ };
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 09/10] arch: arm: exynos: add j7xelte binman config
@ 2026-06-10 1:27 ` Sam Day
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
Note that, as of this commit, j7xelte does not yet exist in U-Boot's
upstream DTS tree. It was accepted into next so it should appear here
eventually.
Link: https://lore.kernel.org/all/177209522223.26390.6219893536178441080.b4-ty@kernel.org/
Signed-off-by: Sam Day <me@samcday.com>
---
arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
new file mode 100644
index 00000000000..0cd4c12a2e4
--- /dev/null
+++ b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/ {
+ binman {
+ filename = "u-boot-samsung-j7xelte.img";
+
+ android-boot {
+ /* S-BOOT ignores offsets, so they are not provided here */
+ kernel {
+ u-boot-nodtb { };
+ };
+
+ vendor-dt {
+ dtbh {
+ dtb-0 {
+ model_info-chip = <7870>;
+ model_info-hw_rev = <0>;
+ model_info-hw_rev_end = <255>;
+
+ u-boot-dtb { };
+ };
+ };
+ };
+ };
+ };
+};
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v3 09/10] arch: arm: exynos: add j7xelte binman config
2026-06-10 1:27 ` Sam Day
(?)
@ 2026-06-13 11:45 ` Kaustabh Chakraborty
-1 siblings, 0 replies; 25+ messages in thread
From: Kaustabh Chakraborty @ 2026-06-13 11:45 UTC (permalink / raw)
To: me, u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin
On 2026-06-10 11:27 +10:00, Sam Day via B4 Relay wrote:
> From: Sam Day <me@samcday.com>
>
> Note that, as of this commit, j7xelte does not yet exist in U-Boot's
> upstream DTS tree. It was accepted into next so it should appear here
> eventually.
>
> Link: https://lore.kernel.org/all/177209522223.26390.6219893536178441080.b4-ty@kernel.org/
> Signed-off-by: Sam Day <me@samcday.com>
> ---
> arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi | 26 ++++++++++++++++++++++++++
> 1 file changed, 26 insertions(+)
>
> diff --git a/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
> new file mode 100644
> index 00000000000..0cd4c12a2e4
> --- /dev/null
> +++ b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
> @@ -0,0 +1,26 @@
> +// SPDX-License-Identifier: GPL-2.0
No copyright notice?
> +
> +/ {
> + binman {
> + filename = "u-boot-samsung-j7xelte.img";
> +
> + android-boot {
> + /* S-BOOT ignores offsets, so they are not provided here */
> + kernel {
> + u-boot-nodtb { };
> + };
> +
> + vendor-dt {
> + dtbh {
> + dtb-0 {
> + model_info-chip = <7870>;
> + model_info-hw_rev = <0>;
> + model_info-hw_rev_end = <255>;
> +
> + u-boot-dtb { };
> + };
> + };
> + };
> + };
> + };
> +};
^ permalink raw reply [flat|nested] 25+ messages in thread* Re: [PATCH v3 09/10] arch: arm: exynos: add j7xelte binman config
2026-06-10 1:27 ` Sam Day
(?)
(?)
@ 2026-06-13 12:03 ` Kaustabh Chakraborty
-1 siblings, 0 replies; 25+ messages in thread
From: Kaustabh Chakraborty @ 2026-06-13 12:03 UTC (permalink / raw)
To: me, u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin
On 2026-06-10 11:27 +10:00, Sam Day via B4 Relay wrote:
> From: Sam Day <me@samcday.com>
>
> Note that, as of this commit, j7xelte does not yet exist in U-Boot's
> upstream DTS tree. It was accepted into next so it should appear here
> eventually.
The description seems lacking. A concise documentation for this build
process would be nice, and the same must also be mentioned in
`doc/board/samsung/exynos-mobile.rst`.
>
> Link: https://lore.kernel.org/all/177209522223.26390.6219893536178441080.b4-ty@kernel.org/
> Signed-off-by: Sam Day <me@samcday.com>
> ---
> arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi | 26 ++++++++++++++++++++++++++
> 1 file changed, 26 insertions(+)
>
> diff --git a/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
> new file mode 100644
> index 00000000000..0cd4c12a2e4
> --- /dev/null
> +++ b/arch/arm/dts/exynos7870-j7xelte-u-boot.dtsi
> @@ -0,0 +1,26 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +/ {
> + binman {
> + filename = "u-boot-samsung-j7xelte.img";
> +
> + android-boot {
> + /* S-BOOT ignores offsets, so they are not provided here */
> + kernel {
> + u-boot-nodtb { };
> + };
> +
> + vendor-dt {
> + dtbh {
> + dtb-0 {
> + model_info-chip = <7870>;
> + model_info-hw_rev = <0>;
> + model_info-hw_rev_end = <255>;
> +
> + u-boot-dtb { };
> + };
> + };
> + };
> + };
> + };
> +};
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v3 10/10] configs: exynos-mobile: pull in binman
2026-06-10 1:27 ` Sam Day
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
-1 siblings, 0 replies; 25+ messages in thread
From: Sam Day @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
All Exynos7 mobile devices need abootimg+DTBH wrapping to be bootable
from the fused S-BOOT bootloader. So we enable binman everywhere.
Ideally we'll have full coverage of these devices with binman configs in
the overlay DTSIs but, for now, we ensure there's a placeholder node.
That way no existing devices will regress their cleanly buildable state.
Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Sam Day <me@samcday.com>
---
arch/arm/dts/exynos-mobile.dtsi | 5 +++++
arch/arm/mach-exynos/Kconfig | 1 +
configs/exynos-mobile_defconfig | 1 +
3 files changed, 7 insertions(+)
diff --git a/arch/arm/dts/exynos-mobile.dtsi b/arch/arm/dts/exynos-mobile.dtsi
new file mode 100644
index 00000000000..9982e996993
--- /dev/null
+++ b/arch/arm/dts/exynos-mobile.dtsi
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0
+/ {
+ binman {
+ };
+};
diff --git a/arch/arm/mach-exynos/Kconfig b/arch/arm/mach-exynos/Kconfig
index b084b7284aa..d4ae182030d 100644
--- a/arch/arm/mach-exynos/Kconfig
+++ b/arch/arm/mach-exynos/Kconfig
@@ -255,6 +255,7 @@ endif
config TARGET_EXYNOS_MOBILE
bool "Samsung Exynos Generic Boards (for mobile devices)"
select ARM64
+ select BINMAN
select BOARD_EARLY_INIT_F
select CLK_EXYNOS
select LINUX_KERNEL_IMAGE_HEADER
diff --git a/configs/exynos-mobile_defconfig b/configs/exynos-mobile_defconfig
index 45ab998bb45..fcaff3d1608 100644
--- a/configs/exynos-mobile_defconfig
+++ b/configs/exynos-mobile_defconfig
@@ -32,6 +32,7 @@ CONFIG_EFI_PARTITION=y
CONFIG_OF_UPSTREAM=y
CONFIG_OF_UPSTREAM_BUILD_VENDOR=y
CONFIG_OF_BOARD=y
+CONFIG_DEVICE_TREE_INCLUDES="exynos-mobile.dtsi"
CONFIG_BLKMAP=y
CONFIG_BUTTON_REMAP_PHONE_KEYS=y
CONFIG_CLK_EXYNOS7870=y
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v3 10/10] configs: exynos-mobile: pull in binman
@ 2026-06-10 1:27 ` Sam Day via B4 Relay
0 siblings, 0 replies; 25+ messages in thread
From: Sam Day via B4 Relay @ 2026-06-10 1:27 UTC (permalink / raw)
To: u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin, Sam Day
From: Sam Day <me@samcday.com>
All Exynos7 mobile devices need abootimg+DTBH wrapping to be bootable
from the fused S-BOOT bootloader. So we enable binman everywhere.
Ideally we'll have full coverage of these devices with binman configs in
the overlay DTSIs but, for now, we ensure there's a placeholder node.
That way no existing devices will regress their cleanly buildable state.
Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Sam Day <me@samcday.com>
---
arch/arm/dts/exynos-mobile.dtsi | 5 +++++
arch/arm/mach-exynos/Kconfig | 1 +
configs/exynos-mobile_defconfig | 1 +
3 files changed, 7 insertions(+)
diff --git a/arch/arm/dts/exynos-mobile.dtsi b/arch/arm/dts/exynos-mobile.dtsi
new file mode 100644
index 00000000000..9982e996993
--- /dev/null
+++ b/arch/arm/dts/exynos-mobile.dtsi
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0
+/ {
+ binman {
+ };
+};
diff --git a/arch/arm/mach-exynos/Kconfig b/arch/arm/mach-exynos/Kconfig
index b084b7284aa..d4ae182030d 100644
--- a/arch/arm/mach-exynos/Kconfig
+++ b/arch/arm/mach-exynos/Kconfig
@@ -255,6 +255,7 @@ endif
config TARGET_EXYNOS_MOBILE
bool "Samsung Exynos Generic Boards (for mobile devices)"
select ARM64
+ select BINMAN
select BOARD_EARLY_INIT_F
select CLK_EXYNOS
select LINUX_KERNEL_IMAGE_HEADER
diff --git a/configs/exynos-mobile_defconfig b/configs/exynos-mobile_defconfig
index 45ab998bb45..fcaff3d1608 100644
--- a/configs/exynos-mobile_defconfig
+++ b/configs/exynos-mobile_defconfig
@@ -32,6 +32,7 @@ CONFIG_EFI_PARTITION=y
CONFIG_OF_UPSTREAM=y
CONFIG_OF_UPSTREAM_BUILD_VENDOR=y
CONFIG_OF_BOARD=y
+CONFIG_DEVICE_TREE_INCLUDES="exynos-mobile.dtsi"
CONFIG_BLKMAP=y
CONFIG_BUTTON_REMAP_PHONE_KEYS=y
CONFIG_CLK_EXYNOS7870=y
--
2.54.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v3 10/10] configs: exynos-mobile: pull in binman
2026-06-10 1:27 ` Sam Day via B4 Relay
(?)
@ 2026-06-13 11:59 ` Kaustabh Chakraborty
-1 siblings, 0 replies; 25+ messages in thread
From: Kaustabh Chakraborty @ 2026-06-13 11:59 UTC (permalink / raw)
To: me, u-boot
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Quentin Schulz,
Bryan Brattlof, Yannic Moog, Wojciech Dubowik, Yegor Yefremov,
Patrice Chotard, Heinrich Schuchardt, Marek Vasut,
Rasmus Villemoes, Javier Tia, Minkyu Kang, Kaustabh Chakraborty,
Henrik Grimler, yan wang, Marek Vasut, Denis Mukhin
On 2026-06-10 11:27 +10:00, Sam Day via B4 Relay wrote:
> From: Sam Day <me@samcday.com>
>
> All Exynos7 mobile devices need abootimg+DTBH wrapping to be bootable
> from the fused S-BOOT bootloader. So we enable binman everywhere.
> Ideally we'll have full coverage of these devices with binman configs in
> the overlay DTSIs but, for now, we ensure there's a placeholder node.
> That way no existing devices will regress their cleanly buildable state.
> Reviewed-by: Simon Glass <sjg@chromium.org>
> Signed-off-by: Sam Day <me@samcday.com>
> ---
> arch/arm/dts/exynos-mobile.dtsi | 5 +++++
> arch/arm/mach-exynos/Kconfig | 1 +
> configs/exynos-mobile_defconfig | 1 +
> 3 files changed, 7 insertions(+)
>
> diff --git a/arch/arm/dts/exynos-mobile.dtsi b/arch/arm/dts/exynos-mobile.dtsi
> new file mode 100644
> index 00000000000..9982e996993
> --- /dev/null
> +++ b/arch/arm/dts/exynos-mobile.dtsi
> @@ -0,0 +1,5 @@
> +// SPDX-License-Identifier: GPL-2.0
No copyright notice?
> +/ {
> + binman {
> + };
> +};
> diff --git a/arch/arm/mach-exynos/Kconfig b/arch/arm/mach-exynos/Kconfig
> index b084b7284aa..d4ae182030d 100644
> --- a/arch/arm/mach-exynos/Kconfig
> +++ b/arch/arm/mach-exynos/Kconfig
> @@ -255,6 +255,7 @@ endif
> config TARGET_EXYNOS_MOBILE
> bool "Samsung Exynos Generic Boards (for mobile devices)"
> select ARM64
> + select BINMAN
> select BOARD_EARLY_INIT_F
> select CLK_EXYNOS
> select LINUX_KERNEL_IMAGE_HEADER
> diff --git a/configs/exynos-mobile_defconfig b/configs/exynos-mobile_defconfig
> index 45ab998bb45..fcaff3d1608 100644
> --- a/configs/exynos-mobile_defconfig
> +++ b/configs/exynos-mobile_defconfig
> @@ -32,6 +32,7 @@ CONFIG_EFI_PARTITION=y
> CONFIG_OF_UPSTREAM=y
> CONFIG_OF_UPSTREAM_BUILD_VENDOR=y
> CONFIG_OF_BOARD=y
> +CONFIG_DEVICE_TREE_INCLUDES="exynos-mobile.dtsi"
What about, instead of this you add an empty binman node to the other
existing devices? I have tested it with on7xelte and j6lte and it works
as intended.
After this is pulled in, I will submit a patch for binman changes for
those devices, so there's no need of a new .dtsi file.
Alternatively, this series can carry said changes for those devices in a
future revision if you want. Up to you.
> CONFIG_BLKMAP=y
> CONFIG_BUTTON_REMAP_PHONE_KEYS=y
> CONFIG_CLK_EXYNOS7870=y
^ permalink raw reply [flat|nested] 25+ messages in thread