Skip to main content

capsules_extra/
shtc3.rs

1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2026.
4
5//! Driver for the Sensirion SHTC3 Temperature and Humidity Sensor.
6//!
7//! <https://sensirion.com/products/catalog/SHTC3/>
8//!
9//! This is a lower power version of the SHT3 sensor.
10
11use core::cell::Cell;
12use kernel::ErrorCode;
13use kernel::hil::i2c;
14use kernel::hil::time::{self, Alarm, ConvertTicks};
15use kernel::utilities::cells::{OptionalCell, TakeCell};
16
17/// I2C address.
18pub const BASE_ADDR: u8 = 0x70;
19
20/// SHTC3 command codes (Tables 9, 10, and 11 in the datasheet). Each
21/// command is sent as two bytes, most-significant byte first.
22mod command {
23    /// Wake the sensor up from sleep mode. Required before any other
24    /// command; the sensor is asleep on power-up.
25    pub const WAKEUP: u16 = 0x3517;
26    /// Send the sensor back to sleep, for lowest power consumption.
27    pub const SLEEP: u16 = 0xB098;
28    /// Measure both temperature and humidity, normal power mode.
29    ///
30    /// Reads temperature first, with clock stretching disabled (the
31    /// sensor NACKs reads until the measurement is ready, rather than
32    /// holding the clock line low while it measures).
33    pub const MEASURE_NORMAL_T_FIRST_NO_STRETCH: u16 = 0x7866;
34}
35
36/// Time to wait after sending [`command::WAKEUP`] before the sensor is
37/// guaranteed ready for the next command.
38///
39/// (Table 5, `t_PU`: typ. 180 us, max. 240 us), rounded up generously
40/// since Tock alarms commonly can't resolve sub-millisecond delays.
41const WAKE_UP_DELAY_MS: u32 = 1;
42
43/// Time to wait after sending the measurement command before the result
44/// is ready to read (Table 5, `t_MEAS`, normal mode: typ. 10.8 ms, max.
45/// 12.1 ms), with margin.
46const MEASURING_DELAY_MS: u32 = 15;
47
48/// Number of bytes in one measurement result: a 2-byte temperature word
49/// and its CRC, followed by a 2-byte humidity word and its CRC.
50const MEASUREMENT_LEN: usize = 6;
51
52/// CRC-8 with polynomial 0x31 (x^8 + x^5 + x^4 + 1), initialized to
53/// 0xFF, as specified in Table 16 of the datasheet. Sensirion uses this
54/// same checksum across its whole humidity/temperature sensor family.
55fn crc8(data: &[u8]) -> u8 {
56    let polynomial = 0x31;
57    let mut crc = 0xff;
58
59    for &byte in data {
60        crc ^= byte;
61        for _ in 0..8 {
62            if (crc & 0x80) != 0 {
63                crc = crc << 1 ^ polynomial;
64            } else {
65                crc <<= 1;
66            }
67        }
68    }
69    crc
70}
71
72#[derive(Clone, Copy, PartialEq)]
73enum State {
74    Idle,
75    /// Wake-up command written; waiting for the I2C write to finish.
76    Waking,
77    /// Waiting out `WAKE_UP_DELAY_MS` before issuing the measurement
78    /// command.
79    WakeDelay,
80    /// Measurement command written; waiting for the I2C write to finish.
81    Measuring,
82    /// Waiting out `MEASURING_DELAY_MS` before the result can be read.
83    MeasuringDelay,
84    /// Reading back the 6-byte measurement result.
85    ReadingMeasurement,
86    /// Put the sensor back to sleep.
87    Sleeping,
88}
89
90pub struct Shtc3<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> {
91    i2c: &'a I,
92    alarm: &'a A,
93    humidity_client: OptionalCell<&'a dyn kernel::hil::sensors::HumidityClient>,
94    temperature_client: OptionalCell<&'a dyn kernel::hil::sensors::TemperatureClient>,
95    state: Cell<State>,
96    buffer: TakeCell<'static, [u8]>,
97
98    /// Whether the client asked for a temperature reading.
99    temperature_wanted: Cell<bool>,
100    /// Whether the client asked for a humidity reading.
101    humidity_wanted: Cell<bool>,
102    /// Holds a temperature reading until it can be delivered to the client.
103    pending_temperature: Cell<Option<Result<i32, ErrorCode>>>,
104    /// Holds a humidity reading until it can be delivered to the client.
105    pending_humidity: Cell<Option<usize>>,
106}
107
108impl<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> Shtc3<'a, A, I> {
109    pub fn new(i2c: &'a I, buffer: &'static mut [u8], alarm: &'a A) -> Shtc3<'a, A, I> {
110        Shtc3 {
111            i2c,
112            alarm,
113            humidity_client: OptionalCell::empty(),
114            temperature_client: OptionalCell::empty(),
115            state: Cell::new(State::Idle),
116            buffer: TakeCell::new(buffer),
117            temperature_wanted: Cell::new(false),
118            humidity_wanted: Cell::new(false),
119            pending_temperature: Cell::new(None),
120            pending_humidity: Cell::new(None),
121        }
122    }
123
124    fn read_temperature(&self) -> Result<(), ErrorCode> {
125        if self.temperature_wanted.get() {
126            Err(ErrorCode::BUSY)
127        } else if self.state.get() == State::Idle {
128            let result = self.start_measurement();
129            if result.is_ok() {
130                self.temperature_wanted.set(true);
131            }
132            result
133        } else {
134            self.temperature_wanted.set(true);
135            Ok(())
136        }
137    }
138
139    fn read_humidity(&self) -> Result<(), ErrorCode> {
140        if self.humidity_wanted.get() {
141            Err(ErrorCode::BUSY)
142        } else if self.state.get() == State::Idle {
143            let result = self.start_measurement();
144            if result.is_ok() {
145                self.humidity_wanted.set(true);
146            }
147            result
148        } else {
149            self.humidity_wanted.set(true);
150            Ok(())
151        }
152    }
153
154    /// Write a two-byte command out to the sensor, taking `self.buffer`
155    /// to do so.
156    fn write_command(&self, command: u16) -> Result<(), ErrorCode> {
157        self.buffer.take().map_or(Err(ErrorCode::NOMEM), |buffer| {
158            buffer[0] = (command >> 8) as u8;
159            buffer[1] = (command & 0xff) as u8;
160
161            match self.i2c.write(buffer, 2) {
162                Ok(()) => Ok(()),
163                Err((error, data)) => {
164                    self.buffer.replace(data);
165                    Err(error.into())
166                }
167            }
168        })
169    }
170
171    fn read_measurement(&self) -> Result<(), ErrorCode> {
172        self.buffer.take().map_or(Err(ErrorCode::NOMEM), |buffer| {
173            match self.i2c.read(buffer, MEASUREMENT_LEN) {
174                Ok(()) => Ok(()),
175                Err((error, data)) => {
176                    self.buffer.replace(data);
177                    Err(error.into())
178                }
179            }
180        })
181    }
182
183    fn start_measurement(&self) -> Result<(), ErrorCode> {
184        self.i2c.enable();
185        self.state.set(State::Waking);
186
187        let result = self.write_command(command::WAKEUP);
188        if result.is_err() {
189            self.state.set(State::Idle);
190            self.i2c.disable();
191        }
192        result
193    }
194
195    /// Abort the in-progress cycle on an I2C error: disable the bus,
196    /// stash the error for whichever client(s) had a reading pending, and
197    /// return to `Idle` before delivering it (see `deliver_pending`).
198    fn abort(&self, error: ErrorCode) {
199        if self.temperature_wanted.get() {
200            self.pending_temperature.set(Some(Err(error)));
201        }
202        if self.humidity_wanted.get() {
203            self.pending_humidity.set(Some(usize::MAX));
204        }
205
206        self.finish();
207    }
208
209    /// Parse the 6-byte measurement result, checking each value's CRC,
210    /// and stash it in `pending_temperature` and `pending_humidity`.
211    fn store_measurement(&self) {
212        self.buffer.map(|buffer| {
213            let result = if crc8(&buffer[0..2]) == buffer[2] {
214                let raw = ((buffer[0] as u32) << 8) | (buffer[1] as u32);
215                // T[centi-degrees C] = -4500 + 17500 * raw / 65536.
216                Ok(((4375 * raw) >> 14) as i32 - 4500)
217            } else {
218                Err(ErrorCode::FAIL)
219            };
220            self.pending_temperature.set(Some(result));
221
222            let value = if crc8(&buffer[3..5]) == buffer[5] {
223                let raw = ((buffer[3] as u32) << 8) | (buffer[4] as u32);
224                // RH[centi-%] = 10000 * raw / 65536.
225                ((625 * raw) >> 12) as usize
226            } else {
227                usize::MAX
228            };
229            self.pending_humidity.set(Some(value));
230        });
231    }
232
233    /// Deliver whichever of `pending_temperature`/`pending_humidity` had
234    /// a client waiting on it.
235    fn deliver_pending(&self) {
236        if self.temperature_wanted.get() {
237            self.temperature_wanted.set(false);
238            if let Some(result) = self.pending_temperature.take() {
239                self.temperature_client
240                    .map(|client| client.callback(result));
241            }
242        }
243        if self.humidity_wanted.get() {
244            self.humidity_wanted.set(false);
245            if let Some(value) = self.pending_humidity.take() {
246                self.humidity_client.map(|client| client.callback(value));
247            }
248        }
249    }
250
251    /// Disable the I2C bus, return to `Idle`, and deliver whatever
252    /// reading(s) ended up pending.
253    fn finish(&self) {
254        self.i2c.disable();
255        self.state.set(State::Idle);
256        self.deliver_pending();
257    }
258
259    fn continue_state_machine(&self) {
260        match self.state.get() {
261            State::Idle => {}
262            State::Waking => {
263                self.state.set(State::WakeDelay);
264                self.alarm
265                    .set_alarm(self.alarm.now(), self.alarm.ticks_from_ms(WAKE_UP_DELAY_MS));
266            }
267            State::WakeDelay => {
268                let res = self.write_command(command::MEASURE_NORMAL_T_FIRST_NO_STRETCH);
269                if let Err(error) = res {
270                    self.abort(error);
271                } else {
272                    self.state.set(State::Measuring);
273                }
274            }
275            State::Measuring => {
276                self.state.set(State::MeasuringDelay);
277                self.alarm.set_alarm(
278                    self.alarm.now(),
279                    self.alarm.ticks_from_ms(MEASURING_DELAY_MS),
280                );
281            }
282            State::MeasuringDelay => {
283                let res = self.read_measurement();
284                if let Err(error) = res {
285                    self.abort(error);
286                } else {
287                    self.state.set(State::ReadingMeasurement);
288                }
289            }
290            State::ReadingMeasurement => {
291                self.store_measurement();
292
293                let res = self.write_command(command::SLEEP);
294                if let Err(_error) = res {
295                    self.finish();
296                } else {
297                    self.state.set(State::Sleeping);
298                }
299            }
300            State::Sleeping => {
301                self.finish();
302            }
303        }
304    }
305}
306
307impl<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> time::AlarmClient for Shtc3<'a, A, I> {
308    fn alarm(&self) {
309        self.continue_state_machine();
310    }
311}
312
313impl<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> i2c::I2CClient for Shtc3<'a, A, I> {
314    fn command_complete(&self, buffer: &'static mut [u8], status: Result<(), i2c::Error>) {
315        self.buffer.replace(buffer);
316
317        if let Err(error) = status {
318            if self.state.get() == State::Sleeping {
319                // The measurement itself already succeeded and is sitting in
320                // `pending_temperature`/`pending_humidity`; only this best-effort
321                // sleep command failed, which doesn't change what gets delivered.
322                self.finish();
323            } else {
324                self.abort(error.into());
325            }
326        } else {
327            self.continue_state_machine();
328        }
329    }
330}
331
332impl<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> kernel::hil::sensors::TemperatureDriver<'a>
333    for Shtc3<'a, A, I>
334{
335    fn set_client(&self, client: &'a dyn kernel::hil::sensors::TemperatureClient) {
336        self.temperature_client.set(client);
337    }
338
339    fn read_temperature(&self) -> Result<(), ErrorCode> {
340        self.read_temperature()
341    }
342}
343
344impl<'a, A: Alarm<'a>, I: i2c::I2CDevice<'a>> kernel::hil::sensors::HumidityDriver<'a>
345    for Shtc3<'a, A, I>
346{
347    fn set_client(&self, client: &'a dyn kernel::hil::sensors::HumidityClient) {
348        self.humidity_client.set(client);
349    }
350
351    fn read_humidity(&self) -> Result<(), ErrorCode> {
352        self.read_humidity()
353    }
354}