这里以stm32u575开发板为例子,其他芯片可能存在细小区别,大部分都是相同的,理解了就好。
1. SAU 与 IDAU
SAU(Security Attribution Unit)
ARM 核内置的安全属性单元,由软件配置,定义哪些地址范围是 Secure / Non-Secure / NSC。
最多 8 个 region,每个 region 可独立配置
最小粒度 32 字节
IDAU(Implementation Defined Attribution Unit)
芯片厂商(ST)在 SoC 层面实现的安全属性单元,硬件固化,软件无法修改。
STM32U5 的 IDAU 按地址高位划分:
SAU 与 IDAU 的关系
最终安全属性取两者中更严格的那个:
IDAU 说是 S,SAU 说是 NS → 结果是 S(IDAU 优先)
IDAU 说是 NS,SAU 说是 S → 结果是 S(SAU 优先)
两者都说 NS → 结果是 NS
2. GTZC 三个子模块详解
GTZC这个需要重点掌握,GTZC分为三个子模块,分别是TZSC,TZIC,MPCBB.
TZSC(TrustZone Security Controller)
负责配置外设的安全属性,决定每个外设归属安全区还是非安全区。
每个外设可独立配置为 Secure 或 Non-Secure
同时支持 Privileged / Non-Privileged 权限控制
非安全区访问被标记为 Secure 的外设,会触发 TZIC 中断并被阻断
本工程配置示例:
将uart1配置为安全外设,非安全区无法直接访问uart1.
配置TIM6为NS分区使用
配置PC7/PB7/PG2为NS分区使用.
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_TIM6_CLK_ENABLE();
if (HAL_GTZC_TZSC_ConfigPeriphAttributes(GTZC_PERIPH_TIM6, GTZC_TZSC_PERIPH_NSEC|GTZC_TZSC_PERIPH_NPRIV) != HAL_OK)
{
Error_Handler();
}
if (HAL_GTZC_TZSC_ConfigPeriphAttributes(GTZC_PERIPH_USART1, GTZC_TZSC_PERIPH_SEC|GTZC_TZSC_PERIPH_NPRIV) != HAL_OK)
{
Error_Handler();
}
/* Configure PC7, PB7, PG2 as non-secure for NonSecure world usage */
HAL_GPIO_ConfigPinAttributes(GPIOC, GPIO_PIN_7, GPIO_PIN_NSEC);
HAL_GPIO_ConfigPinAttributes(GPIOB, GPIO_PIN_7, GPIO_PIN_NSEC);
HAL_GPIO_ConfigPinAttributes(GPIOG, GPIO_PIN_2, GPIO_PIN_NSEC);
TZIC(TrustZone Illegal Access Controller)
负责监控和上报安全违规事件,当非安全区非法访问安全资源时产生中断通知安全区。
监控来自 TZSC 和 MPCBB 的所有违规访问
每个受保护资源对应一个违规标志位,可独立使能中断
安全区在中断处理函数中读取违规来源,决定如何响应(复位、记录、忽略等)
违规来源分两类:
参考用例,配置uart1和sram2的TZIC中断,监控NS非法访问S的外设和RAM
/* Enable TZIC interrupts for USART1 and SRAM2 */
HAL_GTZC_TZIC_EnableIT(GTZC_PERIPH_USART1);
HAL_GTZC_TZIC_EnableIT(GTZC_PERIPH_SRAM2);
HAL_NVIC_SetPriority(GTZC_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(GTZC_IRQn);
void GTZC_IRQHandler(void)
{
HAL_GTZC_IRQHandler();
}
void HAL_GTZC_TZIC_Callback(uint32_t PeriphId)
{
if (PeriphId == GTZC_PERIPH_USART1)
printf("[S] TZIC: illegal access to USART1\r\n");
else if (PeriphId == GTZC_PERIPH_SRAM2)
printf("[S] TZIC: illegal access to SRAM2\r\n");
}
MPCBB(Memory Protection Controller Block-Based)
负责对 SRAM 进行 block 级别的安全属性配置,是比 SAU 更细粒度的 SRAM 保护机制。
SAU 以地址范围划分安全区,最小粒度 32 字节,通常按 KB 划分
MPCBB 以 256 字节为一个 block,可对每个 block 单独设置 Secure/Non-Secure
支持 SRAM1、SRAM2、SRAM3、SRAM4 独立配置
还支持 PrivConfig_array 对每个 block 配置 Privileged/Non-Privileged 访问权限
SRAM3 容量 512KB,共 2048 个 block,对应 SecConfig_array[0..31](32 个元素 × 32 bit):
SecConfig_array[0] bit0 → block0 (0x20040000 ~ 0x200400FF)
SecConfig_array[0] bit1 → block1 (0x20040100 ~ 0x200401FF)
...
SecConfig_array[31] bit31 → block2047
比如我这里配置SRAM1为NS使用,SRAM2为S分区使用,这样NS就无法访问SRAM2了。
/* --- SRAM1: 192KB, 24 elements, all Non-Secure (NS) --- */
MPCBB_Area_Desc.SecureRWIllegalMode = GTZC_MPCBB_SRWILADIS_ENABLE;
MPCBB_Area_Desc.InvertSecureState = GTZC_MPCBB_INVSECSTATE_NOT_INVERTED;
for (int i = 0; i < 24; i++) {
MPCBB_Area_Desc.AttributeConfig.MPCBB_SecConfig_array = 0x00000000;
MPCBB_Area_Desc.AttributeConfig.MPCBB_PrivConfig_array = 0xFFFFFFFF;
}
MPCBB_Area_Desc.AttributeConfig.MPCBB_LockConfig_array[0] = 0x00000000;
if (HAL_GTZC_MPCBB_ConfigMem(SRAM1_BASE, &MPCBB_Area_Desc) != HAL_OK)
{
Error_Handler();
}
/* --- SRAM2: 64KB, 8 elements, all Secure (S) --- */
for (int i = 0; i < 8; i++) {
MPCBB_Area_Desc.AttributeConfig.MPCBB_SecConfig_array = 0xFFFFFFFF;
MPCBB_Area_Desc.AttributeConfig.MPCBB_PrivConfig_array = 0xFFFFFFFF;
}
MPCBB_Area_Desc.AttributeConfig.MPCBB_LockConfig_array[0] = 0x00000000;
if (HAL_GTZC_MPCBB_ConfigMem(SRAM2_BASE, &MPCBB_Area_Desc) != HAL_OK)
{
Error_Handler();
}
本工程使用U575时 SAU 配置
SAU配置在partition_stm32u575xx.h文件中,这里的配置为 SAU 禁用 + ALLNS=1 模式,所有地址默认对 NS 可见,安全边界完全由 MPCBB 和 TZSC 控制。SAU 粒度粗(通常按 KB),无法对 SRAM 做 block 级别(256 字节)的细粒度划分,因此用 MPCBB 作为唯一的 SRAM 安全边界。
这里仅启用的 SAU Region:
配置了SAU,外设,GTZC属性,但是想要在NS中使用中断,还有一个细节需要注意
NVIC 中断路由(Interrupt Target Non-Secure)
TrustZone 架构下,所有中断默认归 Secure 处理。若外设已通过 TZSC 配置为 Non-Secure,还必须在 partition_stm32u575xx.h 中将对应中断路由到 NS,否则 NS 区的中断处理函数永远不会被调用。
中断路由通过 NVIC->ITNS[n] 寄存器控制,每个 bit 对应一个 IRQn:
0 = Secure(默认)
1 = Non-Secure
计算方法
ITNS 寄存器索引 = IRQn / 32
bit 位置 = IRQn % 32
本工程配置
举个例子:
/*
// Interrupts 0..31
// <o.0> WWDG_IRQn <0=> Secure state
// <o.1> PVD_AVD_IRQn <0=> Secure state
// <o.2> RTC_IRQn <0=> Secure state
// <o.3> RTC_S_IRQn <0=> Secure state
// <o.4> TAMP_IRQn <0=> Secure state
// <o.5> RAMCFG_IRQn <0=> Secure state
// <o.6> FLASH_IRQn <0=> Secure state
// <o.7> FLASH_S_IRQn <0=> Secure state
// <o.8> GTZC_IRQn <0=> Secure state
// <o.9> RCC_IRQn <0=> Secure state
// <o.10> RCC_S_IRQn <0=> Secure state
// <o.11> EXTI0_IRQn <0=> Secure state
// <o.12> EXTI1_IRQn <0=> Secure state
// <o.13> EXTI2_IRQn <0=> Secure state
// <o.14> EXTI3_IRQn <0=> Secure state
// <o.15> EXTI4_IRQn <0=> Secure state
// <o.16> EXTI5_IRQn <0=> Secure state
// <o.17> EXTI6_IRQn <0=> Secure state
// <o.18> EXTI7_IRQn <0=> Secure state
// <o.19> EXTI8_IRQn <0=> Secure state
// <o.20> EXTI9_IRQn <0=> Secure state
// <o.21> EXTI10_IRQn <0=> Secure state
// <o.22> EXTI11_IRQn <0=> Secure state
// <o.23> EXTI12_IRQn <0=> Secure state
// <o.24> EXTI13_IRQn <0=> Secure state
// <o.25> EXTI14_IRQn <0=> Secure state
// <o.26> EXTI15_IRQn <0=> Secure state
// <o.27> IWDG_IRQn <0=> Secure state
// <o.29> GPDMA1_Channel0_IRQn <0=> Secure state
// <o.30> GPDMA1_Channel1_IRQn <0=> Secure state
// <o.31> GPDMA1_Channel2_IRQn <0=> Secure state
*/
#define NVIC_INIT_ITNS0_VAL 0x60000000
3. S与NS分区通信方法
NS发消息给S
可以在secure_nsc.c中添加API
举个例子,比如uart1在S分区使用,但是NS想要通过uart1打印日志怎么办?
只需要添加一个NS调用API即可
在secure_nsc.c中添加SECURE_UART_Send函数
在secure_nsc.h中添加声明void SECURE_UART_Send(const char *msg);
CMSE_NS_ENTRY void SECURE_UART_Send(const char *msg)
{
if (msg != NULL)
{
printf(msg);
}
}
NS中添加printf用的__io_putchar,然后NS直接使用printf即可打印日志了
int __io_putchar(int ch)
{
SECURE_UART_Send((char *)&ch);
return ch;
}
S发消息给NS
共享内存方式
可以在secure_nsc.c中添加注册回调
同样举个例子,还是uart1,我这里想把uart1接收到的数据发给NS,因为串口接收到的数据有的多有的少,不确定,而之前已经将RAM划分好了S和NS,S直接传大数据 给NS是没用的,因为NS无法访问S的RAM,这里提出一种共享内存方式。
定义一个共享内存SRAM4:
/* --- SRAM4: 16KB, 2 elements, shared (NS) --- */
MPCBB_Area_Desc.SecureRWIllegalMode = GTZC_MPCBB_SRWILADIS_DISABLE;
for (int i = 0; i < 2; i++) {
MPCBB_Area_Desc.AttributeConfig.MPCBB_SecConfig_array = 0x00000000;
MPCBB_Area_Desc.AttributeConfig.MPCBB_PrivConfig_array = 0xFFFFFFFF;
}
MPCBB_Area_Desc.AttributeConfig.MPCBB_LockConfig_array[0] = 0x00000000;
if (HAL_GTZC_MPCBB_ConfigMem(SRAM4_BASE, &MPCBB_Area_Desc) != HAL_OK)
{
Error_Handler();
}
定一个共享内存结构体
#ifndef SHARED_MEM_H
#define SHARED_MEM_H
#include <stdint.h>
#define UART_RX_BUF_SIZE 64
#define SHARED_MEM_ADDR 0x28000000UL /* SRAM4 physical address, S/NS accessible */
typedef struct {
uint16_t len;
uint8_t data[UART_RX_BUF_SIZE];
} __attribute__((packed)) SharedUartRx_t;
#define SHARED_UART_RX ((volatile SharedUartRx_t *)SHARED_MEM_ADDR)
#endif /* SHARED_MEM_H */
串口接收到数据之后,通过注册回调通知NS,然后NS去读取共享内存数据处理,在secure_nsc.h中添加事件ID
typedef enum
{
SECURE_FAULT_CB_ID = 0x00U,
GTZC_ERROR_CB_ID = 0x01U,
UART_RX_NOTIFY_CB_ID = 0x02U /*!< Secure UART RX notify (no args) callback ID */
} SECURE_CallbackIDTypeDef;
在secure_nsc.c中添加注册回调事件入口
void *pUartRxNotifyCallback = NULL;
CMSE_NS_ENTRY void SECURE_RegisterCallback(SECURE_CallbackIDTypeDef CallbackId, void *func)
{
if(func != NULL)
{
switch(CallbackId)
{
case SECURE_FAULT_CB_ID: /* SecureFault Interrupt occurred */
pSecureFaultCallback = func;
break;
case GTZC_ERROR_CB_ID: /* GTZC Interrupt occurred */
pSecureErrorCallback = func;
break;
case UART_RX_NOTIFY_CB_ID:
pUartRxNotifyCallback = func;
break;
default:
/* unknown */
break;
}
}
}
S串口收到数据,调用NS回调通知NS,这里简化列出相关代码,调用pUartRxNotifyCallback完成通知动作
typedef void CMSE_NS_CALL (*funcptr)(void);
typedef funcptr funcptr_NS;
extern funcptr_NS pUartRxNotifyCallback;
if (pUartRxNotifyCallback != NULL)
pUartRxNotifyCallback();
在NS中添加注册和处理,这里举例子就是将接收到的数据直接打印出来了。
SECURE_RegisterCallback(UART_RX_NOTIFY_CB_ID, (void *)NS_UartRxNotify);
static void NS_UartRxNotify(void)
{
printf("[NS] RX(%u): %.*s\r\n",
SHARED_UART_RX->len,
SHARED_UART_RX->len,
(const char *)SHARED_UART_RX->data);
}
直接回调方式
S可以直接传输4个参数给NS,超过 4 个参数后,会走栈(Stack)。走栈就会涉及到地址合法性了,第五个及以后参数所在的栈,必须是调用方(NS)的栈,并且 Secure 侧访问时会涉及地址权限检查。
这里我只采用前4个参数,这四个参数走的是寄存器方式,不会涉及到RAM地址是否合法问题,每个寄存器32bit,一共16个字节,其实也可以传递很多信息了。
在secure_nsc.h添加事件ID FOUR_DATA_CB_ID
typedef enum
{
SECURE_FAULT_CB_ID = 0x00U,
GTZC_ERROR_CB_ID = 0x01U,
UART_RX_NOTIFY_CB_ID = 0x02U, /*!< Secure UART RX notify (no args) callback ID */
FOUR_DATA_CB_ID = 0x03U
} SECURE_CallbackIDTypeDef;
在secure_nsc.c添加注册回调
void *pFourDataCallback = NULL;
CMSE_NS_ENTRY void SECURE_RegisterCallback(SECURE_CallbackIDTypeDef CallbackId, void *func)
{
if(func != NULL)
{
switch(CallbackId)
{
case SECURE_FAULT_CB_ID: /* SecureFault Interrupt occurred */
pSecureFaultCallback = func;
break;
case GTZC_ERROR_CB_ID: /* GTZC Interrupt occurred */
pSecureErrorCallback = func;
break;
case UART_RX_NOTIFY_CB_ID:
pUartRxNotifyCallback = func;
break;
case FOUR_DATA_CB_ID:
pFourDataCallback = func;
break;
default:
/* unknown */
break;
}
}
}
S调用通知传递4个参数,这里同样简化代码,需要传递的时候,调用pFourDataCallback接口
typedef void CMSE_NS_CALL (*funcptr_data)(uint32_t r0, uint32_t r1, uint32_t r2, uint32_t r3);
typedef funcptr_data funcptr_data_NS;
extern funcptr_data_NS pFourDataCallback;
if (pFourDataCallback != NULL)
pFourDataCallback(0x01, 0x02, 0x03, 0x04);
NS注册回调,处理数据
SECURE_RegisterCallback(FOUR_DATA_CB_ID, (void *)NS_FourDataRx);
static void NS_FourDataRx(uint32_t r0, uint32_t r1, uint32_t r2, uint32_t r3)
{
printf("[NS] r0:0x%08lx r1:0x%08lx r2:0x%08lx r3:0x%08lx \r\n",
r0, r1, r2, r3);
}
NS直接读取S数据
NS还可以通过API直接读取S数据,读取一个uint32数据,也是走寄存器方式,不会走栈,没有地址合法性问题
在secure_nsc.c定义一个API,直接返回一个uint32_t 类型数据
CMSE_NS_ENTRY uint32_t SECURE_GetEvent(void)
{
return 0xFF55AAEE;
}
在secure_nsc.h中添加声明
uint32_t SECURE_GetEvent(void);
在NS中直接调用
printf("SECURE_GetEvent: 0x%08lx\r\n", SECURE_GetEvent());
所以S和NS分区通信方式有多种,可以根据实际需要去选择合适的方式。
常见问题
若 NS 区中断不响应,检查以下三点:
TZSC 外设属性 — 需配置为 GTZC_TZSC_PERIPH_NSEC
NVIC ITNS 路由 — NVIC_INIT_ITNS1_VAL 对应的 bit 需置 1
GPIO 安全属性 — 相关引脚需通过 HAL_GPIO_ConfigPinAttributes() 设为 GPIO_PIN_NSEC
三者缺一不可,否则 NS 区无法正常收发串口中断。
————————————————
版权声明:本文为CSDN博主「IIIIllllIII」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012988644/article/details/162826504
|