Учебный курс. АЦП микроконтроллера AVR или как подключить 4 кнопки к одному выводу. Часть 2

07/10/2009 - 21:00


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

При нажатии S1 напряжение на входе АЦП будет равно 5 В· 2 КОм/(2 + 2 + 2 + 2) КОм = 1,25 В
Для S2  -  5 · 4 /8 = 2,5 В
Для S3 –  5 · 6 /8 = 3,75 В
Для S4 – 5 В.

(Резистор R7 почти не влияет на наши расчеты, поэтому им можно пренебречь.)

Перевести напряжение на входе АЦП в цифровой код, можно по формуле

((2n-1) · Uin )/ Uref

где n – разрядность АЦП, Uin – входное напряжение, Uref – напряжение опорного источника

АЦП AVRа 10-ти разрядный, но мы используем только 8 старших разрядов. Напряжение опорного источника  равно 5 В.

((28-1)  · 1,25)/ 5 = (255· 1,25) / 5 = 64
(255· 2,5) / 5 = 128
(255· 3,75) / 5 = 191
(255· 5) / 5 = 255

Теперь можно свести расчеты в табличку, а заодно и выбрать пороговые значения для нашей программы.
напряжения на входе АЦП, цифровой код после преобразования, пороговые значения

АЦП микроконтроллера AVR


//программирование микроконтроллеров AVR на Си - осваиваем АЦП
#include <ioavr.h>
#include <intrinsics.h>
 
#define StartConvAdc() ADCSRA |= (1<<ADSC) 

#define KEY_NULL  0
#define KEY_S1    1
#define KEY_S2    2
#define KEY_S3    3
#define KEY_S4    4

//кнопочный буфер
volatile unsigned char KeyBuf = 0;

int main(void)
{
  unsigned char tmp;
 
  //настраиваем порты
  DDRC = 0xff;
  PORTC = 0xff;

  //инициализируем АЦП
  //ион - напряжение питания, выравнивание влево, нулевой канал

  ADMUX = (0<<REFS1)|(1<<REFS0)|(1<<ADLAR)|(0<<MUX3)|(0<<MUX2)|(0<<MUX1)|(0<<MUX0);
  //вкл. ацп, режим одиночного преобр., разрешение прер., частота преобр. = FCPU/128
  ADCSRA = (1<<ADEN)|(1<<ADSC)|(0<<ADATE)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);

  //разрешаем прерывания и запускаем преобразование
  __enable_interrupt();
  StartConvAdc();

  //основной цикл программы - опрос кнопочного буфера
  while(1)
  {
    tmp = KeyBuf;
    if (tmp)
    {
      tmp--;
      PORTC = ~(1<<tmp);
    }
    else
      PORTC = 0xff;
  }
  return 0;
}


#pragma vector=ADC_vect
__interrupt void adc_my(void)
{
  //считываем старший регистр АЦП
  unsigned char AdcBuf = ADCH;
 
  //опеределяем в какой диапазон попадает его значение
  if (AdcBuf > 240)
    KeyBuf = KEY_S4;
  else if (AdcBuf > 180)
    KeyBuf = KEY_S3;
  else if (AdcBuf > 120)
    KeyBuf = KEY_S2;
  else if (AdcBuf > 35)
    KeyBuf = KEY_S1;
  else
    KeyBuf = KEY_NULL;
 
  //запускаем преобразование и выходим
  StartConvAdc();
}


Пояснения к коду

Номера кнопок

  В программе мы используем переменную, в которой хранится номер нажатой кнопки. Задаем с помощью директивы #define символические имена этим номерам.

#define KEY_NULL  0
#define KEY_S1    1
#define KEY_S2    2
#define KEY_S3    3
#define KEY_S4    4

Переменная – кнопочный буфер

   Переменная, в которой хранится номер нажатой кнопки, используется и в прерывании и в основном коде программы. В прерывании в нее производится запись, а в основном коде она читается. Перед такими переменными нужно использовать квалификатор volatile. Он сообщает компилятору, что переменная может менять свое значение “непредсказуемым образом” и что не стоит ее оптимизировать.

//кнопочный буфер
volatile unsigned char KeyBuf = 0;

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

Опрос буфера в основном коде программы

   В функции main микроконтроллер инициализирует периферию и попадает в бесконечный цикл while. В цикле он без конца считывает кнопочный буфер, проверяет его на нулевое значение (if(tmp != 0) …и if(tmp) … - эквивалентные записи) и если буфер не пуст, зажигает соответствующий светодиод. Если буфер пуст – все светодиоды гасятся.

while(1)
  {
    tmp = KeyBuf;
    if (tmp)
    {
      tmp--;
      PORTC = ~(1<<tmp);
    }
    else
      PORTC = 0xff;
  }

Обработчик прерывания

   В обработчик прерывания микроконтроллер попадает каждый раз, когда преобразование завершено. Считывает значение регистра ACDH во временную переменную AdcBuf (эта локальная переменная - она объявлена в функции), а затем последовательно сравнивает ее с константами (пороговыми значениями). Если значение переменной удовлетворяет какому-либо условию, в кнопочный буфер KeyBuf – записывается номер соответствующей кнопки.
   Может показаться что этот кусок кода реализован на каком-то новом операторе else if (...), но на самом деле это просто вложенные ветвления if ... else.
   Можно записать и так:
 
  if (AdcBuf > 240)
    KeyBuf = KEY_S4;
  else
      if (AdcBuf > 180)
           KeyBuf = KEY_S3;
      else
           if (AdcBuf > 120)
                 KeyBuf = KEY_S2;
          else
                if (AdcBuf > 50)
                     KeyBuf = KEY_S1;
                else
                     KeyBuf = KEY_NULL;

   но согласитесь, что такая запись менее наглядна.

Устраняем ложные срабатывания

   Код рабочий, но есть нюанс. При нажатии/отпускании кнопки помимо нужного светодиода кратковременно зажигаются и остальные. Почему это происходит? На входе АЦП стоит RC цепочка и при нажатии на кнопку напряжение на конденсаторе появляется не сразу, а медленно нарастает до определенного уровня. АЦП за это время успевает сделать несколько преобразований, а ничего не подозревающий микроконтроллер "подумать", что были быстро нажаты несколько кнопок. Давайте устраним этот момент. Основной код (функцию main) трогать не будем, допишем только обработчик прерывания.

unsigned char comp = 0;
#pragma vector=ADC_vect
__interrupt void adc_my(void)
{
  unsigned char AdcBuf;   
  unsigned char Key;
  static unsigned char LastState;
 
  //считываем старший регистр АЦП
  AdcBuf = ADCH;
 
  //проверяем в какой диапазон попадает его значение
  if (AdcBuf > 240)
    Key = KEY_S4;
  else if (AdcBuf > 180)
    Key = KEY_S3;
  else if (AdcBuf > 120)
    Key = KEY_S2;
  else if (AdcBuf > 50)
    Key = KEY_S1;
  else
    Key = KEY_NULL;
 
//если какая-нибудь из кнопка нажата
//сравниваем предыдущее и текущее состояние
//если совпадают – проверяем счетчик comp,
//если нет – обнуляем его
//кнопка считается нажатой, если она удерживается в течении 100
//преобразований АЦП

  if (Key)
  {
    if (Key == LastState)
    {
      if (comp > 100)
        KeyBuf = Key;      
      else
        comp++;
    }
    else
    {
      LastState = Key;
      comp = 0;
    }
  }
  else
  {
    comp = 0;
    KeyBuf = KEY_NULL;
    LastState = KEY_NULL;
  }

  //запускаем преобразование и выходим
  StartConvAdc();
}

   Мы ввели три дополнительные переменные: comp, LastState и Key.
   Переменная Key временно хранит номер нажатой кнопки. Эта локальная переменная, ее значение  размещается в регистре общего назначения (РОН) и оно доступно только на время выполнения функции.  Переменная LastState хранит номер предыдущей нажатой кнопки. Она объявлена как статическая (с ключевым словом static). Физически она располагается в ОЗУ и поэтому ее значение сохраняется между вызовами функции!! Заметьте, что и Key и LastState не были инициализированы при объявлении. В них может находиться любое число, а вовсе не ноль, как иногда думают.  
   После того как микроконтроллер соотнесет результат преобразования АЦП с одним из диапазонов и запишет номер нажатой кнопки, он сравнит переменные Key и LastState друг с другом. Если они окажутся равны, переменная comp будет увеличена на единицу, если нет – обнулена. Как только значение comp превысит установленный порог, в нашем примере - 100, микроконтроллер перепишет значение переменной Key в кнопочный буфер KeyBuf.
   Подобный алгоритм часто используется для борьбы с дребезгом кнопок.

Итак, из этих двух статей вы узнали

-  Основные характеристики АЦП
-  Как инициализировать и запустить АЦП
-  Как перевести значение напряжения на входе АЦП в цифровой код
-  Конструкция if (…) else if(…)
-  Зачем нужен квалификатор volatile
-  Как объявить статическую переменную и в чем ее особенность
-  Алгоритм для борьбы с дребезгом кнопок

Файлы проекта - АЦП микроконтроллера AVR
Схема для нашего примера

 

Comments   

# Guest 2010-09-30 13:36
спасибо, отличная статья!
# Guest 2010-10-03 12:38
Приветствую всех. Pashgan, прекрасный сайт. Не совсем понятен принцип исключения дребезга кнопок, - ведь запускаем мы одиночное преобразование, как счетчик может до 100 добежать?
# Pashgan 2010-10-03 20:53
Так мы в прерывании постоянно запускаем АЦП.
# Артур 2010-10-08 08:33
С помощью StartConvAdc()?
# InfuriatedCoder 2011-01-29 23:08
Я, когда повторял этот способ подключения кнопок (только для 5 кнопок), сильно укоротил код таким способом:
Code:
unsigned char buttonPressed = (read_adc(0) + 25) / 51;
switch(buttonPressed)
{
case 0:
{
// ничего не выводим
}break;
case 1:
{
lcd_putsf("RIGHT");
}break;
case 2:
{
lcd_putsf("DOWN");
}break;
case 3:
{
lcd_putsf("LEFT");
}break;
case 4:
{
lcd_putsf("UP");
}break;
case 5:
{
lcd_putsf("OK");
}break;
}

цифровые коды при 5 кнопках - 51,102,153,204, 255
# Maxxon 2011-02-09 00:00
static unsigned char LastState;

Эту строку надо вынести из обработчика прерывания, как и comp, так как область действия переменной - только функция обработчика прерывания и last state там никогда не сохранится.

Хочу выразить благодарность за статью, реально помогла понять принцип работы АЦП. Заюзал 8 кнопок на mega16. WinAVR+AvrStudi o.
# Pashgan 2011-02-09 12:28
"Переменная LastState хранит номер предыдущей нажатой кнопки. Она объявлена как статическая (с ключевым словом static). Физически она располагается в ОЗУ и поэтому ее значение сохраняется между вызовами функции."
# Maxxon 2011-02-09 15:11
А, точно :)
Но у меня в WinAVR не заработало, пока не вынес наружу.
# Димка 2011-07-13 11:33
А если надо сделать чтобы вместо светодиодов была звуковая индикация, прерывания от ацп и таймера не будут мешать друг другу?
# Simon 2012-11-12 15:02
Последовательно сть "if (AdcBuf > BOUND4) Key = KEY_S4; else if (AdcBuf > BOUND1) Key = KEY_S1; ..." лучше заменить на отбрасывание от прочитанной из АЦП величины незначащих разрядов (ведь резистивный делитель выдает всего четыре значения, то есть, только старшие два бита несут полезную информацию): "AdcBuf = ADCH >> 5;"

"Заметьте, что и Key и LastState не были инициализирован ы при объявлении. В них может находиться любое число, а вовсе не ноль, как иногда думают."
Это ошибка - статические неинициализиров анные переменные по стандарту языка C содержат на старте программы нулевые значения. И все известные мне компиляторы соблюдают это правило.
# Pashgan 2012-11-12 18:59
Спасибо за замечание. Я этого не знал.
# Simon 2012-11-12 21:57
Да, например, собирают их все в сегмент типа NOINIT и весь его заполняют нулями.
Или совмещают обнуление и инициализацию (из флеша) в чуть более сложную процедуру, которая готовит все сегменты по таблице.
Пример в яре - 'segment_init.c '. :-)

У вас недостаточно прав для комментирования.