Одна кнопка. один диммер

Тема в разделе "Микроконтроллеры AVR", создана пользователем parovoZZ, 10 сен 2020.

  1. parovoZZ

    parovoZZ Гуру

    Что я хочу от однокнопочного диммера? А вот что:
    1. При однократном нажатии на кнопку увеличиваем яркость на один шаг.
    2. При двукратном нажатии на кнопку уменьшаем яркость на один шаг.
    3. Если нажать на кнопку и удерживать её, то плавно увеличиваем яркость.
    4. Если однократно (или более) нажать кнопку, а затем нажать и удерживать её, то плавно уменьшаем яркость.
    5. Если яркость равна нулю (диммер выключен), то при однократном нажатии на кнопку плавно увеличиваем яркость до ранее запомненного значения.
    6. Если нажать на кнопку три раза (или более) без её удержания, то запоминаем значение яркости и плавно уменьшаем яркость до нуля.
    7. Любое автоматическое действие (уменьшение или увеличение яркости) можно отменить в любое время любым действием.
    В качестве лампы у меня будет выступать светодиод. В качестве испытательного полигона будет выступать отладочная плата Xplained mini на Attiny817. Почему она? Всё просто - на ней есть аппаратный дебаггер.
     
    Un_ka и ИгорьК нравится это.
  2. ИгорьК

    ИгорьК Гуру

  3. Airbus

    Airbus Радиохулиган Модератор

    Сколько много телодвижений!Бубен шамана прямо.Это ж записать надо шо и как.Шоб не перепутать.А нельзя просто одна кнопка— up вторая dowm. Обе вместе вкл. Еще раз—выкл?
     
    SergeiL и issaom нравится это.
  4. issaom

    issaom Гуру

    Видел у Гайвера в одном из видео как он Ардуину научил морзянку понимать.... Можно на одной кнопке консоль для управления умным домом запилить, а то напридумывают тут всяких Nextion-ов с графическими интерфейсами..... Кнопка рулит!
     
    Un_ka и Andrey12 нравится это.
  5. parovoZZ

    parovoZZ Гуру

    Кнопка и светодиод у меня подключены вот так:
    Код (C++):
    /*

                   |
          WOC   PC0| - LED
                   |
                PC5| - Button  ---/ ---
                   |                   |
                                      ---
    */
    Яркостью светодиода я буду управлять с помощью изменения скважности ШИМ. А для этого мне нужен таймер. Смотрим в даташит и видим, что к этому выводу можно подключить TimerD через блок сравнения WOC. Этот блок не самостоятельный - его необходимо подключить либо к WOA, либо WOB. TimerD - асинхронный таймер, который может тактироваться от системной шины, встроенного осциллятора или от внешнего генератора. Для конфигурирования таймера предусмотрены различные регистры: какие-то из них буферизированные, какие-то статические, а какие-то обычные IO. Также есть регистры только для чтения. При записи в буферизированные регистры информация из них попадает в рабочие регистры при следующем цикле таймера. Для записи в статические регистры необходимо проверять бит готовности, иначе ничего не запишется. Как итог выше сказанного инициализация TimerD выглядит так:
    Код (C++):
    inline void TimerD_Init (void)
    {
         TCD0.CTRLC = TCD_CMPCSEL_bm | TCD_FIFTY_bm;

         CCP = CCP_IOREG_gc;
         TCD0.FAULTCTRL = TCD_CMPCEN_bm;

         TCD0.CMPBCLR = 0xFFF;

         TCD0.CMPBSET = 4095;

         while (!(TCD_ENRDY_bm & TCD0.STATUS));
           
         TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_ENABLE_bm;      
       
    }
    В первой строчке я блок WOC подключаю к блоку WOB. Последний бит дублирует значения из регистра сравнения блока B в регистр сравнения блока A. Зачем такие фокусы? TimerD предназначен для управления полумостовыми и мостовыми схемами различных импульсных устройств с применением dead time. Поэтому очерёдность следования импульсов с блоков сравнения этого таймера строго регламентирована.
    Второй строчкой я разрешаю блоку WOC управлять физической ножкой МК. Для защиты от случайного переключения данной функции, биты управления в этом регистре защищены регистром CPP. Для того, чтобы изменить функцию, необходимо в регистр CPP записать значение IOREG и в течение 4-х тактов записать новые значения в регистр FAULTCTRL.
    В регистр CMPBCLR я записываю значение, при котором произойдёт сброс (установка в ноль) физической ножки МК. TimerD имеет разрешение 12 бит. Использовать этот таймер я буду на все деньги, поэтому в регистр CMPBCLR записываю максимальное значение - 4096. Регистр CMPBSET устанавливает (переводит в 1) ножку МК. Для модуляции периода ШИМ я буду менять значение в регистре CMPBSET. Таким образом, чем ниже значение, тем выше яркость светодиода.
    Биты выбора источника тактирования в регистре CTRLA статические, а это значит, что необходимо проверять бит готовности таймера в регистре STATUS. Бит ENABLE разрешает работу таймера.
    Таймер имеет разрешение 12 бит, а это значит, что счётчик таймера считает до 4096. Одно из условий - это увеличение яркости на один шаг. Что? 4096 шагов? Зачем?
    Как известно, органы слуха и зрения у человека имеют логарифмическую чувствительность. Поэтому я создал небольшую табличку, которая имеет 100 значений для регистра сравнения таймера. Значения подобраны так, чтобы максимально соответствовать Ln(n):
    Код (C++):
     #include <avr/pgmspace.h>

    const uint16_t light_list[] PROGMEM =                  
    {
    4095,
    4094,
    4093,
    4092,
    4089,
    4086,
    4082,
    4077,
    4071,
    4064,
    4057,
    4049,
    4039,
    4029,
    4018,
    4007,
    3994,
    3981,
    3967,
    3951,
    3936,
    3919,
    3901,
    3882,
    3863,
    3843,
    3822,
    3800,
    3777,
    3754,
    3729,
    3704,
    3678,
    3651,
    3623,
    3594,
    3565,
    3535,
    3504,
    3471,
    3439,
    3405,
    3370,
    3335,
    3298,
    3261,
    3224,
    3185,
    3145,
    3104,
    3063,
    3021,
    2978,
    2934,
    2889,
    2843,
    2797,
    2750,
    2702,
    2653,
    2603,
    2552,
    2501,
    2448,
    2395,
    2341,
    2286,
    2231,
    2174,
    2116,
    2058,
    1999,
    1939,
    1878,
    1817,
    1754,
    1691,
    1627,
    1562,
    1496,
    1429,
    1361,
    1293,
    1224,
    1154,
    1082,
    1011,
    938,
    864,
    790,
    715,
    639,
    562,
    484,
    406,
    326,
    246,
    165,
    83,
    41,
    20
    };

    uint16_t Get_Phase (uint8_t light)
    {
        return pgm_read_word(&(light_list[light]));
    }
    Чтобы таблица не "убежала" в ОЗУ, я принудительно помещаю её во flash память.
    В регистр сравнения значения записываются следующим образом:
    Код (C++):
     TCD0.CMPBSET = Get_Phase(data->PWM.PWM_Phase);

         while (!(TCD_CMDRDY_bm & TCD0.STATUS));

         TCD0.CTRLE = TCD_SYNCEOC_bm;            // Синхронизация регистров с двойным буфером (CMPBCLR и CMPBSET)
     
     
    Последнее редактирование: 10 сен 2020
    ИгорьК нравится это.
  6. parovoZZ

    parovoZZ Гуру

    Наша задача имеет ярко выраженный набор состояний, поэтому решать её я буду с помощью конечного автомата. А конечный автомат и все вспомогательные задачи я реализую с помощью операционной системы quarkTS.
    Для начала определимся, что у нас будет являться узлами машины состояний. В прошлый раз это был светодиод, а в этой задаче пусть будет кнопка. Но состояние светодиода мне также необходимо отслеживать, поэтому я сразу создам новую переменную на основе перечисления:
    Код (C++):
    typedef enum {NO_FLAG = 0, FLAG_ENLARGE, FLAG_REDUCE, FLAG_POWER_ON, FLAG_POWER_OFF} Flags_enum;
    По названиям состояний я думаю понятно, что к чему.
    Текущее состояние кнопки мне также необходимо отслеживать, поэтому и для неё поступаем аналогично:
    Код (C++):
    typedef enum {BUTTON_OFF, BUTTON_SHORT, BUTTON_LONG} Button_enum;
    Отслеживать состояния кнопки я буду по событиям от неё. У меня будут такие события: ожидание события, кнопка нажата, кнопка отжата, кнопка удержана нажатой. Запишем эти события:
    Код (C++):
    qSM_Status_t State_Button_Wait (qSM_Handler_t);
    qSM_Status_t State_Button_Push (qSM_Handler_t);
    qSM_Status_t State_Button_Release (qSM_Handler_t);
    qSM_Status_t State_Button_Hold (qSM_Handler_t);
    Из состояния кнопка нажата в состояние кнопка удержана нажатой она будет переходить по таймеру.
    Нажата кнопка или нет я буду отслеживать с помощью такой функции:
    Код (C++):
    void Get_Button_CallBack (qEvent_t e)
    {
         BUTTON_PRESS = (!(PORTC.IN & PIN5_bm));
    }
    Т.к. физическое состояние пина кнопки не совпадает с логическим, то произвожу инвертирование.
    Вышеприведённую функцию я оформлю как одну из задач операционной системы, которую она будет вызывать с периодом 20 мс. Т.к. отслеживание физического состояния кнопки это первостепенная задача, то назначу ей самый высокий приоритет. Запишем это:
    Код (C++):
    qOS_Add_Task(&Task_Get_Button, Get_Button_CallBack, qHigh_Priority, 20, qPeriodic, qEnabled, NULL);
    Также мне потребуются различные вспомогательные переменные. Определим их в виде структур:
    Код (C++):
    typedef struct
    {
         uint8_t        Count;
    } Button_t;

    typedef struct
    {
         uint8_t        PWM_Phase;
         uint8_t        Save;
    }PWM_t;
    Первая структура является счётчиком количества нажатий кнопки. Во второй структуре запоминаю текущее значение яркости светодиода, а также сохраняю значение яркости на момент выключения диммера.
    По ходу выполнения задачи мне потребуется каким-то образом передавать значения переменных между задачами. Для такого взаимодействия в quarkTS необходимо передать указатель на пользовательскую структуру. У меня она выглядит так:
    Код (C++):
    typedef struct
    {
         Flags_enum     flag;
         PWM_t          PWM;
         Button_t       Button;
    }User_Data_t;
    Теперь достаточно объявить экземпляр структуры и передать указатель на него в задачу конечного автомата:
    Код (C++):
     User_Data_t    Data;

    // <... >

    qOS_Add_StateMachineTask(&Task_Button_Press, qLowest_Priority, 50, &FSM_Button_Press, State_Button_Wait, NULL, NULL, NULL, NULL, qEnabled, &Data);
    Это задача самая низкоприоритетная. От периода её вызовов зависит скорость нарастания и убывания яркости светодиода. Т.к. это явление абсолютно не критичное, я ей назначил низкий приоритет - чтоб не мешала другой, более важной задаче, которую рассмотрим далее.
    Все поля наших структур определены, поэтому теперь можем описать функцию, которая будет непосредственно решать, на какую яркость зажигать светодиод. Эта функция также будет задачей операционной системы, но реагировать она будет только на события. Т.е. если яркость светодиода необходимо изменить, данная задача вызывается и исполняется. Если изменений нет - задача находится в ожидании.
    В quarkTS задачи по событиям вызываются асинхронно. Т.е. даже если у задачи задан период вызовов, то при поступлении события, планировщик такую задачу ставит на исполнение вне очереди, но соблюдая приоритеты. А приоритет ей я назначил самый средний:
    Код (C++):
    qOS_Add_EventTask(&Task_Set_Light, Set_Light_Callback, qMedium_Priority, NULL);
    Указатель на экземпляр пользовательской структуры с данными я не передаю - это сделает вызывающая задача.
    Ну а теперь листинг функции:
    Код (C++):
    void Set_Light_Callback (qEvent_t e)
    {
         User_Data_t*   data = (User_Data_t*)e->EventData;

         switch (data->flag)
         {
              case FLAG_ENLARGE:
                   if (data->PWM.PWM_Phase == 100)
                   {

                   }
                   else
                        {
                             data->PWM.PWM_Phase++;
                        }
                   break;

              case FLAG_REDUCE:
                   if (data->PWM.PWM_Phase == 0)
                   {

                   }
                   else
                        {
                             data->PWM.PWM_Phase--;                      
                        }
                   break;

              case FLAG_POWER_ON:
                   data->PWM.PWM_Phase++;

                   if (data->PWM.PWM_Phase == data->PWM.Save)
                   {
                        data->flag = NO_FLAG;
                   }
                   break;

              case FLAG_POWER_OFF:
                   if (data->PWM.PWM_Phase)
                   {
                        data->PWM.PWM_Phase--;
                   }
                   else
                        {
                             data->flag = NO_FLAG;
                        }
                   break;

              default:
                   break;
         }

         TCD0.CMPBSET = Get_Phase(data->PWM.PWM_Phase);

         while (!(TCD_CMDRDY_bm & TCD0.STATUS));

         TCD0.CTRLE = TCD_SYNCEOC_bm;            // Синхронизация регистров с двойным буфером (CMPBCLR и CMPBSET)

    }
    Указатель на наш экземпляр структуры находится в поле EventData структуры qEvent_t. Но чтобы воспользоваться полями нашей структуры, указатель необходимо привести к виду нашей структуры.
    Далее остаётся извлечь наш флаг с признаком состояния нашего светодиода и в зависимости от его значения произвести необходимые действия. Если требуется увеличить яркость и яркость не максимальна, то выполняем увеличение на один шаг. С уменьшением яркости аналогично, но проверяем значение яркости с нулём. Ниже нуля нам не надо. Если прилетел флаг на включение диммера, то осуществляем режим плавного наращения яркости до тех пор, пока достигнутое значение не сравняется с сохранённым при выключении диммера. Аналогичным образом поступаем и с выключением диммера. По окончании всех итераций ставим признак NO_FLAG, чтобы данная функция более не вызывалась.
    Строчки в самом низу - уже знакомые. Пояснения по ним давал выше.
     
    Последнее редактирование: 11 сен 2020
    ИгорьК нравится это.
  7. parovoZZ

    parovoZZ Гуру

    Задача, которая
    Код (C++):
    qSM_Status_t State_Button_Wait (qSM_Handler_t m)
    {
         qEvent_t       e;
         User_Data_t*   data;

         e = (qEvent_t)m->Data;
         data = (User_Data_t*)e->TaskData;

        switch (m->Signal)
        {
            case QSM_SIGNAL_NONE:
                   switch (data->flag)
                   {
                        case FLAG_POWER_ON:
                           
                        case FLAG_POWER_OFF:
                             qTask_Notification_Send(&Task_Set_Light, data);

                             break;

                        default:
                             break;
                           
                   }

                if (BUTTON_PRESS)
                {
                    m->NextState = State_Button_Push;
                }
                break;

              case QSM_SIGNAL_EXIT:
                   data->Button.Count = 0;
                   break;

              default:
                   break;
        }

        return qSM_EXIT_SUCCESS;
    }
    Здесь всё просто: если присутствует флаг на включение или выключение диммера, то отправляем уведомление (событие) задаче, которая регулирует яркость светодиода. Задача по регулировке яркости светодиода сама разберётся - увеличивать яркость или уменьшать её. Поэтому на два разных флага реагируем одинаково. Не забываем в аргументе функции отправки уведомления про указатель на структуру с нашими переменными. Если обнаруживается, что кнопка нажата, то меняем состояние нашего конечного автомата. Но прежде, чем сменить состояние, планировщик ещё раз поставит эту функцию на выполнение, но уже с признаком QSM_SIGNAL_EXIT. Так как такой переход означает начало нового цикла обработки нажатия кнопки, то сбрасываем счётчик нажатий.
    Кнопка нажата. Что дальше? А дальше необходимо установить таймер и ждать, что произойдёт раньше: будет отпущена кнопка или сработает таймер. В коде это выглядит так:
    Код (C++):
    qSM_Status_t State_Button_Push (qSM_Handler_t m)
    {
         qEvent_t       e;
         User_Data_t*   data;

         e = (qEvent_t)m->Data;
         data = (User_Data_t*)e->TaskData;  

        static qSTimer_t    timeout;

        switch (m->Signal)
        {
            case QSM_SIGNAL_ENTRY:
                qSTimer_Set (&timeout, TIME_BUTTON_PUSH);

                   data->Button.Count++;
               
                break;

            default:
                if (qSTimer_Expired(&timeout))
                {                  
                        m->NextState = State_Button_Hold;  
                }

                if (!BUTTON_PRESS)
                {
                    m->NextState = State_Button_Release;
                }

                   break;
                                       
        }

        return qSM_EXIT_SUCCESS;
    }
    QSM_SIGNAL_ENTRY - это признак того, что функция вызвана сразу после смены состояния конечного автомата. Все последующие вызовы функции до смены состояния конечного автомата будут вызываться с другими признаками, поэтому QSM_SIGNAL_ENTRY как нельзя подходит для начальной инициализации таймера. Здесь же накручиваем счётчик нажатий.
    Период таймера закончился, а кнопку не отпустили - переходим в обработчик состояния удержания кнопки в нажатом состоянии:
    Код (C++):

    qSM_Status_t State_Button_Hold (qSM_Handler_t m)
    {
         qEvent_t  e;
         User_Data_t*    data;

         e = (qEvent_t)m->Data;
         data = (User_Data_t*)e->TaskData;

         switch (data->Button.Count)
         {
              case 0:
                   break;

              case 1:
                   data->flag = FLAG_ENLARGE;
                   break;

              default:
              case 2:
                   data->flag = FLAG_REDUCE;
                   break;
         }

         qTask_Notification_Send(&Task_Set_Light, data);

         if (!BUTTON_PRESS)
         {
              m->NextState = State_Button_Wait;
         }

         return qSM_EXIT_SUCCESS;
    }
    Здесь необходимо отследить количество нажатий на кнопку до её удержания. Нажали один раз и держим её: яркость по условию задачи увеличиваем. Ежели нажали однократно, а затем нажали повторно и не отпускаем - яркость уменьшаем. Здесь же реализуем защиту от дурака: сколько бы раз не нажали на кнопку до её удержания, всё равно яркость будет уменьшаться. Плавное изменение яркости реализуется за счёт периодического вызова данной функции планировщиком. Заканчивается данное безобразие тогда, когда будет отжата кнопка. Конечный автомат сменит состояние и планировщик будет вызывать функцию ожидания, рассмотренную чуть выше.
     
    ИгорьК нравится это.
  8. parovoZZ

    parovoZZ Гуру

    Логично предположить, что задача, которая реагирует на событие отпускания кнопки, должна зафиксировать это событие, если через какое-то время кнопка повторно не нажата. В этой же задаче я проверяю, необходимо ли включить диммер или выключить, или же просто изменить яркость?
    Код (C++):
    qSM_Status_t State_Button_Release (qSM_Handler_t m)
    {
         qEvent_t       e;
         User_Data_t*   data;

         e = (qEvent_t)m->Data;
         data = (User_Data_t*)e->TaskData;  
       
         static qSTimer_t timeout;

        switch (m->Signal)
        {
            case QSM_SIGNAL_ENTRY:
                qSTimer_Set(&timeout, TIME_BUTTON_RELEASE);
                break;

              default:
                   break;

            case QSM_SIGNAL_NONE:
                if (qSTimer_Expired(&timeout))
                {
                     m->NextState = State_Button_Wait;
                 
                        switch (data->Button.Count)
                        {
                             case 1:
                                  if (data->PWM.PWM_Phase)
                                  {
                                       data->flag = FLAG_ENLARGE;
                                  }
                                  else
                                       {
                                            data->flag = FLAG_POWER_ON;
                                       }
                                                       
                                  break;

                             case 2:
                                  data->flag = FLAG_REDUCE;
                                         
                                  break;

                             default:
                             case 3:
                                  data->flag = FLAG_POWER_OFF;

                                  data->PWM.Save = data->PWM.PWM_Phase;

                                  break;
                        }

                        qTask_Notification_Send(&Task_Set_Light, data);
                 
                }

                if (BUTTON_PRESS)
                {
                       m->NextState = State_Button_Push;
                }
                break;
        }

        return qSM_EXIT_SUCCESS;
    }
    Как видите, не надо
    Также
    совсем и не надо. Достаточно владеть теорией конечных автоматов и предварительно построить алгоритм работы. А
    мы грамотно определили структуры переменных: их не много и не мало. А ровно столько, сколько нужно. К тому же, применённая библиотека, реализующая кооперативную операционную систему, позволила нам не создавать глобальные переменные. Распределённые во времени присвоения значений флагов позволило не задействовать такой механизм, как регулирование доступа к общим ресурсам. В quarqTS регулируется такой доступ с помощью мьютексов. Но об этом как-нибудь в другой раз...
     
    ИгорьК нравится это.