ccs811_rs/lib.rs
1//! This is a platform agnostic Rust driver for the CCS881 indoor air quality
2//! sensor, based on the [`embedded-hal blocking i2c`] traits.
3//!
4//! [`embedded-hal blocking i2c`]: https://github.com/rust-embedded/embedded-hal
5//!
6//! This driver allows you to:
7//! - Start App mode.
8//! - Read all info available [eCO2, eTVOC, Status, ErrorId, RawData].
9//! - Reset device.
10//!
11//! ## The device
12//!
13//! The CCS811 The CCS811 is an ultra-low power digital gas sensor solution
14//! which integrates a metal oxide (MOX) gas sensor to detect a
15//! wide range of Volatile Organic Compounds (VOCs) for indoor
16//! air quality monitoring with a microcontroller unit (MCU), which
17//! includes an Analog-to-Digital converter (ADC), and an I²C
18//! interface.
19//!
20//! Datasheet:
21//! - [ccs811](https://ams.com/documents/20143/36005/CCS811_DS000459_7-00.pdf/3cfdaea5-b602-fe28-1a14-18776b61a35a)
22//!
23//!
24//! ## Usage examples (see also examples folder)
25//!
26//! To use this driver, import this crate and an `embedded-hal` implementation,
27//! then instantiate the device.
28
29#![deny(missing_docs, unsafe_code, warnings)]
30#![no_std]
31
32extern crate embedded_hal as hal;
33
34use hal::blocking::i2c::{Read, Write, WriteRead};
35
36/// All possible errors in this crate
37#[derive(Debug)]
38pub enum Error<E> {
39 /// I²C bus error
40 I2C(E),
41 /// Invalid input data
42 InvalidInputData,
43}
44
45/// Measurement Mode
46#[derive(Debug, Clone, Copy, PartialEq)]
47#[repr(u8)]
48pub enum DriveMode {
49 /// Idle Mode
50 DriveMode0Idle = 0b0u8,
51 /// Mode 1 : 1 read per second
52 DriveMode1Sec = 0b1_0000u8,
53 /// Mode 2 : read each 10 seconds
54 DriveMode10Sec = 0b10_0000u8,
55 /// Mode 3 : read each 60 seconds
56 DriveMode60Sec = 0b11_0000u8,
57 /// Mode 4 : read every 250 ms, expose only raw data
58 DriveModeRawData = 0b100_0000u8,
59}
60
61/// Interrupt Mode
62#[derive(Debug, Clone, Copy, PartialEq)]
63#[repr(u8)]
64pub enum InterruptDataReady {
65 /// Assert nINT when data is available in ALG_RESULT_DATA
66 Enabled = 0b1000u8,
67 /// Interrupt generation is disabled
68 Disabled = 0b0u8,
69}
70
71/// Interrupt Mode
72#[derive(Debug, Clone, Copy, PartialEq)]
73#[repr(u8)]
74pub enum InterruptThreshold {
75 /// Assert nINT if ALG_RESULT_DATA crosses thresholds
76 Enabled = 0b100u8,
77 /// Interrupt mode, if asserted, operates normally
78 Disabled = 0x0,
79}
80
81/// ErrorId Codes
82#[derive(Debug, Clone, Copy, PartialEq)]
83#[repr(u8)]
84pub enum ErrorIdCodes {
85 /// WRITE_REG_INVALID
86 ///
87 /// "The CCS811 received an I2C write request addressed to this station but with
88 /// invalid register address ID"
89 WriteRegInvalid = 0b0u8,
90 /// READ_REG_INVALID
91 ///
92 /// "The CCS811 received an I2C read request to a mailbox ID that is invalid"
93 ReadRegInvalid = 0b10u8,
94 /// MEASMODE_INVALID
95 ///
96 /// "The CCS811 received an I2C request to write an unsupported mode to
97 /// MEAS_MODE"
98 MeasModeInvalid = 0b100u8,
99 /// MAX_RESISTANCE
100 ///
101 /// "The sensor resistance measurement has reached or exceeded the maximum
102 /// range"
103 MaxResistance = 0b1000u8,
104 /// HEATER_FAULT
105 ///
106 /// "The Heater current in the CCS811 is not in range"
107 HeaterFault = 0b1_0000u8,
108 /// HEATER_SUPPLY
109 ///
110 /// "The Heater voltage is not being applied correctly"
111 HeaterSupply = 0b10_0000u8,
112}
113
114/// Boot mode Register addresses
115/// Taken from the CCS811 data sheet (Figure 25, p.26)
116#[derive(Copy, Clone, Debug, Eq, PartialEq)]
117#[repr(u8)]
118pub enum BootRegister {
119 /// Status (RO)
120 ///
121 /// "Status register"
122 Status = 0x00,
123 /// HW_ID (RO)
124 ///
125 /// "Hardware ID. The value is 0x81"
126 HwId = 0x20,
127 /// HW Version (RO)
128 ///
129 /// "Hardware Version. The value is 0x1X"
130 HwVersion = 0x21,
131 /// FW_Boot_Version (RO)
132 ///
133 /// "Firmware Boot Version. The first 2 bytes contain the firmware version number for the boot code."
134 FwBootVersion = 0x23,
135 /// FW_App_Version (RO)
136 ///
137 /// "Firmware Application Version. The first 2 bytes contain the firmware version number for the application code"
138 FwAppVersion = 0x24,
139 /// Error Id
140 ///
141 /// "Error ID. When the status register reports an error its source is located in this register"
142 ErrorId = 0xE0,
143 /// APP_START (WO)
144 ///
145 /// "Status register"
146 AppStart = 0xF4,
147 /// SW_RESET
148 ///
149 /// "If the correct 4 bytes (0x11 0xE5 0x72 0x8A) are written to this register in a single
150 /// sequence the device will reset and return to BOOT mode."
151 SwReset = 0xFF,
152}
153
154/// Application mode Register addresses
155/// Taken from the CCS811 data sheet (Figure 14, p.17)
156#[derive(Copy, Clone, Debug, Eq, PartialEq)]
157#[repr(u8)]
158pub enum AppRegister {
159 /// Status (RO)
160 ///
161 /// "Status register"
162 Status = 0x00,
163 /// Measurement Mode (RW)
164 ///
165 /// "Measurement mode and conditions register"
166 MeasMode = 0x01,
167 /// ALG_RESULT_DATA (RO)
168 ///
169 /// "Algorithm result. The most significant 2 bytes contain a
170 /// ppm estimate of the equivalent CO 2 (eCO 2 ) level, and
171 /// the next two bytes contain a ppb estimate of the total
172 /// VOC level."
173 AlgResultData = 0x02,
174 /// RAW_DATA (RO)
175 ///
176 /// "Raw ADC data values for resistance and current source used."
177 RawData = 0x03,
178 /// ENV_DATA (WO)
179 ///
180 /// "Temperature and humidity data can be written to enable compensation"
181 EnvData = 0x05,
182 ///THRESHOLDS (WO)
183 ///
184 /// "Thresholds for operation when interrupts are only generated when eCO 2 ppm crosses a threshold"
185 Thresholds = 0x10,
186 /// BASELINE (R/W)
187 ///
188 /// "The encoded current baseline value can be read. A previously saved encoded baseline can be written."
189 Baseline = 0x11,
190 /// HW_ID (RO)
191 ///
192 /// "Hardware ID. The value is 0x81"
193 HwId = 0x20,
194 /// HW Version (RO)
195 ///
196 /// "Hardware Version. The value is 0x1X"
197 HwVersion = 0x21,
198 /// FW_Boot_Version (RO)
199 ///
200 /// "Firmware Boot Version. The first 2 bytes contain the firmware version number for the boot code."
201 FwBootVersion = 0x23,
202 /// FW_App_Version (RO)
203 ///
204 /// "Firmware Application Version. The first 2 bytes contain the firmware version number for the application code"
205 FwAppVersion = 0x24,
206 /// Internal_State (RO)
207 ///
208 /// "Internal Status register"
209 InternalStatus = 0xA0,
210 /// Error Id
211 ///
212 /// "Error ID. When the status register reports an error its source is located in this register"
213 ErrorId = 0xE0,
214 /// SW_RESET
215 ///
216 /// "If the correct 4 bytes (0x11 0xE5 0x72 0x8A) are written to this register in a single
217 /// sequence the device will reset and return to BOOT mode."
218 SwReset = 0xFF,
219}
220
221/// Result
222#[derive(Debug, Default)]
223pub struct SensorData {
224 /// eCO
225 pub e_co: u16,
226 /// eTVoc
227 pub e_tvoc: u16,
228 /// Status
229 pub status: u8,
230 /// Error Id
231 pub error_id: u8,
232 /// Raw data
233 pub raw: [u8; 2],
234}
235
236/// Ccs811 device driver.
237#[derive(Debug, Default)]
238pub struct Ccs811<I2C> {
239 /// The concrete I²C device implementation.
240 i2c: I2C,
241 /// i2c address
242 address: u8,
243}
244
245impl<I2C, E> Ccs811<I2C>
246where
247 I2C: Write<Error = E> + WriteRead<Error = E> + Read<Error = E>,
248{
249 /// Create new instance of the Ccs811 device.
250 pub fn new(i2c: I2C, address: u8) -> Self {
251 Ccs811 { i2c, address }
252 }
253
254 /// Destroy driver instance, return I²C bus instance.
255 pub fn destroy(self) -> I2C {
256 self.i2c
257 }
258
259 /// Enter App mode
260 pub fn app_start(&mut self) -> Result<(), E> {
261 self.i2c
262 .write(self.address, &[BootRegister::AppStart as u8])
263 }
264
265 /// Set MEAS_MODE Register
266 pub fn set_meas_mode(
267 &mut self,
268 drive_mode: DriveMode,
269 interrupt: InterruptDataReady,
270 threshold: InterruptThreshold,
271 ) -> Result<(), E> {
272 let mode: u8 = drive_mode as u8 | interrupt as u8 | threshold as u8;
273 self.i2c
274 .write(self.address, &[AppRegister::MeasMode as u8, mode])
275 }
276
277 /// Get MEAS_MODE Register
278 pub fn get_meas_mode(&mut self, data: &mut [u8; 1]) -> Result<(), E> {
279 self.i2c
280 .write_read(self.address, &[AppRegister::MeasMode as u8], data)
281 }
282
283 /// Perform a SwReset, which brings firmware in Boot Mode.
284 pub fn reset(&mut self) -> Result<(), E> {
285 self.i2c.write(
286 self.address,
287 &[AppRegister::SwReset as u8, 0x11, 0xe5, 0x72, 0x8a],
288 )
289 }
290
291 /// Returns [HwId;HwVersion]
292 pub fn hw_info(&mut self) -> Result<[u8; 2], E> {
293 let mut hw = [0u8; 2];
294 self.i2c
295 .write_read(self.address, &[AppRegister::HwId as u8], &mut hw[..1])?;
296 self.i2c
297 .write_read(self.address, &[AppRegister::HwVersion as u8], &mut hw[1..])?;
298 Ok(hw)
299 }
300
301 /// Returns [FwBootVersion;FwAppVersion]
302 pub fn fw_info(&mut self) -> Result<[u8; 4], E> {
303 let mut fw = [0u8; 4];
304 self.i2c.write_read(
305 self.address,
306 &[AppRegister::FwBootVersion as u8],
307 &mut fw[0..2],
308 )?;
309 self.i2c.write_read(
310 self.address,
311 &[AppRegister::FwAppVersion as u8],
312 &mut fw[2..],
313 )?;
314 Ok(fw)
315 }
316
317 /// Returns RAW_DATA
318 pub fn raw_data(&mut self) -> Result<[u8; 2], E> {
319 let mut data = [0u8; 2];
320 self.i2c
321 .write_read(self.address, &[AppRegister::RawData as u8], &mut data)?;
322 Ok(data)
323 }
324
325 /// Set a previosuly retrieved baseline
326 ///
327 /// "A previously stored value may be written back to this two byte
328 /// register and the Algorithms will use the new value in its
329 /// calculations (until it adjusts it as part of its internal Automatic
330 /// Baseline Correction). For more information, refer to ams
331 /// application note AN000370: CCS811 Clean Air Baseline Save and
332 /// Restore."
333 pub fn set_baseline(&mut self, baseline: [u8; 2]) -> Result<(), E> {
334 let data: [u8; 3] = [AppRegister::Baseline as u8, baseline[0], baseline[1]];
335 self.i2c.write(self.address, &data)
336 }
337
338 /// Retrieves Baseline
339 pub fn get_baseline(&mut self, baseline: &mut [u8; 2]) -> Result<(), E> {
340 self.i2c
341 .write_read(self.address, &[AppRegister::Baseline as u8], baseline)
342 }
343
344 /// Retrieves Status register
345 pub fn get_status(&mut self, status: &mut [u8; 1]) -> Result<(), E> {
346 self.i2c
347 .write_read(self.address, &[AppRegister::Status as u8], status)
348 }
349
350 /// Retrieves Error_Id register
351 pub fn get_error_id(&mut self) -> Result<u8, E> {
352 let mut data = [0u8; 1];
353 self.i2c
354 .write_read(self.address, &[AppRegister::ErrorId as u8], &mut data)?;
355 Ok(data[0])
356 }
357
358 /// Get result
359 pub fn get_results(&mut self) -> Result<SensorData, E> {
360 let mut data: [u8; 8] = [0u8; 8];
361 self.i2c
362 .write_read(self.address, &[AppRegister::AlgResultData as u8], &mut data)?;
363
364 let mut ret: SensorData = SensorData::default();
365 ret.e_co = (u16::from(data[0]) << 8) + u16::from(data[1]);
366 ret.e_tvoc = (u16::from(data[2]) << 8) + u16::from(data[3]);
367 ret.status = data[4];
368 ret.error_id = data[5];
369 ret.raw[0] = data[6];
370 ret.raw[1] = data[7];
371
372 Ok(ret)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 #[test]
379 fn it_works() {
380 assert_eq!(2 + 2, 4);
381 }
382}