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.modproviding the blink service to the Non-secure program;
- Secure program module
- directory
ns:- Non-secure program module
NS.mod.
- Non-secure program module
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)
- CodeStart =
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.modwith the respective config file. - Run
gen-securein directorysas 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.modwith 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
BOOTSELmode:
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
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 usepicotoolfor uploading. ↩︎
Last updated: 13 February 2026