Powering Down I2C, Coordinating Initialization/Shutdown

I am running in esp_hal land, I have a chip with an I2C bus, and there are multiple devices on the bus. To coordinate things it looks like I could do my own traffic control with a mutex, or use shared-bus to do it nicely for me. Cool.

But I also want to regularly power down the bus, to use as little power as possible. Each device needs a chance to do any needed initialization after the bus powers up, and each device needs a chance to tidy up before it powers down.

What is the best way to let my code for each device know about these events?

Do I need to go back to rolling my own with a mutex and something like a trait with power_up() and power_going_away() functions? Or, is there already a nice Rust way to do this?

Thanks,

-kb

It this the best discussion forum for embedded Rust? The responses here have been deafeningly quiet.

(I know there are I know there are chat-style forums, and they can be good for fast responses, but they tend to require the right people be online. And for an obscure question, that might not be so.)

Thanks,

-kb

well, it depends, as always.

the hal crate provides a portable API to talk to the i2c bus master, on top of which you can write drivers to suit your use case. generally I see two way to structure a driver system design:

  • passive bus/master driver and passive (or semi-active) device/slave drivers

    this design suits simple applications, e.g. if the application is in a "polling" style. here's what I mean by this:

    the bus is passive, means the application logic don't interact with the bus directy (apart from initialization etc). instead, each individual device driver shares the bus controller, and the application only use the device API.

    such designs usually looks like this:

    struct I2cController {
      //...
    }
    struct FooSensor<'b> {
      bus: &‘b Mutex<I2cController>,
      //...
    }
    struct BarActuator<'b> {
      bus: &‘b Mutex<I2cController>,
      //...
    }
    fn main() {
      // initialization
      let i2c = Mutex::new(todo!());
      let foo = FooSensor::new(&i2c);
      let bar = BarActuator::new(&i2c);
      // event loop
      loop {
        if let Some(data) = foo.read_new_data() {
          // data processing
          //...
          bar.set_speed(...);
        }
      }
    }
    

    with this kind of design, the app is in control of everything, it's the app's responsibility to bring the devices to lower power mode and back. the app must control the the power management unit, obviously, and also some external interrupt sources such as a timer, in order to wake up from low power.

    note, the wake up event sources can be the i2c devices themselves, if the hardware is so wired, that's why I say they may be "semi-active", for the interrupts.

    in this scenario, the device drivers can just implement the power_up() and power_going_away() methods (and API to setup the interrupt if needed), and expect the app to call them at the correct time.

  • possibly active bus master (or higher level abstraction such as a subsystem) with passive device drivers

    in this case, the focus is on the master side, e.g. interrupt driven, asynchronous, potentially with very sophisticated scheduling algorithms, etc., to optimize the bus traffic.

    on the other hand, the device drivers are typically implemented via some callback mechanism and registerred with the bus subsystem. it may look like this:

    struct I2cController<'b> {
      devices: ArrayVec<&'b mut dyn I2cDevice>,
      //..
    }
    trait I2cDevice {
      fn init(&mut self, bus: &mut I2cController);
      fn on_incoming_packet(&mut self, packet: &[u8]);
      fn poll_outgoing_data(&mut self, buffer: &mut [u8]) -> usize;
    }
    struct FooSensor {...}
    struct BarActuator {...}
    impl I2cDevice for FooSensor {...}
    impl I2cDevice for BarActuator {...}
    fn main() {
      // initial setup
      let foo = FooSensor::new(...);
      let bar = BarActuator::new(...);
      let i2c = I2cController::new(...);
      // register with the subsystem
      i2c.register_device(FOO_ADDRESS, &mut foo);
      i2c.register_device(BAR_ADDRESS, &mut bar);
      // enter the scheduler
      spawn_task(|| i2c.run());
      set_idle_callback(|| i2c.power_saving());
      scheduler_main_loop_no_return();
    }
    

    in such systems, the most obvious way to add the power saving events is the callback trait.

    if the power saving is specific to the i2c subsystem, then it's perfectly reasonable to add them to the subsystem callbacks, such as I2cDevice in this example.

    another possibility is if you have a dedicated power management subsystem, which manages all power saving events, for all the devices (including bus masters, not only i2c bus, but other buses too). such subsystem would be more complex, since you must consider the order and dependencies graph of all the device nodes.


this forum is for rust the language in general, so as long it's rust related topics, you can definitely post here.

however, since rust is being used in so many different application domains, you might not get the best chance to get a timely response for specific problem. after all, embedded rust is a fraction of the community, and not everyone who is using rust is reading this furom. what's more, it's usually very hardware specific.

other places you can try:

there's a discussion forum in the esp-hal github repository, presumably more people there are familiar with the esp family of devices, so you might get better help there.

there's also a Rust board on the esp32 forum. (there's a quick link to this forum from the espressif developer portal page, so I assume there will be tech staff from the company there)

and you can also post your question on forums for general programming discussions, such as stackoverflow.

in any case, if you post the same information on different websites, you should add cross links to avoid duplicated works for other people.

Probably not. The (chat style) Matrix for embedded Rust is likely better. And that is unfortunate, since that isn't searchable for the next person who has the same question.