Содержание

Эксперимент 36. Прерывания

В предыдущих уроках, мы писали достаточно простые программы. Их простота заключалась в линейности логики алгоритма. В главном цикле проверялось состояние кнопки и сразу же изменялось состояние светодиода. Задержка, если необходимо, вносилась с помощью функции delay(). Этот пример отлично работает, с небольшими схемами и примитивной логикой. Но если хочется чего-то большего, то старыми средствами уже не обойтись и от использования delay() придется отказаться.

Ранее, чтобы проверить состояние вывода микроконтроллера мы использовали команду вроде value_a = digitalRead(ENC_A). Чтобы узнать изменилось ли состояние по сравнению с предыдущей проверкой мы хранили ее результат в переменной и сравнивали. А если нам нужно отслеживать короткие изменения состояния пинов, то проверять их состояние нужно как можно чаще. Все это заставляет микроконтроллер тратить процессорное время на простую работу с сигналом.

Прерывания

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

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

Представь, что ты — это микроконтроллер и ты очень занят важным делом — читаешь эту статью. Но внезапно начинает звонить твой телефон. Ты откладываешь статью и занимаешься обработкой этого прерывания — отвечаешь на звонок и ведешь беседу. Как только беседа завершена, ты возвращаешься к чтению статьи с того места, где прервался. Примерно так устроена обработка прерываний в микроконтроллере.

Эксперимент

Попробуем применить прерывания для работы с энкодером. Нам потребуется сделать две вещи — настроить прерывание и написать функцию-обработчик прерывания.

Схема эксперимента

Оставим схему прошлого эксперимента без изменений. Всё новое будет заключаться в программе. Рисунок 1. Электрическая принципиальная схема эксперимента

Рисунок 2. Монтажная схема эксперимента

Программный код эксперимента

Exp36.py
  1. #include <LCDI2C_Multilingual.h>
  2.  
  3. #define ENC_A 13
  4. #define ENC_B 12
  5. #define DEFAULT_I2C_ADDR 0x3F // Или 0x27 в зависимости от твоей платы IoT
  6.  
  7. LCDI2C_Generic lcd(DEFAULT_I2C_ADDR, 16, 2);
  8.  
  9. bool states[4][2] = {
  10. {0,0},
  11. {1,0},
  12. {1,1},
  13. {0,1}
  14. };
  15.  
  16. volatile int count = 0;
  17. volatile int state = 0;
  18. volatile int state_old = 0;
  19.  
  20. void printLcd(int number) {
  21. lcd.clear();
  22. lcd.print(number);
  23. }
  24.  
  25. int index(bool a, bool b){
  26. for (int i = 0; i < 4; i++){
  27. if (states[i][0] == a and states[i][1] == b) {
  28. return i;
  29. }
  30. }
  31. return -1;
  32. }
  33.  
  34. void ICACHE_RAM_ATTR callback() {
  35. bool value_a = digitalRead(ENC_A);
  36. bool value_b = digitalRead(ENC_B);
  37.  
  38. state = index(value_a, value_b);
  39.  
  40. if ((state - state_old == 1) or (state == 0 and state_old == 3)) {
  41. count++;
  42. if (not(count % 2)){
  43. Serial.println("+");
  44. printLcd(count / 2);
  45. }
  46. state_old = state;
  47. }
  48. else if ((state - state_old == -1) or (state == 3 and state_old == 0)) {
  49. count--;
  50. if (not(count % 2)){
  51. Serial.println("-");
  52. printLcd(count / 2);
  53. }
  54. state_old = state;
  55. }
  56. }
  57.  
  58. void setup() {
  59. lcd.init();
  60. lcd.setBacklight(0);
  61. pinMode(ENC_A, INPUT);
  62. pinMode(ENC_B, INPUT);
  63.  
  64. attachInterrupt(digitalPinToInterrupt(ENC_A), callback, CHANGE);
  65. attachInterrupt(digitalPinToInterrupt(ENC_B), callback, CHANGE);
  66. }
  67.  
  68. void loop() {
  69. }

Сначала настраиваем прерывания:

  1. attachInterrupt(digitalPinToInterrupt(ENC_A), callback, CHANGE);
  2. attachInterrupt(digitalPinToInterrupt(ENC_B), callback, CHANGE);

Мы настроили при помощи указание события CHANGE, что при возникновении события изменения сигнала на выводах ENC_A и ENC_A с высокого на низкий или с низкого на высокий будет вызываться обработчик прерывания — функция callback.

Какие могут быть события:

Обратите внимание на новое ключевое слово volatile. Все переменные изменяемые из функции вызываемой прерыванием должны быть объявлены с этим ключевым словом. Необходимость обусловлена особенностями работой с памятью микроконтроллера.

  1. volatile int count = 0;
  2. volatile int state = 0;
  3. volatile int state_old = 0;

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

  1. void ICACHE_RAM_ATTR callback() {
  2. bool value_a = digitalRead(ENC_A);
  3. bool value_b = digitalRead(ENC_B);
  4.  
  5. state = index(value_a, value_b);
  6.  
  7. if ((state - state_old == 1) or (state == 0 and state_old == 3)) {
  8. count++;
  9. if (not(count % 2)){
  10. Serial.println("+");
  11. printLcd(count / 2);
  12. }
  13. state_old = state;
  14. }
  15. else if ((state - state_old == -1) or (state == 3 and state_old == 0)) {
  16. count--;
  17. if (not(count % 2)){
  18. Serial.println("-");
  19. printLcd(count / 2);
  20. }
  21. state_old = state;
  22. }
  23. }

Она начинается с

  1. bool value_a = digitalRead(ENC_A);
  2. bool value_b = digitalRead(ENC_B);

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

Потом, как и раньше, мы определяем текущее состояние линий A и B энкодера. Определяем индекс состояния и проверяем можем ли мы сделать переход в данное состояние.

Как мы заметили ранее, при одном щелчке энкодера происходит смена двух состояний энкодера. Чтобы одному щелчку соответствовало изменение счетчика на 1, мы просто проверяем счетчик на четность.

  1. if (not(count % 2)){
  2. Serial.println("+");
  3. printLcd(count / 2);
  4. }

Если число четное (нет остатка от деления на 2) то пишем в терминал + и выводим число, деленное на 2 на дисплей.

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