Measuring air quality with uLisp


The Sensirion SGP30 Air Quality Sensor measures eCO2 (CO2 equivalent) and TVOC (Total Volatile Organic Compounds), giving a measure of the overall air quality. It’s available on an SGP30 Air Quality Sensor Breakout from Adafruit with logic-level conversion to allow it to be used with either 3.3V or 5V systems:

It will measure eCO2 (equivalent calculated carbon-dioxide) concentration within a range of 400 to 60,000 parts per million (ppm), and TVOC (Total Volatile Organic Compound) concentration within a range of 0 to 60,000 parts per billion (ppb).

The I2C address is #x58.


The SGP30 routines use two utility functions. The function readbytes reads a specified number of bytes from the stream s and returns them as a list:

(defun readbytes (s n)
  (let (lst) (dotimes (i n (reverse lst)) (push (read-byte s) lst))))

The function crc8 performs a CRC checksum on a list of bytes and returns the CRC:

(defun crc8 (data)
  (let ((crc #xff))
    (mapc #'(lambda (byte)
              (setq crc (logxor crc byte))
              (dotimes (i 8)
                (if (logbitp 7 crc)
                    (setq crc (logxor (ash crc 1) #x31))
                    (setq crc (ash crc 1)))))
    (logand crc #xff)))

Testing the sensor

The SGP30 provides a self-test function that runs an on-chip self-test:

(defun sgp30-test ()
  (with-i2c (s #x58)
    (write-byte #x20 s)
    (write-byte #x32 s))
  (delay 220)
  (with-i2c (s #x58 3)
    (zerop (crc8 (readbytes s 3)))))

The function returns t if the test succeeds.

Initialising the sensor

To start the air quality measurement you need to call the initialisation function:

(defun sgp30-init ()
  (with-i2c (s #x58)
    (write-byte #x20 s)
    (write-byte #x03 s)))

For the first 15s after the sgp30-init command the sensor is in an initialization phase during which an sgp30-measure command will return fixed values of 400 ppm eCO2 and 0 ppb TVOC.

Measuring air quality

To measure the air quality give the sgp30-measure command:

(defun sgp30-measure ()
  (with-i2c (s #x58)
    (write-byte #x20 s)
    (write-byte #x08 s))
  (delay 12)
  (with-i2c (s #x58 6)
    #| data is (co2h co2l crc tvoch tvocl crc) |#
    (let ((data (readbytes s 6)))
      (when (crc8 data)
        (list (logior (ash (nth 0 data) 8) (nth 1 data))
              (logior (ash (nth 3 data) 8) (nth 4 data)))))))

This returns a list of two values: the eCO2 in ppm and the TVOC in ppb. For example:

> (sgp30-measure)
(415 16)

If you breathe on the sensor you will see both values increase.

Running continuous measurements

After the sgp30-init command, an sgp30-measure command should be sent in regular intervals of 1s to ensure proper operation of the dynamic baseline compensation algorithm. This is implemented by the function sgp30-run, which prints out a pair of measurements every second:

(defun sgp30-run ()
   (print (sgp30-measure))
   (delay 1000)))

Alternatively you could plot these values to provide a graphical display of air quality.