2б. STM32U5 and TrustZone: how the new generation differs from the F4
The STM32F405 is the workhorse of the decade: Cortex-M4, 168 MHz, a megabyte of Flash. The STM32U585 from the new U5 line has almost the same frequency, but inside there is a different core — the Cortex-M33 — and this core has something the M4 lacks entirely: TrustZone, a hardware division of the program into "secure" and "non-secure" parts. This section is about what the U5 gives compared with the F4 and how TrustZone works inside — with a live experiment in an emulator.
The U585 figures are taken from its datasheet, TrustZone behaviour on the U5 from
the family reference manual, the transition rules and registers
from the Cortex-M33 core manual, the F405 parameters from the description of the Keil
support pack. The experiment was built with the arm-none-eabi-gcc compiler and run
in QEMU on a board with a Cortex-M33 (mps2-an505). Checked by _verify/tz2b_check.py.
Terms used in the documentation
Term Meaning Secure / Non-secure state the two security states of the core Non-secure callable (NSC) a region from which non-secure code may enter secure code SAU — Security Attribution Unit the security attribute unit (configured by software) IDAU — Implementation Defined Attribution Unit security attributes fixed by the chip vendor SG — Secure Gateway the gateway instruction: entry into the secure state veneer a gateway made of SG and a branch to the real function CMSE C language extensions for the secure part ( -mcmse)
2б.1 U585 versus F405: what has changed
| STM32F405RG | STM32U585CI | |
|---|---|---|
| Core | Cortex-M4 with FPU | Cortex-M33 with FPU and TrustZone |
| Frequency | up to 168 MHz | up to 160 MHz, 240 DMIPS |
| Flash | 1024 KB | 2 MB |
| RAM | 192 KB | 786 KB |
| Run-mode consumption | 238 µA/MHz (40 mA at 168 MHz) | 19.5 µA/MHz at 3.3 V |
| Stop 2 (16 KB of RAM retained) | Stop: 280 µA, all RAM retained | 4.0 µA |
| Standby / Shutdown | 2.2 µA / none | 210 nA / 160 nA |
The main thing here is not the frequency — it is almost the same — but three things: four times more RAM, sleep modes consuming micro- and nanoamperes (U stands for "ultra-low-power") and TrustZone. The F405 is chosen when you need a proven chip with a huge base of examples; the U5 when the device lives on a battery, must store keys and be updated over the air so that substituted firmware cannot get to the secrets.
How to read the part number STM32U585CIU6: U — ultra-low-power,
585 — the subfamily with USB OTG and hardware AES, C — 48 pins,
I — 2 MB of Flash, U — UFQFPN package, 6 — the industrial range
−40…85 °C.
2б.2 The idea of TrustZone: two worlds in one core
On a Cortex-M33 with TrustZone every address has a security attribute: secure (Secure), non-secure (Non-secure) or secure with the right of entry from outside (NSC). At any moment the core is in one of two states. In the secure state everything is accessible to it; in the non-secure state an access to a secure address is blocked and raises an exception.
The attribute of an address is assembled from two sources:
- the IDAU — set by the chip vendor, the program does not change it;
- the SAU — several regions that the secure firmware configures
through the registers from
0xE000EDD0.
If the sources disagree, the stricter attribute wins. In the experiment below this rule fired literally: the SAU declared an NSC gateway window, while the board's IDAU considered it simply secure — and the entry did not happen.
Why a separate NSC type? The gateway instruction SG is allowed only in it. If
it were allowed in all secure memory, any constant that happened to
coincide with the SG encoding would become a door inside. By limiting the gateways to a narrow
window, the firmware vendor knows exactly where its doors are.
2б.3 How to move between the worlds
| From | Where the program goes | What happens |
|---|---|---|
| secure | a non-secure address | the transition only with a BXNS/BLXNS instruction with address bit 0 cleared; otherwise SecureFault |
| non-secure | a secure address in the NSC window | if there is an SG there — entry into the secure state; if not — SecureFault |
| non-secure | a secure address outside NSC | SecureFault |
Returning from a secure function is also done with BXNS. No
system calls as in a big OS: calling a secure function is an ordinary
BL, just to the address of the gateway.
2б.4 The experiment: two firmwares in an emulator
The experiment consists of two separate programs, as on a real U5.
- The secure one (
s_main.c) starts first — after reset the core is always in the secure state. It configures the SAU (region 0 — the code of the non-secure part, 1 — its RAM, 2 — the NSC gateway window), opens to the non-secure part the second halves of the board's RAM in its memory controllers, sets up its vector table and stack and jumps into it viaBLXNS. - The non-secure one (
ns_main.c) knows only two addresses of the secure one — the gateways from theveneers.ofile, which the linker of the secure part created. The UART is not accessible to it, so it prints through a secure function.
The secure part exports two functions:
uint32_t __attribute__((cmse_nonsecure_entry)) s_sign(uint32_t x)
{
s_calls++;
return x ^ secret_key; /* «подпись» — наружу уходит результат, не ключ */
}
void __attribute__((cmse_nonsecure_entry)) s_print(const char *msg)
{
/* указатель пришёл из небезопасного мира: читать только проверенное и только из своей копии */
char buf[33];
for (;;) {
if (!cmse_check_address_range((void *)msg, 32, CMSE_NONSECURE)) {
puts_s("s_print: отказ — адрес не небезопасный\n");
return;
}
int n = 0;
while (n < 32 && (buf[n] = msg[n]) != 0) /* копия: проверенное снаружи уже не подменить */
n++;
buf[n] = 0;
puts_s(buf);
if (n < 32)
return; /* строка кончилась внутри проверенных 32 байт */
msg += 32; /* следующие 32 байта — снова через проверку */
}
}
The run:
S: старт в безопасном состоянии
S: SAU и MPC настроены, BLK_MAX ssram-0 0x0000007f
S: переход в небезопасную часть
NS: запуск в небезопасном состоянии
NS: s_sign(0x12345678) = 0x17d8449f
NS: подсовываю s_print безопасный адрес 0x38000000
s_print: отказ — адрес не небезопасный
NS: читаю безопасное ОЗУ 0x38001000
SecureFault: SFSR=0x00000008 SFAR=0x00000000
done
Three things happened as planned. The non-secure part received the "signature"
0x17d8449f, but not the key. The attempt to slip the secure function a pointer
to secure memory was rejected by the function itself. A direct read of secure RAM
stopped the core with a SecureFault exception.
Sizes: the secure part is 1293 bytes of code, the non-secure one 482.
2б.5 What the compiler did: the gateway and register clearing
The -mcmse flag and the cmse_nonsecure_entry attribute make the compiler
and the linker do two things for each such function.
A gateway in the NSC window. At address 0x10100000 the linker placed 8 bytes:
10100000 <s_sign>:
10100000: e97f e97f sg
10100004: f700 b86a b.w 100000dc <__acle_se_s_sign>
The function itself lives at an ordinary secure address 0x100000dc under the name
__acle_se_s_sign, and jumping to it directly from the non-secure world
is impossible — there is no SG there, and the address is not in the NSC window. The linker collects the gateways
into the .gnu.sgstubs section, and the linker script of the secure part places it in the NSC window.
The options -Wl,--cmse-implib -Wl,--out-implib=veneers.o write a file for the
non-secure part. The veneers.o file contains only the gateway addresses: s_sign = 0x10100000, s_print = 0x10100008.
Clearing on exit. Before bxns lr the function overwrites r1–r3
and ip with the value of lr and clears the flags (msr CPSR_fs, lr). r0 carries the
result, while everything left in the registers from computations with the key does
not leave. Without this clearing the key could leak through "garbage" in the registers.
Checking pointers is your job. The compiler does not know where
msg points. The cmse_check_address_range function asks the core (with the TT instruction)
whether the whole range is accessible to the non-secure world; without it the non-secure part
would make the secure one read a secret for it. Check exactly what
the function will read: if you check 32 bytes but print up to the zero byte,
a string without a zero right at the boundary of non-secure RAM will lead the printing into
secure memory. That is why s_print reads in chunks of 32 bytes, each one
only after a check, and prints its own copy: the checked string can no longer be substituted
from an interrupt of the non-secure part.
2б.6 SecureFault and the SFSR register
SecureFault is a separate Cortex-M33 exception; so that it does not turn
into a HardFault, it is enabled with the SECUREFAULTENA bit (bit 19 in SHCSR). The cause
is shown by SFSR at address 0xE000EDE4:
| Bit | Name | When |
|---|---|---|
| 0 | INVEP | non-secure code entered secure code at an address without a gateway or outside NSC |
| 3 | AUVIOL | non-secure code accessed a secure address |
In the experiment the direct read gave SFSR = 0x00000008 — AUVIOL. The second run —
the same firmware, but without the CODENSC bit in the board's NSCCFG register — failed
on the very first call:
S: переход в небезопасную часть
SecureFault: SFSR=0x00000001 SFAR=0x00000000
This is INVEP: the SAU considered the 0x10100000 window an NSC region, but the board's IDAU considered it simply
secure, and the stricter one wins. The core manual describes this
cause directly: the target of the branch is an SG instruction, but there is no SAU or IDAU region
with the NSC flag.
SFAR is zero in both cases: the violation address there is valid only
when the SFARVALID bit (bit 6) is set; here it is not set. This is a peculiarity of the
emulator: on silicon, on an AUVIOL during a data read, the core writes the access address into SFAR
and sets SFARVALID. So first look at this bit,
and determine the cause from the SFSR bits.
2б.7 TrustZone on a real STM32U5
The experiment shows the mechanics of the core; the U5 has its own surroundings around it.
- By default TrustZone is disabled in all STM32U5. It is enabled by the option
bit
TZENinFLASH_OPTR, and only at read protection level RDP 0. The way back is more expensive:TZENcan be cleared only together with a regression of read protection from level 1 to level 0 — RDP is first raised to 1, then returned to 0, and the regression erases the Flash and all the RAM. From level 2 there is no regression at all unless the OEM2 key has been written beforehand, and even with it only down to level 1. Before settingTZENon a development board, it is worth knowing how to clear it. - After it is enabled, the core comes out of reset in the secure state, the boot address must point into secure memory, all NVIC interrupts and the whole memory map via IDAU/SAU are secure.
- The U5 IDAU splits the addresses in half: the window
0x0C00_0000–0x0FFF_FFFFis a secure or NSC alias of the same Flash that is visible as non-secure from0x0800_0000. - Peripherals and RAM are divided by the GTZC block: the TZSC controller assigns security to peripherals, the MPCBB to RAM blocks, the TZIC catches violations. In the experiment the same role was played by the board's memory controllers (MPC).
- While
TZEN = 0, the secure Flash registers read as zeros, and writes to them are ignored: code written for TrustZone "does nothing" on a disabled chip without any errors.
The usual path is to start with TZEN = 0: the U5 then works as a very economical
Cortex-M33 without partitioning, and all examples build as a single project. Two
projects (secure and non-secure) are needed only when TrustZone
is actually enabled.
2б.8 When to take the U5 and when the F4
| Task | Choice |
|---|---|
| A battery-powered instrument that sleeps for years | U5: Stop 2 from 4.0 µA, Standby 210 nA |
| Key storage, secure update | U5 with TrustZone |
| Lots of RAM for buffers, graphics, a model | U5: 786 KB |
| Existing code and libraries for the F4, production is running | F405 |
| First steps with Cortex-M | F4 or F1: fewer entities |
HAL code carries over between series more easily than register code (section 5), but the clocking (section 6) and sleep modes (section 10) of the U5 are its own — they are reread from scratch.
2б.9 Pitfalls
The non-secure part fails on the very first call with INVEP (2б.6). The SAU marked NSC, but the attribute from the vendor remained secure; the stricter wins. On the U5 the gateways are placed in the NSC alias of the Flash, on the board the corresponding bit is set.
The linker threw away the gateway section (2б.5). If there is nothing yet to put
in .gnu.sgstubs at layout time, ld will consider it empty; a symbol inside
the section keeps it.
The secure function trusted a pointer (2б.5). Everything that came
from the non-secure world is checked with cmse_check_address_range, otherwise
the secure part will itself read the secret for the attacker. The checked
range must match the one read, and you work with a copy — otherwise
a string without a zero, or one substituted after the check, will be read on into secure memory.
TrustZone "does not work", but does not get in the way either (2б.7). With TZEN = 0
the secure Flash registers are zeros and writes go nowhere. The first thing to look at is the option
bit.
The cause in SFAR is zero (2б.6). The address is there only when SFARVALID is set;
the cause is read from the SFSR bits.
2б.10 Summary
- STM32U585 versus F405: Cortex-M33 instead of M4, 160 MHz against 168, 2 MB of Flash and 786 KB of RAM, sleep from 160 nA, and TrustZone.
- TrustZone divides addresses into secure, non-secure and NSC; the attribute is the strictest of the SAU and the IDAU.
- Entry into the secure world is only through
SGin the NSC window, exit throughBXNS. The compiler with-mcmsemakes the gateway and clears the registers; pointer checks are written by the programmer. - In the experiment: the signature
0x17d8449fwithout giving out the key, a rejection of a foreign pointer, SecureFault AUVIOL (0x00000008) on a direct read and INVEP (0x00000001) without NSC in the IDAU. - On the U5 TrustZone is disabled until the
TZENbit is set; peripherals and RAM are divided by the GTZC.
Full sources
s_main.c — the secure firmware of experiment 2б.4: SAU configuration, NSC gateways and the transition to the non-secure part.
/* Раздел 2б: TrustZone на Cortex-M33 — безопасная часть. QEMU mps2-an505 (ядро Cortex-M33 с TrustZone).
* Старт всегда в безопасном состоянии. Прошивка:
* 1) делит память атрибутами SAU: код и ОЗУ небезопасной части, окно шлюзов (NSC);
* 2) разрешает небезопасный доступ контроллерам памяти платы (MPC) к тем же областям;
* 3) ставит небезопасной части таблицу векторов и стек и переходит в неё командой BLXNS.
* Небезопасная часть видит только функции, помеченные cmse_nonsecure_entry, — через шлюз SG. */
#include <arm_cmse.h>
#include <stdint.h>
#define REG(a) (*(volatile uint32_t *)(a))
/* UART0 платы (CMSDK APB UART), безопасный адрес */
#define UART0 0x50200000u
#define UART_DATA REG(UART0 + 0x00)
#define UART_STATE REG(UART0 + 0x04)
#define UART_CTRL REG(UART0 + 0x08)
#define UART_BAUDDIV REG(UART0 + 0x10)
/* SAU — блок атрибутов безопасности ядра */
#define SAU_CTRL REG(0xE000EDD0)
#define SAU_RNR REG(0xE000EDD8)
#define SAU_RBAR REG(0xE000EDDC)
#define SAU_RLAR REG(0xE000EDE0)
#define SFSR REG(0xE000EDE4)
#define SFAR REG(0xE000EDE8)
#define SHCSR REG(0xE000ED24)
#define VTOR_NS REG(0xE002ED08) /* VTOR небезопасного состояния — через его псевдоним */
#define NS_CODE 0x00200000u /* небезопасный код: вторая половина ssram-0, небезопасный псевдоним */
#define NS_RAM 0x28100000u /* небезопасное ОЗУ: вторая половина ssram-1 */
static void putc_s(char c)
{
while (UART_STATE & 1u) {
}
UART_DATA = (uint32_t)c;
}
static void puts_s(const char *s)
{
while (*s)
putc_s(*s++);
}
static void puthex_s(uint32_t v)
{
static const char h[] = "0123456789abcdef";
puts_s("0x");
for (int i = 28; i >= 0; i -= 4)
putc_s(h[(v >> i) & 0xF]);
}
/* ── функции, которые небезопасная часть может вызвать ── */
static uint32_t secret_key = 0x5EC12E7u; /* живёт только в безопасной части, наружу не отдаётся */
volatile uint32_t s_calls;
uint32_t __attribute__((cmse_nonsecure_entry)) s_sign(uint32_t x)
{
s_calls++;
return x ^ secret_key; /* «подпись» — наружу уходит результат, не ключ */
}
void __attribute__((cmse_nonsecure_entry)) s_print(const char *msg)
{
/* указатель пришёл из небезопасного мира: читать только проверенное и только из своей копии */
char buf[33];
for (;;) {
if (!cmse_check_address_range((void *)msg, 32, CMSE_NONSECURE)) {
puts_s("s_print: отказ — адрес не небезопасный\n");
return;
}
int n = 0;
while (n < 32 && (buf[n] = msg[n]) != 0) /* копия: проверенное снаружи уже не подменить */
n++;
buf[n] = 0;
puts_s(buf);
if (n < 32)
return; /* строка кончилась внутри проверенных 32 байт */
msg += 32; /* следующие 32 байта — снова через проверку */
}
}
/* SecureFault: небезопасная часть полезла в безопасную память */
void SecureFault_Handler(void)
{
puts_s("SecureFault: SFSR=");
puthex_s(SFSR);
puts_s(" SFAR=");
puthex_s(SFAR);
puts_s("\ndone\n");
for (;;) {
}
}
static void mpc_all_ns(uint32_t mpc, uint32_t from_blk)
{
/* MPC платы: BLK_MAX — число слов таблицы, BLK_IDX — номер слова, BLK_LUT — по биту на блок (1 — небезопасный) */
uint32_t max = REG(mpc + 0x10);
for (uint32_t i = from_blk; i < max; i++) {
REG(mpc + 0x18) = i;
REG(mpc + 0x1C) = 0xFFFFFFFFu;
}
}
int main(void)
{
UART_BAUDDIV = 16;
UART_CTRL = 1; /* TX */
puts_s("S: старт в безопасном состоянии\n");
/* SAU: 0 — код небезопасной части, 1 — её ОЗУ, 2 — окно шлюзов (NSC) */
SAU_RNR = 0; SAU_RBAR = NS_CODE; SAU_RLAR = (NS_CODE + 0x1FFFE0u) | 1u;
SAU_RNR = 1; SAU_RBAR = NS_RAM; SAU_RLAR = (NS_RAM + 0xFFFE0u) | 1u;
SAU_RNR = 2; SAU_RBAR = 0x10100000u; SAU_RLAR = (0x10100000u + 0xFE0u) | (1u << 1) | 1u; /* NSC */
SAU_CTRL = 1u;
/* IDAU платы разрешает NSC в области кода 0x1xxxxxxx только битом CODENSC регистра NSCCFG;
* атрибут адреса — строжайший из двух: SAU говорит «NSC», IDAU — тоже должен */
#ifndef NO_CODENSC
REG(0x50080014u) |= 1u; /* NSCCFG.CODENSC */
#endif
/* MPC платы для ssram-0 и ssram-1: вторые половины — небезопасные */
uint32_t b0 = REG(0x58007000u + 0x10), b1 = REG(0x58008000u + 0x10);
mpc_all_ns(0x58007000u, b0 / 2);
mpc_all_ns(0x58008000u, b1 / 2);
puts_s("S: SAU и MPC настроены, BLK_MAX ssram-0 ");
puthex_s(b0);
puts_s("\n");
SHCSR |= 1u << 19; /* SECUREFAULTENA — отдельный обработчик, а не HardFault */
const uint32_t *ns_vec = (const uint32_t *)NS_CODE;
VTOR_NS = NS_CODE;
__asm volatile("msr msp_ns, %0" ::"r"(ns_vec[0]));
puts_s("S: переход в небезопасную часть\n");
typedef void __attribute__((cmse_nonsecure_call)) ns_fn(void);
ns_fn *ns_reset = (ns_fn *)cmse_nsfptr_create(ns_vec[1]); /* младший бит 0 — переход в небезопасное состояние */
ns_reset();
for (;;) {
}
}
/* ── старт ── */
extern uint32_t _estack;
void Reset_Handler(void)
{
main();
}
void Default_Handler(void)
{
puts_s("S: исключение без обработчика\n");
for (;;) {
}
}
__attribute__((section(".isr_vector"), used)) const void *const vectors[16] = {
&_estack, Reset_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler, SecureFault_Handler,
};
Next: section 2в "STM8: the eight-bit relative — what to take and what not" — how ST's eight-bit core resembles the STM32 and where it is still appropriate.