Сенсорная клавиатура для микроконтроллера


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

Есть несколько способов прикрутить сенсорную клавиатуру к МК. Если нагружать МК дополнительными вычислениями нет желания или возможности, можно воспользоваться специализированной микросхемой TTP223, она выпускается в корпусах типа SSOP-16, SOT-23 и SOP-8. Подобных микросхем достаточно много - к примеру AT42QT1012. В принципе, ёмкостной сенсор не сложно собрать и на дискретных элементах, однако это не всегда удобно.

Если необходимо сократить количество элементов до минимума, на микроконтроллере можно собрать простой ёмкостной сенсор. Схема сенсора предельно проста:

Схема сенсорной кнопки для микроконтроллера


Здесь резистор и конденсатор образуют RC-цепь. Постоянная времени такой цепи τ = R · C

В качестве сенсора выступает любая проводящая пластинка, или полоска фольги - она также обладает некоторой, пусть и очень малой, емкостью, порядка нескольких пикофарад. Время τ, за которое такая пластинка зарядится, при резисторе в 1 МОм составляет примерно несколько микросекунд. Очень маленькая величина, однако, для микроконтроллера, работающего на частоте 10 МГц, успеет пройти десяток тактов.

Теперь представим, что пластинки коснулся человек. Его емкость на пару порядков выше емкости пластинки и составляет несколько сотен пикофарад. Следовательно, время, за которое пластинка зарядится, существенно возрастет. Исходя из этого можно сделать вывод о прикосновении к сенсору.

Алгоритм опроса сенсора следующий:
  1. Изначально на пин RB0 подан логический 0. Таким образом, ток течет от источника питания через мегаомный резистор в пин. Если сенсор был заряжен, то заряд с него также будет стекать в пин RB0.
  2. В момент опроса мы переключаем пин с выхода на вход (подтяжки отключены!). В этот момент пин переходит в высокоимпедансное состояние, с сопротивлением порядка нескольких десятков МОм. Ток в направлении пина практически прекращает течь, и начинает течь в сторону сенсора. Как только сенсор зарядится до напряжения уровня логической 1, данный вход микроконтроллера покажет единицу.
  3. Измерив время, которое прошло с момента перевода RB0 в высокоимпедансное состояние до появления на нем единицы, можно сделать вывод об изменении емкости сенсора.


Весь код умещается в две простейшие функции (в моём варианте использовался порт RB на МК PIC18F2580):

  // При использовании другого порта указывайте соответствующие TRIS и LAT
  // Пример опроса: button = CallibrateButton(0, &button_open_time);
  // Здесь button_open_time - глобальная переменная, адрес которой передаётся в функцию
  // Обратите внимание! Здесь в функцию должен передаваться именно адрес переменной, а не её значение!

void CallibrateButton(unsigned char button_id, unsigned int open_button_time) {
  // Калибровка проводится для оценки времени заряда сенсора без прикосновения
  // Так как в моём случае я использую порт B, нужно убедиться, что перед началом опроса он работает как Digital Out
  // Состояние порта B в PIC18F2580 зависит в том числе и от регистра ADCON1
  ADCON1 |= 0b00001000;
  TRISB &= ~(1 << button_id); // Порт на выход
  LATB &= ~(1 << button_id); // В порт ноль
  __delay_us(200); // Ждём пока сенсор разрядится
  TMR0H = 0; TMR0L = 0; GIE = 0; TMR0ON = 1;
  // Обнуление таймера, запрет прерываний и запуск таймера
  TRISB |= (1 << button_id); // Переводим порт в высокоимпедансное состояние
  while ( (PORTB&(1 << button_id)) == 0 ); // ждём пока порт не покжает единицу
  TMR0ON = 0; GIE = 1; // Выключаем таймер и разрешаем прерывания
  open_button_time = TMR0 + 5;
  // Сохраняем значение таймера, с запасом для исключения ложных нажатий
  // Вместо "5" тут можно использовать переменную и ею подстраивать чувствительность
  // В моём случае я просто использовал 16-bit таймер и глобальную переменную типа int,
  // Но вообще-то должно хватать и 8-bit таймера

  TRISB &= ~(1 << button_id); // Порт на выход
  LATB &= ~(1 << button_id); // В порт ноль
  }
  // Обратите внимание! Так как здесь и далее в функцию передаётся адрес переменной, а не её значение, сама функция никаких значений не возвращает!



  // Пример вызова: button = CheckButton(0, &button_open_time);
  // Здесь button_open_time - глобальная переменная, адрес которой передаётся в функцию

unsigned char CheckButton(unsigned char button_id, unsigned int open_button_time) {
  // Собственно опрос кнопки. Функция возвращает состояние кнопки
  unsigned int button_time = 0;
  ADCON1 |= 0b00001000;   TRISB &= ~(1 << button_id); // Порт на выход
  LATB &= ~(1 << button_id); // В порт ноль
  __delay_us(200); // Ждём пока сенсор разрядится
  TMR0H = 0; TMR0L = 0; GIE = 0; TMR0ON = 1;
  // Обнуление таймера, запрет прерываний и запуск таймера
  TRISB |= (1 << button_id); // Переводим порт в высокоимпедансное состояние
  while ( (PORTB&(1 << button_id)) == 0 ); // ждём пока порт не покжает единицу
  TMR0ON = 0; GIE = 1; // Выключаем таймер и разрешаем прерывания
  button_time = TMR0; // Сохраняем значение таймера
  TRISB &= ~(1 << button_id); // Порт на выход
  LATB &= ~(1 << button_id); // В порт ноль
  if ( button_time > open_button_time) { return 1; } else { return 0; }; }
  // Если заряжался дольше открытого - значит есть касание


Данный пример призван показать принцип и не учитывает некоторых моментов. Например, в случае сильного загрязнения сенсора и возникновения паразитных утечек заряда на землю, он при калибровке может вообще не зарядится и МК зависнет в цикле. На этот случай можно сделать принудительный выход из цикла (например, по истечению какого-то времени) и индикацию неисправности сенсора.

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

Если требуется сделать несколько сенсоров, достаточно опросить их последовательно - займёт это не больше миллискунды, так что для пользователя будет выглядеть как "мультитач".

На что следует обратить внимание:
  • Для приемлемой точности распознавания касания нужно чтобы таймер, по которому ведётся отсчёт, за расчётное время τ успевал сделать как минимум несколько приращений. У меня всё стабильно работало с кварцевым резонатором 10 МГц. Полагаю со встроенным генератором на 8 МГц тоже должно корректно работать.
  • Плату, на которой вы собираете устройство нужно тщательно отмывать от флюса, а также следить, чтобы провода, идущие от сенсора не имели никаких паразитных связей с землёй - даже небольшая проводимость на землю, не даваст зарядиться сенсору и микроконтроллер будет постоянно висеть в цикле while при опросе.
  • С порта, используемого для сенсорного ввода, надо убирать всё лишнее - например, если подключить сенсор ко входу с обратно включенным на землю защитным диодом, ток утечки через диод на землю тоже не даст зарядиться сенсору.
  • Если используется порт с внутренними подтягивающими резисторами, то в конфигурации порта их нужно отключить, иначе получите проблему с симптомами, которые уже описали. В частности, например, в моём случае при инициализации я установил бит RBPU = 1, согласно даташиту "All PORTB pull-ups are control by latch".
  • Сильно увеличивать номинал резистора к питанию на сенсоре не имеет особого смысла, так как при слишком большом сопротивлении заряд, через различные паразитные утечки, будет быстрее стекать на землю и сенсор тоже не зарядится. Оптимальное значение лежит в пределах 600кОм - 2 МОм.


Для удобства повторного использования весь код вынесен в отдельный файл sensor.c.

Пример использования:

// В данном случае файл sensor.c следует положить в корень папки с вашим проектом
// Включаем файл где-нибудь перед объявлением глобальных переменных
#include "sensor.c"

// Глобальные переменные, для хранения состояния кнопок
volatile unsigned char button_1 = 0, button_2 = 0, button_3 = 0;
// Переменные для хранения калиброванного времени зарядки сенсора без прикосновения
volatile unsigned int button_1_open_time = 0, button_2_open_time = 0, button_3_open_time = 0;

// Далее приведён пример для трёх кнопок
void CallibrateAllButtons(void) {
  CallibrateButton(0, &button_1_open_time);
  CallibrateButton(1, &button_2_open_time);
  CallibrateButton(2, &button_3_open_time); }
  // Калибруем все кнопки по очереди

void CheckAllButtons(void) {
  button_1 = CheckButton(0, &button_1_open_time);
  button_2 = CheckButton(1, &button_2_open_time);
  button_3 = CheckButton(2, &button_3_open_time);
  // Опрашиваем все кнопки по очереди

  idle_millisec = 400; OSCCON = OSCCON_START_STATE; SLEEP(); }
// После опроса желательно сделать небольшую паузу чтобы пользователь успел убрать руку



Стоит отметить, что в Microchip разработали кучу библиотек для ускорения разработки сенсорных устройств, объединили всё это в одну технологию и назвали её mTouch. Способов реализации сенсора несколько, чаще всего используется т.н. "ёмкостной делитель", он позволяет использовать минимум внешней обвязки. Более того, новые контроллеры несут в себе модули "переферии независимой от ядра", которые позволяют собирать сенсоры вообще без внешних компонентов. Также есть модели МК, которые содержат специализированные Touch Controller'ы, что тоже позволяет обойтись без внешней обвязки при построении сенсора. С микрочиповскими гайдами по mTouch настоятельно рекомнедую ознакомиться - там много интересного как про разводку устройств с сенсорами, так и про способы реализации.

Скачать

Скачать Sensor.c Sensor.c

(3.3 кБайт)



P.S. Отдельное спасибо хабра-юзеру @Ariman за его статьи по данной теме - очень помогли разобраться.