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