Bringing up a Cortex-M4 Buzzer on STM32MP153D: Lessons from the Pain
I just spent a long week trying to bring up a small Cortex-M4 firmware on an STM32MP153D board running OpenSTLinux. The firmware is simple in spirit. It plays sounds on a piezo buzzer using PWM from TIM1. The A7 cores keep running Linux. The M4 just plays beeps and short tunes.
That sounds easy. It was not easy. I want to write down the traps I fell into so other people do not lose the same week.
What I wanted
The STM32MP153D has two Cortex-A7 cores that run Linux, and one Cortex-M4 core for real-time work. I wanted to:
- Build my own M4 firmware (
Buzzer.elf). - Have U-Boot load it into the M4 at boot.
- Let Linux just “attach” to the running M4 and talk to it later through RPMsg, which is a way for the two CPU cores to talk to each other.
- Not break the rest of Linux.
The plan was clear. The pain was in the small details.
Trap 1: “rproc attached” means your reboot does nothing
On the Linux side I checked the M4 status:
cat /sys/class/remoteproc/remoteproc0/state
It said attached. Not running. Not offline. Just attached.
Here is what that really means. U-Boot loads the M4 firmware. Linux does not. Linux only attaches to whatever is already running on the M4. So when I did a normal reboot from Linux, only the A7 cores reset. The M4 kept running the OLD firmware. My new code never ran.
I wasted an entire debug session because of this. I rebuilt my firmware, copied it to /boot/rproc-m4-fw.elf, ran reboot, and tested. The bug was still there. Of course it was. The board was still running last week’s image.
The fix is to do a real hardware reboot:
echo 1 > /proc/sys/kernel/sysrq
sync
echo b > /proc/sysrq-trigger
echo b triggers a hardware-level reboot. Both the A7 cores and the M4 core reset. U-Boot runs again and loads the new ELF. Now you are testing what you think you are testing.
Lesson: if your remoteproc state says attached, treat reboot as a lie.
Trap 2: The ETZPC firewall is hiding behind everything
STM32MP1 has a peripheral firewall called ETZPC. It decides which world owns each peripheral: secure (TF-A / OP-TEE), Linux, or the M4. By default, almost nothing is given to the M4.
I wanted TIM1 for the PWM, plus DMA2 and DMAMUX1 to feed samples to TIM1. So I added them to the OP-TEE device tree as DECPROT_MCU_ISOLATION, which is the setting that gives the device to the M4 core:
DECPROT(STM32MP1_ETZPC_TIM1_ID, DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)
DECPROT(STM32MP1_ETZPC_DMA2_ID, DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)
DECPROT(STM32MP1_ETZPC_DMAMUX1_ID, DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)
If you forget this step, the M4 firmware runs but its writes to TIM1 do nothing. No error message. No crash. Just silence. Or a hang, if you are unlucky and the bus parks on an unanswered access.
Lesson: when something on the M4 just “does not work,” check ETZPC before you check anything else.
Trap 3: Linux still tries to probe what you handed off
So I told ETZPC that TIM1 belongs to the M4. Done, right?
Boot. Hang.
The reason: Linux did not know about my decision. Its device tree still had &timers1 enabled. The PWM driver tried to probe TIM1, hit the firewall, and got -EACCES. That error then cascaded into a device-link hang during boot. The kernel sat there waiting for a device that would never come up.
The fix is short. Disable TIM1 on the Linux side too:
&timers1 {
status = "disabled";
};
Both sides of the system have to agree about who owns each peripheral. The secure world hands it off, and Linux has to stop touching it. If only one side knows, you get a hang.
Lesson: every ETZPC handoff is two edits, not one.
Trap 4: Page alignment for OpenAMP carveouts
For RPMsg to work, you need shared memory carveouts. A carveout is a memory region we set aside that the kernel does not use for normal allocation. I needed four:
mcuram_rprocfor the M4 firmware code itself.vdev0vring0andvdev0vring1for the two queues in shared memory (a vring is a queue in shared memory, used by virtio).vdev0bufferfor the actual RPMsg message buffers.
My first try used the sizes from a sample:
vdev0vring0: vdev0vring0@10040000 { reg = <0x10040000 0x400>; ... };
vdev0vring1: vdev0vring1@10040400 { reg = <0x10040400 0x400>; ... };
vdev0buffer: vdev0buffer@10040800 { reg = <0x10040800 0x3000>; ... };
That is 1 KB for each vring and 12 KB for the buffer. Boot. The kernel rproc-virtio probe failed with -ENOMEM.
It took me a while to figure out why. The vring carveouts MUST be aligned to PAGE_SIZE (4 KB on ARM) AND sized in 4 KB multiples. 1 KB sizes are simply not allowed. The kernel cannot map a sub-page region as DMA-coherent memory.
The fix is to bump everything to 4 KB or more, and slide the addresses so each region starts on a 4 KB boundary:
vdev0vring0: vdev0vring0@10040000 { reg = <0x10040000 0x1000>; ... };
vdev0vring1: vdev0vring1@10041000 { reg = <0x10041000 0x1000>; ... };
vdev0buffer: vdev0buffer@10042000 { reg = <0x10042000 0x4000>; ... };
Lesson: every reserved-memory region for remoteproc must be page-aligned and page-sized. No exceptions.
Trap 5: The M4 ELF must use M4-side addresses
This one stung. The M4 sees its own memory through different addresses than Linux does. The chip has an AHB bridge that maps the M4 retention RAM at 0x38000000 from the A7 side, but the M4 itself only sees that same RAM starting at 0x00000000.
When I built the ELF, I had to make sure the vector table goes at 0x00000000, not 0x38000000. The dma-ranges in stm32mp151.dtsi map M4 0x00000000 to Linux 0x38000000, but that mapping is for DMA, not for the M4 program counter.
Get this wrong, and the M4 boots into garbage and never even fetches your reset handler. You will see nothing. No log. No life sign.
Lesson: in an asymmetric multi-processing system, you have to remember which side is reading which address.
Trap 6: How do I know my firmware is even running?
After all this, how do you actually know the M4 is running your new code?
md5sum /boot/rproc-m4-fw.elf is not enough. The file on disk can be new while the M4 still runs the old image (see Trap 1).
Here is the trick I ended up using. Pick a 4-byte word well inside the .text section. On the host, read it from the ELF file:
xxd -s 0x28fc -l 4 ssonic-m4-fw/STM32CubeIDE/CM4/Debug/Buzzer.elf
The math: my .text LMA is 0x10000000, and the ELF file places .text at file offset 0x2000. So file offset 0x28fc is .text + 0x8fc, which sits at runtime address 0x100008fc.
On the target, read that address from M4 memory using a small debug command I added:
m4dbg 0x100008fc
If the two values match, the M4 is running the new firmware. If not, do echo b > /proc/sysrq-trigger and try again.
Pick an address well inside .text, not near the start. The first kilobyte or so is mostly identical between builds (vector table, startup code), so it does not tell you anything.
Lesson: build a “fingerprint” check into your dev loop. You will save hours.
Closing thoughts
The buzzer is making sounds now. The MP3-to-PWM streaming side still has a producer/consumer race I am chasing, but at least I trust my dev loop today. A week ago I did not.
If I had to give one piece of advice to my past self, it would be this: do not trust reboot. That single misunderstanding was responsible for the most wasted hours.
The other lessons in short:
- ETZPC handoff is two edits: OP-TEE DTS, and Linux DTS. Both sides must agree.
- Vrings are page-aligned and page-sized. Always. No 1 KB shortcuts.
- M4 sees its own memory at
0x00000000. Linux sees the same RAM at0x38000000. Build the ELF for the M4’s view. - Always check the firmware fingerprint at runtime, not just on disk.
If you are starting an M4 project on STM32MP1, I hope this saves you a few of the days it cost me.
댓글 없음:
댓글 쓰기