Skip to main content

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