šŸ¤– AI 资讯

Ā· Ā· ↗
← čæ”å›žåˆ—č”Ø

Playing with the STM32MP2

Lobstersē®—åŠ›čŠÆē‰‡,å…·čŗ«ę™ŗčƒ½,开源,OpenAI,Meta,Agentę™ŗčƒ½ä½“,ęŽØē†ę€č€ƒ,å¼ŗåŒ–å­¦ä¹ ,端侧AI,ꋛ聘HR,ē½‘ē»œå®‰å…Ø,å¼€å‘č€…ē”Ÿę€åŽŸę–‡ ↗

Playing with the STM32MP2

Record scratch

First off, an apology - this post is long overdue. I started writing it back even before the TCXO post, I think some time in December 2025. But then I ended up accidentally finding a silicon bug with security impacts (described later on in this post, and stay tuned for a blog from work on the bug in particular) that I had to keep quiet about until ST had a chance to review and put an advisory out, and there wasn’t a whole lot I could publish about this project until then. Once that finally came out I got busy on other things.

Anyway, here we are. My long time readers will know that I like using STM32s and pairing them with FPGAs, and have got some pretty decent performance doing this, e.g. close to a Gbps of Ethernet bandwidth from the MCU to an FPGA-side RGMII MAC. There’s just one problem: the STM32H7 FMC needs a lot of pins - 30+ if you want all the chip selects and address lines. And they’re not nicely located all on one side of the package either, that would be too easy. They’re all over the place.

Here’s an example of what the FMC looks like on the TFBGA240 package for the H750.

And the TFBGA216 isn’t any better, either.

For years, I’ve dreamed of a better way. My ultimate dream MCU, which doesn’t exist because I’m probably the only weirdo who would buy it, would be a 1 GHz Cortex-M7 or M85, probably made on TSMC 28LP, with a megabyte of NOR flash or ReRAM, a megabyte of SRAM, an RGMII MAC, multiple FPGA-style I/O banks with independent VCCIO rails, and an AXI Chip2Chip SERDES block that I could use for directly integrating with FPGA-based peripherals using only two differential pairs.

The STM32H735 paired with an external FPGA over FMC got close, but was annoying to use on a low-cost board (e.g. 4 layer batch fab) because the FMC is almost impossible to route on those constraints. Some other STM32H7 parts like the H75x line are available in an 0.8mm package which is doable (painfully) on batch fab design rules, but these have a lower max CPU speed (480 vs 550 MHz) and this is really just kicking the can down the road a bit rather than fully solving the problem. You’re still burning a ton of pins.

A fast M7 with PCIe would almost be what I want (PCIe isn’t exactly AXI, but bridging them is totally doable), but nobody makes those either. You can get Cortex-R’s with PCIe, which I seriously considered (and still want to check out) but it would still be a big jump from the familiar STM32 peripheral set. And a lot of these parts either don’t have crypto acceleration or security-by-obscurity lock up the docs to the crypto accelerator, which is an absolute dealbreaker for me.

Enter the STM32MP2

But then ST came out with the STM32MP2 line. This is a full applications processor SoC with a dual Cortex-A35, some slower auxiliary processors like an M33 and a M0+, a full GPU/NPU I may play with one day, and – most interestingly to me – a 5 Gbps SERDES with PIPE interface that could be configured to operate as either PCIe gen1/2 x1 or USB 3 depending on which internal protocol IP you connect to it (there’s a register bit to mux them). It also kept most of the same familiar STM32 peripherals I already had lots of libraries for and experience working with, so the learning curve getting spun up on it would be short - including the same AES-GCM and TRNG IPs that I already had drivers for, and were reasonably well documented.

It’s quite a beast with far more computing power than I needed, but I did want to have a part in my back pocket I could pull out for future designs that needed more oomph. And the PCIe would potentially allow easy memory mapping of an FPGA from firmware via only two differential pairs.

There’s just one problem: the STM32MP2 was clearly designed to run Linux, and the vendor BSPs included all kinds of u-boot, TrustZone, etc. stuff that I didn’t care about. I just wanted a big microcontroller that could run a simple RTOS or even a while(1) { doStuff(); } event loop.

How hard could it be?

So I decided to see if I could convince the MP2 that it was, in fact, just a very fast MCU with some oddball peripherals like PCIe and LPDDR PHYs that garden-variety MCUs don’t have.

The goals of the experiment were:

  • Build entirely with upstream aarch64 / arm g++ and binutils, no vendor SDKs or HALs
  • Get code running on the A35s plus the M33 auxiliary core
  • Don’t use external DDR (this is a massive board complexity adder and I wanted to stay as close to MCU-level BOM/PCB design effort levels as possible); all code and data lives in on-die SRAM
  • Don’t use trustzone, uboot, or any other baggage. Just run a simple event loop in EL3 out of the initial ā€œfirst stage boot loaderā€ (FSBL) flash image and have that be the entire firmware.
  • Get PCIe working to an FPGA under these constraints

I bought a STM32MP257 development board (STM32MP257F-EV1) and started out the same as I would with any other new STM32 part: added a linker script and header to stm32-cpp with the addresses of the peripherals that used the same register interface as other STM32 parts I already had supported in the library. The only difference was that the image base address was in SRAM rather than flash.

M33 bringup

Then I built a simple LED blinky firmware for the Cortex-M33 auxiliary core and loaded it over JTAG just like I would for a regular STM32. There were a few false starts but I got a blinky running fairly quickly. Achievement #1 unlocked: I can talk to the chip and poke registers.

The next milestone was to actually boot the same blinky from flash. It took some digging through example code and wikis to figure out all of the fine points (for example, the internal SRAM is mapped at four different physical addresses with different TrustZone etc settings, but the boot ROM only lets you boot from one of them). Before too long I had a tool that took a flat binary RAM image and bolted on the necessary FSBL headers for the STM32MP2 boot ROM to load it from flash and execute. Then I made a little flasher utility that could be loaded into SRAM and would write an image from another SRAM location to flash, bootstrapping the whole process. Achievement #2 unlocked: the chip is booting from nonvolatile memory and doesn’t need to be loaded over JTAG each boot.

A few things to keep in mind when preparing the FSBL image:

  • Place your FSBL-M code image in RETRAM, specifically within the boot alias at 0x0e08_0000.
  • Your stack and initial globals must live in RETRAM too, but on the D-side bus alias at 0x3008_0000.
  • Your memory map must ensure that the physical memory of these doesn’t overlap, i.e. if you have code at 0x0e08_4141 you cannot also have data at 0x3008_4141. The linker script gymnastics to handle .text, .data, and .bss going to different logical addresses but also not colliding with one another are an adventure but not too bad once you realize how it works.
  • There are two slots for FSBL-M images, primary at flash offset 0x0000_0000 and fallback at 0x0004_0000

This firmware booted in ā€œM33-TDā€ mode, in which the boot ROM immediately transfers control to the M33 after loading the FSBL image, and the M33 has full TrustZone privileges. I chose this over ā€œA35-TDā€ (in which the A35 boots first) mostly because I wanted to do a staged bring-up of the platform and not have my first time working with the SoC also be my first time bringing up a bare metal Cortex-A.

I continued along the same way I would with any other new STM32 platform for a bit, bringing up a serial console, enabling caches, configuring the PLLs and clock tree, dumping a bunch of platform information to the console on boot, etc.

The code is all on my GitHub but a quick summary of what you need to do for bare metal bringup in M33-TD is as follows. None of the SoC setup code here requires custom assembly, it can all be done in C/C++ except for CPU configuration stuff like enabling/disabling interrupts or the FPU when you’re ready to do that.

  • Enable the VDDIO monitor for VDDIO1…4. Wait until they report valid, then tell the PWR they’re ready. Without this step, most of your IOs won’t be available.
  • Optional: Enable writing to backup SRAM (by default it’s gated by POR/brownout detection to avoid glitches when the Vcore domain drops out of regulation)
  • Set SYSCFG.VDERAMCR to use the video encoder RAM as processor memory since we’re not using the video block
  • Enable all of the SRAMs (SYSRAM, SRAM1, SRAM2, backup SRAM, VDERAM, LPSRAM1…3) in the RCC. There’s a lot of them.
  • Set the RISAB SECCFGR register settings to enable secure domain code to access SRAM1/2 and VDERAM. By default these aren’t accessible to EL3 secure mode. We’re writing all of our code in EL3 secure so we want everything allocated there.
  • Initialize the clock tree. There’s a LOT of different clocks, you can look at my code to see the details of how I set it up.
  • Enable and invalidate the M33’s I-side and D-side L1 caches

On to the A35

Then it came time to get the A35 running, which I had to do before playing with the PCIe because the M33’s memory map did not allow it to talk to the PCIe BAR region – annoying, but something I could live with if the M33 is mostly acting as a management processor and all our application code is on the A35.

I had previously avoided working with Cortex-A’s because the overhead involved in getting them set up was a lot higher.

First, once you have the A35 image in flash, you need to actually start it. Figuring this out took quite a bit of reading of various third party library code and such, the datasheet didn’t make it obvious. The basic steps are:

  • Check the DSTATE of the CPU in PWR.CPU1D1SR. It should be in STANDBY.
  • Send a SEV via EXTI1.RPR3 to wake up the CPU
  • Reset the CPU via RCC.C1RSTCSETR
  • Set EXTI1.SWIER3
  • Wait until PWR.CPU1D1SR reports the CPU is in HOLD_BOOT state
  • Release the CPU to boot by setting RCC.CPUBOOTCR to 2

The FSBL-A image in flash (at offset 0x0008_0000 for primary and 0x000c_0000 for fallback) itself then is launched in parallel on both cores in the cluster simultaneously in aarch64 mode (although there is a fuse you can blow to boot in aarch32 mode instead if you wanted to for some reason). The first part of this initialization does have to be done in assembly because there’s a bunch of stuff needed that pokes hardware registers and initializes the stack etc.

Note also that this image has to be in flash: even if you have code executing on the M33 already, there does not appear to be a good way to just have the M33 dump an image into SYSRAM and have the A35 execute it: when the A35 is released to boot the ROM will check strap pins and load the image from the specified location (in my case SPI NOR flash); even though the M33 is already running the A35 is going to re-run the boot ROM.

  • The image should be loaded to the boot/secure SYSRAM alias (0x0e00_0000)
  • The image begins with a jump to the reset vector, then a bunch of padding so the exception table is aligned at a 2 kB boundary as required by hardware. There’s probably some room to optimize here, right now that space is wasted but it should be possible to fit most of the low level initialization in there too.
  • The exception table is repeated four times for EL0, higher exception levels, and synchronous exceptions from lower levels. You have 128 bytes for each handler in the table itself, they’re not vectored like in M. But you can jump to external code if you need more space. I’m not a huge fan of this, I prefer the Cortex-M NVIC. Maybe this saves a branch instruction and reduces interrupt latency slightly? Anyway, for early testing I just set every exception handler to be an infinite loop.
  • Register values are undefined at startup (whatever the boot ROM left them at), so I clear them all to zero before going any further.
  • Exception status registers also need to be cleared
  • Initialize the floating point unit and NEON/vector units if you plan to use them. My current startup code doesn’t do this yet since I only do integer stuff, but if you plan to ever use floats it needs to happen here.
  • Point VBAR_EL3 to the exception table so if we throw an exception we don’t end up back in the boot ROM’s exception handler
  • Enable cache coherency in CPUECTLR_EL1

At this point the lowest levels of the core are set up, but we don’t know which core we’re on yet. So the next step is to read MPIDR_EL1 and figure out which one we’re on, since the subsequent steps vary depending on which core we’re on.

If we’re on core 0:

  • Initialize the stack pointer. Now we have a valid environment to start runing C++ code, with one caveat: we cannot call memcpy yet, because until the MMU is enabled everything is considered ā€œdeviceā€ memory and the newlib aarch64 memcpy implementation will try to ldp from unaligned addresses which isn’t allowed from device memory.
  • Create initial page tables. My mapping has a total of three page tables, one level 1 (512x 1GB pages covering the entire memory map) and then two level 2 (512x 2 MB pages) under it. One of them covers the on chip SRAM and PCIe BAR region, the other covers the peripherals, OCTOSPI, and FMC. In my mapping, everything is set up as cacheable except MMIO ranges and the translation is 1:1 (VA = PA) since I am only using the MMU to control NX and caching attributes (I wish I could get a Cortex-M MPU here). I intentionally left the pages covering the boot ROM region as unmapped since it’s mapped at 0x0000_0000 and I want null pointers to segfault, not read random stuff from the ROM.
  • Once page tables are set up, turn on the MMU.
  • Call BSP hooks to do any other power, clock, etc. initialization required before application code starts. This includes configuring and starting the main PLL for the A35 itself.
  • Call __libc_init_array to run constructors for global objects
  • Set a flag to tell core 1 that we’re done with the system init

If we’re on core 1:

  • Initialize the stack pointer
  • Spin until core 0 finishes setup
  • Set up the MMU pointing at the same page table as core 0. Page tables are 2 kB each and we have limited on chip RAM… we don’t want to burn two page tables worth of memory unless there’s a good reason to have different mappings on each core. (This is one of the reasons I like Cortex-M’s, the MPU has far less baggage than the A’s MMU. But nobody makes M’s with PCIe so here we are.)

On either core:

  • Invalidate and enable the instruction and data caches
  • Call the CoreMain() function provided by the BSP, passing the core number as the argument, so application code can start running.

All cores are set up. What now?

You’ll notice that I haven’t said much about the DDR. That’s because I don’t plan to use it. It’s a Synopsys DDR3L/DDR4/LPDDR4 controller that requires a blob plus annoying PCB layout for all the data lines, uses a bunch of power, and is just all around overkill. The whole point of switching from the STM32H735 to the MP2 was to replace the parallel memory bus with PCIe and using DDR would negate most of those benefits.

Anyway… at this point, you basically have a STM32H7 on ste