본문 바로가기

ESP32

ESP32 Arduino: 타이머 인터럽트

반응형

 

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 – 타이머 인터럽트 프로그램의 출력. 

 

위 포스팅 참고자료는 이 링크를 따라가시면 만날 수 있습니다. 배움을 멈추지 마세요.

 

 

반응형

캐어랩 고객 지원

취업, 창업의 막막함, 외주 관리, 제품 부재!

당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약, 아이디어는 있지만 구현할 기술이 없는 막막함.

우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.

이제 고민을 멈추고, 캐어랩을 만나세요!

코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.

제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!

귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.

지난 30년 여정, 캐어랩이 얻은 모든 것을 함께 나누고 싶습니다.

카카오 채널 추가하기

카톡 채팅방에서 무엇이든 물어보세요

당신의 성공을 위해 캐어랩과 함께 하세요.

캐어랩 온라인 채널 바로가기

캐어랩