- /*!
- * [url=home.php?mod=space&uid=288409]@file[/url] main.c
- *
- * [url=home.php?mod=space&uid=247401]@brief[/url] Main program body
- *
- * [url=home.php?mod=space&uid=895143]@version[/url] V1.0.3
- *
- * [url=home.php?mod=space&uid=212281]@date[/url] 2023-07-31
- *
- * @attention
- *
- * Copyright (C) 2021-2023 Geehy Semiconductor
- *
- * You may not use this file except in compliance with the
- * GEEHY COPYRIGHT NOTICE (GEEHY SOFTWARE PACKAGE LICENSE).
- *
- * The program is only for reference, which is distributed in the hope
- * that it will be useful and instructional for customers to develop
- * their software. Unless required by applicable law or agreed to in
- * writing, the program is distributed on an "AS IS" BASIS, WITHOUT
- * ANY WARRANTY OR CONDITIONS OF ANY KIND, either express or implied.
- * See the GEEHY SOFTWARE PACKAGE LICENSE for the governing permissions
- * and limitations under the License.
- */
- /* Includes */
- #include "main.h"
- #include "Board.h"
- #include "apm32f4xx_gpio.h"
- #include "apm32f4xx_adc.h"
- #include "apm32f4xx_misc.h"
- #include "apm32f4xx_usart.h"
- #include "apm32f4xx_tmr.h"
- #include <stdio.h>
- #include <string.h>
- /** @addtogroup Examples
- @{
- */
- /** @addtogroup ADC_AnalogWindowWatchdog
- @{
- */
- /** @defgroup ADC_AnalogWindowWatchdog_Macros Macros
- @{
- */
- /* printf using USART1 */
- #define DEBUG_USART USART1
- #define APM_COMInit APM_TINY_COMInit
- /**@} end of group ADC_AnalogWindowWatchdog_Macros*/
- /** @defgroup ADC_AnalogWindowWatchdog_Functions Functions
- @{
- */
- // 用户模式堆栈配置
- #define PSP_STACK_SIZE 512
- static uint32_t psp_stack[PSP_STACK_SIZE/4] __attribute__((aligned(8)));
- void USARTInit(void);
- void generate_BusFault(void);
- void generate_UsageFault(void);
- void generate_HardFault(void);
- void print_fault_status(void);
- // MPU配置(触发保护错误)
- void MPU_Config(void)
- {
- // 关闭MPU
- ARM_MPU_Disable();
- /* 配置区域0:全地址空间禁止访问 */
- ARM_MPU_SetRegion(
- ARM_MPU_RBAR(0, 0x00000000), // 区域编号0,基地址0x00000000
- ARM_MPU_RASR(
- 1, // DisableExec:禁止指令获取
- ARM_MPU_AP_NONE, // 无访问权限
- 0, // TypeExtField:设备内存
- 0, // IsShareable:非共享
- 0, // IsCacheable:不可缓存
- 0, // IsBufferable:不可缓冲
- 0x00, // SubRegionDisable:不禁止任何子区域
- ARM_MPU_REGION_SIZE_4GB // 覆盖整个4GB地址空间
- )
- );
- // 启用MPU并允许特权模式默认内存映射
- ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
- // 确保配置生效
- __DSB();
- __ISB();
- }
- // 硬错误诊断处理
- __attribute__((naked)) void HardFault_Handler(void)
- {
- __asm volatile (
- "TST LR, #4\n" // 检查EXC_RETURN的位2
- "ITE EQ\n" // 根据结果选择堆栈指针
- "MRSEQ R0, MSP\n" // 使用MSP
- "MRSNE R0, PSP\n" // 使用PSP
- "B HardFault_Diagnose\n"
- );
- }
- void HardFault_Diagnose(uint32_t *stackFrame)
- {
- volatile uint32_t cfsr = SCB->CFSR;
- volatile uint32_t hfsr = SCB->HFSR;
- volatile uint32_t bfar = SCB->BFAR;
- printf("\n!!! HardFault Occurred !!!\n");
- printf("CFSR: 0x%08X\n", cfsr);
- printf("HFSR: 0x%08X\n", hfsr);
-
- if(cfsr & (1 << 16)) { // 检查BFARVALID标志
- printf("BFAR: 0x%08X\n", bfar);
- }
-
- printf("Stack Frame:\n");
- printf("R0 : 0x%08X\n", stackFrame[0]);
- printf("R1 : 0x%08X\n", stackFrame[1]);
- printf("R2 : 0x%08X\n", stackFrame[2]);
- printf("R3 : 0x%08X\n", stackFrame[3]);
- printf("R12: 0x%08X\n", stackFrame[4]);
- printf("LR : 0x%08X\n", stackFrame[5]);
- printf("PC : 0x%08X\n", stackFrame[6]);
- printf("xPSR:0x%08X\n", stackFrame[7]);
-
- while(1); // 进入死循环便于调试
- }
- // 用户模式任务(故意触发错误)
- __attribute__((noreturn)) void User_Task(void)
- {
- printf("\n[User Task] Entering User Mode\n");
- printf("Current CONTROL: 0x%08X\n", __get_CONTROL());
-
- // 尝试非法操作(以下三种错误任选其一)
- // 1. 访问特权寄存器
- SCB->SHCSR = 0; // 触发UsageFault
- // 2. 访问受保护内存
- // uint32_t *ptr = (uint32_t*)0x20000000; // 受MPU保护区域
- // *ptr = 0xDEADBEEF; // 触发MemManage
-
- // 3. 非法地址访问
- // uint32_t *bad_ptr = (uint32_t*)0xE0000000; // 保留地址
- // *bad_ptr = 0xCAFEBABE; // 触发BusFault
-
- while(1);
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Main program
- *
- * @param None
- *
- * @retval None
- */
- int main(void)
- {
- // 系统初始化
- USARTInit();
- printf("\nSystem Startup\n");
-
- // 配置MPU
- MPU_Config();
-
- // 初始化PSP堆栈
- __set_PSP((uint32_t)(psp_stack + PSP_STACK_SIZE/4 - 1));
- memset(psp_stack, 0xAA, PSP_STACK_SIZE); // 填充测试模式
-
- // 切换到用户模式
- printf("Switching to User Mode...\n");
- __set_CONTROL(0x01); // 启用PSP
- __ISB(); // 指令同步屏障
-
- // 跳转到用户任务
- User_Task();
-
- // 理论上不会执行到这里
- while(1);
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] USART Init
- *
- * @param None
- *
- * @retval None
- */
- void USARTInit(void)
- {
- /* USART Initialization */
- USART_Config_T usartConfigStruct;
- /* USART configuration */
- USART_ConfigStructInit(&usartConfigStruct);
- usartConfigStruct.baudRate = 115200;
- usartConfigStruct.mode = USART_MODE_TX_RX;
- usartConfigStruct.parity = USART_PARITY_NONE;
- usartConfigStruct.stopBits = USART_STOP_BIT_1;
- usartConfigStruct.wordLength = USART_WORD_LEN_8B;
- usartConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
- /* COM1 init*/
- APM_COMInit(COM1, &usartConfigStruct);
- }
- #if defined (__CC_ARM) || defined (__ICCARM__) || (defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050))
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param ch: The characters that need to be send.
- *
- * @param *f: pointer to a FILE that can recording all information
- * needed to control a stream
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int fputc(int ch, FILE* f)
- {
- /* send a byte of data to the serial port */
- USART_TxData(DEBUG_USART, (uint8_t)ch);
- /* wait for the data to be send */
- while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
- return (ch);
- }
- #elif defined (__GNUC__)
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param ch: The characters that need to be send.
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int __io_putchar(int ch)
- {
- /* send a byte of data to the serial port */
- USART_TxData(DEBUG_USART, ch);
- /* wait for the data to be send */
- while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
- return ch;
- }
- /*!
- * [url=home.php?mod=space&uid=247401]@brief[/url] Redirect C Library function printf to serial port.
- * After Redirection, you can use printf function.
- *
- * @param file: Meaningless in this function.
- *
- * @param *ptr: Buffer pointer for data to be sent.
- *
- * @param len: Length of data to be sent.
- *
- * @retval The characters that need to be send.
- *
- * @note
- */
- int _write(int file, char* ptr, int len)
- {
- int i;
- for (i = 0; i < len; i++)
- {
- __io_putchar(*ptr++);
- }
- return len;
- }
- #else
- #warning Not supported compiler type
- #endif
- /**@} end of group ADC_AnalogWindowWatchdog_Functions */
- /**@} end of group ADC_AnalogWindowWatchdog */
- /**@} end of group Examples */
示例代码中重要的逻辑节点可总结如下:
1. USARTInit( ):串口初始化,用于调试打印;
2. MPU_Config( ):配置MPU,使得默认整个地址空间不可访问,仅特权模式可正常运作;
3. 初始化PSP堆栈:分配一块512字节的数组 psp_stack,然后通过 __set_PSP() 设置PSP指针;
4. 切换到PSP并进入用户模式:__set_CONTROL(0x01) + __ISB();
5. 在User_Task()中添加故意触发错误的操作,如访问“受MPU保护的地址”0x20000000;
6. 当错误发生后,会进入HardFault_Handler或对应的Fault处理函数;
7. 在HardFault_Handler中,通过汇编判断是MSP还是PSP产生的异常,再调用HardFault_Diagnose()进行打印。
代码运行后,串口可根据我们选择情况,打印出相应的错误信息。
例程详细分析
上文介绍的示例代码中,在“User_Task()”里故意留了三种触发错误的写法,每种对应不同的故障类型。以下将分别说明在实际运行该代码时,各场景所呈现的现象。
1.使用“SCB->SHCSR = 0;”触发UsageFault
在User_Task()中,如果取消注释“SCB->SHCSR = 0;”这行代码,而注释掉其他两种非法访问,就会发生以下情况:
• 该行代码试图在非特权模式下写访问系统控制块(SCB)的寄存器,这是一个仅供特权模式访问的寄存器区域;
• 根据Cortex-M的权限模型,用户态访问特权寄存器必然触发UsageFault异常;
• 由于示例代码中的Fault处理统一最终会进入HardFault_Handler,再由我们在汇编里检测LR、选择当前栈指针,然后跳到HardFault_Diagnose()打印信息;实际观测到的CFSR寄存器中,UsageFault相关比特会被置位(如UNDEFINSTR或INVSTATE),从而指示这是一次UsageFault;
• 终端打印可能类似:“CFSR: 0x00000100”或“CFSR: 0x00000002”等,具体位需根据内核版本、访问地址来判定。当看到CFSR里UsageFault标志置1,就能知道是用户态违规访问了特权寄存器;
• 其后系统进入死循环,让我们能在调试器里查看寄存器、栈信息。
2.使用“(uint32_t)0x20000000 = 0xDEADBEEF;”触发MemManage Fault
在示例中,这行实际已被取消注释,是默认触发的场景:
• 代码把指针指向0x2000 0000,然后写入0xDEADBEEF。由于MPU_Config()中设置了整个4GB地址空间都不可访问(除了特权模式默认映射),所以在用户态下,这个地址被禁止写;
• 因此执行到“*ptr = 0xDEADBEEF;”时会触发MemManage Fault;
• 同样地,我们的Fault处理函数会跳到HardFault_Handler里的汇编逻辑,再传递栈信息给HardFault_Diagnose();
• 此时CFSR中MemManage Fault相关的位(如MMARVALID)会被置位,同时SCB->MMFAR(即SCB->BFAR在某些内核版本中共用或部分重叠)里可能记录了触发故障的地址“0x20000000”;
• 在输出信息中可以看到如“BFAR: 0x20000000”或“MemManage Fault”提示;具体名称可能因Cortex-M3与M4、M7的寄存器结构稍有差异,需要读者实测。
• 最终,该Demo在此故障出现后会停留在硬Fault死循环中,等待我们在调试器中观测或复位重启。
3.使用“(uint32_t)0xE0000000 = 0xCAFEBABE;”触发BusFault
若在User_Task()中取消注释“(uint32_t)0xE0000000 = 0xCAFEBABE;”一行,并注释掉前两者:
• 0xE0000000~0xE00FFFFF 通常是内核私有外设(如NVIC、SysTick、SCB等)的物理地址或保留地址空间。在部分MCU中,访问其中某些保留地址或未映射相应外设的区间,就会发生总线访问失败:BusFault;
• 执行这行语句后,处理器会发现总线地址无效或没有对应功能,随即抛出BusFault。若BusFault处理流程本身出问题,可能进一步升级为HardFault。
• 进入Fault后,CFSR中BusFault相关位会被置位,可能具体表现为“PRECISERR”或“IMPRECISERR”等;SCB->BFAR寄存器也会呈现最后一次触发故障的地址(0xE0000000)。
• 同样,HardFault_Diagnose()会打印“BFAR: 0xE0000000”或类似信息,提示BusFault来源于该地址。
• 对于绝大多数MCU,此时在终端或调试器也能觀察到相应的BusFault标志信息。再结合MPU配置,可以更精准地锁定错误发生时访问的是哪个地址区间。
整体现象比较
• UsageFault主要与非法指令或非法访问(如用户态干预特权寄存器)相关;
• MemManage Fault直接与MPU权限或栈保护等内存安全策略相关;
• BusFault多与访问无效外设、无效总线地址等问题相关;
在本Demo中,最终都统归到HardFault_Handler进行调试,但从CFSR、HFSR、BFAR/MMFAR寄存器的值,可以明确分辨是哪种具体Fault类型。
以上三种案例可帮助我们一次过演示“如何进入故障”“故障如何被捕获”“如何查看寄存器并判断故障类型”,为之后排除更复杂的问题打下基础。
代码执行顺序与收尾
由于在User_Task()中我们陷入了死循环或故障状态,理论上不会再回到main()的后续部分,因此本示例运行后要么保持在死循环,要么被调试器暂停。我们可以在这种状态下仔细查看栈内存(PSP区域是否真的写入了某些数据)、寄存器值和MPU配置寄存器等,“看见”硬件一级的响应机制,对学习与后续开发帮助甚大。
七、常见故障类型与Fault处理
在Cortex-M内核上,常见故障类型可以被分为:
1. MemManage Fault
当用户态访问了被MPU禁止的内存区域,或者是出于对栈的保护而故意设置的不可访问区域,即会抛出MemManage Fault。
2. BusFault
总线访问错误,例如访问空地址、访问不可用的外设或保留地址空间,也会触发BusFault。如果BusFault处理不当,也可升级成HardFault。
3. UsageFault
当执行了未定义指令、或在非特权模式下访问特权寄存器时,会抛出UsageFault。比如示例中故意修改SCB->SHCSR可以触发UsageFault。
4. HardFault
当上述Fault无法被单独捕获或出现更严重的状况,如处理Fault本身也出错时,Fault会升级成HardFault。此时,需要查看SCB->HFSR, SCB->CFSR等寄存器来做进一步分析。
5. Fault处理例程:汇编与C结合的诊断手段
在Demo中,我们用__attribute__((naked))来修饰HardFault_Handler,使编译器不生成函数栈帧的入口/出口代码;随后在汇编代码里区分当前堆栈指针(PSP或MSP),并将其地址传递给C函数 HardFault_Diagnose()。这种方式可以详细获取R0~R3、R12、LR、PC、xPSR等寄存器内容,对深入定位问题非常有用,我这里也是借鉴那些大佬的栈回溯定位,编写的一个简单的调试结果打印。
八、更深层的应用与调试技巧
1. 与RTOS的结合
在RTOS场景下,不同任务可使用不同PSP起始地址,从而将任务栈完全分离;中断处理会使用MSP,这样既避免了多个任务共享堆栈可能导致的干扰,也允许我们对任务栈进行更细粒度的保护。
2. 保证PSP和MSP独立性
通常我们会在链接脚本或启动文件中,单独划出一段内存作为进程堆栈(PSP),另一段作为主堆栈(MSP),并在MPU_Config()中明确指出哪一段内存对用户模式可访问、哪一段仅能特权访问。
3. 常见调试手段
• 观察CONTROL寄存器与PSP/MSP当前值;
• 检查MPU配置与内存对齐;
• 看SCB->CFSR、SCB->HFSR、SCB->MMFAR、SCB->BFAR等Fault相关寄存器;
• 使用裸机汇编或GDB在出现Fault时查看栈帧。
只有充分掌握这些调试技能,才能更好地处理各种突发或诡异的错误现象。
4. 切换堆栈后的函数调用与返回
在用户模式下调用的函数,若本身要访问特权寄存器,必须在进入函数前切换到特权模式,否则一旦操作就会抛出UsageFault。一个常见的做法是通过SVC指令(Supervisor Call)让特权层代理执行关键操作。
5. 注意编译器优化和堆栈保护
在高优化等级下,编译器可能对函数调用、变量存储做内联或优化处理;需要确认不会破坏对PSP的使用模式,可通过观察.map文件或使用断点逐步调试来核实真正的访存地址。
6. 生产环境中的安全考量
在对安全性要求较高的场景下,双堆栈结合MPU可以极大阻止“误操作”或“恶意破坏”对关键寄存器或数据区域的访问。但要真正形成安全闭环,还需要从启动代码、RTOS内核、外设驱动、Bootloader权限等多方面入手统筹设计。
九、总结
APM32双堆栈及MPU保护机制作为Cortex-M架构的精髓之一,既有硬件底层的技术魅力,也有与应用场景(RTOS、多任务、安全需求)完美契合的实用价值。尤其在嵌入式系统日益复杂、对安全与可靠性要求越来越高的当下,通过学习与掌握这些技术,可以让我们在驱动程序或应用开发中更游刃有余,更加放心地将用户任务与系统特权操作分隔开来,也能在故障排查时快速定位问题。
常见疑问
• 如果只是在单任务裸机环境中开发,是否必须启用PSP?
并非必须,但当项目规模较大、对安全要求较高时,启用PSP仍具价值;即便是裸机,也可以享受到更好排错与安全隔离的好处。
• MPU是否会影响性能?
一般地,MPU本身的开销非常小,主要是配置时需要谨慎;在运行时,若访问受限或越界,触发Fault那才会产生额外调试成本。所以合理配置比担心性能更关键。
• 如果在用户模式下,需要访问特权资源怎么办?
通常做法是使用SVC指令(软件中断)或回调到特权模式执行关键操作;也可以在启动时给予适当权限,但要确保安全风险可控。
附件
1.本文例程源码:
dualStackConfigSwitchTemplate.zip
(783.32 KB, 下载次数: 0)