How to configure MPU on a multicore Arm Cortex-M7 device
I have been fiddling with the Arm Cortex-M7 at work and I thought I should write a blog post about some of the tips/tricks and common pitfalls for bringing up the MPU ( memory protection unit ).
The main purpose of the MPU is to provide the ability to explicitly define attributes and behaviors when accessing memory in your application. It can isolate different areas of memory on your chip, restricting different cores to only be able to access certain regions of your memory map and sits between the CPU and memory subsystems to enforce rules on memory accesses. These memory accesses can also be given Read/Write/Execute permissions, i.e. allow you to make sure that no code executes from any region that you don't want it to and make sure certain regions aren't readable or writeable. You can define caching behavior as well, for the purpose of correctly defining memory access patterns between different cores and making sure the data the application sees isn't corrupted or stale.
I recommend first reading through the Armv7-M Architecture Reference Manual. My bare minimum recommended reading is chapter A2, A3, B1, B2, and B3. Reread B3.5 before starting your bringup. You should begin MPU configuration with a good foundation of how the system memory map works and that on a multicore chip, they share a unified memory map between all cores.
You can download and read the manual here - https://developer.arm.com/documentation/ddi0403/ee/
Let's begin.
1. The most important thing to do before even thinking about bringing up the MPU is organizing your memory map. On a multicore chip, you have different cores accessing different regions of RAM, as well as Flash memory. Typically, your linker script and build system will collect the Data, Code, stack/heap allocations, etc. for each core and stitch them together, and then each core will begin their execution at their respective locations in the unified memory map that Arm Cortex M7 devices use, from 0x00000000 to 0xFFFFFFFF.
I say one must organize their memory map because of specific rules for defining an "MPU region" - this basically being a starting address and a size for the region, where all accesses in that region will have to adhere to the specific rules for caching as well as access permissions defined for that region. Each core has their own on-chip MPU, and each core will have to configure their own MPU correctly, keeping in mind the overall system as well as what memory itself (the core) should be allowed to touch, as well as what memory in the overall memory map should shared between cores, what memory is prone to being touched by other bus masters such as DMA, etc.
The two rules for defining an MPU region are the following
1. The size of the region must be a power of 2, with a minimum of 32 bytes and a maximum of 4GB. This means that an mpu region can only be sized like 64 KB, 32MB, etc; always being a power of 2.
2. The base address, or where the mpu region starts, must be a multiple of ts own size. For example, a 32 MB MPU region can only sit on addresses that are a multiple of 32 MB, such as 0x00000000, 0x200000000, etc.
In order to effectively configure the MPU for a multicore chip, you must first define what RAM allocations each core will have. For a seamless MPU bringup, it is typical for RAM allocations to be in powers of 2, so that the MPU region for that core can obey rule #1, and the entire RAM allocation for the core can be done easily with one mpu region. Moreover, the core's base address must be aligned to a multiple of its own size, to obey rule #2.
With this in mind, is extremely helpful to create an excel sheet with the default system memory map and define how you want to setup your memory for all your cores. Doing this properly and thoroughly will set you up for success later on turning the MPU on. For each core, define its power of 2 size, and then place it at a base address that is a multiple of that size. You can do funky mpu configurations with subregions, but that should be a last resort as it typically complicates your process a lot.
After you have wrote down all your RAM allocations per core as well as where they will be in memory on paper, you will want to edit your linker script so that this memory allocation can take effect. Before turning on your mpu, organize your memory and then run your application as normal to make sure that all your cores are behaving as expected and there aren't weird things happening with your app. If you have done this properly, there should be no change in application behavior with the MPU off.
Next is to actually define your mpu regions and turn it on. The goal is to enable to MPU and to set the PRIVDEFENA bit off, so that any access to a region of memory without an explicit MPU region will MemManage Fault.
When defining your MPU regions per core, these are the most important registers.
MPU_RASR -> Region Attribute and Size register. This is where you will write the power of 2 size for that specific region number, as well as define all the memory attributes for that region.
MPU_RBAR -> this is where you will write the base address the mpu region starts.
MPU_CTRL -> this is how you will enable your mpu, as well as write PRIVDEFENA which will basically turn your mpu into "allow non-defined memory accesses through" mode or "be very strict in enforcing rules" mode.
1. It is always good to start with the default memory attributes for each memory region. This way, the behavior stays the same as what the core would use without the MPU turned on, and then you can change up the access/caching behavior to match whatever use case you need. For the default memory attributes, remember the following.
- The code region of the system memory map uses Write back, no write allocate caching. - The SRAM/RAM region uses Write Back, Write Allocate caching behavior. - Peripheral memory uses Device memory configurations.
Carefully read B3.5.9 of the reference manual to learn how to define your memory attributes correctly for that region. Also refer to B3.1 to find out what the default memory attributes are for each region of the system address map.
To facilitate the actual writing of code, it is good to define in a header file all the necessary #defines to place bits correctly to configure the mpu and reuse as much code as you can. For example, for the MPU_RASR, make a macro that takes a number and correctly shifts the bits over to write into the correct bit position for that register. Remember, if you use one file to configure the mpu for all your cores, you will have to make sure that one file works for all different cores, which will inevitably have different requirements for accessing memory.
The following below are some general tips/tricks for defining mpu regions. - Shared memory between cores should be marked Non-Cacheable. They should have their own mpu region with Non-Cachebale attributes for all cores that use that shared memory. - Memory that is touched by other bus masters, such as DMA, should be marked non-cacheable. - The peripheral region should be configured as device memory. - Make sure you either are aware of the cache configuration before you initialize your MPU, or you clean and invalidate your caches before turning on your mpu, which will affect memory access behaviors. You can end up with weird bugs that manifest in weird ways ( such as failed stack canary checks) when initially turning on your mpu.
The following are some general tips/tricks for debugging a misconfigured MPU. - Put while(1)s on all your reset paths that are triggered via exceptions or other app failures so that when you do reach a MemManageFault, you just spin in a loop and you can JTAG in and inspect the stack frame to find out exactly what went wrong. - Disable all watchdogs during debugging so that you don't run into watchdog failures that aren't a symptom of a wrong mpu configuration. - When you have a valid exception stack frame on a memmange fault, you can read stack frame pointer + 0x18 to find out exactly what instruction caused the fault. Use arm-none-eabi --numeric-sort on your elf files to find out what that instruction points to and figure out the root cause for your fault. - Thoroughly read the Armv7 exception model in section B1.5 of the Reference manual to understand what gets placed into the stack on exceptions to debug these faults. - Triple check to make sure that your mpu is being configured and the right bits are being written to isolate your application failures to application level bugs (like a core is accessing memory without a mpu region) instead of the hardware just being misconfigured.
There are many more intricate details and peculiarities about the MPU that you find when bringing up the MPU, but some of those details are just better learned when experienced. I hope this article gives you a good foundation of what to expect when turning the MPU on :)