Skip to main content

ariel_os_nrf/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::impl_async_i2c_for_driver_enum;
8
9use embassy_nrf::{
10    bind_interrupts,
11    gpio::Pin as GpioPin,
12    peripherals,
13    twim::{InterruptHandler, Twim},
14};
15
16use static_cell::ConstStaticCell;
17
18/// I2C bus configuration.
19#[non_exhaustive]
20#[expect(clippy::struct_excessive_bools)]
21#[derive(Clone)]
22pub struct Config {
23    /// The frequency at which the bus should operate.
24    pub frequency: Frequency,
25    /// Whether to enable the internal pull-up resistor on the SDA pin.
26    pub sda_pullup: bool,
27    /// Whether to enable the internal pull-up resistor on the SCL pin.
28    pub scl_pullup: bool,
29    /// Whether to set the SDA pin's drive strength to
30    /// [`DriveStrength::High`](crate::gpio::DriveStrength::High).
31    pub sda_high_drive: bool,
32    /// Whether to set the SCL pin's drive strength to
33    /// [`DriveStrength::High`](crate::gpio::DriveStrength::High).
34    pub scl_high_drive: bool,
35}
36
37impl Default for Config {
38    fn default() -> Self {
39        Self {
40            frequency: Frequency::_100k,
41            sda_pullup: false,
42            scl_pullup: false,
43            sda_high_drive: false,
44            scl_high_drive: false,
45        }
46    }
47}
48
49/// I2C bus frequency.
50// NOTE(hal): the datasheets only mention these frequencies.
51#[cfg(any(
52    context = "nrf52833",
53    context = "nrf52840",
54    context = "nrf5340-app",
55    context = "nrf91"
56))]
57#[derive(Debug, Copy, Clone, PartialEq, Eq)]
58#[cfg_attr(feature = "defmt", derive(defmt::Format))]
59pub enum Frequency {
60    /// Standard mode.
61    _100k,
62    /// 250 kHz.
63    #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
64    _250k,
65    /// Fast mode.
66    _400k,
67    // FIXME(embassy): the upstream Embassy crate does not support this frequency
68    // #[cfg(context = "nrf5340-app", context = "nrf91")]
69    // _1M,
70}
71
72#[doc(hidden)]
73impl Frequency {
74    #[must_use]
75    pub const fn first() -> Self {
76        Self::_100k
77    }
78
79    #[must_use]
80    pub const fn last() -> Self {
81        Self::_400k
82    }
83
84    #[must_use]
85    pub const fn next(self) -> Option<Self> {
86        match self {
87            #[cfg(context = "nrf52840")]
88            Self::_100k => Some(Self::_400k),
89            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
90            Self::_100k => Some(Self::_250k),
91            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
92            Self::_250k => Some(Self::_400k),
93            Self::_400k => None,
94        }
95    }
96
97    #[must_use]
98    pub const fn prev(self) -> Option<Self> {
99        match self {
100            Self::_100k => None,
101            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
102            Self::_250k => Some(Self::_100k),
103            #[cfg(context = "nrf52840")]
104            Self::_400k => Some(Self::_100k),
105            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
106            Self::_400k => Some(Self::_250k),
107        }
108    }
109
110    #[must_use]
111    pub const fn khz(self) -> u32 {
112        match self {
113            Self::_100k => 100,
114            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
115            Self::_250k => 250,
116            Self::_400k => 400,
117        }
118    }
119}
120
121ariel_os_embassy_common::impl_i2c_from_frequency!();
122
123impl From<Frequency> for embassy_nrf::twim::Frequency {
124    fn from(freq: Frequency) -> Self {
125        match freq {
126            Frequency::_100k => embassy_nrf::twim::Frequency::K100,
127            #[cfg(any(context = "nrf52833", context = "nrf5340-app", context = "nrf91"))]
128            Frequency::_250k => embassy_nrf::twim::Frequency::K250,
129            Frequency::_400k => embassy_nrf::twim::Frequency::K400,
130        }
131    }
132}
133
134macro_rules! define_i2c_drivers {
135    ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => {
136        $(
137            /// Peripheral-specific I2C driver.
138            pub struct $peripheral {
139                twim: Twim<'static>,
140            }
141
142            // Ensure this peripheral has only one active Instance.
143            paste::paste! {
144                static [< ACTIVE_ $peripheral >]: AtomicBool = AtomicBool::new(false);
145            }
146
147            impl $peripheral {
148                /// Returns a driver implementing [`embedded_hal_async::i2c::I2c`] for this
149                /// I2C peripheral.
150                #[expect(clippy::new_ret_no_self)]
151                #[must_use]
152                pub fn new<SDA: GpioPin, SCL: GpioPin>(
153                    sda_pin: impl $crate::IntoPeripheral<'static, SDA>,
154                    scl_pin: impl $crate::IntoPeripheral<'static, SCL>,
155                    config: Config,
156                ) -> I2c {
157                    let mut twim_config = embassy_nrf::twim::Config::default();
158                    twim_config.frequency = config.frequency.into();
159                    twim_config.sda_pullup = config.sda_pullup;
160                    twim_config.scl_pullup = config.scl_pullup;
161                    twim_config.sda_high_drive = config.sda_high_drive;
162                    twim_config.scl_high_drive = config.scl_high_drive;
163
164                    bind_interrupts!(
165                        struct Irqs {
166                            $interrupt => InterruptHandler<peripherals::$peripheral>;
167                        }
168                    );
169
170                    // Make this struct a compile-time-enforced singleton: having multiple statics
171                    // defined with the same name would result in a compile-time error.
172                    paste::paste! {
173                        #[allow(dead_code)]
174                        static [<PREVENT_MULTIPLE_ $peripheral>]: () = ();
175                    }
176
177                    // Check if we can initialize this peripheral (check if the value was previously false, set it to true).
178                    paste::paste! {
179                        if [< ACTIVE_ $peripheral >].swap(true, Ordering::AcqRel) {
180                            panic!("I2C peripheral already initialized")
181                        }
182                    }
183
184                    // FIXME(safety): enforce that the init code indeed has run
185                    // SAFETY: We check with an AtomicBool that only one instance of this peripheral
186                    // is active at once.
187                    let twim_peripheral = unsafe { peripherals::$peripheral::steal() };
188
189                    // NOTE(hal): the I2C implementation for nrf needs a buffer, define it here.
190                    // TODO: which size? make configurable
191                    paste::paste! {
192                        static [<RAM_BUFFER_ $peripheral>]: ConstStaticCell<[u8; 16]> = ConstStaticCell::new([0; 16]);
193                    }
194
195                    // NOTE(hal): the I2C peripheral and driver do not have any built-in timeout,
196                    // we implement it at a higher level, not in this HAL-specific module.
197                    paste::paste! {
198                        let twim = Twim::new(
199                            twim_peripheral,
200                            Irqs,
201                            sda_pin.into_hal_peripheral(),
202                            scl_pin.into_hal_peripheral(),
203                            twim_config,
204                            [<RAM_BUFFER_ $peripheral>].take(),
205                        );
206                    }
207
208                    I2c::$peripheral(Self { twim })
209                }
210            }
211
212            impl Drop for $peripheral {
213                fn drop(&mut self) {
214                    paste::paste! {
215                        [< ACTIVE_ $peripheral >].store(false, Ordering::Release);
216                    }
217                }
218            }
219        )*
220
221        /// Peripheral-agnostic driver.
222        pub enum I2c {
223            $(
224                #[doc = concat!(stringify!($peripheral), " peripheral.")]
225                $peripheral($peripheral),
226            )*
227        }
228
229        impl embedded_hal_async::i2c::ErrorType for I2c {
230            type Error = ariel_os_embassy_common::i2c::controller::Error;
231        }
232
233        impl_async_i2c_for_driver_enum!(I2c, $( $peripheral ),*);
234    }
235}
236
237// We cannot impl From because both types are external to this crate.
238fn from_error(err: embassy_nrf::twim::Error) -> ariel_os_embassy_common::i2c::controller::Error {
239    use embassy_nrf::twim::Error::{
240        AddressNack, DataNack, Overrun, RAMBufferTooSmall, Receive, RxBufferTooLong, Timeout,
241        Transmit, TxBufferTooLong,
242    };
243
244    use ariel_os_embassy_common::i2c::controller::{Error, NoAcknowledgeSource};
245
246    #[expect(clippy::match_same_arms, reason = "non-exhaustive upstream enum")]
247    match err {
248        TxBufferTooLong | RxBufferTooLong | Transmit | Receive | RAMBufferTooSmall => Error::Other,
249        AddressNack => Error::NoAcknowledge(NoAcknowledgeSource::Address),
250        DataNack => Error::NoAcknowledge(NoAcknowledgeSource::Data),
251        Overrun => Error::Overrun,
252        Timeout => Error::Timeout,
253        _ => Error::Other,
254    }
255}
256
257// FIXME: support other nRF archs
258// Define a driver per peripheral
259#[cfg(any(context = "nrf52833", context = "nrf52840"))]
260define_i2c_drivers!(
261    TWISPI0 => TWISPI0,
262    TWISPI1 => TWISPI1,
263);
264#[cfg(context = "nrf5340-app")]
265define_i2c_drivers!(
266    SERIAL0 => SERIAL0,
267    SERIAL1 => SERIAL1,
268);
269#[cfg(context = "nrf91")]
270define_i2c_drivers!(
271    SERIAL0 => SERIAL0,
272    SERIAL1 => SERIAL1,
273);