Skip to content

Implement deep sleep in embassy-mspm0 - #6580

Closed
CordlessCoder wants to merge 7 commits into
embassy-rs:mainfrom
CordlessCoder:mspm0-low-power
Closed

Implement deep sleep in embassy-mspm0#6580
CordlessCoder wants to merge 7 commits into
embassy-rs:mainfrom
CordlessCoder:mspm0-low-power

Conversation

@CordlessCoder

@CordlessCoder CordlessCoder commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

I'm generally following the approach embassy-stm32 took where it makes sense - the testing is done on a custom MSPM0L1305 board since that's what I have access to.

I used an LLM to assist me in navigating the TRMs for all the MSPM0 product families, to review the code as I write it and for first drafts of some code and documentation.

@CordlessCoder

CordlessCoder commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

I believe I fixed the race condition that caused the time driver to jump backwards when driven by LFCLK.

@CordlessCoder

CordlessCoder commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

To reproduce the timer jump bug I used this snippet after adjusting TICK_HZ and switching the clock source from MFCLK to LFCLK.

let mut last = Instant::now();
loop {
    let n = Instant::now();
    defmt::assert!(n >= last, "time went backwards: {}us -> {}us", last.as_micros(), n.as_micros());
    last = n;
}

It reliably panicked after 2 seconds(a single overflow cycle).
After the changes I left it running for 15 minutes without getting a panic.

This was tested on the MSPM0L1305RHB, but I'm quite confident about the fix translating to other MCUs.

@CordlessCoder CordlessCoder changed the title Implement deel sleep in embassy-mspm0 Implement deep sleep in embassy-mspm0 Jul 22, 2026
@CordlessCoder

Copy link
Copy Markdown
Contributor Author

I'm measuring current consumption in sleep/standby that matches the datasheet on my MSPM0L1305, so the sleep modes work.
I'll start working on integrating sleep into the executor.

Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated
@i509VCB

i509VCB commented Jul 23, 2026

Copy link
Copy Markdown
Member

The assembly output of now() is quite a bit of code, I feel there is some room for optimization:

.section .text._embassy_time_now,"ax",%progbits
        .globl  _embassy_time_now
        .p2align        2
        .type   _embassy_time_now,%function
        .code   16
        .thumb_func
_embassy_time_now:
        .fnstart
        .cfi_startproc
        .save   {r4, r5, r7, lr}
        push {r4, r5, r7, lr}
        .cfi_def_cfa_offset 16
        .cfi_offset lr, -4
        .cfi_offset r7, -8
        .cfi_offset r5, -12
        .cfi_offset r4, -16
        .setfp  r7, sp, #8
        add r7, sp, #8
        .cfi_def_cfa r7, 8
        ldr r1, .LCPI141_0
        ldr r2, .LCPI141_1
        b .LBB141_2
.LBB141_1:
        @MEMBARRIER
        ldr r4, [r2, #16]
        cmp r0, r4
        beq .LBB141_7
.LBB141_2:
        ldr r0, [r2, #16]
        @MEMBARRIER
        ldr r3, [r1]
        uxth r4, r3
.LBB141_3:
        ldr r3, [r1]
        uxth r5, r3
        cmp r4, r5
        mov r3, r4
        beq .LBB141_1
        ldr r3, [r1]
        uxth r4, r3
        cmp r5, r4
        mov r3, r5
        beq .LBB141_1
        ldr r3, [r1]
        uxth r5, r3
        cmp r4, r5
        mov r3, r4
        beq .LBB141_1
        ldr r3, [r1]
        uxth r4, r3
        cmp r5, r4
        mov r3, r5
        bne .LBB141_3
        b .LBB141_1
.LBB141_7:
        movs r2, #1
        ands r2, r0
        lsrs r1, r3, #15
        cmp r2, r1
        bne .LBB141_9
        lsls r2, r1, #15
        eors r2, r3
        lsrs r3, r0, #17
        lsls r0, r0, #15
        movs r1, #0
        b .LBB141_11
.LBB141_9:
        lsrs r1, r0, #17
        lsls r0, r0, #15
        movs r4, #129
        lsls r4, r4, #1
        ldr r5, .LCPI141_2
        ldr r5, [r5]
        tst r5, r4
        beq .LBB141_12
        lsls r2, r2, #15
        eors r2, r3
        movs r3, #0
.LBB141_11:
        adds r0, r0, r2
        adcs r1, r3
.LBB141_12:
        pop {r4, r5, r7, pc}
        .p2align        2
.LCPI141_0:
        .long   1074288640
.LCPI141_1:
        .long   embassy_mspm0::time_driver::driver::DRIVER
.LCPI141_2:
        .long   1074286640

@CordlessCoder

CordlessCoder commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

I suspect a lot of the bloat is the "torn read protection" loops - I'll double-check if TI's SDK actually does something similar. If it doesn't - they can be plain reads and that gets rid of the loops.

Edit: Removing them seems to break things :(

@i509VCB

i509VCB commented Jul 26, 2026

Copy link
Copy Markdown
Member

I would like to split the timer changes into a different pull request, along with the rhb packages.

Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated

// Only TIMG0 and TIMG1 remain clocked in STANDBY, so they are the only timers that can wake the
// core from deep sleep via the time driver. Reject a `low-power` build on any other timer.
// TODO: Or maybe allow using them, but disable STANDBY? STOP0 still work.

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.

I do think we should allow timers which cannot run in STANDBY by leaking an appropriate wake guard in time driver init.

Would also be worth documenting that specific timers for time driver use would not allow certain sleep modes.

Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated
@i509VCB

i509VCB commented Jul 26, 2026

Copy link
Copy Markdown
Member

Okay the timers were fixed in #6613

@bugadani bugadani added the e-mspm0 Issues for the MSPM0 family of chips label Jul 27, 2026
Comment thread embassy-mspm0/src/low_power/mod.rs Outdated
Comment thread embassy-mspm0/src/low_power/mod.rs Outdated
Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated
Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated
Comment thread embassy-mspm0/src/time_driver/tim.rs Outdated
Comment thread embassy-mspm0/src/uart/mod.rs Outdated
Comment thread embassy-mspm0/src/adc.rs Outdated
let r = T::info().regs;
let state = T::state();

// Busy-poll ENC by self-waking; never let the executor deep-sleep while a software-triggered

@i509VCB i509VCB Jul 27, 2026

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.

This will be better in the future when we have more options for clock source for the ADC (ULPCLK or HFCLK).

What we should do is set up an appropriate wake guard when doing an async read if our clock source happens to be SYSOSC which needs to be kept alive.

Otherwise the system will never truly go to a lower sleep mode if possible because we will effectively be doing a yield_now in a loop.

Comment thread embassy-mspm0/src/executor.rs
Comment thread embassy-mspm0/src/executor.rs Outdated
Comment thread embassy-mspm0/src/trng.rs Outdated
Comment thread embassy-mspm0/src/lib.rs Outdated
Comment thread embassy-mspm0/src/gpio.rs Outdated
Comment thread embassy-mspm0/src/gpio.rs Outdated
});
let (rise, fall, mask) = want_masks(id);

// The status bit does not say which edge fired, so the level standing now is the evidence.

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.

This is extra wording for no reason.

Something along the lines of "GPIO_ERR_01 requires that we detect level changes to determine if an edge event happened."

Comment thread embassy-mspm0/src/gpio.rs Outdated
let fired = if level.dio(i as usize) { rise } else { fall };

if fired.load(Ordering::Relaxed) & mask != 0 {
// Withdrawing both requests is what tells the waiting future its edge arrived.

@i509VCB i509VCB Jul 27, 2026

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.

"Clear the mask for this pin since the edge arrived."

Comment thread embassy-mspm0/src/gpio.rs Outdated
Comment on lines +1185 to +1188
// The other edge, or a wait that has since been cancelled. Discard it and stay armed.
// Waking at all is what errata GPIO_ERR_01 case 2 needs - the detector has no clock in
// STANDBY1, so it only observes the release if the release is itself a wake event - and
// leaving the status bit set would re-enter here forever.

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.

"Did not receive the correct event or no event is being waited. Per GPIO_ERR_01 clear this event otherwise entering STANDBY1 will mean the next wake up event is not detected."

Comment thread embassy-mspm0/src/gpio.rs
Comment on lines +1180 to +1183
// Notify the future that an edge event has occurred by masking the interrupt for this pin.
gpio.cpu_int().imask().modify(|w| {
w.set_dio(i as usize, false);
});

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.

We don't read RIS anymore in the future?

Comment thread embassy-mspm0/src/gpio.rs Outdated
Comment on lines +438 to +443
if matches!(polarity, Polarity::RISE | Polarity::RISE_FALL) {
rise.fetch_or(mask, Ordering::Relaxed);
}
if matches!(polarity, Polarity::FALL | Polarity::RISE_FALL) {
fall.fetch_or(mask, Ordering::Relaxed);
}

@i509VCB i509VCB Jul 27, 2026

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.

I don't think these ors have strong enough ordering.

One thread could be reading and then another thread preempts, reads and writes causing us to lose one bit. I think this needs AcqRel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't believe that's the case, AcqRel would only cause these to form happens-before relationships, which we don't care about because the order that the bits are set in doesn't matter, as long as they're set.

Comment thread embassy-mspm0/src/gpio.rs
Comment on lines +465 to +466
rise.fetch_and(!mask, Ordering::Relaxed);
fall.fetch_and(!mask, Ordering::Relaxed);

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.

Same here, I think this needs to be AcqRel.

Comment thread embassy-mspm0/src/gpio.rs
// the completion signal. Testing the request rather than the status bit also closes
// the window before the interrupt is first unmasked: an edge landing there is still
// held in the status bit, and unmasking below re-enters the handler to classify it.
if (rise.load(Ordering::Relaxed) | fall.load(Ordering::Relaxed)) & mask == 0 {

@i509VCB i509VCB Jul 27, 2026

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.

Note for ordering: Relaxed is fine here since the interrupt handler will preempt if needed (or we will wait).

Comment thread embassy-mspm0/src/gpio.rs

// Armed before the stale events are cleared, so the write below also discards anything
// latched by the arming itself.
let _arm = EdgeArm::new(block, key, polarity);

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.

I think we should use OnDrop from embassy-hal-internal here and do the setup and then have the drop closure do the deinit in here.

Comment thread embassy-mspm0/src/low_power/mod.rs Outdated
Comment on lines +7 to +13
//! Each family implements the SYSCTL power-mode sequence from the device TRM (chapter
//! "System Control (SYSCTL)" -> "Operating Modes"), cross-checked against TI driverlib's
//! `DL_SYSCTL_setPowerPolicy*`. The families are split into three behaviors:
//! - `full.rs` — STOP0/1/2 + STANDBY0/1: all G families and the supported L families.
//! - `c110x.rs` — STOP0/2 + STANDBY0/1 (no STOP1); STOP0 also clears `USELFCLK`: C-series.
//! - `h321x.rs` — STOP0/2 + STANDBY0/1 (no STOP1): H321x.
//!

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.

There is no value here in my opinion top say we copied driverlib. I think this highlighted section is needed in the docs.

Comment thread embassy-mspm0/src/low_power/mod.rs Outdated
Comment thread embassy-mspm0/src/low_power/mod.rs
Comment thread embassy-mspm0/src/low_power/mod.rs Outdated
Comment thread embassy-mspm0/src/sysctl/mod.rs Outdated
Comment thread embassy-mspm0/src/sysctl/mod.rs Outdated
Comment on lines +36 to +37
/// STOP1; rounded to STOP0 on families without it.
Stop1,

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.

Should this even be a variant on chip families that don't support it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But then each peripheral needs to care about what sleep modes are supported on what family, when all it actually cares about is "don't go into STOP1 or lower".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Retention has turned out to be a lot more complex, it'll be addressed in separate peripheral-specific PRs.

Comment thread embassy-mspm0/src/sysctl/mod.rs Outdated
Comment thread embassy-mspm0/src/sysctl/mod.rs Outdated
Comment thread embassy-mspm0/src/sysctl/mod.rs
Comment thread embassy-mspm0/src/time_driver/mod.rs
Comment on lines +31 to +42

/// Use the bus clock (ULPCLK), which runs at the MCLK rate.
///
/// Required to receive after a deep sleep. An asynchronous fast clock request forces the
/// MCLK/ULPCLK tree back to the SYSOSC base frequency, so a UART on this source recovers a usable
/// baud clock; LFCLK is not restored by the request and MFCLK only comes back if it was already
/// enabled. Pair with [`Config::low_power_rx_wake`].
///
/// The baud divisors are sized for the full ULPCLK rate, so an idle or in-flight transfer needs
/// the chip awake — a receiver holds a sleep floor of `Stop0` unless `low_power_rx_wake` is set,
/// and a transmitter holds one for the duration of each flush.
BusClk,

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.

This is power domain dependent which the HAL does not do yet. I don't see a good reason to add this right now.

@CordlessCoder CordlessCoder Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the only clock source that UART can issue an async fast clock request from, so the only one that will work for a deep sleep wake.
I2C already exposes BusClk as a clock option, even though it also depends on power domains.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Power domains have been implemented in mspm0-metapac and are waiting to be merged - power domain support will be implemented in peripheral specific PRs.

Comment thread embassy-mspm0/src/uart/mod.rs
Comment thread embassy-mspm0/src/executor.rs
Comment thread embassy-mspm0/src/gpio.rs Outdated
@CordlessCoder

Copy link
Copy Markdown
Contributor Author

The core executor and sleep mode changes have been split off into #6646, this PR will probably be superseded by multiple smaller PRs implementing low-power support for individual peripherals.

chiptool now generates register values in PascalCase rather than
SCREAMING_SNAKE, and emits the key/index fieldsets as newtypes carrying
associated constants instead of enums.

No functional change.
Which power domain a peripheral instance sits in decides whether it can
work through deep sleep at all: PD0 is powered in RUN/SLEEP/STOP/STANDBY,
PD1 only in RUN and SLEEP, and SYSCTL forces PD1 peripherals to a disabled
state on STOP/STANDBY entry. Arming a wake source on a PD1 instance is
therefore silently dead.

The domain is a per-instance and per-chip property, not a property of the
peripheral kind: UART3 is PD1 on G-series but PD0 on L122x/L222x, UART7 is
PD0 while UART3-6 are PD1, and TIMA0 differs between families. It has to
come from the chip metadata.
@CordlessCoder

Copy link
Copy Markdown
Contributor Author

It doesn't make much sense to keep this PR around since I've decided to split all of the changes off into smaller PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e-mspm0 Issues for the MSPM0 family of chips

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants