Embedded HandbookApps
All sections › I. Basics
STM32U585, STM32F405 · level intermediate · 15 min read
Open in the interactive handbook

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.

The STM32U585CIU6 part number letter by letter, and U585 versus F405: almost the same frequency, twice the Flash, four times the RAM, sleep from 4.0 µA down to 160 nA


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:

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 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.

TrustZone in the experiment: the secure world, the non-secure one and the narrow door between them

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.

Security attribute map: in the AN505 experiment and on the STM32U5 the same memory is visible at a non-secure and at a secure address; in the experiment the NSC window 0x10100000 with the gateways s_sign 0x10100000 and s_print 0x10100008, rejections at 0x38000000 and 0x38001000; on the U5 Flash at 0x08000000 and 0x0C000000, RAM at 0x20000000 and 0x30000000, peripherals at 0x40000000 and 0x50000000

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

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.

Open in the interactive handbook