/ firmware / src / hardware / i2c.rs
i2c.rs
  1  use esp_hal::i2c::master::{Config as I2cConfig, I2c};
  2  use esp_hal::time::Rate;
  3  
  4  use crate::config::board;
  5  
  6  pub const MAX_DISCOVERED_DEVICES: usize = 16;
  7  pub const SENSOR_CANDIDATE_ADDRESSES: [u8; 4] = [0x44, 0x45, 0x46, 0x47];
  8  pub const SENSOR_MEASUREMENT_COMMAND: [u8; 2] = [0x24, 0x00];
  9  const I2C_SCAN_ADDRESS_MIN: u8 = 0x08;
 10  const I2C_SCAN_ADDRESS_MAX: u8 = 0x77;
 11  
 12  #[derive(Clone, Copy, defmt::Format)]
 13  pub struct DiscoveredDevice {
 14      pub bus: u8,
 15      pub address: u8,
 16  }
 17  
 18  impl DiscoveredDevice {
 19      pub fn bus_name(self) -> &'static str {
 20          match self.bus {
 21              0 => "i2c.0",
 22              1 => "i2c.1",
 23              _ => "i2c.?",
 24          }
 25      }
 26  }
 27  
 28  pub struct I2cSnapshot {
 29      pub frequency_khz: u32,
 30      pub power_gpio: u8,
 31      pub buses: [BusStatusSnapshot; 2],
 32      pub discovered_devices: heapless::Vec<DiscoveredDevice, MAX_DISCOVERED_DEVICES>,
 33  }
 34  
 35  #[derive(Clone, Copy)]
 36  pub struct BusStatusSnapshot {
 37      pub name: &'static str,
 38      pub bus_index: u8,
 39      pub sda_gpio: u8,
 40      pub scl_gpio: u8,
 41  }
 42  
 43  static DISCOVERY_CACHE: critical_section::Mutex<
 44      core::cell::RefCell<heapless::Vec<DiscoveredDevice, MAX_DISCOVERED_DEVICES>>,
 45  > = critical_section::Mutex::new(core::cell::RefCell::new(heapless::Vec::new()));
 46  
 47  static DISCOVERY_DONE: core::sync::atomic::AtomicBool =
 48      core::sync::atomic::AtomicBool::new(false);
 49  
 50  // ─────────────────────────────────────────────────────────────────────────────
 51  //  Bus initialization
 52  // ─────────────────────────────────────────────────────────────────────────────
 53  
 54  pub fn initialize_bus_0(
 55      peripheral: esp_hal::peripherals::I2C0<'static>,
 56      sda: esp_hal::peripherals::GPIO8<'static>,
 57      scl: esp_hal::peripherals::GPIO9<'static>,
 58  ) -> I2c<'static, esp_hal::Async> {
 59      I2c::new(
 60          peripheral,
 61          I2cConfig::default().with_frequency(Rate::from_khz(board::i2c::FREQUENCY_KHZ)),
 62      )
 63      .unwrap()
 64      .with_sda(sda)
 65      .with_scl(scl)
 66      .into_async()
 67  }
 68  
 69  pub fn initialize_bus_1(
 70      peripheral: esp_hal::peripherals::I2C1<'static>,
 71      sda: esp_hal::peripherals::GPIO17<'static>,
 72      scl: esp_hal::peripherals::GPIO18<'static>,
 73  ) -> I2c<'static, esp_hal::Async> {
 74      I2c::new(
 75          peripheral,
 76          I2cConfig::default().with_frequency(Rate::from_khz(board::i2c::FREQUENCY_KHZ)),
 77      )
 78      .unwrap()
 79      .with_sda(sda)
 80      .with_scl(scl)
 81      .into_async()
 82  }
 83  
 84  // ─────────────────────────────────────────────────────────────────────────────
 85  //  Discovery — mirrors C++ hardware::i2c::discoverAll / runDiscovery / findDevice
 86  // ─────────────────────────────────────────────────────────────────────────────
 87  
 88  pub async fn discover_all(
 89      i2c0_bus: &mut Option<I2c<'static, esp_hal::Async>>,
 90      i2c1_bus: &mut Option<I2c<'static, esp_hal::Async>>,
 91  ) -> heapless::Vec<DiscoveredDevice, MAX_DISCOVERED_DEVICES> {
 92      let mut devices = heapless::Vec::new();
 93  
 94      if let Some(bus) = i2c0_bus.as_mut() {
 95          discover_bus_devices(0, bus, &mut devices).await;
 96      }
 97  
 98      if let Some(bus) = i2c1_bus.as_mut() {
 99          discover_bus_devices(1, bus, &mut devices).await;
100      }
101  
102      devices
103  }
104  
105  pub async fn run_discovery(
106      i2c0_bus: &mut Option<I2c<'static, esp_hal::Async>>,
107      i2c1_bus: &mut Option<I2c<'static, esp_hal::Async>>,
108  ) -> usize {
109      let devices = discover_all(i2c0_bus, i2c1_bus).await;
110      let count = devices.len();
111  
112      for device in devices.iter() {
113          defmt::info!(
114              "[i2c] found {:#04x} on bus {=u8} — {}",
115              device.address,
116              device.bus,
117              device_name_at(device.address)
118          );
119      }
120  
121      critical_section::with(|cs| {
122          let mut cache = DISCOVERY_CACHE.borrow_ref_mut(cs);
123          cache.clear();
124          for device in devices.iter() {
125              let _ = cache.push(*device);
126          }
127      });
128      DISCOVERY_DONE.store(true, core::sync::atomic::Ordering::Release);
129  
130      count
131  }
132  
133  pub fn find_device(address: u8) -> Option<DiscoveredDevice> {
134      critical_section::with(|cs| {
135          DISCOVERY_CACHE
136              .borrow_ref(cs)
137              .iter()
138              .find(|d| d.address == address)
139              .copied()
140      })
141  }
142  
143  pub fn device_count() -> usize {
144      critical_section::with(|cs| DISCOVERY_CACHE.borrow_ref(cs).len())
145  }
146  
147  pub fn is_discovery_done() -> bool {
148      DISCOVERY_DONE.load(core::sync::atomic::Ordering::Acquire)
149  }
150  
151  pub fn device_name_at(address: u8) -> &'static str {
152      match address {
153          0x40 => "Texas Instruments INA228 Current Monitor",
154          0x44 => "Sensirion SHT3x Temperature & Humidity Sensor",
155          0x48 => "Texas Instruments ADS1115 16-Bit ADC",
156          0x50 => "Microchip Technology AT24C32 EEPROM",
157          0x5C | 0x5D => "Adafruit LPS25 Pressure Sensor",
158          0x61 => "Sensirion SCD30 CO2 Infrared Gas Sensor",
159          0x62 => "Sensirion SCD41 CO2 Optical Gas Sensor",
160          0x68 => "Analog Devices DS3231 RTC",
161          0x70 => "Adafruit TCA9548A I2C Multiplexer",
162          _ => "unknown",
163      }
164  }
165  
166  // ─────────────────────────────────────────────────────────────────────────────
167  //  Snapshot
168  // ─────────────────────────────────────────────────────────────────────────────
169  
170  pub fn snapshot() -> I2cSnapshot {
171      let mut discovered_devices = heapless::Vec::new();
172  
173      critical_section::with(|cs| {
174          for device in DISCOVERY_CACHE.borrow_ref(cs).iter() {
175              let _ = discovered_devices.push(*device);
176          }
177      });
178  
179      I2cSnapshot {
180          frequency_khz: board::i2c::FREQUENCY_KHZ,
181          power_gpio: board::i2c::LEGACY_POWER_GPIO,
182          buses: [
183              BusStatusSnapshot {
184                  name: "i2c.0",
185                  bus_index: 0,
186                  sda_gpio: board::i2c::BUS_0.sda_gpio,
187                  scl_gpio: board::i2c::BUS_0.scl_gpio,
188              },
189              BusStatusSnapshot {
190                  name: "i2c.1",
191                  bus_index: 1,
192                  sda_gpio: board::i2c::BUS_1.sda_gpio,
193                  scl_gpio: board::i2c::BUS_1.scl_gpio,
194              },
195          ],
196          discovered_devices,
197      }
198  }
199  
200  // ─────────────────────────────────────────────────────────────────────────────
201  //  Mux helpers
202  // ─────────────────────────────────────────────────────────────────────────────
203  
204  pub async fn select_mux_channel(
205      i2c_bus: &mut I2c<'static, esp_hal::Async>,
206      mux_channel: u8,
207  ) -> Result<(), &'static str> {
208      if mux_channel > 7 {
209          return Err("mux channel out of range");
210      }
211  
212      let mux_channel_mask = 1_u8 << mux_channel;
213      i2c_bus
214          .write_async(board::i2c::MUX_ADDR, &[mux_channel_mask])
215          .await
216          .map_err(|_| "failed to select I2C mux channel")?;
217      Ok(())
218  }
219  
220  pub async fn clear_selection(
221      i2c_bus: &mut I2c<'static, esp_hal::Async>,
222  ) -> Result<(), &'static str> {
223      i2c_bus
224          .write_async(board::i2c::MUX_ADDR, &[0x00])
225          .await
226          .map_err(|_| "failed to clear I2C mux selection")?;
227      Ok(())
228  }
229  
230  // ─────────────────────────────────────────────────────────────────────────────
231  //  Utilities
232  // ─────────────────────────────────────────────────────────────────────────────
233  
234  pub fn calculate_crc8(data_bytes: &[u8]) -> u8 {
235      let mut crc_value: u8 = 0xFF;
236  
237      for data_byte in data_bytes {
238          crc_value ^= *data_byte;
239          for _ in 0..8 {
240              crc_value = if (crc_value & 0x80) != 0 {
241                  (crc_value << 1) ^ 0x31
242              } else {
243                  crc_value << 1
244              };
245          }
246      }
247  
248      crc_value
249  }
250  
251  // ─────────────────────────────────────────────────────────────────────────────
252  //  Internal
253  // ─────────────────────────────────────────────────────────────────────────────
254  
255  async fn discover_bus_devices(
256      bus_index: u8,
257      i2c_bus: &mut I2c<'static, esp_hal::Async>,
258      devices: &mut heapless::Vec<DiscoveredDevice, MAX_DISCOVERED_DEVICES>,
259  ) {
260      for address in I2C_SCAN_ADDRESS_MIN..=I2C_SCAN_ADDRESS_MAX {
261          if address == board::i2c::MUX_ADDR {
262              continue;
263          }
264  
265          if i2c_bus.write_async(address, &[]).await.is_ok() {
266              let _ = devices.push(DiscoveredDevice {
267                  bus: bus_index,
268                  address,
269              });
270              if devices.is_full() {
271                  break;
272              }
273          }
274      }
275  }