Эта статья будет о программировании. О простом и сложном одновременно — мигании светодиодом.
Зачем все это?
Бывает при программирование какого-нибудь устройства не хватает портов ввода-вывода микроконтроллера. Или из экономических соображений, а может нехватки места в корпусе, не хочется устанавливать дисплей, а как то сигнализировать о режимах работы устройства очень хотелось бы. Часто достаточно сигнализировать о этих режимах горением или миганием светодиода. А если режимов много?
На мысль меня навела автомобильная сигнализация, в которой я как то программировал режим автозапуска. Там, чтобы установить 14-й бит определенного регистра нужно было после входа в режим программирования этого регистра 14 раз нажать на определенную кнопку брелка, а потом дождаться 14-ти коротких сигналов (или мигания поворотников). Затем нажать кнопку в подтверждения и услышать длинный сигнал.
А почему бы это же самое ни использовать в моих прошивках микроконтроллеров:
Режим 1 — мигаем светодиодом один раз в секунду, режим 2 — два раза и так далее …
Как это работает
Рассмотрим мигание светодиодом, подключенным к 13 порту на Ардуино. Это первая программа которую осваивают при изучении Ардуино. Во многих контроллерах, которые мне попадались в последнее время, эта программа зашита на заводе, видимо для тех кто не осилил и это. 🙂
Казалось бы чего проще
1 2 3 4 5 6 7 8 9 10 11 |
void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); } void loop() { digitalWrite(13,HIGH); delay(500); digitalWrite(13,LOW); delay(500); } |
Чередование высокого и низкого уровня на выводе 13 каждые 0.5 секунды.
Можно было установить и два мигания в секунду и три. Но при этом контроллер целиком занят миганием светодиода. Функция delay() подразумевает, что долгие 500 мс больше ничего не происходит (ну разве что обрабатываются прерывания).
Обработка события с использованием millis()
Несложный код, выполняемый в основном цикле делает тоже самое.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); } uint32_t ms, ms1 = 0; bool led_stat = true; void loop() { ms = millis(); // Событие срабатывающее каждые 500 мс if( ( ms - ms1 ) > 500 || ms < ms1 ){ ms1 = ms; // Инвертируем светодиод digitalWrite(13, led_stat); led_stat = !led_stat; } } |
Событие может сдвигаться, если в цикле выполняется еще ряд действий, занимающих какое то время. В этом примере мы добились главного — мигаем светодиодом практически не занимая процессорного времени у основной программы.
Правда чтобы определить двойное и тройное мигание светодиода, нужно написать несколько событий с разным интервалом и реализовать логику включения выключения. Сложно.
Обработка битовой матрицы состояния светодиода
Уменьшаем время срабатывания события до 1/8 секунды и в 1 байте кодируем 8 бит состояний, отображаемых последовательно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// Массив режимов работы светодиода byte modes[] = { 0B00000000, //Светодиод выключен 0B11111111, //Горит постоянно 0B00001111, //Мигание по 0.5 сек 0B00000001, //Короткая вспышка раз в секунду 0B00000101, //Две короткие вспышки раз в секунду 0B00010101, //Три короткие вспышки раз в секунду 0B01010101 //Частые короткие вспышки (4 раза в секунду) }; uint32_t ms, ms1 = 0, ms2 = 0; uint8_t blink_loop = 0; uint8_t blink_mode = 0; uint8_t modes_count = 0; void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); modes_count = 1; blink_mode = modes[modes_count]; } void loop() { ms = millis(); // Событие срабатывающее каждые 125 мс if( ( ms - ms1 ) > 125|| ms < ms1 ){ ms1 = ms; // Режим светодиода ищем по битовой маске if( blink_mode & 1<<(blink_loop&0x07) ) digitalWrite(13, HIGH); else digitalWrite(13, LOW); blink_loop++; } // Этот код служит для демонстрации переключения режимов // Один раз в 5 секунд меняем эффект if( ( ms - ms2 ) > 5000|| ms < ms2 ){ ms2 = ms; blink_mode = modes[modes_count++]; if( modes_count >= 7 )modes_count = 1; } } |
Первый и второй и третий режимы слишком просты а вот дальше начинается интересное. Что может уже нормально использоваться для отображения режимов.
4-й режим. Короткая вспышка 1 раз в секунду
5-й режим. Две короткие вспышки в секунду
6-й режим. Три вспышки.
Ну и постоянная череда коротких вспышек
В принципе, на этом можно было и остановиться, так как для большинства проектов этого бы хватило. Но если этого мало и вам нужно будет разрабатывать программирование автосигнализации )))
Что если 8 бит состояний светодиодов мало?
Можно использовать несколько байт. Например, для кодирования сигнала SOS азбукой Морзе я использовал 4 байта, которые используются последовательно
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
byte bytes[] = {0B00010101,0B00110011,0B10100011,0B00000010}; uint32_t ms, ms1 = 0; uint8_t blink_loop = 0; void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); } void loop() { ms = millis(); // Событие срабатывающее каждые 125 мс if( ( ms - ms1 ) > 125|| ms < ms1 ){ ms1 = ms; // Выделяем сдвиг светодиода (3 бита) uint8_t n_shift = blink_loop&0x07; // Выделяем номер байта в массиве (2 байта со здвигом 3 ) uint8_t b_count = (blink_loop>>3)&0x3; if( bytes[b_count] & 1<< n_shift )digitalWrite(13, HIGH); else digitalWrite(13, LOW); blink_loop++; } } |
Получаем циклический сигнал SOS — три коротких, три длинных и снова три коротких сигнала светодиодом, повторяемый каждые 4 секунды
Для тех, кто считает, что программировать микроконтроллеры в цикле loop() это не по Фен шую 😉 Несмотря на то, что millis() использует прерывание по первому таймеру
Только хардкор. Только прерывания!
Берем 16-ти битный Таймер 1. Устанавливаем прерывание на переполнение за 125мс
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
uint8_t blink_loop = 0; uint8_t blink_mode = 0; uint8_t modes_count = 0; // Начальное значение таймера uint16_t n = 63583; // Обработчик прерывания по переполнению таймера ISR( TIMER1_OVF_vect ) { if( blink_mode & 1<<(blink_loop&0x07) ) digitalWrite(13, HIGH); else digitalWrite(13, LOW); blink_loop++; TCNT1 = n; //выставляем начальное значение TCNT1 } void setup() { pinMode(13,OUTPUT); blink_mode = 0B00000000; // А вот и хардкор - установка регистров таймера TCCR1A = 0; // Устанавливаем делитель 1024 к тактовой частоте 16МГц TCCR1B = 1<<CS22 | 0<<CS21 | 1<<CS20; //Подключаем прерывание по переполнению Timer1 TIMSK1 = 1<<TOIE1; //Загружаем начальное значение таймера для первого цикла TCNT1 = n; sei(); // выставляем бит общего разрешения прерываний} } void loop() { blink_mode = 0B00001111; //Мигание по 0.5 сек delay(5000); blink_mode = 0B00000001; //Короткая вспышка раз в секунду delay(5000); blink_mode = 0B00000101; //Две короткие вспышки раз в секунду delay(5000); blink_mode = 0B00010101; //Три короткие вспышки раз в секунду delay(5000); blink_mode = 0B01010101; //Частые короткие вспышки (4 раза в секунду) delay(5000); } |
Подробно по программированию таймера можно почитать здесь. При этом delay() на 5 секунд в Loop() совершенно не мешают управлению нашим светодиодом.
Недостаток такого метода в том, что не будут работать некоторые функции и библиотеки, использующие таймер 1. Например, ШИМ.
Если с программированием регистров сложно, а прерывание по таймеру использовать интересно —
Прерывание по таймеру с «человеческим лицом»
Добрые люди написали программный интерфейс к таймеру в виде библиотеки TimerOne
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include "TimerOne.h" uint8_t blink_loop = 0; uint8_t blink_mode = 0; uint8_t modes_count = 0; // Callback функция по таймеру void timerIsr() { if( blink_mode & 1<<(blink_loop&0x07) ) digitalWrite(13, HIGH); else digitalWrite(13, LOW); blink_loop++; } void setup() { pinMode(13,OUTPUT); blink_mode = 0B00000000; Timer1.initialize(125000); Timer1.attachInterrupt( timerIsr ); } void loop() { blink_mode = 0B00001111; //Мигание по 0.5 сек delay(5000); blink_mode = 0B00000001; //Короткая вспышка раз в секунду delay(5000); blink_mode = 0B00000101; //Две короткие вспышки раз в секунду delay(5000); blink_mode = 0B00010101; //Три короткие вспышки раз в секунду delay(5000); blink_mode = 0B01010101; //Частые короткие вспышки (4 раза в секунду) delay(5000); } |
Библиотеку с таймером TimerOne можно скачать здесь
Ну, и напоследок, код для тех, кто как и я «грызет» программирование WiFi модулей ESP8266 в среде Arduino IDE.
Прерывание по таймеру в ESP8266
Там другие добрые люди прямо в ядро ESP для Arduino встроили библиотеку Ticker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <Ticker.h> uint8_t blink_loop = 0; uint8_t blink_mode = 0; uint8_t modes_count = 0; Ticker blinker; void timerIsr() { if( blink_mode & 1<<(blink_loop&0x07) ) digitalWrite(13, HIGH); else digitalWrite(13, LOW); blink_loop++; } void setup() { pinMode(13,OUTPUT); blink_mode = 0B00000000; blinker.attach(0.125, timerIsr); } void loop() { blink_mode = 0B00001111; //Мигание по 0.5 сек delay(5000); blink_mode = 0B00000001; //Короткая вспышка раз в секунду delay(5000); blink_mode = 0B00000101; //Две короткие вспышки раз в секунду delay(5000); blink_mode = 0B00010101; //Три короткие вспышки раз в секунду delay(5000); blink_mode = 0B01010101; //Частые короткие вспышки (4 раза в секунду) delay(5000); } |
Использовать прерывания в ESP следует осторожно, так как очень часто это вызывает срабатывание злобного сторожевого таймера WDT, который считает, что на обработку встроенных WiFi функций выделяется слишком мало времени.
Надеюсь, эта статья будет немного полезной для любителей помигать светодиодами )))
Отличная статья! За библиотеку для работы с таймером отдельное спасибо!
Огромное спасибо за изложенный материал!
Мне очень помогла данная статья. Искал пример реализации прерывания по таймеру для esp8266 и наткнулся на вашу статью.
🙂
Спасибо за статью, полезная.
Подумалось, что для индикации режимов можно использовать RGB светодиод, поскольку человеческий глаз различает достаточное количество цветов и оттенков (навскидку, основных цветов + оттенков, различимых всеми, думаю, 255 наберется). Конечно, нужны еще 3 пина, но можно их подключить через микросхему сдвига (будем задействован 1 пин).
В случае RGB светодиода можно завязать интенсивность цвета на какой-то из параметров. Например, температура: бледно светим синим при низкой температуре, интенсивнее при повышении, при переходе определенной границы, начинаем светить бледным красным. Светим ярко-красным когда горячо.
В автосигнализации все сделано для экономии деталей в ущерб юзабилити. Ну правда, считать 14 импульсов задолбаешься.
Ну это ШИМ задействовать.
Я это для индикатора батареи реализовывал
Всё хорошо, но что за типы данных uint8_t, uint16_t и uint32_t? В описании по программированию ардуино такого не нашёл…
uint8_t = unsigned short int; — 1 байт
uint16_t = unsigned int; — 2 байта
uint32_t = unsigned long int; — 4 байта
Добрый человек, написавший TimerOne, кажется, Jesse Tane, вполне заслуживает чтобы упомянули именно так, а не просто как безымянного человека