ШИМ регулятор на Digispark с защитой от дребезга контактов

 
    18.02.2021

ШИМ возможности Digispark

    Изначально я был уверен в фиксированной частоте 1024 Гц аппаратного ШИМ у Digispark. Однако, как оказалось это было верно до определённой версии прошивки. В актуальной версии Digispark при помощи настройки файлов эту частоту можно изменять. Однако при нештатных настройках немалая часть программ не будет работать, так что не забывайте после корректировки значений возвращать их к изначальным значениям. Чтобы найти файлы для настройки, включите вывод доп.информации в настройках IDE. Там при сборке скетча отобразится путь, по которому находится исходник ядра. Что то вроде: */packages/digistump/hardware/avr/1.6.7/cores/tiny/ Там нас интересует два файла, первый core_build_options.h в нём параметр FAVOR_PHASE_CORRECT_PWM. От него зависит частота ШИМ на выводе 1, она будет равна частоте вывода 0 (при 1), иначе - удвоенной частоте вывода 4. Второй файл wiring.c, и параметр в нём MS_TIMER_TICK_EVERY_X_CYCLES. По умолчанию он равен 64 и является делителем рабочей частоты кристалла. Математика простая 16 Мгц / 256 значений / 64 = 1024. Допустимы значения кратные 8, то есть 1, 8, 64, 256. Частота при этом будет от 256 Гц (делитель = 256) до 65 кГц (делитель = 1) на выводе 4. А вот это уже интересно и позволяет управлять моторами и дросселями за пределами слышимости человеком.

    Самый простой вариант применения - мигалка. Выглядит следующим образом:

Скетч довольно примитивен, и я думаю комментариев не требует. Для большей красоты, диапазоны изменения скважности следует подстроить под участки линейной зависимости светового потока диодов от напряжения.
int way = 1,value = 0;
const int freq = (1000 >> 8);

void setup() {
    pinMode(0, OUTPUT);
    pinMode(1, OUTPUT);
}

void loop() {
    analogWrite(4,   7 + value);
    
    analogWrite(0, 255 - value);
    analogWrite(1,       value);
    delay(freq);

    value += way;

    if( value >= 248 ){
      value = 247;
      way = -1;
    }

    if( value == -1 ){
      value = 1;
      way = 1;
    }    
}
Следующий интересный момент с возможностями ШИМ генератора Digispark связан с фазами. При равной частоте ШИМ выводов 0 и 1, между ними сдвиг порядка половины фазы, и это относительно бесполезно:
А вот при равной частоте ШИМ на выводах 1 и 4 они полностью синфазны (на осциллограмме один из сигналов инвертирован), и вот это открывает очень интересные перспективы:
С использованием данной возможности, получится реализовать очень интересные вещи, но не буду забегать вперёд :).

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

   

Защита от дребезга контактов

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

    Какие же способы решения существуют. Добавление конденсатора. По сути это фильтр, уменьшающий вероятность проблемы и не решающий её полностью. Плюс это повышает нагрузку на вход чипа. Второй вариант - использование прерываний, не буду детально расписывать, так как он также не подходит, не все "виртуальные" нажатия вызывают генерацию прерывания, так что кнопка несмотря на нажатие, может остаться в некорректном состоянии. Более правильный подход - использование специализированных микросхем для защиты от эффекта дребезга контактов. Один из вариантов, триггер Шмидта. Но я данный вариант задействовать не стал, так как это рушит концепцию использования максимально дешёвого Digispark.

    В итоге я пришёл к полностью программному решению, которое показало себя безупречно. Логика его предельно простая. Устанавливается определённый контрольный интервал, например 5 сотых секунды. При этом максимально часто идёт опрос состояния кнопки и при этом считается количество времени, когда кнопка была нажата. По истечению интервала просто сравнивается, если в течение данного интервала больше половины времени кнопка была нажата, значит она реально была нажата и наоборот. Если состояние кнопки изменилось по сравнению с прошлым контрльным интервалом, вызывается обработчик, настраивающий требуемым образом выходной ШИМ сигнал.

   

ШИМ регулятор с турбо режимом

    Первым применением ШИМ регулятора стал моторчик мини пылесоса. Логику я заложил такую, при первом нажатии мотор включается на 70%, во включенном состоянии при удержании кнопки более секунды включается турбо режим, а именно 100%. На самом деле причина была в том, что на тот момент у меня не было подходящего блока питания, а при 70% мощности мотора его как раз хватало. Сам алгоритм описан ранее, данный скетч был написан написан первым и для отсчёта времени здесь используется millis(), потом перешёл на micros(). Из особенностей, здесь для первого нажатия используется отпускание кнопки. Это сделано, чтобы во включенном состоянии при включении турбо режима не проходило выключение. По настройкам, за короткое нажатие здесь используется интервал в 100 миллисекунд, для включения турборежима кнопку нужно удерживать секунду. Скетч приведен ниже:

byte mode = 0, last_mode = 0;
//0 - off, 1 - cruiser, 2 - full
byte button_state = 0, last_button_state = 0;
//0 - off, 1 - press, 2 - long_press
int votes_press = 0, votes_long_press = 0;
const int press_voting = 100, long_press_voting = 1000;
unsigned long loop_time = 0, last_loop_time = 0,
	 last_election = 0, last_long_election = 0;

void setup() {
  pinMode(0, OUTPUT);
  analogWrite(0, 0);
  pinMode(1, OUTPUT);//led
  pinMode(2, OUTPUT);//button
}

void button_event(){

    if( mode == 0 && button_state == 0 ){
      mode = 1;
    }else{
      if( mode == 1 && button_state == 0 ){
        mode = 0;
      }else{
        if( mode == 2 && button_state == 0 ){
          mode = 1;
        }else{
          if( mode == 1 && button_state == 2 ){
            mode = 2;
          }
        }
      }
    }

  if( last_mode != mode ){
    
    if( mode == 0 ){
      analogWrite(0, 0);
    }else{
      if( mode == 1 ){
          analogWrite(0, 199);
        }else{
          analogWrite(0, 254);
        }
    }
  }

  last_mode = mode;
}

void loop() {
  loop_time = millis();
  if( loop_time != last_loop_time ){
    int cycle_length = (loop_time - last_loop_time);
    
    if( digitalRead(2) == HIGH ){
      votes_press += cycle_length;
      votes_long_press += cycle_length;
    }

    if( (loop_time - last_election) > press_voting ){
      if( votes_press >= (press_voting >> 1) ){
        button_state = 1;
      }else{
        button_state = 0;
      }

      votes_press = 0;
      last_election = loop_time;
    }
    
    if( (loop_time - last_long_election) > long_press_voting ){
      if( votes_long_press >= (long_press_voting >> 1) ){
        button_state = 2;
      }
      
      votes_long_press = 0;
      last_long_election = loop_time;
    }

    if( button_state != last_button_state ){
      digitalWrite(1, button_state);
      button_event();        
    }

    last_button_state = button_state;
  }
  

  last_loop_time = loop_time;
}
Пример работы ниже. Короткое Нажатие кнопки срабатывает не всегда, так как её нужно удерживать не менее 0.1 сек. В общем это не напрягает, так включается и выключается пылесос редко. Если есть вопросы по подключению кнопки, смотрите следующий вариант регулятора.

   

ШИМ регулятор на 3 скорости

    На алиэкспрессе обнаружилась очень интересная турбинка, и для неё я решил сделать 3х ступенчатый PWM регулятор. Начну со схемы. Выход сигнала вывод 0, выводы 1 и 2 вниз\вверх, 3 и 4 - диоды индикации режима. Сперва задействовал и 5й вывод, но с диодом на нём плата работала не корректно, этот вывод кажется связан с аппаратным сбросом. Питается схема от 12 Вольт, 5 Вольт для диодов и подтяжки кнопок берётся со стаблизатора платы. Однако не нужно забывать, что он довольно маломощный, так сильно нагружать этот источник не следует. Кнопки могут быть любыми, диоды по вкусу.

Готовя плата выглядит следующим образом:
Перед внедрением в эксплуатацию контакты будут пропитаны цапон-лаком и вся конструкция будет обмотана синей лентой:
Скетч приведен ниже. Частота ШИМ подбирается под каждый мотор, некоторым нужно 100 Гц, некоторым 30 кГц. Интервал замера для нажатия 50 миллисекунд.
byte mode = 0, last_mode = 0,
	button = 0, last_button = 0;//0 - off, 1..3 - speed
unsigned long votes_press1 = 0,votes_press2 = 0;
const unsigned long press_voting = 50000;
unsigned long loop_time = 0,
	 last_loop_time = 0,
	 last_election = 0;

void setup() {
    pinMode(0, OUTPUT);
    pinMode(1,  INPUT); //down
    pinMode(2,  INPUT); //up
    pinMode(3, OUTPUT); //01
    pinMode(4, OUTPUT); //10
    pinMode(5, OUTPUT);

    digitalWrite(3,  LOW);
    digitalWrite(4,  LOW);
    digitalWrite(5,  LOW);
}

void button_event(byte updown){//1 == down, 2 == up

  if( updown == 1 && mode > 0 ) mode--;

  if( updown == 2 && mode < 3 ) mode++;

  if( last_mode != mode ){
    switch( mode ) {
      case 1:
        analogWrite(0, 143);
        digitalWrite(3, HIGH);
        digitalWrite(4,  LOW);
        break;
      case 2:
        analogWrite(0, 117);
        digitalWrite(3,  LOW);
        digitalWrite(4, HIGH);
        break;
      case 3:
        analogWrite(0,   1);
        digitalWrite(3, HIGH);
        digitalWrite(4, HIGH);
        break;
      default:
      analogWrite(0, 0);
      digitalWrite(3,  LOW);
      digitalWrite(4,  LOW);
    }
  }    

  last_mode = mode;
}


void loop() {
  loop_time = micros();

  if( loop_time != last_loop_time ){

    unsigned long cycle_length = (loop_time - last_loop_time);
    
    if( digitalRead(1) == HIGH ) votes_press1 += cycle_length;
    if( digitalRead(2) == HIGH ) votes_press2 += cycle_length;

    if( (loop_time - last_election) >= press_voting ){
      button = 0;
      if( votes_press1 >= (press_voting >> 1) ) button = 1;
      if( votes_press2 >= (press_voting >> 1) ) button = 2;

      if( button != last_button ) button_event(button);       

      votes_press1 = 0;
      votes_press2 = 0;
      last_election = loop_time;
      last_button = button;
    }
  
  }

  last_loop_time = loop_time;
}
Пример работы ШИМ регулятора. Видно, что реакция мгновенная и безошибочная:

   

ШИМ регулятор на 7 скоростей с индикацией

    Довольно быстро при эксплуатации 3х скоростного регулятора стало ясно, что этого мало. Иногда нужна почти бесшумная работа двигателя на ХХ (холостом ходу). Если с индикацией всё было ясно, то дальше начались сложности. Три выхода на индикацию, один на выход сигнала. 5й вывод - сброс. Итого для двух кнопок остался один вывод. Пришлось немного поразмыслить, пришёл к такой схеме:

Внизу отображена классическая схема. Идея в том, что вывод переводится в аналоговый режим и замеряется уровень напряжения. Резисторы образуют делитель напряжения, при нажатии кнопок напряжение меняется заданным образом. Кнопок в принципе может быть любое количество. Для классической схеме, при равных сопротивлениях всех трёх резисторов, при отпущенных кнопках напряжения на выходе 1/3 U, при нажатии нижней кнопки 1 U, верхней 1/2 U. Меня здесь не устроило, то что при нажатии нижней кнопки напряжение в какой-то момент времени соответствует нажатию верхней. То есть есть вариант ложного срабатывания, и нужно будет учитывать время нажатия, усложнять алгоритм. Поэтому я сделал по другом, на выходе напряжение 1/2 U, нажатие кнопок однозначно увеличивает или уменьшает напряжение. Конечно, это справедливо только для двух кнопок. Резисторы на 100 Ом добавлены для избежания короткого замыкания при нажатии обеих кнопок. Минусы подключения нескольких кнопок на один вывод - постоянное потребление тока, и не менее существенно - дольшая обработка. Например, в пульте управления опрос 4х цифровых кнопок занимал 25 мкСек, опрос одного аналогового входа занимает 108 мкСек, в пересчете на две цифровых это в 10 раз больше процессорного времени. В некоторых случаях это может быть критично.

    Плату по традиции вырезал, получилось довольно компактно:

Диоды и резисторы пришлось паять пастой, паяльником было бы проблематично:
С пастой конечно прикольно, но навык нанесения и расстановки элементов конечно нужен:
Вид готового регулятора:

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

#include <core_adc.h>

byte mode = 0, last_mode = 0, button = 0, last_button = 0;
unsigned long votes_press1 = 0,votes_press2 = 0;
const unsigned long press_voting = 50000;
unsigned long loop_time = 0, last_loop_time = 0, last_election = 0;
 
void setup() {
  pinMode(0, OUTPUT);//pwm
  digitalWrite(0, LOW);
  analogWrite(0, 255);
  pinMode(1, OUTPUT);//001
  digitalWrite(1, LOW);
  pinMode(3, OUTPUT);//010
  digitalWrite(3, LOW);
  pinMode(4, OUTPUT);//100
  digitalWrite(4, LOW);

  pinMode(2, INPUT);//up&down
}

void button_event(byte updown){//1 == down, 2 == up

  if( updown == 1 && mode > 0 ) mode--;

  if( updown == 2 && mode < 7 ) mode++;

  if( last_mode != mode ){
    analogWrite(0, 255 - mode * 36.4 );

    digitalWrite(1,  mode      & 1);
    digitalWrite(3,  mode >> 1 & 1);
    digitalWrite(4,  mode >> 2 & 1);
  }    

  last_mode = mode;
}


void loop() {
  int ADCvalue;
  
  
  loop_time = micros();

  if( loop_time != last_loop_time ){

    unsigned long cycle_length = (loop_time - last_loop_time);

    ADC_SetInputChannel((adc_ic_t) 1); //pin2
    ADC_StartConversion();
    while( ADC_ConversionInProgress() );
    ADCvalue = ADC_GetDataRegister();
    //spend 108 mks

    if(ADCvalue < 256 ) votes_press1 += cycle_length; //down
    if(ADCvalue > 768 ) votes_press2 += cycle_length; //up

    if( (loop_time - last_election) >= press_voting ){
      button = 0;
      if( votes_press1 >= (press_voting >> 1) ) button = 1;
      if( votes_press2 >= (press_voting >> 1) ) button = 2;

      if( button != last_button ) button_event(button);       

      votes_press1 = 0;
      votes_press2 = 0;
      last_election = loop_time;
      last_button = button;
    }
  
  }

  last_loop_time = loop_time;
}
Пример работы индикации регулятора. По-моему впорне наглядно и удобно:

   

Фотогалерея