pamoja_sensors/opt3001.rs
1//! Texas Instruments OPT3001 ambient light sensor.
2//!
3//! The OPT3001 is a single-chip lux meter with an optical filter matched to the
4//! photopic response of the human eye. It reports illuminance as a 16-bit word holding
5//! a 4-bit exponent and a 12-bit mantissa, across twelve binary-weighted full-scale
6//! ranges the part can select on its own; the low- and high-limit registers that drive
7//! its interrupt pin share that encoding. This module decodes the word, encodes a lux
8//! threshold back into it, and builds and parses the configuration register field by
9//! field, following the datasheet's register tables and its table of worked decoding
10//! examples.
11//!
12//! Illuminance is returned in integer milli-lux, which holds every register value
13//! exactly since the smallest LSB is 0.01 lux; an `f32` lux convenience sits beside it.
14
15/// The I2C address with the ADDR pin tied to GND.
16pub const I2C_ADDRESS_GND: u8 = 0x44;
17/// The I2C address with the ADDR pin tied to VDD.
18pub const I2C_ADDRESS_VDD: u8 = 0x45;
19/// The I2C address with the ADDR pin tied to SDA.
20pub const I2C_ADDRESS_SDA: u8 = 0x46;
21/// The I2C address with the ADDR pin tied to SCL.
22pub const I2C_ADDRESS_SCL: u8 = 0x47;
23
24/// The value the manufacturer ID register returns: `0x5449`, the ASCII bytes "TI".
25pub const MANUFACTURER_ID: u16 = 0x5449;
26/// The value the device ID register returns for an OPT3001.
27pub const DEVICE_ID: u16 = 0x3001;
28
29#[cfg(feature = "embedded-hal")]
30mod driver;
31#[cfg(feature = "embedded-hal")]
32pub mod sim;
33
34#[cfg(feature = "embedded-hal")]
35pub use driver::{Opt3001, POLL_INTERVAL_MILLIS, STATUS_POLLS};
36
37/// The OPT3001 register addresses. Every register is 16 bits, sent most significant
38/// byte first.
39pub mod register {
40 /// Result register: the exponent and mantissa of the latest conversion.
41 pub const RESULT: u8 = 0x00;
42 /// Configuration register: range, conversion time, mode, status flags, and the
43 /// interrupt reporting settings.
44 pub const CONFIGURATION: u8 = 0x01;
45 /// Low-limit register, in the result register's encoding.
46 pub const LOW_LIMIT: u8 = 0x02;
47 /// High-limit register, in the result register's encoding.
48 pub const HIGH_LIMIT: u8 = 0x03;
49 /// Manufacturer ID register; reads [`super::MANUFACTURER_ID`].
50 pub const MANUFACTURER_ID: u8 = 0x7E;
51 /// Device ID register; reads [`super::DEVICE_ID`] for an OPT3001.
52 pub const DEVICE_ID: u8 = 0x7F;
53}
54
55/// The power-on value of the configuration register (0xC810): automatic full-scale
56/// range, 800 ms conversion time, shutdown mode, latched window-style comparison, an
57/// active-low INT pin, the exponent not masked, and a fault count of one.
58pub const CONFIGURATION_RESET: u16 = 0xC810;
59/// The power-on value of the low-limit register: exponent 0, mantissa 0, or 0 lux.
60pub const LOW_LIMIT_RESET: u16 = 0x0000;
61/// The power-on value of the high-limit register: exponent 11, mantissa 0xFFF, the
62/// largest encodable threshold of 83865.60 lux.
63pub const HIGH_LIMIT_RESET: u16 = 0xBFFF;
64/// The low-limit register value that selects end-of-conversion mode, where the INT
65/// pin and the flags report every completed conversion: the exponent's two most
66/// significant bits set to `11b`.
67pub const LOW_LIMIT_END_OF_CONVERSION: u16 = 0xC000;
68
69/// The range number that selects automatic full-scale setting, `1100b`. Range numbers
70/// `0` to `11` select one of the fixed full-scale ranges; `13` to `15` are reserved.
71pub const RANGE_AUTOMATIC: u8 = 0b1100;
72/// The largest range number that selects a fixed full-scale range.
73pub const RANGE_MAX: u8 = 11;
74
75/// Returns the LSB size, in milli-lux, of a result or limit register at an exponent.
76///
77/// The datasheet's `LSB_Size = 0.01 lux * 2^E`, so 10 milli-lux doubled per step.
78///
79/// # Arguments
80///
81/// * `exponent` - the 4-bit exponent, a range number from `0` to `11`.
82///
83/// # Returns
84///
85/// The LSB size in milli-lux, or `None` if `exponent` is above [`RANGE_MAX`].
86pub fn lsb_milli_lux(exponent: u8) -> Option<u32> {
87 (exponent <= RANGE_MAX).then(|| 10u32 << exponent)
88}
89
90/// Returns the full-scale illuminance, in milli-lux, of a range number.
91///
92/// Full scale is the 12-bit mantissa's maximum, 4095, at that range's LSB size, so
93/// 40.95 lux at range `0` up to 83865.60 lux at range `11`.
94///
95/// # Arguments
96///
97/// * `range_number` - the range number, `0` to `11`.
98///
99/// # Returns
100///
101/// The full-scale illuminance in milli-lux, or `None` for the automatic and reserved
102/// range numbers, which have no single full scale.
103pub fn full_scale_milli_lux(range_number: u8) -> Option<u32> {
104 lsb_milli_lux(range_number).map(|lsb| lsb * 0xFFF)
105}
106
107/// Decodes a result or limit register word to milli-lux.
108///
109/// The datasheet's `lux = 0.01 * 2^E[3:0] * R[11:0]`, exact in integer milli-lux for
110/// every possible word.
111///
112/// # Arguments
113///
114/// * `raw` - the 16-bit register word, exponent in bits 15:12 and mantissa in 11:0.
115///
116/// # Returns
117///
118/// The illuminance in milli-lux.
119pub fn milli_lux(raw: u16) -> u32 {
120 let exponent = raw >> 12;
121 let mantissa = u32::from(raw & 0x0FFF);
122 (10u32 << exponent) * mantissa
123}
124
125/// Decodes a result or limit register word to lux.
126///
127/// # Arguments
128///
129/// * `raw` - the 16-bit register word.
130///
131/// # Returns
132///
133/// The illuminance in lux.
134pub fn lux(raw: u16) -> f32 {
135 milli_lux(raw) as f32 / 1000.0
136}
137
138/// Encodes an illuminance into the result and limit registers' exponent-and-mantissa
139/// word, at the smallest exponent that holds the value.
140///
141/// The inverse of [`milli_lux`]. The smallest exponent gives the finest LSB, so the
142/// encoded threshold is the closest one the part can compare against; the mantissa is
143/// truncated to that LSB. An illuminance above the largest full scale, 83865.60 lux,
144/// saturates to [`HIGH_LIMIT_RESET`], the largest encodable word.
145///
146/// # Arguments
147///
148/// * `milli_lux` - the illuminance in milli-lux.
149///
150/// # Returns
151///
152/// The 16-bit register word.
153pub fn raw_from_milli_lux(milli_lux: u32) -> u16 {
154 for exponent in 0..=RANGE_MAX {
155 let mantissa = milli_lux / (10u32 << exponent);
156 if mantissa <= 0x0FFF {
157 return (u16::from(exponent) << 12) | mantissa as u16;
158 }
159 }
160 HIGH_LIMIT_RESET
161}
162
163/// Assembles a register word from the two bytes the part sends, most significant
164/// byte first.
165///
166/// # Arguments
167///
168/// * `bytes` - the two data bytes of a register read.
169///
170/// # Returns
171///
172/// The 16-bit register word.
173pub fn word_from_bytes(bytes: [u8; 2]) -> u16 {
174 u16::from_be_bytes(bytes)
175}
176
177/// Splits a register word into the two bytes written to the part, most significant
178/// byte first.
179///
180/// # Arguments
181///
182/// * `word` - the 16-bit register word.
183///
184/// # Returns
185///
186/// The two data bytes of a register write.
187pub fn word_to_bytes(word: u16) -> [u8; 2] {
188 word.to_be_bytes()
189}
190
191/// The conversion time (configuration bit 11).
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum ConversionTime {
194 /// 100 ms; on ranges `0` to `5` this drops one to three bits of resolution.
195 Ms100,
196 /// 800 ms, the full specified resolution on every range (the default).
197 Ms800,
198}
199
200impl ConversionTime {
201 /// Returns the conversion time in milliseconds.
202 pub fn millis(self) -> u16 {
203 match self {
204 ConversionTime::Ms100 => 100,
205 ConversionTime::Ms800 => 800,
206 }
207 }
208}
209
210/// The mode of conversion operation (configuration bits 10:9).
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum Mode {
213 /// Shutdown; the flags and INT pin keep their last state (the default).
214 Shutdown,
215 /// One conversion, after which the field reads back as shutdown.
216 SingleShot,
217 /// Continuous conversions.
218 Continuous,
219}
220
221impl Mode {
222 /// Returns the 2-bit field code for this mode.
223 pub fn code(self) -> u8 {
224 match self {
225 Mode::Shutdown => 0b00,
226 Mode::SingleShot => 0b01,
227 Mode::Continuous => 0b10,
228 }
229 }
230
231 /// Builds a mode from a 2-bit field code.
232 ///
233 /// Codes `10` and `11` both select continuous conversion and map to
234 /// [`Mode::Continuous`].
235 pub fn from_code(code: u8) -> Mode {
236 match code & 0b11 {
237 0b00 => Mode::Shutdown,
238 0b01 => Mode::SingleShot,
239 _ => Mode::Continuous,
240 }
241 }
242}
243
244/// The interrupt reporting style, the latch field (configuration bit 4).
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub enum Latch {
247 /// Transparent hysteresis-style comparison: the INT pin and flags follow the
248 /// comparison directly, with no clearing event.
249 TransparentHysteresis,
250 /// Latched window-style comparison: the INT pin and flags hold until the
251 /// configuration register is read (the default).
252 LatchedWindow,
253}
254
255/// The INT pin polarity (configuration bit 3).
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum Polarity {
258 /// INT pulls low on an interrupt event (the default).
259 ActiveLow,
260 /// INT goes high impedance on an interrupt event, to be pulled high.
261 ActiveHigh,
262}
263
264/// The number of consecutive fault events that trigger a report (configuration
265/// bits 1:0).
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum FaultCount {
268 /// One fault (the default).
269 One,
270 /// Two consecutive faults.
271 Two,
272 /// Four consecutive faults.
273 Four,
274 /// Eight consecutive faults.
275 Eight,
276}
277
278impl FaultCount {
279 /// Returns the 2-bit field code for this fault count.
280 pub fn code(self) -> u8 {
281 match self {
282 FaultCount::One => 0b00,
283 FaultCount::Two => 0b01,
284 FaultCount::Four => 0b10,
285 FaultCount::Eight => 0b11,
286 }
287 }
288
289 /// Builds a fault count from a 2-bit field code.
290 pub fn from_code(code: u8) -> FaultCount {
291 match code & 0b11 {
292 0b00 => FaultCount::One,
293 0b01 => FaultCount::Two,
294 0b10 => FaultCount::Four,
295 _ => FaultCount::Eight,
296 }
297 }
298
299 /// Returns the number of consecutive faults this setting requires.
300 pub fn count(self) -> u8 {
301 match self {
302 FaultCount::One => 1,
303 FaultCount::Two => 2,
304 FaultCount::Four => 4,
305 FaultCount::Eight => 8,
306 }
307 }
308}
309
310/// A decoded OPT3001 configuration register.
311///
312/// Build one, set the fields, and turn it into the 16-bit register value with
313/// [`bits`](Configuration::bits); or parse a register read with
314/// [`from_bits`](Configuration::from_bits). [`Configuration::default`] is the power-on
315/// state, [`CONFIGURATION_RESET`]. The four status fields are read-only on the part
316/// and are ignored by [`bits`](Configuration::bits).
317///
318/// # Examples
319///
320/// ```
321/// use pamoja_sensors::opt3001::{Configuration, Mode};
322///
323/// // Convert continuously with the range set automatically, everything else default.
324/// let config = Configuration {
325/// mode: Mode::Continuous,
326/// ..Configuration::default()
327/// };
328/// assert_eq!(config.bits(), 0xCC10);
329/// assert_eq!(Configuration::from_bits(0xCC10), config);
330/// ```
331#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub struct Configuration {
333 /// The range number (bits 15:12): `0` to `11` for a fixed full-scale range, or
334 /// [`RANGE_AUTOMATIC`] to let the part choose and report its choice in the
335 /// result's exponent.
336 pub range_number: u8,
337 /// The conversion time.
338 pub conversion_time: ConversionTime,
339 /// The mode of conversion operation.
340 pub mode: Mode,
341 /// Read-only: the last conversion overflowed its full-scale range.
342 pub overflow: bool,
343 /// Read-only: a conversion has completed since the register was last read or
344 /// written with a non-shutdown mode.
345 pub conversion_ready: bool,
346 /// Read-only: the result exceeded the high limit for the fault count.
347 pub flag_high: bool,
348 /// Read-only: the result fell below the low limit for the fault count.
349 pub flag_low: bool,
350 /// The interrupt reporting style.
351 pub latch: Latch,
352 /// The INT pin polarity.
353 pub polarity: Polarity,
354 /// Force the result's exponent to zero on a fixed range, so the mantissa alone
355 /// is the reading at that range's LSB.
356 pub mask_exponent: bool,
357 /// The consecutive faults required to trigger a report.
358 pub fault_count: FaultCount,
359}
360
361impl Default for Configuration {
362 fn default() -> Self {
363 Configuration {
364 range_number: RANGE_AUTOMATIC,
365 conversion_time: ConversionTime::Ms800,
366 mode: Mode::Shutdown,
367 overflow: false,
368 conversion_ready: false,
369 flag_high: false,
370 flag_low: false,
371 latch: Latch::LatchedWindow,
372 polarity: Polarity::ActiveLow,
373 mask_exponent: false,
374 fault_count: FaultCount::One,
375 }
376 }
377}
378
379impl Configuration {
380 /// Assembles the 16-bit configuration register value.
381 ///
382 /// The read-only status bits are written as zero, and the range number is
383 /// truncated to its four bits.
384 ///
385 /// # Returns
386 ///
387 /// The register value to write, most significant byte first.
388 pub fn bits(self) -> u16 {
389 let mut bits = 0u16;
390 bits |= u16::from(self.range_number & 0x0F) << 12;
391 bits |= u16::from(matches!(self.conversion_time, ConversionTime::Ms800)) << 11;
392 bits |= u16::from(self.mode.code()) << 9;
393 bits |= u16::from(matches!(self.latch, Latch::LatchedWindow)) << 4;
394 bits |= u16::from(matches!(self.polarity, Polarity::ActiveHigh)) << 3;
395 bits |= u16::from(self.mask_exponent) << 2;
396 bits |= u16::from(self.fault_count.code());
397 bits
398 }
399
400 /// Parses a 16-bit configuration register value.
401 ///
402 /// # Arguments
403 ///
404 /// * `bits` - the register value, as read from the device.
405 ///
406 /// # Returns
407 ///
408 /// The decoded configuration, status flags included.
409 pub fn from_bits(bits: u16) -> Configuration {
410 Configuration {
411 range_number: (bits >> 12) as u8,
412 conversion_time: if bits & (1 << 11) != 0 {
413 ConversionTime::Ms800
414 } else {
415 ConversionTime::Ms100
416 },
417 mode: Mode::from_code((bits >> 9) as u8),
418 overflow: bits & (1 << 8) != 0,
419 conversion_ready: bits & (1 << 7) != 0,
420 flag_high: bits & (1 << 6) != 0,
421 flag_low: bits & (1 << 5) != 0,
422 latch: if bits & (1 << 4) != 0 {
423 Latch::LatchedWindow
424 } else {
425 Latch::TransparentHysteresis
426 },
427 polarity: if bits & (1 << 3) != 0 {
428 Polarity::ActiveHigh
429 } else {
430 Polarity::ActiveLow
431 },
432 mask_exponent: bits & (1 << 2) != 0,
433 fault_count: FaultCount::from_code(bits as u8),
434 }
435 }
436
437 /// Returns whether the range number selects automatic full-scale setting.
438 pub fn is_automatic_range(&self) -> bool {
439 self.range_number == RANGE_AUTOMATIC
440 }
441}
442
443/// One result register word, with the conversion to illuminance.
444///
445/// # Examples
446///
447/// ```
448/// use pamoja_sensors::opt3001::Reading;
449///
450/// // Exponent 0 over mantissa 3200: 0.01 lux per count, 32 lux.
451/// let reading = Reading::new(0x0C80);
452/// assert_eq!(reading.milli_lux(), 32_000);
453/// assert_eq!(u16::from_be_bytes(reading.to_bytes()), 0x0C80, "high byte first");
454/// ```
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct Reading {
457 raw: u16,
458}
459
460impl Reading {
461 /// Wraps a result register word.
462 ///
463 /// # Arguments
464 ///
465 /// * `raw` - the word: a four-bit exponent over a twelve-bit mantissa.
466 ///
467 /// # Returns
468 ///
469 /// The reading.
470 pub fn new(raw: u16) -> Reading {
471 Reading { raw }
472 }
473
474 /// Returns the register word.
475 pub fn raw(&self) -> u16 {
476 self.raw
477 }
478
479 /// Returns the illuminance in millilux, exact in integer arithmetic.
480 pub fn milli_lux(&self) -> u32 {
481 milli_lux(self.raw)
482 }
483
484 /// Returns the illuminance in lux.
485 pub fn lux(&self) -> f32 {
486 lux(self.raw)
487 }
488
489 /// Returns the register bytes, most significant first, as the part sends them.
490 pub fn to_bytes(&self) -> [u8; 2] {
491 word_to_bytes(self.raw)
492 }
493}
494
495#[cfg(test)]
496mod driver_support_tests {
497 use super::*;
498
499 #[test]
500 fn a_reading_converts_its_word_like_the_free_functions() {
501 let raw = raw_from_milli_lux(1_000_000);
502 let reading = Reading::new(raw);
503 assert_eq!(reading.raw(), raw);
504 assert_eq!(reading.milli_lux(), milli_lux(raw));
505 assert!((reading.lux() - 1000.0).abs() < 1.0);
506 assert_eq!(reading.to_bytes(), word_to_bytes(raw));
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn addresses_follow_the_addr_pin_table() {
516 // Table 1: 1000100b with ADDR to GND, then VDD, SDA, SCL.
517 assert_eq!(I2C_ADDRESS_GND, 0b100_0100);
518 assert_eq!(I2C_ADDRESS_VDD, 0b100_0101);
519 assert_eq!(I2C_ADDRESS_SDA, 0b100_0110);
520 assert_eq!(I2C_ADDRESS_SCL, 0b100_0111);
521 }
522
523 #[test]
524 fn register_map_and_ids_match_the_datasheet() {
525 // Table 6 register map; Tables 14 and 15 for the ID values.
526 assert_eq!(register::RESULT, 0x00);
527 assert_eq!(register::CONFIGURATION, 0x01);
528 assert_eq!(register::LOW_LIMIT, 0x02);
529 assert_eq!(register::HIGH_LIMIT, 0x03);
530 assert_eq!(register::MANUFACTURER_ID, 0x7E);
531 assert_eq!(register::DEVICE_ID, 0x7F);
532 assert_eq!(MANUFACTURER_ID, 0x5449);
533 assert_eq!(MANUFACTURER_ID.to_be_bytes(), *b"TI");
534 assert_eq!(DEVICE_ID, 0x3001);
535 }
536
537 #[test]
538 fn result_register_decodes_per_the_datasheet_examples() {
539 // Table 9, every row: register word, LSB weight, and resulting lux.
540 let rows: [(u16, u32, u32); 10] = [
541 (0x0001, 10, 10),
542 (0x0FFF, 10, 40_950),
543 (0x3456, 80, 88_800),
544 (0x789A, 1_280, 2_818_560),
545 (0x8800, 2_560, 5_242_880),
546 (0x9400, 5_120, 5_242_880),
547 (0xA200, 10_240, 5_242_880),
548 (0xB100, 20_480, 5_242_880),
549 (0xB001, 20_480, 20_480),
550 (0xBFFF, 20_480, 83_865_600),
551 ];
552 for (raw, lsb, expected) in rows {
553 assert_eq!(
554 lsb_milli_lux((raw >> 12) as u8),
555 Some(lsb),
556 "lsb {raw:#06x}"
557 );
558 assert_eq!(milli_lux(raw), expected, "milli-lux {raw:#06x}");
559 assert!(
560 (lux(raw) - expected as f32 / 1000.0).abs() < 0.001,
561 "lux {raw:#06x}"
562 );
563 }
564 }
565
566 #[test]
567 fn full_scale_table_matches_each_range_number() {
568 // Table 8: full-scale range and LSB size for exponents 0000b to 1011b.
569 let rows: [(u8, u32, u32); 12] = [
570 (0, 40_950, 10),
571 (1, 81_900, 20),
572 (2, 163_800, 40),
573 (3, 327_600, 80),
574 (4, 655_200, 160),
575 (5, 1_310_400, 320),
576 (6, 2_620_800, 640),
577 (7, 5_241_600, 1_280),
578 (8, 10_483_200, 2_560),
579 (9, 20_966_400, 5_120),
580 (10, 41_932_800, 10_240),
581 (11, 83_865_600, 20_480),
582 ];
583 for (range, full_scale, lsb) in rows {
584 assert_eq!(
585 full_scale_milli_lux(range),
586 Some(full_scale),
587 "range {range}"
588 );
589 assert_eq!(lsb_milli_lux(range), Some(lsb), "range {range}");
590 assert_eq!(milli_lux((u16::from(range) << 12) | 0x0FFF), full_scale);
591 }
592 // The automatic and reserved range numbers have no full scale of their own.
593 for range in RANGE_AUTOMATIC..=0x0F {
594 assert_eq!(full_scale_milli_lux(range), None);
595 assert_eq!(lsb_milli_lux(range), None);
596 }
597 // Electrical Characteristics: 0.01 lux resolution, 83865.6 lux full scale.
598 assert_eq!(lsb_milli_lux(0), Some(10));
599 assert_eq!(full_scale_milli_lux(RANGE_MAX), Some(83_865_600));
600 }
601
602 #[test]
603 fn encoder_picks_the_smallest_exponent_that_holds_the_value() {
604 // Table 9 lists four words for 5242.88 lux; 08h/800h is the smallest exponent.
605 assert_eq!(raw_from_milli_lux(5_242_880), 0x8800);
606 // 88.80 lux fits a 0.04 lux LSB, one step finer than Table 9's 03h/456h.
607 assert_eq!(raw_from_milli_lux(88_800), 0x28AC);
608 assert_eq!(milli_lux(0x28AC), 88_800);
609 assert_eq!(raw_from_milli_lux(10), 0x0001);
610 assert_eq!(raw_from_milli_lux(40_950), 0x0FFF);
611 assert_eq!(raw_from_milli_lux(40_960), 0x1800);
612 assert_eq!(raw_from_milli_lux(0), 0x0000);
613 // The largest encodable threshold, and saturation above it.
614 assert_eq!(raw_from_milli_lux(83_865_600), HIGH_LIMIT_RESET);
615 assert_eq!(raw_from_milli_lux(83_865_601), HIGH_LIMIT_RESET);
616 assert_eq!(raw_from_milli_lux(u32::MAX), HIGH_LIMIT_RESET);
617 // A value between two LSB steps truncates to the step below.
618 assert_eq!(raw_from_milli_lux(15), 0x0001);
619 }
620
621 #[test]
622 fn every_result_word_survives_a_round_trip_through_the_encoder() {
623 for raw in 0..=HIGH_LIMIT_RESET {
624 let value = milli_lux(raw);
625 let encoded = raw_from_milli_lux(value);
626 assert_eq!(milli_lux(encoded), value, "word {raw:#06x}");
627 assert!(
628 encoded >> 12 <= raw >> 12,
629 "word {raw:#06x} re-encoded {encoded:#06x}"
630 );
631 }
632 }
633
634 #[test]
635 fn integer_decode_tracks_the_floating_point_reference() {
636 // Equation 3, transcribed: lux = 0.01 * 2^E[3:0] * R[11:0].
637 for raw in 0..=HIGH_LIMIT_RESET {
638 let exponent = f64::from(raw >> 12);
639 let mantissa = f64::from(raw & 0x0FFF);
640 let reference = 0.01 * exponent.exp2() * mantissa;
641 let integer = f64::from(milli_lux(raw)) / 1000.0;
642 assert!(
643 (integer - reference).abs() < 1e-6,
644 "word {raw:#06x}: {integer} vs {reference}"
645 );
646 }
647 }
648
649 #[test]
650 fn register_words_travel_most_significant_byte_first() {
651 // Figures 20 and 21: data MSByte then data LSByte.
652 assert_eq!(word_from_bytes([0x34, 0x56]), 0x3456);
653 assert_eq!(word_to_bytes(0x3456), [0x34, 0x56]);
654 assert_eq!(milli_lux(word_from_bytes([0x78, 0x9A])), 2_818_560);
655 assert_eq!(word_from_bytes(word_to_bytes(0xC810)), 0xC810);
656 }
657
658 #[test]
659 fn default_configuration_is_the_datasheet_reset_value() {
660 // Section 7.6.1.1.2: configuration register reset C810h, Table 10 per field.
661 assert_eq!(Configuration::default().bits(), CONFIGURATION_RESET);
662 assert_eq!(
663 Configuration::from_bits(CONFIGURATION_RESET),
664 Configuration::default()
665 );
666 let reset = Configuration::default();
667 assert_eq!(reset.range_number, 0b1100);
668 assert!(reset.is_automatic_range());
669 assert_eq!(reset.conversion_time, ConversionTime::Ms800);
670 assert_eq!(reset.mode, Mode::Shutdown);
671 assert_eq!(reset.latch, Latch::LatchedWindow);
672 assert_eq!(reset.polarity, Polarity::ActiveLow);
673 assert!(!reset.mask_exponent);
674 assert_eq!(reset.fault_count, FaultCount::One);
675 }
676
677 #[test]
678 fn limit_registers_reset_per_the_datasheet() {
679 // Table 11: LE = 0h, TL = 000h. Table 13: HE = Bh, TH = FFFh.
680 assert_eq!(LOW_LIMIT_RESET, 0x0000);
681 assert_eq!(milli_lux(LOW_LIMIT_RESET), 0);
682 assert_eq!(HIGH_LIMIT_RESET, 0xBFFF);
683 assert_eq!(milli_lux(HIGH_LIMIT_RESET), 83_865_600);
684 // Section 7.4.2.3: end-of-conversion mode is LE[3:2] = 11b.
685 assert_eq!(LOW_LIMIT_END_OF_CONVERSION >> 14, 0b11);
686 }
687
688 #[test]
689 fn configuration_field_codes_match_the_datasheet() {
690 // Table 10: CT 1 = 800 ms; M 01 = single-shot, 10 and 11 = continuous;
691 // FC 00, 01, 10, 11 = one, two, four, eight faults.
692 assert_eq!(ConversionTime::Ms100.millis(), 100);
693 assert_eq!(ConversionTime::Ms800.millis(), 800);
694 assert_eq!(Mode::SingleShot.code(), 0b01);
695 assert_eq!(Mode::from_code(0b10), Mode::Continuous);
696 assert_eq!(Mode::from_code(0b11), Mode::Continuous);
697 assert_eq!(FaultCount::Eight.code(), 0b11);
698 assert_eq!(FaultCount::from_code(0b10).count(), 4);
699 // Continuous conversion on the automatic range is the reset word with M = 10b.
700 let continuous = Configuration {
701 mode: Mode::Continuous,
702 ..Configuration::default()
703 };
704 assert_eq!(continuous.bits(), 0xCC10);
705 // A read with OVF, CRF, and FH set: the flags decode and are not written back.
706 let status = Configuration::from_bits(0xCDD0);
707 assert!(status.overflow);
708 assert!(status.conversion_ready);
709 assert!(status.flag_high);
710 assert!(!status.flag_low);
711 assert_eq!(status.bits(), 0xCC10);
712 }
713
714 #[test]
715 fn configuration_round_trips_through_bits() {
716 let config = Configuration {
717 range_number: 6,
718 conversion_time: ConversionTime::Ms100,
719 mode: Mode::SingleShot,
720 latch: Latch::TransparentHysteresis,
721 polarity: Polarity::ActiveHigh,
722 mask_exponent: true,
723 fault_count: FaultCount::Four,
724 ..Configuration::default()
725 };
726 assert_eq!(config.bits(), 0x620E);
727 assert_eq!(Configuration::from_bits(config.bits()), config);
728 assert!(!config.is_automatic_range());
729 assert_eq!(full_scale_milli_lux(config.range_number), Some(2_620_800));
730 }
731}