ariel_os_rp/i2c/controller/
mod.rs

1//! Provides support for the I2C communication bus in controller mode.
2
3#![expect(unsafe_code)]
4
5use ariel_os_embassy_common::{i2c::controller::Kilohertz, impl_async_i2c_for_driver_enum};
6use embassy_rp::{
7    Peripheral, bind_interrupts,
8    i2c::{InterruptHandler, SclPin, SdaPin},
9    peripherals,
10};
11
12const KHZ_TO_HZ: u32 = 1000;
13
14/// I2C bus configuration.
15// We do not provide configuration for internal pull-ups as the RP2040 datasheet mentions in
16// section 4.3.1.3 that the GPIO used should have pull-ups enabled.
17#[derive(Clone)]
18#[non_exhaustive]
19pub struct Config {
20    /// The frequency at which the bus should operate.
21    pub frequency: Frequency,
22}
23
24impl Default for Config {
25    fn default() -> Self {
26        Self {
27            frequency: Frequency::UpTo100k(Kilohertz::kHz(100)),
28        }
29    }
30}
31
32/// I2C bus frequency.
33// FIXME(embassy): fast mode plus is supported by hardware but requires additional configuration
34// that Embassy does not seem to currently provide.
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37#[repr(u32)]
38pub enum Frequency {
39    /// Standard mode.
40    UpTo100k(Kilohertz), // FIXME: use a ranged integer?
41    /// Fast mode.
42    UpTo400k(Kilohertz), // FIXME: use a ranged integer?
43}
44
45#[doc(hidden)]
46impl Frequency {
47    #[must_use]
48    pub const fn first() -> Self {
49        Self::UpTo100k(Kilohertz::kHz(1))
50    }
51
52    #[must_use]
53    pub const fn last() -> Self {
54        Self::UpTo400k(Kilohertz::kHz(400))
55    }
56
57    #[must_use]
58    pub const fn next(self) -> Option<Self> {
59        match self {
60            Self::UpTo100k(f) => {
61                if f.to_kHz() < 100 {
62                    // NOTE(no-overflow): `f` is small enough due to if condition
63                    Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() + 1)))
64                } else {
65                    Some(Self::UpTo400k(Kilohertz::kHz(self.khz() + 1)))
66                }
67            }
68            Self::UpTo400k(f) => {
69                if f.to_kHz() < 400 {
70                    // NOTE(no-overflow): `f` is small enough due to if condition
71                    Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() + 1)))
72                } else {
73                    None
74                }
75            }
76        }
77    }
78
79    #[must_use]
80    pub const fn prev(self) -> Option<Self> {
81        match self {
82            Self::UpTo100k(f) => {
83                if f.to_kHz() > 1 {
84                    // NOTE(no-overflow): `f` is large enough due to if condition
85                    Some(Self::UpTo100k(Kilohertz::kHz(f.to_kHz() - 1)))
86                } else {
87                    None
88                }
89            }
90            Self::UpTo400k(f) => {
91                if f.to_kHz() > 100 + 1 {
92                    // NOTE(no-overflow): `f` is large enough due to if condition
93                    Some(Self::UpTo400k(Kilohertz::kHz(f.to_kHz() - 1)))
94                } else {
95                    Some(Self::UpTo100k(Kilohertz::kHz(self.khz() - 1)))
96                }
97            }
98        }
99    }
100
101    #[must_use]
102    pub const fn khz(self) -> u32 {
103        match self {
104            Self::UpTo100k(f) | Self::UpTo400k(f) => f.to_kHz(),
105        }
106    }
107}
108
109ariel_os_embassy_common::impl_i2c_from_frequency_up_to!();
110
111macro_rules! define_i2c_drivers {
112    ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => {
113        $(
114            /// Peripheral-specific I2C driver.
115            pub struct $peripheral {
116                twim: embassy_rp::i2c::I2c<'static, peripherals::$peripheral, embassy_rp::i2c::Async>,
117            }
118
119            impl $peripheral {
120                /// Returns a driver implementing [`embedded_hal_async::i2c::I2c`] for this
121                /// I2C peripheral.
122                #[expect(clippy::new_ret_no_self)]
123                #[must_use]
124                pub fn new(
125                    sda_pin: impl Peripheral<P: SdaPin<peripherals::$peripheral>> + 'static,
126                    scl_pin: impl Peripheral<P: SclPin<peripherals::$peripheral>> + 'static,
127                    config: Config,
128                ) -> I2c {
129                    // Make this struct a compile-time-enforced singleton: having multiple statics
130                    // defined with the same name would result in a compile-time error.
131                    paste::paste! {
132                        #[allow(dead_code)]
133                        static [<PREVENT_MULTIPLE_ $peripheral>]: () = ();
134                    }
135
136                    let mut i2c_config = embassy_rp::i2c::Config::default();
137                    i2c_config.frequency = config.frequency.khz() * KHZ_TO_HZ;
138
139                    bind_interrupts!(
140                        struct Irqs {
141                            $interrupt => InterruptHandler<peripherals::$peripheral>;
142                        }
143                    );
144
145                    // FIXME(safety): enforce that the init code indeed has run
146                    // SAFETY: this struct being a singleton prevents us from stealing the
147                    // peripheral multiple times.
148                    let i2c_peripheral = unsafe { peripherals::$peripheral::steal() };
149
150                    // NOTE(hal): even though we handle bus timeout at a higher level as well, it
151                    // does not seem possible to disable the timeout feature on RP.
152                    let i2c = embassy_rp::i2c::I2c::new_async(
153                        i2c_peripheral,
154                        scl_pin,
155                        sda_pin,
156                        Irqs,
157                        i2c_config,
158                    );
159
160                    I2c::$peripheral(Self { twim: i2c })
161                }
162            }
163        )*
164
165        /// Peripheral-agnostic driver.
166        pub enum I2c {
167            $(
168                #[doc = concat!(stringify!($peripheral), " peripheral.")]
169                $peripheral($peripheral),
170            )*
171        }
172
173        impl embedded_hal_async::i2c::ErrorType for I2c {
174            type Error = ariel_os_embassy_common::i2c::controller::Error;
175        }
176
177        impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*);
178    }
179}
180
181// We cannot impl From because both types are external to this crate.
182fn from_error(err: embassy_rp::i2c::Error) -> ariel_os_embassy_common::i2c::controller::Error {
183    #[allow(deprecated)]
184    use embassy_rp::i2c::{
185        AbortReason,
186        Error::{
187            Abort, AddressOutOfRange, AddressReserved, InvalidReadBufferLength,
188            InvalidWriteBufferLength,
189        },
190    };
191
192    use ariel_os_embassy_common::i2c::controller::{Error, NoAcknowledgeSource};
193
194    match err {
195        Abort(reason) => match reason {
196            AbortReason::NoAcknowledge => Error::NoAcknowledge(NoAcknowledgeSource::Unknown),
197            AbortReason::ArbitrationLoss => Error::ArbitrationLoss,
198            AbortReason::TxNotEmpty(_) | AbortReason::Other(_) => Error::Other,
199        },
200        #[allow(deprecated)]
201        AddressReserved(_)
202        | InvalidWriteBufferLength
203        | AddressOutOfRange(_)
204        | InvalidReadBufferLength => Error::Other,
205    }
206}
207
208// Define a driver per peripheral
209define_i2c_drivers!(
210    I2C0_IRQ => I2C0,
211    I2C1_IRQ => I2C1,
212);