Oberon RTK

Secure/Non-secure Part 4

lib/v3.0 Prototype: Secure/Non-secure programs – RP2350, update

Overview, Purpose

In Secure/Non-secure Part 3 – RP2350 we had explored how to implement Secure/Non-secure code separation on the RP2350 – following the concepts and implementation as found in the bootrom. That is, using a single dispatch handler and a lookup table to call Secure procedures from the Non-secure program. The dispatch handler is called by the one single veneer (in ARM's terms) implemented in the bootrom, which runs in a Non-secure Callable memory region.

Even though I was able to cobble together a working solution – of protoype quality --, I had expressed my uneasiness with both the whole concept as well as the implementation. Compared to the rather lightweight and straightforward implementation on the STM32U585, which directly follows ARM's reference implementation (see Part 2), the RP2350's approach seems unsatisfactory in many respects – see section Bottom Line of Part 3.

Last night, after publishing Part 3, a thought ran through my mind: do I actually have to use the bootrom facilities, or could I use my newly gained know-how about flash memory partitions and address translation to implement a pure TrustZone solution?!

In short: yes. Let's have a look.

Pro Memoria: Pure TrustZone Implementation

Pro memoria, this is what we want to implement:

Non-secure world            Secure world
NS memory                   NSC memory                    S memory
Non-secure program          veneers module S0             Secure module S0
+-----------------+         +-----------------+           +-----------------+
|                 |-------->| veneer S0.v0    |---------->| procedure S0.p0 |
|                 |-------->| veneer S0.v1    |---------->| procedure S0.p1 |
|                 |-------->| ...             |---------->| ...             |
|                 |         +-----------------+           +-----------------+
|                 |         veneers module S1              Secure module S1
|                 |         +-----------------+           +-----------------+
|                 |-------->| veneer S1.v0    |---------->| procedure S1.p0 |
|                 |-------->| ...             |---------->| ...             |
+-----------------+         +-----------------+           +-----------------|
         ^                                                         |
         |                  return                                 |
         +---------------------------------------------------------+

Test Program

I use the same test program as in Part 3, found in directory Secure3 in the examples for the Pico2 (repo link at the bottom):

  • directory s:
    • Secure program module S.mod;
    • module S0.mod providing the blink service to the Non-secure program;
  • directory ns:
    • Non-secure program module NS.mod.

Both programs are minimal, with empty modules Main.mod, running with the restart clock frequency.

Flash Memory Partitions and Layout

You may remember from Part 2 that, apart from the program images for the Secure program and the Non-secure program, we also have a third image with the NSC veneers.

Consequently, we need three flash memory partitions (pt.json in directory s):

{
    "$schema": "https://raw.githubusercontent.com/raspberrypi/picotool/master/json/schemas/partition-table-schema.json",
    "version": [1, 0],
    "unpartitioned": {
        "families": ["absolute"],
        "permissions": {
            "secure": "rw",
            "nonsecure": "rw",
            "bootloader": "rw"
        }
    },
    "partitions": [
        {
            "name": "Secure",
            "id": "0",
            "size": "64K",
            "families": ["rp2350-arm-s"],
            "permissions": {
                "secure": "rw",
                "bootloader": "rw"
            }
        },
        {
            "name": "Non-secure",
            "id": "1",
            "size": "512K",
            "families": ["rp2350-arm-ns"],
            "permissions": {
                "secure": "rw",
                "nonsecure": "rw",
                "bootloader": "rw"
            },
            "link": ["owner", 0],
            "no_reboot_on_uf2_download": true,
            "ignored_during_arm_boot": true,
            "ignored_during_riscv_boot": true
        },
        {
            "name": "Non-secure Callable",
            "id": "2",
            "size": "32K",
            "families": ["rp2350-arm-s"],
            "permissions": {
                "secure": "rw",
                "nonsecure": "r",
                "bootloader": "rw"
            },
            "link": ["owner", 0],
            "no_reboot_on_uf2_download": true,
            "ignored_during_arm_boot": true,
            "ignored_during_riscv_boot": true
        }
    ]
}

Note the family ID of rp2350-arm-s for the third partition. If we wanted to load the NSC image by file copying, this would not work, as the bootloader would put the image into partition 0. There is no family ID rp2350-arm-nsc defined, probably since the TrustZone concept implemented in the bootrom does not require it – there is no NSC image to be loaded, the NSC part is right in the bootrom. We could define a custom family ID, implant it into the UF2 file for the NSC image, and set our custom ID for partition 2.

However, we'll use picotool for the image upload, so this does not matter. In fact, as we'll see, we don't even need an UF2 file for the Non-secure image, we can directly load the binary (.bin) as produced by Astrobe. Only the Secure program image needs the IMAGE_DEF embedded in its UF2 file to boot the Secure program.[1]

See Part 3 how to install this partition table in the flash memory.

Here's a depiction of the partition layout:

+-----------------+ 000400000H
|                 |
~                 ~
|                 |
+-----------------+ 00009A000H
| partition 2     |
~ Non-secure      ~
| Callable        |<---------------------+
| 32k             |                      |
+-----------------+ 000092000H           |
| partition 1     |                      |
| Non-secure      |                      | owns
| 512k            |<-------------+       |
| rp2350-arm-ns   |              |       |
~                 ~              |       |
|                 |              |       |
+-----------------+ 000012000H   | owns  |
| partition 0     |              |       |
| Secure          |              |       |
| 64k             |--------------+       |
| rp2350-arm-s    |----------------------+
~                 |
| boot            |
+-----------------+ 000002000H
| partition table |
| slot 1          |
| 4k              |
+-----------------+ 000001000H
| partition table |
| slot 0          |
| 4k              |
+-----------------+ 000000000H

The addresses are inside the flash memory itself.
On the bus, the flash memory is at address 010000000H.

Installing and Accessing the NS and NSC Images

As described in Part 3, we use the hardware translation feature of RP2350's QMI flash memory interface to access the Non-secure (NS) image: we set the second virtual address range to point to the physical flash memory where the NS image is stored: pane 1 with a virtual address range of 0400000H to 0800000H (exclusive), ie. on the bus 010400000H to 10800000H.

I am sure the idea is obvious: we do the same for the NSC image, using pane 2 with 0800000H to 0C00000H. Yes, I know.

See Secure.InstallNonSecImage and Secure.InstallNonSecCallImage for the details and implementation.

Test Program Modules

I'll spare you the full test module listings, and only point out a few interesting parts.

MODULE S;

  (* ... *)

  PROCEDURE configSAU;
    CONST Disabled = 0; Enabled = 1;
    VAR cfg: SAU.RegionCfg; r: INTEGER;
  BEGIN
    (* SRAM *)
    cfg.baseAddr := 020040000H;
    cfg.limitAddr := 020080000H - 1;
    cfg.nsc := Disabled;
    SAU.ConfigRegion(0, cfg);
    (* NS code 512k *)
    cfg.baseAddr := 010400000H;
    cfg.limitAddr := 010480000H - 1;
    cfg.nsc := Disabled;
    SAU.ConfigRegion(1, cfg);
    (* NSC code 32k *)
    cfg.baseAddr := 010800000H;
    cfg.limitAddr := 010808000H - 1;
    cfg.nsc := Enabled;
    SAU.ConfigRegion(2, cfg);
    (* make sure the unused regions are disabled *)
    (* leave reserved region 7 alone, even though it's not used here *)
    r := 3;
    WHILE r < 7 DO
      SAU.DisableRegion(r);
      INC(r)
    END;
    SAU.Enable
  END configSAU;

BEGIN
  configGPIO;
  configSAU;
  Secure.InstallNonSecImage(NSimageAddr);
  Secure.InstallNonSecCallImage(NSCimageAddr);
  Secure.StartNonSecProg(NSimageAddr, VTORoffset)
END S.
  • There is an additional SAU region for the NSC veneers.
  • The module body installs the NS and the NSC images.
MODULE S0;

  (* ... *)

  PROCEDURE* ToggleLED*(led: INTEGER);
  BEGIN
    SYSTEM.PUT(MCU.SIO_GPIO_OUT_XOR, {led});
    (* manually inserted Secure epilogue *)
    (* no add sp,#n as leaf procedure *)
    SYSTEM.EMIT(MCU.POP_LR);
    SYSTEM.EMITH(MCU.BXNS_LR) (* ok within Secure code *)
  END ToggleLED;
END S0.
  • As with the STM32U585, the Secure procedure requires a Secure epilogue.

Astrobe Config Files

Again, there are two config files in the rp2350-secure directory, for the Secure and Non-secure programs, respectively. They are the same as used in Part 3, with one change:

  • Address specs for the Non-secure program:
    • CodeStart = 10400000H, CodeEnd = 010480000H (512k)

In Part 3, CodeStart was 10400100H, ie. with 256 bytes reserved for the IMAGE_DEF data. We don't need this using picotool, see below.

Build

  • Compile and link the Secure program S.mod with the respective config file.
  • Run gen-secure in directory s as follows:
> python -m gen-secure make s0.lst 010001890 010800000

where 010001890 is the absolute address of module S0.mod, read from map file S.map. With possible changes in the library modules as found in the repo as you read this, this value may be different. 010800000 is the virtual address of the NSC image with address translation. gen-secure takes the addresses as issued by the processor upon instruction fetch.

This creates (see Part 2) the NSC binary NSC_S0.bin in directory s, and the Non-secure gateway module NS_SO.mod in directory ns:

MODULE NS_S0;
(* generated, do not edit *)
(* Secure module: S0 *)
(* NSC veneer address: 010800000H *)
IMPORT SYSTEM;
PROCEDURE* ToggleLED*(led: INTEGER);
BEGIN
SYSTEM.EMITH(0B001H); (* add sp,#4, fix stack *)
SYSTEM.EMIT(0F8DFB004H); (* ldr.w r11,[pc,#4] *)
SYSTEM.EMITH(04758H); (* bx r11 *)
SYSTEM.ALIGN; (* word alignment *)
SYSTEM.DATA(010800001H); (* nsc target address *)
END ToggleLED;
END NS_S0.
  • Compile and link the Non-secure program NS.mod with the respective config file.
  • Create the UF2 file for the Secure program by running, in directory s:
> python -m make-uf2 rp2350 s.bin
  • Upload the three images by running, in this order, with the RP2350 in BOOTSEL mode:

In directory ns:

> picotool load -v -p 1 ns.bin

Note that we directly upload the .bin file, no need for an UF2.

In directory s:

> picotool load -v -p 2 nsc_s0.bin
> picotool load -v -x -p 0 s.uf2

The RP2350 will restart and run the Secure and eventually the Non-secure program.

Bottom Line

There we have it, a pure TrustZone implementation, simple and straightforward one-to-one procedure calls from NS to S code. No dispatch handler, no lookup table, no parameter weirdness.

Going forward, I will use this implementation on the RP2350. I'll leave the current functionality for the bootrom-based solution in module Secure though. For now.

Now I can focus on the open points listed in Secure/Non-secure Part 2 – implementation (STM32), with the same Secure/Non-secure concepts and implementation on the RP2350 and the STM32 MCUs.

Phew.

Repository

  • lib/v3.0 <repo>/examples/v3.0/rpi/pico2

  1. I'll need to check if we even need UF2 files in general. The metadata could possibly be added directly to the .bin, if we use picotool for uploading. ↩︎

Last updated: 13 February 2026