pico advanced 30 min

Pico 2: TrustZone on the RP2350, when the secure world matters

Use TrustZone on the Raspberry Pi Pico 2 RP2350 to isolate secure boot, sign firmware, and lock down GPIO. C SDK only; MicroPython does not expose TrustZone.

Code available for: MicroPython
Published Aug 26, 2026

The original Pico had a Cortex-M0+ chip and no security story beyond “do not lose your firmware.” The Pico 2 has a Cortex-M33 with ARM’s TrustZone, which is a hardware way to split your firmware into two worlds: a secure world that can do anything, and a non-secure world that can only do what the secure world lets it. The boot ROM on the RP2350 checks a signature on your firmware before it lets any code run, and the keys for that check live in one-time-programmable (OTP) memory that nobody can read out.

That is the headline. The slow part of the TrustZone story is what you do with it, and whether you actually need it for what you are building. Most people reading this do not. If you are shipping a product that has to resist firmware replacement in the field, you do. This tutorial is for the second group.

This is also a C SDK tutorial. MicroPython on the RP2350 does not expose TrustZone at all (e.g. there is no way from MicroPython to mark a GPIO as secure-only or to call secure-world functions). If you want to use TrustZone, you are writing C against the Pico SDK. That is not a value judgment; it is just where the support is in 2026.

What TrustZone actually is

TrustZone is an ARM feature that has been in Cortex-A chips for years and landed on Cortex-M with the M33. The hardware view: every bus access on the chip has a “secure” or “non-secure” bit attached. Memory, peripherals, and GPIO can be marked as secure-only (non-secure code gets a fault when it tries to access them), non-secure-only (secure code can still touch them, which is what you want for shared RAM), or shared (both worlds can use them).

The firmware view: your code runs in one of two worlds. The non-secure world is where your application lives. It looks like normal code. The secure world holds the things you do not want the application to be able to subvert: the boot verification keys, the cryptographic functions, the factory calibration data, and any GPIO that drives hardware you do not want the application to be able to bypass (e.g. a motor controller that has to ignore the application in a fault state).

The two worlds share the chip. They have separate stacks, separate interrupt vectors, and separate views of memory. When the non-secure world needs a secure-world function (e.g. to verify a message signature), it calls into the secure world through a controlled entry point. The secure world does the work, returns the result, and the non-secure world continues.

Why the RP2350 added it

The Raspberry Pi folks have been clear about this in their RP2350 docs: the original Pico had no secure boot, and people who shipped products based on the Pico had to roll their own security (e.g. an external secure element, a signed bootloader in an SPI flash, or “trust the enclosure”). The RP2350 puts the secure boot in the silicon.

The boot ROM on the RP2350 does the following at reset:

  1. Read the image header from the boot media (typically the internal flash).
  2. Check the signature against a public key burned into OTP.
  3. If the signature is valid, start executing. If not, refuse to boot.

The OTP keys are written once at factory provisioning time. After that, they are not readable by any code, secure or non-secure. They are also not writable by any code in the field. To change the keys, you provision a new chip.

What you need

  • A Raspberry Pi Pico 2
  • USB-C cable
  • A computer with the Pico C SDK installed (pico-sdk from the Raspberry Pi GitHub)
  • arm-none-eabi-gcc and cmake and make
  • A signing key (you generate this; do not commit it to your repo)
  • A way to provision the OTP key (this is the part I want to be honest about up front; see below)

The “I want to update my own firmware” problem

This is the part that catches people on the first try. Once you burn a secure boot key into the RP2350 OTP, that key will only ever verify firmware you signed with the matching private key. If you lose the private key, you cannot update your own firmware. You cannot re-flash over USB. You cannot run unsigned firmware for debugging.

For development, you do not want to burn a key yet. The Pico SDK ships a “development signing key” that is the same for every developer, and the boot ROM has a way to say “in dev mode, accept the dev key.” You use this for the entire prototype phase. You only burn your own key when you are about to ship.

The mechanism is called KEY_REVOKE and friends in the boot ROM. The pattern is:

  1. Build with the dev key. Flash and test.
  2. When you are ready to lock down, generate your own keypair.
  3. Provision the public key into OTP (one-time; you do not get to change it).
  4. Sign your firmware with the matching private key.
  5. Flash the signed firmware.

If you skip step 1 and start with your own key, you will lock yourself out the first time you try to fix a bug.

The code

This is the smallest TrustZone example that does anything useful: a non-secure application that calls a secure function. The secure function returns a value from OTP (or a constant if the OTP is empty in dev mode).

// secure_world.c - runs in the secure world
#include "pico/stdlib.h"
#include "pico/secure.h"

uint32_t secret_value(void) {
    // Return a value that the non-secure world is not allowed to read
    // directly from OTP. The secure world can read OTP; the non-secure
    // world cannot.
    return 0xDEADBEEF;
}
// nonsecure_world.c - runs in the non-secure world
#include "pico/stdlib.h"

extern uint32_t secret_value(void);

int main(void) {
    stdio_init_all();

    // Call into the secure world through the veneer.
    uint32_t v = secret_value();
    printf("got %lx from the secure world\n", v);

    while (true) {
        tight_loop_contents();
    }
}
# CMakeLists.txt (snippet) - tells the linker which world each .o lives in
add_executable(secure_app
    secure_world.c
    nonsecure_world.c
)

target_compile_definitions(secure_app PRIVATE
    PICO_BOARD=pico2
    PICO_PLATFORM=rp2350
)

# Mark secure_world.c as secure-world code. The Pico SDK CMake helpers
# generate the secure/non-secure veneer and put each function in the
# right world at link time.
target_compile_secure(secure_app secure_world.c)
target_compile_nonsecure(secure_app nonsecure_world.c)

The full Pico SDK has a pico_secure_library example that walks through the linker setup. The snippet above is the shape, not the complete build configuration. For the actual CMake invocation, follow the SDK’s secure/non-secure example.

The MicroPython story

There is no MicroPython story for TrustZone. The MicroPython port for the RP2350 runs everything in the non-secure world and does not expose any secure-world calls. If you read the MicroPython source, you will find the secure/non-secure distinction is not handled at all.

This is a deliberate scope decision, not an oversight. MicroPython is a Python interpreter that runs your code; TrustZone is a hardware feature that requires partitioning your C build at link time. Bridging the two would mean a Python-visible API for marking functions secure, and that is not a thing the MicroPython maintainers have built.

If you want TrustZone, write C. If you want MicroPython, accept that TrustZone is invisible.

The secure vs non-secure GPIO gotcha

You can mark any GPIO on the RP2350 as secure-only. The non-secure world will get a fault if it tries to drive that pin. This is the right way to lock down a hardware line (e.g. a motor enable pin that the application must not be able to assert).

The gotcha is that secure-only GPIO is also invisible to MicroPython. There is no machine.Pin from the Python side for a secure-only pin. If you need to drive a pin from MicroPython and also lock it down with TrustZone, you need to call a secure-world function from MicroPython, which means writing a C extension. That is a project, not a tutorial.

The production use case

The production use case for TrustZone on the RP2350 is: you are shipping a device with a Pico 2 inside, and you do not want the user to be able to flash their own firmware. The reasons vary. Sometimes it is regulatory (e.g. the device has a radio and the firmware has to be locked down to keep the type approval). Sometimes it is commercial (e.g. the firmware is the product and you do not want clones). Sometimes it is safety (e.g. the firmware drives a motor and an attacker could flash malicious firmware to damage the hardware).

In all three cases the recipe is the same: generate a keypair, burn the public key into OTP, sign every firmware image with the private key, distribute the signed image through your update channel. Anyone with physical access to the chip can desolder it and read its flash, but they cannot replace the firmware with their own unsigned image, because the boot ROM will reject it.

What you learned

TrustZone on the RP2350 is a hardware isolation feature for shipping products, not for hobby projects. The boot ROM verifies a signature on your firmware using a public key in OTP, and your code can be split into secure and non-secure worlds at link time. The MicroPython port does not expose it. To use it, write C against the Pico SDK, plan your key management before you burn a key, and treat the dev key as the default during development.

When something breaks

  • The Pico 2 refuses to boot after flashing. You signed with the wrong key. Reflash with the dev key (or the matching key if you burned one) and start over.
  • The non-secure world gets a HardFault when calling a secure function. The function is not in the secure veneer. Check the linker setup and the target_compile_secure / target_compile_nonsecure declarations.
  • OTP is not writable from your code. Correct. OTP is one-time and only writable from a specific boot ROM mode. Use the Pico SDK’s otp_program function in a provisioning build.
  • You locked yourself out by burning a key. You have to physically replace the RP2350 chip. This is on purpose, but it stings. Plan ahead.

What to build next

  • The Pico SDK secure boot examples. The Raspberry Pi GitHub has a pico-examples repo with a secure_boot directory that walks through the full signing flow.
  • A custom bootloader that lives in the secure world and verifies application updates before handing off to the non-secure world. This is the standard OTA update pattern for TrustZone devices.
  • The Pico 2 PIO improvements tutorial. PIO programs run identically in both worlds, so you can use PIO from secure-world code without any extra setup.
  • The Pico MicroPython SDK book does not cover TrustZone (MicroPython does not expose it). For TrustZone content, look for the Pico SDK book when it ships.

(these are sample tutorials written for Brian to review on his return. They will not be promoted to “ready” status without his approval.)