Uncertain about type/trait bounds for passing generic perepheral into embassy task/async function

I am struggling to wrap my head around generic types and possibly some questions about how best to structure my code.

I have a pretty simple embassy-rs project that consists of the main task and a second task. Below is a simplified version of the code. This PoC is meant to monitor an accelerometer and signal via led/sound depending on weather sufficient movement has been detected.

It seemed prudent to not put all code in main loop so I created a task dedicated to handling the blink_task function. Eventually I plan on moving all of the accelerometer/filter code into it's own task but I wanted to start with the simpler of the two tasks and got stuck trying to break things out.

My Questions

  1. Is there a better guideline / rule of thumb for how much peripheral setup should be done and where?
  • This pattern seems interesting but doesn't seem wide spread.
  1. What can I do to adjust the signature for blink_task so the pwm argument can be generic? Right now, the type expression explicitly declares PWM0: (SequencePwm<'static, embassy_nrf::peripherals::PWM0>) but I'm having one hell of a time trying to figure out the syntax + trait/lifetime constraints needed to make this generic to any peripheral that implements SequencePwm . I've tried several different arrangements but always get back some rather verbose/intimidating compiler errors.

One such error:

the trait bound `(dyn embassy_nrf::pwm::Instance + 'static): Peripheral` is not satisfied
the following other types implement trait `Peripheral`:
  AnyPin
  AnyChannel
  AnyStaticChannel
  AnyConfigurableChannel
  AnyGroup
  AnyInput
  VddInput
  embassy_nrf::peripherals::RTC0
and 93 othersrustcClick for full compiler diagnostic
pwm.rs(27, 31): required by a bound in `SequencePwm`

Which happens when I try this signature:

#[embassy_executor::task]
async fn blink_task(
    led_1: AnyPin,
    receiver: Receiver<'static, NoopRawMutex, MotionState, 1>,
    // Works, explicit: 
    //     mut pwm: SequencePwm<'static, embassy_nrf::peripherals::PWM0>,
    // Produces Error(s) depending on syntax variation(s) used:
    mut pwm: SequencePwm<'static, embassy_nrf::pwm::Instance>,
) {
// ...
}

Which is a bit confusing to me as I did manage to track the peripheral set up to this macro:


macro_rules! impl_pwm {
    ($type:ident, $pac_type:ident, $irq:ident) => {
        impl crate::pwm::SealedInstance for peripherals::$type {
            fn regs() -> &'static pac::pwm0::RegisterBlock {
                unsafe { &*pac::$pac_type::ptr() }
            }
        }
        impl crate::pwm::Instance for peripherals::$type {
            type Interrupt = crate::interrupt::typelevel::$irq;
        }
    };
}

Which seems to imply that pwm::Instance is correct?

PoC Code:

#![no_std]
#![no_main]

use defmt::{info, unwrap};

use embassy_executor::Spawner;
use embassy_nrf::gpio::{AnyPin, Level, Output, OutputDrive, Pin};
use embassy_nrf::pwm::{Prescaler, SequenceConfig, SequencePwm, SingleSequenceMode, SingleSequencer};
use embassy_nrf::{bind_interrupts, peripherals, spim};
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::channel::{Channel, Receiver};
use embassy_time::Timer;

use static_cell::StaticCell;

use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(struct Irqs {
    SPIM0_SPIS0_SPI0 => spim::InterruptHandler<peripherals::SPI0>;
});

enum MotionState {
    Still,
    Moving,
}

static CHANNEL: StaticCell<Channel<NoopRawMutex, MotionState, 1>> = StaticCell::new();

#[embassy_executor::task]
async fn blink_task(
    led_1: AnyPin,
    receiver: Receiver<'static, NoopRawMutex, MotionState, 1>,
    mut pwm: SequencePwm<'static, embassy_nrf::peripherals::PWM0>,
) {
    info!("beep_task: init!");
    let mut led_1 = Output::new(led_1, Level::Low, OutputDrive::Standard);

    // Set up sequence engine; NOTE: configuration omitted here for brevity
    let seq_config = SequenceConfig::default();

    // Last step is 0, silent, effectively. This is a cheap way to insert a gap so loops are more distinct with seq_config delay config (omitted here)
    let seq_words: [u16; 3] = [800, 100, 0];

    let sequencer = SingleSequencer::new(&mut pwm, &seq_words, seq_config);

    loop {
        match receiver.receive().await {
            MotionState::Still => {
                led_1.set_high();
                sequencer.start(SingleSequenceMode::Times(2)).unwrap();
            }
            MotionState::Moving => {
                led_1.set_low();
                sequencer.stop();
            }
        }
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    info!("main: Creating channel...");
    let channel = CHANNEL.init(Channel::new());

    info!("main: Configure GPIO...");
    let p = embassy_nrf::init(Default::default());

    info!("main: Configuring SPI...");
    // Omitted for brevity: setting up some GPIO pins, passing them into SPI peripheral and passing the peripheral into SPI driver
    // Omitted for brevity: a bunch of register writes to set up the accelerometer / SPI peripheral

    // The goal here is to do just enough setup on pwm peripheral before passing it on to the task that will use it
    info!("main: setting up PWM...");
    let mut pwm_config = embassy_nrf::pwm::Config::default();
    pwm_config.prescaler = Prescaler::Div4;
    let pwm: SequencePwm<'_, peripherals::PWM0> = SequencePwm::new_1ch(p.PWM0, p.P0_07, pwm_config).unwrap();

    // Omitted for brevity: setting up a low pass filter on the SPI accelerometer inputs

    info!("main: spawning blink task...");
    unwrap!(spawner.spawn(blink_task(p.P0_08.degrade(), channel.receiver(), pwm)));

    loop {
        // Omitted for brevity: sample the accelerometer and determine if the device is moving or still

        // Placeholder for actual logic
        let is_currently_moving = false;

        if is_currently_moving {
            info!("movement!");
            channel.send(MotionState::Moving).await;
        } else {
            info!("still!");
            channel.send(MotionState::Still).await;
        }

        // Wait before we sample again
        // TODO: confirm we enter low power mode here
        // TODO: see if we can put the accelerometer to sleep?
        Timer::after_millis(1000).await;
    }
}

I don't have the embassy/embedded experience to answer your actual questions. However, it may help others if you say what chip you are using and link to the docs. For example the chip in that link doesn't have a pwm module.


I did find the module for this chip. If it is representative, you just can't use type erasure (dyn Trait) here.

  • Instance isn't object safe, so you can't have a dyn Instance at all
  • And SinglePwm<'_, T> doesn't support unsized T anyway
  • Moreover Instance it has a Peripheral<P = Self> bound, and
    • Peripheral has an explicit Sized bound
    • P can't be unsized either

Thanks for getting back to me!

The specific chip is 52810, the link you provided is perfect fit.

That might explain why this has felt so hard.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.