Skip to content

src: Bound MSPLIM to the running task's stack in the scheduler. - #6

Merged
dpgeorge merged 1 commit into
micropython:mainfrom
kwagyeman:kwabena/sched-msplim
Sep 18, 2026
Merged

dpgeorge merged 1 commit into
micropython:mainfrom
kwagyeman:kwabena/sched-msplim

Conversation

@kwagyeman

Copy link
Copy Markdown
Collaborator

The cooperative scheduler runs each task on its own stack, allocated from the driver's pool, and mm_halow_context_switch() moves the CPU stack pointer onto it directly. On Armv8-M the main stack pointer has a hardware limit register (MSPLIM), and a port may point it at the main stack (the alif M55_HP startup does). A task stack sits below that limit, so the first push after the switch takes SP under MSPLIM and raises a stack-overflow UsageFault that escalates to a HardFault.

Move MSPLIM to the active task's own stack base while it runs and restore it when control returns to the scheduler. Every task entry goes through the one context switch in mm_halow_sched_run(), so wrapping it covers all cases. Guarded on a non-zero MSPLIM, so a port that leaves it at the reset value of zero is unaffected; where it is set the limit now tracks whichever stack is live, which is the correct behaviour.

@kwagyeman
kwagyeman requested a review from dpgeorge September 18, 2026 05:26
@kwagyeman

Copy link
Copy Markdown
Collaborator Author

@dpgeorge - This is the issue that was causing the AE3 not to work. Once merged, I'll soak test the N6 and AE3 again for the USA. Then, I should be able to have some insight into the AU issue. Not sure how this bug wasn't hit before on the AE3 as it was tested thoroughly previously.

@dpgeorge

Copy link
Copy Markdown
Member

Guarded on a non-zero MSPLIM, so a port that leaves it at the reset value of zero is unaffected; where it is set the limit now tracks whichever stack is live, which is the correct behaviour.

Is it worth making this configurable via a config option like MM_HALOW_SCHED_SET_MSPLIM, so it doesn't have to check MSPLIM=0 each context switch (and maybe get the logic wrong)? So, alif would enable it but stm32 would not.


I tried to test this change on OPENMV_AE3 with the corresponding MicroPython integration, but now I get:

>>> import network
>>> network.country("AU")
>>> h=network.HaLow()
>>> h.active(1)
E    15439 Transport init failed
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 5] EIO
>>>

That feels like a hardware connection error to me, eg lack of power or wrong IO, but I didn't change anything else.

The cooperative scheduler runs each task on its own stack, allocated from
the driver's pool, and mm_halow_context_switch() moves the CPU stack
pointer onto it directly.  On Armv8-M the main stack pointer has a hardware
limit register (MSPLIM), and a port may point it at the main stack (the
alif M55_HP startup does).  A task stack sits below that limit, so the
first push after the switch takes SP under MSPLIM and raises a
stack-overflow UsageFault that escalates to a HardFault.

A port whose startup sets MSPLIM enables MM_HALOW_SCHED_SET_MSPLIM; the
scheduler then moves MSPLIM to the active task's own stack base while it
runs and restores it when control returns.  Every task entry goes through
the one context switch in mm_halow_sched_run(), so wrapping it covers all
cases.  It is off by default and compiles to nothing where unset, so a
port that leaves MSPLIM at its reset value of zero is unaffected.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
@kwagyeman

kwagyeman commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Claude found why this was needed:

The AE3 did work before (your memory of AE3 perf tests is correct — the PR's first post publishes AE3 numbers). What broke it: rebasing PR #19617 onto newer MicroPython pulled the alif CMSIS-DFP 1.3.4 bump (micropython commit d4b5eb8f0, 2026-03-04), whose startup_M55_HP.c sets the hardware stack-limit register MSPLIM = __StackLimit. The driver's cooperative scheduler switches MSP onto heap-allocated task stacks, and on the AE3 the GC heap sits entirely below __StackLimit — so every task stack push trips a stack-overflow fault (STKOF → HardFault → USB drops). Deterministic on that memory map. Pre-rebase, MSPLIM was 0, which is the fix's no-op path, so it passed. The N6 sets MSPLIM too but its task stacks happen to land above it, which is why the N6 never showed it.

3. Perf + soak — both boards, matching PR #19617 (channel 28 / 2 MHz, 20-min soaks)

Metric AE3 fixed (54 cyc) PR AE3 N6 fixed (52 cyc) PR N6
Assoc 3.48 s 3.6 s 3.10 s 6.3 s
UDP-up 3.33 3.2 8.11 8.1
TCP-up 3.26 2.1 7.50 6.0
TCP-down 3.24 2.6 3.74 5.0

Both 20-minute soaks passed — connected throughout, no crashes, asserts, or leaks. The AE3 matches/exceeds every published number. The N6 hits UDP-up 8.1 = the PR's 8.1 exactly and exceeds TCP-up; the one low figure is N6 TCP-down (3.7 vs 5.0), which is an environment/server-side artifact, not the fix — the AE3's TCP-down actually exceeded its PR number, and my fix doesn't touch the datapath. (I used my own throughput server since .137 wasn't up; udp_down reads ~0 on both from an unpaced-flood quirk, and it isn't a PR-reported metric.)

Both boards are back at a clean REPL on the fixed firmware. Full logs are on the boxes if you want to see the per-cycle data. Nothing needs your attention except pushing the driver branch when you're up.

...

Anyway, updating this PR to be configurable. Also, I've bought the Morse Micro access points for JP, EU, and AU. I'll get the driver working on all 3 of them too, in addition to the USA. Routers should be here on Monday.

@kwagyeman

kwagyeman commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Configurable version is in: MM_HALOW_SCHED_SET_MSPLIM, default off, compiles out entirely where unset (so no per-switch check on ports that don't set it). The MicroPython side enables it for both OPENMV_AE3 and OPENMV_N6.

On the N6, it does have MSPLIM set, just not from the app image. I read it at runtime and it's 0x30001830, which mboot leaves in a lower SRAM region than where the app runs its stacks (MSP ~0x341Fxxxx, heap/stacks in 0x34xxxxxx). So today the task stacks all sit above it and it never trips, but that's a dependency on mboot's leftover rather than a guarantee, so I turned the flag on for the N6 too. Both boards associate with it on.

On the Transport init failed: that's downstream of this change. The stack fault is gone, so morselib now boots and reaches the SPI transport init, which is where you're stopping. The bench AE3 runs the same firmware through association and a 20 minute soak at ~3.3 Mbit/s, so the driver path is good. That EIO is the transceiver not answering on SPI, so I'd check sitting correctly. However, let me get through testing AU/JP/EU first to make sure the driver is working on all systems.

@dpgeorge dpgeorge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating. Good to know the updated alif_ensemble-cmsis-dfp was the reason. Also good to see it enabled on N6.

I also note the assoc time for N6 is much improved, now matching AE3

@dpgeorge

Copy link
Copy Markdown
Member

That EIO is the transceiver not answering on SPI, so I'd check sitting correctly.

Yes that's what I suspect. I will test again and check voltages.

let me get through testing AU/JP/EU first to make sure the driver is working on all systems.

That would be useful. I tried to change my router's region to US (by logging in via ssh to the router and low-level editing the /etc files) but that did not work, I suspect the MM chipset itself is locked to the region.

@dpgeorge
dpgeorge merged commit 208ee54 into micropython:main Sep 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants