인터럽트 핸들러 작성하기¶
적합한 하드웨어에서 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은 인터럽트를 유발한 하드웨어 장치를 처리하여 다음 인터럽트가 발생할 수 있도록 준비시킵니다. ISR은 공유 데이터를 갱신하여 인터럽트가 발생했음을 표시함으로써 메인 루프와 통신하고, 그런 다음 반환합니다. ISR은 가능한 한 빨리 메인 루프로 제어를 반환해야 합니다. 이는 MicroPython에 특유한 문제가 아니므로 아래에서 더 자세히 다룹니다.
ISR과 메인 프로그램 간의 통신¶
일반적으로 ISR은 메인 프로그램과 통신해야 합니다. 이를 수행하는 가장 간단한 방법은 전역으로 선언하거나 클래스를 통해 공유되는(아래 참조) 하나 이상의 공유 데이터 객체를 이용하는 것입니다. 이를 수행하는 데에는 다양한 제약과 위험이 따르며, 아래에서 더 자세히 다룹니다. 정수, bytes 및 bytearray 객체가 이 목적으로 흔히 사용되며, 다양한 데이터 타입을 저장할 수 있는 배열(array 모듈)도 함께 사용됩니다.
콜백으로 객체 메서드 사용하기¶
MicroPython은 ISR이 기반 코드와 인스턴스 변수를 공유할 수 있게 해주는 이 강력한 기법을 지원합니다. 또한 장치 드라이버를 구현하는 클래스가 여러 장치 인스턴스를 지원할 수 있게 합니다. 다음 예제는 두 개의 LED가 서로 다른 속도로 깜박이도록 합니다.
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 Hz 가상 타이머로 빨간색 LED를 구동합니다. 타이머가 발화할 때마다 red.cb()가 호출되어 빨간색 LED를 토글합니다. green 인스턴스는 0.8 Hz 타이머로 녹색 LED를 토글하며 유사하게 동작합니다. 인스턴스 메서드를 사용하면 두 가지 이점이 있습니다. 첫째, 단일 클래스로 여러 하드웨어 인스턴스 간에 코드를 공유할 수 있습니다. 둘째, 바운드 메서드이므로 콜백 함수의 첫 번째 인자가 self입니다. 이를 통해 콜백이 인스턴스 데이터에 접근하고 연속된 호출 사이에 상태를 저장할 수 있습니다. 예를 들어, 위 클래스에 생성자에서 0으로 설정된 self.count 변수가 있다면, cb()가 카운터를 증가시킬 수 있습니다. 그러면 red와 green 인스턴스는 각 LED가 상태를 변경한 횟수를 독립적으로 카운트하게 됩니다.
Python 객체 생성¶
ISR은 Python 객체의 인스턴스를 생성할 수 없습니다. 이는 MicroPython이 heap이라고 불리는 빈 메모리 블록 저장소에서 객체를 위한 메모리를 할당해야 하기 때문입니다. 힙 할당은 재진입이 불가능하므로 인터럽트 핸들러에서는 이것이 허용되지 않습니다. 다시 말해, 메인 프로그램이 할당을 수행하는 도중에 인터럽트가 발생할 수 있는데, 힙의 무결성을 유지하기 위해 인터프리터는 ISR 코드에서의 메모리 할당을 금지합니다.
이로 인한 결과로 ISR은 부동소수점 연산을 사용할 수 없습니다. 이는 부동소수점이 Python 객체이기 때문입니다. 마찬가지로 ISR은 리스트에 항목을 추가할 수 없습니다. 실제로 어떤 코드 구조가 메모리 할당을 시도하고 오류 메시지를 유발할지 정확히 판단하기 어려울 수 있습니다. 이것이 ISR 코드를 짧고 단순하게 유지해야 하는 또 다른 이유입니다.
이 문제를 피하는 한 가지 방법은 ISR이 미리 할당된 버퍼를 사용하는 것입니다. 예를 들어 클래스 생성자가 bytearray 인스턴스와 불리언 플래그를 생성합니다. ISR 메서드는 버퍼의 위치에 데이터를 할당하고 플래그를 설정합니다. 메모리 할당은 ISR이 아니라 객체가 인스턴스화될 때 메인 프로그램 코드에서 발생합니다.
MicroPython 라이브러리 I/O 메서드는 일반적으로 미리 할당된 버퍼를 사용하는 옵션을 제공합니다. 예를 들어 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)
다른 기법으로는 생성자에서 메서드를 정의하고 인스턴스화하거나, self 인자와 함께 Foo.bar()를 전달하는 것이 있습니다.
Python 객체 사용¶
객체에 대한 추가적인 제약은 Python의 동작 방식 때문에 발생합니다. import 문이 실행되면 Python 코드는 bytecode로 컴파일되며, 일반적으로 한 줄의 코드가 여러 개의 바이트코드에 매핑됩니다. 코드가 실행될 때 인터프리터는 각 바이트코드를 읽어 일련의 기계어 명령어로 실행합니다. 인터럽트는 기계어 명령어 사이의 어느 때나 발생할 수 있으므로, 원래의 Python 코드 한 줄이 부분적으로만 실행되었을 수 있습니다. 결과적으로 메인 루프에서 수정된 집합, 리스트 또는 딕셔너리와 같은 Python 객체는 인터럽트가 발생하는 순간에 내부 일관성이 결여될 수 있습니다.
전형적인 결과는 다음과 같습니다. 드물게 ISR이 객체가 부분적으로 갱신된 바로 그 순간에 실행될 수 있습니다. ISR이 객체를 읽으려고 하면 충돌이 발생합니다. 이러한 문제는 일반적으로 드물고 무작위적인 경우에 발생하기 때문에 진단하기 어려울 수 있습니다. 이 문제를 우회하는 방법은 아래 임계 구역에서 설명합니다.
객체의 수정이 무엇을 구성하는지에 대해 명확히 하는 것이 중요합니다. 배열이나 bytearray의 내용을 변경하는 것은 안전합니다. 이는 바이트나 워드가 중단될 수 없는 단일 기계어 명령어로 기록되기 때문입니다. 실시간 프로그래밍 용어로 이 쓰기는 원자적(atomic)입니다. 딕셔너리 항목을 갱신하는 것도 마찬가지입니다. 항목은 정수이거나 객체에 대한 포인터인 기계어 워드이기 때문입니다. 사용자 정의 객체는 배열이나 bytearray를 인스턴스화할 수 있습니다. 메인 루프와 ISR 모두 이들의 내용을 변경하는 것은 유효합니다.
위험은 객체의 구조가 변경될 때, 특히 딕셔너리의 경우에 발생합니다. 키를 추가하거나 삭제하면 리해시(rehash)가 트리거될 수 있습니다. 리해시가 진행 중일 때 하드 ISR이 실행되어 항목에 접근하려고 하면 충돌이 발생할 수 있습니다. 내부적으로 전역 변수는 딕셔너리로 구현됩니다. 따라서 메인 프로그램은 하드 인터럽트를 생성하는 프로세스를 시작하기 전에 필요한 모든 전역 변수를 생성해야 합니다. 애플리케이션 코드는 또한 전역 변수 삭제를 피해야 합니다.
MicroPython은 임의 정밀도의 정수를 지원합니다. 230 -1과 -230 사이의 값은 단일 기계어 워드에 저장됩니다. 더 큰 값은 Python 객체로 저장됩니다. 따라서 긴 정수에 대한 변경은 원자적이라고 볼 수 없습니다. 변수의 값이 변경될 때 메모리 할당이 시도될 수 있으므로 ISR에서 긴 정수를 사용하는 것은 안전하지 않습니다.
부동소수점 제한 극복하기¶
일반적으로 ISR 코드에서 부동소수점 사용을 피하는 것이 가장 좋습니다. 하드웨어 장치는 보통 정수를 처리하며 부동소수점으로의 변환은 보통 메인 루프에서 수행됩니다. 그러나 부동소수점을 필요로 하는 일부 DSP 알고리즘이 있습니다. 하드웨어 부동소수점을 갖춘 플랫폼(STM32 기반 OpenMV Cam 등)에서는 인라인 ARM Thumb 어셈블러를 사용하여 이 제한을 우회할 수 있습니다. 이는 프로세서가 부동소수점 값을 기계어 워드에 저장하기 때문입니다. 값은 부동소수점 배열을 통해 ISR과 메인 프로그램 코드 간에 공유될 수 있습니다.
micropython.schedule 사용하기¶
이 함수는 ISR이 “매우 곧” 실행될 콜백을 예약할 수 있게 합니다. 콜백은 힙이 잠겨 있지 않은 시점에 실행되도록 큐에 들어갑니다. 따라서 콜백은 Python 객체를 생성하고 부동소수점을 사용할 수 있습니다. 또한 콜백은 메인 프로그램이 Python 객체의 갱신을 모두 완료한 시점에 실행되도록 보장되므로, 부분적으로 갱신된 객체를 만나지 않습니다.
전형적인 용도는 센서 하드웨어를 처리하는 것입니다. ISR은 하드웨어에서 데이터를 획득하고 추가 인터럽트를 발생시킬 수 있도록 합니다. 그런 다음 데이터를 처리할 콜백을 예약합니다.
예약된 콜백은 아래에 설명된 인터럽트 핸들러 설계 원칙을 준수해야 합니다. 이는 메인 프로그램 루프를 선점하는 모든 코드에서 발생할 수 있는 I/O 활동 및 공유 데이터 수정으로 인한 문제를 피하기 위함입니다.
실행 시간은 인터럽트가 발생할 수 있는 빈도와 관련하여 고려해야 합니다. 이전 콜백이 실행되는 동안 인터럽트가 발생하면, 콜백의 추가 인스턴스가 실행을 위해 큐에 들어가며, 이는 현재 인스턴스가 완료된 후에 실행됩니다. 따라서 지속적으로 높은 인터럽트 반복 속도는 무제한 큐 증가와 결국 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()의 실행 사이에 가변적인 양의 지연 시간이 있을 것입니다. 이는 협력형 스케줄링에 내재된 것입니다. 최대 지연 시간은 애플리케이션과 플랫폼에 따라 다르지만 일반적으로 수십 ms 단위로 측정될 수 있습니다.
일반적인 문제¶
이것은 실시간 프로그래밍 주제에 대한 간략한 소개에 불과합니다. 초보자는 실시간 프로그램의 설계 오류가 진단하기 특히 어려운 결함으로 이어질 수 있다는 점에 유의해야 합니다. 이는 그러한 결함이 드물게, 본질적으로 무작위적인 간격으로 발생할 수 있기 때문입니다. 초기 설계를 올바르게 하고 문제가 발생하기 전에 예상하는 것이 중요합니다. 인터럽트 핸들러와 메인 프로그램 모두 다음 문제들을 이해하여 설계해야 합니다.
인터럽트 핸들러 설계¶
위에서 언급했듯이 ISR은 가능한 한 단순하게 설계되어야 합니다. ISR은 항상 짧고 예측 가능한 시간 내에 반환해야 합니다. 이는 ISR이 실행되는 동안 메인 루프가 실행되지 않기 때문에 중요합니다. 메인 루프는 코드의 무작위 지점에서 불가피하게 실행 중단을 겪습니다. 이러한 중단은 특히 그 지속 시간이 길거나 가변적일 경우 진단하기 어려운 버그의 원인이 될 수 있습니다. ISR 실행 시간의 함의를 이해하려면 인터럽트 우선순위에 대한 기본적인 이해가 필요합니다.
인터럽트는 우선순위 체계에 따라 구성됩니다. ISR 코드 자체가 더 높은 우선순위의 인터럽트에 의해 중단될 수 있습니다. 이는 두 인터럽트가 데이터를 공유하는 경우 함의가 있습니다(아래 임계 구역 참조). 그러한 인터럽트가 발생하면 ISR 코드에 지연이 끼어듭니다. ISR이 실행되는 동안 더 낮은 우선순위의 인터럽트가 발생하면, 그것은 ISR이 완료될 때까지 지연됩니다. 지연이 너무 길면 더 낮은 우선순위의 인터럽트가 실패할 수 있습니다. 느린 ISR의 또 다른 문제는 그 실행 중에 동일한 유형의 두 번째 인터럽트가 발생하는 경우입니다. 두 번째 인터럽트는 첫 번째 인터럽트가 종료될 때 처리됩니다. 그러나 들어오는 인터럽트의 속도가 ISR이 처리할 수 있는 용량을 지속적으로 초과하면 결과는 좋지 않을 것입니다.
따라서 반복 구조는 피하거나 최소화해야 합니다. 인터럽트를 유발하는 장치 이외의 장치에 대한 I/O는 일반적으로 피해야 합니다. 디스크 접근, print 문, UART 접근과 같은 I/O는 비교적 느리며 그 지속 시간이 가변적일 수 있습니다. 여기서 또 다른 문제는 파일시스템 함수가 재진입 불가능하다는 점입니다. ISR과 메인 프로그램에서 파일시스템 I/O를 사용하는 것은 위험합니다. 무엇보다 ISR 코드는 이벤트를 기다려서는 안 됩니다. 코드가 예측 가능한 시간 내에 반환되는 것이 보장될 수 있다면, 예를 들어 핀이나 LED를 토글하는 경우라면 I/O가 허용됩니다. I2C 또는 SPI를 통해 인터럽트 장치에 접근하는 것이 필요할 수 있지만, 그러한 접근에 걸리는 시간은 계산하거나 측정해야 하며 애플리케이션에 미치는 영향을 평가해야 합니다.
일반적으로 ISR과 메인 루프 간에 데이터를 공유할 필요가 있습니다. 이는 전역 변수를 통하거나 클래스 또는 인스턴스 변수를 통해 수행될 수 있습니다. 변수는 일반적으로 정수 또는 불리언 타입이거나, 정수 또는 바이트 배열(미리 할당된 정수 배열은 리스트보다 빠른 접근을 제공함)입니다. ISR이 여러 값을 수정하는 경우, 메인 프로그램이 일부 값에는 접근했지만 전부에는 접근하지 않은 시점에 인터럽트가 발생하는 경우를 고려해야 합니다. 이는 불일치로 이어질 수 있습니다.
다음 설계를 고려해 봅시다. ISR은 들어오는 데이터를 bytearray에 저장한 다음, 처리할 준비가 된 총 바이트 수를 나타내는 정수에 수신한 바이트 수를 더합니다. 메인 프로그램은 바이트 수를 읽고 바이트를 처리한 다음 처리 준비된 바이트 수를 0으로 지웁니다. 이는 메인 프로그램이 바이트 수를 읽은 직후에 인터럽트가 발생하기 전까지는 동작합니다. ISR은 추가된 데이터를 버퍼에 넣고 수신된 수를 갱신하지만, 메인 프로그램은 이미 그 수를 읽었으므로 원래 수신된 데이터만 처리합니다. 새로 도착한 바이트는 손실됩니다.
이 위험을 피하는 다양한 방법이 있으며, 가장 간단한 것은 원형 버퍼를 사용하는 것입니다. 내재된 스레드 안전성을 가진 구조를 사용할 수 없는 경우의 다른 방법은 아래에 설명되어 있습니다.
재진입성¶
함수나 메서드가 메인 프로그램과 하나 이상의 ISR 간에 또는 여러 ISR 간에 공유되면 잠재적 위험이 발생할 수 있습니다. 여기서의 문제는 함수 자체가 중단되고 그 함수의 추가 인스턴스가 실행될 수 있다는 것입니다. 이런 일이 발생하려면 함수가 재진입 가능하도록 설계되어야 합니다. 이를 수행하는 방법은 이 튜토리얼의 범위를 벗어나는 고급 주제입니다.
임계 구역¶
임계 코드 구역의 한 예는 ISR에 의해 영향을 받을 수 있는 둘 이상의 변수에 접근하는 코드입니다. 개별 변수에 대한 접근 사이에 인터럽트가 발생하면 그 값들이 불일치하게 됩니다. 이는 경쟁 조건(race condition)이라고 알려진 위험의 한 사례입니다. 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 줄은 읽기-수정-쓰기(read-modify-write)로 알려진 특정한 경쟁 조건 위험을 수반합니다. 이는 실시간 시스템에서 버그의 고전적인 원인입니다. 메인 루프에서 MicroPython은 count의 값을 읽고, 거기에 1을 더한 다음, 다시 씁니다. 드물게 읽기 후 쓰기 전에 인터럽트가 발생합니다. 인터럽트는 count를 수정하지만 그 변경은 ISR이 반환할 때 메인 루프에 의해 덮어쓰여집니다. 실제 시스템에서 이는 드물고 예측 불가능한 실패로 이어질 수 있습니다.
위에서 언급했듯이, Python 내장 타입의 인스턴스가 메인 코드에서 수정되고 그 인스턴스가 ISR에서 접근되는 경우 주의를 기울여야 합니다. 수정을 수행하는 코드는 ISR이 실행될 때 인스턴스가 유효한 상태에 있도록 보장하기 위해 임계 구역으로 간주되어야 합니다.
데이터셋이 서로 다른 ISR 간에 공유되는 경우 특별한 주의가 필요합니다. 여기서의 위험은 더 낮은 우선순위의 인터럽트가 공유 데이터를 부분적으로 갱신했을 때 더 높은 우선순위의 인터럽트가 발생할 수 있다는 것입니다. 이 상황을 다루는 것은 이 소개의 범위를 벗어나는 고급 주제이지만, 아래에 설명된 뮤텍스(mutex) 객체가 때때로 사용될 수 있다는 점은 언급해 둡니다.
임계 구역의 지속 시간 동안 인터럽트를 비활성화하는 것이 일반적이고 가장 간단한 방법이지만, 이는 문제를 일으킬 가능성이 있는 인터럽트 하나만이 아니라 모든 인터럽트를 비활성화합니다. 일반적으로 인터럽트를 오래 비활성화하는 것은 바람직하지 않습니다. 타이머 인터럽트의 경우 콜백이 발생하는 시점에 변동성을 도입합니다. 장치 인터럽트의 경우 장치가 너무 늦게 처리되어 장치 하드웨어에서 데이터 손실이나 오버런 오류가 발생할 수 있습니다. ISR과 마찬가지로 메인 코드의 임계 구역도 짧고 예측 가능한 지속 시간을 가져야 합니다.
인터럽트가 비활성화되는 시간을 획기적으로 줄이면서 임계 구역을 다루는 접근 방식은 뮤텍스(mutex, 상호 배제 개념에서 유래한 이름)라고 불리는 객체를 사용하는 것입니다. 메인 프로그램은 임계 구역을 실행하기 전에 뮤텍스를 잠그고 끝에서 잠금을 해제합니다. ISR은 뮤텍스가 잠겨 있는지 검사합니다. 잠겨 있으면 임계 구역을 피하고 반환합니다. 설계상의 과제는 임계 변수에 대한 접근이 거부되는 경우 ISR이 무엇을 해야 하는지 정의하는 것입니다. 뮤텍스의 간단한 예는 여기에서 찾을 수 있습니다. 뮤텍스 코드가 인터럽트를 비활성화하기는 하지만 단 여덟 개의 기계어 명령어 지속 시간 동안만 그렇게 한다는 점에 유의하세요. 이 접근 방식의 이점은 다른 인터럽트가 사실상 영향을 받지 않는다는 것입니다.
인터럽트와 REPL¶
타이머와 연관된 것과 같은 인터럽트 핸들러는 프로그램이 종료된 후에도 계속 실행될 수 있습니다. 이는 콜백을 발생시키는 객체가 범위를 벗어났을 것으로 예상했던 곳에서 예기치 않은 결과를 낳을 수 있습니다. 예를 들어 OpenMV Cam에서:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
이것은 타이머가 명시적으로 비활성화되거나 보드가 Ctrl-D로 재설정될 때까지 계속 실행됩니다.