Написание обработчиков прерываний¶
На подходящем оборудовании MicroPython предоставляет возможность писать обработчики прерываний на Python. Обработчики прерываний, также известные как процедуры обслуживания прерываний (ISR), определяются как функции обратного вызова. Они выполняются в ответ на событие, такое как срабатывание таймера или изменение напряжения на выводе. Подобные события могут происходить в любой момент выполнения программного кода. Это влечёт за собой значительные последствия, часть из которых специфична для языка MicroPython. Другие же характерны для всех систем, способных реагировать на события в реальном времени. В этом документе сначала рассматриваются вопросы, специфичные для языка, а затем приводится краткое введение в программирование в реальном времени для тех, кто с ним не знаком.
В этом введении используются расплывчатые термины, такие как «медленно» или «как можно быстрее». Это сделано намеренно, поскольку скорости зависят от приложения. Допустимая длительность ISR зависит от частоты возникновения прерываний, характера основной программы и наличия других одновременно происходящих событий.
Советы и рекомендуемые практики¶
Здесь обобщаются подробно изложенные ниже моменты и перечисляются основные рекомендации для кода обработчиков прерываний.
Делайте код как можно более коротким и простым.
Избегайте выделения памяти: никакого добавления в списки или вставки в словари, никаких чисел с плавающей точкой.
Рассмотрите возможность использования
micropython.scheduleдля обхода указанного выше ограничения.Если ISR возвращает несколько байтов, используйте заранее выделенный
bytearray. Если между ISR и основной программой необходимо разделять несколько целых чисел, рассмотрите массив (array.array).Когда данные разделяются между основной программой и ISR, рассмотрите возможность отключения прерываний перед доступом к данным в основной программе и их немедленного повторного включения сразу после этого (см. Критические секции).
Выделите аварийный буфер исключений (см. ниже).
Особенности MicroPython¶
Аварийный буфер исключений¶
Если в ISR возникает ошибка, MicroPython не может сформировать отчёт об ошибке, если для этой цели не создан специальный буфер. Отладка упрощается, если в любую программу, использующую прерывания, включить следующий код.
import micropython
micropython.alloc_emergency_exception_buf(100)
Аварийный буфер исключений может хранить только одну трассировку стека исключения. Это означает, что если во время обработки исключения при заблокированной куче выбрасывается второе исключение, трассировка стека этого второго исключения заменит исходную, даже если второе исключение корректно обработано. Это может привести к запутанным сообщениям об исключениях, если буфер впоследствии будет выведен.
Простота¶
По ряду причин важно делать код ISR как можно более коротким и простым. Он должен выполнять только то, что необходимо сделать немедленно после вызвавшего его события: операции, которые можно отложить, следует делегировать основному циклу программы. Обычно ISR работает с аппаратным устройством, вызвавшим прерывание, подготавливая его к следующему прерыванию. Он взаимодействует с основным циклом, обновляя разделяемые данные, чтобы указать на то, что прерывание произошло, после чего завершается. ISR должен возвращать управление основному циклу как можно быстрее. Это не является специфической проблемой MicroPython, поэтому подробнее рассматривается ниже.
Взаимодействие между ISR и основной программой¶
Обычно ISR необходимо взаимодействовать с основной программой. Простейший способ сделать это — один или несколько разделяемых объектов данных, объявленных либо как глобальные, либо разделяемых через класс (см. ниже). С этим связаны различные ограничения и опасности, которые подробнее рассматриваются ниже. Для этой цели обычно используются целые числа, объекты bytes и bytearray, а также массивы (из модуля array), которые могут хранить различные типы данных.
Использование методов объектов в качестве функций обратного вызова¶
MicroPython поддерживает эту мощную технику, которая позволяет ISR разделять переменные экземпляра с базовым кодом. Она также позволяет классу, реализующему драйвер устройства, поддерживать несколько экземпляров устройств. Следующий пример заставляет два светодиода мигать с разной частотой.
import machine
import micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
def __init__(self, freq, led):
self.led = led
self.timer = machine.Timer(-1, freq=freq, callback=self.cb, hard=True)
def cb(self, tim):
self.led.toggle()
red = Foo(1, machine.LED("LED_RED"))
green = Foo(0.8, machine.LED("LED_GREEN"))
В этом примере экземпляр red управляет красным светодиодом от виртуального таймера с частотой 1 Гц: при каждом срабатывании таймера вызывается red.cb(), переключающий красный светодиод. Экземпляр green работает аналогично, с таймером 0,8 Гц, переключающим зелёный светодиод. Использование методов экземпляра даёт два преимущества. Во-первых, единый класс позволяет разделять код между несколькими аппаратными экземплярами. Во-вторых, поскольку это связанный метод, первым аргументом функции обратного вызова является self. Это позволяет функции обратного вызова обращаться к данным экземпляра и сохранять состояние между последовательными вызовами. Например, если бы в приведённом выше классе была переменная self.count, установленная в ноль в конструкторе, cb() мог бы увеличивать счётчик. Тогда экземпляры red и green поддерживали бы независимые подсчёты количества переключений состояния каждого светодиода.
Создание объектов Python¶
ISR не могут создавать экземпляры объектов Python. Это связано с тем, что MicroPython необходимо выделить память для объекта из хранилища свободных блоков памяти, называемого heap. Это не разрешено в обработчике прерываний, поскольку выделение памяти из кучи не является реентерабельным. Иными словами, прерывание может произойти, когда основная программа находится посередине выполнения операции выделения памяти; для сохранения целостности кучи интерпретатор запрещает выделение памяти в коде ISR.
Следствием этого является то, что ISR не могут использовать арифметику с плавающей точкой; это связано с тем, что числа с плавающей точкой являются объектами Python. Аналогичным образом ISR не может добавить элемент в список. На практике бывает трудно точно определить, какие конструкции кода попытаются выполнить выделение памяти и спровоцируют сообщение об ошибке: ещё одна причина делать код ISR коротким и простым.
Один из способов избежать этой проблемы — использовать в ISR заранее выделенные буферы. Например, конструктор класса создаёт экземпляр bytearray и логический флаг. Метод ISR записывает данные в позиции буфера и устанавливает флаг. Выделение памяти происходит в коде основной программы при создании экземпляра объекта, а не в ISR.
Методы ввода-вывода библиотеки MicroPython обычно предоставляют возможность использовать заранее выделенный буфер. Например, machine.I2C.readfrom_into() читает в предоставленный вызывающей стороной изменяемый буфер: это позволяет использовать его в ISR.
Способ создания объекта без использования класса или глобальных переменных выглядит следующим образом:
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
Компилятор создаёт экземпляр аргумента по умолчанию buf при первой загрузке функции (обычно при импорте модуля, в котором она находится).
Один из случаев создания объекта происходит при создании ссылки на связанный метод. Это означает, что ISR не может передать связанный метод функции. Одно из решений — создать ссылку на связанный метод в конструкторе класса и передать эту ссылку в ISR. Например:
class Foo():
def __init__(self):
self.bar_ref = self.bar # Allocation occurs here
self.x = 0.1
self.tim = machine.Timer(-1, freq=2, callback=self.cb, hard=True)
def bar(self, _):
self.x *= 1.2
print(self.x)
def cb(self, t):
# Passing self.bar would cause allocation.
micropython.schedule(self.bar_ref, 0)
Другие приёмы — определить и создать экземпляр метода в конструкторе или передать Foo.bar() с аргументом self.
Использование объектов Python¶
Дополнительное ограничение на объекты возникает из-за особенностей работы Python. При выполнении инструкции import код Python компилируется в bytecode, причём одна строка кода обычно отображается на несколько байт-кодов. При выполнении кода интерпретатор считывает каждый байт-код и выполняет его в виде серии инструкций машинного кода. Учитывая, что прерывание может произойти в любой момент между инструкциями машинного кода, исходная строка кода Python может быть выполнена лишь частично. Следовательно, объект Python, такой как множество, список или словарь, изменяемый в основном цикле, может оказаться внутренне несогласованным в момент возникновения прерывания.
Типичный исход выглядит следующим образом. В редких случаях ISR срабатывает в тот самый момент, когда объект обновлён частично. Когда ISR пытается прочитать объект, происходит сбой. Поскольку подобные проблемы обычно возникают редко и случайно, их бывает трудно диагностировать. Существуют способы обойти эту проблему, описанные в разделе Критические секции ниже.
Важно чётко понимать, что составляет изменение объекта. Изменение содержимого массива или bytearray безопасно. Это связано с тем, что байты или слова записываются как одна инструкция машинного кода, которая не прерывается: на языке программирования в реальном времени такая запись является атомарной. То же справедливо и для обновления элемента словаря, поскольку элементы являются машинными словами, представляя собой целые числа или указатели на объекты. Определённый пользователем объект может создавать экземпляр массива или bytearray. Изменение содержимого таких структур допустимо как для основного цикла, так и для ISR.
Опасность возникает при изменении структуры объекта, особенно в случае словарей. Добавление или удаление ключей может вызвать перехеширование. Если жёсткий ISR срабатывает во время выполнения перехеширования и пытается обратиться к элементу, может произойти сбой. Внутренне глобальные переменные реализованы как словарь. Следовательно, основная программа должна создать все необходимые глобальные переменные до запуска процесса, генерирующего жёсткие прерывания. Кроме того, прикладной код должен избегать удаления глобальных переменных.
MicroPython поддерживает целые числа произвольной точности. Значения между 230 -1 и -230 хранятся в одном машинном слове. Бо́льшие значения хранятся как объекты Python. Следовательно, изменения длинных целых чисел нельзя считать атомарными. Использование длинных целых чисел в ISR небезопасно, поскольку при изменении значения переменной может быть предпринята попытка выделения памяти.
Преодоление ограничения на числа с плавающей точкой¶
В целом лучше избегать использования чисел с плавающей точкой в коде ISR: аппаратные устройства обычно работают с целыми числами, а преобразование в числа с плавающей точкой обычно выполняется в основном цикле. Тем не менее существует несколько алгоритмов ЦОС, которым требуется плавающая точка. На платформах с аппаратной плавающей точкой (таких как камеры OpenMV Cam на базе STM32) для обхода этого ограничения можно использовать встроенный ассемблер ARM Thumb. Это связано с тем, что процессор хранит значения с плавающей точкой в машинном слове; значения могут разделяться между ISR и кодом основной программы через массив чисел с плавающей точкой.
Использование micropython.schedule¶
Эта функция позволяет ISR запланировать выполнение функции обратного вызова «очень скоро». Функция обратного вызова ставится в очередь на выполнение, которое произойдёт в момент, когда куча не заблокирована. Следовательно, она может создавать объекты Python и использовать числа с плавающей точкой. Функция обратного вызова также гарантированно выполняется в момент, когда основная программа завершила любое обновление объектов Python, поэтому функция обратного вызова не столкнётся с частично обновлёнными объектами.
Типичное применение — обработка аппаратного датчика. ISR получает данные от оборудования и позволяет ему сгенерировать следующее прерывание. Затем он планирует функцию обратного вызова для обработки данных.
Запланированные функции обратного вызова должны соответствовать изложенным ниже принципам проектирования обработчиков прерываний. Это нужно для избежания проблем, возникающих из-за активности ввода-вывода и изменения разделяемых данных, которые могут проявиться в любом коде, прерывающем основной цикл программы.
Время выполнения необходимо рассматривать в соотношении с частотой, с которой могут возникать прерывания. Если прерывание происходит во время выполнения предыдущей функции обратного вызова, в очередь на выполнение будет поставлен ещё один экземпляр функции обратного вызова; он запустится после завершения текущего экземпляра. Поэтому устойчиво высокая частота повторения прерываний несёт риск неограниченного роста очереди и в конечном итоге сбоя с ошибкой RuntimeError.
Если функция обратного вызова, передаваемая в schedule(), является связанным методом, обратите внимание на примечание в разделе «Создание объектов Python».
Исключения¶
Если ISR вызывает исключение, оно не распространится в основной цикл. Прерывание будет отключено, если только исключение не обработано кодом ISR.
Взаимодействие с asyncio¶
Когда выполняется ISR, он может прервать планировщик asyncio. Если ISR выполняет операцию asyncio, работа планировщика может быть нарушена. Это применимо независимо от того, является ли прерывание жёстким или мягким, а также применимо, если ISR передал выполнение другой функции через micropython.schedule. В частности, создание или отмена задач недопустимы в контексте ISR. Безопасный способ взаимодействия с asyncio — реализовать корутину с синхронизацией, выполняемой через asyncio.ThreadSafeFlag. Следующий фрагмент иллюстрирует создание задачи в ответ на прерывание:
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
В этом примере между выполнением ISR и выполнением foo() будет переменная величина задержки. Это присуще кооперативному планированию. Максимальная задержка зависит от приложения и платформы, но обычно может измеряться десятками мс.
Общие вопросы¶
Это всего лишь краткое введение в тему программирования в реальном времени. Начинающим следует учитывать, что ошибки проектирования в программах реального времени могут приводить к неисправностям, которые особенно трудно диагностировать. Это связано с тем, что они могут возникать редко и через по сути случайные интервалы. Крайне важно с самого начала правильно спроектировать систему и предвидеть проблемы до их возникновения. И обработчики прерываний, и основная программа должны проектироваться с учётом следующих вопросов.
Проектирование обработчиков прерываний¶
Как упоминалось выше, ISR следует проектировать как можно более простыми. Они всегда должны завершаться за короткий, предсказуемый промежуток времени. Это важно, потому что пока выполняется ISR, основной цикл не выполняется: основной цикл неизбежно испытывает паузы в выполнении в случайных точках кода. Такие паузы могут быть источником трудно диагностируемых ошибок, особенно если их длительность велика или переменна. Чтобы понять последствия времени выполнения ISR, необходимо базовое представление о приоритетах прерываний.
Прерывания организованы по схеме приоритетов. Код ISR сам может быть прерван прерыванием более высокого приоритета. Это имеет последствия, если два прерывания разделяют данные (см. Критические секции ниже). Если такое прерывание происходит, оно вносит задержку в код ISR. Если прерывание более низкого приоритета происходит во время выполнения ISR, оно будет отложено до завершения ISR: если задержка слишком велика, прерывание более низкого приоритета может дать сбой. Ещё одна проблема с медленными ISR — случай, когда во время его выполнения происходит второе прерывание того же типа. Второе прерывание будет обработано по завершении первого. Однако если частота поступающих прерываний постоянно превышает способность ISR их обслуживать, исход будет не радостным.
Следовательно, конструкций с циклами следует избегать или сводить их к минимуму. Ввода-вывода к устройствам, отличным от прерывающего устройства, обычно следует избегать: такой ввод-вывод, как доступ к диску, инструкции print и доступ к UART, относительно медленный, а его длительность может варьироваться. Ещё одна проблема здесь в том, что функции файловой системы не реентерабельны: использование ввода-вывода файловой системы в ISR и основной программе было бы опасным. Принципиально важно, чтобы код ISR не ожидал события. Ввод-вывод допустим, если можно гарантировать, что код вернётся за предсказуемый промежуток времени, например переключение вывода или светодиода. Доступ к прерывающему устройству через I2C или SPI может быть необходим, но время, затрачиваемое на такие обращения, следует рассчитать или измерить и оценить его влияние на приложение.
Обычно возникает необходимость разделять данные между ISR и основным циклом. Это можно сделать либо через глобальные переменные, либо через переменные класса или экземпляра. Переменные обычно имеют целочисленный или логический тип, либо являются массивами целых чисел или байтов (заранее выделенный массив целых чисел обеспечивает более быстрый доступ, чем список). Когда ISR изменяет несколько значений, необходимо учитывать случай, когда прерывание происходит в момент, когда основная программа обратилась к некоторым, но не всем значениям. Это может привести к несогласованности.
Рассмотрим следующую конструкцию. ISR сохраняет поступающие данные в bytearray, затем добавляет количество принятых байтов к целому числу, представляющему общее количество байтов, готовых к обработке. Основная программа считывает количество байтов, обрабатывает байты, затем сбрасывает количество готовых байтов. Это будет работать до тех пор, пока прерывание не произойдёт сразу после того, как основная программа прочитала количество байтов. ISR помещает добавленные данные в буфер и обновляет количество принятых, но основная программа уже прочитала количество, поэтому обрабатывает первоначально полученные данные. Вновь поступившие байты теряются.
Существуют различные способы избежать этой опасности, простейший из которых — использование кольцевого буфера. Если невозможно использовать структуру с присущей ей потокобезопасностью, ниже описаны другие способы.
Реентерабельность¶
Потенциальная опасность может возникнуть, если функция или метод разделяется между основной программой и одним или несколькими ISR либо между несколькими ISR. Проблема здесь в том, что функция сама может быть прервана и запущен ещё один экземпляр этой функции. Если такое возможно, функция должна быть спроектирована реентерабельной. То, как это делается, является продвинутой темой, выходящей за рамки этого руководства.
Критические секции¶
Примером критической секции кода является секция, обращающаяся более чем к одной переменной, на которую может повлиять ISR. Если прерывание случайно происходит между обращениями к отдельным переменным, их значения окажутся несогласованными. Это случай опасности, известной как состояние гонки: ISR и основной цикл программы соревнуются за изменение переменных. Чтобы избежать несогласованности, необходимо применить средство, гарантирующее, что ISR не изменит значения на протяжении критической секции. Один из способов достичь этого — выполнить machine.disable_irq() перед началом секции и machine.enable_irq() в её конце. Вот пример такого подхода:
import machine
import micropython
import array
import random
import time
micropython.alloc_emergency_exception_buf(100)
class BoundsException(Exception):
pass
ARRAYSIZE = const(20)
index = 0
data = array.array('i', [0] * ARRAYSIZE)
def callback1(t):
global data, index
for x in range(5):
data[index] = random.getrandbits(30) # simulate input
index += 1
if index >= ARRAYSIZE:
raise BoundsException('Array bounds exceeded')
tim = machine.Timer(-1, freq=100, callback=callback1, hard=True)
for loop in range(1000):
if index > 0:
irq_state = machine.disable_irq() # Start of critical section
for x in range(index):
print(data[x])
index = 0
machine.enable_irq(irq_state) # End of critical section
print('loop {}'.format(loop))
time.sleep_ms(1)
tim.deinit()
Критическая секция может состоять из одной строки кода и одной переменной. Рассмотрим следующий фрагмент кода.
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
Этот пример иллюстрирует тонкий источник ошибок. Строка count += 1 в основном цикле несёт конкретную опасность состояния гонки, известную как чтение-изменение-запись. Это классическая причина ошибок в системах реального времени. В основном цикле MicroPython считывает значение count, прибавляет к нему 1 и записывает обратно. В редких случаях прерывание происходит после чтения и до записи. Прерывание изменяет count, но его изменение перезаписывается основным циклом, когда ISR возвращается. В реальной системе это может привести к редким, непредсказуемым сбоям.
Как упоминалось выше, следует проявлять осторожность, если экземпляр встроенного типа Python изменяется в основном коде и к этому экземпляру обращается ISR. Код, выполняющий изменение, следует рассматривать как критическую секцию, чтобы гарантировать, что экземпляр находится в корректном состоянии при выполнении ISR.
Особую осторожность необходимо проявлять, если набор данных разделяется между разными ISR. Опасность здесь в том, что прерывание более высокого приоритета может произойти, когда прерывание более низкого приоритета частично обновило разделяемые данные. Решение этой ситуации является продвинутой темой, выходящей за рамки данного введения, за исключением замечания о том, что иногда могут использоваться описанные ниже объекты мьютекса.
Отключение прерываний на время критической секции — обычный и простейший способ действия, но он отключает все прерывания, а не только то, которое потенциально может вызвать проблемы. Обычно нежелательно отключать прерывание надолго. В случае прерываний таймера это вносит изменчивость во время срабатывания функции обратного вызова. В случае прерываний устройств это может привести к слишком позднему обслуживанию устройства с возможной потерей данных или ошибками переполнения в аппаратуре устройства. Подобно ISR, критическая секция в основном коде должна иметь короткую, предсказуемую длительность.
Подход к работе с критическими секциями, который радикально сокращает время отключения прерываний, заключается в использовании объекта, называемого мьютексом (название происходит от понятия взаимного исключения). Основная программа блокирует мьютекс перед выполнением критической секции и разблокирует его в конце. ISR проверяет, заблокирован ли мьютекс. Если да, он избегает критической секции и возвращается. Задача проектирования состоит в том, чтобы определить, что должен делать ISR в случае, когда доступ к критическим переменным запрещён. Простой пример мьютекса можно найти здесь. Обратите внимание, что код мьютекса действительно отключает прерывания, но только на время выполнения восьми машинных инструкций: преимущество этого подхода в том, что другие прерывания практически не затрагиваются.
Прерывания и REPL¶
Обработчики прерываний, такие как связанные с таймерами, могут продолжать работать после завершения программы. Это может приводить к неожиданным результатам там, где вы могли бы ожидать, что объект, вызывающий функцию обратного вызова, вышел из области видимости. Например, на OpenMV Cam:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
Это продолжает выполняться до тех пор, пока таймер не будет явно отключён или плата не будет сброшена с помощью Ctrl-D.