Skip to main content

ariel_os_esp/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;
8use esp_hal::{
9    Async,
10    gpio::interconnect::PeripheralOutput,
11    i2c::master::{BusTimeout, I2c as EspI2c},
12    peripherals,
13};
14
15/// I2C bus configuration.
16#[non_exhaustive]
17#[derive(Clone)]
18pub struct Config {
19    /// The frequency at which the bus should operate.
20    pub frequency: Frequency,
21}
22
23impl Default for Config {
24    fn default() -> Self {
25        Self {
26            frequency: Frequency::_100k,
27        }
28    }
29}
30
31/// I2C bus frequency.
32// NOTE(hal): the technical references only mention these frequencies.
33#[derive(Debug, Copy, Clone, PartialEq, Eq)]
34#[cfg_attr(feature = "defmt", derive(defmt::Format))]
35pub enum Frequency {
36    /// Standard mode.
37    _100k,
38    /// Fast mode.
39    _400k,
40}
41
42#[doc(hidden)]
43impl Frequency {
44    #[must_use]
45    pub const fn first() -> Self {
46        Self::_100k
47    }
48
49    #[must_use]
50    pub const fn last() -> Self {
51        Self::_400k
52    }
53
54    #[must_use]
55    pub const fn next(self) -> Option<Self> {
56        match self {
57            Self::_100k => Some(Self::_400k),
58            Self::_400k => None,
59        }
60    }
61
62    #[must_use]
63    pub const fn prev(self) -> Option<Self> {
64        match self {
65            Self::_100k => None,
66            Self::_400k => Some(Self::_100k),
67        }
68    }
69
70    #[must_use]
71    pub const fn khz(self) -> u32 {
72        match self {
73            Self::_100k => 100,
74            Self::_400k => 400,
75        }
76    }
77}
78
79ariel_os_embassy_common::impl_i2c_from_frequency!();
80
81impl From<Frequency> for esp_hal::time::Rate {
82    fn from(freq: Frequency) -> Self {
83        match freq {
84            Frequency::_100k => esp_hal::time::Rate::from_khz(100),
85            Frequency::_400k => esp_hal::time::Rate::from_khz(400),
86        }
87    }
88}
89
90macro_rules! define_i2c_drivers {
91    ($( $peripheral:ident ),* $(,)?) => {
92        $(
93            /// Peripheral-specific I2C driver.
94            pub struct $peripheral {
95                twim: EspI2c<'static, Async>,
96            }
97
98            // Ensure this peripheral has only one active Instance.
99            paste::paste! {
100                static [< ACTIVE_ $peripheral >]: AtomicBool = AtomicBool::new(false);
101            }
102
103            impl $peripheral {
104                /// Returns a driver implementing [`embedded_hal_async::i2c::I2c`] for this
105                /// I2C peripheral.
106                #[expect(clippy::new_ret_no_self)]
107                #[must_use]
108                pub fn new<SDA: PeripheralOutput<'static>, SCL: PeripheralOutput<'static>>(
109                    sda_pin: impl $crate::IntoPeripheral<'static, SDA>,
110                    scl_pin: impl $crate::IntoPeripheral<'static, SCL>,
111                    config: Config,
112                ) -> I2c {
113                    // Make this struct a compile-time-enforced singleton: having multiple statics
114                    // defined with the same name would result in a compile-time error.
115                    paste::paste! {
116                        #[allow(dead_code)]
117                        static [<PREVENT_MULTIPLE_ $peripheral>]: () = ();
118                    }
119
120                    let twim_config = esp_hal::i2c::master::Config::default()
121                        .with_frequency(config.frequency.into())
122                        // disable timeout as we handle that at a higher level.
123                        .with_timeout(
124                            #[cfg(any(context = "esp32c3", context = "esp32c6", context = "esp32s2", context = "esp32s3"))]
125                            BusTimeout::Disabled,
126                            // Use the maximum value as timeout cannot be disabled.
127                            #[cfg(context = "esp32")]
128                            BusTimeout::Maximum
129                            );
130
131                    // Check if we can initialize this peripheral (check if the value was previously false, set it to true).
132                    paste::paste! {
133                        if [< ACTIVE_ $peripheral >].swap(true, Ordering::AcqRel) {
134                            panic!("I2C peripheral already initialized")
135                        }
136                    }
137
138                    // FIXME(safety): enforce that the init code indeed has run
139                    // SAFETY: We check with an AtomicBool that only one instance of this peripheral
140                    // is active at once.
141                    let i2c_peripheral = unsafe { peripherals::$peripheral::steal() };
142
143                    let twim = EspI2c::new(
144                        i2c_peripheral,
145                        twim_config,
146                    )
147                        .unwrap()
148                        .into_async()
149                        .with_sda(sda_pin.into_hal_peripheral())
150                        .with_scl(scl_pin.into_hal_peripheral());
151
152                    I2c::$peripheral(Self { twim })
153                }
154            }
155
156            impl Drop for $peripheral {
157                fn drop(&mut self) {
158                    paste::paste! {
159                        [< ACTIVE_ $peripheral >].store(false, Ordering::Release);
160                    }
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: esp_hal::i2c::master::Error) -> ariel_os_embassy_common::i2c::controller::Error {
183    use esp_hal::i2c::master::{AcknowledgeCheckFailedReason, Error as EspError};
184
185    use ariel_os_embassy_common::i2c::controller::{Error, NoAcknowledgeSource};
186
187    #[expect(clippy::match_same_arms, reason = "non-exhaustive upstream enum")]
188    match err {
189        EspError::FifoExceeded => Error::Overrun,
190        EspError::AcknowledgeCheckFailed(reason) => {
191            let reason = match reason {
192                AcknowledgeCheckFailedReason::Address => NoAcknowledgeSource::Address,
193                AcknowledgeCheckFailedReason::Data => NoAcknowledgeSource::Data,
194                AcknowledgeCheckFailedReason::Unknown | _ => NoAcknowledgeSource::Unknown,
195            };
196            Error::NoAcknowledge(reason)
197        }
198        EspError::Timeout => Error::Timeout,
199        EspError::ArbitrationLost => Error::ArbitrationLoss,
200        EspError::ExecutionIncomplete
201        | EspError::CommandNumberExceeded
202        | EspError::ZeroLengthInvalid => Error::Other,
203        _ => Error::Other,
204    }
205}
206
207// Define a driver per peripheral
208#[cfg(context = "esp32")]
209define_i2c_drivers!(I2C0, I2C1);
210#[cfg(context = "esp32c3")]
211define_i2c_drivers!(I2C0);
212#[cfg(context = "esp32c6")]
213define_i2c_drivers!(I2C0);
214#[cfg(context = "esp32s2")]
215define_i2c_drivers!(I2C0, I2C1);
216#[cfg(context = "esp32s3")]
217define_i2c_drivers!(I2C0, I2C1);