ESP32 타이머 인터럽트
이 게시물의 목적은 Arduino 코어를 사용하여 ESP32에서 타이머 인터럽트를 구성하는 방법을 설명하는 것입니다. 테스트는 ESP32 FireBeetle 보드에 통합된 DFRobot의 ESP-WROOM-32 장치에서 수행되었습니다.
소개
이 게시물의 목적은 Arduino 코어를 사용하여 ESP32에서 타이머 인터럽트를 구성하는 방법을 설명하는 것입니다. 여기에 표시된 코드는 Arduino 코어 라이브러리의 이 예제를 기반으로 하며, 시도해 보시기를 권장합니다.
따라서 이 튜토리얼에서는 타이머를 구성하여 주기적으로 인터럽트를 생성하는 방법과 이를 처리하는 방법을 살펴보겠습니다.
테스트는 ESP32 FireBeetle 보드에 통합된 DFRobot의 ESP-WROOM-32 장치에서 수행되었습니다.
알람
ESP32에는 두 개의 타이머 그룹이 있으며, 각각 두 개의 범용 하드웨어 타이머가 있습니다. 모든 타이머는 64비트 카운터와 16비트 프리스케일러를 기반으로 합니다[1].
프리스케일러는 기본 신호(일반적으로 80MHz)의 주파수를 나누는 데 사용되며, 이는 타이머 카운터를 증가/감소시키는 데 사용됩니다[2]. 프리스케일러는 16비트이므로 클록 신호 주파수를 2에서 65536까지의 계수로 나눌 수 있습니다[2]. 이는 많은 구성 자유도를 제공합니다.
타이머 카운터는 카운트업 또는 카운트다운을 구성하고 자동 리로드 및 소프트웨어 리로드를 지원할 수 있습니다[2]. 또한 소프트웨어에서 정의한 특정 값에 도달하면 알람을 생성할 수도 있습니다[2]. 카운터의 값은 소프트웨어 프로그램에서 읽을 수 있습니다[2].
전역 변수
코드를 시작하기 위해 일부 전역 변수를 선언합니다. 첫 번째 변수는 인터럽트 서비스 루틴에서 인터럽트가 발생했음을 메인 루프에 알리는 데 사용되는 카운터입니다.
이전 외부 인터럽트 튜토리얼에서 카운터를 사용하는 방법을 살펴보았습니다. 설명한 대로 ISR은 가능한 한 빨리 실행되어야 하며 직렬 포트에 쓰는 것과 같은 긴 작업을 수행해서는 안 됩니다. 따라서 인터럽트 처리 코드를 구현하는 좋은 방법은 ISR이 인터럽트 발생만 신호로 보내고 실제 처리(시간이 걸리는 작업이 포함될 수 있음)를 메인 루프로 미루는 것입니다.
카운터는 메인 루프에서 인터럽트를 처리하는 데 어떤 이유에서인지 예상보다 오래 걸리고 그 사이에 더 많은 인터럽트가 발생하더라도 카운터가 그에 따라 증가하기 때문에 손실되지 않기 때문에 유용합니다. 반면 플래그를 신호 메커니즘으로 사용하면 true로 계속 설정되고 메인 루프는 추가 인터럽트가 발생했다고 가정하기 때문에 인터럽트가 손실됩니다.
평소와 같이 이 카운터 변수는 메인 루프와 ISR에서 공유되므로 컴파일러 최적화로 인해 제거되는 것을 방지하기 위해 volatile 키워드로 선언해야 합니다.
volatile int interruptCounter;
프로그램 시작 이후 발생한 인터럽트 수를 추적하는 추가 카운터가 있습니다. 이 카운터는 메인 루프에서만 사용되므로 volatile로 선언할 필요가 없습니다.
int totalInterruptCounter;
타이머를 구성하려면 hw_timer_t 유형의 변수에 대한 포인터가 필요합니다. 이 포인터는 나중에 Arduino 설정 함수에서 사용할 것입니다.
hw_timer_t * timer = NULL;
마지막으로 공유 변수를 수정할 때 메인 루프와 ISR 간의 동기화를 처리하는 데 사용할 portMUX_TYPE 유형의 변수를 선언해야 합니다.
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
설정 함수
평소처럼 설정 함수를 시작하려면 직렬 연결을 열어 나중에 Arduino IDE 직렬 모니터에서 사용할 수 있는 프로그램 결과를 출력할 수 있습니다.
Serial.begin(115200);
다음으로, timerBegin 함수를 호출하여 타이머를 초기화합니다. 이 함수는 이전 섹션에서 선언한 타이머 전역 변수 중 하나인 hw_timer_t 유형의 구조체에 대한 포인터를 반환합니다.
이 함수는 입력으로 사용하려는 타이머의 번호(하드웨어 타이머가 4개이므로 0~3), 프리스케일러의 값, 카운터가 카운트업(참) 또는 카운트다운(거짓)해야 하는지를 나타내는 플래그를 받습니다.
이 예제에서는 첫 번째 타이머를 사용하고 마지막 매개변수에 참을 전달하여 카운터가 카운트업되도록 합니다.
프리스케일러와 관련하여 소개 섹션에서 ESP32 카운터에서 일반적으로 사용하는 기본 신호의 주파수는 80MHz라고 말했습니다(FireBeetle 보드의 경우 해당). 이 값은 80,000,000Hz와 같으며, 이는 신호가 타이머 카운터를 초당 80,000,000회 증가시킨다는 것을 의미합니다.
이 값으로 인터럽트를 생성하기 위한 카운터 번호를 설정하기 위해 계산을 할 수 있지만, 이를 단순화하기 위해 프리스케일러를 활용할 것입니다. 따라서 이 값을 80으로 나누면(프리스케일러 값으로 80 사용) 타이머 카운터를 초당 1,000,000회 증가시키는 1MHz 주파수의 신호를 얻게 됩니다.
이전 값에서 반전하면 카운터가 마이크로초마다 증가한다는 것을 알 수 있습니다. 따라서 프리스케일러를 80으로 사용하여 인터럽트를 생성하기 위한 카운터 값을 설정하는 함수를 호출할 때 해당 값을 마이크로초 단위로 지정하게 됩니다.
timer = timerBegin(0, 80, true);
하지만 타이머를 활성화하기 전에 인터럽트가 생성될 때 실행되는 처리 함수에 바인딩해야 합니다. 이는 timerAttachInterrupt 함수를 호출하여 수행됩니다.
이 함수는 초기화된 타이머에 대한 포인터를 입력으로 받습니다. 이 포인터는 글로벌 변수에 저장되어 있고, 인터럽트를 처리할 함수의 주소와 생성할 인터럽트가 에지(참)인지 레벨(거짓)인지를 나타내는 플래그입니다. 에지 인터럽트와 레벨 인터럽트의 차이점에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
따라서 언급했듯이 글로벌 타이머 변수를 첫 번째 입력으로 전달하고, 두 번째로 나중에 지정할 onTimer라는 함수의 주소를 전달하고, 세 번째로 true 값을 전달하여 생성된 인터럽트가 에지 유형이 되도록 합니다.
timerAttachInterrupt(timer, &onTimer, true);
다음으로 timerAlarmWrite 함수를 사용하여 타이머 인터럽트가 생성될 카운터 값을 지정합니다. 따라서 이 함수는 첫 번째 입력으로 타이머에 대한 포인터를 받고, 두 번째로 인터럽트가 생성되어야 하는 카운터 값을 받고, 세 번째로 인터럽트를 생성할 때 타이머가 자동으로 다시 로드되어야 하는지 여부를 나타내는 플래그를 받습니다.
따라서 첫 번째 인수로 타이머 전역 변수를 다시 전달하고 세 번째 인수로 true를 전달하여 카운터가 다시 로드되고 인터럽트가 주기적으로 생성됩니다.
두 번째 인수와 관련하여 인터럽트가 발생해야 하는 마이크로초 수를 의미하도록 프리스케일러를 설정한다는 점을 기억하세요. 따라서 이 예에서는 매 초마다 인터럽트를 생성하고자 한다고 가정하고 1,000,000마이크로초 값을 전달하는데, 이는 1초와 같습니다.
중요: 이 값은 프리스케일러에 80 값을 지정한 경우에만 마이크로초 단위로 지정됩니다. 다른 프리스케일러 값을 사용할 수 있으며, 이 경우 카운터가 특정 값에 도달할 시기를 알기 위해 계산을 수행해야 합니다.
timerAlarmWrite(timer, 1000000, true);
타이머 변수를 입력으로 전달하여 timerAlarmEnable 함수를 호출하여 타이머를 활성화하여 설정 함수를 마칩니다.
timerAlarmEnable(timer);
setup 함수의 최종 코드는 아래에서 볼 수 있습니다.
void setup() {
Serial.begin(115200);
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000000, true);
timerAlarmEnable(timer);
}
메인 루프
앞서 말했듯이 메인 루프는 ISR에서 신호를 보낸 후 실제로 타이머 인터럽트를 처리하는 곳입니다. 단순화를 위해 폴링을 사용하여 인터럽트 카운터의 값을 확인하지만 자연스럽게 훨씬 더 효율적인 방법은 세마포어를 사용하여 메인 루프를 잠그고 ISR에서 잠금을 해제하는 것입니다. 이는 원래 예제에서 사용된 방법입니다.
따라서 interruptCounter 변수가 0보다 큰지 확인하고 큰 경우 인터럽트 처리 코드로 들어갑니다. 거기서 가장 먼저 할 일은 이 카운터를 감소시켜 인터럽트가 확인되었고 처리될 것임을 알리는 것입니다.
이 변수는 ISR과 공유되므로 portENTER_CRITICAL 및 portEXIT_CRITICAL 매크로를 사용하여 지정하는 임계 섹션 내에서 이를 수행합니다. 이 두 호출 모두 인수로 글로벌 portMUX_TYPE 변수의 주소를 받습니다.
if (interruptCounter > 0) {
portENTER_CRITICAL(&timerMux);
interruptCounter--;
portEXIT_CRITICAL(&timerMux);
// Interrupt handling code
}
실제 인터럽트 처리란 단순히 프로그램 시작 이후 발생한 총 인터럽트 수로 카운터를 증가시키고 직렬 포트에 인쇄하는 것입니다. 이 호출이 이미 포함된 전체 메인 루프 코드를 아래에서 확인할 수 있습니다.
void loop() {
if (interruptCounter > 0) {
portENTER_CRITICAL(&timerMux);
interruptCounter--;
portEXIT_CRITICAL(&timerMux);
totalInterruptCounter++;
Serial.print("An interrupt as occurred. Total number: ");
Serial.println(totalInterruptCounter);
}
}
ISR 코드
인터럽트 서비스 루틴은 void를 반환하고 인수를 받지 않는 함수여야 합니다.
함수는 인터럽트가 발생했음을 메인 루프에 알리는 인터럽트 카운터를 증가시키는 것만큼 간단합니다. 이는 portENTER_CRITICAL_ISR 및 portEXIT_CRITICAL_ISR 매크로로 선언된 임계 섹션 내에서 수행되며, 둘 다 입력 매개변수로 앞서 선언한 portMUX_TYPE 전역 변수의 주소를 받습니다.
업데이트: 인터럽트 처리 루틴에는 컴파일러가 코드를 IRAM에 배치할 수 있도록 IRAM_ATTR 속성이 있어야 합니다. 또한 인터럽트 처리 루틴은 IDF 설명서에서 볼 수 있듯이 IRAM에 배치된 함수만 호출해야 합니다. 이 점을 지적해 주신 Manuato에게 감사드립니다.
이 함수의 전체 코드는 아래에서 볼 수 있습니다.
void IRAM_ATTR onTimer() {
portENTER_CRITICAL_ISR(&timerMux);
interruptCounter++;
portEXIT_CRITICAL_ISR(&timerMux);
}
최종 코드
주기적 타이머 인터럽트 프로그램의 최종 소스 코드는 아래에서 볼 수 있습니다.
volatile int interruptCounter;
int totalInterruptCounter;
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer() {
portENTER_CRITICAL_ISR(&timerMux);
interruptCounter++;
portEXIT_CRITICAL_ISR(&timerMux);
}
void setup() {
Serial.begin(115200);
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000000, true);
timerAlarmEnable(timer);
}
void loop() {
if (interruptCounter > 0) {
portENTER_CRITICAL(&timerMux);
interruptCounter--;
portEXIT_CRITICAL(&timerMux);
totalInterruptCounter++;
Serial.print("An interrupt as occurred. Total number: ");
Serial.println(totalInterruptCounter);
}
}
코드 테스트
코드를 테스트하려면 ESP32 보드에 업로드하고 Arduino IDE 직렬 모니터를 엽니다. 그림 1과 비슷한 출력이 표시되어야 하며, 여기서 메시지는 1초 주기로 인쇄되어야 합니다.
그림 1 – 타이머 인터럽트 프로그램의 출력.
위 포스팅 참고자료는 이 링크를 따라가시면 만날 수 있습니다. 배움을 멈추지 마세요.
'ESP32' 카테고리의 다른 글
esp32 AT command 사용법 정리 (1) | 2024.10.28 |
---|---|
Arduino IDE로 ESP32를 프로그래밍하는 방법 (7) | 2024.10.28 |
MQTT란 무엇이며 어떻게 작동하는가 (8) | 2024.10.26 |
ESP32 실시간 클록 모듈(DS1302) RTC 모듈 (8) | 2024.10.24 |
ESP32 타이머 및 타이머 인터럽트 (4) | 2024.10.24 |
ESP32: Bluetooth를 사용하여 WiFi 연결 설정 (6) | 2024.10.23 |
Android와 함께하는 ESP32 블루투스 (2) | 2024.10.23 |
ESP32 터치 버튼 시작하기 [코드 및 배선 다이어그램 포함] (11) | 2024.10.22 |
더욱 좋은 정보를 제공하겠습니다.~ ^^