Phosfor / pca9548a

PCA9548a I2C-Expander Rust driver using embedded-hal
MIT License
0 stars 0 forks source link

Example when not using std? #1

Open haraldkubota opened 1 week ago

haraldkubota commented 1 week ago

Since this crate has features for std, by default it does not use std. Unfortunately all the (few, yet sufficient) examples use std:

let pca = Pca9548a::<std::sync::Mutex<_>>::new(i2c_bus, BASE_ADDRESS);

That std::sync::Mutex<_> needs to be something else when using no_std. What is it? I cannot figure it out. I know it's not

let pca = Pca9548a::<SyncMutex<_>>::new(i2c_bus, BASE_ADDRESS);

but what is the correct thing/syntax?

Phosfor commented 1 week ago

Thanks for the feedback.

I built this crate for a project that uses the esp_idf_hal which provides std; that's why I never bothered providing examples for other platforms. However, it was built with no_std in mind (at least in theory). The problem is that the mutex implementations are often highly platform dependent; in other words there is no singular mutex implementation for all no_std platforms. In order to support all systems, the mutex implementation is abstracted with three traits: MutexBase, SyncMutex and AsyncMutex.

You will have to implement MutexBase and either SyncMutex or AsyncMutex for the mutex type for your platform. Because of the orphan rule you will probably need to create a newtype struct and implemented the traits for that. Then you can use it just like the std::sync::Mutex.

Example:

use pca9548a::*;

// We cannot implement foreign traits for foreign types (orphan rule), so we need to wrap them in a newtype.
struct MyMutex<T>(some_platform::Mutex<T>);

impl<T> MutexBase for MyMutex<T> {
    type Bus = T;
    type Error = (); // You might want to use a custom error type, or maybe even core::convert::Infallible if locking the mutex cannot fail.

    fn new(v: Self::Bus) -> Self {
        Self::new(v)
    }
}

// If you want to use async code you probably want to use a mutex that supports it. Then you should implement AsyncMutex instead.
impl<T> SyncMutex for MyMutex<T> {
    fn lock(&self) -> Result<impl DerefMut<Target = Self::Bus>, Self::Error> {
        self.lock().or(Err(()))
    }
}

fn main() {
    let i2c_bus = ...;
    let pca = Pca9548a::<MyMutex<_>>::new(i2c_bus, BASE_ADDRESS);
    ...
}

If you can tell me what platform (i.e. microcontroller) you are using and if you want to use async, I might be able to help you with a more concrete implementation 😉

haraldkubota commented 1 week ago

Thanks for the quick reply. I do need some help here as this is more Rust than I understand (yet). Microcontroller: RP2040, using embassy-rp. And yes, async if possible.

Phosfor commented 1 week ago

Using embassy_sync this should be fairly easy. The AsyncMutex trait was made with the embassy_sync mutex in mind. I had already implemented the trait for it but never got around testing it, so never did a commit.

The implementation is now in the feature-embassy branch. For now, to use it you will need to change the Cargo.toml in order to use the git version (instead of crates.io's) and add the embassy feature:

pca9548 = { git = "https://github.com/Phosfor/pca9548a.git", branch = "feature-embassy" , features = ["embassy"] }

You will also have to add embyssy-sync to your dependencies (if you do not already have it).

Have a look at this is little test I wrote, that gives you an idea how to use the async version: embassy.rs.

Disclaimer: I did not really test the async API yet, only the test mentioned above; specifically I did not test it on real hardware. So please let me know if there is anything that is not working as expected or if you need more help. On the other hand I would also appreciate if you can tell me if there are no problems. Then I can maybe merge it into main and publish it on crates.io

haraldkubota commented 6 days ago

Cargo.toml updated for the pca9548a module. embassy-sync I already got. Can't get the parameter type right though. Not a problem of your crate, but my lack of knowledge on Rust.

` // Set up I2C0 for the SSD1306 OLED Display let i2c0 = i2c::I2c::new_async(p.I2C0, p.PIN_5, p.PIN_4, Irqs, Config::default());

// Set up I2C1 for the PCA9548A which has a BMP280 on Bus 2
let i2c1 = i2c::I2c::new_async(p.I2C1, p.PIN_3, p.PIN_2, Irqs, Config::default());
let pca = Pca9548a::<Mutex<CriticalSectionRawMutex, _>>::new(i2c1, BASE_ADDRESS);
let pca_2 = pca.single_subbus(2);

[...] let executor0 = EXECUTOR0.init(Executor::new()); executor0.run(|spawner: embassy_executor::Spawner| unwrap!(spawner.spawn(core0_task(pca_2)))); }

[embassy_executor::task]

async fn core0_task(i2cbus: embassy_rp::i2c::I2c<'static, I2C1, Async>) { [...] `

pca_2 obviously does not match the type provided in core0_task(), but from my understanding, single_subbus() is supposed to return an I2c object just like i2c::I2c::new_async() does (at least it should show the same traits). I clearly don't know what I am doing here. Or maybe I don't understand the single_subbus() part.

PS: Since copy&pasting sections of code is not that helpful, I uploaded a test-repo to https://github.com/haraldkubota/rp2040-pca9548a Line 74 is where I call core0_task() and I cannot get the parameter type right.

Phosfor commented 6 days ago

Thanks for uploading the code. It is indeed more complex than it might seem; even if you were familiar with Rust.

The object you get by calling single_subbus acts like an i2c bus, that is, it implements the embedded_hal::i2c::I2c trait (or embedded_hal_async::i2c::I2c respectively). However, it is not the same concrete type as the original bus. In this case the original type is embassy_rp::i2c::I2c<...> whereas the subbus is of type pca9548::SubBus<...>; both implement the I2c trait from the embedded-hal(-async) crate. Think of traits like interfaces in other programming languages.

For a normal function you could usually use generics or impl Trait parameters to solve this. Unfortunately, embassy tasks cannot use generics (and that includes impl Trait) and requires you to specify the concrete type. What's worse is that the SubBus type requires a lifetime parameter which is not 'static which cannot even be expressed without generics. We can probably get this lifetime to be static, but unfortunately there are a few more issues: the BMP280 driver uses the old embedded hal 0.2 API and SubBus currently does not support synchronous access with an async mutex.

I implemented some changes that should allow you to use it. I will create a pull request for your project so you can more easily see what changes you would have to make.

haraldkubota commented 5 days ago

Thanks for the explanation. I tried using impl Trait, but that did not work and I did not understand why, and I had no idea about the embedded-hal v0.2 which made things even more complicated that Rust already is. I learned a lot in this short discussion. Thanks a lot for the pull request. It works as expected and now I have a working example of how to access an I2C device behind a PCA9548A! PXL_20241016_122602456