No artigo de hoje vamos estudar sobre dois sensores ambientais, veremos como medir eCO2 e TVOC. Ambas são formas de qualificar o ar no ambiente à nossa volta, através da medição de nível de gases presentes na atmosfera.
Falaremos do sensor CCS811 que mede eCO2, uma “vertente” ou “derivação” da medida do gás CO2. Falaremos também do sensor AGS10 que mede TVOC, “total volatile organic compounds”, no Português “compostos orgânicos voláteis totais”. A unidade de medida do eCO2 do CCS811 é ppm (parte por milhão), enquano do AGS10 é ppb (parte por bilhão).
Teremos ainda no circuito uma medição de temperatura e umidade, visto que o sensor CCS811 faz leituras mais precisas quando estas variáveis ambientais estão bem definidas. Então vamos ler um sensor AHT21 e alimentar o CCS811 com estas leituras.
Neste experimento utilizaremos um microcontrolador Xiao ESP32-C3 da SeeedStudio, no caso de você querer enviar dados via protocolo MQTT para a internet. Para isto você precisa criar uma conta na Adafruit, veja tudo aqui neste artigo à parte.
Neste caso pode ser qualquer modelo de ESP32, todos tem Wi-Fi. Caso queira apenas aplicação local você pode até usar um Arduino UNO ou similar, te mostro o código para isto logo abaixo.
Definindo eCO2 e TVOC
Um dos sensores que vamos ler é o CCS811, que nos entrega um valor de eCO2. Segundo este artigo oficial do governo Brasileiro, eCO2 seria o equivalente em dióxido de carbono às emissões atmosféricas que outros gases (sem ser o próprio CO2) realizam.
Já neste outro link há um comentário sobre a maior utilidade do valor de eCO2 frente ao próprio CO2, visto que o dióxido de carbono não é o único responsável pelo (por exemplo) aquecimento global. Já o sensor AGS10 nos entrega um valor de TVOC, que pode ser definido como um grupo de substâncias voláteis que podem ser prejudiciais à nossa saúde.
Elementos como formaldeídos, Benzeno, Tolueno e Etileno-glicol, à depender da quantidade/concentração podem nos prejudicar. Os sensores que nos entregam valores de TVOC portanto tem papel essencial até na segurança das pessoas em ambientes fechados.
A ideia do projeto
O que vamos fazer aqui hoje vai girar em torno de ler os valores de eCO2 do CCS811, TVOC do AGS10 e temperatura + umidade do AHT21. Tudo isto será feito pelo microcontrolador Xiao ESP32-C3. Aí eu vou te mostrar um código bem enxuto e simples: ler todos os sensores a cada cinco (5) segundos e envia-los ao computador.
Aí você vai abrir o software IDE do Arduino, abrir o monitor serial ou “serial plotter” e observar os valores ao vivo. Depois disso você pode deixar sua criatividade rolar solta, implementar mais coisas, verificações, automações, etc.

Aqui também não vou te ensinar nem a calibrar nem interpretar as leituras, até porque não tenho “padrões” nem referências para isso. Mas você vai notar ao longo do experimento que os valores de eCO2 do sensor CCS811 estão em torno de 550. Já os valores de TVOC do AGS10 estão em torno de 180 – 200, o que seria considerado “normal” para ambientes residenciais.
O circuito/hardware
A fiação elétrica do nosso experimento é bem simples, pois estamos utilizando apenas dois fios para comunicar com cada sensor (SDA e SCL da porta i2c). Fora isso apenas levamos alimentação para cada sensor, 3V3 e GND.
Para ligar tudo isso eu utilizei apenas uma pequena protoboard de 400 pontos. Isto é possível pelo tamanho do Xiao ESP32-C3, e também porque eu fiz uma placa de montagem para o mesmo. Nesta placa todos os 11 pinos de IO ficam em uma única linha, economizando espaço na protoboard.


A alimentação do nosso protótipo é totalmente via cabo USB-C, o mesmo cabo que vai levar os dados dos sensores até o computador. Mas nada impediria de você usar uma fonte externa em 5V, ou até mesmo um banco de baterias/power bank.
Um detalhe importante é para a plaquinha do CCS811, ela tem alguns sinais a mais, além do SDA e SCL para i2c. Neste sentido esta referência me ajudou bastante, em definir onde colocar cada pino da plaquinha.
- WAK ou nWake tem que ir para GND,
- RST ou nReset vai para Vcc (3V3),
- ADD ou ADDR define o endereço i2c do sensor. Conectei ao GND para endereço 0X5A.
O código/firmware
Para este exemplo vamos trabalhar com o software Arduino IDE. No meu caso eu tenho uma versão um pouco mais “defasada”, a 2.3.8 nightly, porém você pode e deve baixar a versão mais atual direto do site Arduino.cc.
Você vai precisar instalar as seguintes bibliotecas, de dentro da IDE do Arduino mesmo:
- Adafruit CCS811,
- Adafruit AHTX0.
Faremos a leitura do sensor AGS10 direto nos seus registradores, sem uso de biblioteca. No caso das bibliotecas da Adafruit acima, caso ela peça para instalar outras bibliotecas no conjunto, confirme. Há ainda o uso da biblioteca Wire para toda comunicação i2c, porém não é necessário instala-la, pois já vem com a IDE.
O código completo está abaixo e também neste Github, para você copiar e usar. Fiz algumas “brincadeiras” legais, que vou explicar logo abaixo.
#include "Adafruit_CCS811.h"
#include <Adafruit_Sensor.h>
#include <Adafruit_AHTX0.h>
#include <Wire.h>
#define I2C_ADDRESS 0x1A // Replace with the AGS10 I2C address
#define SDA_PIN 6 // I2C SDA pin for ESP32-C3
#define SCL_PIN 7 // I2C SCL pin for ESP32-C3
Adafruit_CCS811 ccs;
Adafruit_AHTX0 aht;
sensors_event_t humidity, temp;
class MovingAverage {
private:
int _numReadings;
float *_readings;
int _readIndex = 0;
int _samplesCollected = 0;
float _total = 0.0;
public:
MovingAverage(int size) {
_numReadings = size;
_readings = new float[_numReadings];
for (int i = 0; i < _numReadings; i++) {
_readings[i] = 0.0;
}
}
~MovingAverage() {
delete[] _readings;
}
float update(float newValue) {
_total -= _readings[_readIndex];
_readings[_readIndex] = newValue;
_total += newValue;
_readIndex++;
if (_readIndex >= _numReadings) {
_readIndex = 0;
}
// Count valid samples until buffer is full
if (_samplesCollected < _numReadings) {
_samplesCollected++;
}
return _total / (float)_samplesCollected;
}
};
float readCCS811= 0;
float readAGS10= 0;
float readTempAHT21 = 0;
float readHumAHT21 = 0;
long sensorsTime= 0;
float smoothCCS811;
float smoothAGS10;
float smoothTemp;
float smoothHum;
int startup = 1;
long oldtime;
long oldtimeled;
/*************************** Sketch Code ************************************/
// Functions (x2) to read AGS10, this first one calculates the CRC
uint8_t crc8_ags10(uint8_t *data, uint8_t len) {
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; b++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1);
}
}
return crc;
}
// now we effectively read the sensor value
uint32_t read_ags10(void) {
uint8_t buf[5] = {0};
// AGS10 requires a register write (0x00) before reading
Wire.beginTransmission(I2C_ADDRESS);
uint8_t err = Wire.endTransmission(); // send and capture whether it responded
if (err == 0) { // if Wire.beginTransmission was successfull
Wire.write(0x00);
Wire.endTransmission();
// Read 5 bytes: 1 status + 3 data + 1 CRC
Wire.requestFrom(I2C_ADDRESS, 5);
if (Wire.available() == 5) {
for (int i = 0; i < 5; i++) buf[i] = Wire.read();
} else {
Serial.println("AGS10: read failed");
return 0;
}
// Validate CRC over first 4 bytes
if (crc8_ags10(buf, 4) != buf[4]) {
Serial.println("AGS10: CRC error");
return 0;
}
// Check status bit 0: must be 0 (ready)
if (buf[0] & 0x01) {
Serial.println("AGS10: sensor not ready");
return 0;
}
// Assemble 24-bit big-endian TVOC value from bytes 1-3
return ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | buf[3];
}else{ // if Wire.beginTransmission was not successfull
readAGS10 = 0;
}
}
MovingAverage valueCCS811(6);
MovingAverage valueAGS10(6);
MovingAverage valueTemp(6);
MovingAverage valueHum(6);
void setup() {
Wire.begin(SDA_PIN, SCL_PIN, 10000); // speed has to be this low to accomodate the AGS10 sensor
Serial.begin(115200);
pinMode(2, OUTPUT);
delay(2000);
if(!ccs.begin()){
Serial.println("Failed to start CCS811 sensor! Please check your wiring.");
}
if (!ccs.available()) {
Serial.println("CCS811 timeout during startup");
}
if (! aht.begin()) {
Serial.println("Could not find AHT? Check wiring");
}
readSensors(); // makes all sensor readings at startup
}
void loop() {
if(millis() - oldtimeled > 200){ // if this LED stops blinking, the program has hang somewhere
oldtimeled+= 200;
digitalWrite(2, !digitalRead(2));
}
if(millis() - sensorsTime >= 5000){
sensorsTime+= 5000;
// All sensor readings here >>
readSensors();
smoothTemp = valueTemp.update(readTempAHT21);
smoothHum = valueHum.update(readHumAHT21);
smoothCCS811 = valueCCS811.update(readCCS811);
smoothAGS10 = valueAGS10.update(readAGS10);
char buf[80];
snprintf(buf, sizeof(buf), "Temperature:%.2f,Humidity:%.2f,CCS811:%.2f,AGS10:%.2f",
smoothTemp, smoothHum, smoothCCS811, smoothAGS10);
Serial.println(buf);
}
}
void readSensors(){ // do all sensors readings here
// We first get humidity and temperature from AHT21, so that
// we can feed that into CCS811
if (aht.begin()) {
aht.getEvent(&humidity, &temp);// populate temp and humidity AHT21 objects with fresh data
readTempAHT21 = temp.temperature;
readHumAHT21 = humidity.relative_humidity;
} else {
readTempAHT21 = 0;
readHumAHT21 = 0;
}
// get AGS10 TVOC (sensor presence/verification is embedded in the function)
readAGS10 = read_ags10();
// get CCS811 eCO2
if(ccs.available()){
ccs.setEnvironmentalData(smoothHum, smoothTemp); // humidity, temperature
if(!ccs.readData()){
readCCS811 = ccs.geteCO2();
}
}else{
readCCS811 = 0;
}
}
O que está implementado?
A primeira coisa que eu fiz foi ler os sensores somente a cada cinco (5) segundos. Até porque todas as variáveis lidas são lentas, não tem vantagem em ler nada mais rápido. Fiz isto com o condicional IF abaixo. Obs: meu código não é bloqueante, não tem nenhum delay().
if(millis() - sensorsTime >= 5000){
sensorsTime+= 5000;
Então eu criei variáveis chamadas “smoothXXXX”, que servem para fazer médias dos valores lidos. Implementei funções para isso, onde cada variável é lidad seis (6) vezes para fazer a média. Novamente, devido à natureza das variáveis, médias assim são inclusive super necessárias.
Então eu criei uma função chamada “readSensors()”, para fazer a leitura dos sensores. Os três sensores (CCS811, AGS10 e AHT21) são lidos via protocolo i2c, porém cada um de uma forma específica. Primeiro eu leio o AHT21 (temperatura e umidade), verificando se seu begin retorna verdadeiro:
if (aht.begin()) {
Caso não retorne, eu atribuo zero “0” às variáveis temperatura e umidade. Depois passo à leitura do AGS10, onde dentro da sua função eu verifico se é possível encontra-lo via i2c. Caso sim, então eu faço sua leitura:
Wire.beginTransmission(I2C_ADDRESS);
uint8_t err = Wire.endTransmission(); // send and capture whether it responded
if (err == 0) { // if Wire.beginTransmission was successfull
Caso não o encontre, atribuo zero “0” à variável de leitura do mesmo. Por último eu leio o CCS811, fazendo:
if(ccs.available()){
Caso ele esteja disponível, eu primeiro passo para ele a temperatura e umidade lidos pelo AHT21:
ccs.setEnvironmentalData(smoothHum, smoothTemp); // humidity, temperature
Depois então passo a fazer sua leitura. Caso não o encontre eu atribuo zero “0” à sua variável. Finalmente dentro do “loop()” principal eu consigo fazer a impressão dos valores lidos para o monitor serial da IDE do Arduino. Para isto eu utilizo a função “snprintf”, para conseguir montar o “pacote” com textos e variáveis:
char buf[80];
snprintf(buf, sizeof(buf), "Temperature:%.2f,Humidity:%.2f,CCS811:%.2f,AGS10:%.2f",
smoothTemp, smoothHum, smoothCCS811, smoothAGS10);
Serial.println(buf);
Obs: esta forma de impressão já contempla a visualização dos gráficos no serial plotter da IDE do Arduino. Veja abaixo.
Testando o circuito e código
Copie o código acima e cole na sua IDE do Arduino. Tenha certeza de ter instalado as bibliotecas indicadas no texto. Conecte o cabo USB C plugado ao ESP32-C, ao computador. Selecione a placa correta (no meu caso a Xiao ESP32-C3) na parte superior central da IDE do Arduino.
Então clique na “seta para a direita” que está localizada no canto superior esquerdo do software. Aguarde upload do código, momento no qual você já vai poder abrir tanto o monitor serial como o serial plotter. Veja exemplo de ambos nas imagens abaixo.

Note que as informações aparecem todas meio “juntas” no monitor serial. Isto é para que a impressão no serial plotter funcione, tudo está apenas separado por vírgulas. Claro que isto tudo pode ser alterado, basta encontrar o lugar e o jeito certo.

Fiz este experimento dentro de um ambiente fechado, um quarto dentro do meu apartamento. A forma de onda em “verde” acima é do CCS811, está lendo em média ~580. Segundo esta referência é um valor muito bom para ambientes internos. Já o sensor AGS10 (forma de onda em amarelo escuro) lê na média de ~350. Segundo esta referência este é um valor aceitável e seguro para este tipo de variável.
No início do artigo, lá em cima, tem um vídeo sobre o funcionamento do experimento. Volte lá e assista, tendo dúvidas tem a seção de perguntas abaixo e também lá no Youtube. Quer também comprar os sensores utilizados aqui no experimento? veja no meu Aliexpress: CCS811, AGS10 e AHT21.




