/*
volatile указывает компилятору, что переменная может измениться в любой момент
внутри функции прерывания (ISR) категорически нельзя делать долгие паузы или
использовать delay().
Прерывание должно быть молниеносным: микроконтроллер бросает все дела, заходит
в функцию прерывания, выполняет её за микросекунды и возвращается к основной
программе. Если заставить его ждать 10 секунд внутри прерывания, зависнет вся
система (даже таймеры, отвечающие за delay(), перестанут нормально работать).
Правильный подход: в прерывании мы только меняем состояние (ставим «флажок»), а в
основном цикле loop(), увидев этот флажок, включаем сирену на 10 секунд.
Что делает attachInterrupt под капотом?
Для пина 2 (PD2 / INT0) используются три ключевых механизма:
1) Регистр EICRA (External Interrupt Control Register A): Определяет, по какому
событию сработает прерывание. Установив биты ISC01 = 1 и ISC00 = 0,
мы говорим МК реагировать на спад сигнала (FALLING), то есть момент нажатия кнопки.
2) Регистр EIMSK (External Interrupt Mask Register): Это локальный переключатель.
Установив бит INT0 в единицу, мы включаем отслеживание прерываний именно на
втором пине.
3) Функция sei() (Set Global Interrupt Flag): Это главный рубильник (регистр SREG).
Разрешает микроконтроллеру в принципе обрабатывать прерывания.
4) Вектор прерывания ISR(INT0_vect): Это аналог функции triggerAlarm() из первого
примера. Это блок кода, куда МК прыгнет при срабатывании прерывания.
В коде мы включили внутренний подтягивающий резистор
(PORTD |= (1 << PD2);), поэтому внешний резистор нам не нужен.
Процессор Arduino Uno — 8-битный. Он читает uint8_t (8 бит / 1 байт) за один такт.
Прерывание физически не успеет вклиниться и сломать данные в процессе чтения
(что легко может произойти с двухбайтным int, который читается за два такта)
I2C это протокол связи, который позволяет микроконтроллеру общаться с кучей разных датчиков и экранов,
используя всего два провода (плюс питание и земля)
1) SDA (Serial Data): По этому проводу передаются сами данные (нули и единицы).
На Arduino это пин A4.
2) SCL (Serial Clock): Это провод синхронизации (тактовый сигнал). Он задает ритм,
чтобы устройства понимали, где заканчивается один бит и начинается другой.
На Arduino это пин A5.
3) Arduino выступает в роли «Мастера». Она управляет шиной, задает ритм (SCL) и решает,
к кому обратиться. Дисплей выступает в роли «Раба» — он слушает шину и отвечает только тогда,
когда Мастер называет его адрес.
4) Адресация: Поскольку на двух проводах может висеть сразу 10-20 устройств,
у каждого есть свой уникальный адрес. Дисплей 1602 с модулем I2C обычно имеет адрес 0x27 или 0x3F.
Модуль I2C работает как переводчик: он получает команды по двум проводам I2C от Arduino
и сам «дергает» нужные ножки дисплея
1) <Wire.h> Подключаем встроенную библиотеку Arduino для работы с шиной I2C
2) #include <LiquidCrystal_I2C.h>: Библиотека, которая знает, какие именно команды нужно отправить
по I2C (через Wire), чтобы дисплей 1602 очистился, включил подсветку или напечатал букву.
3) #include <avr/interrupt.h>: Она дает нам доступ к функции sei() (разрешение прерываний) и
макросу ISR (обработчик прерывания).
LiquidCrystal_I2C lcd(0x27, 16, 2)
Мы создаем объект с именем lcd. В скобках передаем настройки: 0x27 — адрес I2C-модуля на шине,
16 — количество символов в строке, 2 — количество строк.
volatile uint8_t show_message = 0;: Создаем 8-битную беззнаковую переменную-флаг
просим библиотеку отправить по I2C серию команд для инициализации матрицы экрана и
включения светодиода подсветки
TWI Control Register) — Регистр управления:
Это главный пульт управления шиной. С помощью его битов мы отдаем команды аппаратуре. Самые важные биты в нем:
TWEN (TWI Enable): Включает сам аппаратный модуль.
TWSTA (START Condition): Приказывает МК захватить шину и сгенерировать сигнал СТАРТ.
TWSTO (STOP Condition): Приказывает отпустить шину и сгенерировать СТОП.
TWINT (TWI Interrupt Flag): Самый хитрый бит. Когда МК заканчивает любое действие (например, закончил отправку 1 байта), этот флаг поднимается в 1, и модуль замирает.
Чтобы модуль продолжил работу, программист должен записать в этот бит 1 (да, именно единицу для сброса — так устроено железо AVR).
TWDR (TWI Data Register) — Регистр данных:
Если мы хотим отправить байт на дисплей,
мы записываем его в TWDR, а затем дергаем регистр управления TWCR, чтобы начать передачу.
Если мы читаем данные с датчика, то после получения байта мы забираем его из этого же регистра TWDR
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <avr/interrupt.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
volatile uint8_t show_message = 0;
void setup() {
lcd.init();
lcd.backlight();
// выход и напряжение
DDRB |= (1 << PB5);
PORTB &= ~(1 << PB5);
// вход и внутренний подтягивающий резистр
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2);
EICRA |= (1 << ISC01);
EICRA &= ~(1 << ISC00);
EIMSK |= (1 << INT0);
sei();
}
void loop() {
if (show_message) {
PORTB |= (1 << PB5);
lcd.setCursor(0, 0);
lcd.print("Interrupt");
lcd.setCursor(0, 1);
lcd.print("Red light");
delay(5000);
lcd.clear();
PORTB &= ~(1 << PB5);
show_message = 0;
} else {
}
}
ISR(INT0_vect) {
show_message = 1;
}