diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index 0a51080df..f2832f6db 100644 --- a/.github/workflows/build-deploy-docs.yml +++ b/.github/workflows/build-deploy-docs.yml @@ -48,7 +48,7 @@ jobs: - name: Build rustdoc docs run: | - cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,no-boards,random,threading,usb + cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb echo "" > target/doc/index.html mkdir -p ./_site/dev/docs/api && mv target/doc/* ./_site/dev/docs/api diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ff045f3b..d9f3c736d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -110,12 +110,20 @@ jobs: # TODO: we'll eventually want to enable relevant features - name: Run crate tests run: | - cargo test --no-default-features --features no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros - cargo test --features external-interrupts,embassy-rp/rp2040 -p riot-rs-rp - cargo test --features external-interrupts,'embassy-nrf/nrf52840' -p riot-rs-nrf - cargo test --features external-interrupts,'embassy-stm32/stm32wb55rg' -p riot-rs-stm32 + cargo test --no-default-features --features i2c,no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros cargo test -p rbi -p ringbuffer -p coapcore + # We need to set `RUSTDOCFLAGS` as well in the following jobs, because it + # is used for doc tests. + - name: cargo test for RP + run: RUSTDOCFLAGS='--cfg context="rp2040"' RUSTFLAGS='--cfg context="rp2040"' cargo test --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp + + - name: cargo test for nRF + run: RUSTDOCFLAGS='--cfg context="nrf52840"' RUSTFLAGS='--cfg context="nrf52840"' cargo test --features external-interrupts,i2c,'embassy-nrf/nrf52840' -p riot-rs-nrf + + - name: cargo test for STM32 + run: RUSTDOCFLAGS='--cfg context="stm32wb55rgvx"' RUSTFLAGS='--cfg context="stm32wb55rgvx"' cargo test --features external-interrupts,i2c,'embassy-stm32/stm32wb55rg' -p riot-rs-stm32 + lint: runs-on: ubuntu-latest @@ -164,40 +172,47 @@ jobs: with: args: --verbose --locked --features no-boards,external-interrupts -p riot-rs -p riot-rs-boards -p riot-rs-chips -p riot-rs-debug -p riot-rs-embassy -p riot-rs-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-threads -p riot-rs-utils + - run: echo 'RUSTFLAGS=--cfg context="esp32c6"' >> $GITHUB_ENV - name: clippy for ESP32 uses: clechasseur/rs-clippy-check@v3 with: - args: --verbose --locked --target=riscv32imac-unknown-none-elf --features external-interrupts,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp + args: --verbose --locked --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp + - run: echo 'RUSTFLAGS=--cfg context="rp2040"' >> $GITHUB_ENV - name: clippy for RP uses: clechasseur/rs-clippy-check@v3 with: - args: --verbose --locked --features external-interrupts,embassy-rp/rp2040 -p riot-rs-rp + args: --verbose --locked --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp + - run: echo 'RUSTFLAGS=--cfg context="nrf52840"' >> $GITHUB_ENV - name: clippy for nRF uses: clechasseur/rs-clippy-check@v3 with: - args: --verbose --locked --features external-interrupts,embassy-nrf/nrf52840 -p riot-rs-nrf + args: --verbose --locked --features external-interrupts,i2c,embassy-nrf/nrf52840 -p riot-rs-nrf + - run: echo 'RUSTFLAGS=--cfg context="stm32wb55rgvx"' >> $GITHUB_ENV - name: clippy for STM32 uses: clechasseur/rs-clippy-check@v3 with: - args: --verbose --locked --features external-interrupts,embassy-stm32/stm32wb55rg -p riot-rs-stm32 + args: --verbose --locked --features external-interrupts,i2c,embassy-stm32/stm32wb55rg -p riot-rs-stm32 + + # Reset `RUSTFLAGS` + - run: echo 'RUSTFLAGS=' >> $GITHUB_ENV - name: rustdoc - run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,no-boards,random,threading,usb + run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb - name: rustdoc for ESP32 - run: RUSTDOCFLAGS='-D warnings' cargo doc --target=riscv32imac-unknown-none-elf --features external-interrupts,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp + run: RUSTDOCFLAGS='-D warnings --cfg context="esp32c6"' cargo doc --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp - name: rustdoc for RP - run: RUSTDOCFLAGS='-D warnings' cargo doc --features external-interrupts,embassy-rp/rp2040 -p riot-rs-rp + run: RUSTDOCFLAGS='-D warnings --cfg context="rp2040"' cargo doc --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp - name: rustdoc for nRF - run: RUSTDOCFLAGS='-D warnings' cargo doc --features external-interrupts,embassy-nrf/nrf52840 -p riot-rs-nrf + run: RUSTDOCFLAGS='-D warnings --cfg context="nrf52840"' cargo doc --features external-interrupts,i2c,embassy-nrf/nrf52840 -p riot-rs-nrf - name: rustdoc for STM32 - run: RUSTDOCFLAGS='-D warnings' cargo doc --features external-interrupts,embassy-stm32/stm32wb55rg -p riot-rs-stm32 + run: RUSTDOCFLAGS='-D warnings --cfg context="stm32wb55rgvx"' cargo doc --features external-interrupts,i2c,embassy-stm32/stm32wb55rg -p riot-rs-stm32 - name: rustfmt run: cargo fmt --check --all diff --git a/Cargo.lock b/Cargo.lock index 5664322ff..5c02c9a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,9 +360,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.19" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" +checksum = "45bcde016d64c21da4be18b655631e5ab6d3107607e71a73a9f53eb48aae23fb" dependencies = [ "shlex", ] @@ -2065,6 +2065,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7" dependencies = [ + "defmt", "gcd", ] @@ -2440,6 +2441,19 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +[[package]] +name = "i2c-controller" +version = "0.1.0" +dependencies = [ + "embassy-executor", + "embassy-sync 0.6.0", + "embedded-hal 1.0.0", + "embedded-hal-async", + "once_cell", + "riot-rs", + "riot-rs-boards", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2822,9 +2836,9 @@ checksum = "29be4f60e41fde478b36998b88821946aafac540e53591e76db53921a0cc225b" [[package]] name = "minijinja" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7d3e3a3eece1fa4618237ad41e1de855ced47eab705cec1c9a920e1d1c5aad" +checksum = "333d5c65ea267f8aa0516c4ddeeb6547a53bd2d003659307eda21ceff9d19dbf" dependencies = [ "serde", ] @@ -3682,7 +3696,9 @@ name = "riot-rs-embassy" version = "0.1.0" dependencies = [ "cfg-if", + "const_panic", "critical-section", + "embassy-embedded-hal", "embassy-executor", "embassy-hal-internal", "embassy-net", @@ -3712,7 +3728,12 @@ dependencies = [ name = "riot-rs-embassy-common" version = "0.1.0" dependencies = [ + "defmt", + "embassy-futures", + "embassy-time", "embedded-hal 1.0.0", + "embedded-hal-async", + "fugit", ] [[package]] @@ -3720,12 +3741,17 @@ name = "riot-rs-esp" version = "0.1.0" dependencies = [ "cfg-if", + "defmt", "embassy-executor", "embassy-time", + "embedded-hal 1.0.0", + "embedded-hal-async", "esp-hal", "esp-hal-embassy", "esp-wifi", + "fugit", "once_cell", + "paste", "riot-rs-debug", "riot-rs-embassy-common", "riot-rs-utils", @@ -3751,8 +3777,11 @@ name = "riot-rs-nrf" version = "0.1.0" dependencies = [ "cfg-if", + "defmt", "embassy-executor", "embassy-nrf", + "embedded-hal-async", + "paste", "portable-atomic", "riot-rs-debug", "riot-rs-embassy-common", @@ -3773,11 +3802,15 @@ dependencies = [ name = "riot-rs-rp" version = "0.1.0" dependencies = [ + "cfg-if", "cyw43", "cyw43-pio", + "defmt", "embassy-executor", "embassy-net-driver-channel", "embassy-rp", + "embedded-hal-async", + "paste", "riot-rs-debug", "riot-rs-embassy-common", "riot-rs-random", @@ -3815,8 +3848,12 @@ name = "riot-rs-stm32" version = "0.1.0" dependencies = [ "cfg-if", + "defmt", + "embassy-embedded-hal", "embassy-executor", "embassy-stm32", + "embedded-hal-async", + "paste", "portable-atomic", "riot-rs-embassy-common", "riot-rs-random", @@ -4536,9 +4573,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap 2.5.0", "serde", diff --git a/Cargo.toml b/Cargo.toml index f550486b9..a514da890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "tests/gpio", "tests/gpio-interrupt-nrf", "tests/gpio-interrupt-stm32", + "tests/i2c-controller", "tests/threading-lock", ] @@ -52,6 +53,7 @@ portable-atomic = { version = "1.7.0", default-features = false, features = [ "require-cas", ] } +embassy-embedded-hal = { version = "0.2.0", default-features = false } embassy-executor = { version = "0.6", default-features = false } embassy-futures = { version = "0.1.1", default-features = false } embassy-hal-internal = { version = "0.2.0", default-features = false } @@ -89,6 +91,7 @@ riot-rs-utils = { path = "src/riot-rs-utils", default-features = false } const_panic = { version = "0.2.8", default-features = false } defmt = { version = "0.3.7" } document-features = "0.2.8" +fugit = { version = "0.3.7", default-features = false } heapless = { version = "0.8.0", default-features = false } konst = { version = "0.3.8", default-features = false } ld-memory = { version = "0.2.9" } diff --git a/book/src/support_matrix.html b/book/src/support_matrix.html index 2b2cbf2b9..4b0c9df84 100644 --- a/book/src/support_matrix.html +++ b/book/src/support_matrix.html @@ -4,13 +4,14 @@ Chip Testing Board - Functionality + Functionality GPIO Debug Output + I2C Controller Mode Logging User USB Wi-Fi @@ -25,6 +26,7 @@ ✅ ✅ ✅ + ✅ ❌ ☑️ ❌ @@ -37,6 +39,7 @@ ✅ ✅ ✅ + ✅ – ✅ ✅ @@ -48,6 +51,7 @@ ✅ ✅ ✅ + ✅ – ✅ ✅ @@ -59,6 +63,7 @@ ✅ ✅ ✅ + ✅ – ✅ ✅ @@ -73,12 +78,14 @@ ✅ ✅ ✅ + ✅ STM32F401RETX ST NUCLEO-F401RE ✅ ✅ + ❌ ✅ – – @@ -92,6 +99,7 @@ ✅ ✅ ✅ + ✅ – ❌ ✅ @@ -103,6 +111,7 @@ ✅ ✅ ✅ + ✅ – ✅ ✅ diff --git a/doc/support_matrix.yml b/doc/support_matrix.yml index c6acc641e..a21da6a12 100644 --- a/doc/support_matrix.yml +++ b/doc/support_matrix.yml @@ -23,6 +23,9 @@ functionalities: - name: debug_output title: Debug Output description: + - name: i2c_controller + title: I2C Controller Mode + description: I2C in controller mode - name: logging title: Logging description: @@ -47,6 +50,7 @@ chips: gpio: supported debug_output: supported hwrng: supported + i2c_controller: supported logging: supported wifi: not_available @@ -56,6 +60,7 @@ chips: gpio: supported debug_output: supported hwrng: supported + i2c_controller: supported logging: supported wifi: not_available @@ -65,6 +70,7 @@ chips: gpio: supported debug_output: supported hwrng: supported + i2c_controller: supported logging: supported wifi: not_available @@ -74,6 +80,7 @@ chips: gpio: supported debug_output: supported hwrng: not_currently_supported + i2c_controller: supported logging: supported wifi: not_available @@ -83,6 +90,7 @@ chips: gpio: supported debug_output: supported hwrng: not_available + i2c_controller: not_currently_supported logging: supported wifi: not_available @@ -92,6 +100,7 @@ chips: gpio: supported debug_output: supported hwrng: supported + i2c_controller: supported logging: supported wifi: not_available @@ -101,6 +110,7 @@ chips: gpio: supported debug_output: supported hwrng: supported + i2c_controller: supported logging: supported wifi: not_available diff --git a/src/riot-rs-embassy-common/Cargo.toml b/src/riot-rs-embassy-common/Cargo.toml index c1422f7f4..7058ee863 100644 --- a/src/riot-rs-embassy-common/Cargo.toml +++ b/src/riot-rs-embassy-common/Cargo.toml @@ -10,8 +10,18 @@ repository.workspace = true workspace = true [dependencies] +defmt = { workspace = true, optional = true } +fugit = { workspace = true, optional = true } +embassy-futures = { workspace = true } +embassy-time = { workspace = true } embedded-hal = { workspace = true } +embedded-hal-async = { workspace = true } [features] ## Enables GPIO interrupt support. external-interrupts = [] + +## Enables I2C support. +i2c = ["dep:fugit"] + +defmt = ["dep:defmt", "fugit?/defmt"] diff --git a/src/riot-rs-embassy-common/src/i2c/controller/mod.rs b/src/riot-rs-embassy-common/src/i2c/controller/mod.rs new file mode 100644 index 000000000..a200db8f1 --- /dev/null +++ b/src/riot-rs-embassy-common/src/i2c/controller/mod.rs @@ -0,0 +1,187 @@ +//! Provides architecture-agnostic I2C-related types, for controller mode. + +use embassy_time::Duration; + +pub use embedded_hal::i2c::Operation; +pub use fugit::KilohertzU32 as Kilohertz; + +/// Timeout value for I2C operations. +/// +/// Architectures are allowed to timeout earlier. +pub const I2C_TIMEOUT: Duration = Duration::from_millis(100); + +/// I2C bus frequency. +// FIXME: rename this to Bitrate, and use kbit/s instead? +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Frequency { + /// Standard mode: 100 kHz. + _100k, + /// Fast mode: 400 kHz. + _400k, +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_i2c_from_frequency { + () => { + impl From for Frequency { + fn from(freq: riot_rs_embassy_common::i2c::controller::Frequency) -> Self { + match freq { + riot_rs_embassy_common::i2c::controller::Frequency::_100k => Frequency::_100k, + riot_rs_embassy_common::i2c::controller::Frequency::_400k => Frequency::_400k, + } + } + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_i2c_from_frequency_up_to { + () => { + impl From for Frequency { + fn from(freq: riot_rs_embassy_common::i2c::controller::Frequency) -> Self { + match freq { + riot_rs_embassy_common::i2c::controller::Frequency::_100k => { + Frequency::UpTo100k($crate::i2c::controller::Kilohertz::kHz(100)) + } + riot_rs_embassy_common::i2c::controller::Frequency::_400k => { + Frequency::UpTo400k($crate::i2c::controller::Kilohertz::kHz(400)) + } + } + } + } + }; +} + +/// An I2C error, for controller mode. +// FIXME: make this non_exhaustive? +// NOTE(eq): not deriving `Eq` here because it *could* semantically contain floats later. +#[derive(Debug, Clone, PartialEq)] +pub enum Error { + /// A protocol error occurred (e.g., the transaction was terminated earlier than expected). + Bus, + /// Bus arbitration was lost (e.g., because there are multiple controllers on the bus). + ArbitrationLoss, + /// No acknowledgement was received when expected. + NoAcknowledge(NoAcknowledgeSource), + /// Overrun of the receive buffer. + Overrun, + /// Timeout when attempting to use the bus; most likely the target device is not connected. + Timeout, + /// An other error occurred. + Other, +} + +impl embedded_hal::i2c::Error for Error { + fn kind(&self) -> embedded_hal::i2c::ErrorKind { + #[expect(clippy::enum_glob_use, reason = "local import only")] + use embedded_hal::i2c::ErrorKind::*; + + match self { + Self::Bus => Bus, + Self::ArbitrationLoss => ArbitrationLoss, + Self::NoAcknowledge(ack_source) => NoAcknowledge((*ack_source).into()), + Self::Overrun => Overrun, + Self::Timeout | Self::Other => Other, + } + } +} + +/// Indicates what protocol step was not acknowledged by the target device. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NoAcknowledgeSource { + /// The device did not acknowledge its address. + Address, + /// The device did not acknowledge the data. + Data, + /// The device did not acknowledge either its address or its data. + Unknown, +} + +impl From for embedded_hal::i2c::NoAcknowledgeSource { + fn from(src: NoAcknowledgeSource) -> Self { + match src { + NoAcknowledgeSource::Address => embedded_hal::i2c::NoAcknowledgeSource::Address, + NoAcknowledgeSource::Data => embedded_hal::i2c::NoAcknowledgeSource::Data, + NoAcknowledgeSource::Unknown => embedded_hal::i2c::NoAcknowledgeSource::Unknown, + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_async_i2c_for_driver_enum { + ($driver_enum:ident, $( $peripheral:ident ),*) => { + impl $crate::reexports::embedded_hal_async::i2c::I2c for $driver_enum { + async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> { + match self { + $( + Self::$peripheral(i2c) => { + $crate::handle_i2c_timeout_res!(i2c, read, address, read) + } + )* + } + } + + async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> { + match self { + $( + Self::$peripheral(i2c) => { + $crate::handle_i2c_timeout_res!(i2c, write, address, write) + } + )* + } + } + + async fn write_read( + &mut self, + address: u8, + write: &[u8], + read: &mut [u8], + ) -> Result<(), Self::Error> { + match self { + $( + Self::$peripheral(i2c) => { + $crate::handle_i2c_timeout_res!(i2c, write_read, address, write, read) + } + )* + } + } + + async fn transaction( + &mut self, + address: u8, + operations: &mut [$crate::i2c::controller::Operation<'_>], + ) -> Result<(), Self::Error> { + match self { + $( + Self::$peripheral(i2c) => { + $crate::handle_i2c_timeout_res!(i2c, transaction, address, operations) + } + )* + } + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! handle_i2c_timeout_res { + ($i2c:ident, $op:ident, $address:ident, $( $param:ident ),+) => {{ + let res = $crate::reexports::embassy_futures::select::select( + // Disambiguate between the trait methods and the direct methods. + $crate::reexports::embedded_hal_async::i2c::I2c::$op(&mut $i2c.twim, $address, $( $param ),+), + $crate::reexports::embassy_time::Timer::after($crate::i2c::controller::I2C_TIMEOUT), + ).await; + + if let $crate::reexports::embassy_futures::select::Either::First(op) = res { + // `from_error` is defined in each arch + op.map_err(from_error) + } else { + Err($crate::i2c::controller::Error::NoAcknowledge($crate::i2c::controller::NoAcknowledgeSource::Unknown)) + } + }} +} diff --git a/src/riot-rs-embassy-common/src/i2c/mod.rs b/src/riot-rs-embassy-common/src/i2c/mod.rs new file mode 100644 index 000000000..021f806e0 --- /dev/null +++ b/src/riot-rs-embassy-common/src/i2c/mod.rs @@ -0,0 +1,4 @@ +//! Provides architecture-agnostic I2C-related types. + +#[doc(alias = "master")] +pub mod controller; diff --git a/src/riot-rs-embassy-common/src/lib.rs b/src/riot-rs-embassy-common/src/lib.rs index 88f370951..41002b5f4 100644 --- a/src/riot-rs-embassy-common/src/lib.rs +++ b/src/riot-rs-embassy-common/src/lib.rs @@ -9,3 +9,15 @@ pub mod gpio; #[cfg(context = "cortex-m")] pub mod executor_swi; + +#[cfg(feature = "i2c")] +pub mod i2c; + +pub mod reexports { + //! Crate re-exports. + + // Used by macros provided by this crate. + pub use embassy_futures; + pub use embassy_time; + pub use embedded_hal_async; +} diff --git a/src/riot-rs-embassy/Cargo.toml b/src/riot-rs-embassy/Cargo.toml index 777b9f2df..b515bf02d 100644 --- a/src/riot-rs-embassy/Cargo.toml +++ b/src/riot-rs-embassy/Cargo.toml @@ -8,13 +8,14 @@ edition = "2021" workspace = true [dependencies] +const_panic.workspace = true critical-section.workspace = true linkme.workspace = true static_cell.workspace = true cfg-if.workspace = true +embassy-embedded-hal = { workspace = true, optional = true } embassy-executor = { workspace = true, features = ["nightly"] } - embassy-hal-internal = { workspace = true } embassy-net = { workspace = true, optional = true, features = [ "dhcpv4", @@ -67,6 +68,16 @@ external-interrupts = [ "riot-rs-stm32/external-interrupts", ] time = ["dep:embassy-time", "embassy-executor/integrated-timers"] + +## Enables I2C support. +i2c = [ + "dep:embassy-embedded-hal", + "riot-rs-embassy-common/i2c", + "riot-rs-esp/i2c", + "riot-rs-nrf/i2c", + "riot-rs-rp/i2c", + "riot-rs-stm32/i2c", +] usb = [ "dep:embassy-usb", "riot-rs-nrf/usb", @@ -103,6 +114,7 @@ defmt = [ "embassy-net?/defmt", "embassy-time?/defmt", "embassy-usb?/defmt", + "riot-rs-embassy-common/defmt", "riot-rs-esp/defmt", "riot-rs-nrf/defmt", "riot-rs-rp/defmt", diff --git a/src/riot-rs-embassy/src/arch/i2c/controller.rs b/src/riot-rs-embassy/src/arch/i2c/controller.rs new file mode 100644 index 000000000..5df78b747 --- /dev/null +++ b/src/riot-rs-embassy/src/arch/i2c/controller.rs @@ -0,0 +1,63 @@ +//! Architecture- and MCU-specific types for I2C. +//! +//! This module provides a driver for each I2C peripheral, the driver name being the same as the +//! peripheral; see the tests and examples to learn how to instantiate them. +//! These driver instances are meant to be shared between tasks using +//! [`I2cDevice`](crate::i2c::controller::I2cDevice). + +/// Peripheral-agnostic I2C driver implementing [`embedded_hal_async::i2c::I2c`]. +/// +/// This type is not meant to be instantiated directly; instead instantiate a peripheral-specific +/// driver provided by this module. +// NOTE: we keep this type public because it may still required in user-written type signatures. +pub enum I2c { + // Make the docs show that this enum has variants, but do not show any because they are + // MCU-specific. + #[doc(hidden)] + Hidden, +} + +/// MCU-specific I2C bus frequency. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Frequency { + /// Standard mode. + _100k, + /// Fast mode. + _400k, + #[doc(hidden)] + Hidden, +} + +impl Frequency { + pub const fn first() -> Self { + Self::_100k + } + + pub const fn last() -> Self { + Self::_400k + } + + pub const fn next(self) -> Option { + match self { + Self::_100k => Some(Self::_400k), + Self::_400k => None, + Self::Hidden => unreachable!(), + } + } + + pub const fn prev(self) -> Option { + match self { + Self::_100k => None, + Self::_400k => Some(Self::_100k), + Self::Hidden => unreachable!(), + } + } + + pub const fn khz(self) -> u32 { + match self { + Self::_100k => 100, + Self::_400k => 400, + Self::Hidden => unreachable!(), + } + } +} diff --git a/src/riot-rs-embassy/src/arch/i2c/mod.rs b/src/riot-rs-embassy/src/arch/i2c/mod.rs new file mode 100644 index 000000000..82163327f --- /dev/null +++ b/src/riot-rs-embassy/src/arch/i2c/mod.rs @@ -0,0 +1,8 @@ +#[doc(alias = "master")] +pub mod controller; + +use crate::arch; + +pub fn init(_peripherals: &mut arch::OptionalPeripherals) { + unimplemented!(); +} diff --git a/src/riot-rs-embassy/src/arch/mod.rs b/src/riot-rs-embassy/src/arch/mod.rs index aa4ae30b2..32f55fa52 100644 --- a/src/riot-rs-embassy/src/arch/mod.rs +++ b/src/riot-rs-embassy/src/arch/mod.rs @@ -12,6 +12,9 @@ pub mod peripheral { #[cfg(feature = "hwrng")] pub mod hwrng; +#[cfg(feature = "i2c")] +pub mod i2c; + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-embassy/src/i2c/controller/mod.rs b/src/riot-rs-embassy/src/i2c/controller/mod.rs new file mode 100644 index 000000000..00e2fd0b6 --- /dev/null +++ b/src/riot-rs-embassy/src/i2c/controller/mod.rs @@ -0,0 +1,129 @@ +//! Provides support for the I2C communication bus in controller mode. + +use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice as InnerI2cDevice; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; + +use crate::arch; + +pub use riot_rs_embassy_common::i2c::controller::*; + +/// An I2C driver implementing [`embedded_hal_async::i2c::I2c`]. +/// +/// It needs to be provided with an MCU-specific I2C driver tied to a specific I2C peripheral, +/// obtained as [`arch::i2c::controller::I2c`]. +/// +/// See [`embedded_hal::i2c`] to learn more about how to share the bus. +/// +/// # Note +/// +/// Despite the driver interface being `async`, it may block during operations. +/// However, it cannot block indefinitely as a timeout is implemented, either by leveraging +/// I2C-specific hardware capabilities or through a generic software timeout. +// TODO: do we actually need a CriticalSectionRawMutex here? +pub type I2cDevice = InnerI2cDevice<'static, CriticalSectionRawMutex, arch::i2c::controller::I2c>; + +/// Returns the highest I2C frequency available on the architecture that fits into the requested +/// range. +/// +/// # Examples +/// +/// Assuming the architecture is only able to do 100 kHz and 400 kHz (not 250 kHz): +/// +/// ``` +/// # use riot_rs_embassy::{arch, i2c::controller::{highest_freq_in, Kilohertz}}; +/// let freq = const { highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::kHz(250)) }; +/// assert_eq!(freq, arch::i2c::controller::Frequency::_100k); +/// ``` +/// +/// # Panics +/// +/// This function is only intended to be used in a `const` context. +/// It panics if no suitable frequency can be found. +pub const fn highest_freq_in( + range: core::ops::RangeInclusive, +) -> arch::i2c::controller::Frequency { + let min = range.start().to_kHz(); + let max = range.end().to_kHz(); + + assert!(max >= min); + + let mut freq = arch::i2c::controller::Frequency::first(); + + loop { + // If not yet in the requested range + if freq.khz() < min { + if let Some(next) = freq.next() { + freq = next; + } else { + const_panic::concat_panic!( + "could not find a suitable I2C frequency: ", + min, + " kHz (minimum requested)", + " > ", + freq.khz(), + " kHz (highest available)" + ); + } + } else { + break; + } + } + + loop { + // If already outside of the requested range + if freq.khz() > max { + const_panic::concat_panic!( + "could not find a suitable I2C frequency: ", + max, + " kHz (maximum requested) < ", + freq.khz(), + " kHz (lowest available)" + ); + } else if let Some(next) = freq.next() { + // The upper bound is inclusive. + if next.khz() <= max { + freq = next; + } else { + break; + } + } else { + break; + } + } + + return freq; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_highest_freq_in() { + use arch::i2c::controller::Frequency; + use riot_rs_embassy_common::i2c::controller::Kilohertz; + + const FREQ_0: Frequency = highest_freq_in(Kilohertz::kHz(50)..=Kilohertz::kHz(150)); + const FREQ_1: Frequency = highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::kHz(100)); + const FREQ_2: Frequency = highest_freq_in(Kilohertz::kHz(50)..=Kilohertz::kHz(100)); + const FREQ_3: Frequency = highest_freq_in(Kilohertz::kHz(50)..=Kilohertz::kHz(400)); + const FREQ_4: Frequency = highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::kHz(400)); + const FREQ_5: Frequency = highest_freq_in(Kilohertz::kHz(300)..=Kilohertz::kHz(400)); + const FREQ_6: Frequency = highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::kHz(450)); + const FREQ_7: Frequency = highest_freq_in(Kilohertz::kHz(300)..=Kilohertz::kHz(450)); + + // The only available values in the dummy arch are 100k and 400k. + assert_eq!(FREQ_0, Frequency::_100k); + assert_eq!(FREQ_1, Frequency::_100k); + assert_eq!(FREQ_2, Frequency::_100k); + assert_eq!(FREQ_3, Frequency::_400k); + assert_eq!(FREQ_4, Frequency::_400k); + assert_eq!(FREQ_5, Frequency::_400k); + assert_eq!(FREQ_6, Frequency::_400k); + assert_eq!(FREQ_7, Frequency::_400k); + + // FIXME: add another test to check when max < min + // and with + // const FREQ_0: Frequency = highest_freq_in(Kilohertz::kHz(50)..=Kilohertz::kHz(80)); + } +} diff --git a/src/riot-rs-embassy/src/i2c/mod.rs b/src/riot-rs-embassy/src/i2c/mod.rs new file mode 100644 index 000000000..ab1b1800b --- /dev/null +++ b/src/riot-rs-embassy/src/i2c/mod.rs @@ -0,0 +1,5 @@ +//! Provides support for the I2C communication bus. +#![deny(missing_docs)] + +#[doc(alias = "master")] +pub mod controller; diff --git a/src/riot-rs-embassy/src/lib.rs b/src/riot-rs-embassy/src/lib.rs index 0a79580e1..a7b72c6cb 100644 --- a/src/riot-rs-embassy/src/lib.rs +++ b/src/riot-rs-embassy/src/lib.rs @@ -24,6 +24,9 @@ cfg_if::cfg_if! { } } +#[cfg(feature = "i2c")] +pub mod i2c; + #[cfg(feature = "usb")] pub mod usb; @@ -41,6 +44,9 @@ pub use static_cell::{ConstStaticCell, StaticCell}; // All items of this module are re-exported at the root of `riot_rs`. pub mod api { + #[cfg(feature = "i2c")] + pub use crate::i2c; + #[cfg(feature = "threading")] pub use crate::blocker; #[cfg(feature = "usb")] @@ -170,6 +176,9 @@ async fn init_task(mut peripherals: arch::OptionalPeripherals) { #[cfg(context = "esp")] arch::gpio::init(&mut peripherals); + #[cfg(feature = "i2c")] + arch::i2c::init(&mut peripherals); + #[cfg(feature = "hwrng")] arch::hwrng::construct_rng(&mut peripherals); // Clock startup and entropy collection may lend themselves to parallelization, provided that diff --git a/src/riot-rs-esp/Cargo.toml b/src/riot-rs-esp/Cargo.toml index 1482e78a9..64ab6c5a4 100644 --- a/src/riot-rs-esp/Cargo.toml +++ b/src/riot-rs-esp/Cargo.toml @@ -11,8 +11,11 @@ workspace = true [dependencies] cfg-if = { workspace = true } +defmt = { workspace = true, optional = true } embassy-executor = { workspace = true, default-features = false } embassy-time = { workspace = true, optional = true } +embedded-hal = { workspace = true } +embedded-hal-async = { workspace = true } esp-hal = { workspace = true, default-features = false, features = [ "async", "embedded-hal", @@ -23,7 +26,9 @@ esp-wifi = { workspace = true, default-features = false, features = [ "embassy-net", "wifi", ], optional = true } +fugit = { workspace = true, optional = true } once_cell = { workspace = true } +paste = { workspace = true } riot-rs-debug = { workspace = true } riot-rs-embassy-common = { workspace = true } riot-rs-utils = { workspace = true } @@ -55,8 +60,15 @@ esp-wifi = { workspace = true, default-features = false, features = [ ## Enables GPIO interrupt support. external-interrupts = ["riot-rs-embassy-common/external-interrupts"] +## Enables I2C support. +i2c = [ + "dep:fugit", + "riot-rs-embassy-common/i2c", + "embassy-executor/integrated-timers", +] + ## Enables defmt support. -defmt = ["esp-wifi?/defmt"] +defmt = ["dep:defmt", "esp-wifi?/defmt", "fugit?/defmt"] ## Enables Wi-Fi support. wifi = [] diff --git a/src/riot-rs-esp/src/i2c/controller/mod.rs b/src/riot-rs-esp/src/i2c/controller/mod.rs new file mode 100644 index 000000000..b66187359 --- /dev/null +++ b/src/riot-rs-esp/src/i2c/controller/mod.rs @@ -0,0 +1,158 @@ +use esp_hal::{ + gpio::{InputPin, OutputPin}, + i2c::I2C, + peripheral::Peripheral, + peripherals, Async, +}; +use riot_rs_embassy_common::impl_async_i2c_for_driver_enum; + +/// I2C bus configuration. +#[non_exhaustive] +#[derive(Clone)] +pub struct Config { + pub frequency: Frequency, +} + +impl Default for Config { + fn default() -> Self { + Self { + frequency: Frequency::_100k, + } + } +} + +/// I2C bus frequency. +// NOTE(arch): the technical references only mention these frequencies. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Frequency { + /// Standard mode. + _100k, + /// Fast mode. + _400k, +} + +impl Frequency { + pub const fn first() -> Self { + Self::_100k + } + + pub const fn last() -> Self { + Self::_400k + } + + pub const fn next(self) -> Option { + match self { + Self::_100k => Some(Self::_400k), + Self::_400k => None, + } + } + + pub const fn prev(self) -> Option { + match self { + Self::_100k => None, + Self::_400k => Some(Self::_100k), + } + } + + pub const fn khz(self) -> u32 { + match self { + Self::_100k => 100, + Self::_400k => 400, + } + } +} + +riot_rs_embassy_common::impl_i2c_from_frequency!(); + +impl From for fugit::HertzU32 { + fn from(freq: Frequency) -> Self { + match freq { + Frequency::_100k => fugit::Rate::::kHz(100), + Frequency::_400k => fugit::Rate::::kHz(400), + } + } +} + +macro_rules! define_i2c_drivers { + ($( $peripheral:ident ),* $(,)?) => { + $( + /// Peripheral-specific I2C driver. + pub struct $peripheral { + twim: I2C<'static, peripherals::$peripheral, Async>, + } + + impl $peripheral { + #[expect(clippy::new_ret_no_self)] + #[must_use] + pub fn new( + sda_pin: impl Peripheral

+ 'static, + scl_pin: impl Peripheral

+ 'static, + config: Config, + ) -> I2c { + let frequency = config.frequency.into(); + let clocks = crate::CLOCKS.get().unwrap(); + + // Make this struct a compile-time-enforced singleton: having multiple statics + // defined with the same name would result in a compile-time error. + paste::paste! { + #[allow(dead_code)] + static []: () = (); + } + + // FIXME(safety): enforce that the init code indeed has run + // SAFETY: this struct being a singleton prevents us from stealing the + // peripheral multiple times. + let i2c_peripheral = unsafe { peripherals::$peripheral::steal() }; + + // NOTE(arch): even though we handle bus timeout at a higher level as well, it + // does not seem possible to disable the timeout feature on ESP; so we keep the + // default timeout instead (encoded as `None`). + let timeout = None; + let twim = I2C::new_with_timeout_async( + i2c_peripheral, + sda_pin, + scl_pin, + frequency, + &clocks, + timeout, + ); + + I2c::$peripheral(Self { twim }) + } + } + )* + + /// Peripheral-agnostic driver. + pub enum I2c { + $( $peripheral($peripheral), )* + } + + impl embedded_hal_async::i2c::ErrorType for I2c { + type Error = riot_rs_embassy_common::i2c::controller::Error; + } + + impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*); + } +} + +// We cannot impl From because both types are external to this crate. +fn from_error(err: esp_hal::i2c::Error) -> riot_rs_embassy_common::i2c::controller::Error { + use esp_hal::i2c::Error::*; + + use riot_rs_embassy_common::i2c::controller::{Error, NoAcknowledgeSource}; + + match err { + ExceedingFifo => Error::Overrun, + AckCheckFailed => Error::NoAcknowledge(NoAcknowledgeSource::Unknown), + TimeOut => Error::Timeout, + ArbitrationLost => Error::ArbitrationLoss, + ExecIncomplete => Error::Other, + CommandNrExceeded => Error::Other, + } +} + +// FIXME: support other archs +// Define a driver per peripheral +#[cfg(context = "esp32c6")] +define_i2c_drivers!(I2C0); diff --git a/src/riot-rs-esp/src/i2c/mod.rs b/src/riot-rs-esp/src/i2c/mod.rs new file mode 100644 index 000000000..3695dd8bc --- /dev/null +++ b/src/riot-rs-esp/src/i2c/mod.rs @@ -0,0 +1,13 @@ +#[doc(alias = "master")] +pub mod controller; + +pub fn init(peripherals: &mut crate::OptionalPeripherals) { + // Take all I2C peripherals and do nothing with them. + cfg_if::cfg_if! { + if #[cfg(context = "esp32c6")] { + let _ = peripherals.I2C0.take().unwrap(); + } else { + compile_error!("this ESP32 chip is not supported"); + } + } +} diff --git a/src/riot-rs-esp/src/lib.rs b/src/riot-rs-esp/src/lib.rs index 549efa4c3..0deca2be7 100644 --- a/src/riot-rs-esp/src/lib.rs +++ b/src/riot-rs-esp/src/lib.rs @@ -4,8 +4,13 @@ #![feature(trait_alias)] #![feature(type_alias_impl_trait)] +use once_cell::sync::OnceCell; + pub mod gpio; +#[cfg(feature = "i2c")] +pub mod i2c; + #[cfg(feature = "wifi")] pub mod wifi; @@ -55,13 +60,21 @@ pub mod peripherals { } } -use esp_hal::{clock::ClockControl, system::SystemControl, timer::timg::TimerGroup}; +use esp_hal::{ + clock::{ClockControl, Clocks}, + system::SystemControl, + timer::timg::TimerGroup, +}; pub use esp_hal::peripherals::OptionalPeripherals; #[cfg(feature = "executor-single-thread")] pub use esp_hal_embassy::Executor; +// NOTE(once-cell): using a `once_cell::OnceCell` here for critical-section support, just to be +// sure. +pub(crate) static CLOCKS: OnceCell = OnceCell::new(); + pub fn init() -> OptionalPeripherals { let mut peripherals = OptionalPeripherals::from(peripherals::Peripherals::take()); let system = SystemControl::new(peripherals.SYSTEM.take().unwrap()); @@ -93,5 +106,8 @@ pub fn init() -> OptionalPeripherals { let timer_group0 = TimerGroup::new(peripherals.TIMG0.take().unwrap(), &clocks); esp_hal_embassy::init(&clocks, timer_group0.timer0); + // Discard the error in (the impossible) case that it was already populated. + let _ = CLOCKS.set(clocks); + peripherals } diff --git a/src/riot-rs-nrf/Cargo.toml b/src/riot-rs-nrf/Cargo.toml index de855b2af..0070f5f45 100644 --- a/src/riot-rs-nrf/Cargo.toml +++ b/src/riot-rs-nrf/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] cfg-if = { workspace = true } +defmt = { workspace = true, optional = true } embassy-executor = { workspace = true, default-features = false, features = [ "arch-cortex-m", ] } @@ -20,6 +21,8 @@ embassy-nrf = { workspace = true, default-features = false, features = [ "unstable-pac", "rt", ] } +embedded-hal-async = { workspace = true } +paste = { workspace = true } portable-atomic = { workspace = true } riot-rs-debug = { workspace = true } riot-rs-embassy-common = { workspace = true } @@ -44,11 +47,14 @@ external-interrupts = [ ## Enables seeding the random number generator from hardware. hwrng = ["dep:riot-rs-random"] +## Enables I2C support. +i2c = ["riot-rs-embassy-common/i2c", "embassy-executor/integrated-timers"] + ## Enables USB support. usb = [] ## Enables defmt support. -defmt = ["embassy-nrf/defmt"] +defmt = ["dep:defmt", "embassy-nrf/defmt"] ## Enables the interrupt executor. executor-interrupt = ["embassy-executor/executor-interrupt"] diff --git a/src/riot-rs-nrf/src/i2c/controller/mod.rs b/src/riot-rs-nrf/src/i2c/controller/mod.rs new file mode 100644 index 000000000..18157d46a --- /dev/null +++ b/src/riot-rs-nrf/src/i2c/controller/mod.rs @@ -0,0 +1,200 @@ +use embassy_nrf::{ + bind_interrupts, + gpio::Pin as GpioPin, + peripherals, + twim::{InterruptHandler, Twim}, + Peripheral, +}; +use riot_rs_embassy_common::impl_async_i2c_for_driver_enum; + +/// I2C bus configuration. +#[non_exhaustive] +#[derive(Clone)] +pub struct Config { + pub frequency: Frequency, + pub sda_pullup: bool, + pub scl_pullup: bool, + pub sda_high_drive: bool, + pub scl_high_drive: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + frequency: Frequency::_100k, + sda_pullup: false, + scl_pullup: false, + sda_high_drive: false, + scl_high_drive: false, + } + } +} + +/// I2C bus frequency. +// NOTE(arch): the datasheets only mention these frequencies. +#[cfg(any(context = "nrf52840", context = "nrf5340"))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Frequency { + /// Standard mode. + _100k, + #[cfg(context = "nrf5340")] + _250k, + /// Fast mode. + _400k, + // FIXME(embassy): the upstream Embassy crate does not support this frequency + // #[cfg(context = "nrf5340")] + // _1M, +} + +impl Frequency { + pub const fn first() -> Self { + Self::_100k + } + + pub const fn last() -> Self { + Self::_400k + } + + pub const fn next(self) -> Option { + match self { + #[cfg(not(context = "nrf5340"))] + Self::_100k => Some(Self::_400k), + #[cfg(context = "nrf5340")] + Self::_100k => Some(Self::_250k), + #[cfg(context = "nrf5340")] + Self::_250k => Some(Self::_400k), + Self::_400k => None, + } + } + + pub const fn prev(self) -> Option { + match self { + Self::_100k => None, + #[cfg(context = "nrf5340")] + Self::_250k => Some(Self::_100k), + #[cfg(not(context = "nrf5340"))] + Self::_400k => Some(Self::_100k), + #[cfg(context = "nrf5340")] + Self::_400k => Some(Self::_250k), + } + } + + pub const fn khz(self) -> u32 { + match self { + Self::_100k => 100, + #[cfg(context = "nrf5340")] + Self::_250k => 250, + Self::_400k => 400, + } + } +} + +riot_rs_embassy_common::impl_i2c_from_frequency!(); + +impl From for embassy_nrf::twim::Frequency { + fn from(freq: Frequency) -> Self { + match freq { + Frequency::_100k => embassy_nrf::twim::Frequency::K100, + #[cfg(context = "nrf5340")] + Frequency::_250k => embassy_nrf::twim::Frequency::K250, + Frequency::_400k => embassy_nrf::twim::Frequency::K400, + } + } +} + +macro_rules! define_i2c_drivers { + ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => { + $( + /// Peripheral-specific I2C driver. + pub struct $peripheral { + twim: Twim<'static, peripherals::$peripheral>, + } + + impl $peripheral { + #[expect(clippy::new_ret_no_self)] + #[must_use] + pub fn new( + sda_pin: impl Peripheral + 'static, + scl_pin: impl Peripheral + 'static, + config: Config, + ) -> I2c { + let mut twim_config = embassy_nrf::twim::Config::default(); + twim_config.frequency = config.frequency.into(); + twim_config.sda_pullup = config.sda_pullup; + twim_config.scl_pullup = config.scl_pullup; + twim_config.sda_high_drive = config.sda_high_drive; + twim_config.scl_high_drive = config.scl_high_drive; + + bind_interrupts!( + struct Irqs { + $interrupt => InterruptHandler; + } + ); + + // Make this struct a compile-time-enforced singleton: having multiple statics + // defined with the same name would result in a compile-time error. + paste::paste! { + #[allow(dead_code)] + static []: () = (); + } + + // FIXME(safety): enforce that the init code indeed has run + // SAFETY: this struct being a singleton prevents us from stealing the + // peripheral multiple times. + let twim_peripheral = unsafe { peripherals::$peripheral::steal() }; + + // NOTE(arch): the I2C peripheral and driver do not have any built-in timeout, + // we implement it at a higher level, not in this arch-specific module. + let twim = Twim::new(twim_peripheral, Irqs, sda_pin, scl_pin, twim_config); + + I2c::$peripheral(Self { twim }) + } + } + )* + + /// Peripheral-agnostic driver. + pub enum I2c { + $( $peripheral($peripheral), )* + } + + impl embedded_hal_async::i2c::ErrorType for I2c { + type Error = riot_rs_embassy_common::i2c::controller::Error; + } + + impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*); + } +} + +// We cannot impl From because both types are external to this crate. +fn from_error(err: embassy_nrf::twim::Error) -> riot_rs_embassy_common::i2c::controller::Error { + use embassy_nrf::twim::Error::*; + + use riot_rs_embassy_common::i2c::controller::{Error, NoAcknowledgeSource}; + + match err { + TxBufferTooLong => Error::Other, + RxBufferTooLong => Error::Other, + Transmit => Error::Other, + Receive => Error::Other, + BufferNotInRAM => Error::Other, + AddressNack => Error::NoAcknowledge(NoAcknowledgeSource::Address), + DataNack => Error::NoAcknowledge(NoAcknowledgeSource::Data), + Overrun => Error::Overrun, + Timeout => Error::Timeout, + _ => Error::Other, + } +} + +// FIXME: support other nRF archs +// Define a driver per peripheral +#[cfg(context = "nrf52840")] +define_i2c_drivers!( + SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0 => TWISPI0, + SPIM1_SPIS1_TWIM1_TWIS1_SPI1_TWI1 => TWISPI1, +); +#[cfg(context = "nrf5340")] +define_i2c_drivers!( + SERIAL0 => SERIAL0, + SERIAL1 => SERIAL1, +); diff --git a/src/riot-rs-nrf/src/i2c/mod.rs b/src/riot-rs-nrf/src/i2c/mod.rs new file mode 100644 index 000000000..ef31326b1 --- /dev/null +++ b/src/riot-rs-nrf/src/i2c/mod.rs @@ -0,0 +1,17 @@ +#[doc(alias = "master")] +pub mod controller; + +pub fn init(peripherals: &mut crate::OptionalPeripherals) { + // Take all I2C peripherals and do nothing with them. + cfg_if::cfg_if! { + if #[cfg(context = "nrf52840")] { + let _ = peripherals.TWISPI0.take().unwrap(); + let _ = peripherals.TWISPI1.take().unwrap(); + } else if #[cfg(context = "nrf5340")] { + let _ = peripherals.SERIAL0.take().unwrap(); + let _ = peripherals.SERIAL1.take().unwrap(); + } else { + compile_error!("this nRF chip is not supported"); + } + } +} diff --git a/src/riot-rs-nrf/src/lib.rs b/src/riot-rs-nrf/src/lib.rs index c8d57a4a6..94a139915 100644 --- a/src/riot-rs-nrf/src/lib.rs +++ b/src/riot-rs-nrf/src/lib.rs @@ -13,6 +13,9 @@ pub mod extint_registry; #[cfg(feature = "hwrng")] pub mod hwrng; +#[cfg(feature = "i2c")] +pub mod i2c; + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-rp/Cargo.toml b/src/riot-rs-rp/Cargo.toml index dc24f8e0b..4351b3a80 100644 --- a/src/riot-rs-rp/Cargo.toml +++ b/src/riot-rs-rp/Cargo.toml @@ -10,6 +10,8 @@ repository.workspace = true workspace = true [dependencies] +cfg-if = { workspace = true } +defmt = { workspace = true, optional = true } embassy-net-driver-channel = { workspace = true, optional = true } embassy-rp = { workspace = true, default-features = false, features = [ "rt", @@ -17,6 +19,8 @@ embassy-rp = { workspace = true, default-features = false, features = [ "unstable-pac", # "unstable-traits", ] } +embedded-hal-async = { workspace = true } +paste = { workspace = true } riot-rs-debug = { workspace = true } riot-rs-embassy-common = { workspace = true } riot-rs-random = { workspace = true, optional = true } @@ -44,11 +48,14 @@ external-interrupts = ["riot-rs-embassy-common/external-interrupts"] ## Enables seeding the random number generator from hardware. hwrng = ["dep:riot-rs-random"] +## Enables I2C support. +i2c = ["riot-rs-embassy-common/i2c", "embassy-executor/integrated-timers"] + ## Enables USB support. usb = [] ## Enables defmt support. -defmt = ["embassy-rp/defmt"] +defmt = ["dep:defmt", "embassy-rp/defmt"] ## Enables Wi-Fi support. wifi = [] diff --git a/src/riot-rs-rp/src/i2c/controller/mod.rs b/src/riot-rs-rp/src/i2c/controller/mod.rs new file mode 100644 index 000000000..a314b0e6c --- /dev/null +++ b/src/riot-rs-rp/src/i2c/controller/mod.rs @@ -0,0 +1,189 @@ +use embassy_rp::{ + bind_interrupts, + i2c::{InterruptHandler, SclPin, SdaPin}, + peripherals, Peripheral, +}; +use riot_rs_embassy_common::{i2c::controller::Kilohertz, impl_async_i2c_for_driver_enum}; + +const KHZ_TO_HZ: u32 = 1000; + +/// I2C bus configuration. +// We do not provide configuration for internal pull-ups as the RP2040 datasheet mentions in +// section 4.3.1.3 that the GPIO used should have pull-ups enabled. +#[derive(Clone)] +#[non_exhaustive] +pub struct Config { + pub frequency: Frequency, +} + +impl Default for Config { + fn default() -> Self { + Self { + frequency: Frequency::UpTo100k(Kilohertz::kHz(100)), + } + } +} + +/// I2C bus frequency. +// FIXME(embassy): fast mode plus is supported by hardware but requires additional configuration +// that Embassy does not seem to currently provide. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum Frequency { + /// Standard mode. + UpTo100k(Kilohertz), // FIXME: use a ranged integer? + /// Fast mode. + UpTo400k(Kilohertz), // FIXME: use a ranged integer? +} + +impl Frequency { + pub const fn first() -> Self { + Self::UpTo100k(Kilohertz::kHz(1)) + } + + pub const fn last() -> Self { + Self::UpTo400k(Kilohertz::kHz(400)) + } + + pub const fn next(self) -> Option { + match self { + Self::UpTo100k(f) => { + if f.to_kHz() < 100 { + // NOTE(no-overflow): `f` is small enough due to if condition + Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() + 1))) + } else { + Some(Self::UpTo400k(Kilohertz::kHz(self.khz() + 1))) + } + } + Self::UpTo400k(f) => { + if f.to_kHz() < 400 { + // NOTE(no-overflow): `f` is small enough due to if condition + Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() + 1))) + } else { + None + } + } + } + } + + pub const fn prev(self) -> Option { + match self { + Self::UpTo100k(f) => { + if f.to_kHz() > 1 { + // NOTE(no-overflow): `f` is large enough due to if condition + Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() - 1))) + } else { + None + } + } + Self::UpTo400k(f) => { + if f.to_kHz() > 100 + 1 { + // NOTE(no-overflow): `f` is large enough due to if condition + Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() - 1))) + } else { + Some(Self::UpTo100k(Kilohertz::kHz(self.khz() - 1))) + } + } + } + } + + pub const fn khz(self) -> u32 { + match self { + Self::UpTo100k(f) | Self::UpTo400k(f) => f.to_kHz(), + } + } +} + +riot_rs_embassy_common::impl_i2c_from_frequency_up_to!(); + +macro_rules! define_i2c_drivers { + ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => { + $( + /// Peripheral-specific I2C driver. + pub struct $peripheral { + twim: embassy_rp::i2c::I2c<'static, peripherals::$peripheral, embassy_rp::i2c::Async>, + } + + impl $peripheral { + #[expect(clippy::new_ret_no_self)] + #[must_use] + pub fn new( + sda_pin: impl Peripheral> + 'static, + scl_pin: impl Peripheral> + 'static, + config: Config, + ) -> I2c { + let mut i2c_config = embassy_rp::i2c::Config::default(); + i2c_config.frequency = config.frequency.khz() * KHZ_TO_HZ; + + bind_interrupts!( + struct Irqs { + $interrupt => InterruptHandler; + } + ); + + // Make this struct a compile-time-enforced singleton: having multiple statics + // defined with the same name would result in a compile-time error. + paste::paste! { + #[allow(dead_code)] + static []: () = (); + } + + // FIXME(safety): enforce that the init code indeed has run + // SAFETY: this struct being a singleton prevents us from stealing the + // peripheral multiple times. + let i2c_peripheral = unsafe { peripherals::$peripheral::steal() }; + + // NOTE(arch): even though we handle bus timeout at a higher level as well, it + // does not seem possible to disable the timeout feature on RP. + let i2c = embassy_rp::i2c::I2c::new_async( + i2c_peripheral, + scl_pin, + sda_pin, + Irqs, + i2c_config, + ); + + I2c::$peripheral(Self { twim: i2c }) + } + } + )* + + /// Peripheral-agnostic driver. + pub enum I2c { + $( $peripheral($peripheral), )* + } + + impl embedded_hal_async::i2c::ErrorType for I2c { + type Error = riot_rs_embassy_common::i2c::controller::Error; + } + + impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*); + } +} + +// We cannot impl From because both types are external to this crate. +fn from_error(err: embassy_rp::i2c::Error) -> riot_rs_embassy_common::i2c::controller::Error { + use embassy_rp::i2c::{AbortReason, Error::*}; + + use riot_rs_embassy_common::i2c::controller::{Error, NoAcknowledgeSource}; + + match err { + Abort(reason) => match reason { + AbortReason::NoAcknowledge => Error::NoAcknowledge(NoAcknowledgeSource::Unknown), + AbortReason::ArbitrationLoss => Error::ArbitrationLoss, + AbortReason::TxNotEmpty(_) => Error::Other, + AbortReason::Other(_) => Error::Other, + }, + InvalidReadBufferLength => Error::Other, + InvalidWriteBufferLength => Error::Other, + AddressOutOfRange(_) => Error::Other, + AddressReserved(_) => Error::Other, + } +} + +// Define a driver per peripheral +define_i2c_drivers!( + I2C0_IRQ => I2C0, + I2C1_IRQ => I2C1, +); diff --git a/src/riot-rs-rp/src/i2c/mod.rs b/src/riot-rs-rp/src/i2c/mod.rs new file mode 100644 index 000000000..a09010cba --- /dev/null +++ b/src/riot-rs-rp/src/i2c/mod.rs @@ -0,0 +1,14 @@ +#[doc(alias = "master")] +pub mod controller; + +pub fn init(peripherals: &mut crate::OptionalPeripherals) { + // Take all I2C peripherals and do nothing with them. + cfg_if::cfg_if! { + if #[cfg(context = "rp2040")] { + let _ = peripherals.I2C0.take().unwrap(); + let _ = peripherals.I2C1.take().unwrap(); + } else { + compile_error!("this RP chip is not supported"); + } + } +} diff --git a/src/riot-rs-rp/src/lib.rs b/src/riot-rs-rp/src/lib.rs index 42a52b9e5..bb1d5b879 100644 --- a/src/riot-rs-rp/src/lib.rs +++ b/src/riot-rs-rp/src/lib.rs @@ -18,6 +18,9 @@ pub mod cyw43; #[cfg(feature = "hwrng")] pub mod hwrng; +#[cfg(feature = "i2c")] +pub mod i2c; + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-stm32/Cargo.toml b/src/riot-rs-stm32/Cargo.toml index cc7d65099..0c72132e0 100644 --- a/src/riot-rs-stm32/Cargo.toml +++ b/src/riot-rs-stm32/Cargo.toml @@ -11,10 +11,14 @@ workspace = true [dependencies] cfg-if = { workspace = true } +defmt = { workspace = true, optional = true } +embassy-embedded-hal = { workspace = true } embassy-executor = { workspace = true, default-features = false, features = [ "arch-cortex-m", ] } embassy-stm32 = { workspace = true, default-features = false } +embedded-hal-async = { workspace = true } +paste = { workspace = true } portable-atomic = { workspace = true } riot-rs-embassy-common = { workspace = true } riot-rs-random = { workspace = true, optional = true } @@ -41,6 +45,14 @@ hwrng = ["dep:riot-rs-random"] stm32-hash-rng = [] stm32-rng = [] +## Enables I2C support. +# Time-related features are required for timeout support. +i2c = [ + "riot-rs-embassy-common/i2c", + "embassy-stm32/time", + "embassy-executor/integrated-timers", +] + ## Enables USB support. usb = [] # These are chosen automatically by riot-rs-boards and select the correct stm32 @@ -49,7 +61,7 @@ stm32-usb = [] stm32-usb-synopsis = [] ## Enables defmt support. -defmt = ["embassy-stm32/defmt"] +defmt = ["dep:defmt", "embassy-stm32/defmt"] ## Enables the interrupt executor. executor-interrupt = ["embassy-executor/executor-interrupt"] diff --git a/src/riot-rs-stm32/src/i2c/controller/mod.rs b/src/riot-rs-stm32/src/i2c/controller/mod.rs new file mode 100644 index 000000000..2edc82cdb --- /dev/null +++ b/src/riot-rs-stm32/src/i2c/controller/mod.rs @@ -0,0 +1,218 @@ +use embassy_embedded_hal::adapter::{BlockingAsync, YieldingAsync}; +use embassy_stm32::{ + bind_interrupts, + i2c::{ErrorInterruptHandler, EventInterruptHandler, I2c as InnerI2c, SclPin, SdaPin}, + mode::Blocking, + peripherals, + time::Hertz, + Peripheral, +}; +use riot_rs_embassy_common::{i2c::controller::Kilohertz, impl_async_i2c_for_driver_enum}; + +/// I2C bus configuration. +#[non_exhaustive] +#[derive(Clone)] +pub struct Config { + pub frequency: Frequency, + pub sda_pullup: bool, + pub scl_pullup: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + frequency: Frequency::UpTo100k(Kilohertz::kHz(100)), + sda_pullup: false, + scl_pullup: false, + } + } +} + +/// I2C bus frequency. +// FIXME(embassy): fast mode plus is supported by hardware but requires additional configuration +// that Embassy does not seem to currently provide. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum Frequency { + /// Standard mode. + UpTo100k(Kilohertz), // FIXME: use a ranged integer? + /// Fast mode. + UpTo400k(Kilohertz), // FIXME: use a ranged integer? +} + +impl Frequency { + pub const fn first() -> Self { + Self::UpTo100k(Kilohertz::kHz(1)) + } + + pub const fn last() -> Self { + Self::UpTo400k(Kilohertz::kHz(400)) + } + + pub const fn next(self) -> Option { + match self { + Self::UpTo100k(f) => { + if f.to_kHz() < 100 { + // NOTE(no-overflow): `f` is small enough due to if condition + Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() + 1))) + } else { + Some(Self::UpTo400k(Kilohertz::kHz(self.khz() + 1))) + } + } + Self::UpTo400k(f) => { + if f.to_kHz() < 400 { + // NOTE(no-overflow): `f` is small enough due to if condition + Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() + 1))) + } else { + None + } + } + } + } + + pub const fn prev(self) -> Option { + match self { + Self::UpTo100k(f) => { + if f.to_kHz() > 1 { + // NOTE(no-overflow): `f` is large enough due to if condition + Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() - 1))) + } else { + None + } + } + Self::UpTo400k(f) => { + if f.to_kHz() > 100 + 1 { + // NOTE(no-overflow): `f` is large enough due to if condition + Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() - 1))) + } else { + Some(Self::UpTo100k(Kilohertz::kHz(self.khz() - 1))) + } + } + } + } + + pub const fn khz(self) -> u32 { + match self { + Self::UpTo100k(f) | Self::UpTo400k(f) => f.to_kHz(), + } + } +} + +riot_rs_embassy_common::impl_i2c_from_frequency_up_to!(); + +impl From for Hertz { + fn from(freq: Frequency) -> Self { + match freq { + Frequency::UpTo100k(f) | Frequency::UpTo400k(f) => Hertz::khz(f.to_kHz()), + } + } +} + +macro_rules! define_i2c_drivers { + ($( $ev_interrupt:ident + $er_interrupt:ident => $peripheral:ident ),* $(,)?) => { + $( + /// Peripheral-specific I2C driver. + // NOTE(arch): this is not required on this architecture, as the inner I2C type is + // not generic over the I2C peripheral, and is only done for consistency with + // other architectures. + pub struct $peripheral { + twim: YieldingAsync>>, + } + + impl $peripheral { + #[expect(clippy::new_ret_no_self)] + #[must_use] + pub fn new( + sda_pin: impl Peripheral> + 'static, + scl_pin: impl Peripheral> + 'static, + config: Config, + ) -> I2c { + let mut i2c_config = embassy_stm32::i2c::Config::default(); + i2c_config.sda_pullup = config.sda_pullup; + i2c_config.scl_pullup = config.scl_pullup; + i2c_config.timeout = riot_rs_embassy_common::i2c::controller::I2C_TIMEOUT; + + bind_interrupts!( + struct Irqs { + $ev_interrupt => EventInterruptHandler; + $er_interrupt => ErrorInterruptHandler; + } + ); + + // Make this struct a compile-time-enforced singleton: having multiple statics + // defined with the same name would result in a compile-time error. + paste::paste! { + #[allow(dead_code)] + static []: () = (); + } + + // FIXME(safety): enforce that the init code indeed has run + // SAFETY: this struct being a singleton prevents us from stealing the + // peripheral multiple times. + let twim_peripheral = unsafe { peripherals::$peripheral::steal() }; + + let frequency = config.frequency; + let i2c = InnerI2c::new_blocking( + twim_peripheral, + scl_pin, + sda_pin, + frequency.into(), + i2c_config, + ); + + I2c::$peripheral(Self { twim: YieldingAsync::new(BlockingAsync::new(i2c)) }) + } + } + )* + + /// Peripheral-agnostic driver. + pub enum I2c { + $( $peripheral($peripheral), )* + } + + impl embedded_hal_async::i2c::ErrorType for I2c { + type Error = riot_rs_embassy_common::i2c::controller::Error; + } + + impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*); + } +} + +// We cannot impl From because both types are external to this crate. +fn from_error(err: embassy_stm32::i2c::Error) -> riot_rs_embassy_common::i2c::controller::Error { + use embassy_stm32::i2c::Error::*; + + use riot_rs_embassy_common::i2c::controller::{Error, NoAcknowledgeSource}; + + match err { + Bus => Error::Bus, + Arbitration => Error::ArbitrationLoss, + Nack => Error::NoAcknowledge(NoAcknowledgeSource::Unknown), + Timeout => Error::Timeout, + Crc => Error::Other, + Overrun => Error::Overrun, + ZeroLengthTransfer => Error::Other, + } +} + +// Define a driver per peripheral +#[cfg(context = "stm32f401retx")] +define_i2c_drivers!( + I2C1_EV + I2C1_ER => I2C1, + I2C2_EV + I2C2_ER => I2C2, + I2C3_EV + I2C3_ER => I2C3, +); +#[cfg(context = "stm32h755zitx")] +define_i2c_drivers!( + I2C1_EV + I2C1_ER => I2C1, + I2C2_EV + I2C2_ER => I2C2, + I2C3_EV + I2C3_ER => I2C3, + I2C4_EV + I2C4_ER => I2C4, +); +#[cfg(context = "stm32wb55rgvx")] +define_i2c_drivers!( + I2C1_EV + I2C1_ER => I2C1, + // There is no I2C2 + I2C3_EV + I2C3_ER => I2C3, +); diff --git a/src/riot-rs-stm32/src/i2c/mod.rs b/src/riot-rs-stm32/src/i2c/mod.rs new file mode 100644 index 000000000..d52e28f79 --- /dev/null +++ b/src/riot-rs-stm32/src/i2c/mod.rs @@ -0,0 +1,26 @@ +#[doc(alias = "master")] +pub mod controller; + +pub fn init(peripherals: &mut crate::OptionalPeripherals) { + // This macro has to be defined in this function so that the `peripherals` variables exists. + macro_rules! take_all_i2c_peripherals { + ($peripherals:ident, $( $peripheral:ident ),*) => { + $( + let _ = peripherals.$peripheral.take().unwrap(); + )* + } + } + + // Take all I2c peripherals and do nothing with them. + cfg_if::cfg_if! { + if #[cfg(context = "stm32f401retx")] { + take_all_i2c_peripherals!(I2C1, I2C2, I2C3); + } else if #[cfg(context = "stm32h755zitx")] { + take_all_i2c_peripherals!(I2C1, I2C2, I2C3, I2C4); + } else if #[cfg(context = "stm32wb55rgvx")] { + take_all_i2c_peripherals!(I2C1, I2C3); + } else { + compile_error!("this STM32 chip is not supported"); + } + } +} diff --git a/src/riot-rs-stm32/src/lib.rs b/src/riot-rs-stm32/src/lib.rs index 7c0fd6966..207bd5596 100644 --- a/src/riot-rs-stm32/src/lib.rs +++ b/src/riot-rs-stm32/src/lib.rs @@ -11,6 +11,9 @@ pub mod peripheral { #[cfg(feature = "external-interrupts")] pub mod extint_registry; +#[cfg(feature = "i2c")] +pub mod i2c; + use embassy_stm32::Config; pub use embassy_stm32::{interrupt, peripherals, OptionalPeripherals, Peripherals}; diff --git a/src/riot-rs/Cargo.toml b/src/riot-rs/Cargo.toml index d25b02892..da6ffc527 100644 --- a/src/riot-rs/Cargo.toml +++ b/src/riot-rs/Cargo.toml @@ -44,7 +44,9 @@ csprng = ["riot-rs-random/csprng"] ## Enables seeding the random number generator from hardware. hwrng = ["riot-rs-embassy/hwrng"] -#! ## Wired communication +#! ## Serial communication +## Enables I2C support. +i2c = ["riot-rs-embassy/i2c"] ## Enables USB support. usb = ["riot-rs-embassy/usb"] diff --git a/tests/i2c-controller/Cargo.toml b/tests/i2c-controller/Cargo.toml new file mode 100644 index 000000000..73b4eee05 --- /dev/null +++ b/tests/i2c-controller/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "i2c-controller" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +embassy-executor = { workspace = true } +embassy-sync = { workspace = true } +embedded-hal = { workspace = true } +embedded-hal-async = { workspace = true } +once_cell = { workspace = true } +riot-rs = { path = "../../src/riot-rs", features = ["i2c"] } +riot-rs-boards = { path = "../../src/riot-rs-boards" } diff --git a/tests/i2c-controller/README.md b/tests/i2c-controller/README.md new file mode 100644 index 000000000..5423e58bd --- /dev/null +++ b/tests/i2c-controller/README.md @@ -0,0 +1,15 @@ +# i2c-controller + +## About + +This application is testing raw I2C bus usage in RIOT-rs. + +## How to run + +In this folder, run + + laze build -b nrf52840dk run + +This test requires an LIS3DH sensor (3-axis accelerometer) attached to the pins configured in the +`pins` module. +It attempts to read the `WHO_AM_I` register and checks the received value against the expected id. diff --git a/tests/i2c-controller/laze.yml b/tests/i2c-controller/laze.yml new file mode 100644 index 000000000..7fc0dd891 --- /dev/null +++ b/tests/i2c-controller/laze.yml @@ -0,0 +1,15 @@ +apps: + - name: i2c-controller + env: + global: + CARGO_ENV: + - CONFIG_ISR_STACKSIZE=16384 + context: + - espressif-esp32-c6-devkitc-1 + - nrf52840 + - nrf5340 + - rp2040 + - st-nucleo-h755zi-q + - st-nucleo-wb55 + selects: + - ?release diff --git a/tests/i2c-controller/src/main.rs b/tests/i2c-controller/src/main.rs new file mode 100644 index 000000000..cca71ac7a --- /dev/null +++ b/tests/i2c-controller/src/main.rs @@ -0,0 +1,61 @@ +//! This example is merely to illustrate and test raw bus usage. +//! +//! Please use [`riot_rs::sensors`] instead for a high-level sensor abstraction that is +//! architecture-agnostic. +//! +//! This example requires a LIS3DH sensor (3-axis accelerometer). +#![no_main] +#![no_std] +#![feature(type_alias_impl_trait)] +#![feature(used_with_arg)] +#![feature(impl_trait_in_assoc_type)] + +mod pins; + +use embassy_sync::mutex::Mutex; +use embedded_hal_async::i2c::I2c as _; +use riot_rs::{ + arch, + debug::{ + exit, + log::{debug, info}, + EXIT_SUCCESS, + }, + i2c::controller::{highest_freq_in, I2cDevice, Kilohertz}, +}; + +const LIS3DH_I2C_ADDR: u8 = 0x19; + +// WHO_AM_I register of the LIS3DH sensor +const WHO_AM_I_REG_ADDR: u8 = 0x0f; + +pub static I2C_BUS: once_cell::sync::OnceCell< + Mutex, +> = once_cell::sync::OnceCell::new(); + +#[riot_rs::task(autostart, peripherals)] +async fn main(peripherals: pins::Peripherals) { + let mut i2c_config = arch::i2c::controller::Config::default(); + i2c_config.frequency = const { highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::kHz(400)) }; + debug!("Selected frequency: {}", i2c_config.frequency); + + let i2c_bus = pins::SensorI2c::new(peripherals.i2c_sda, peripherals.i2c_scl, i2c_config); + + let _ = I2C_BUS.set(Mutex::new(i2c_bus)); + + let mut i2c_device = I2cDevice::new(I2C_BUS.get().unwrap()); + + let mut id = [0]; + i2c_device + .write_read(LIS3DH_I2C_ADDR, &[WHO_AM_I_REG_ADDR], &mut id) + .await + .unwrap(); + + let who_am_i = id[0]; + info!("LIS3DH WHO_AM_I_COMMAND register value: 0x{:x}", who_am_i); + assert_eq!(who_am_i, 0x33); + + info!("Test passed!"); + + exit(EXIT_SUCCESS); +} diff --git a/tests/i2c-controller/src/pins.rs b/tests/i2c-controller/src/pins.rs new file mode 100644 index 000000000..5778c216d --- /dev/null +++ b/tests/i2c-controller/src/pins.rs @@ -0,0 +1,43 @@ +use riot_rs::arch::{i2c, peripherals}; + +#[cfg(context = "esp")] +pub type SensorI2c = i2c::controller::I2C0; +#[cfg(context = "esp")] +riot_rs::define_peripherals!(Peripherals { + i2c_sda: GPIO_2, + i2c_scl: GPIO_0, +}); + +#[cfg(context = "nrf52840")] +pub type SensorI2c = i2c::controller::TWISPI0; +#[cfg(context = "nrf5340")] +pub type SensorI2c = i2c::controller::SERIAL0; +#[cfg(context = "nrf")] +riot_rs::define_peripherals!(Peripherals { + i2c_sda: P0_00, + i2c_scl: P0_01, +}); + +#[cfg(context = "rp")] +pub type SensorI2c = i2c::controller::I2C0; +#[cfg(context = "rp")] +riot_rs::define_peripherals!(Peripherals { + i2c_sda: PIN_12, + i2c_scl: PIN_13, +}); + +#[cfg(context = "stm32h755zitx")] +pub type SensorI2c = i2c::controller::I2C1; +#[cfg(context = "stm32h755zitx")] +riot_rs::define_peripherals!(Peripherals { + i2c_sda: PB9, + i2c_scl: PB8, +}); + +#[cfg(context = "stm32wb55rgvx")] +pub type SensorI2c = i2c::controller::I2C1; +#[cfg(context = "stm32wb55rgvx")] +riot_rs::define_peripherals!(Peripherals { + i2c_sda: PB9, + i2c_scl: PB8, +}); diff --git a/tests/laze.yml b/tests/laze.yml index 36505d1b3..9e73d211c 100644 --- a/tests/laze.yml +++ b/tests/laze.yml @@ -3,4 +3,5 @@ subdirs: - gpio - gpio-interrupt-nrf - gpio-interrupt-stm32 + - i2c-controller - threading-lock