Back to Articles

Tales from the Trenches · #1

Debugging an OP-TEE Panic on STM32MP1

How conflicting OP-TEE Device Tree and ETZPC configuration caused an STM32MP1 to panic before Linux booted

Debugging an OP-TEE Panic on STM32MP1

While working on the Device Tree for an STM32MP157-based board, I hit a boot failure…

NOTICE: CPU: STM32MP157AAC Rev.Z
NOTICE: Model: IPEC RMU TF-A
NOTICE: Reset reason (0x14):
NOTICE: BL2: v2.10-stm32mp1-r2.0(release):lts-v2.10.24-dirty(a07367a1)
NOTICE: BL2: Built : 16:16:36, Oct 3 2025
NOTICE: BL2: Booting BL32
I/TC: Early console on UART#4
I/TC:
I/TC: Pager is enabled. Hashes: 3840 bytes
I/TC: Pager pool size: 80kB
I/TC: Embedded DTB found
I/TC: OP-TEE version: 4.0.0-dev (gcc version 13.4.0 (GCC)) #5 Fri Oct 20 18:29:31 UTC 2023 arm
I/TC: WARNING: This OP-TEE configuration might be insecure!
I/TC: WARNING: Please check https://optee.readthedocs.io/en/latest/architecture/porting_guidelines.html
I/TC: Primary CPU initializing
I/TC: Override the OTP 18: 0x7b4a0000 to 0x7b4a01e0
I/TC: WARNING: Embeds insecure stm32mp_provisioning driver
E/TC:0 0 stm32_etzpc_dt_probe_bus:833 bus@5c007000: serial@40019000 not accessible: 0xffff0001
E/TC:0 0 Panic

First, work out what is actually failing

The first useful step with this sort of boot failure is to establish which part of the boot chain produced the error.

NOTICE: BL2: Booting BL32

followed by:

I/TC: Embedded DTB found
I/TC: OP-TEE version: 4.0.0-dev

and finally:

E/TC:0 0 ...
E/TC:0 0 Panic

The I/TC and E/TC messages are coming from OP-TEE. TF-A has started BL32 and OP-TEE is initialising the secure world.

That matters because an STM32MP1 system using OP-TEE has more than one Device Tree involved in getting Linux running. Looking exclusively at the kernel DTS isn’t enough when the failure occurs before the kernel has even been loaded.

There is another useful line in the log:

I/TC: Embedded DTB found

OP-TEE is telling us explicitly that it has found and is processing its own Device Tree.

Finding serial@40019000

The next step was to identify the device mentioned in the panic.

In the OP-TEE Device Tree, serial@40019000 was UART8:

uart8: serial@40019000 {
    ...
    access-controllers = <&etzpc STM32MP1_ETZPC_UART8_ID>;
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart8_pins_common>;
};

There are two particularly relevant properties here:

access-controllers = <&etzpc STM32MP1_ETZPC_UART8_ID>;
status = "okay";

The first tells OP-TEE that access to UART8 is controlled through ETZPC.

The second tells OP-TEE that this node is enabled.

To understand why those two lines resulted in a panic, it helps to look at what ETZPC actually does.

What ETZPC is and why it matters

The STM32MP1 uses Arm TrustZone to separate execution into secure and non-secure worlds. In this system, OP-TEE runs in the secure world while Linux runs in the non-secure world.

That separation has to extend beyond the CPU itself. If secure software is supposed to protect a resource, non-secure software cannot simply be allowed unrestricted access to the same hardware.

The STM32MP1 therefore contains hardware for controlling access to resources between the two worlds.

One of the components involved in this is the Enhanced TrustZone Protection Controller (ETZPC).

For the problem here, the important role of ETZPC is that it controls whether particular peripherals can be accessed from the secure or non-secure world.

This gives us two separate questions when configuring a peripheral:

Device Tree:
    Should this software use the peripheral?

ETZPC:
    Is this software allowed to access the peripheral?

Those are not the same question.

A peripheral being present and enabled in a Device Tree does not by itself grant the software access to the hardware. The security configuration still has to permit that access.

For UART8, that distinction turned out to be the cause of the problem.

The ETZPC configuration for UART8

The ETZPC configuration contained the following entry for UART8:

DECPROT(STM32MP1_ETZPC_UART8_ID,
        DECPROT_NS_RW,
        DECPROT_UNLOCK)

The first argument identifies the resource being protected:

STM32MP1_ETZPC_UART8_ID

The second defines its access policy:

DECPROT_NS_RW

In this case, UART8 was configured for non-secure read/write access.

That was intentional. UART8 was supposed to be used from the non-secure side of the system, where Linux would eventually run.

The final argument:

DECPROT_UNLOCK

means that the protection configuration isn’t locked against subsequent modification.

For this particular problem, the logic is therefore:

The problem became apparent when I compared it with the OP-TEE Device Tree node.

Two configurations that didn’t agree

The OP-TEE Device Tree still contained:

uart8: serial@40019000 {
    ...
    access-controllers = <&etzpc STM32MP1_ETZPC_UART8_ID>;
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart8_pins_common>;
};

The ETZPC configuration said:

UART8 -> non-secure read/write

while the OP-TEE Device Tree said:

UART8 -> enabled in OP-TEE

That was the conflict.

It is tempting to think of:

status = "okay";

as little more than “this hardware exists and works.” However, that isn’t a useful way of looking at it when several pieces of software have their own Device Trees.

This was the OP-TEE Device Tree. Having UART8 enabled there meant that OP-TEE considered the UART an active device that it should process during initialisation.

The node also explicitly identified ETZPC as its access controller:

access-controllers = <&etzpc STM32MP1_ETZPC_UART8_ID>;

When OP-TEE processed the enabled UART8 node, its ETZPC code checked whether the peripheral was accessible and ETZPC had configured it for non-secure access.

The result was exactly what the boot log had been telling me:

serial@40019000 not accessible

followed by the panic.

Conceptually, the configuration looked like this:

Once I understood that, the fix was considerably less exciting than the investigation.

The Fix

UART8 shouldn’t belong to OP-TEE. It should configured as a non-secure peripheral ready for Linux to use.

The fix was therefore to disable UART8 in the OP-TEE Device Tree:

 uart8: serial@40019000 {
     ...
     access-controllers = <&etzpc STM32MP1_ETZPC_UART8_ID>;
-    status = "okay";
+    status = "disabled";
     pinctrl-names = "default";
     pinctrl-0 = <&uart8_pins_common>;
 };

The ETZPC configuration remained:

DECPROT(STM32MP1_ETZPC_UART8_ID,
        DECPROT_NS_RW,
        DECPROT_UNLOCK)

This wasn’t disabling UART8 on the system. It was telling OP-TEE not to use UART8.

UART8 remained assigned to the non-secure world and could subsequently be configured and used by Linux.

With that change, OP-TEE no longer attempted to access a peripheral that ETZPC had made non-secure, and the panic was resolved.

There isn’t just one Device Tree

Most of my day-to-day Device Tree work is in Linux, so there is a natural tendency to think of “the Device Tree” as the DTS used by the kernel. On an STM32MP1 system involving TF-A and OP-TEE, it is more subtle than that.

By the time Linux starts, other firmware has already run and made decisions about the hardware. OP-TEE has its own Device Tree describing the hardware relevant to the secure world, while ETZPC is involved in enforcing which world is allowed to access particular resources.

The same physical UART can therefore appear in more than one Device Tree, but those Device Trees are being consumed by different software.

For UART8, what I wanted was effectively:

ETZPC:
    UART8 -> non-secure read/write

OP-TEE DT:
    UART8 -> disabled

Linux DT:
    UART8 -> enabled and configured for Linux

These aren’t contradictory uses of status.

Disabling UART8 in the OP-TEE Device Tree doesn’t mean that UART8 is globally disabled. It means OP-TEE should leave that peripheral alone.

Linux has its own Device Tree and can enable the same physical UART later in the boot process. This is an important distinction when working with systems where hardware ownership is split between secure firmware and Linux.

Debug the boot stage before the peripheral

If an STM32MP1 board stops during boot, first establish which component produced the failure and at which stage;

In this case it was clear that the failure was in OP-TEE:

NOTICE: BL2: Booting BL32
I/TC: Embedded DTB found
I/TC: OP-TEE version: ...
E/TC: ... Panic

Next, identify the peripheral from its address:

Then look at the subsystem reporting the problem

That last check was the one that mattered here. Once UART8 was disabled in the OP-TEE Device Tree, the configuration became consistent:

ETZPC       -> UART8 is non-secure
OP-TEE DT   -> don't use UART8
Linux DT    -> configure UART8 and use for Linux

So when debugging a peripheral on an STM32MP1, don’t assume that the Linux Device Tree is the only Device Tree that matters.

Look at which stage of the boot process is actually failing, which Device Tree that component is using, and which world is supposed to own the peripheral.

Sometimes the Device Tree entry that needs changing isn’t in the kernel source at all. And if the board hasn’t reached Linux yet, changing the kernel DTS is unlikely to improve matters.