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