Secure/Non-secure Part 3
lib/v3.0 Prototype: Secure/Non-secure programs – RP2350.
Overview, Purpose
In Secure/Non-secure Part 1 – basics we had explored some basics and mechanisms of Secure and Non-secure programs and library modules using Oberon and Astrobe for RP2350. In Secure/Non-secure Part 2 – implementation (STM32) we then created a solution with fully enabled Secure protection and separation.
This document describes the basic concepts and a prototype implementation on the RP2350.
You may also want to have a look at Secure/Non-secure Part 4 – RP2350, update.
Anatomy of a Secure and Non-Secure Program Combo
Pro memoria, here's the relationships and the control flow for a program consisting of both Non-secure and Secure parts.
Non-secure world Secure world
+------------+
reset ---> | Program S |
+------------+
+------------+ start | | call
| |<-----------+ V
| Program NS | +------------+
| |--------->| lib S0 |
+------------+ call +------------+
| call ^
V |
+------------+ call |
| Lib NS0 |-----------------+
+------------
-
The program as a whole always starts with the Secure program, that is, after reset, the code must run as
Secureprivileged. The Secure code sets up all the memory separation and other resource isolation components and parameters. -
The Secure program then passes control to the Non-secure program, which is usually the actual control program.
-
The Non-secure program and its library modules can make use of the Secure library modules.
-
The Secure software can access Non-secure memory, including calling procedures, and load/store operations in SRAM (not depicted above).
-
Program development is usually organised in two projects, one for Secure and one for the Non-secure world. The projects can be staffed by the same teams, of course, but the code of the two projects is completely separated on a technical level, with the Secure side providing interface code to the Non-secure side, but not the Secure modules, which are never directly imported into Non-secure modules.
-
In fact, both projects each result in separate program images that are loaded into the MCU program memory at address regions with different security attributes. The Non-secure project can update its modules and the resulting program image, with the Secure code staying unchanged in place in the physical memory. The Secure project can change its code independently as well – under certain conditions.
-
The Secure code usually provides services to the Non-secure program. For this, the Secure developers provide an Oberon Non-secure interface module – part of the aforementioned interface – that is being linked into the Non-secure program, which will then execute the protocol to invoke the corresponding procedures contained in the Secure image.
-
The Secure code is not directly accessible to the Non-secure code. TrustZone and other security components enforce a strict separation in hardware. The separation parameters and their corresponding implementation in hardware are either defined in non-volatile memory in the MCU, and loaded upon reset before the software even runs, or defined by the Secure program after reset.
TrustZone and Other Security Components
Please refer to Part 2 for more information about TrustZone.
Like other MCUs, the RP2350 has additional security components, since TrustZone alone with the IDAU and the SAU is not sufficiently granular and flexible. On the Cortex-M33, for example, the maximum SAU regions is 8.
The RP2350 centralises the granular Secure/Non-secure configuration in the ACCESSCTRL block, where each component of the MCU has a dedicated register. The reset configuration is not the same for each component and peripheral device. SRAM, for example, is fully open after reset, while GPIO, the UARTS, etc. are locked down to Secure privileged. Since the MCU starts in this mode, programs that don't employ Secure/Non-secure separation, simply work without having to configure anything.
It's a systematic and useful concept. I wish other MCUs did the same.
The RP2350's Secure/Non-secure Concepts
While basically TrustZone-compliant, the approach to Secure/Non-secure separation is pretty different with the RP2350, compared to the STM32U585:
STM32U585:
Each Secure procedure available to Non-secure programs is called directly, via a simple redirection using branches, with exactly the same procedure signature.
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 |
+---------------------------------------------------------+
Each Secure procedure is callable independently of the others, within the confines of the program logic, and possibly resource lockout and protection, just as any program. In particular, two cores can use two different Secure procedures at the same time, give there's not data or resource conflict. If the Secure procedures are implemented in a re-entrant manner, the two cores could even use the same procedure at the same time.
In summary, it's a lightweight, but very modular and flexible approach. In fact, it's the concept that ARM recommends, and is described in the corresponding documentation.
RP2350:
The RP2350, on the other hand, employs a dispatcher (or handler) approach, using functionality in the bootrom. If you're familiar with trap handlers in operating systems and kernels you'll recognise the pattern.
Non-sec world Secure world
NS memory NSC memory S memory
Non-secure part of
program bootrom Secure module S0
+-----------+ +----------+ call +-----------------+
| | | | /<---->| procedure S0.p0 |
| | call | | call +---------+ |<---->| procedure S0.p1 |
| |<------->| 1 veneer |<----->| handler |--|<---->| ... |
| | return | | ret +---------+ | +-----------------+
| | | | | | Secure module S1
| | +----------+ +---------+ | +-----------------+
| | | lookup | |<---->| procedure S1.p0 |
| | | table | \<---->| ... |
+-----------+ | | ret +-----------------|
+---------+
There is basically one veneer, implemented in the bootrom, and the Secure program can install one handler to be called by that single veneer when the Non-secure program wants to call a Secure procedure:
set_rom_callbackto install the handler by the Secure software;secure_callto call a Secure procedure from the Non-secure program, via the handler.
The IDAU sets up an NSC region on the veneer code in the bootrom, and the bootloader defines an SAU NS region there, so the resulting security attribution is the required NSC (region 7 of the SAU is reserved for this purpose).
In lieu of the straight-through "one-to-one" calls with a pure TrustZone implementation, the Non-secure program has one single entry point to the Secure software, which calls the handler[1]. The arguments for the Secure procedure are passed to that handler, together with an integer key. The handler has a lookup table where it matches the key to the Secure procedure, which it then calls. Note that each call returns to its caller.
The handler needs to be implemented by the Secure programmer in Secure memory, including the lookup table and the corresponding lookup algorithm. The one handler and lookup table needs to cover the whole Secure side, that is, each Secure module to be exposed to the Non-secure program.
All parameters for the Secure side need are passed to secure_call, which passes them to the Secure handler, which passes them to the Secure procedure. That is, the signature of secure_call essentially stands for each and every possible signature of Secure procedures. We'll discuss the implications below.
Since we deal with C code in the bootrom, with its own peculiarities, for example the procedure calling conventions.
Test Program Overview
The test program is again a simple blinker, with the Non-secure program calling into the Secure code to operate the LED. It can be found in directory Secure2 in the repo (link at the bottom of this page).
Secure2 contains two directories, s for the Secure code, ns for the Non-secure one.
As before, the code is minimal, for example with an empty module Main, in order to reduce any dependencies on existing modules, whose implications on the Secure/Non-secure separation I do not yet fully understand.
Flash Memory Layout
As with the STM32U585, I'll only cover running both Secure and Non-secure code from flash memory.
The Pico2 has 4 MB of flash memory. It is what I am covering here. There are Pico2-compatible boards out there with larger flash memory chips. The RP2350 can address 32 MB of flash memory, divided into two physical banks of 16 MB (two chip select lines for the QSPI interface). Note that the flash is on the Pico2 board, not inside the MCU.
The Secure and Non-secure code is loaded into separate flash memory partitions. That is, we need to define a partition table definition using RPi's picotool.
Here's partition definition (see pt.json in the test program s directory):
{
"$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",
"size": "64K",
"families": ["rp2350-arm-s"],
"permissions": {
"secure": "rw",
"bootloader": "rw"
}
},
{
"name": "Non-secure",
"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
}
]
}
You create a partition table thusly:
> picotool partition create pt.json pt.uf2
> picotool erase
> picotool load pt.uf2
> picotool reboot
Check the installed partition table using
> picotool partition info
which prints
un-partitioned_space : S(rw) NSBOOT(rw) NS(rw), uf2 { absolute }
partitions:
0(A) 00002000->00012000 S(rw) NSBOOT(rw) NS(-), "Secure", uf2 { rp2350-arm-s }, arm_boot 1, riscv_boot 1
1(A ob/ 0) 00012000->00092000 S(rw) NSBOOT(rw) NS(rw), "Non-secure", uf2 { rp2350-arm-ns }, arm_boot 0, riscv_boot 0
Here's a depiction of the partition layout:
+-----------------+ 000400000H
| |
~ ~
| |
+-----------------+ 000092000H
| partition 1 |
| Non-secure |
| 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.
The Secure software gets loaded into the boot partition, the Non-secure part into the partition owned by the boot partition. The usual UF2 files can be used, even though the Non-secure image requires a different family ID rp2350-arm-ns to be loaded automatically into the Non-secure partition, but this can be worked around using picotool to load the image directly into a partition.[2]
As you see, the physical address in the flash memory is now different than when loading a single image, which gets loaded at physical address 0H. However, the base linker address, ie. the corresponding setting in Astrobe's config file, remains the same. The bootloader will set up the corresponding address translation in the QSPI Memory Interface (QMI). This translation happens in hardware. More about address translation below, since it's central to get the Non-secure image started.
Accessing the Non-secure Image
The concept of partitions allows to change the flash memory layout without the need for rebuilding the programs. But – if the Non-secure program is not fixed in memory, how to access the two crucial addresses at the start of the image to read the initial stack pointer and the entry point? You translate the image address in the flash memory to a well-known address.
The flash memory is accessed via the QSPI Memory Interface (QMI), which provides a linear address space, while reading the corresponding instructions serially from the actual flash chip (via QSPI). This linear address range of 16MB[3] is divided into four so called panes of 4MB. Each of these pane address ranges can be translated to a different physical address range in the flash memory. It's like a simple virtual address scheme.
So, to access the Non-secure image, we set up the second pane, with an address range of 4MB from 0400000H to 0800000H (exclusive) to point to the physical flash address of the image. For this, we configure register QMI_ATRANS1 accordingly. Now we can access the Non-secure image at bus address 010400000H.
See Secure.InstallNonSecImage for the details and implementation.
Noteworthy Library Modules
Module Secure
Module Secure implements the security features as needed and used for this test program:
- the just mentioned installation of the Non-secure image at a virtual address;
- starting the Non-secure program;
- a simple (example) Secure handler that can be installed via bootrom;
- installing a Secure handler;
- adding and removing Secure procedures into the lookup table for the Secure handler.
The Secure handler is really simple – it uses an array to store the key and procedure address of the Secure procedures, and linearly searches this array for a key match when the Non-secure uses secure_call to call one of the Secure procedures. The single Secure handler for a complete program is already a bottleneck by design and concept, and a linear search might add insult to injury, just not being sufficiently fast for a big number of Secure procedures. Of course, other implementations of the lookup table can be used. The key is 30 bits, so a systematic structuring of the key, combined with pattern matching, could be a first measure to tackle this issue.
Module Bootrom
The RP2350's bootrom (32k) implements a plethora of functions, among them the ones to access the flash partition information, as well as the aforementioned functionality to install the Secure handler (set_rom_callback), and to call it (secure_call).
Module Bootrom implements, or interfaces, the functionality as required for this test program.
There are "raw" basic bootrom API equivalent procedures, plus some derived ones that make use of the basic procedures. The functional structure is the same as with the RP2040: first you get a lookup function, to which you then pass a two letter code to get back the actual address of the function for this code. This redirection allows to update the bootrom code itself without invalidating existing code that calls the bootrom API.
A first note about C code, and the related conventions and habits as employed in the bootrom. Most the bootrom API functions return a value, ie. they are function procedures in Oberon parlance. The returned value is either an actual result, but mostly it's an error/success code. I cannot speak for the Oberon community at large, but for me this goes against the very concept of a function.[4] Consequently, the procedures in module Bootrom use a VAR res parameter to signal success and error, in an attempt to bring the C API closer to what I consider the right thing to do in Oberon. Your mileage may vary.
A second note concerns the C function calling conventions as defined by ARM for their processors. The first four arguments are passed in registers r0 to r3, the rest on the stack. r0 to r3 are caller-saved registers, ie. the callee is free to use them in any way (including r12). The other registers are callee-saved, that is, if the callee wants to use them, they must be preserved upon function entry, and restored on exit. This is different from Oberon, where all registers are caller-saved. Consequently, as long as we only call C code from Oberon, we're safe. If C code were to call Oberon code, we would need to make sure the C conventions regarding registers other than r0 to r3 are not violated. As outlined above, this only happens when our Secure handler is called via secure_call in the bootrom.
The C calling rules also stipulate that the stack pointer on function entry is double-word aligned (8 bytes). Oberon does not adhere to this rule. I haven't implemented any functionality for this, but have not encountered any problems yet. Maybe I was just being lucky?
As with Oberon, a function result is returned in register r0.
As an aside, you'll recognise the match between the above register rules and the stack frame created by an exception. This allows exception handlers in C to be coded and compiled as normal functions.
Test Program Modules
We again have module S.mod, NS.mod, and S0.mod, for the Secure program, the Non-secure program, and the Secure code providing a service to the Non-secure side, respectively.
MODULE S;
IMPORT SYSTEM, MCU := MCU2, Main, Secure, SAU, AccessCtrl, NSC_S0;
CONST
NSimageAddr = 010400000H;
LEDpico = 25;
LED0 = 27;
LED1 = 28;
LED2 = 26;
LED3 = 22;
LEDs = {22, 25, 26, 27, 28};
PROCEDURE setFunction(pinNo: INTEGER);
CONST PADS_ISO = 8;
VAR addr, x: INTEGER;
BEGIN
addr := MCU.IO_BANK0_GPIO0_CTRL + (pinNo * MCU.IO_BANK0_GPIO_Offset);
SYSTEM.GET(addr, x);
BFI(x, 4, 0, MCU.IO_BANK0_Fsio);
SYSTEM.PUT(addr, x);
addr := MCU.PADS_BANK0_GPIO0 + MCU.ACLR + (pinNo * MCU.PADS_BANK0_GPIO_Offset);
SYSTEM.PUT(addr, {PADS_ISO})
END setFunction;
PROCEDURE configGPIO;
CONST Dev = {MCU.RESETS_IO_BANK0, MCU.RESETS_PADS_BANK0};
VAR done: SET;
BEGIN
(* release resets *)
SYSTEM.PUT(MCU.RESETS_RESET + MCU.ACLR, Dev);
REPEAT
SYSTEM.GET(MCU.RESETS_DONE, done)
UNTIL done * Dev = Dev;
setFunction(LEDpico);
setFunction(LED0);
setFunction(LED1);
setFunction(LED2);
setFunction(LED3);
(* enable outputs and clear *)
SYSTEM.PUT(MCU.SIO_GPIO_OE_SET, LEDs);
SYSTEM.PUT(MCU.SIO_GPIO_OUT_CLR, LEDs);
(* set LED pin to Non-secure access *)
(* note: LEDpico remains Secure *)
AccessCtrl.SetGPIOnonSec(MCU.ACCESSCTRL_GPIO_NSMASK0, {LED0});
END configGPIO;
PROCEDURE configSAU;
(* Region 7 is used and reserved for the bootrom, set by the bootloader,
with baseAddr = 000046A0H limitAddr = 00007FFFH;
important here is that this region allows the IDAU configuration
of NSC for the SecureCall mechanism.
*)
CONST Disabled = 0;
VAR cfg: SAU.RegionCfg; r: INTEGER;
BEGIN
(* SRAM *)
cfg.baseAddr := 020040000H;
cfg.limitAddr := 020080000H - 1;
cfg.nsc := Disabled;
SAU.ConfigRegion(0, cfg);
(* code *)
cfg.baseAddr := 010400000H;
cfg.limitAddr := 010480000H - 1;
cfg.nsc := Disabled;
SAU.ConfigRegion(1, cfg);
(* make sure the unused regions are disabled *)
r := 2;
WHILE r < 7 DO
SAU.DisableRegion(r);
INC(r)
END;
SAU.Enable
END configSAU;
BEGIN
configGPIO;
configSAU;
Secure.InstallHandler(Secure.Dispatch);
NSC_S0.InstallProcs;
Secure.InstallNonSecImage(NSimageAddr);
Secure.StartNonSecure(NSimageAddr)
END S.
-
The SAU regions (remember that SAU regions only can punch NS or NSC holes into basically Secure address ranges):
- region 0:
020040000Hto02007FFFFHas Non-secure SRAM - region 1:
10400000Hto01047FFFFHas Non-secure flash - region 7:
000046A0Hto00007FFFHas Non-secure Callable flash – not set here, but by the bootloader
- region 0:
-
I have used some LEDs to debug the code, which are all set up here directly without the GPIO module.
-
Note how LED0 used in
NS.modis set for Non-secure use using moduleAccessCtrl, but the toggled LED remains Secure, as it will be operated by the Secure code inS0.mod. -
NSC_S0.InstallProcs: see below
MODULE NS;
IMPORT SYSTEM, Main, MCU := MCU2, SO := NS_S0;
CONST
LEDpico = 25;
LSET = MCU.SIO_GPIO_OUT_SET;
LED0 = 27;
PROCEDURE run;
VAR i, x: INTEGER;
BEGIN
SYSTEM.PUT(LSET, {LED0});
REPEAT
x := S0.ToggleLED(LEDpico); (* Secure call *)
i := 0;
WHILE i < 1000000 DO INC(i) END
UNTIL FALSE
END run;
BEGIN
run
END NS.
NS_S0: see below
MODULE S0;
IMPORT SYSTEM, MCU := MCU2;
PROCEDURE* ToggleLED*(led: INTEGER);
BEGIN
SYSTEM.PUT(MCU.SIO_GPIO_OUT_XOR, {led})
END ToggleLED;
END S0.
- Note that no specific Secure epilogue is required.
Interface Modules
Above, there are two references to interface modules:
NS_S0NSC_S0
Let's dive into this…
The bootrom function secure_call expects four 32 bit parameter values in registers r0 to r3, plus the integer index key in register r4 for the Secure procedure to execute via the dispatch handler. The parameter values can be required by the Secure procedure or not, but this signature has to be adhered to.[5]
What is important here: the actually relevant parameters are between the Non-secure caller and the Secure callee, both regarding the number and their type. secure_call simply transports the arguments as raw 32 bit values, and passes them to the Secure handler, which in turn passes them to the Secure callee. The Secure handler and the Secure callee are Oberon code, that is, the four arguments will be passed in registers r0 to r4 to the callee.
So far so good. But – what if the callee actually has fewer than four parameters? On the Non-secure side, four arguments must be provided to keep secure_call and the whole call chain happy, but on the Secure side the callee will only "pick" the relevant ones from registers r0 to r3. So we can provide dummy values for the unused argument slots, which will get lost as soon as the Secure callee executes its prologue. No harm done, just some overhead on the calling side.[6]
The case of more than four parameters I have not investigated yet. The C convention would put the extra ones on the stack, where the callee would retrieve them, which is what the C compiler does automatically. But not the Oberon compiler, which only uses registers for this purpose. Also, the arguments would be on the Non-secure stack, which actually can be accessed from the Secure code, but would need specific coding. Not sure if the C compiler would handle that case. Anyway, to be explored.
Another question concerns the type of the parameters. On a very basic level, this is again between the Non-secure caller and the Secure callee, which will interpret and handle the transported 32 bit values according to the declarations of their arguments. Of course, the procedure signature the Non-secure side "sees" must be the same as the one of the Secure code, that is, we should be able to compile and link the Non-secure code either by directly importing the Secure module, or across the Secure/Non-secure separation without changes. In the same vein, the Secure module should not require changes just because it is executed on the Secure side.
However, since Oberon is a statically and strictly typed language, what are the corresponding types used in Bootrom.SecureCall and the Secure handler, such as Secure.Dispatch?
It would be tempting to use ARRAY 4 OF BYTE parameters, for which the compiler will accept the assignment of any four byte value. But we cannot pass constant values this way.
For now, I have just used INTEGER, since it's easy to cast most other basic values to INTEGER using ORD:
SecProc* = PROCEDURE(p0, p1, p2, p3: INTEGER): INTEGER;
But questions remain, not least if we have a ARRAY or RECORD parameter.
The interface module for the Non-secure code, below, is handcrafted, but I will try to write one of my infamous tools to generate it from the Secure module SO.
NS_S0 represent SO, so it must have the same API: the Non-secure code should not require changes to use Secure procedures.
MODULE NS_S0;
IMPORT Bootrom;
CONST UserProcKey = 0C0000000H;
PROCEDURE ToggleLED*(led: INTEGER): INTEGER;
RETURN Bootrom.SecureCall(led, 0, 0, 0, UserProcKey + 42)
END ToggleLED;
END NS_S0.
Note the dummy values passed to Bootrom.SecureCall. UserProcKey is the index key range allocated for private functionality as per the reference manual, 42 is a randomly chosen number here (well, maybe not so randomly).
Module NSC_S0 sets up the interface between the dispatch handler and the Secure code. As with NS_S0 this could be generated by a tool, but is handcrafted here.
As with NS_S0 and the Non-secure software, the Secure software should not require specific changes only to be able to be called from Non-secure world. At least that should be an objective.
MODULE NSC_S0;
IMPORT SYSTEM, Secure, Bootrom, S0;
TYPE SecProc = Secure.SecProc;
PROCEDURE Install*;
VAR res: INTEGER;
BEGIN
Secure.AddSecProc(SYSTEM.VAL(SecProc, S0.ToggleLED), 42, res)
(* error handling here *)
END Install;
END NSC_S0.
Here we type cast the Secure procedure, to have it accepted by Secure.AddSecProc, which expects a SecProc. This works down to the execution – see Secure.Dispatch --, since a procedure is reduced to an address here. As outlined above, the signature of the Secure procedure does not really matter when it's called: extra arguments it does not expect will be discarded by its prologue, and the types of the used arguments will be interpreted as per the procedure code. Also any return value will be deposited in register r0, and it's the caller that will make sense of it.
As an aside, the name NSC_SO is a bit misleading, since it does not contain any NSC code.
Build and Install
Astrobe Configuration Files
There are two configuration files in directory rp2350-secure:
-
Address specs for the Secure program:
- CodeStart =
10000100H, CodeEnd =010010000H(64k) - DataStart =
020000000H, DataEnd =020040000H
- CodeStart =
-
Address specs for the Non-secure program:
- CodeStart =
10400100H, CodeEnd =010480000H(512k) - DataStart =
020040000H, DataEnd =020080000H
- CodeStart =
Note the base code address for the Non-secure program in the translated range, as described above.
Build
- Compile and link each program with their respective config file.
- Create the UF2 files running
> python -m make-uf2 rp2350 s.bin
> python -m make-uf2 rp2350 ns.bin
in their respective directories. This will create a Non-secure program with the wrong family ID, so we need to use picotool for the upload. I am working on an updated version of make-uf2.
Upload
Set the Pico2 into BOOTSEL mode, and run, in this order
> picotool load -v -p 1 ns.uf2
> picotool load -v -x -p 0 s.uf2
in their respective directories.
Bottom Line
The Secure/Non-secure concepts and implementation of the RP2350 have somewhat surprised me, I didn't expect this after working with the STM32U585, which directly follows ARM's guidance. I am not sure I understand the motivation – maybe that we don't need any specific compiler features to support TrustZone? But gcc offers them.
I am not yet sure if the concept of the partitions in flash memory profit of this construction. Yes, the partitions, with the possibility of A/B partitions, which allow updating the code without overwriting the existing program, roll-back, and all that, may be useful features.[7]
I may well not understand everything well enough yet to understand the big benefits of the RP2350's implementation of TrustZone, extended by additional controls as outlined above (ACCESSCTRL).
Having said that, I see quite a few drawbacks, among them:
-
The dispatch handler is a bottleneck, adding execution overhead, in particular for the lookup.
-
A lot of scaffolding is required, unlike with a pure, straight-forward implementation.
-
Lots and lots of type casting is required; this may be easier and usual with
C, where type casting is just a fact of programming life, but I think it runs counter to Oberon's basic concepts, or, dare I say it, philosophy. -
The
secure_calland thus handler interface with its fixed four parameters appears to be limiting and cumbersome, compared to the pure implementation, where this is a non-issue. -
The partition table is an additional potential failure point, which is not specific to the Secure concepts, but I haven't used them up to now, so I'll add it here anyway.
I will need to review the concepts and my implementation attempt for this test program. It works, but there are open points, for example:
-
parameters other than the basic types, that is,
ARRAYandRECORD; -
procedure signatures with more than four parameters.
Plus there are the open points listed in Secure/Non-secure Part 2 – implementation (STM32).
You may have realised by now that I am not overly enthusiastic about the Secure/Non-secure concepts and implementation on the RP2350. Compared to the lightweight and straightforward pure TrustZone solution it's complex and clunky. Such TrustZone solution has one-to-one matching procedure signatures between the Non-secure and the Secure worlds, without the passed arguments ever leaving the registers where they were assigned by the Non-secure software, and corresponding direct and fast calls (a constant and predictable three instructions overhead, important for real-time control programs). Compare that to what I have tried to describe above. I'll admit that this view may be due to my current, possibly limited understanding, and may change. Fingers crossed.
Related Work
In parallel, I have done some exploration on the STM32U585 with ARRAY and RECORD parameters. They work, but type testing is an open question.
An updated version of make-uf2 is in the works to assign the correct family ID to Non-secure programs.
Repository
lib/v3.0<repo>/examples/v3.0/rpi/pico2
Called callback in the documentation. But also the called Secure procedures are called callbacks, which can be confusing, in particular when you're in the process of learning the ropes. ↩︎
I have had mixed experience with loading the UF2 files via drag-and-drop onto the drive. I have used
picotoolfor now, leaving the drag-and-drop (or copy) issue for later. I may incorporate the use ofpicotoolinto themake-uf2tool in general, in lieu of the file copying. ↩︎Or two linear address ranges of 16MB if we have two flash memory chips. ↩︎
In its purest form, a function takes input values, operates on them, and returns a result – just like a mathematical function. I would "allow" :) also reading stored values and reading hardware status for the operation on the input values, ie. the arguments. But a function (procedure) does not have side effects, be it storing information or – gasp! – manipulating hardware. (I really hope I have adhered to this principle throughout my Oberon RTK code…) ↩︎
Not sure if
Csupports variadic parameter lists, though. And if yes, if they would work with a fixed required argument for the key inr4. InLua, for example, the variadic arguments can only be at the end of the argument list, if memory serves. ↩︎More precisely, the caller side does not even need to provide the dummy arguments, but the index key must be in register
r4. It's just the easiest at this point to use dummy arguments to achieve that. ↩︎MCUs such as the STM32U585 offer similar features, if implemented differently. Regarding life cycle management they go further. ↩︎
Last updated: 13 February 2026