leoniv (leoniv) wrote,
leoniv
leoniv

Category:

Как мигать светодиодом

Leds_blink.gif

Почему-то считается, что мигание светодиодом является самым дном программирования микроконтроллеров. На самом деле эта задача может быть весьма сложной. Вот и на этот раз – делаю блок управления катушечным магнитофоном. В нем есть светодиоды, индицирующие разные режимы работы. Этих светодиодов там 7. Это не совсем маленькое количество, как 1 или 2, но и не совсем большое, как сотни в различных светодиодных панелях. Поэтому здесь требуется некий средний подход – и не слишком общий, и не слишком индивидуальный.



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

Очевидным решением является создание класса светодиода, реализующего требуемое его поведение. Для каждого светодиода нужно создать экземпляр, привязав к определенному выводу микроконтроллера. Тогда будет полная свобода. Наверное, я бы так и поступил на микроконтроллере уровня STM32. Но для AVR это будет большой обузой. Вот и приходится ужиматься в поисках компромиссов. А поиск компромиссов – весьма утомительное занятие.

Мигание должно подчиняться некоторым правилам, которые часто являются взаимоисключающими.

Проблема первого и последнего цикла. Основная программа в любой момент времени может захотеть включить или выключить какому-то светодиоду режим мигания. Здесь возникает проблема неполного первого и последнего цикла. В момент, когда светодиод включается в мигающий режим, цикл таймера мигания может подходить к концу. Тогда первая вспышка будет короткой. Такая же ситуация может возникнуть и в конце мигания – светодиод только зажегся, а его выключают. Начало мигания можно синхронизировать, если перезапускать таймер мигания одновременно с включением этого режима. А вот с концом сложнее. Надо запомнить, что основная программа хочет мигание выключить, но сделать это надо позже, когда светодиод погаснет естественным образом.

Проблема начальной фазы мигания. С чего надо начинать мигание, с зажигания или гашения? Хочется ответить «с зажигания». А если до этого светодиод горел? Изменение режима индикации должно быть видно сразу. А так получится, что нажали какую-то кнопку, а светодиод как горел, так и горит. И только спустя некоторое время, которое зависит от периода мигания, он погаснет. Поэтому если мигание включается с состояния «выключено», надо начинать с включения. Если с состояния «включено», то наоборот. Т.е. мигание желательно начинать с инверсии.

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

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

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

Синхронизация светодиодов с другими процессами. Кроме световой индикации в устройстве может еще что-то происходить. Например, работа какого-то механизма, или звуковая сигнализация, или вывод чего-то на дисплей. Мигание светодиодов по возможности должно быть привязано к этим процессам. Пример одной из ошибок – когда прерывистый звук и прерывистое мигание выполняются с разными частотами или фазами. Другой классический пример – мигающая точка в часах, разделяющая часы и минуты. Часто можно видеть, как программист сделал мигание этой точки от какого-то левого таймера, имеющего период примерно 1 сек. Такой таймер плывет относительно часов реального времени, в результате смена минут приходится на случайную фазу мигания точки. Обычно в часах есть секундное прерывание, но с его помощью невозможно напрямую сделать мигание, для этого нужна удвоенная частота. Как выход, по прерыванию можно зажигать светодиод, а гасить примерно через 0.5 сек. с помощью программного таймера. Время горения не настолько критично, как синхронизация.

Яркость. Это непосредственно к миганию не относится, но относится в общем к светодиодам. Они должны быть выровнены по яркости между собой и с другими элементами индикации. Особенно трудно это сделать для светодиодов разных цветов. Разные светодиоды могут иметь и разное оформление в одном приборе. Например, один светодиод может быть установлен за стеклом дисплея, а другой высунут в отверстие панели. Визуально они должны иметь одинаковую яркость. На практике часто яркости не равны, такую ошибку допускают даже именитые производители, например, такое наблюдается на панели стиральной машины «Bosch».

Выполнить все эти требования в общем случае невозможно, поэтому опять вспоминаем ненавистное слово «компромисс». В этом плане класс светодиода и отдельный объект для каждого светодиода проблемы не решает, придется придумывать какую-то синхронизацию между объектами. Ну или наследовать их от какого-то класса-блинкера, который будет задавать правила игры. В любом случае, куча объектов – это довольно расточительно, они все хотят кушать.

В аппаратной части ограничения связаны в основном с двумя вещами – увеличением объема устройства и увеличением его энергопотребления. В программной части ограничения носят похожий характер.

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

Второе ограничение в области ПО выглядит как чрезмерное потребление вычислительной мощности. Как и в схемах, каждый процесс в программе надо «питать» – давать ему процессорное время. У меня разъемом питания для каждого объекта является его метод «execute», который периодически вызываться в основном цикле. Сколько можно потреблять процессу, в кооперативной многозадачности решает он сам. Здесь не место «наглым» процессам, забирающим за один вызов кучу процессорного времени. Здесь все основано на взаимном уважении – процесс должен скушать немного и передать тарелку другому.
Как и в аппаратной части, где выходной ток блока питания надо делить между всеми схемами.

Для маломощных процессоров, таких как AVR, вычислительную мощность приходится экономить. Поэтому для начала ввел некоторые ограничения. Мигать каждым светодиодом совершенно произвольным образом не очень-то и надо. Вполне можно ограничиться, скажем, тремя режимами мигания: медленным, средним и быстрым. Для каждого режима можно произвольно задавать частоту и скважность мигания, но все светодиоды, находящиеся в данный момент в одном режиме мигания, будут мигать одинаково. Период мигания тоже можно ограничить, чтобы вписаться в 8-разрядный счетчик. Если выбрать его период тактирования 10 мс, тогда максимальный период составит 2.56 секунды, что вполне достаточно. Поскольку режима мигания 3, понадобятся 3 таких счетчика:

  uint8_t SlowBlink; //счетчик медленного мигания
  uint8_t FastBlink; //счетчик быстрого мигания
  uint8_t NormBlink; //счетчик нормального мигания

Для хранения состояния каждого из светодиодов требуется 3 бита, так как состояний 5: выключено, включено и 3 режима мигания. Чтобы упростить анализ, выделил для каждого светодиода 4 бита. И разместил их не рядом в байте, а в разных байтах. Эдакая вертикальная структура, которая иногда используется в вертикальных счетчиках. В результате имеем 4 набора битовых полей. Первый набор отвечает за постоянное горение, остальные – за режимы мигания. Размер переменных зависит от количества светодиодов, у меня их 7, поэтому вписался в байт.

typedef struct
{
  uint8_t cont; //непрерывно горящие светодиоды
  uint8_t slow; //медленно мигабщие светодиоды
  uint8_t norm; //нормально мигабщие светодиоды
  uint8_t fast; //быстро мигабщие светодиоды
} leds_t;

  leds_t Mask; //маски светодиодов для всех режимов

Например, если в Mask.cont установлен бит для какого-то светодиода, то он горит постоянно. Если установлен в Mask.slow – медленно мигает. И т.д. Бит конкретного светодиода может быть установлен только в одной из масок. Хотя в частном случае может и в нескольких, мигание с разной частотой сложится по OR и получится что-то веселое. Если правильно выбрать частоты и скважности, можно получить дополнительные режимы мигания.

Каждая из масок включает «свои» светодиоды внутри интервала, когда счетчик данного режима мигания имеет значение от 0 до половины периода (это в случае меандра, в общем случае может быть любой код в пределах периода). Результаты проверок всех масок складываются по OR, в результате формируется набор светодиодов, которые должны гореть в данный момент времени.

void TLeds::Execute(void) //вызывается с периодом T_SLOW = 10 мс
{
  uint8_t leds = Mask.cont; //горящие светодиоды

  if(Mask.slow || Mask.norm || Mask.fast)
  {
    //выполняется, если есть хоть один мигающий светодиод:
    if(SlowBlink <= (SLOW_PERIOD / T_SLOW * SLOW_DUTY / 100))
      leds |= Mask.slow;
    if(++SlowBlink == (SLOW_PERIOD / T_SLOW))
      SlowBlink = 0;
    if(NormBlink <= (NORM_PERIOD / T_SLOW * NORM_DUTY / 100))
      leds |= Mask.norm;
    if(++NormBlink == (NORM_PERIOD / T_SLOW))
      NormBlink = 0;
    if(FastBlink <= (FAST_PERIOD / T_SLOW * FAST_DUTY / 100))
      leds |= Mask.fast;
    if(++FastBlink == (FAST_PERIOD / T_SLOW))
      FastBlink = 0;
  }
  else
  {
    //мигания нет, синхронизация счетчиков:
    SlowBlink = 0;
    NormBlink = 0;
    FastBlink = 0;
  }
  //если есть изменения, обновление светодиодов:
  if(PreLeds != leds)
  {
    PreLeds = leds;
    Update(leds);
  }
}

С помощью метода Update этот набор отправляется в порты, к которым подключены светодиоды. Управление производится побитовое, так как светодиоды могут быть произвольно разбросаны по разным портам. Пины портов управляются с помощью шаблонного класса. Благодаря перегруженному оператору присваивания вывод в порт записывается как простое присвоение, например, Led_Stop = 1.

inline void TLeds::Update(uint8_t leds)
{
  Led_PlayF = leds & LED_PLAYF;
  Led_PlayR = leds & LED_PLAYR;
  Led_Pause = leds & LED_PAUSE;
  Led_Rec   = leds & LED_REC;
  Led_Ffd   = leds & LED_FFD;
  Led_Rew   = leds & LED_REW;
  Led_Stop  = leds & LED_STOP;
}

Интерфейс класса содержит метод Set, который может включать в заданный режим один или несколько светодиодов, например Leds->Set(LED_STOP + LED_PAUSE, LEDS_FAST)

void TLeds::Set(uint8_t leds, uint8_t mode)
{
  if(mode == LEDS_CONT) Mask.cont |= leds; else Mask.cont &= ~leds;
  if(mode == LEDS_SLOW) Mask.slow |= leds; else Mask.slow &= ~leds;
  if(mode == LEDS_NORM) Mask.norm |= leds; else Mask.norm &= ~leds;
  if(mode == LEDS_FAST) Mask.fast |= leds; else Mask.fast &= ~leds;
}

Требования, которые были озвучены выше, здесь выполняются только частично. Например, мигание обрывается в случайной фазе. Начинается мигание всегда с фазы горения. Если до этого не было мигающих светодиодов, то счетчики мигания сбрасываются, поэтому первый включенный мигающий светодиод всегда будет включаться красиво. Этой меры вполне достаточно, так как несколько одновременно мигающих светодиодов, включающихся в разное время, нужны не везде. К тому же, тут нет других вариантов – требование синфазности мигания важнее.

Мигание работает, но остался какой-то неприятный осадок от всей этой проблемы. Полный код можно найти внутри этого проекта.

Дополнение.

В комментариях появились предложения использовать двух- или трехцветные светодиоды. Индикацию такими светодиодами можно делать лишь в самом крайнем случае. Например, в каком-то очень сжатом дизайне, где невозможно разместить несколько светодиодов. И то нельзя поручать такой индикации показывать что-то важное. Отличие цвета свечения воспринимается гораздо хуже, чем состояния горит/не горит. Разные светодиоды на панели делать разноцветными можно (в разумных пределах). В этом случае цвет лишь дополняет картину, информация передается самим фактом свечения. Но если одна и та же точка на панели просто меняет цвет, это никуда не годится. Надо еще учесть, что довольно много людей плохо различают цвет. Я, например, желтый и зеленый отличаю с трудом. Хорошо выполненная индикация не должна терять информативности, будучи переведенной в монохромную.

В качестве примера можно привести лабораторный источник питания. У него есть два режима работы - стабилизация напряжения (CV) и стабилизация тока (CC). Есть соблазн сделать индикацию с помощью двухцветного светодиода. Например, зеленый цвет - CV, красный цвет - CC. Когда выход выключен - светодиод не горит. Но это так себе решение. Гораздо удобней, когда есть два раздельных светодиода. Погасание одного и загорание другого намного заметнее, даже боковым зрением, чем смена цвета. Быструю смену цветов вообще можно не заметить, а перемигивание двух светодиодов вполне заметно.

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

Tags: avr, c++, микроконтроллер, программирование
Subscribe

  • Измеритель уровня V0.1

    Сделал первую версию прошивки нового измерителя уровня для магнитофона "Электроника-004". Все еще очень-очень сырое, но уже полоски как-то…

  • Sharp GF-777

    Попал тут ко мне Sharp GF-777. Без преувеличения можно сказать, что это легенда. Обладать таким аппаратом могли лишь избранные. Стоил он когда-то…

  • JVC TD-V662

    Когда просят посмотреть кассетную деку, говорят удивительные вещи. Что не могут найти мастера, который за это бы взялся. Но ведь аналоговая…

  • Post a new comment

    Error

    default userpic
    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 31 comments

  • Измеритель уровня V0.1

    Сделал первую версию прошивки нового измерителя уровня для магнитофона "Электроника-004". Все еще очень-очень сырое, но уже полоски как-то…

  • Sharp GF-777

    Попал тут ко мне Sharp GF-777. Без преувеличения можно сказать, что это легенда. Обладать таким аппаратом могли лишь избранные. Стоил он когда-то…

  • JVC TD-V662

    Когда просят посмотреть кассетную деку, говорят удивительные вещи. Что не могут найти мастера, который за это бы взялся. Но ведь аналоговая…