(
+ 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