AGS10 – это цифровой датчик регистрации общего количества летучих органических соединений (сокращённо – ЛОС. В англоязычной технической литературе применяется термин Total Volatile Organic Compounds – TVOC), разработанный компанией Guangzhou Aosong Electronic Co., Ltd. (Гуанчжоу, КНР) и выпускаемый под торговой маркой ASAIR. Он предназначен для обнаружения и измерения концентрации различных газов в окружающей среде.
На сегодняшний день данный сенсор позиционируется как сбалансированный вариант с точки зрения цены, удобства применения и точности измерений, что идеально подходит для применения в нетребовательных к точности системах «умного» дома и бытовой технике, в DIY-проектах, а также широко используется в системах мониторинга воздуха (офисы, теплицы, вентиляция).
В рамках данной статьи мы подробно изучим датчик TVOC AGS10, а в в качестве практики соберём на базе ESP32 простую домашнюю систему контроля воздуха, которая сигнализирует о том, что пора проветрить комнату.

Содержание
- Общее описание
- Устройство и принцип работы
- Параметры и характеристики
- AGS10 в составе отладочного модуля
- Схема подключения
- Программный код (скетч)
- Эксперименты
- Заключение
Общее описание
Датчик TVOC AGS10 измеряет общее количество органических испарений в воздухе (общая загрязнённость). То есть определяется концентрация не какого-то конкретного типа газа (например, аммиак, водород или пары спиртов), а суммарное количество множества летучих органических соединений от разных источников: бытовая химия, испарения от мебели или пластика, запахи на кухне при готовке, парфюмерия, масляная краска и так далее.
Внутри корпуса чипа находится подогревающаяся кремниевая пластинка с напылением оксида металла. Когда молекулы органических веществ попадают на эту горячую поверхность, они окисляются, что меняет электрическое сопротивление этой поверхности. По величине этого изменения вычисляется концентрация.
Результат вычислений выдается в единице измерения ppb (parts per billion — частиц на миллиард). Это отношение числа «загрязняющих» молекул к общему числу молекул в воздухе.
Если говорить по-простому, то при помощи AGS10 можно определить, на сколько сильно пахнет краской в помещении. Разумеется, определяется не именно сама краска, а общая интенсивность всех ЛОС в помещении (краска, дыхание людей, пыль).
На практике свежий воздух на природе соответствует значению (0…50) ppb. В помещении – около 50…100. Готовка на кухне – примерно 300. А если уж совсем «воняет» краской – 600 и более. На практике, фактический рабочий диапазон соответствует значениям от 0 до 10000 (десять тысяч).
Забегая вперёд, отмечу, что эти количественные показания сильно разнятся, поэтому для каждого конкретного случая нужно на практике производить проверку с Вашим экземпляром сенсора. Например, Вы решили сделать автоматику для включения вентиляции для проветривания в помещении. Проведите эксперимент, выдержав хотя бы 6 часов анализатор в душном помещении. Следите за измеряемой величиной. То значение, которое последний час эксперимента находится на стабильном уровне, и, на Ваш взгляд, это соответствует «душно, надо проветрить», на него и ориентируетесь как порог срабатывания при написании программы для автоматической вентиляции.
После длительного хранения микросхемы необходимо производить восстановительную процедуру – стабилизация посредством длительной безостановочной работы:

После первого включения показания концентрации будут на уровне нескольких тысяч единиц. Со временем величина будет плавно снижаться.
Выполнив процедуру восстановления, можно работать с сенсором в спокойном режиме: после каждого нового включения нужно выжидать 10…20 минут, чтобы чувствительный элемент прогрелся. В документации указано 120 секунд, но по личному опыту нужно куда больше времени.
Обратите на это внимание, потому что сразу после подачи питания на холодный модуль будет выдаваться ошибка, поскольку он сам определяет, что ему требуется прогрев. Как только он прогреется, в чипе установится бит готовности RDY = 1, и можно запрашивать данные.
В процессе отладки кода для отладочной платы придётся часто производить перепрошивку или перезагрузку («Reset»). Так как датчик уже будет физически «горячий», после команды «Reset» показания сразу будут считываться.
Важно отметить, что сенсор обладает существенной инерционностью: реакция на изменение концентрации происходит с задержкой до 1 минуты, а полное возвращение в базовое состояние может занять от 10 минут и более.
В процессе эксплуатации важно соблюдать ряд мер предосторожности:
– избегать попадания влаги и конденсата на микросхему;
– слишком агрессивная и высокая концентрация летучих органических соединений или углекислого газа (CO₂) может вызвать ярко выраженный дрейф показаний;
– прямой обдув сенсора (сквозняки, кондиционер, вентилятор, вентиляция) приводит к неверным значениям;
– высокая чувствительность к уровню напряжения питания, превышение 3,0 Вольта может «сжечь» чип; – пары агрессивных кислот необратимо повреждают чувствительный элемент.
AGS10 не претендует на точность измерений (заявленная производителем относительная погрешность ±25 %), но он идеально подходит для применения в бытовых и нетребовательных решениях:
– «умный» дом и бытовая техника (анализаторы качества и очистители воздуха, системы вентиляции и кондиционеры);
– DIY-проекты;
– нетребовательные и простые промышленные решения (мониторинг на рабочих местах в офисах и в учебных заведениях, в теплицах или в небольших мебельных мастерских).
Среди аналогичных газовых анализаторов популярными вариантами являются:
1) аналоговый модуль MQ-135, который сильно зависит от температуры и влажности, требует длительного прогрева и сложной калибровки;
2) CCS811 – весьма популярный, лучше точность и есть алгоритмическая компенсация, меньшее энергопотребление, дополнительно определяет эквивалент углекислого газа (eCO₂), но имеет значительный дрейф с течением времени, требуется частая калибровка;
3) семейство SGP30 и SGP40 от компании Sensirion – аналоги по функционалу, но это более продвинутые сенсоры премиального сегмента. Отличаются стабильностью, точностью (±15 %) и наличием алгоритмической компенсации влажности и температуры. SGP40 измеряет только TVOC, а SGP30 – дополнительно eCO₂;
4) BME680 от Bosch Sensortec – комбинированный климатический датчик (давление, температура, влажность, VOC), но характеризуется слабой чувствительностью к летучим органическим соединениям.
Важным преимуществом AGS10 перед конкурентами является то, что он не требует первичной калибровки по «чистому воздуху». Чип на заводе проходит калибровку на пары этанола при температуре +25 °C и относительной влажности 50 %. Заводские калибровочные коэффициенты хранятся в энергонезависимой памяти, которые загружаются при каждом включении. Благодаря этому газовый анализатор более удобен в эксплуатации, в отличие от MQ-135. Но, разумеется, со временем всё же придётся производить перекалибровку (к этому чуть подробнее в разделе “Программный код”).
Устройство и принцип работы
Датчик TVOC AGS10 – это полупроводниковый металлооксидный газовый сенсор (MOS – Metal Oxide Semiconductor), построенный технологии MEMS (Micro-Electro-Mechanical Systems, микроэлектромеханическая система). Эта технология подразумевает, что функциональные части системы (чувствительный элемент и блоки обработки сигнала) изготавливаются не как отдельные детали, а они вытравливаются на одной кремниевой подложке теми же методами, которые применяются при производстве микросхем. Благодаря этому создаются миниатюрные, точные и энергоэффективные системы на единой подложке из полупроводникового материала.
Если коротко, то принцип работы MOS-сенсора можно описать так: это миниатюрный «горячий» кремниевый чип с нанесённым слоем оксида металла, чьё электрическое сопротивление меняется при воздействии «загрязняющих» газов.
Рассмотрим подробнее. На рисунке ниже схематично представлено устройство чувствительного элемента чипа (приводится в официальной документации к датчику). При его создании на подложке формируется микротермическая пластина (micro-thermal plate), которая, фактически, представляет собой миниатюрный нагреватель. На эту пластину наносят газочувствительный материал на основе оксидов металла, который в чистом воздухе обладает низкой электропроводностью.

Микронагреватель поддерживает материал чувствительного элемента при оптимальной рабочей температуре, которой достаточно для активации химических реакций на поверхности оксида металла. А это требует значительной части от общего потребления тока (максимальная потребляемая мощность 75 мВт).
Когда молекулы летучих органических соединений, такие как этанол, аммиак, пары бензола, контактируют с поверхностью оксидного материала, они адсорбируются. Это приводит к химической реакции на поверхности, в результате которой в полупроводнике происходит изменение концентрации носителей заряда. Оно напрямую влияет на электрическую проводимость (сопротивление) материала чувствительного элемента.
В данном случае зависимость прямая: чем выше концентрация газа, тем выше проводимость (тем ниже сопротивление) материала. Соответственно, при изменении сопротивления меняется уровень напряжения выходного сигнала.
Для преобразования аналогового сигнала напряжения (от изменения сопротивления) в цифровые данные используется многоступенчатая обработка. Она выполняется в специализированной интегральной схеме ASIC (Application-Specific Integrated Circuit). На рисунке ниже представлена упрощённая блок-схема измерительной системы:

ASIC-чип включает в себя аналого-цифровой преобразователь (АЦП), который преобразует напряжение в цифровые отсчёты. При оцифровке данных используется специальная технология цифрового сбора данных (digital module acquisition technology), которая включает фильтрацию, усреднение и компенсацию влияния температуры для повышения стабильности.
Встроенный в ASIC-чип контроллер управляет логикой преобразования. В частности, регламентирует цикл измерений (период опроса не менее 2 секунд) и контролирует состояние чувствительного элемента (прогрев в течение не менее 120 секунд, после чего устанавливается бит готовности RDY = 1).
Также встроенный контроллер хранит и использует калибровочные коэффициенты. Преобразователь выходит с завода полностью откалиброванным. При этом пользователю предоставляется возможность самостоятельно произвести калибровку сенсора, и контроллер запишет новые коэффициенты во внутреннюю энергонезависимую память (при необходимости можно произвести сброс к заводским настройкам). Отметим, что в модуле отсутствует внутренняя температурная компенсация.
Для обеспечения целостности данных встроенный в ASIC-чип контроллер применяет алгоритм обнаружения ошибок CRC (Cyclic Redundancy Check). Данный алгоритм используется при передачи данных по I2C-интерфейсу с внешним master-контроллером.
Параметры и характеристики

Сенсор вообще не реагирует на углекислый газ (CO₂), оксид углерода или угарный газ (CO) и диоксид азота (NO₂).
Также данный датчик не предназначен для регистрации утечки природного газа метана (CH₄), даже несмотря на то, что он ощутимо пахнет (меркаптан). То есть его нельзя использовать для контроля взрывоопасных или токсичных газов (в том числе угарный газ). Он для этого не предназначен.
AGS10 в составе платы (модуля)
Для экспериментов с преобразователем воспользуемся покупной специальной отладочной платой со всей необходимой обвязкой. Модуль включает в себя линейный стабилизатор из серии XC6206 на 3,0 Вольта с низким падением напряжения. Поэтому можно питать модуль от 3,3 или 5 Вольт.

Исходя из питания 3,3 или 5 Вольт, необходимо согласовать низковольтные контакты микросхемы с высоковольтными выводами микроконтроллера (например, Arduino UNO с 5-вольтовой логикой). С этой целью применены N-канальные MOSFET-транзисторы (транзисторная сборка 2N7002DW).
Подтягивающие резисторы номиналом 10 кОм представлены в виде сборки из 4-х штук, что значительно экономит место на плате.
Ниже представлены таблица распиновки модуля и его схема электрическая принципиальная.

Как подключить датчик TVOC AGS10 к ESP32
Для построения системы контроля воздуха воспользуемся отладочной платой NodeMCU-32S (38 pin), которая построена на базе модуля ESP-WROOM-32.
Для отображения информации применим OLED-дисплей на базе контроллера SSD1306. Диагональ экрана составляет 0,91 дюйма и имеет разрешение 128×32 пикселя, что отлично подходит для индикации в миниатюрных устройствах.
Интерфейс подключения для обоих модулей — I2C. Ниже представлена таблица подключений сенсора и дисплея к отладочной плате.


Программный код (скетч)
Проект разрабатывался в среде программирования Arduino IDE 2. Компания Arduino теперь принадлежит Qualcomm, и могут возникнуть трудности с доступом к официальному сайту. В качестве альтернативного варианта можно воспользоваться официальным репозиторием на GitHub. Подробная инструкция по настройке среды Arduino IDE для работы с ESP32 представлена в статье про диктофон на ESP32 с цифровым микрофоном INMP411.
Для работы с OLED-дисплеем воспользуемся популярной библиотекой Adafruit_SSD1306. Дополнительно можно подключить Adafruit_GFX для отображения графики.
Для работы с газоанализатором достаточно будет встроенной в среду программирования библиотеки Wire, предназначенной для работы с I2C-интерфейсом. Всю логику опроса микросхемы мы пропишем сами в скетче.
Важно отметить, что заводская калибровка производилась на парах этанола. Для нового чипа пользовательская калибровка для бытовых нужд не требуется. Но со временем из-за дрейфа нуля периодически производить калибровку по “чистой атмосфере” всё же настоятельно рекомендуется. Но обращаю внимание на слова из даташита: “Пользователь может заново откалибровать нулевую точку по необходимости, и откалиброванные данные будут потеряны после отключения питания”. То есть придётся производить калибровку заново после каждого сброса питания. Это необходимо учитывать при проектировании программы. Пример кода для калибровки представлен после основного скетча.
Ниже представлен очень подробно прокомментированный листинг кода (включает в себя чтение данных TVOC и «сырое» значение сопротивления чувствительного элемента):
// Подключаем библиотеку для работы с I2C интерфейсом
#include <Wire.h>
// Подключаем графическую библиотеку Adafruit_GFX
#include <Adafruit_GFX.h>
// Подключаем библиотеку для работы с OLED-дисплеем SSD1306
#include <Adafruit_SSD1306.h>
// Настройки OLED-дисплея
// Константы для настройки размеров и адреса дисплея
// Ширина дисплея в пикселях (стандарт для маленьких OLED 128x32 или 128x64)
#define SCREEN_WIDTH 128
// Высота дисплея в пикселях
#define SCREEN_HEIGHT 32
// Определяет номер пина для аппаратного сброса дисплея.
// Значение -1 означает, что отдельного пина сброса нет, и сброс не используется.
#define OLED_RESET -1
// Адрес дисплея на шине I2C в 7-битном формате.
// Адрес 0x3C является наиболее распространенным для дисплеев на чипе SSD1306.
#define SCREEN_ADDRESS 0x3C
// Создаем объект дисплея с указанными параметрами.
// Конструктор принимает: ширину, высоту, указатель на объект Wire (для I2C), номер пина сброса.
// Это инициализирует объект в памяти, но сам дисплей начнет работать после вызова .begin().
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Адрес датчика AGS10 на шине I2C (фиксированный адрес из даташита).
#define AGS10_ADDR 0x1A
// Глобальная переменная для значения TVOC (в частях на миллиард, ppb).
uint32_t tvoc = 0;
// Функция для расчета CRC8 (из даташита)
uint8_t calculateCRC8(const uint8_t* data, uint8_t length) {
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < length; i++) {
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; bit++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc <<= 1;
}
}
}
return crc;
}
// Начальная настройка микроконтроллера
void setup() {
// Инициализируем последовательный порт (UART)
// Скорость передачи данных установлена на 115200 бит в секунду.
Serial.begin(115200);
// Инициализируем шину I2C с явным указанием номеров пинов для ESP32.
// На ESP32: GPIO21 - это SDA, GPIO22 - это SCL
Wire.begin(21, 22);
// Инициализируем OLED-дисплей
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("Ошибка инициализации SSD1306"));
while (true)
;
}
// Очищаем дисплей
display.clearDisplay();
// Устанавливаем размер шрифта.
// Аргумент 1 означает базовый размер (6x8 пикселей для стандартного шрифта библиотеки).
display.setTextSize(1);
// Устанавливаем цвет текста.
// SSD1306_WHITE означает, что текст будет рисоваться белыми пикселями на черном фоне.
display.setTextColor(SSD1306_WHITE);
// Устанавливаем курсор в начало экрана (координаты X=0, Y=0, где Y - это строка текста).
display.setCursor(0, 0);
// Выводим текстовую строку в буфер дисплея.
display.println("TVOC AGS10");
// Отправляем содержимое буфера на физический дисплей, чтобы надпись стала видимой.
display.display();
// Ждем 2 секунды, чтобы надпись можно было прочитать.
delay(2000);
// ЗАКОММЕНТИРОВАННЫЙ БЛОК ПРОГРЕВА ДАТЧИКА
// Согласно даташиту AGS10 (раздел 2), требуется 120 секунд прогрева после подачи питания.
// Это необходимо для стабилизации температуры чувствительного элемента.
// Раскомментируйте этот блок, если хотите включить визуальный отсчет прогрева на дисплее.
/*
Serial.println("Прогрев датчика...");
for (int i = 120; i > 0; i--) {
display.clearDisplay();
display.setCursor(0, 0);
display.print("Heating: ");
display.print(i);
display.println(" sec");
display.display();
Serial.print(i);
Serial.println(" sec");
delay(1000);
}
*/
// Выводим сообщение о готовности в последовательный порт.
Serial.println("AGS10 TVOC Sensor Ready");
// Очищаем дисплей и выводим сообщение о готовности на него.
display.clearDisplay();
display.setCursor(0, 0);
display.print("AGS10 TVOC Ready");
display.display();
// Ждем 1 секунду, прежде чем перейти к основному циклу измерений.
delay(1000);
// Очищаем дисплей перед первым циклом loop().
display.clearDisplay();
}
// Вспомогательная функция для чтения данных из указанного регистра датчика по I2C.
// Параметры:
// reg - адрес регистра датчика, из которого нужно прочитать данные.
// (например, 0x00 для TVOC, 0x20 для сопротивления)
// data - указатель на массив (буфер), куда будут записаны считанные байты.
// len - количество байт, которое ожидается прочитать.
// Возвращает:
// фактическое количество успешно прочитанных байт (может быть меньше len при ошибке).
uint8_t readData(uint8_t reg, uint8_t* data, uint8_t len) {
// Начинаем передачу на устройство с адресом AGS10_ADDR.
Wire.beginTransmission(AGS10_ADDR);
// Отправляем байт регистра. Это указывает датчику, откуда мы хотим читать.
Wire.write(reg);
// Завершаем передачу
Wire.endTransmission(false);
// Временная задержка перед чтением
if (reg == 0x00 || reg == 0x20) {
delay(1500); // TVOC и сопротивление требуют 1500 мс
} else {
delay(30); // Остальные команды - 30 мс
}
// Запрашиваем от датчика (с того же адреса) 'len' байт данных.
Wire.requestFrom(AGS10_ADDR, len);
// Считываем байты в массив 'data'.
uint8_t i = 0;
// Wire.available() возвращает количество байт, ожидающих чтения из буфера I2C.
// Читаем, пока есть данные и не прочитали запрошенное количество ('len').
while (Wire.available() && i < len) {
// Считываем один байт и помещаем его в массив, затем увеличиваем индекс i.
data[i++] = Wire.read();
}
// Эта задержка обеспечивает корректную работу датчика.
delay(30);
// Возвращаем количество фактически прочитанных байт.
return i;
}
// Бесконечный цикл loop()
void loop() {
// Объявляем массивы для хранения сырых данных, полученных от датчика.
// Для данных TVOC: [Статус, Байт2, Байт3, Байт4, CRC]
uint8_t tvocData[5];
// Для данных сопротивления: [Байт1, Байт2, Байт3, Байт4, CRC]
uint8_t resData[5];
// Флаги успешности чтения и проверки CRC
bool tvocValid = false;
bool resValid = false;
display.clearDisplay(); // Очищаем буфер (заливаем черным).
display.setCursor(0, 0); // Устанавливаем курсор в начало.
display.setTextSize(1); // Устанавливаем базовый размер шрифта для заголовка.
// Выводим статический заголовок в верхней строке дисплея.
display.println(" SPRYTRON.RU AGS10 ");
// --- БЛОК ЧТЕНИЯ ДАННЫХ TVOC ---
// Читаем 5 байт из регистра 0x00 датчика
// Если функция readData вернула 5 (все байты прочитаны успешно), обрабатываем данные.
if (readData(0x00, tvocData, 5) == 5) {
// Проверяем CRC для первых 4 байтов (данные без CRC)
uint8_t calculatedCRC = calculateCRC8(tvocData, 4);
if (calculatedCRC == tvocData[4]) {
// CRC совпадает, данные корректны
tvocValid = true;
// Формируем 24-битное (3-байтовое) значение TVOC из байтов данных.
// Согласно формату данных (раздел 3.2 даташита):
// tvocData[0] - Status Byte (состояние, бит 0 - RDY - готовность данных)
// tvocData[1] - Старший байт (Most Significant Byte - MSB) значения TVOC
// tvocData[2] - Средний байт значения TVOC
// tvocData[3] - Младший байт (Least Significant Byte - LSB) значения TVOC
// tvocData[4] - Контрольная сумма CRC для проверки целостности данных.
tvoc = (tvocData[1] << 16) | (tvocData[2] << 8) | tvocData[3];
// (tvocData[1] << 16) - сдвигаем старший байт на 16 позиций влево (в старшие разряды 32-битного числа).
// (tvocData[2] << 8) - сдвигаем средний байт на 8 позиций влево.
// tvocData[3] - младший байт остается на месте.
// Оператор "|" (битовое ИЛИ) объединяет эти три части в одно число.
// Проверяем бит готовности данных (RDY) в Status Byte (tvocData[0]).
// Согласно даташиту (раздел 3.2):
// RDY=0 - данные готовы, RDY=1 - данные не готовы или датчик в режиме прогрева.
// Маска 0x01 (двоичное 00000001) выделяет только младший (нулевой) бит.
if ((tvocData[0] & 0x01) == 0) {
// Если данные готовы, выводим значение TVOC в последовательный порт для отладки.
Serial.print("TVOC: ");
Serial.print(tvoc);
Serial.println(" ppb (CRC OK)");
} else {
// Данные не готовы, но CRC верный
Serial.println("TVOC data not ready (RDY=1) but CRC OK");
}
} else {
// Ошибка CRC
Serial.print("TVOC CRC Error! Calculated: 0x");
Serial.print(calculatedCRC, HEX);
Serial.print(", Received: 0x");
Serial.println(tvocData[4], HEX);
}
} else {
// Если не удалось прочитать 5 байт (например, ошибка связи по I2C)
Serial.println("Error reading TVOC data!");
}
// --- БЛОК ЧТЕНИЯ СОПРОТИВЛЕНИЯ ДАТЧИКА ---
// Читаем 5 байт из регистра 0x20 датчика (команда "Read current resistance" из даташита, таблица 3.8).
if (readData(0x20, resData, 5) == 5) {
// Проверяем CRC для первых 4 байтов (данные без CRC)
uint8_t calculatedCRC = calculateCRC8(resData, 4);
if (calculatedCRC == resData[4]) {
// CRC совпадает, данные корректны
resValid = true;
// Формируем 32-битное значение сырого сопротивления из первых четырех байтов.
// Согласно формату данных (раздел 3.4 даташита):
// resData[0] - Байт 1 значения сопротивления.
// resData[1] - Байт 2 значения сопротивления.
// resData[2] - Байт 3 значения сопротивления.
// resData[3] - Байт 4 значения сопротивления.
// resData[4] - Контрольная сумма CRC.
uint32_t rawRes = (resData[0] << 24) | (resData[1] << 16) | (resData[2] << 8) | resData[3];
// Здесь сдвиги на 24, 16 и 8 бит собирают четыре байта в одно 32-битное число.
// Пересчитываем сырое значение в физические величины.
// Согласно даташиту (раздел 3.4), единица измерения сырого значения (rawRes) - это 0.1 кОм (100 Ом).
// Умножаем на 100, чтобы получить сопротивление в Омах.
float res_Ohm = rawRes * 100.0;
// Делим на 1,000,000, чтобы получить сопротивление в Мегаомах (МОм).
float res_MOhm = res_Ohm / 1000000.0;
// Выводим значение сопротивления в последовательный порт.
Serial.print("Сопротивление: ");
Serial.print(res_MOhm);
Serial.println(" МОм (CRC OK)");
} else {
// Ошибка CRC
Serial.print("Resistance CRC Error! Calculated: 0x");
Serial.print(calculatedCRC, HEX);
Serial.print(", Received: 0x");
Serial.println(resData[4], HEX);
}
} else {
// Ошибка чтения
Serial.println("Error reading resistance data!");
}
// Разделитель в последовательном порту для удобства чтения логов.
Serial.println("----------------");
// --- БЛОК ОТОБРАЖЕНИЯ ДАННЫХ НА OLED ДИСПЛЕЕ ---
// Устанавливаем увеличенный размер шрифта (в 2 раза больше базового) для основного значения TVOC.
display.setTextSize(2);
// Устанавливаем курсор для вывода основного значения. Координата Y=15 пикселей.
// При высоте дисплея 32 пикселя и строке заголовка (~8 пикселей) это примерно по центру оставшейся области.
display.setCursor(0, 15);
// Проверяем, есть ли корректные данные для отображения
if (tvocValid && (tvocData[0] & 0x01) == 0) {
// Выводим значение TVOC, хранящееся в глобальной переменной 'tvoc'.
display.print(tvoc);
} else {
// Если данные не готовы или CRC неверная, показываем прочерк
display.print("---");
}
// Возвращаем маленький шрифт (размер 1) для вывода единиц измерения.
display.setTextSize(1);
// Выводим текст "ppb" сразу после числа.
display.println(" ppb");
// Отрисовываем всё на OLED-дисплее
display.display();
// Задержка между циклами измерений.
// Согласно даташиту AGS10 (раздел 3.8, примечание 2),
// интервал между командами чтения TVOC ("Data acquisition")
// должен быть не менее 1500 мс
delay(2000);
}
Для более гибкой и глубокой настройки и эксплуатации сенсора производитель предлагает набор команд, представленных в таблице ниже:

Ниже представлены примеры кода для каждой команды.
1) Чтение данных TVOC
bool readTVOC(uint32_t &tvoc_ppb) {
static uint32_t lastCommandTime = 0;
if (millis() - lastCommandTime < 30) return false; // 30 мс между командами
Wire.beginTransmission(0x1A);
Wire.write(0x00);
Wire.endTransmission(false); // false для Repeated Start!
delay(1500); // 1.5 сек на обработку данных
Wire.requestFrom(0x1A, 5);
if (Wire.available() != 5) return false;
uint8_t data[5];
for (int i = 0; i < 5; i++) data[i] = Wire.read();
lastCommandTime = millis(); // Фиксируем время выполнения команды
if (calculateCRC8(data, 4) != data[4]) return false;
if ((data[0] & 0x01) != 0) return false;
tvoc_ppb = ((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
data[3];
return true;
}
// Пример использования
uint32_t tvoc = 0;
uint32_t tvocValue = 0;
if (readTVOC(tvocValue)) {
tvoc = tvocValue;
Serial.print("TVOC: ");
Serial.print(tvoc);
Serial.println(" ppb");
}
2) Чтение сопротивления
bool readSensorResistance(uint32_t &res_raw, float &res_kohm) {
static uint32_t lastCommandTime = 0;
if (millis() - lastCommandTime < 30) return false;
Wire.beginTransmission(0x1A);
Wire.write(0x20); // Регистр сопротивления
Wire.endTransmission(false); // false для Repeated Start!
// Датчику нужно 1500 мс на обработку этой команды
delay(1500);
Wire.requestFrom(0x1A, 5);
if (Wire.available() != 5) return false;
uint8_t data[5];
for (int i = 0; i < 5; i++) data[i] = Wire.read();
lastCommandTime = millis(); // Фиксируем время
if (calculateCRC8(data, 4) != data[4]) return false;
res_raw = ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
data[3];
res_kohm = res_raw * 0.1f; // Перевод в кОм (0.1 кОм единицы)
return true;
}
// Пример использования
uint32_t rawRes;
float res_kohm;
if (readSensorResistance(rawRes, res_kohm)) {
float res_MOhm = res_kohm / 1000.0; // кОм → МОм
Serial.print("Сопротивление: ");
Serial.print(res_MOhm);
Serial.println(" МОм");
}
3) Калибровка по текущему сопротивлению (чистые условия)
bool calibrateToClearAir() {
static uint32_t lastCommandTime = 0;
if (millis() - lastCommandTime < 30) return false;
Wire.beginTransmission(0x1A);
Wire.write(0x01);
Wire.write(0x00);
Wire.write(0x0C);
Wire.write(0x00);
Wire.write(0x00);
Wire.write(0xAC);
if (Wire.endTransmission() != 0) return false;
delay(30);
lastCommandTime = millis();
return true;
}
// Пример использования
Serial.println("Поместите датчик в чистый воздух на 20 минут...");
// 20 минут = 1200 секунд = 1200000 мс
delay(240000);
delay(240000);
delay(240000);
delay(240000);
delay(240000);
if (calibrateToClearAir()) {
Serial.println("Калибровка в чистом воздухе выполнена");
}
4) Чтение версии прошивки
bool readFirmwareVersion(uint8_t version[4]) {
Wire.beginTransmission(AGS10_ADDR);
Wire.write(0x11);
Wire.endTransmission(false);
delay(30);
Wire.requestFrom(AGS10_ADDR, 5);
if (Wire.available() != 5) return false;
uint8_t data[5];
for (int i = 0; i < 5; i++) data[i] = Wire.read();
if (calculateCRC8(data, 4) != data[4]) return false;
for (int i = 0; i < 4; i++) version[i] = data[i];
return true;
}
// Пример использования
uint8_t version[4];
if (readFirmwareVersion(version)) {
Serial.println("✓ Данные получены успешно");
// HEX формат
Serial.print("HEX: ");
for (int i = 0; i < 4; i++) {
Serial.print("0x");
if (version[i] < 0x10) Serial.print("0");
Serial.print(version[i], HEX);
Serial.print(" ");
}
Serial.println();
// DEC формат
Serial.print("DEC: ");
for (int i = 0; i < 4; i++) {
Serial.print(version[i]);
Serial.print(" ");
}
Serial.println();
// Data4 особо (биты 20-19 из даташита)
Serial.print("Data4 (биты 20-19): 0x");
if (version[3] < 0x10) Serial.print("0");
Serial.println(version[3], HEX);
} else {
Serial.println("✗ Ошибка чтения версии!");
Serial.println("Проверьте подключение I2C:");
Serial.println("- Контакты SDA/SCL");
Serial.println("- Питание 3.0V");
Serial.println("- Адрес 0x1A");
}
5) Сброс на заводские настройки (Zero-point reset)
bool factoryReset() {
static uint32_t lastCommandTime = 0;
if (millis() - lastCommandTime < 30) return false;
Wire.beginTransmission(0x1A);
Wire.write(0x01); // Регистр калибровки
Wire.write(0x00); // Data1
Wire.write(0x0C); // Data2
Wire.write(0xFF); // Data3
Wire.write(0xFF); // Data4
Wire.write(0x81); // CRC
if (Wire.endTransmission() != 0) return false;
delay(30); // Обработка команды
lastCommandTime = millis();
return true;
}
// Пример использования
if (factoryReset()) {
Serial.println("Сброс на заводские настройки выполнен");
}
6) Изменение I2C-адреса (например, на 0x1B)
bool changeI2CAddress(uint8_t newAddr) {
// Проверка корректности адреса
if (newAddr < 0x08 || newAddr > 0x77) {
Serial.print("Некорректный I2C адрес: 0x");
Serial.println(newAddr, HEX);
return false;
}
// Проверка времени между командами
static uint32_t lastCommandTime = 0;
if (millis() - lastCommandTime < 30) return false;
uint8_t revAddr = ~newAddr; // Инвертированный адрес
// Подготавливаем 4 байта данных для CRC: New_Addr, RevNew_Addr, New_Addr, RevNew_Addr
uint8_t dataForCRC[] = {newAddr, revAddr, newAddr, revAddr};
uint8_t crc = calculateCRC8(dataForCRC, 4);
// Формируем полную команду (7 байт)
Wire.beginTransmission(0x1A); // Текущий адрес датчика
Wire.write(0x34); // Команда записи
Wire.write(0x21); // Регистр изменения адреса
Wire.write(newAddr); // New_Addr (1)
Wire.write(revAddr); // RevNew_Addr (1)
Wire.write(newAddr); // New_Addr (2) - повтор
Wire.write(revAddr); // RevNew_Addr (2) - повтор
Wire.write(crc); // CRC для 4 байт данных
// Отправляем команду
if (Wire.endTransmission() != 0) {
Serial.println("Ошибка передачи по I2C");
return false;
}
delay(30); // Время обработки команды
lastCommandTime = millis();
// Адрес изменён и сохранился в энергонезависимой памяти!
return true;
}
// Пример использования
changeI2CAddress(0x1B);
7) Функция расчета CRC8 (обязательна для проверки)
uint8_t calculateCRC8(const uint8_t *data, uint8_t length) {
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < length; i++) {
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; bit++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31; // Полином 0x31
} else {
crc <<= 1;
}
}
}
return crc;
}
// Пример использования
void testCRC() {
// Тест из раздела 3.3: 0x00, 0x0C, 0xFF, 0xFF = CRC 0x81
uint8_t test1[] = {0x00, 0x0C, 0xFF, 0xFF};
uint8_t crc1 = calculateCRC8(test1, 4);
Serial.print("Test 1: 0x");
Serial.println(crc1, HEX); // Должно быть 0x81
// Тест из раздела 3.3: 0x00, 0x0C, 0x00, 0x00 = CRC 0xAC
uint8_t test2[] = {0x00, 0x0C, 0x00, 0x00};
uint8_t crc2 = calculateCRC8(test2, 4);
Serial.print("Test 2: 0x");
Serial.println(crc2, HEX); // Должно быть 0xAC
}
Эксперименты
Как упоминалось ранее, после покупки модуля, который долго хранился на складе, необходимо провести восстановление посредством длительного прогрева. Мне потребовалось несколько дней непрерывной работы (точно более 72 часов), чтобы показания стабилизировались на уровне 100 ppb (офисное помещение, примерно 18 м², с вентиляцией).
Допустим, газовый анализатор на ночь выключили, а после обеда следующего дня решили провести эксперимент. Тогда нужно дать хотя бы 20 минут, чтобы сенсор при чистой атмосфере прогрелся и показания стабилизировались. И вообще, всегда на прогрев выделяйте не менее 20 (двадцать) минут.
1) Эксперимент №1: ватка, смоченная спиртом.
Фоновые показания при относительно чистой атмосфере:

Поднесём к анализатору кусочек ватки, смоченной спиртом, на расстояние примерно 5 сантиметров. Подержим 1 минуту. Видим показания: более 30 тысяч.

Приблизительно через полчаса показания вернулись на исходный уровень.
2) Эксперимент №2: освежитель воздуха.
Разместим преобразователь в туалете и выдержим не менее 1 часа:

Теперь распылим освежитель, но так, чтобы капли аэрозоля не оседали прямо на модуль. Ждём пару минут и фиксируем незначительное увеличение, хотя пахнет ощутимо. Это, возможно, обусловлено тем, что датчик не реагирует на некоторые отдушки из бытовой химии.

3) Эксперимент №3: душная комната.
Маленькое помещение (12 м²), окно плотно закрыто, включено отопление. Выдерживаем анализатор пару часов: более 700 ppb, и в помещении явно душно.

После того, как открыли окно, потребовалось более получаса, чтобы уровень концентрации ЛОС снизился до 100, даже если для проветривания понадобилось всего 10 минут. Соответственно, надо учитывать при проектировании вентиляционных систем, что анализатор имеет значительную инерционность.
Заключение
В этой статье мы подробно познакомились с микросхемой AGS10 – это доступный, малогабаритный, удобный и простой в программировании и эксплуатации датчик для мониторинга качества воздуха в реальном времени.
Данный сенсор на сегодняшний день является самым доступным простым вариантом для бытового мониторинга концентрации общего количества летучих органических веществ. Главное преимущество – наличие базовой калибровки с завода. Благодаря этому можно пользоваться анализатором сразу после того, как будет осуществлён длительный процесс восстановления в течение не менее 72 часов. Он не претендует на лабораторную точность, но его вполне достаточно для большинства бытовых и учебных задач по доступной цене.
Датчик TVOC AGS10 — это бюджетная «рабочая лошадка» для мониторинга в доме, в офисе, на складе или в мебельной мастерской (клей, лакокрасочне покрытия). Он отличается весьма значительной инерционностью изменения показаний, особенно в процессе возвращения к исходному состоянию после проветривания. И очень важно помнить, что после подачи питания необходимо давать по-хорошему не менее двадцати минут на прогрев, чтобы показания стабилизировались.
