Linux

STM32MP135 Without OP-TEE

Published 26 Sep 2025. By Jakob Kastelic.

This is Part 5 in the series: Linux on STM32MP135. See other articles.

Arm chips, such as the STM32MP135, implementing the TrustZone extension divide the execution into two worlds: a normal, non-secure world inhabited by the application operating system, and a secure world serviced by a secure OS such as OP-TEE. The ST wiki[1] assures us that OP-TEE is required on all STM32MP1 produces “due to the hardware architecture”. It is our purpose in this article to show that that is not the case: OP-TEE is in fact entirely optional.

The only mechanism to enter the “secure world” is via the SMC instruction (secure monitor call). This is analogous to how user-space applications invoke kernel system calls via the SVC (supervisor call) instruction to enter privileged mode. So long as the kernel does not issue the SMC instruction, the secure world need never be entered. Thus, we can restate our purpose as removing all secure monitor calls from the kernel configuration.

The present article is somewhat more involved than the preceding ones in the series. For this reason I offer the “Quick Start” version, where the required modifications to kernel drivers are offered as patches to apply to a particular version. For those interested, the “Theory” section fill in the details. As in other articles, we conclude with a brief discussion.

Quick Start

Start by cloning Buildroot as above. However, this time we check out a different sequence of patches and board files:

$ git clone https://gitlab.com/buildroot.org/buildroot.git
$ git clone git@github.com:js216/stm32mp135_simple.git

$ cd buildroot
$ git checkout 3645e3b781be5cedbb0e667caa70455444ce4552

$ git apply ../stm32mp135_simple/patches/add_falcon.patch
$ cp ../stm32mp135_simple/configs/stm32mp135f_dk_nonsecure_defconfig configs
$ cp -r ../stm32mp135_simple/board/stm32mp135f-dk-nonsecure board/stmicroelectronics

Now build:

$ make stm32mp135f_dk_nonsecure_defconfig
$ make

Write the generated image to the SD card (either directly with a tool such as dd, or using the STM32CubeProg as explained here). Watch it boot up without U-Boot, and without OP-TEE.

Theory

To understand the modifications we are about to do in the next section, we need to take a closer look at the boot process from TF-A to OP-TEE to Linux. In particular, we need to explain how secure monitor calls (SMC) calls work; the use of secure interrupts (FIQ) in OP-TEE; and explain how SCMI clocks work

Boot process from TF-A to OP-TEE to Linux

When Arm Trusted Firmware (TF-A) is done with its own initialization, it loads several images into memory. In the STM32MP1 case, these are defined in the array bl2_mem_params_desc in file plat/st/stm32mp1/plat_bl2_mem_params_desc.c, and include the following:

TrustZone memory regions that is used by TF-A itself

Just before passing control to OP-TEE, the TF-A prints a couple messages in the bl2_main() function (bl2/bl2_main.c), and then runs bl2_run_next_image (bl2/aarch32/bl2_run_next_image.S). There, we disable MMU, put the OP-TEE entry address into the link register (either lr or lr_svc), load the SPSR register, and then do an “exception return” to atomically change the program counter to the link register value, and restore the Current Program Status Register (CPSR) from the Saved Program Status Register (SPSR).

How do secure monitor calls (SMC) work?

The ARMv7-A architecture provides optional TrustZone extension, which are implemented on the STM32MP135 chips (as well as the virtualisation extension). In this scheme, the processor is at all times executing in one of two “worlds”, either the secure or the non-secure one.

The NS bit of the SCR register defines which world we’re currently in. If NS=1, we are in non-secure world, otherwise we’re in the secure world. The one exception to this is that when the processor is running in Monitor mode; in that case, the code is executing the secure world and SCR.NS merely indicates which world the processor was in before entering the Monitor mode. (The current processor mode is given by the M bits of the CPSR register.)

The processor starts execution in the secure world. How do we transition to the non-secure world? Outside of Monitor mode, Arm does not recommend direct manipulation of the SCR.NS bit to change from the secure world to the non-secure world or vice versa. Instead, the right way is to first change into Monitor mode, flip the SCR.NS bit, and leave monitor mode. To enter Monitor mode, execute the SMC instruction. This triggers the SMC exception, and the processor begins executing the SMC handler.

The location of the SMC handler has to be previously stored in the MVBAR register. The initial setup required is as follows:

  1. Write a SMC handler. As an example, consult OP-TEE source code, which provides the handler sm_smc_entry, defined in core/arch/arm/sm/sm_a32.S.

  1. Create a vector table for monitor mode. As specified in the Arm architecture manual, the monitor vector table has eight entries:

    1. Unused

    2. Unused

    3. Secure Monitor Call (SMC) handler

    4. Prefetch Abort handler

    5. Data Abort handler

    6. Unused

    7. IRQ interrupt handler

    8. FIQ interrupt handler

    Obviously entry number 3 has to point to the SMC handler defined previously. For example, OP-TEE defines the following vector table in core/arch/arm/sm/sm_a32.S:

    LOCAL_FUNC sm_vect_table , :, align=32
    UNWIND(	.cantunwind)
    
    b	.		/* Reset			*/
    b	.		/* Undefined instruction	*/
    b	sm_smc_entry	/* Secure monitor call		*/
    b	.		/* Prefetch abort		*/
    b	.		/* Data abort			*/
    b	.		/* Reserved			*/
    b	.		/* IRQ				*/
    b	sm_fiq_entry	/* FIQ				*/

    ENDFUNC smvect_table

    We see only the SMC and FIQ handlers are installed, since OP-TEE setup disables all other Monitor-mode interrupts and exceptions.

Incoherent Thoughts

Action Without Clinging

Published 21 Sep 2025. By Jakob Kastelic.

Decisions are illusions, especially difficult decisions. In reality, the only choice available is to sit back and watch events unfold; there is only one way in which they will unfold (at least in this universe), and that’s the way things are going to be.

Action must flow out of detachment from the “human” world of “I” and “mine”; it must be free from thoughts of control, achievement, and getting things done. Instead, the mind must be free to operate on the level of the mind, free to go where mind goes. There is no thought of goal as effect; the mind knows what it is doing when allowed so.

This is not passivity but a kind of wu wei—acting without forcing.

Linux

Linux as TF-A BL33 on Qemu (No U-Boot)

Published 15 Sep 2025, modified 6 Feb 2026. By Jakob Kastelic.

This is Part 4 in the series: Linux on STM32MP135. See other articles.

With Qemu, anyone can customize the Linux boot process and run it without the need for custom hardware. In this article, we will adapt a Buildroot defconfig to make TF-A boot Linux and OP-TEE directly without U-Boot.

This approach was suggested by A. Vandecappelle on the Buildroot mailing list[1]. He was correct to point out that it would be interesting to see a Qemu simulation of the “Falcon mode” boot process:

Perhaps it would also be a good idea to add a variant of the qemu defconfigs that tests this option. We can use the qemu_arm_vexpress_tz_defconfig, drop U-Boot from it, and switch to booting to Linux directly from TF-A.

First, we will look at the “normal” boot process with U-Boot to understand how to remove it. Then, we will provide tutorial-style steps to remove U-Boot from the boot process. Then, we suggest with how to integrate this into Buildroot. We conclude with a discussion of alternative approaches.

“Normal” boot process

In the qemu_arm_vexpress_tz_defconfig defconfig, Qemu is instructed to load Arm Trusted Firmware (TF-A) as “bios“. Qemu auto-generates a Device Tree Blob (DTB) and loads it in memory at the start of RAM. As the Qemu documentation[2] explains:

In our case, TF-A is booted in the “bare-metal” mode. We can see in file plat/qemu/qemu/include/platform_def.h that this is so:

#define PLAT_QEMU_DT_BASE           NS_DRAM0_BASE

TF-A patches the Qemu-provided DTB by inserting the information about the reserved memory addresses used by the secure OS (OP-TEE), as well as the protocol (PSCI) that Linux is to use to communicate with OP-TEE. Then, it passes control to U-Boot.

U-Boot only task in this configuration, as far as I can tell, is to load the initial compressed filesystem image into some range of memory addresses, then patch the DTB with these addresses. Then, it passes control to the Linux kernel.

Linux reads the DTB, either from the address given in register r2 or perhaps from the pre-defined memory location (not sure). Then, it reads the initrd-start location from the chosen node, decompresses the filesystem, locates the init process, and runs it.

Thus to remove U-Boot, we just have to load the initramfs ourselves, and add its address to the DTB. Of course, we must also tell TF-A to not load the U-Boot and instead run Linux directly. In the following section, we explain how to do that.

Falcon-mode tutorial

  1. Obtain Buildroot and check out and build the defconfig that we’re starting from:

        $ git clone https://gitlab.com/buildroot.org/buildroot.git --depth=1
        $ make qemu_arm_vexpress_tz_defconfig
        $ make

    This builds everything and gives the script start_qemu.sh (under output/images) with the suggested Qemu command line.

  1. Extract the DTB by modifying the Qemu command as follows (note the dumpdtb=qemu.dtb):

       $ qemu-system-arm -machine virt,dumpdtb=qemu.dtb -cpu cortex-a15
  1. Uncompile the DTB into the source format so we can edit it:

       $ dtc -I dtb -O dts qemu.dtb > new.dts

    Open new.dts in a text editor and modify the chosen node as follows, adding the location of the initramfs (initrd):

       chosen {
       	linux,initrd-end = <0x00 0x7666e09d>;
       	linux,initrd-start = <0x00 0x76000040>;
       	bootargs = "test console=ttyAMA0,115200 earlyprintk=serial,ttyAMA0,115200";
       	stdout-path = "/pl011@9000000";
       };

    Compile it back into the DTB format:

       dtc -I dts -O dtb new.dts > new.dtb
  1. Open make menuconfig and navigate to Bootloaders ---> Arm Trusted Firmware (ATF). Switch the BL33 to None, and add the following Additional ATF build variables:

       BL33=$(BINARIES_DIR)/zImage

    Exit and save new configuration and rebuild:

       $ make arm-trusted-firmware-rebuild
       $ make

    Check that output/images contains updated fip.bin, which should be about 5 or 6M in size since it contains the whole kernel rather than just U-Boot.

  1. Run Qemu with the following commands:

       $ cd output/images
       $ exec qemu-system-arm -machine virt -dtb art.dtb -device \
            loader,file=rootfs.cpio.gz,addr=0x76000040 -machine secure=on -cpu \
            cortex-a15 -smp 1 -s -m 1024 -d unimp -netdev user,id=vmnic -device \
            virtio-net-device,netdev=vmnic -nographic \
            -semihosting-config enable=on,target=native -bios flash.bin

    This is of course just the old command from start-qemu.sh, with the DTB and initramfs added. With some luck, you should see messages from TF-A directly transitioning into the ones from the kernel, with no U-Boot in between:

       NOTICE:  Booting Trusted Firmware
       NOTICE:  BL1: v2.7(release):v2.7
       NOTICE:  BL1: Built : 20:55:52, Sep 12 2025
       NOTICE:  BL1: Booting BL2
       NOTICE:  BL2: v2.7(release):v2.7
       NOTICE:  BL2: Built : 20:55:52, Sep 12 2025
       NOTICE:  BL1: Booting BL32
       Booting Linux on physical CPU 0x0
       Linux version 6.12.27 (jk@Lutien) (arm-buildroot-linux-gnueabihf-gcc.br_real (Buildroot -g5b6b80bf) 14.3.0, GNU ld (GNU Binutils) 2.43.1) #2 SMP Fri Sep 12 20:03:32 PDT 2025
       CPU: ARMv7 Processor [414fc0f0] revision 0 (ARMv7), cr=10c5387d
       CPU: div instructions available: patching division code
       CPU: PIPT / VIPT nonaliasing data cache, PIPT instruction cache
       OF: fdt: Machine model: linux,dummy-virt
       OF: fdt: Ignoring memory range 0x40000000 - 0x60000000

TF-A support for Linux as BL33

We saw above that TF-A is happy to boot Linux directly so long as we just point it to a kernel image for the BL33 executable. It turns out that there we can find limited support for this use case already in the TF-A source tree via the ARM_LINUX_KERNEL_AS_BL33 flag.

The flag is specific to a few platforms. For AArch64 on Qemu, the documentation (docs/plat/qemu.rst, as well as docs/plat/arm/arm-build-options.rst) explains that the flag makes TF-A pass the Qemu-generated DTB to the kernel via the x0 register. We see the implementation of it in plat/qemu/common/qemu_bl2_setup.c (and very similar lines in plat/arm/common/arm_bl31_setup.c):

#if ARM_LINUX_KERNEL_AS_BL33
		/*
		 * According to the file ``Documentation/arm64/booting.txt`` of
		 * the Linux kernel tree, Linux expects the physical address of
		 * the device tree blob (DTB) in x0, while x1-x3 are reserved
		 * for future use and must be 0.
		 */
		bl_mem_params->ep_info.args.arg0 =
			(u_register_t)ARM_PRELOADED_DTB_BASE;
		bl_mem_params->ep_info.args.arg1 = 0U;
		bl_mem_params->ep_info.args.arg2 = 0U;
		bl_mem_params->ep_info.args.arg3 = 0U;

On AArch32, the flag as currently implemented is intended for operation with SP_MIN. This is clear from the documentation: “for AArch32 `RESET_TO_SP_MIN must be 1 when using" the ARMLINUXKERNELASBL33 flag (docs/plat/arm/arm-build-options.rst). The plat/arm/common/arm_common.mk` Makefile enforces this.

Unfortunately this limits the potential use cases of ARM_LINUX_KERNEL_AS_BL33 to AArch64, or else to AArch32 with SP_MIN enabled. The Buildroot defconfig we have adapted in the previous section uses OP-TEE instead of SP_MIN, and it is also possible to use no BL32 at all.

Patching initramfs address

In the tutorial above, we dumped the Qemu DTB and modified it just to add two lines into the chosen node. The same can be done by TF-A.

The file plat/qemu/common/qemu_bl2_setup.c defines the function update_dt() which is used for precisely this purpose, updating the DTB with some extra board-specific details. (In the defconfig, it inserts PSCI nodes.)

We can insert the two chosen lines in the middle of update_dt():

fdt_setprop_u64(fdt, fdt_path_offset(fdt, "/chosen"),
        "linux,initrd-start", 0x76000040);
fdt_setprop_u64(fdt, fdt_path_offset(fdt, "/chosen"),
        "linux,initrd-end",   0x7666e09d);

On recompile, there is no need to manually modify the DTB anymore.

The disadvantage of this approach is that we have to patch TF-A, making our defconfig fragile against future changes in TF-A. It would be better to include that DTB compilation as a post-build script in Buildroot.

Discussion

Is it practical to assume that the initramfs will be loaded in memory before TF-A even starts executing? Of course not. But on a real embedded platform, such as the setup from the previous article, the root filesystem is the SD card or some other non-volatile storage. There appears to be no good reason to use U-Boot since TF-A can read from these just fine. If, on the other hand, your setup requires some complicated configuration of the root filesystem, possibly involving Ethernet, then U-Boot may well be a good choice. Still, I believe that the best tool for the job is the simplest one that works reliably.

It is also not reasonable to assume that the DTB would be loaded in memory before TF-A even begins execution. After all, as the only bootloader, it is its job to load it and point the kernel to where it loaded it. As P. Maydell explains on the qemu-discuss mailing list[3], providing the -dtb option to Qemu “overrides the autogenerated file. But generally you shouldn’t do that.” Instead, the Qemu user should provide the DTB, if emulating real hardware, or else to have the Qemu

autogenerate the DTB matching whatever it does, like the virt board. This is the unusual case – virt only does this because it is a purely “virtual” board that doesn’t match any real physical hardware and which changes depending on what the user asked for.

For example, on STM32MP1, the TF-A fiptool is used to package the DTB in a form that TF-A is able to load it in memory using the BL33_CFG flag, as we have used in previous article.

There may be other ways to load the DTB and initramfs in Qemu, but the one presented in our tutorial above appears to be the easiest. We could, for example, modify Qemu to allow using the -initrd command line flag without the -kernel flag, and emit the DTB with the appropriate address. Or, we could teach TF-A how to read the initramfs file via the semihosting or virtio protocols, load it into memory, and modify the DTB accordingly.

However, the tutorial method above works without modifying Qemu or TF-A code. It uses an explicit DTB, as one is likely to do on a physical embedded target. Since it passes the initramfs using an explicit command line option, it avoids hard-coding it into any compiled code.

Upstreaming Status

17/9/2025: first submission of the Qemu defconfig (link)

02/03/2026: response by Thomas Petazzoni (link)

02/04/2026: amended submission (v3) as a runtime test (link)

02/05/2026: response by Thomas Petazzoni (link)

02/04/2026: amended submission (v4) (link)

All Articles in This Series


  1. Buildroot mailing list, Fri May 16 2025 message: boot/arm-trusted-firmware: optional Linux as BL33 (cited on 09/15/2025)
  2. Qemu: ‘virt’ generic virtual platform (cited 09/15/2025).
  3. qemu-discuss mailing list, Thu 4 Aug 2022 message: Re: how to prevent automatic dtb load? (cited 09/15/2025)
Philosophy

Dead Code Elimination is a False Promise

Published 14 Sep 2025. By Jakob Kastelic.

Go through the source of any nontrivial program and chances are, most of the code in there is used very rarely, and a lot if it may never be used. This is the more so if a program is supposed to be very portable, such as in the Linux kernel. Of course, compilers will eliminate it, so there’s no problem?

The Problem

Compilers are supposed to detect what’s not being used, and remove the code. They don’t do that. For example, processing one “compilation unit” at a time, a C compiler has no idea which functions will be referenced to from other units and which are entirely dead. (If the function is declared static, this does work, so declare as many of them static.)

Surely by the time the linker is invoked, all the function calls are clear and the rest can be stripped away? Also not likely. For example, the function calls could be computed during runtime as casts of integers into function pointers. If the linker were to remove them, this mechanism would fail. So long as several functions are compiled into the same section, the linker will always include all of them so long as at least one of them is used.

What if we instead explicitly mark which things we would like excluded?

Conditional Compilation

With conditional compilation, you can include/exclude whatever you want. When a program has these conditional compilation switches, dead code does get entirely deleted before the compiler even sees it. Most often, the result is a myriad of poorly-documented (more likely: entirely undocumented) switches that you don’t know what you’re allowed to disable.

For example, the Linux kernel provides the amazing menuconfig tool to manage these inclusions and exclusions. Still, it can take days of work trying out disabling and re-enabling things, till you give up and wisely conclude that this “premature optimization” is not worth your time and leave everything turned on as it is by default.

“Packages”

The sad reality of modern scripting languages, and even compiled ones like Rust, is that their robust ecosystems of packages and libraries encourage wholesale inclusion of code whose size is entirely out of proportion to the task they perform. (Don’t even mention shared libraries.)

As an example, let’s try out the popular Rust GUI library egui. According to its Readme, it is modular, so you can use small parts of egui as needed, and comes with “minimal dependencies”. Just what we need to make a tiny app! Okay, first we need Rust itself:

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ du -hs .rustup .cargo
1.3G    .rustup
20M     .cargo

So far so good—the entire compiler and toolchain fits inside 1.3G, and we start with 20M of packages. Now let’s clone the GUI library and compile its simple example with a couple really simple widgets:

$ git clone git@github.com:emilk/egui.git
$ cd egui/examples/hello_world_simple
$ cargo run -p hello_world_simple
$ cd && du -hs .rustup .cargo
2.6G    .rustup
210M    .cargo

Oops! How many gigabytes of code does it take to show a couple characters and rectangles on the screen? Besides, the above took more than 20 min to complete on a machine vastly superior to the Cray-2 supercomputer. The compiled program was 236M in size, or 16M after stripping. Everyday We Stray Further ...

This is far from being a “freak” example; even the simplest tasks in Rust and Python and pretty much anything else considered “modern” will pull in gigabytes of “essential” packages.

Packages get inextricably linked with the main program, resulting in an exponential explosion of complexity (besides the linear growth in size). Once linked, the program and its libraries/packages are no longer separate modules; you cannot simply replace a library for a different one, despite the host of false promises from the OOP crowd.

This is because the interfaces between these modules are very complex: hundreds or thousands of function calls, complex object operations, &c.

The Solution

The only way I know of that works is to not have dead code to begin with. Extra features should be strictly opt-in, not opt-out. These should be implemented with separate compilation and linking; in other words, each feature is a new program, not a library.

The objection may be raised that we’re advocating an extremely inefficient paradigm, increasing the already significant overhead of function calls with the much greater one of executing new programs. As an “extreme” example, a typical Unix shell will parse each command (with few exceptions) as the name of a new program to execute. How inefficient!?

Maintainable, replaceable code reuse can only happen when the interfaces are well specified and minimal, such as obtain between cooperating independent programs in a Unix pipeline.

The key to problem-solving on the UNIX system is to identify the right primitive operations and to put them at the right place. UNIX programs tend to solve general problems rather than special cases. In a very loose sense, the programs are orthogonal, spanning the space of jobs to be done (although with a fair amount of overlap for reasons of history, convenience or efficiency). Functions are placed where they will do the most good: there shouldn’t be a pager in every program that produces output any more than there should be filename pattern matching in every program that uses filenames.

One thing that UNIX does not need is more features. It is successful in part because it has a small number of good ideas that work well together. Merely adding features does not make it easier for users to do things — it just makes the manual thicker.[1]


  1. Pike, Rob, and Brian Kernighan. “Program design in the UNIX environment.” AT&T Bell Laboratories Technical Journal 63.8 (1984): 1595-1605. See also UNIX Style, or cat -v Considered Harmful.
Linux

STM32MP135 Without U-Boot (TF-A Falcon Mode)

Published 11 Sep 2025, modified 4 Feb 2026. By Jakob Kastelic.

This is Part 3 in the series: Linux on STM32MP135. See other articles.

In this article, we use Arm Trusted Firmware (TF-A) to load the Linux kernel directly, without using U-Boot.[1] I have seen the idea of omitting the Secondary Program Loader (SPL) referred to as “falcon mode”, since it makes the boot process (slightly) faster. However, I am primarily interested in it as a way of reducing overall complexity of the software stack.

In this article, we will implement this in two ways. First, we modify the files as needed manually. At the end of the article, we provide an alternative method: directly integrate the changes into Buildroot.

Prerequisites

To get started, make sure to have built the default configuration as per the first article of this series. Very briefly, this entails cloning the official Buildroot repository, selecting a defconfig, and compiling:

$ git clone https://gitlab.com/buildroot.org/buildroot.git --depth=1
$ cd buildroot
$ make stm32mp135f_dk_defconfig
$ make menuconfig # add the STM32MP_USB_PROGRAMMER=1 flag to TF-A build
$ make

It is also recommended to learn how to flash the SD card without removing it via a USB connection, as explained in the second article.

Tutorial

The procedure is pretty simple. All we need to do is to modify some files, adjust some build parameters, recompile, and the new SD card image is ready to test.

  1. Before making any modifications, make a backup of the file containing U-Boot.

       $ cd output/images
       $ cp fip.bin fip_uboot.bin

    Double check that the above fip.bin was built using the additional ATF build variable STM32MP_USB_PROGRAMMER=1, otherwise USB flashing will not work!

    Open flash.tsv, and update the fip.bin to fip_uboot.bin there as well.

    (Despite removing U-Boot from the boot process, we are still going to use it to flash the SD card image via USB using the STM32CubeProg.)

  1. Two TF-A files need to be modified, so navigate to the TF-A build directory:

       $ cd ../build/arm-trusted-firmware-lts-v2.10.5

    Since the kernel is much bigger than U-Boot, it takes longer to load. We need to adjust the SD card reading timeout. In drivers/st/mmc/stm32_sdmmc2.c, find the line

       timeout = timeout_init_us(TIMEOUT_US_1_S);

    and replace it with

       timeout = timeout_init_us(TIMEOUT_US_1_S * 5);

    Next, we would like to load the kernel deep enough into the memory space so that relocation of the compressed image is not necessary. In file plat/st/stm32mp1/stm32mp1_def.h, find the line

       #define STM32MP_BL33_BASE              STM32MP_DDR_BASE

    and replace it with

       #define STM32MP_BL33_BASE              (STM32MP_DDR_BASE + U(0x2008000))

    Finally, in order to allow loading such a big BL33 as the kernel image, we adjust the max size. In the same file, find the line

       #define STM32MP_BL33_MAX_SIZE          U(0x400000)

    and replace it with

       #define STM32MP_BL33_MAX_SIZE          U(0x3FF8000)
  1. Next, we need to modify a couple build parameters. Open the make menuconfig and navigate to Bootloaders ---> ARM Trusted Firmware (ATF).

    • Under BL33, change from U-Boot to None.

    • Under Additional ATF build variables, make sure that U-Boot is not present and add the following key-value pairs:

           BL33=$(BINARIES_DIR)/zImage BL33_CFG=$(BINARIES_DIR)/stm32mp135f-dk.dtb

    Select “Ok” and “Esc” out of the menus, making sure to save the new configuration.

    Next, open the file board/stmicroelectronics/common/stm32mp1xx/genimage.cfg.template and increase the size of the fip partition, for example:

       partition fip {
       	image = "fip.bin"
       	size = 8M
       }

    Finally, since U-Boot will no longer be around to pass the Linux command line arguments, we can instead pass them through the device tree source. Open the file output/build/linux-6.12.22/arch/arm/boot/dts/st/stm32mp135f-dk.dts (you may have a different Linux version, just modify the path as appropriate) and add the bootargs into the chosen section, as follows:

       chosen {
       	stdout-path = "serial0:115200n8";
       	bootargs = "root=/dev/mmcblk0p4 rootwait";
       };
  1. Now we can rebuild the TF-A, the device tree blob, and regenerate the SD card image. Thanks to the magic of Buildroot, all it takes is:

       $ make linux-rebuild
       $ make arm-trusted-firmware-rebuild
       $ make

    Keep in mind that rebuilding TF-A is needed any time the Linux kernel or DTS or TF-A sources change, since the kernel gets packaged into the fip by the TF-A build process. In this case, the first make rebuilds the DTB, the second packages it in the fip, and the third makes sure it gets into the SD card.

  1. Set DIP switch to serial boot (press in the upper all of all rockers) and flash to SD card:

       $ sudo ~/cube/bin/STM32_Programmer_CLI -c port=usb1 -w output/images/flash.tsv

    Then reconfigure the DIP switches for SD card boot (press the bottom side of the second rocker switch from the left), and press the black reboot button.

If you watch the serial monitor carefully, you will notice that we transition from TF-A directly to OP-TEE and Linux. Success! No U-Boot in the boot process:

NOTICE:  Model: STMicroelectronics STM32MP135F-DK Discovery Board
NOTICE:  Board: MB1635 Var1.0 Rev.E-02
NOTICE:  BL2: v2.10.5(release):lts-v2.10.5
NOTICE:  BL2: Built : 20:58:52, Sep 10 2025
NOTICE:  BL2: Booting BL32
I/TC: Early console on UART#4
I/TC: 
I/TC: Embedded DTB found
I/TC: OP-TEE version: Unknown_4.3 (gcc version 14.3.0 (Buildroot 2025.08-rc3-87-gbbb0164de0)) #1 Thu Sep  4 03:06:46 UTC 2025 arm
...
(more OP-TEE messages here)
...
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 6.12.22 (jk@Lutien) (arm-buildroot-linux-gnueabihf-gcc.br_real (Buildroot 2025.08-rc3-87-gbbb0164de0) 14.3.0, GNU ld (GNU Binutils) 2.43.1) #1 SMP PREEMPT Wed Sep  3 20:23:46 PDT 2025
[    0.000000] CPU: ARMv7 Processor [410fc075] revision 5 (ARMv7), cr=10c5387d

Buildroot integration

Instead of following the above instructions, we can automate the build process by integrating it into Buildroot. To this end, I provide the GitHub repository stm32mp135_simple that can be used as follows.

Clone the Buildroot repository. To make the procedure reproducible, let’s start from a fixed commit (latest at the time of this writing):

$ git clone https://gitlab.com/buildroot.org/buildroot.git
$ cd buildroot
$ git checkout 5b6b80bfc5237ab4f4e35c081fdac1376efdd396

Obtain this repository with the patches we need. Copy the defconfig and the board-specific files into the Buildroot tree.

$ git clone git@github.com:js216/stm32mp135_simple.git
$ cd buildroot # NOT stm32mp135_simple
$ git apply ../stm32mp135_simple/patches/add_falcon.patch
$ git apply ../stm32mp135_simple/patches/increase_fip.patch
$ cp ../configs/stm32mp135_simple/stm32mp135f_dk_falcon_defconfig configs
$ cp -r ../board/stm32mp135_simple/stm32mp135f-dk-falcon board/stmicroelectronics

Build as usual, but using the new defconfig:

$ make stm32mp135f_dk_falcon_defconfig
$ make

Flash to the SD card and boot into the new system. You should reach the login prompt exactly as in the default configuration—but without involving U-Boot

Discussion

To port the “default” STM32MP135 setup[2] to a new board design, one is expected to be comfortable writing and modifying the drivers and device tree sources that work with

That is a tall order for a new embedded developer trying to get started integrating Linux in their products. To make things worse, there is at present almost no literature to be found suggesting that a simpler, saner method exists. Certainly the chip vendors themselves do not encourage it.[3]

With this article, we have began chipping away at the unnecessary complexity. We have removed U-Boot from the boot chain. (We still use it for copying the SD card image via USB. One thing at a time!) Since our goal is to run Linux, the list above gives us a blueprint for the work that remains to be done: get rid of everything that is not Linux.

The software that you do not run is software you do not have to understand, test, debug, maintain, and be responsible for when it breaks down ten years down the line in some deeply embedded application, perhaps in outer space.

Upstreaming Status

19/12/2024: original Buildroot mailing list submission (1/1)

16/12/2025: response by Arnout Vandecappelle (link)

17/9/2025: amended submission (v2 0/2, 1/2, 2/2)

02/04/2026: merged by Thomas Petazzoni as Buildroot commit 8e4c663529d135088c78a9c7f4b59354f19d6580

All Articles in This Series


  1. This approach is inspired by the ST wiki article How to optimize the boot time, under “Optimizing boot-time by removing U-Boot”. (cited 09/11/2025)
  2. See the ST Wiki, OpenSTLinux distribution (cited 09/11/2025)
  3. As per the ST forum, (cited 09/11/2025) the approach outlined in the present article is officially not supported by ST.