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

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

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

Прерывания

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

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

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

Эксперимент

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

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

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

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

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

Exp36.py
  1. from machine import I2C, Pin
  2. from esp8266_i2c_lcd import I2cLcd
  3. import time
  4. _init()
  5.  
  6. DEFAULT_I2C_ADDR = 0x3F # Или 0x27 в зависимости от модели микросхемы на плате
  7.  
  8. encA = Pin(13, Pin.IN)
  9. encB = Pin(12, Pin.IN)
  10.  
  11. count = 0
  12.  
  13. states = (
  14. (1,1),
  15. (0,1),
  16. (0,0),
  17. (1,0)
  18. )
  19.  
  20. old_state = 0
  21.  
  22. def print_lcd(data):
  23. lcd.clear()
  24. lcd.putstr(str(data))
  25.  
  26. def callback(p):
  27. global count
  28. global encB
  29. global encA
  30. global states
  31. global old_state
  32.  
  33. value_a = encA.value()
  34. value_b = encB.value()
  35.  
  36. current_state = states.index((value_a, value_b))
  37.  
  38. if (current_state - old_state == 1) or (current_state == 0 and old_state == 3):
  39. count += 1
  40. if not (count % 2):
  41. print('+');
  42. print_lcd(int(count/2))
  43. old_state = current_state
  44. elif (current_state - old_state == -1) or (current_state == 3 and old_state == 0):
  45. count -= 1
  46. if not (count % 2):
  47. print('-');
  48. print_lcd(int(count/2))
  49. old_state = current_state
  50.  
  51.  
  52. i2c = I2C(scl=Pin(5), sda=Pin(4), freq=400000)
  53. lcd = I2cLcd(i2c, DEFAULT_I2C_ADDR, 2, 16)
  54. lcd.backlight_on()
  55.  
  56. encA.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=callback)
  57. encB.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=callback)
  58.  
  59.  
  60. while True:
  61. pass

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

  1. encA.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=callback)
  2. encB.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=callback)

Мы настроили, что при возникновении события изменения сигнала на выводах encA и encB с высокого на низкий (Pin.IRQ_FALLING) или с низкого на высокий (Pin.IRQ_RISING) будет вызываться обработчик прерывания — функция callback.

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

  • Pin.IRQ_FALLING изменение с 1 на 0
  • Pin.IRQ_RISING изменение с 0 на 1
  • Pin.IRQ_LOW_LEVEL наличие логического 0
  • Pin.IRQ_HIGH_LEVEL наличие логической 1

События можно комбинировать. Например, если нужна реакция и на событие Pin.IRQ_FALLING и Pin.IRQ_RISING, то можно записать так: trigger=(Pin.IRQ_FALLING | Pin.IRQ_RISING).

Рассмотрим функцию-обработчик прерывания:

  1. def callback(p):
  2. global count
  3. global encB
  4. global encA
  5. global states
  6. global old_state
  7.  
  8. value_a = encA.value()
  9. value_b = encB.value()
  10.  
  11. current_state = states.index((value_a, value_b))
  12.  
  13. if (current_state - old_state == 1) or (current_state == 0 and old_state == 3):
  14. count += 1
  15. if not (count % 2):
  16. print('+');
  17. print_lcd(int(count/2))
  18. old_state = current_state
  19. elif (current_state - old_state == -1) or (current_state == 3 and old_state == 0):
  20. count -= 1
  21. if not (count % 2):
  22. print('-');
  23. print_lcd(int(count/2))
  24. old_state = current_state

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

  1. global count
  2. global encB
  3. global encA
  4. global states
  5. global old_state

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

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

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

  1. if not (count % 2):
  2. print('+');
  3. print_lcd(int(count/2))

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

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

В этом эксперименте мы производили настройку аппаратных средств микроконтроллера, а именно настраивали прерывания. Как мы уже узнали, работа прерываний не зависит от программы и происходит автоматически. Это значит, что обработчик прерывания будет вызываться даже после завершения работы нашей программы, ведь настройки никуда не исчезли. Это может оказать влияние на следующие эксперименты, в которых не нужны эти прерывания. Они могут мешать. Для того, чтобы следующие эксперименты проходили нормально, необходимо перезагрузить микроконтроллер. Это можно сделать выдернув на некоторое время кабель USB или нажать на кнопку Reset на плате конструктора.