Мультиплексирование кнопок и LED-дисплея

В статье «Динамическая индикация: экономим выводы МК» мы разобрались с семисегментными LED-индикаторами и научились работать с ними в динамическом режиме. В этой статье перейдем к устройствам ввода информации в девайс, а именно к механическим кнопкам.

Когда мы подключали семисегментный индикатор в динамическом режиме, то мы выяснили, что тратим мы 9 контактов МК + один контакт на каждый дополнительный индикатор. Если мы хотим прицепить еще и кнопки, с помощью которых можно управлять нашим прибором, то нужно потратить еще некоторое количество выводов микроконтроллера. А можно и не тратить, а подключить их к тем де контактам МК, что и индикаторы, как и сделано в говорящих LED-часах. Там рассказано, как сделать так, чтобы все это правильно работало. Если кратко, то перед каждым новым обновлением дисплея мы настраиваем порты микроконтроллера, к которым подключены кнопки на вход и считываем значение с него. После этого возвращаем все как было. Реализуем один из вариантов этого алгоритма.

Перед началом работы, поговорим немного о таком явлении, как дребезг контактов. Это несколько быстрых замыканий и размыканий контактов кнопки в момент нажатия. Возьмем кнопку без фиксации от корпуса ПК и проверим ее на ее дребезг. Соберем следующую схему:

Рис. 1. Исследуемая схема с кнопкой

К выводу OUT подключаем осциллограф и смотрим, что там происходит:

Рис. 2. Осциллограмма сигнала на кнопке в момент нажатия

Что и требовалось ожидать! Перед нажатием напряжение на кнопке составляет +5,2 вольта, потом мы на нее нажимаем и начинается куча переключений, затем все устаканивается и на кнопке появляется ноль. Переходный процесс занимает примерно 280 мкс. Чтобы понимать масштаб времени приведу пример. При моргании человек закрывает глаза примерно на 100 мс, или 100*1000=100000 мкс. А дребезг контактов у нас составляет 280 мкс. Однако, микроконтроллер, работающий на частоте 8 МГц одну команду выполняет за 0,125 мкс, так что если не принять специальных мер, то одно нажатие юзера на кнопку микроконтроллер воспримет как несколько, что нехорошо. Бороться с дребезгом будем программным способом.

За основу возьмем схему и программу из статьи про динамическую индикацию. Кнопки подключил к тому же порту, что и сегменты индикатора. В итоге, получаем такую схему:

Рис. 3. Схема подключения индикатора и кнопок к AVR

Теперь займемся программированием. На этот раз потренируемся в IAR AVR 5.40.0. Вся задача заключается в доработке функции DispProcess(), которая отвечает за вывод информации на LED-дисплей. Вот код модифицированной функции:

//Эту функцию надо вызывать 400 раз в секунду
void DispProcess(void)
{
  static uint8_t led_index = 0;
  
  //Переменные, которые используются
  //в конечном автомате опроса кнопок
  static uint8_t c_btns = 0;
  static uint8_t last_btns = 0;
  static uint8_t btn_state = 0;
  
  AllDigitsOff();
  
  //////////////////////////////////////////////////////////////////////////
  //Начало конечного автомата опроса кнопок
  //выполняем опрос кнопок в начале каждого цикла
  //вывода на дисплей
  if(led_index == 0)
  {
    SEG_DDR = 0x00; //порт с кнопками на вход	
    //подключаем внутренние подтягивающие резисторы
    //отключать их потом не надо,
    //так как потом операцией
    //SEG_PORT = led_buffer[led_index];
    //все станет нормально
    SEG_PORT = 0xFF;
    
    c_btns = SEG_PIN; //читаем, что у нас на кнопках
    switch(btn_state)
    {
      ////
      case 0:
        if(c_btns != 0xFF) //если что-то нажато
        {
          last_btns = c_btns; //сохраняем это в глобальную переменную
          
          //антидребезг
          //перезодим в состояние, в котором
          //проверяем, будут ли через 10 мс (1/100Гц)
          //по прежнему нажаты те же кнопки
          //что и сейчас
          btn_state = 1;
        }
      break;
			
      ////
      case 1:
      //если по-прежнему нажаты те же кнопки
        if(c_btns == last_btns)
        {
          //в переменную ButtonsStatus заносим, что было нажато
          ButtonsStatus = ~c_btns;
          
          //переходим в состояние, в котором
          //будем ждать,
          //когда пользователь отпустит нажатые кнопки
          btn_state = 2;
        }
        else //иначе
        {
          //переходим в исходное состояние
          btn_state = 0;
        }
      break;
      
      ////
      case 2:
      if(c_btns == 0xFF) //если все кнопки отпустили
      {
        ButtonsStatus = 0; //очищаем нажатые кнопки
        btn_state = 0; //переходим в исходное состояние
      }
      break;
      
    }
    
    SEG_DDR = 0xFF; //возвращаем как было
  }
	
  //Конец
  //////////////////////////////////////////////////////////////////////////
  
  //устанавливаем активный уровень на нужных сегментах
  SEG_PORT = led_buffer[led_index];
  
  //зажигаем цифру
  DigitOn(led_index);
  
  //увеличиваем счетчик цифр на 1
  led_index++;
  
  //проверяем, не вышли ли мы за границы
  if(led_index > sizeof(led_buffer))
    led_index = 0;
}

Сразу после AllDigitsOff() вставляем конечный автомат, который занимается опросом кнопок. Он выполняется в начале каждого цикла обновления дисплея, когда led_index == 0. Таким образом, при частоте обновления 100 Гц, опрос кнопок выполняется так же с частотой 100 Гц. Для начала настраиваем порт, к которому подключены сегменты индикатора на вход и включаем внутренние подтягивающие резисторы порта. Далее, в c_btns читаем, что у нас на выводах. Далее, заходим в сам конечный автомат. Начальное состояние у нас 0 (ноль). В нем мы проверяем, не нажата ли какая-нибудь кнопка, и если это так, то в last_btns сохраняем значение на входе для дальнейшего его использования и переходим в состояние 1. Если какая-либо кнопка нажата, то соответствующий бит в c_btns будет сброшен в ноль.

Через 1/100=10 мс наступает следующий цикл работы конечного автомата и мы попадаем в состояние 1. Тут происходит проверка текущего состояния кнопок c_btns с ранее сохраненным состоянием last_btns. Если они равны, то это означает, что нажатие действительно произошло и все переходные процессы, связанные с дребезгом контактов завершены, и можно в переменную ButtonsStatus занести зажатую кнопку и перейти в состояние 2. Если же условие c_btns == last_btns не выполнилось, то сразу переходим в начальное состояние.

Через еще 10 мс мы попадаем в состояние 2, в котором будем находиться до тех пор, пока пользователь не отпустит все кнопки. Как только это произошло, переходим в начальное состояние 0, и система снова готова регистрировать нажатия.

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

uint8_t GetButtonsStatus(void)
{
  uint8_t tmp;
  __disable_interrupt();
  tmp = ButtonsStatus;
  ButtonsStatus = 0;
  __enable_interrupt();
  return tmp;
}

Предполагается, что где-то в бесконечном цикле будет производиться непрерывный вызов функции GetButtonsStatus(). Поэтому, чтобы программа, использующая GetButtonsStatus() не воспринимала нажатую кнопку как многократное нажатие, эта функция сбрасывает флаг нажатой кнопки после своего вызова. Еще тут есть один небольшой момент. Возможна ситуация, при которой мы вызываем GetButtonsStatus(), выполняется  tmp = ButtonsStatus, затем возникает прерывание, в котором переменной ButtonsStatus присваивается какое-то значение. Затем возвращаемся из прерывания назад, и выполняем ButtonsStatus = 0!!! Все, мы потеряли одно нажатие на кнопку. Это называется нарушение атомарности выполнения операций. Может возникать, когда к одной и той же переменной обращаются из разных потоков. Нарушение атомарности в некоторых случаях вызывает появление плавающего бага, который очень трудно отловить. Поэтому эти две операции помещаем между __disable_interrupt() и __enable_interrupt(), тем самым решаем проблему.

Ну и демонстрация. Сделаем счетчик, значение которого можно увеличивать и уменьшать кнопками + и -, причем считать он может от 0 до 99. Для этого добавим еще немного кода:

static unsigned char dec2bcdTens(unsigned char num){return num / 10;}
static unsigned char dec2bcdUnits(unsigned char num){return num % 10;}

int main(void)
{
  char my_counter = 0;
  
  DispInit();
  __enable_interrupt();
  while (1)
  {
    //читаем состояние кнопок в переменную tmp
    //она нуджна для того, чтобы можно было реализовать
    //что-то типа
    //if(tmp == кнопка_плюс)
    //	что-то делаем
    //else if(tmp == кнопка_минус)
    //	что-то делаем
    //
    //при этом
    //if(GetButtonsStatus() == кнопка_плюс)
    //	что-то делаем
    //else if(GetButtonsStatus() == кнопка_минус)
    //	что-то делаем
    //
    //корректно работать на будет,
    //так как в GetButtonsStatus()
    //обнуляет переменную с нажатыми кнопками
    //после обращения
    //и этот код будет реагирповать только на
    //нажатие кнопки плюс
    uint8_t tmp = GetButtonsStatus();
    
    
    //у нас на 7-м выводе сидит кнопка плюс
    //на 6-м минус
    
    if(tmp == 1<<7) //нажали плюс
    {
      if(my_counter < 99) //если не перевалили за сотню
        my_counter++; //увеличиваем наш счетчик на единицу
    }
    else if(tmp == 1<<6) //нажали минус
    {
        if(my_counter > 0) //если еще не уходим в минус
          my_counter--; //уменьшаем счетчик на единицу
    }
    
    //Обновляем диспло
    LedSetVal(0, LED_CHAR_NULL); //гасим первые две цифры, чтоб не светили
    LedSetVal(1, LED_CHAR_NULL);
    LedSetVal(2, dec2bcdTens(my_counter)); //выделяем десятки и отправляем
    LedSetVal(3, dec2bcdUnits(my_counter)); //выделяем единицы и отправляем
  }
}

Собственно все)) Вот демонстрация работы для наглядности:

В данном примере используются только 2 кнопки, однако их количество можно увеличить до 8-и без изменения программы опроса. Только в main() нужно будет добавить обработчики нажатий на эти кнопки.

Архив с исходниками и моделью Proteus 8: https://yadi.sk/d/9JCOlRxc3NxGXD

 

 

Закладка Постоянная ссылка.

Добавить комментарий