Мигание светодиода в Ардуино, что может быть проще и бесполезнее. На самом деле практическую пользу от этой простой функции можно найти.

Бывает при программирование какого-нибудь устройства не хватает портов ввода-вывода микроконтроллера. Или из экономических соображений, а может нехватки места в корпусе, не хочется устанавливать дисплей, а как то сигнализировать о режимах работы устройства очень хотелось бы. Часто достаточно сигнализировать о этих режимах горением или миганием светодиода. А если режимов много?

На мысль меня навела автомобильная сигнализация, в которой я как то программировал режим автозапуска. Там, чтобы установить, например, 14-й бит определенного регистра нужно было после входа в режим программирования этого регистра 14 раз нажать на определенную кнопку брелка, а потом дождаться 14-ти коротких сигналов (или мигания поворотников). Затем нажать кнопку в подтверждения и услышать длинный сигнал. Гениально! Никаких дисплеев и экранных меню. Правда, одновременно, и жутко неудобно.

Но если внутренних режимов немного, то использовать количество морганий светодиодом вполне функционально.

Начнем с простого.

Пример мигания светодиодом для Ардуино



Это первая программа которую осваивают при изучении Ардуино. Во многих контроллерах, которые мне попадались в последнее время, эта программа зашита на заводе, видимо для тех кто не осилил и это.

Простейший пример мигания светодиодом
void setup() {
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW); 
}

void loop() {
  digitalWrite(13,HIGH);
  delay(500);
  digitalWrite(13,LOW);
  delay(500);
}



Казалась бы задавай различные интервалы между высокими и низкими уровнями порта и будет нужное. Но при этом контроллер больше ничего не делает (ну почти ничего, прерывания он все таки обрабатывает). Делать что-то еще он конечно может, но не в основном цикле loop().

Поэтому отказываемся от delay() и переходим на события с использованием millis()

Использование событий с использованием millis()



Код мигания светодиодом с использованием millis()
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;
   }
}



Ну вот. Цель достигнута. Светодиод мигает, а процессорное время в цикле loop() практически полностью доступно для других функций. Правда в таком коде использование требуемых режимов мигания реализуется довольно сложно — несколько событий с разными интервалами, много условий по необходимому режиму и предыдущему состоянию. Слишком сложно.

Обработка битовой матрицы состояния светодиода



Уменьшаем время срабатывания события до 1/8 секунды и в 1 байте кодируем 8 бит состояний, отображаемых последовательно.
Код мигания светодиода с битовой матрицей состояний
// Массив режимов работы светодиода
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;
   }
}



Первые три режима работы светодиода простые. А вот остальные уже можно использовать для демонстрации режима микроконтроллера:

Короткая вспышка 1 раз в секунду

Две вспышки в секунду

Три вспышки

И постоянные вспышки четыре раза в секунду


В принципе, на этом можно было и остановиться, так как для большинства проектов этого бы хватило. Но если этого мало и вам нужно будет разрабатывать программирование автосигнализации )))

Что если 8 бит состояний светодиодов мало?

Использование 4-х байт для определения состояния светодиода



Код сигнала SOS азбукой Морзе
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 секунды


Очень много людей критиковали Ардуино за ужасный стиль программирования микроконтроллеров без использования прерываний

Только хардкор. Только прерывания!



Берем 16-ти битный Таймер 1. Устанавливаем прерывание на переполнение за 125мс

Код многорежимного мигания светодиода с использованием прерываний по таймеру
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

Код многорежимного мигания светодиодом с использованием TimerOne
#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
Код многорежимного мигания светодиода по таймеру в ESP8266
#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 функций выделяется слишком мало времени.

Надеюсь, эта статья будет немного полезной для всех любителей мигать светодиодами в Ардуино и не только им.

О всех моих экспериментах с микроконтроллерами и умным домом читайте в мое блоге

Комментарии (28)


  1. AntonSor
    07.02.2016 10:33
    +1

    Спасибо, очень интересно. Надо будет где-нибудь применить такой метод индикации. Только таблицу с расшифровкой морганий придется вешать на готовом устройстве, а то через неделю забудется, что там чему соответствует.


    1. sav13
      07.02.2016 10:35

      Или азбуку Морзе учить и первые буквы режимов траслировать )))


      1. kalbasa
        07.02.2016 11:58

        в последнее время перешёл на вот такую конструкцию

        pause = millis() % 200; // pause will loop from 0 to 199
        if(pause == 0)
        {
        digitalWrite(13,!digitalRead(13));
        }


        1. sav13
          07.02.2016 13:37

          Может тогда так для классики жанра?

          if( !( millis() % 200 ) )digitalWrite( 13, !digitalRead( 13 ) );
          


          1. kalbasa
            07.02.2016 13:53

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


            1. sav13
              07.02.2016 14:06

              Последняя 1.6.7?
              Она еще и с ESP8266 core плохо дружит. Уже сколько народу присылало претензии, что код не хочет компилироваться. При откате на 1.6.5 все собиралось как часы
              Кстати, кто мешает фигурные скобки в одну строчку использовать?

              if( !( millis() % 200 ) ){ digitalWrite( 13, !digitalRead( 13 ) ); }
              

              Это Си, великий и могучий!


              1. kalbasa
                07.02.2016 14:53

                трындуино айди мешает :))
                точнее скомпилируется то нормально и зальётся на ура. вот только в цикле эта строчка работать не будет и светодиод естественно не будет моргать. такую фигню заметил в последней версии трындуино айди когда размашистый код марафетил, что бы читать одной строчкой. за более ранние версии ничего сказать не могу. но на поиск касячка потратил время. потому и сказал, что надо придерживаться дьявольской конструкции в предыдущем комменте.


              1. kalbasa
                07.02.2016 14:54

                последняя это 1.6.7


  1. Celtis
    07.02.2016 13:32

    Использовать прерывания в ESP следует осторожно, так как очень часто это вызывает срабатывание злобного сторожевого таймера WDT, который считает, что на обработку встроенных WiFi функций выделяется слишком мало времени.
    В NodeMCU это решается вызовом в длинном цикле tmr.wdclr(), который сбрасывает watchdog- счетчик. Но тут нужно быть уверенным, что цикл не станет бесконечным ни при каких условиях.


  1. sav13
    07.02.2016 13:42

    Даже если бы сброс WDT работал без глюков, это не всегда спасает.
    Например, делаю выгрузку LOG-фала с SD-карточки через WEB-сервер.
    При размере файла 100-200к WDT гарантированно срабатывает.


    1. Celtis
      07.02.2016 14:26

      Ва подключили SD-карточку непосредстенно к 8266?
      Было бы очень интересно увидеть статью на эту тему!


  1. sav13
    07.02.2016 14:36

    А что там подключать?
    Пример есть в ESP8266 Core для Arduino IDE
    Внешний SPI там работает:
    pinMode(SCK, SPECIAL); ///< GPIO14
    pinMode(MISO, SPECIAL); ///< GPIO12
    pinMode(MOSI, SPECIAL); ///< GPIO13
    ну а CS к любому GPIO

    Там даже есть WEB-сервер с файлами на карточке!

    Что не попробовал еще, это зарузку программы с карты памяти


  1. icbook
    07.02.2016 18:09

    Передача данных с использованием манчестерского кода давно уже не новость в использовании светодиодов.


    1. sav13
      07.02.2016 19:54
      +1

      Реализация манчестерского кода несколько сложнее, чем описано в данной статье. В ту же тиньку 13 ее еще нужно умудриться запихнуть.


      1. icbook
        07.02.2016 22:00

        Это правда. Зато появляется возможность общаться на «понятном языке», как это делает Intel и многие другие известные бренды. Азбука Морзе в данном случае видится не лучшим выходом для коммуникации. Хотя… Если будет вменяемое мобильное приложение, способное транслировать мигание светодиода в текстовые сообщения, то какая, по сути, разница?


  1. GrakovNe
    07.02.2016 19:24

    ну почти ничего, прерывания он все таки обрабатывает

    Если память мне не изменяет, ничего он не обрабатывает как раз, иначе немного непонятно, кто гарантирует мне, что моя задержка будет длиться ровно 500 мс. Представим, что я отвратительный программист и по приемке байта в UART устроил себе O(N^4) да еще и с пересылкой этого байта пару сотен раз обратно. А в цикле у меня _delay_ms(500);. Контроллер, дойдя до этой строки, радостно делает NOP много-много раз. А на 250-й миллисекунде ему как раз байт прислали. Если бы прерывания не были бы запрещены, _delay_ms(500) легко могло бы превратиться в _delay_ms(650), а это сулит безумные проблемы с программно реализованными протоколами. Так что, первое, что делает delay, это выдает ASM(«CLI»), чтобы неповадно было.


    1. sav13
      07.02.2016 19:51

      Ну delay() точно не блокирует прерывания. Все примеры с прерываниями в данной статье это иллюстрируют. Можете убедиться, примеры абсолютно рабочие, хотя там сплошные delay() в цикле.
      А кто сказал, что delay делает NOP много раз? За delay() скорее всего отвечает 0-й таймер, который генерит миллисекунды для функции millis()


      1. GrakovNe
        07.02.2016 20:17

        Может, с июня '14 много что изменилось в стандартной либе, но вот здесь как раз человеку приходится писать неблокирующий delay() для Arduino.
        P.S. А что будет, если у меня заняты все таймеры контроллера, но delay я все равно хочу? Там же не RTOS с диспетчером крутится


        1. sav13
          07.02.2016 20:22

          Вы с delayMicrosecons() не путаете?

          void delay(unsigned long ms)
          {
          	uint16_t start = (uint16_t)micros();
          
          	while (ms > 0) {
          		yield();
          		if (((uint16_t)micros() - start) >= 1000) {
          			ms--;
          			start += 1000;
          		}
          	}
          } 
          

          Вот код delay() из ардуины. Где там блокирование прерываний?


          1. sav13
            07.02.2016 20:32

            p.s. Кстати в той ссылке, что вы привели написано, что один из подходов обхождения «блокировки delay()» это использование прерывания, но это не его метод )))

            Его метод очень похож на мой второй-третий пример, только все зашито в функцию, а не прямо в цикле условия


  1. tormozedison
    07.02.2016 21:41
    +1

    Звуком будет удобнее, чем светом. Займёт также одну ногу. В биосах применяется давно.


    1. sav13
      07.02.2016 21:48

      С одной стороны вы правы. Звук в Ардуино реализуется встроенной функцией tone() очень просто.
      С другой стороны, если все устройства моего умного дома буду периодически (при смене режимов) день и ночь попискивать, то через какое то время со мной, как изготовителем может случиться акт насилия )))


      1. tormozedison
        07.02.2016 23:24

        Если тон выбрать басовый, раздражать почти не будет. О вкусах не спорят, но меня мигание раздражает больше.


  1. Elmot
    07.02.2016 22:35

    Ужас какой… куда катится гиктаймс?


  1. nommoke
    08.02.2016 05:10

    Мне кажется хорошо было бы создать какой-нибудь унифицированный led-протокол который можно было бы распознавать с помощью камеры и приложения на телефоне. Тогда любое устройство могло бы сообщать свое состояние целыми строками, и может даже заменить экранчики которые любят ставить на самоделки. Вопрос только в скорости передачи информации таким способом.


    1. tormozedison
      08.02.2016 06:12

      И снова — звук. Потому что приложения для распознавания морзянки смартфоном через микрофон уже давно написаны. А через камеру — ещё нет.


  1. alexhott
    08.02.2016 07:52
    +1

    делал блок управления насосом в скважине, с контролем уровня в промежуточной емкости, и уровнем сухого хода.
    Итого 5 датчиков и две реле.

    Чтобы понять какие датчики в каком положении использовал разные мигания светодиодом, быстрее, медленнее. три вспышки пауза и тд.
    На момент написания программы восхищался собой — одним светодиодом выведу всю информацию.
    Подключил блок — светодиод замигал.
    Я долго и упорно пытался вспомнить что же означает


  1. ignat99
    08.02.2016 21:40

    http://www.state-machine.com/doc/AN_Event-Driven_Arduino-1.6.x.pdf
    https://bytespeicher.org/2015/compiling-qm-lib-examples-on-arduino/

    Ну можно несколько потоков запустить мигания и сделать планировщик :-)