ARM Cortex-M4开发实战:MPU内存保护与SysTick高精度定时配置指南
1. 项目概述为什么需要深入理解MPU与SysTick在嵌入式开发尤其是基于ARM Cortex-M4这类高性能微控制器的项目中内存保护和精确的时间管理是两个看似基础、实则决定系统稳定性和可靠性的核心支柱。很多开发者特别是从51或早期ARM9平台转过来的朋友初期可能会觉得直接操作寄存器、让程序“跑起来”就万事大吉。但当你开始处理更复杂的任务调度、涉及第三方库、或者需要满足功能安全标准时两个“拦路虎”就会频繁出现一是程序莫名其妙地跑飞最终触发HardFault二是定时不准延时飘忽多任务调度时序混乱。前者往往是因为内存访问越界、堆栈溢出或者非法地址访问而内存保护单元MPU就是Cortex-M系列为我们提供的硬件“保镖”。后者则是系统心跳不齐的问题SysTick定时器作为ARM内核标准的系统定时器它的配置直接关系到操作系统的任务调度、软件延时的精度以及整个系统的时间基准。我见过不少项目因为SysTick配置不当导致使用HAL_Delay()函数时实际延时差了好几倍或者移植了RTOS后发现任务调度频率完全不对。因此今天我们就来彻底拆解这两个核心部件。这不是一份简单的寄存器配置清单而是结合我多年在STM32、NXP Kinetis等Cortex-M4平台上的实战经验从“为什么要这么做”到“具体怎么做”再到“踩过哪些坑”为你呈现一份可以直接用于产品级开发的配置指南。无论你是正在学习RTOS还是希望提升裸机程序的健壮性这篇文章都能提供实实在在的帮助。2. MPU内存保护单元配置全解析2.1 MPU的工作原理与核心价值MPU不是一个阻止所有非法访问的“万能盾牌”而是一个可编程的、基于区域的内存访问控制器。你可以把它想象成一座大楼的保安系统。CPU访客想要访问某个内存地址进入某个房间MPU保安会查对访客的权限卡当前运行的程序权限和房间的准入规则内存区域的属性配置判断是否放行。Cortex-M4的MPU通常支持8个或16个独立的可编程区域Region。每个区域你可以定义其起始地址、大小、访问权限如只读、只写、不可访问和内存属性如是否可缓存、是否可缓冲、是否可共享。此外MPU还支持一个背景区域Background Region当MPU启用但某个地址不属于任何已定义区域时将使用背景区域的属性。在特权模式下背景区域通常允许完全访问在用户模式下则可能被禁止。它的核心价值在于隔离与保护将代码区Flash、数据区SRAM、外设区、堆栈区等隔离开。例如防止用户任务的代码意外修改内核数据或关键外设寄存器。提升可靠性在堆栈溢出时如果配置了MPU保护区域之外的RAM为不可访问溢出瞬间就会触发MemManage Fault而不是破坏其他数据后导致不可预知的崩溃极大方便了调试。支持多任务/RTOS这是RTOS如FreeRTOS、ThreadX实现任务内存隔离的基础。每个任务可以拥有自己独立的数据段和堆栈段任务A无法篡改任务B的数据。2.2 MPU区域规划实战策略直接配置寄存器比较繁琐我们通常借助CubeMX或直接编写清晰的结构化代码。规划区域是第一步也是最关键的一步。以下是一个针对典型STM32F4系列具有1MB Flash192KB SRAM的裸机或简单RTOS应用的区域规划示例区域编号用途起始地址大小访问权限 (AP)内存属性 (TEX, C, B, S)说明Region 0Flash (代码/只读数据)0x0800 00001MB特权/用户只读 (APRO/RO)TEX0, C1, B1, S0 (Normal, WBWA)保护代码不被意外写操作破坏。Region 1SRAM (数据)0x2000 0000128KB特权/用户全访问 (APRW/RW)TEX0, C1, B1, S0 (Normal, WBWA)主数据区可读写。Region 2外设 (Peripheral)0x4000 00001GB特权只读/用户无访问 (APRO/NO)TEX0, C0, B0, S1 (Device, nGnRE)防止用户代码随意修改外设需特权操作。Region 3堆栈保护0x2002 0000 (末尾)1KB特权/用户不可访问 (APNO/NO)TEX0, C0, B0, S0 (Strongly-ordered)放在SRAM末尾用于检测栈溢出。Region 4RTOS任务堆栈A0x2001 F0004KB特权/用户全访问 (APRW/RW)TEX0, C1, B1, S0分配给任务A的独立堆栈。Region 5RTOS任务堆栈B0x2001 E0004KB特权/用户全访问 (APRW/RW)TEX0, C1, B1, S0分配给任务B的独立堆栈。规划要点解析起始地址与大小起始地址必须对齐到其自身的大小。例如一个128KB的区域其起始地址必须是128KB0x20000的整数倍。大小必须是2的幂且最小为32字节某些实现为256字节。MPU-RBAR区域基址寄存器和MPU-RASR区域属性和大小寄存器的配置需要严格遵守此规则。访问权限 (AP)这是安全的关键。AP[2:0]字段控制特权模式和用户模式的读/写/无访问权限。对于外设我强烈建议设置为特权只读/用户无访问。这意味着在用户模式如RTOS的用户任务下尝试写外设寄存器会触发故障而特权模式如RTOS内核、中断服务程序可以读写。这能有效防止任务“越权”操作硬件。内存属性 (TEX, C, B, S)这决定了CPU核心与内存之间的访问行为对性能和一致性至关重要。Device(TEX0, C0, B0)用于外设。访问是严格的non-Gathering, non-Reordering, Early Write Acknowledgement即指令顺序执行且不缓冲确保对寄存器的读写立即生效。Normal(TEX0, C1, B1)用于Flash和SRAM。允许缓存Cacheable和缓冲BufferableCPU可以为了性能进行预取和写缓冲。Write-Back, Write-Allocate (WBWA)是一种常见的优化策略。S(Shareable)对于多核或带有DMA的系统如果该内存区域可能被多个主设备如CPU和DMA访问则应设置为共享(S1)以确保缓存一致性。单核系统中访问外设时通常也需要设置S1。2.3 基于HAL库与寄存器的配置代码实现这里以STM32的HAL库为例展示如何配置上述Region 2外设保护区域。理解寄存器操作有助于你脱离特定库进行移植。// 方法一使用STM32 HAL库推荐可读性好 void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct {0}; // 1. 禁用MPU HAL_MPU_Disable(); // 2. 配置外设区域 (Region 2) MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.Number MPU_REGION_NUMBER2; // 使用区域2 MPU_InitStruct.BaseAddress 0x40000000; // 外设基址 MPU_InitStruct.Size MPU_REGION_SIZE_1GB; // 大小1GB覆盖整个AHB/APB外设空间 MPU_InitStruct.AccessPermission MPU_REGION_PRIV_RO_URO; // 特权只读用户只读可根据需要改为用户无访问 MPU_InitStruct.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_ACCESS_SHAREABLE; // 外设通常共享 MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable 0x00; // 不禁止任何子区域 MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_ENABLE; // 允许指令获取对于外设此设置通常无效但需配置 HAL_MPU_ConfigRegion(MPU_InitStruct); // 3. 可以继续配置其他区域... // MPU_InitStruct.Number MPU_REGION_NUMBER0; // MPU_InitStruct.BaseAddress 0x08000000; // ... 配置Flash区域 // 4. 启用MPU并启用默认内存映射背景区域 // MPU_CTRL_PRIVDEFENA_Msk 使能后在特权模式下未覆盖的区域使用默认属性全访问。 // MPU_CTRL_HFNMIENA_Msk 决定在NMI和HardFault中是否启用MPU通常禁用以保证故障处理能访问所有内存。 HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } // 方法二直接操作寄存器更底层适用于无HAL库或需要极致控制 void MPU_Config_Reg(void) { // 禁用MPU MPU-CTRL 0; // 配置区域2外设保护 // RBAR: 基地址 | VALID | REGION number MPU-RBAR (0x40000000 MPU_RBAR_ADDR_Msk) | MPU_RBAR_VALID_Msk | (2 MPU_RBAR_REGION_Pos); // RASR: ENABLE | SIZE (1GB0x1F) | AP (PRIV RO / USER NO) | TEX/C/B/S | SRD | XN uint32_t rasr (0 MPU_RASR_ENABLE_Pos) | // 先构建最后再置位ENABLE (0x1F MPU_RASR_SIZE_Pos) | // SIZE 31, 表示 2^(311) 4GB? 注意公式为 2^(SIZE1) // 正确计算对于1GBSize log2(1*1024*1024*1024) - 1 30 -1 29? 这里容易出错。 // 更安全的方法是使用预定义的宏或查表。HAL库的MPU_REGION_SIZE_1GB就是0x1F。 (MPU_RASR_AP_PRIV_RO__U_NO MPU_RASR_AP_Pos) | (0x0 MPU_RASR_TEX_Pos) | // TEX0 (0 MPU_RASR_C_Pos) | // C0 (0 MPU_RASR_B_Pos) | // B0 (1 MPU_RASR_S_Pos) | // S1 (0x00 MPU_RASR_SRD_Pos) | // 不禁止子区域 (1 MPU_RASR_XN_Pos); // 禁止执行 (XN1) rasr | (1 MPU_RASR_ENABLE_Pos); // 置位ENABLE位 MPU-RASR rasr; // 启用MPU和特权默认访问 MPU-CTRL MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; // 确保配置生效 __DSB(); __ISB(); }注意直接计算SIZE字段极易出错。ARM的公式是Region Size in bytes 2^(SIZE1)。所以对于1GB2^30字节SIZE应设置为29。MPU_REGION_SIZE_1GB这个宏的值就是29。上面的寄存器示例中0x1F31是错误的会导致配置的区域大小远超预期。务必使用芯片厂商提供的宏或仔细计算。2.4 MPU配置的常见陷阱与调试技巧区域重叠与优先级MPU区域有编号默认情况下编号越小优先级越高。当两个区域重叠时高优先级区域的属性生效。规划时要避免非预期的重叠或者利用重叠实现更复杂的保护策略如用一个小区域覆盖大区域的一部分禁止其访问。启用时机一定要在系统初始化早期、主要外设和堆栈初始化之后但在任务调度开始之前启用MPU。如果在全局变量或堆栈还未初始化时就启用MPU并设置了严格保护初始化代码本身可能就会触发故障。故障处理启用MPU后一旦有违规访问会触发MemManage Fault。你需要在启动文件中启用MemManage_Handler。在故障处理函数中读取SCB-CFSR配置故障状态寄存器的MMFSR字段SCB-MMFARMemManage Fault地址寄存器会保存引发故障的地址。结合这些信息能快速定位是哪个任务或函数在访问非法地址。void MemManage_Handler(void) { uint32_t cfsr SCB-CFSR; uint32_t mmfar SCB-MMFAR; printf(“MemManage Fault! CFSR: 0x%08lX, MMFAR: 0x%08lX\n”, cfsr, mmfar); // 进一步解析cfsr的位域判断是访问违例、执行违例还是背景区域违例 while(1); // 死循环或进行系统复位 }与Cache的协同如果你的芯片有Cache如STM32F7/H7MPU区域的内存属性Cacheable, Bufferable必须与实际物理内存的配置一致否则会导致数据一致性问题。通常通过SCB_EnableICache和SCB_EnableDCache使能Cache前必须先配置好MPU。3. SysTick系统定时器深度配置与应用3.1 SysTick的本质不仅仅是“延时函数”SysTick是一个24位的递减计数器集成在Cortex-M内核中所有基于该内核的芯片都有。它的主要设计目的是为操作系统提供一个周期性的“心跳”中断。但它的用途远不止于此RTOS的心跳几乎所有RTOSFreeRTOS, uC/OS, ThreadX都使用SysTick作为其时间片轮转或定时调度的基准。精准延时在裸机程序中可以基于SysTick实现微秒(us)或毫秒(ms)级别的阻塞或非阻塞延时比简单的for循环更精确、更省电。时间戳通过读取SysTick的当前值VAL和重装载值LOAD可以计算出自某个事件以来经过的精确时间用于性能分析或超时判断。它的时钟源可以是处理器时钟HCLK或其分频通常为HCLK/8。选择哪个时钟源直接决定了定时精度和功耗。3.2 时钟源选择与重装载值计算这是配置SysTick最核心的一步直接关系到定时的准确性。时钟源选择HCLK高精度但功耗稍高。如果你的系统主频稳定如使用外部晶振且需要高精度延时或RTOS调度这是首选。在STM32CubeMX中对应SysTick clock source选择Core Clock。HCLK/8精度较低但功耗更低。适用于对功耗敏感、定时精度要求不高的场景。在CubeMX中对应External Clock。重装载值LOAD计算SysTick从LOAD值开始递减减到0时产生中断并重新加载。因此中断周期T_int (LOAD 1) / F_clk。目标实现1ms中断常用于RTOS心跳假设F_clk 168 MHz(HCLK)LOAD F_clk * T_int - 1 168,000,000 * 0.001 - 1 168,000 - 1 167,999检查是否超过24位最大值16,777,215。167,999远小于此值可行。目标实现1us延时用于高精度裸机延时此时通常不开启中断而是用查询方式。但计算原理相同。LOAD_us F_clk * 0.000001 - 1 168 - 1 167这意味着将SysTick配置为每1us溢出一次。但注意24位计数器最大只能计数约16.7ms在168MHz下。对于更长的us级延时需要软件做循环计数。实操心得在SystemCoreClock变量尚未被正确初始化比如在SystemInit()函数执行之前的启动代码阶段不要尝试配置SysTick。早期的错误配置会导致后续基于SysTick的延时全部失效。通常在main()函数中HAL_Init()会调用HAL_InitTick()来根据HAL_RCC_GetHCLKFreq()的返回值配置SysTick。3.3 实现高精度微秒(us)延时函数很多HAL库只提供了毫秒延时HAL_Delay()但实际开发中经常需要us级延时例如驱动WS2812B灯珠、读取DHT11温湿度传感器等。下面是一个不依赖中断的、基于SysTick的us延时实现它比空循环更精确且不受编译器优化影响。// 首先定义一个全局变量存储每微秒对应的SysTick计数周期数 static uint32_t usTicks 0; // 初始化函数在系统时钟配置完成后调用 void SysTick_US_Init(void) { // 假设SysTick时钟源已设置为HCLK核心时钟 // SystemCoreClock 变量已在SystemInit()中更新 usTicks SystemCoreClock / 1000000; // 计算1us需要多少个时钟周期 // 注意这里计算的是周期数不是LOAD值。LOAD 周期数 - 1 } // 阻塞式微秒延时函数 void delay_us(uint32_t us) { uint32_t startTick, targetTick, currentTick; uint32_t loadVal us * usTicks - 1; // 计算需要计数的总周期数并减1得到LOAD值 // 如果要求的延时时间太长超过了24位计数器单次的最大计数范围 // 单次最大计数周期数 0xFFFFFF 1 16,777,216 if (loadVal 0xFFFFFF) { // 对于超长延时可以分段处理或者直接调用毫秒延时函数。 // 这里简单处理提示错误或使用循环。更稳健的做法是分段。 // 我们先实现一个支持长延时的版本 uint32_t ms_part us / 1000; uint32_t us_part us % 1000; if (ms_part 0) { HAL_Delay(ms_part); // 借用HAL的毫秒延时 } us us_part; // 剩余部分用下面的精确延时 loadVal us * usTicks - 1; if (us 0) return; } // 获取当前SysTick计数器的值VAL是递减的读出来是当前值 startTick SysTick-VAL; targetTick (startTick - loadVal) 0xFFFFFF; // 计算目标值注意24位掩码和递减方向 // 如果 startTick 已经小于 loadVal减法会借位通过 0xFFFFFF 处理回绕 // 但更清晰的逻辑是我们等待VAL从startTick递减经过(loadVal1)个周期后会达到或越过0并重载 // 实际上更通用的方法是使用“经过的周期数”来判断。 // 推荐以下更清晰的实现 uint32_t ticksNeeded us * usTicks; uint32_t start SysTick-VAL; uint32_t elapsed; do { current SysTick-VAL; // 处理计数器重载如果current start说明发生了重载VAL从0重载到了LOAD if (current start) { elapsed (start 1) (SysTick-LOAD - current); } else { elapsed start - current; } } while (elapsed ticksNeeded); } // 更简洁但稍欠精确的版本适用于短延时且假设LOAD值很大短时间内不会重载 void delay_us_simple(uint32_t us) { uint32_t start SysTick-VAL; uint32_t ticks us * (SystemCoreClock / 1000000); // 等待经过的ticks数 while ((start - SysTick-VAL) 0xFFFFFF ticks) { // 注意如果发生重载(start - VAL)会变成一个很大的数所以用 0xFFFFFF 确保结果正确 // 但这种方法在延时接近重载周期时可能不准。 } }这段代码的要点和坑重载处理SysTick-VAL是24位递减计数器。当它减到0后下一个时钟周期会重载LOAD值并继续递减。在延时函数中如果延时时间跨度超过了计数器从start减到0再重载的时间简单的减法逻辑就会出错。上面的delay_us函数中的do...while循环通过判断current start来检测是否发生了重载并正确计算经过的周期数这是更稳健的做法。时钟精度SystemCoreClock / 1000000可能不是整数。例如168MHz / 1MHz 168是整数。但180MHz / 1MHz 180也是整数。如果是72MHz72也是整数。只要主频是1MHz的整数倍us延时就是精确的。否则会有最多1个时钟周期的误差。中断冲突如果你的SysTick已经用于RTOS心跳中断例如HAL_InitTick配置的1ms中断那么LOAD寄存器已经被设置为一个固定值如167999。此时你不能在延时函数中修改LOAD或VAL寄存器否则会破坏RTOS的心跳。这种情况下高精度延时需要使用其他通用定时器如TIM2。3.4 SysTick在RTOS中的关键作用与配置以FreeRTOS为例它在port.c文件中实现了与硬件相关的接口其中xPortSysTickHandler就是SysTick中断服务程序。在FreeRTOSConfig.h中你需要定义configTICK_RATE_HZ例如1000表示RTOS的心跳频率是1000Hz1ms一次。FreeRTOS的移植层会据此计算LOAD值#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) ) #define portNVIC_SYSTICK_CLK_BIT ( 1UL 2UL ) uint32_t ulTimerCountsForOneTick ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ); portNVIC_SYSTICK_LOAD_REG ( ulTimerCountsForOneTick - 1UL );configSYSTICK_CLOCK_HZ要么是configCPU_CLOCK_HZ使用HCLK要么是其八分之一。关键注意事项优先级设置SysTick中断的优先级需要设置为RTOS可管理的最低优先级。在FreeRTOS中通过configKERNEL_INTERRUPT_PRIORITY定义通常为15或255取决于优先级位数。确保所有其他外设中断的优先级都高于此值否则在关键代码段如任务切换发生中断可能导致系统不稳定。与HAL库共存STM32CubeMX生成的代码默认HAL_Init()会初始化SysTick用于HAL_Delay()和内部计时。当你启用FreeRTOS时CubeMX通常会自动将HAL的时基源从SysTick切换到另一个定时器如TIM1以避免冲突。你需要检查SysTick_Handler是否被替换为xPortSysTickHandler以及HAL_SYSTICK_Config是否被正确调用。Tickless Idle模式在低功耗应用中RTOS的Tickless Idle模式会在系统空闲时停止SysTick以进入深度睡眠。唤醒后需要根据睡眠时间补偿RTOS的Tick计数。这部分逻辑在移植层的vPortSuppressTicksAndSleep函数中实现配置相对复杂需要仔细处理唤醒源和时间补偿。4. 综合实战一个带MPU保护与SysTick延时的任务示例假设我们在FreeRTOS上创建两个任务一个任务正常运行另一个任务试图非法写入受MPU保护的外设地址比如GPIOA的MODER寄存器观察系统行为。#include “main.h” #include “FreeRTOS.h” #include “task.h” // 受保护的外设地址GPIOA MODER #define PROTECTED_PERIPH_ADDR ((volatile uint32_t*)0x40020000) // 正常任务 void vNormalTask(void *pvParameters) { while(1) { printf(“[Normal] Task is running…\n”); vTaskDelay(pdMS_TO_TICKS(1000)); // 使用RTOS延时基于SysTick } } // 恶意任务尝试非法访问 void vMaliciousTask(void *pvParameters) { vTaskDelay(pdMS_TO_TICKS(3000)); // 先让正常任务跑一会儿 printf(“[Malicious] Attempting to write to protected peripheral…\n”); // 此操作在用户模式下且MPU配置了该区域为特权访问将触发MemManage Fault *PROTECTED_PERIPH_ADDR 0xFFFFFFFF; printf(“[Malicious] You should never see this line!\n”); while(1); } // MemManage故障处理函数 void MemManage_Handler(void) { printf(“\n!!! MemManage Fault !!!\n”); printf(“CFSR (MMFSR): 0x%08lX\n”, SCB-CFSR); if (SCB-CFSR (1 7)) { // MMARVALID printf(“Fault Address: 0x%08lX\n”, SCB-MMFAR); } // 在这里可以记录错误、重启任务或复位系统 NVIC_SystemReset(); } int main(void) { HAL_Init(); SystemClock_Config(); MPU_Config(); // 配置MPU保护外设区域 // 初始化USART等… // 创建任务 xTaskCreate(vNormalTask, “Normal”, 128, NULL, 1, NULL); xTaskCreate(vMaliciousTask, “Malicious”, 128, NULL, 2, NULL); vTaskStartScheduler(); while(1); }运行这个程序预期输出是[Normal] Task is running… [Normal] Task is running… [Normal] Task is running… [Malicious] Attempting to write to protected peripheral… !!! MemManage Fault !!! CFSR (MMFSR): 0x00000100 (假设是数据访问违例) Fault Address: 0x40020000然后系统复位。这直观地证明了MPU在阻止非法内存访问、保护系统核心资源方面的作用。5. 常见问题排查与经验实录问题1启用MPU后程序在启动阶段就触发HardFault或MemManage Fault。排查检查向量表重定位如果应用程序从Bootloader跳转或者使用了RTOS向量表可能被重定位到SRAM。确保SCB-VTOR寄存器指向了正确的、MPU允许访问的向量表地址。检查堆栈指针初始化在Reset_Handler中系统会从向量表加载主堆栈指针MSP。确保向量表所在的地址通常是Flash起始地址被MPU配置为可读。检查初始化代码的访问权限在main()函数之前运行的启动代码如__libc_init_array用于初始化全局变量可能会访问.data和.bss段。确保这些SRAM区域在MPU中配置为可读写。简化配置开始时只配置一两个最基本的区域如Flash只读SRAM全访问逐步添加其他保护区域以定位是哪个区域配置导致了问题。问题2SysTick延时函数delay_us在延时时间长于几毫秒后严重不准。排查计数器重载逻辑错误这是最常见的原因。确保你的延时函数正确处理了SysTick计数器从0到LOAD的重载。参考上文提供的带重载处理的delay_us实现。SysTick被中断如果SysTick中断服务程序执行时间过长会阻塞主循环中查询SysTick-VAL的代码导致延时变长。确保中断服务程序尽可能短小精悍。系统时钟变化如果SystemCoreClock变量在延时期间被改变例如切换了系统时钟源计算出的usTicks就不准了。确保在系统时钟稳定后再初始化延时函数且运行时不变。问题3使用FreeRTOS时HAL_Delay()卡死或不准确。排查时基源冲突检查stm32f4xx_hal_conf.h或CubeMX中的HAL_TIME_BASE_SOURCE定义。在FreeRTOS项目中它应该被定义为除SysTick之外的定时器例如TIM1。中断优先级确保为替代时基源的定时器中断设置了合适的优先级通常应高于RTOS可管理的最高任务优先级即低于configMAX_SYSCALL_INTERRUPT_PRIORITY。osKernelRunning标志HAL库的HAL_Delay()函数在RTOS运行后会调用osDelay()。确保osKernelRunning标志已被正确设置通常在osKernelStart()中设置。问题4MPU配置后DMA传输失败。排查内存属性DMA访问的内存区域其MPU属性必须设置为Shareable (S1)。因为DMA是另一个总线主设备需要与CPU核心共享内存视图设置共享属性可以防止缓存一致性问题对于带Cache的芯片尤其重要。访问权限DMA控制器通常以“特权”模式访问内存。确保DMA要访问的源地址和目标地址所在的MPU区域至少允许特权模式访问。如果区域配置为用户模式无访问DMA传输也会失败。个人经验分享在资源紧张的Cortex-M4项目上如果不需要内存保护可以考虑禁用MPU以节省一点点功耗和性能开销虽然很小。但对于任何涉及第三方代码、复杂状态机或多任务交互的项目花时间配置MPU是值得的它就像给你的系统买了一份“保险”在程序跑飞时能给你一个清晰的故障现场而不是一个随机死机。SysTick的配置则要像对待心脏起搏器一样精确特别是当它同时服务于RTOS和裸机延时的时候一定要理清时钟源、优先级和重载逻辑避免“心跳紊乱”导致整个系统“心律失常”。

相关新闻