/*
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 <avr/interrupt.h>
volatile uint8_t show_message = 0;
// ==========================================
// 1. УПРОЩЕННЫЙ, НО ПРАВИЛЬНЫЙ I2C
// ==========================================
void lcd_nibble(uint8_t val) {
// 1. СТАРТ
TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
while (!(TWCR & (1 << TWINT)));
// 2. АДРЕС ЭКРАНА (0x27 сдвинутый влево = 0x4E)
TWDR = 0x4E;
TWCR = (1 << TWINT) | (1 << TWEN);
while (!(TWCR & (1 << TWINT)));
// 3. ОТПРАВЛЯЕМ ДАННЫЕ (EN = 1)
// 0x0C - это бит подсветки(0x08) + бит EN(0x04)
TWDR = val | 0x0C;
TWCR = (1 << TWINT) | (1 << TWEN);
while (!(TWCR & (1 << TWINT)));
// 4. ОТПРАВЛЯЕМ ТЕ ЖЕ ДАННЫЕ (EN = 0)
// 0x08 - это только бит подсветки (EN упал в ноль, буква "защелкнулась")
TWDR = val | 0x08;
TWCR = (1 << TWINT) | (1 << TWEN);
while (!(TWCR & (1 << TWINT)));
// 5. СТОП
TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
// КРИТИЧЕСКИ ВАЖНО: Ждем, пока аппаратура реально сгенерирует СТОП
// Без этой строчки следующий СТАРТ намертво повесит контроллер!
while(TWCR & (1 << TWSTO));
}
// Отправляем букву или команду целиком (режем байт пополам)
void lcd_write(uint8_t data, uint8_t rs) {
lcd_nibble((data & 0xF0) | rs); // Отправили левую половину байта
lcd_nibble(((data << 4) & 0xF0) | rs); // Отправили правую половину байта
}
// ==========================================
// 2. ОСНОВНОЙ КОД
// ==========================================
void setup() {
// Настройка I2C на 100 кГц и включение модуля заранее
TWBR = 72;
TWCR = (1 << TWEN);
// --- Стандартная инициализация экрана из даташита ---
delay(50);
lcd_nibble(0x30); delay(5);
lcd_nibble(0x30); delay(1);
lcd_nibble(0x30); delay(1);
lcd_nibble(0x20); delay(1); // Включаем 4-битный режим связи
lcd_write(0x28, 0); // 2 строки
lcd_write(0x0C, 0); // Включить экран
lcd_write(0x01, 0); // Очистить экран
delay(2);
// --- Настройка портов и прерываний ---
DDRB |= (1 << PB5); // LED выход (Пин 13)
PORTB &= ~(1 << PB5); // LED выкл
DDRD &= ~(1 << PD2); // Кнопка вход (Пин 2)
PORTD |= (1 << PD2); // Кнопка подтяжка
EICRA |= (1 << ISC01);
EICRA &= ~(1 << ISC00);
EIMSK |= (1 << INT0);
sei();
}
void loop() {
if (show_message) {
PORTB |= (1 << PB5); // Зажгли диод
// Пишем слово "ALARM" по буквам (1 - значит это текст, а не команда)
lcd_write(0x80, 0); // Команда: поставить курсор в начало
lcd_write('A', 1);
lcd_write('L', 1);
lcd_write('A', 1);
lcd_write('R', 1);
lcd_write('M', 1);
delay(5000);
lcd_write(0x01, 0); // Команда: очистить экран
delay(2); // После очистки всегда нужна пауза 2 мс
PORTB &= ~(1 << PB5); // Погасили диод
show_message = 0;
}
}
ISR(INT0_vect) {
show_message = 1;
}