SGP30 driver for Raspberry Pico

Co-authored-by: ZodiusInfuser <christopher.parrott2@gmail.com>
pull/136/head
Simon Reap 2021-03-17 15:53:33 +00:00 zatwierdzone przez Phil Howard
rodzic 3f379d04e7
commit 62e085e81c
14 zmienionych plików z 586 dodań i 0 usunięć

Wyświetl plik

@ -7,3 +7,4 @@ add_subdirectory(rv3028)
add_subdirectory(trackball)
add_subdirectory(vl53l1x)
add_subdirectory(is31fl3731)
add_subdirectory(sgp30)

Wyświetl plik

@ -0,0 +1 @@
include(sgp30.cmake)

Wyświetl plik

@ -0,0 +1,105 @@
# The SGP30 Environmental Sensor
The SGP30 environmental sensor breakout gives CO2 (eCO2) and Volatile Compound (TVOC) readings.
## Code description
This is a C++ library which creates a class to manage the SGP30 breakout. In the calls below, the functions returning a `bool` will return `true` if the request is successful, `false` if something went wrong.
## Instantiation
Create an SGP30 instance with default I2C connection parameters with:
```c++
sgp30 = SGP30();
```
To specify the i2c channel (`i2c0` or `i2c1` on a Pico), the SDA and SCL GPIO pins:
```c++
sgp30 = SGP30(i2c0, 20, 21);
```
## Initialisation
Before using the SGP30, you must initialise it. This sets up the GPIO pins, resets the device, retrieves the unique ID for the chip, and verifies that the required features are supported by the chip.
```c++
bool sgp30.init()
```
## Soft Reset
This clears the chip ready for measurement, It is done as part of the `init()` function, but can be called later if desired. After calling `soft_reset()`, you must issue a new `start_measurement()` call.
```c++
bool sgp30.soft_reset()
```
## Get the chip's Unique ID
This returns the unique ID that was retrieved during initialistion, as a 48-bit value (in a 64-bit integer). You can print this out (in hex) by using the printf format "%012llx".
```c++
uint64_t sgp30.get_unique_id()
```
## Measure air quality
To start the measurement process, call this.
The chip then runs a dynamic baseline compensation algorithm for up to 15 seconds to improve the accuracy of readings.
You call `get_air_quality()` to get the current eCO2 and TVOC values.
While the algortihm is running, (i.e. for those first 15 seconds) it returns default values (eCO2=400, TVOC=0).
The `wait_for_setup` parameter is `false` if you want to return as soon as the command is given - you should then write code to issue `get_air_quality()` once per second for up to 15 seconds.
If true, the function itself issues `get_air_quality()` for up to 15 seconds, until values other than 400 and 0 are returned.
```c++
bool sgp30.start_measurement(bool wait_for_setup)
```
To retrieve the current readings, pass in the address of two 16-bit unsigned integer variables.
remember, these values will be 400 and 0 respectively while the chip is establishing its baseline.
```c++
bool sgp30.get_air_quality(uint16_t * eCO2, uint16_t * TVOC)
```
The above values are modified to make them more accurate, based on chip performance. To get the raw values that the above values are based on, you can use this (though it is really only there for testing the chip)
```c++
bool sgp30.get_air_quality_raw(uint16_t * rawH2, uint16_t * rawEthanol)
```
## Manage baseline readings
The initial initialisation process sets baseline values. To allow the baseline to be reached more quickly on subsequent start-ups, you can copy out the current baseline values and store them safely. Later, after a reset or restart, you can write those baseline values back.
```c++
bool sgp30.get_baseline(uint16_t *eco2, uint16_t *tvoc)
void sgp30.set_baseline(uint16_t eco2, uint16_t tvoc)
```
## Manage humidity
The CO2 and Volatile Compounds values can be affected by the humidity around the chip. To allow for this, the absolute humidy (for example, as returned by the SHTXX chip series) can be reported to the chip. To reset the humidity to the default, use 0 (zero) as the parameter.
```c++
bool sgp30.set_humidity(uint32_t absolute_humidity)
```
## Private functions
The following functions are used within the SGP30 class to retrieve information from and send commands to the chip. The crc calculation functions generate a chip-specific CRC value from 2 8-bit or 1 16-bit values.
```c++
bool retrieve_unique_id()
bool read_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t * value)
bool read_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2)
bool read_reg_3_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2, uint16_t * value3)
bool write_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t value)
bool write_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t value1, uint16_t value2)
bool write_reg(uint16_t reg, uint16_t delayms)
uint8_t calculate_crc(uint16_t value)
uint8_t calculate_crc(uint8_t value1, uint8_t value2)
```

Wyświetl plik

@ -0,0 +1,10 @@
set(DRIVER_NAME sgp30)
add_library(${DRIVER_NAME} INTERFACE)
target_sources(${DRIVER_NAME} INTERFACE
${CMAKE_CURRENT_LIST_DIR}/${DRIVER_NAME}.cpp)
target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR})
# Pull in pico libraries that we need
target_link_libraries(${DRIVER_NAME} INTERFACE pico_stdlib hardware_i2c)

Wyświetl plik

@ -0,0 +1,245 @@
/******************************************************************************
sgp30.cpp
Code based on "Sensirion_Gas_Sensors_Datasheet_SGP30.pdf" on sensirion.com.
Code written by Simon Reap, March 17, 2021
https://github.com/simon3270/pico-pimoroni
This code is released under the [MIT License](http://opensource.org/licenses/MIT).
Please review the LICENSE file included with this example.
Distributed as-is; no warranty is given.
******************************************************************************/
#include "sgp30.hpp"
namespace pimoroni {
/***** Device registers and masks here *****/
bool SGP30::init() {
i2c_init(i2c, 400000);
gpio_set_function(sda, GPIO_FUNC_I2C);
gpio_pull_up(sda);
gpio_set_function(scl, GPIO_FUNC_I2C);
gpio_pull_up(scl);
soft_reset();
if (!retrieve_unique_id()) {
return false;
}
// Retrieve and check Feature Set
uint16_t featureset;
if (!read_reg_1_word(GET_FEATURE_SET_VERSION, 10, &featureset))
return false;
if ((featureset & 0xF0) != SGP30_REQ_FEATURES)
return false;
// Start the measurement process
// - parameter true = wait for readings to initialise
// false = return immediately
// As usual, function returns true if the request succeeded
// if (!start_measurement(true))
// return false;
return true;
}
// Get the unique ID from the Chip. Will fail if no chip attached
bool SGP30::retrieve_unique_id() {
// return the Chip ID, in three separate 16-bit values
return read_reg_3_words(GET_SERIAL_ID, 10,
serial_number, serial_number+1, serial_number+2);
}
// get the previously-retreved Chip ID as the lower 48 bits of a 64-bit uint
uint64_t SGP30::get_unique_id() {
return (((uint64_t)serial_number[0]) << 32) \
+ (((uint64_t)serial_number[1]) << 16) \
+ serial_number[2];
}
// Write a soft reset - writes globally, so all devices on this
// I2C bus receive the request
bool SGP30::soft_reset() {
return write_global(SOFT_RESET, 10);
}
// Start the measurement process.
// If the parameter is true, wait for the readings to be valid
// If false, return immediately
bool SGP30::start_measurement(bool wait_for_setup) {
// First kick off the "measurement" phase
bool rc = write_reg(INIT_AIR_QUALITY, 10);
// Optionally wait up to 20 seconds for the measurement process to initiate
if (wait_for_setup) {
// It takes 15 seconds to start the measurement process but allow 20.
// Ignore the first 2 readings completely.
uint16_t eCO2, TVOC;
uint8_t sec_count = 0;
while (sec_count < 20) {
sleep_ms(988); // Will sleep last 12ms of 1sec in get_air_quality()
get_air_quality(&eCO2, &TVOC);
if ((sec_count >= 2) && (eCO2 != 400 || TVOC != 0)) {
// startup process finished
break;
}
sec_count++;
}
}
return rc;
}
// get the air quality values - will be 400 and 0 respectively for the
// first 15 seconds after starting measurement
bool SGP30::get_air_quality(uint16_t * eCO2, uint16_t * TVOC) {
return read_reg_2_words(MEASURE_AIR_QUALITY, 12, eCO2, TVOC);
}
// Get the raw readings - not useful in real-world settings
bool SGP30::get_air_quality_raw(uint16_t * rawH2, uint16_t * rawEthanol) {
return read_reg_2_words(MEASURE_RAW_SIGNALS, 25, rawH2, rawEthanol);
}
// Get the baseline compensation values for eCO2 and VOC
bool SGP30::get_baseline(uint16_t *eco2, uint16_t *tvoc) {
return read_reg_2_words(GET_BASELINE, 10, eco2, tvoc);
}
// Write the baseline compensation values for eCO2 and VOC
void SGP30::set_baseline(uint16_t eco2, uint16_t tvoc) {
write_reg_2_words(SET_BASELINE, 10, eco2, tvoc);
}
// Set the absolute humidity, e.g. from an SHTxx chip
bool SGP30::set_humidity(uint32_t absolute_humidity) {
if (absolute_humidity > 256000) {
return false;
}
uint16_t ah_scaled =
(uint16_t)(((uint64_t)absolute_humidity * 256 * 16777) >> 24);
return write_reg_1_word(SET_HUMIDITY, 10, ah_scaled);
}
// Write a single byte globally (not to a specifc I2c address)
bool SGP30::write_global(uint16_t reg, uint16_t delayms)
{
uint8_t buffer[1] = { (uint8_t)(reg & 0xFF)};
i2c_write_blocking(i2c, 0, buffer, 1, false);
sleep_ms(delayms);
return true;
}
// Write just the register to the i2c address, no parameter
bool SGP30::write_reg(uint16_t reg, uint16_t delayms)
{
uint8_t buffer[2] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF)};
i2c_write_blocking(i2c, address, buffer, 2, false);
sleep_ms(delayms);
return true;
}
// Write one 16-bit word (+CRC)
bool SGP30::write_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t value) {
uint8_t buffer[5] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF),
(uint8_t)((value >> 8) & 0xFF), (uint8_t)(value & 0xFF), calculate_crc(value)};
i2c_write_blocking(i2c, address, buffer, 5, false);
sleep_ms(delayms);
return true;
}
// Write two 16-bit words (+CRC)
bool SGP30::write_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t value1, uint16_t value2) {
uint8_t buffer[8] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF),
(uint8_t)((value1 >> 8) & 0xFF), (uint8_t)(value1 & 0xFF), calculate_crc(value1),
(uint8_t)((value2 >> 8) & 0xFF), (uint8_t)(value2 & 0xFF), calculate_crc(value2)};
i2c_write_blocking(i2c, address, buffer, 8, false);
sleep_ms(delayms);
return true;
}
// Write register and read one 16-bit word in response
bool SGP30::read_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t * value) {
uint8_t regbuf[2] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF) };
uint8_t buffer[3];
i2c_write_blocking(i2c, address, regbuf, 2, true);
sleep_ms(delayms);
i2c_read_blocking(i2c, address, buffer, 3, false);
if (buffer[2] != calculate_crc(buffer[0], buffer[1]))
return false;
*value = (buffer[0] << 8) + buffer[1];
return true;
}
// Write register and read two 16-bit words
bool SGP30::read_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2) {
uint8_t regbuf[2] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF) };
uint8_t buffer[6];
i2c_write_blocking(i2c, address, regbuf, 2, true);
sleep_ms(delayms);
i2c_read_blocking(i2c, address, buffer, 6, false);
if ((buffer[2] != calculate_crc(buffer[0], buffer[1])) || (buffer[5] != calculate_crc(buffer[3], buffer[4]))) {
return false;
}
*value1 = (buffer[0] << 8) + buffer[1];
*value2 = (buffer[3] << 8) + buffer[4];
return true;
}
// Write register and read three 16-bit words
bool SGP30::read_reg_3_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2, uint16_t * value3) {
uint8_t regbuf[2] = { (uint8_t)((reg >> 8) & 0xFF), (uint8_t)(reg & 0xFF) };
uint8_t buffer[9];
i2c_write_blocking(i2c, address, regbuf, 2, true);
sleep_ms(delayms);
i2c_read_blocking(i2c, address, buffer, 9, false);
if (buffer[2] != calculate_crc(buffer[0], buffer[1])) {
return false;
}
if (buffer[5] != calculate_crc(buffer[3], buffer[4])) {
return false;
}
if (buffer[8] != calculate_crc(buffer[6], buffer[7])) {
return false;
}
*value1 = (buffer[0] << 8) + buffer[1];
*value2 = (buffer[3] << 8) + buffer[4];
*value3 = (buffer[6] << 8) + buffer[7];
return true;
}
// calculate the CRC for a single 16-bit word
uint8_t SGP30::calculate_crc(uint16_t value) {
return calculate_crc((value >> 8) && 0xFF, value & 0xFF);
}
// calculate the CRC for two 8-bit bytes
uint8_t SGP30::calculate_crc(uint8_t value1, uint8_t value2) {
// calculates 8-Bit checksum with given polynomial
uint8_t crc = SGP30_CRC_BASE;
crc ^= value1;
for (uint8_t b = 0; b < 8; b++) {
if (crc & 0x80)
crc = (crc << 1) ^ SGP30_CRC_SEED;
else
crc <<= 1;
}
crc ^= value2;
for (uint8_t b = 0; b < 8; b++) {
if (crc & 0x80)
crc = (crc << 1) ^ SGP30_CRC_SEED;
else
crc <<= 1;
}
return crc;
}
}

Wyświetl plik

@ -0,0 +1,114 @@
#pragma once
#include "hardware/i2c.h"
#include "hardware/gpio.h"
// commands and constants
#define SGP30_REQ_FEATURES 0x0020 // The required feature set
#define SGP30_CRC_SEED 0x31 // CRC Seed
#define SGP30_CRC_BASE 0xFF // Base value for calculating CRC
namespace pimoroni {
class SGP30 {
//--------------------------------------------------
// Constants
//--------------------------------------------------
public:
static const uint8_t DEFAULT_I2C_ADDRESS = 0x58;
static const uint8_t DEFAULT_SDA_PIN = 20;
static const uint8_t DEFAULT_SCL_PIN = 21;
/***** More public constants here *****/
private:
/***** Private constants here *****/
const uint16_t SOFT_RESET = 0x0006;
const uint16_t INIT_AIR_QUALITY = 0x2003;
const uint16_t MEASURE_AIR_QUALITY = 0x2008;
const uint16_t GET_BASELINE = 0x2015;
const uint16_t SET_BASELINE = 0x201e;
const uint16_t SET_HUMIDITY = 0x2061;
const uint16_t MEASURE_TEST = 0x2032;
const uint16_t GET_FEATURE_SET_VERSION = 0x202f;
const uint16_t MEASURE_RAW_SIGNALS = 0x2050;
const uint16_t GET_SERIAL_ID = 0x3682;
//--------------------------------------------------
// Variables
//--------------------------------------------------
private:
i2c_inst_t *i2c = i2c0;
// interface pins with our standard defaults where appropriate
int8_t address = DEFAULT_I2C_ADDRESS;
int8_t sda = DEFAULT_SDA_PIN;
int8_t scl = DEFAULT_SCL_PIN;
/***** More variables here *****/
//--------------------------------------------------
// Constructors/Destructor
//--------------------------------------------------
public:
SGP30() {}
SGP30(i2c_inst_t *i2c, uint8_t sda, uint8_t scl) :
i2c(i2c), sda(sda), scl(scl) {}
//--------------------------------------------------
// Methods
//--------------------------------------------------
public:
bool init(); //This should be present in all drivers
// uint8_t command(uint8_t command_name, uint8_t parameters/*=None*/);
// Get the unique ID from the chip and store locally
bool retrieve_unique_id();
// Return the 48-bit unique ID to the caller
uint64_t get_unique_id();
bool start_measurement(bool wait_for_setup);
bool get_air_quality(uint16_t * eCO2, uint16_t * TVOC);
bool get_air_quality_raw(uint16_t * rawH2, uint16_t * rawEthanol);
bool soft_reset();
bool get_baseline(uint16_t *eco2, uint16_t *tvoc);
void set_baseline(uint16_t eco2, uint16_t tvoc);
bool set_humidity(uint32_t absolute_humidity);
// The most recent Total Volatile Organic Compounds in ppb and equivalent CO2 in ppm
// uint16_t TVOC;
// uint16_t eCO2;
// The raw TVOC and eCO2 values - only really for chip testing
// uint16_t rawTVOC;
// uint16_t rawEthanol;
// The 48-bit unique serial number for this chip, retrieved on init
uint16_t serial_number[3];
private:
/***** Private methods here *****/
bool write_global(uint16_t reg, uint16_t delayms);
bool write_reg(uint16_t reg, uint16_t delayms);
bool write_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t value);
bool write_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t value1, uint16_t value2);
bool read_reg_1_word(uint16_t reg, uint16_t delayms, uint16_t * value);
bool read_reg_2_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2);
bool read_reg_3_words(uint16_t reg, uint16_t delayms, uint16_t * value1, uint16_t * value2, uint16_t * value3);
// bool readWordFromCommand(uint8_t command[], uint8_t commandLength,
// uint16_t delay, uint16_t *readdata = NULL,
// uint8_t readlen = 0);
// uint8_t generateCRC(uint8_t data[], uint8_t datalen);
//
uint8_t calculate_crc(uint16_t value);
uint8_t calculate_crc(uint8_t value1, uint8_t value2);
};
}

Wyświetl plik

@ -4,6 +4,7 @@ add_subdirectory(breakout_roundlcd)
add_subdirectory(breakout_rgbmatrix5x5)
add_subdirectory(breakout_matrix11x7)
add_subdirectory(breakout_trackball)
add_subdirectory(breakout_sgp30)
add_subdirectory(pico_display)
add_subdirectory(pico_unicorn)
add_subdirectory(pico_unicorn_plasma)

Wyświetl plik

@ -0,0 +1,12 @@
set(OUTPUT_NAME sgp30_demo)
add_executable(
${OUTPUT_NAME}
demo.cpp
)
# Pull in pico libraries that we need
target_link_libraries(${OUTPUT_NAME} pico_stdlib breakout_sgp30)
# create map/bin/hex file etc.
pico_add_extra_outputs(${OUTPUT_NAME})

Wyświetl plik

@ -0,0 +1,71 @@
// SGP30 Air Quality Sensor demo
// Initalises connection, retrieves unique chip ID, starts measurement.
// Call returns immediately so that air quality can be measured every second.
// After 30 seconds, resets the chip, and restarts measurement,
// but that call waits up to 20 seconds for the readings to start being useful.
// Reports status and results to Serial connection on GPIO 0 and 1.
#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "breakout_sgp30.hpp"
using namespace pimoroni;
BreakoutSGP30 sgp30;
int main() {
uint8_t prd;
uint16_t eCO2, TVOC;
uint16_t raweCO2, rawTVOC;
uint16_t baseCO2, baseTVOC;
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
stdio_init_all();
bool init_ok = sgp30.init();
if (init_ok) {
printf("SGP30 initialised - about to start measuring without waiting\n");
sgp30.start_measurement(false);
printf("Started measuring for id %012llx\n", sgp30.get_unique_id());
if (sgp30.get_air_quality(&eCO2, &TVOC)) {
prd = 1;
} else {
printf("SGP30 not found when reading air quality\n");
prd = 2;
}
} else {
printf("SGP30 not found when initialising\n");
prd = 3;
}
uint16_t j = 0;
while(true) {
j++;
for (uint8_t i=0; i<prd; i++) {
gpio_put(PICO_DEFAULT_LED_PIN, true);
sleep_ms(50);
gpio_put(PICO_DEFAULT_LED_PIN, false);
sleep_ms(50);
}
sleep_ms(1000-(100*prd));
if (prd == 1) {
sgp30.get_air_quality(&eCO2, &TVOC);
sgp30.get_air_quality_raw(&raweCO2, &rawTVOC);
printf("%3d: CO2 %d TVOC %d, raw %d %d\n", j, eCO2, TVOC, raweCO2, rawTVOC);
if (j == 30) {
printf("Resetting device\n");
sgp30.soft_reset();
sleep_ms(500);
printf("Restarting measurement, waiting 15 secs before returning\n");
sgp30.start_measurement(true);
printf("Measurement restarted, now read every second\n");
}
}
}
return 0;
}

Wyświetl plik

@ -4,6 +4,7 @@ add_subdirectory(breakout_roundlcd)
add_subdirectory(breakout_rgbmatrix5x5)
add_subdirectory(breakout_matrix11x7)
add_subdirectory(breakout_trackball)
add_subdirectory(breakout_sgp30)
add_subdirectory(pico_graphics)
add_subdirectory(pico_display)
add_subdirectory(pico_unicorn)

Wyświetl plik

@ -0,0 +1 @@
include(breakout_sgp30.cmake)

Wyświetl plik

@ -0,0 +1,11 @@
set(LIB_NAME breakout_sgp30)
add_library(${LIB_NAME} INTERFACE)
target_sources(${LIB_NAME} INTERFACE
${CMAKE_CURRENT_LIST_DIR}/${LIB_NAME}.cpp
)
target_include_directories(${LIB_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR})
# Pull in pico libraries that we need
target_link_libraries(${LIB_NAME} INTERFACE pico_stdlib hardware_i2c sgp30)

Wyświetl plik

@ -0,0 +1,5 @@
#include "breakout_sgp30.hpp"
namespace pimoroni {
}

Wyświetl plik

@ -0,0 +1,8 @@
#pragma once
#include "../../drivers/sgp30/sgp30.hpp"
namespace pimoroni {
typedef SGP30 BreakoutSGP30;
}