Добавить в корзинуПозвонить
Найти в Дзене

Простая программируемая мигалка на ESP32-S3 и 74HC595

Мы создадим элементарную схему, которая позволяет управлять восемью светодиодами всего с четырьмя пинами микроконтроллера. Это станет основой для понимания ключевых принципов embedded-разработки: Экономия пинов GPIO с помощью сдвигового регистра. Необходимые компоненты Микроконтроллер ESP32-S3 имеет много пинов, но что, если нужно управлять 50 светодиодами? 74HC595 решает эту проблему: Аналогия: Представьте, что вы хотите передать слово из 8 букв через узкую трубу (один провод). Вы кричите буквы по одной (последовательно), а на другом конце кто-то записывает их в строку и только потом показывает всем (параллельно). Когда кнопка не нажата, её контакты не соединены. Пин микроконтроллера "висит в воздухе" — это состояние называется "плавающий вход" и может читаться как 0, так и 1 случайным образом. Решение: Используем внутреннюю подтяжку пина к питанию через INPUT_PULLUP. Теперь, когда кнопка не нажата, пин подключен к 3.3V через большой резистор (20-50 кОм внутри чипа) и читается как HIG
Оглавление

Что будем делать и зачем

Мы создадим элементарную схему, которая позволяет управлять восемью светодиодами всего с четырьмя пинами микроконтроллера. Это станет основой для понимания ключевых принципов embedded-разработки:

Экономия пинов GPIO с помощью сдвигового регистра.

  1. Чтение цифрового ввода (кнопка) с защитой от дребезга.
  2. Неблокирующая логика работы с помощью millis().
  3. Программное переключение состояний.
Полученная схема
Полученная схема

Необходимые компоненты

Таблица компонентов
Таблица компонентов

Как это работает: теория за 2 минуты

1. Сдвиговый регистр — наш "расширитель портов"

Микроконтроллер ESP32-S3 имеет много пинов, но что, если нужно управлять 50 светодиодами? 74HC595 решает эту проблему:

  • Принимает данные последовательно (бит за битом) по одному проводу (DATA)
  • Тактируется вторым проводом (CLOCK)
  • После получения всех 8 бит "защелкивает" их третьим сигналом (LATCH)
  • Выдает данные параллельно на 8 выходов (Q0-Q7)

Аналогия: Представьте, что вы хотите передать слово из 8 букв через узкую трубу (один провод). Вы кричите буквы по одной (последовательно), а на другом конце кто-то записывает их в строку и только потом показывает всем (параллельно).

2. Кнопка и подтягивающий резистор

Когда кнопка не нажата, её контакты не соединены. Пин микроконтроллера "висит в воздухе" — это состояние называется "плавающий вход" и может читаться как 0, так и 1 случайным образом.

Решение: Используем внутреннюю подтяжку пина к питанию через INPUT_PULLUP. Теперь, когда кнопка не нажата, пин подключен к 3.3V через большой резистор (20-50 кОм внутри чипа) и читается как HIGH (1). При нажатии кнопка замыкает пин на землю (GND), и состояние становится LOW (0).

1 - Четкое состояние 0. 2 - четкое состояние 1. 3 - "плавающий вход"
1 - Четкое состояние 0. 2 - четкое состояние 1. 3 - "плавающий вход"

Изображение взято с сайта.

3. Почему millis() вместо delay()?

Функция delay(500) останавливает выполнение всей программы на 500 мс. В это время микроконтроллер не может реагировать на кнопку, вычислять что-либо, или управлять другими устройствами.

millis() возвращает количество миллисекунд с момента включения. Сравнивая текущее время с сохраненным значением, мы можем выполнять действия через определенные интервалы без остановки программы.

Собираем схему

Подключение 74HC595 к ESP32-S3

Регистр сдвига, его схема
Регистр сдвига, его схема

ESP32-S3 -> 74HC595

GPIO12 (пин 12) -> DS(14) // Данные

GPIO13 (пин 13) -> SH_CP(11) // Тактовый сигнал

GPIO14 (пин 14) -> ST_CP(12) // Защелка (обновление выходов)

3.3V -> VCC (16) // Питание

GND -> GND (8) // Земля

-> MR (10) // Сброс (подтянуть к VCC)

-> OE (13) // Разрешение выходов (на GND)

Пояснение:

  • MR (Master Reset) — при низком уровне сбрасывает регистр. У нас всегда высокий (3.3V).
  • OE (Output Enable) — включает выходы. У нас всегда низкий (GND), значит выходы всегда активны.

Подключение светодиодов

Важно: У светодиода есть полярность! Более длинная ножка — анод (+), подключается к Q. Более короткая — катод (-), подключается к GND через резистор.

Q0 (15) -> |->| LED1 -> резистор 220 Ом -> GND
Q1 (1) -> |->| LED2 -> резистор 220 Ом -> GND
...
Q7 (7) -> |->| LED8 -> резистор 220 Ом -> GND

Подключение кнопки

ESP32 GPIO4 (пин 4) -> один контакт кнопки Второй контакт кнопки -> GND

Подключение кнопки
Подключение кнопки

Внешний резистор не нужен! Используем внутреннюю подтяжку в коде.

Программа: от простого к сложному

Шаг 1: Базовый код с блокирующей задержкой

#define DATA_PIN 12
#define CLOCK_PIN 13
#define LATCH_PIN 14
#define BUTTON_PIN 4
void setup() {
pinMode(DATA_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // Ключевой момент!
}
// Функция записи байта в регистр
void writeToRegister(uint8_t data) {
digitalWrite(LATCH_PIN, LOW); // Готовимся к приему
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, data); // Отправляем 8 бит
digitalWrite(LATCH_PIN, HIGH); // Показываем результат
}
void loop() {
// Простейший бегущий огонь
for(int i = 0; i < 8; i++) {
writeToRegister(1 << i); // 1, 2, 4, 8, 16, 32, 64, 128
delay(200);
}
}

Проблема: Нажмите и удержите кнопку во время работы. Ничего не происходит! Код "застрял" в delay() и не проверяет кнопку.

Реализация

Шаг 2: Добавляем обработку кнопки (неудачная попытка)

void loop() {
// Проверяем кнопку
if(digitalRead(BUTTON_PIN) == LOW) {
// Включаем все светодиоды при нажатии
writeToRegister(0xFF); // 0xFF = 255 = все 8 бит в 1
} else {
// Бегущий огонь, когда кнопка отпущена
for(int i = 0; i < 8; i++) {
writeToRegister(1 << i);
delay(200);
}
}
}

Проблема: Теперь кнопка работает, но бегущий огонь прерывается. Нужно, чтобы эффект продолжался, а кнопка просто переключала режимы. Для этого нам нужно состояние.

Реализация

Шаг 3: Вводим состояние и millis()

// Глобальные переменные
int currentEffect = 0; // 0 = бегущий огонь, 1 = все горят
unsigned long lastChange = 0;
int ledPosition = 0;
void loop() {
unsigned long now = millis(); // Текущее время
// Проверка кнопки с простейшей задержкой
static unsigned long lastButtonCheck = 0;
if(now - lastButtonCheck > 50) { // Проверяем кнопку каждые 50мс
lastButtonCheck = now;
if(digitalRead(BUTTON_PIN) == LOW) {
currentEffect = 1 - currentEffect; // Переключаем 0<->1
delay(300); // Ждем, пока палец уберут
}
}
// Выполняем текущий эффект
if(currentEffect == 0) { // Бегущий огонь
if(now - lastChange > 200) { // Каждые 200мс
lastChange = now;
writeToRegister(1 << ledPosition);
ledPosition = (ledPosition + 1) % 8;
}
} else { // Все светодиоды горят
writeToRegister(0xFF);
}
}

Улучшение: Теперь кнопка переключает эффекты, и анимация не прерывается! Но есть проблема с дребезгом кнопки.

Реализация

Шаг 4: Полноценная обработка кнопки с антидребезгом

// Переменные для обработки кнопки
int buttonState = HIGH; // Текущее отфильтрованное состояние
int lastButtonState = HIGH; // Предыдущее состояние
unsigned long lastDebounceTime = 0;
void loop() {
unsigned long now = millis();
// === ОБРАБОТКА КНОПКИ С АНТИДРЕБЕЗГОМ ===
int reading = digitalRead(BUTTON_PIN); // Сырое значение
// Если состояние изменилось (может быть дребезг)
if (reading != lastButtonState) {
lastDebounceTime = now; // Сбрасываем таймер
}
// Если прошло достаточно времени (50мс) после последнего изменения
if ((now - lastDebounceTime) > 50) {
// Если состояние действительно изменилось
if (reading != buttonState) {
buttonState = reading;
// Если кнопку нажали (переход HIGH -> LOW)
if (buttonState == LOW) {
// Меняем эффект
currentEffect = (currentEffect + 1) % 3; // Теперь 3 эффекта
}
}
}
lastButtonState = reading; // Сохраняем для следующего цикла
// === ВЫПОЛНЕНИЕ ЭФФЕКТОВ ===
switch(currentEffect) {
case 0: // Бегущий огонь
if(now - lastChange > 200) {
lastChange = now;
writeToRegister(1 << ledPosition);
ledPosition = (ledPosition + 1) % 8;
}
break;
case 1: // Все горят
writeToRegister(0xFF);
break;
case 2: // Двойной бег
if(now - lastChange > 150) {
lastChange = now;
// Два светодиода на противоположных сторонах
static int pos1 = 0, pos2 = 4;
writeToRegister((1 << pos1) | (1 << pos2));
pos1 = (pos1 + 1) % 8;
pos2 = (pos2 + 1) % 8;
}
break;
}
}

Как работает антидребезг: пошагово

  1. Дребезг — это когда контакты кнопки при нажатии замыкаются и размыкаются несколько раз за миллисекунды.
  2. Читаем сырое значение (reading) — оно может быстро меняться.
  3. При любом изменении сбрасываем таймер.
  4. Ждем 50 мс — за это время дребезг прекращается.
  5. Если через 50 мс состояние стабильно и отличается от предыдущего — это реальное нажатие.
  6. Фиксируем нажатие только при переходе HIGH → LOW.

Готовый финальный код

#define DATA_PIN 12
#define CLOCK_PIN 13
#define LATCH_PIN 14
#define BUTTON_PIN 4
// Переменные для таймеров и состояний
int currentEffect = 0;
unsigned long lastChange = 0;
int ledPosition = 0;
// Переменные для кнопки
int buttonState = HIGH;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
void setup() {
pinMode(DATA_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(115200);
Serial.println("Система запущена!");
}
void writeToRegister(uint8_t data) {
digitalWrite(LATCH_PIN, LOW);
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, data);
digitalWrite(LATCH_PIN, HIGH);
}
void loop() {
unsigned long now = millis();
// === АНТИДРЕБЕЗГ КНОПКИ ===
int reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonState) {
lastDebounceTime = now;
}
if ((now - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == LOW) {
currentEffect = (currentEffect + 1) % 3;
Serial.print("Эффект: ");
Serial.println(currentEffect);
}
}
}
lastButtonState = reading;
// === ВЫБОР И ВЫПОЛНЕНИЕ ЭФФЕКТА ===
switch(currentEffect) {
case 0: // Бегущий огонь
if(now - lastChange > 200) {
lastChange = now;
writeToRegister(1 << ledPosition);
ledPosition = (ledPosition + 1) % 8;
}
break;
case 1: // Все включены
writeToRegister(0xFF);
break;
case 2: // Мигание
if(now - lastChange > 300) {
lastChange = now;
static bool lightsOn = true;
writeToRegister(lightsOn ? 0xFF : 0x00);
lightsOn = !lightsOn;
}
break;
}
}

Реализация

Почему именно эта реализация?

  1. Простота: Минимальное количество кода для решения задачи
  2. Эффективность: Нет блокирующих задержек, система отзывчива
  3. Масштабируемость: Легко добавить новый эффект — просто новый case
  4. Надежность: Антидребезг устраняет ложные срабатывания
  5. Понятность: Каждый блок кода выполняет одну конкретную задачу

Что можно улучшить дальше?

  1. Добавить больше эффектов: Создайте эффект "сердцебиение", "случайные вспышки", "затухание"
  2. Регулировать скорость: Добавьте вторую кнопку или потенциометр для изменения скорости анимации
  3. Сохранять состояние: Используйте EEPROM, чтобы запомнить последний эффект после выключения
  4. Сделать каскад: подключите второй регистр 74HC595 к первому и управляйте 16 светодиодами

Заключение

Вы создали полноценную управляемую систему всего на 15 строчках кода (без учета эффектов)! Этот проект демонстрирует ключевые принципы, которые применяются в 90% embedded-проектов:

  • Разделение задач (опрос кнопки, выполнение эффекта)
  • Работа с временем без блокировки
  • Обработка пользовательского ввода с защитой от ошибок
  • Управление внешними устройствами через стандартные интерфейсы

Теперь у вас есть основа, на которой можно строить более сложные системы: светомузыку, бегущие строки, или даже простые игры на светодиодах.

N. Нейросетевая генерация статьи

Текст статьи генерируется с помощью нейросетей, и затем правится автором с
добавлением собственного (не большого, но достаточного) опыта. Если вы
заметили не сходящиеся данные или любые другие ошибки убедительная
просьба о них сообщить! Все примеры, код и подключения проверены автором и предъявлены результаты по ссылке в слове РЕАЛИЗАЦИЯ.

Использованные нейросети: DeepSeek+Глубокое мышление

Теги: #ESP32 #74HC595 #Arduino #DIY #электроника #программирование

#светодиоды #микроконтроллеры #своимируками #PlatformIO

#бегущийогонь #схемотехника #электроникадляначинающих

#LED #Embedded #мигалка