Сегодня разберем проект умного таймера на Arduino Nano, который умеет:
✅ Отсчитывать время с точностью до миллисекунд
✅ Управлять нагрузкой через реле
✅ Выводить данные на LCD экран
✅ Реагировать на кнопки: старт, пауза, сброс, +/- время
Необходимые компоненты:
- Arduino Nano
- LCD 16x2 с модулем I2C (адрес 0x27)
- Релейный модуль 5V
- 4 кнопки
- Провода и макетная плата
Схема подключений
1. Кнопки с внешними подтягивающими резисторами
- Резистор 10 кОм подключен между пином Arduino и +5V.
- Кнопка подключена между пином и GND.
- Принцип работы:
Когда кнопка не нажата: пин через резистор подтянут к +5V → читается HIGH.
Когда кнопка нажата: пин замыкается на GND → читается LOND.
⚠️ Внимание! В предоставленном коде используются внутренние подтягивающие резисторы (INPUT_PULLUP), поэтому внешние резисторы не нужны. Эта схема — альтернативный вариант.
Кнопки с внутренними подтягивающими резисторами (рекомендуется)
Кнопка ADD: D2 ----◄ КНОПКА ---- GND
Кнопка START: D3 ----◄ КНОПКА ---- GND
В коде:
buttons[i].attach(BUTTON_PINS[i], INPUT_PULLUP);
Преимущества:
Не требует внешних компонентов.
Меньше проводов.
Сравнение вариантов подключения кнопок
Параметр Внешний резистор Внутренний INPUT_PULLUP
Схема Пин → 10kΩ → 5V Пин → Кнопка → GND
Пин → Кнопка → GND
Код INPUT INPUT_PULLUP
Состояние покоя HIGH HIGH
При нажатии LOW LOW
Плюсы Гибкость Простота, экономия места
Минусы Требует резисторы Нет
Важно!
Если используете схему с внешними резисторами:
- Уберите INPUT_PULLUP из кода:
- buttons[i].attach(BUTTON_PINS[i], INPUT); // Без подтяжки
Для внутренних резисторов (как в исходном коде):
Резисторы не требуются, код уже оптимизирован.
Подключение:
Компонент Пин Arduino Примечание
LCD SDA A4 Синий провод
LCD SCL A5 Желтый провод
LCD VCC 5V Красный провод
LCD GND GND Черный провод
Реле (IN) D12 Сигнальный провод
Кнопка ADD D2 Подключена к GND
Кнопка START D3 Подключена к GND
Кнопка STOP D4 Подключена к GND
Кнопка SUB D5 Подключена к GND
Важно! Все кнопки подключаются по схеме: пин Arduino → кнопка → GND. Внутренние подтягивающие резисторы активированы в коде.
1. Подключение библиотек
#include <Bounce2.h> // Для обработки кнопок (антидребезг) #include <Wire.h> // Для работы с I2C #include <LiquidCrystal_I2C.h> // Для LCD дисплея
2. Настройка LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Адрес дисплея 0x27, размер 16x2 // Если не работает — проверьте адрес сканером I2C!
3. Конфигурация реле
const int RELAY_PIN = 12; // Пин управления реле const bool RELAY_ACTIVE = LOW; // Активация LOW (для большинства модулей) const bool RELAY_IDLE = HIGH; // Режим покоя
4. Кнопки
5. Инициализация в setup()
void setup() { // Настройка реле
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELAY_IDLE); // Выключаем реле при старте
// Настройка кнопок с антидребезгом
for(uint8_t i = 0; i < 4; i++) {
buttons[i].attach(BUTTON_PINS[i], INPUT_PULLUP); // Внутренний резистор
buttons[i].interval(25); // Интервал обработки дребезга
}
// Инициализация дисплея
lcd.init(); // Запуск LCD
delay(100); // Пауза для стабилизации
lcd.backlight(); // Включаем подсветку
lcd.clear(); // Очищаем экран
lcd.print("TEXT"); // Стартовый текст }
Логика работы
1. Основной цикл
void loop() {
updateButtons(); // Чтение кнопок
processLogic(); // Обработка таймера
updateDisplay(); // Обновление экрана
controlRelay(); // Управление реле
}
2. Обработка кнопок (пример для ADD)
void handleAddButton() {
if(buttons[0].fell() && !isRunning) { // Если кнопка нажата
timerValue += 250; // +250 мс
resetExpiredState(); // Сброс состояния
}
}
3. Форматирование времени
void printPaddedTime(uint32_t time) {
uint16_t sec = time / 1000; // Секунды
uint16_t ms = time % 1000; // Миллисекунды
lcd.print(sec < 10 ? "0" : ""); // Добавляем ноль для 05 → "05"
lcd.print(sec);
lcd.print(".");
if(ms < 100) lcd.print("0"); // 250 → "250", 50 → "050"
if(ms < 10) lcd.print("0");
lcd.print(ms);
}
Если не работает LCD:
Поверните синий потенциометр на модуле I2C
Проверьте адрес через сканер I2C
Убедитесь, что подключены SDA (A4) и SCL (A5)
Реле не щелкает:
- Проверьте логику активации (LOW/HIGH)
- Тестовый код:
digitalWrite(RELAY_PIN, LOW); delay(1000); // Включить
digitalWrite(RELAY_PIN, HIGH); delay(1000); // Выключить
Кнопки не реагируют:
Убедитесь, что подключение: пин → кнопка → GND
Проверьте параметр INPUT_PULLUP в коде
Полный код для ардуино нано.
#include <Bounce2.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); // Адрес 0x27, 16x2 дисплей
// Конфигурация реле
const int RELAY_PIN = 12; // Пин реле
const bool RELAY_ACTIVE = LOW; // Логика активации (LOW/HIGH)
const bool RELAY_IDLE = HIGH;
// Пины кнопок
const int BUTTON_PINS[] = {2, 3, 4, 5}; // ADD, START, STOP/RESET, SUB
// Состояния таймера
volatile uint32_t timerValue = 0;
uint32_t targetTime = 0;
bool isRunning = false;
bool isExpired = false;
Bounce buttons[4]; // Массив для обработки кнопок
void setup() {
// Инициализация реле
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELAY_IDLE);
// Инициализация кнопок
for(uint8_t i = 0; i < 4; i++) {
buttons[i].attach(BUTTON_PINS[i], INPUT_PULLUP);
buttons[i].interval(25);
}
// Инициализация дисплея с проверкой
lcd.init();
delay(100); // Даем время на инициализацию
lcd.backlight();
lcd.clear();
// Вывод стартового сообщения
lcd.setCursor(0, 0);
lcd.print("TEXT");
lcd.setCursor(0, 1);
lcd.print("Time: 00.000");
// Настройка контрастности через потенциометр на модуле I2C
// Поворачивайте отверткой пока не появится текст!
}
void loop() {
updateButtons();
processLogic();
updateDisplay();
controlRelay();
}
void updateButtons() {
for(uint8_t i = 0; i < 4; i++) {
buttons[i].update();
}
}
void processLogic() {
handleAddButton();
handleStartButton();
handleStopResetButton();
handleSubButton();
checkTimer();
}
void handleAddButton() {
if(buttons[0].fell() && !isRunning) {
timerValue += 250;
resetExpiredState();
}
}
void handleStartButton() {
if(buttons[1].fell() && !isRunning && timerValue > 0) {
targetTime = millis() + timerValue;
isRunning = true;
resetExpiredState();
}
}
void handleStopResetButton() {
if(buttons[2].fell()) {
if(isRunning) {
timerValue = (targetTime > millis()) ? targetTime - millis() : 0;
isRunning = false;
} else {
timerValue = 0;
}
resetExpiredState();
}
}
void handleSubButton() {
if(buttons[3].fell() && !isRunning) {
timerValue = (timerValue >= 250) ? timerValue - 250 : 0;
resetExpiredState();
}
}
void checkTimer() {
if(isRunning && (millis() >= targetTime)) {
isRunning = false;
timerValue = 0;
isExpired = true;
}
}
void resetExpiredState() {
if(isExpired) {
isExpired = false;
digitalWrite(RELAY_PIN, RELAY_IDLE);
}
}
void updateDisplay() {
static uint32_t lastUpdate = 0;
if(millis() - lastUpdate < 50) return;
lastUpdate = millis();
uint32_t displayTime = isRunning ?
((targetTime > millis()) ? targetTime - millis() : 0)
: timerValue;
lcd.setCursor(6, 1);
printPaddedTime(displayTime);
}
void printPaddedTime(uint32_t time) {
uint16_t sec = time / 1000;
uint16_t ms = time % 1000;
lcd.print(sec < 10 ? "0" : ""); lcd.print(sec);
lcd.print(".");
if(ms < 100) lcd.print("0");
if(ms < 10) lcd.print("0");
lcd.print(ms);
lcd.print(" "); // Очистка остаточных символов
}
void controlRelay() {
digitalWrite(RELAY_PIN, isExpired ? RELAY_ACTIVE : RELAY_IDLE);
}