Геркон на двери гаража я повесил на пин 7 — просто потому, что провод от двери шёл вдоль стены к левому краю щитка, и укорачивать его не хотелось. Скетч собрался за пять минут: pinMode(7, INPUT_PULLUP), attachInterrupt(7, dver, CHANGE), в обработчике счётчик открытий. Компилятор промолчал. Залил — и получил тишину: сколько ни хлопай дверью, в мониторе ни строки. Я полчаса не доверял геркону — звонил его тестером, менял подтяжку, пережимал клеммник — пока не дошёл, что обманывал меня не контакт, а сам вызов: на Uno внешние прерывания живут только на пинах 2 и 3, а чужие пины attachInterrupt глотает без предупреждения. Об этом честно говорит урок про attachInterrupt, но я в своё время прочёл его по диагонали и вынес только правила ISR.
А выход лежал в том же чипе. У ATmega328P есть второй, тихий механизм — pin change interrupt, PCINT: смена уровня на любом из двадцати пинов способна дёрнуть обработчик. Он не умеет выбирать фронт — только сам факт смены, — зато покрывает всю макетку: цифровые пины и аналоговые входы впридачу. Плата за универсальность — структура: пины разбиты на три группы, у каждой группы один общий обработчик и по паре регистров настройки.
План урока такой: карта групп и регистров, железный скетч геркона на D7 через регистры PCICR и PCMSK2 — движок симулятора макросы ISR не исполняет, поэтому он для Arduino IDE, — грабли, которые я собрал на том гаражном щитке, и симулятор, который признаётся прямо: PCINT он не показывает, зато даёт потрогать главную разницу между прерыванием и опросом, из-за которой PCINT вообще существует.
Три группы по портам: B, C, D
Внутри чипа пины сгруппированы по портам, и PCINT наследует эту разбивку. Порт B — это пины D8-D13, порт C — аналоговые входы A0-A5, порт D — D0-D7. У каждого пина есть свой бит в линии наблюдения: у D7 это PCINT23, у A3 — PCINT11. Всего двадцать линий против двух каналов INT0 и INT1. Номер линии не совпадает с номером пина — на этом спотыкаются первым делом: биты идут сквозным счётом по портам, 0-5 на порт B, 8-13 на порт C, 16-23 на порт D.
Регистров на группу пара. Маска PCMSK0, PCMSK1 или PCMSK2 решает, какие пины группы слушать: единица в бите — пин под наблюдением. Регистр-выключатель PCICR с битами PCIE0, PCIE1, PCIE2 разрешает саму группу — без него маска хоть вся из единиц, событий ноль. Есть и регистр флагов PCIFR, но в скетчах его обычно не трогают: флаг сбрасывается сам при входе в обработчик.
Самая важная цена PCINT — один вектор на всю группу. Обработчик PCINT2_vect зовётся, когда дрогнул любой из восьми пинов порта D, и не сообщает, какой именно. Поэтому в нём сравнивают текущее состояние порта — регистр PINB, PINC или PIND — с сохранённой копией: XOR покажет, какие биты изменились. Библиотеки вроде PinChangeInt прячут эту рутину за привычным вызовом в духе attachInterrupt, но механику я советую понять руками: она же объясняет, почему у PCINT нет режимов RISING и FALLING — железо сообщает только о смене, а какой была смена, решаете вы по новому уровню бита.
| Свойство | INT0 и INT1 (пины 2 и 3) | PCINT (любой пин) |
|---|---|---|
| Сколько пинов | 2 | 20: D0-D13 плюс A0-A5 |
| Режимы | LOW, RISING, FALLING, CHANGE — выбираете при подключении | только смена уровня, фронт фильтруется вручную по новому состоянию |
| Обработчики | свой вектор на каждый канал | один вектор на группу: PCINT0_vect для B, PCINT1_vect для C, PCINT2_vect для D |
| Пробуждение из power-down | да | да — и единственный способ слышать не-INT пины, пока чип спит |
// Геркон на D7 (линия PCINT23, порт D). Любая смена уровня — flagSobytia.volatile int flagSobytia = 0;byte portDByl = 0; ISR(PCINT2_vect) { // один обработчик на весь порт D byte izmenil = PIND ^ portDByl; // XOR: какие биты дрогнули portDByl = PIND; // обновляем копию состояния if (izmenil & (1 << PD7)) { // нас интересует только D7 flagSobytia = 1; // в ISR — только флаг }} void setup() { pinMode(7, INPUT_PULLUP); // геркон замыкает пин на землю portDByl = PIND; // первая копия до включения прерываний Serial.begin(9600); PCICR |= (1 << PCIE2); // разрешаем группу PCINT2 (порт D) PCMSK2 |= (1 << PCINT23); // маска: слушаем только D7 Serial.println("Gerkon na D7: zhdem smeny urovnya");} void loop() { if (flagSobytia == 1) { flagSobytia = 0; Serial.print("dver ur="); Serial.println(digitalRead(7)); }}Разбор по строкам. PCICR |= (1 << PCIE2) включает группу порта D; PCMSK2 |= (1 << PCINT23) подводит наблюдение к конкретному пину D7 — остальные семь пинов порта остаются шумными, но невидимыми. В setup() сперва запишите первую копию PIND и только потом включайте прерывания: иначе первое событие придёт от сравнения с нулём, и счётчик дверей начнёт с фантомного открытия.
Внутри ISR(PCINT2_vect) три шага: XOR состояния с копией, обновить копию, проверить нужный бит. Проверка бита обязательна, даже если замаскирован один пин: я однажды повесил параллельно светодиод на D6 того же порта — и счётчик открытий рос от его миганий, потому что сверка с копией была, а проверки бита D7 не было. Правила гигиены те же, что в уроке про прерывания: volatile на разделяемые переменные, никакого Serial и delay внутри обработчика, вся логика — в loop().
Если нужен только переход с высокого на низкий, фильтруйте по новому состоянию: после XOR прочитайте бит PIND и сравните с нулём. Дребезг геркона PCINT ловит так же честно, как ловил бы INT0, — программный антидребезг из урока про кнопку никуда не уходит; моему геркону хватает окна в 50 мс и копии времени последнего принятого события.
Симулятор: прерывание против опроса
Движок PCINT-регистры не эмулирует, и врать не будем: в макетке ниже — аналогия. Кнопка D2 сидит на честном аппаратном прерывании через attachInterrupt: канал INT0 и так слушает свой пин, PCINT там не нужен. А смены тумблера D3 вылавливает опрос в loop(): сравнение с прошлым значением — та же работа, с которой я начинал историю про энкодер. Эта пара и показывает нишу PCINT: на реальной плате он делает для D3 ровно то, что я тут делаю руками, — только без занятого loop() и без пропусков.
Светодиод мигает из обработчика: каждое нажатие D2 переворачивает его состояние. Счётчик irq= считает фронты D2, pol= — смены тумблера, d3= показывает текущий уровень. Самое интересное — сломать опрос нарочно: вставьте в loop() паузу побольше и пощёлкайте тумблером быстро — pol начнёт пропускать смены, пока irq не потеряет ни одной. На плате эту пропажу снимает PCINT ценой двух строк регистров.
После игры с макеткой зафиксирую два практических вывода. PCINT — не замена attachInterrupt, а расширение: если пин 2 или 3 свободен, INT-канал удобнее, потому что различает фронты сам и не требует сравнения с копией порта. А если событий несколько и они попали в один порт — не плодите обработчики: один ISR, заводите по флагу на каждый замаскированный бит и разбирайте флаги в loop(); логика та же, к которой приводит конечный автомат светофора, — событие только взводит состояние, решения принимает главный цикл.
И третье — связка со сном. PCINT — единственный способ услышать смену на не-INT пине из power-down: чип проснулся по геркону на D7, сосчитал событие и снова уснул. Этим мы займёмся дальше: в следующем уроке триплета — сон по кругу на библиотеке LowPower, а база режимов сна и арифметика батарейки уже разобраны в уроке про watchdog и сон. Оттуда же возьмите главное предостережение: Uno с её обвязкой для батарейных узлов не годится, и оба следующих урока написаны под Pro Mini и голый чип.
- На ATmega328P PCINT есть у всех двадцати пинов: порт B — D8-D13, порт C — A0-A5, порт D — D0-D7; INT0 и INT1 остаются только на пинах 2 и 3
- Настройка: маска PCMSK0/1/2 — какие пины слушать, PCICR с битами PCIE0/1/2 — включатель группы; маска без PCICR молчит
- Один вектор на группу: в ISR сравнивайте PINB/PINC/PIND с копией через XOR и проверяйте нужный бит — иначе счётчик врёт от соседей по порту
- Фронтов PCINT не различает — только смену уровня: переход с высокого на низкий фильтруйте по новому состоянию бита, дребезг — антидребезгом
- Главная роль PCINT в моих проектах — будильник: смена любого пина будит чип из power-down, чего на занятых D2 и D3 не сделать никак
- 1. Сколько пинов Uno умеют прерывание по смене уровня PCINT?
- 2. Сколько обработчиков прерываний у группы PCINT2 из восьми пинов порта D?
- 3. Как ISR PCINT выясняет, какой пин порта изменился?
- 4. Чего PCINT не умеет аппаратно по сравнению с INT0 и INT1?
- 5. PCMSK2 настроен, геркон на D7 щёлкает, событий нет. Что забыли?
Частые вопросы по теме урока
- Сколько пинов Uno умеют прерывание по смене уровня PCINT?
- Линии PCINT покрывают оба порта данных и порт аналоговых входов: D0-D13 и A0-A5, всего двадцать пинов в трёх группах. Отдельно от этого INT0 и INT1 дают пины 2 и 3. 20 — каждый пин ATmega328P, включая A0-A5
- Сколько обработчиков прерываний у группы PCINT2 из восьми пинов порта D?
- PCINT2_vect — один вектор на порт D. Кто именно дрогнул, обработчик выясняет сам: XOR регистра PIND с сохранённой копией состояния. один общий вектор на всю группу
- Как ISR PCINT выясняет, какой пин порта изменился?
- Вектор не сообщает источник. Классический приём: izmenil = PIND ^ portDByl; затем обновить копию и проверить свой бит через маску. берёт XOR регистра PIND с сохранённой копией и проверяет нужный бит
- Чего PCINT не умеет аппаратно по сравнению с INT0 и INT1?
- Режимы фронта есть только у INT-каналов. PCINT зовёт обработчик на любую смену, а направление перехода вы определяете по новому уровню бита сами. выбирать RISING или FALLING — фиксируется только сам факт смены
- PCMSK2 настроен, геркон на D7 щёлкает, событий нет. Что забыли?
- Маска определяет, какие пины слушать, но группа включается отдельно — битом PCIE2 в PCICR. Самая тихая ошибка PCINT: всё готово, кроме выключателя. разрешить группу: бит PCIE2 в регистре PCICR