Мой складской логгер провисел на батарейке месяц, и я им гордился: чип просыпался по сторожевому таймеру каждые восемь секунд, опрашивал шину и засыпал обратно. Гордость кончилась, когда я посчитал кадры. Каждый цикл сна ходил по шине четырежды: пинг часов, чтение времени, опрос датчика, запись в журнал. Десять тысяч восемьсот циклов в сутки — сорок с лишним тысяч кадров, и девяносто процентов из них спрашивали одно и то же: ну что, случилось уже что-нибудь? А случаться могло одно событие в сутки — выход влажности за порог. Ответ при этом торчал на четвёртой ножке датчика, которую я год игнорировал: ножка с маркировкой INT.
Тупик был мой, и я его люблю пересказывать: опросный цикл я написал «чтобы наверняка» и ещё гордился надёжностью — как же, датчик спрашивается десять раз в секунду, ничего не пропустим. Пропустил. Экономика опроса дурная: чтобы заметить короткое событие, спрашивать надо чаще, чем событие длится, а чтобы не платить за опрос — надо не спрашивать вовсе и слушать. Слушателя я в своё время отмахнул, потому что в примерах из сети датчик висел на тех же двух проводах, что и всё остальное, а четвёртая ножка модуля уходила в воздух.
В этом уроке разберём, кто из I2C-чипов умеет дёргать такую ножку и за что, как повесить её на пин 2 и ловить через attachInterrupt, почему в обработчике живёт только volatile-флаг, а разговор с датчиком переезжает в loop. И поговорим о главной ловушке: INT — это не кнопка, он не «отпустит сам», пока вы не сбросите причину внутри датчика.
Кто и за что дёргает INT
Логика у всех одинаковая: пока новостей нет, чип молчит и шина молчит; как только произошло событие, чип меняет уровень на ножке INT — и мастер узнаёт о нём без единого кадра опроса. Различаются только события и электрика. MPU6050 поднимает INT, когда готов пакет данных или сработал детектор движения; по умолчанию выход активный высокий и push-pull, поведение настраивается в регистре 0x37 — про сам чип мы подробно говорили в уроке про акселерометр и гироскоп. Расширитель MCP23017 дёргает INT при изменении на любом из шестнадцати входов — кнопки можно вообще не сканировать. Часы DS3231 подают ножку, когда срабатывает будильник, а ADS1115 — когда компаратор перешагнул порог.
Третья развилка, о которой в примерах молчат, — длительность сигнала. Одни чипы держат линию, пока причина не сброшена; другие дают короткий импульс и отпускают. У MPU6050 в импульсном режиме линия живёт порядка пятидесяти микросекунд — опрос раз в десять миллисекунд не увидит его никогда, сколько ни тужься; это чистый довод за прерывание, которое ловит фронт железом в момент события. У режимов «уровень до сброса» свои правила: ножка остаётся прижатой, пока вы не разберётесь с регистром причины, — к этому вернёмся ниже, на той грабле я провёл сутки.
Вторая важная развилка — сток. У MPU6050 выход толкает уровень сам, подтяжка ему не нужна. У MCP23017, DS3231 и ADS1115 ножка открытого стока: чип умеет только прижимать линию к земле, поднимать её обязан внешний резистор 4,7-10 кОм к питанию — та же механика, что разбиралась в уроке про номиналы подтяжек. Отсюда и разный режим прерывания: push-pull ловим по тому фронту, который задан в даташите, открытый сток — по FALLING, потому что событие тут — падение линии, когда чип прижал её к земле. Перепутаете — обработчик будет стрелять на противоположном событии, таблица ниже это фиксирует.
| Чип | За что отвечает INT | Электрика | Что сбросить, чтобы отпустил |
|---|---|---|---|
| MPU6050 | данные готовы, детектор движения | активный высокий, push-pull, настраивается в регистре 0x37 | прочитать регистр причины или снять бит в 0x3A |
| MCP23017 | изменение на любом из 16 входов | открытый сток, активный низ | прочитать регистр INTCAP — он снимает флаг |
| DS3231 | сработал будильник 1 или 2 | открытый сток, активный низ | записать 0 в бит A1F/A2F регистра 0x0F |
| ADS1115 | компаратор перешагнул порог | открытый сток, ножка ALERT | прочитать результат конверсии |
Схема и скетч: INT на пине 2
Разводка короткая: INT датчика идёт на пин 2 — это INT0, внешние прерывания на Uno живут только на пинах 2 и 3, второй датчик сядет на третий. Для открытого стока включаю INPUT_PULLUP — бортовая подтяжка около 35 кОм; если линия длиннее полуметра или её трясёт, ставлю наружные 4,7 кОм, они честнее держат фронт. Дальше attachInterrupt с FALLING — вся механика фронтов, краткости обработчика и volatile была разобрана в уроке про attachInterrupt, здесь только применение: в обработчике одна строка flagInt = true. Разбор причины — чтение регистра статуса по флагу — делает loop, когда ему удобно; правила двухходовки «записать номер регистра, потом прочитать» те же, что в уроке про чтение регистров. Планируйте пины заранее: оба прерывательных пина стоят на вес золота, и матрица кнопок, съевшая D2 и D3, оставит INT без места — в итоговом проекте раздела мы раскладку честно считали.
#include <Wire.h> // DS3231: ножка SQW/INT дёргает D2, когда срабатывает будильник 1.// INT — открытый сток: INPUT_PULLUP обязателен, активный уровень — низкий. const byte PIN_INT = 2;const byte LED_DELA = 12;const byte RTC_ADR = 0x68;const byte REG_STATUS = 0x0F; // здесь живут биты флагов будильников volatile bool flagInt = false;unsigned long krugov = 0; void izInt() { flagInt = true; } // в ISR только флаг, никакой шины void setup() { pinMode(PIN_INT, INPUT_PULLUP); pinMode(LED_DELA, OUTPUT); attachInterrupt(digitalPinToInterrupt(PIN_INT), izInt, FALLING); Wire.begin(); Serial.begin(9600); Serial.println("Slushayu INT chasov, shina spit");} void loop() { krugov++; digitalWrite(LED_DELA, krugov % 2); // «делаем дела», шина молчит if (flagInt) { // датчик дёрнул INT flagInt = false; Wire.beginTransmission(RTC_ADR); Wire.write(REG_STATUS); Wire.endTransmission(); Wire.requestFrom(RTC_ADR, 1); // читаем регистр статуса byte status = 0; if (Wire.available() > 0) status = Wire.read(); if (status & 0x01) { // бит A1F: будильник 1 сработал Wire.beginTransmission(RTC_ADR); Wire.write(REG_STATUS); Wire.write(status & ~0x01); // 0 в A1F — ножка отпускает линию Wire.endTransmission(); Serial.println("BUDILNIK: prichina sbita"); } } delay(100);}Что здесь честно, а что нет. Кнопка — идеальный источник фронта: никакого дребезга, как у настоящей ножки INT — датчик не механический контакт, дребезга у него нет. Настоящего датчика в движке тоже нет: заглушка ACK-ает всё и отдаёт 42 в каждом байте, поэтому prichina всегда 42, а на живой плате там будет содержимое регистра статуса. Регистра 0x0F движок не знает: панель покажет запись байта 15 на адрес 0x68 и запрос одного байта — сам протокол подлинный, содержимое условное.
Зато главная идея видна без прикрас: панель I2C пуста, пока вы не нажали кнопку, и krugov тикает десятками в секунду — сводка в Serial сравнивает два счётчика наглядно. Прикинем экономию на своём логгере: опрос регистра статуса раз в 100 мс — это десять кадров в секунду, около 864 тысяч кадров в сутки только на вопрос «ну что?»; прослушивание — два кадра на событие, которых за сутки может не быть вовсе. На батарейке разница превращается в недели: каждый кадр будит TWI-периферию, а она ест ток и в паузах между кадрами.
Эксперименты для закрепления
Первое: замените FALLING на CHANGE и посмотрите в симуляторе, что нажатие и отпускание кнопки стали двумя событиями — у датчиков с открытым стоком режим CHANGE двойнит счётчик, обычно он не нужен. Второе: перенесите Wire-вызовы внутрь izInt — на живой плате это путь к зависшей шине: если обработчик придёт в момент, когда другой кусок кода ведёт обмен, TWI заклинит; страховкой служит таймаут из урока про зависание шины, но лучше просто не пускать шину в ISR. Третье: повесьте второй датчик на пин 3 и заведите два флага — два ISR на двух пинах живут независимо, а разбор в loop читает тот регистр, чей флаг взведён.
Четвёртое, на живой плате: настройте у MPU6050 детектор движения и словите встряску по RISING — не скопируйте FALLING из этого урока: у него активный высокий выход, и фронт тут противоположный. Пятое: соедините этот урок с часами — аппаратные будильники DS3231 с флагами A1F/A2F разбирались в уроке про будильники, а выдача секундных импульсов на ножку SQW и пробуждение от неё — в уроке про SQW-прерывание; тот же пин 2 примет сигнал от RTC без единой правки скетча.
- INT превращает опрос в реакцию: шина молчит, пока датчику нечего сказать, — кадры в сутки падают с сотен тысяч до единиц
- INT — на пин 2 или 3; открытый сток ловим FALLING с подтяжкой, у MPU6050 выход активный высокий — ему RISING
- В обработчике только volatile-флаг; чтение регистра статуса и решения — в loop
- INT держит LOW, пока причина не сброшена: прочитать регистр, записать 0 в бит флага
- После сброса питания датчика ножка может уже стоять в LOW — одноразовое чтение статуса после begin снимает залипание
- 1. Куда вешают ножку INT на Arduino Uno?
- 2. Что должно быть внутри обработчика прерывания от INT?
- 3. Датчик сработал, ножка INT осталась в LOW и больше не дёргается. Что делать?
- 4. У MPU6050 выход INT по умолчанию активный высокий. Какой режим attachInterrupt нужен?
- 5. Почему в симуляторе долгое зажатие кнопки даёт ровно одно событие?
Частые вопросы по теме урока
- Куда вешают ножку INT на Arduino Uno?
- Внешние прерывания INT0/INT1 на Uno привязаны к пинам 2 и 3; остальные пины attachInterrupt молча игнорирует. на D2 или D3 — внешние прерывания живут там
- Что должно быть внутри обработчика прерывания от INT?
- В ISR — одна строка flagInt = true. Шина и Serial внутри прерывания — прямой путь к зависанию обмена, поэтому разбор причины всегда переезжает в loop. только присваивание volatile-флага
- Датчик сработал, ножка INT осталась в LOW и больше не дёргается. Что делать?
- Открытый сток держит линию, пока не погашена причина: прочитайте регистр и запишите 0 в бит флага — только после этого появится новый фронт. читать регистр статуса и сбросить бит причины
- У MPU6050 выход INT по умолчанию активный высокий. Какой режим attachInterrupt нужен?
- Событие — рост уровня с LOW до HIGH, значит RISING. FALLING поймал бы отпускание линии и перепутал бы все события. RISING
- Почему в симуляторе долгое зажатие кнопки даёт ровно одно событие?
- Прерывание реагирует на переход HIGH-LOW, а не на уровень: держите пин в LOW сколько угодно — второго фронта нет. ISR взводит флаг только по фронту FALLING, а фронт случился один