소개
이 게시물의 목적은 Arduino 코어를 사용하여
ESP32에서 타이머 인터럽트를 구성하는 방법을 설명하는 것입니다.
여기에 표시된 코드는 Arduino 코어 라이브러리의 예제 를 기반으로 하였습니다.
따라서 이 자습서에서는 주기적으로 인터럽트를 생성하도록
타이머를 구성하고 처리하는 방법을 확인합니다.
테스트는 ESP32 FireBeetle 보드에 통합된 DFRobot의 ESP-WROOM-32 장치에서 수행되었습니다 .
The alarms
ESP32에는 두 개의 타이머 그룹이 있으며 각 그룹에는 두 개의 범용 하드웨어 타이머가 있습니다.
모든 타이머는 64비트 카운터와 16비트 prescalers 기반으로 합니다 [1].
prescaler 는 기본 신호의 주파수(보통 80MHz)를 나누는 데 사용되며,
이 주파수는 타이머 카운터를 증가/감소시키는 데 사용됩니다[2].
프리스케일러는 16비트이기 때문에
클럭 신호 주파수를 2에서 65536까지 나눌 수 있어 [2] 많은 구성이 가능합니다.
타이머 카운터는 카운트 업 또는 다운하도록 구성할 수 있으며
자동 재로드 및 소프트웨어 재로드를 지원합니다[2].
또한 소프트웨어[2]에서 정의한 특정 값에 도달하면 알람을 생성할 수도 있습니다.
카운터 값은 소프트웨어 프로그램[2]에서 읽을 수 있습니다.
Global variables 전역 변수
일부 전역 변수를 선언하여 코드를 시작합니다.
첫 번째는 인터럽트가 발생했음을 메인 루프에 알리기 위해
인터럽트 서비스 루틴에서 사용할 카운터입니다.
설명된 대로 ISR은 가능한 한 빨리 실행되어야 하고 직렬 포트에 쓰기와 같은
긴 작업을 수행하지 않아야 하기 때문에 외부 인터럽트 에 대한
이전 자습서 에서 카운터를 사용하는 것을 보았습니다 .
따라서 인터럽트 처리 코드를 구현하는 좋은 방법은
ISR이 인터럽트 발생에 대해서만 신호를 보내고
메인 루프에 대한 실제 처리(시간이 걸리는 작업이 포함될 수 있음)를 연기하는 것입니다.
카운터는 어떤 이유로 메인 루프에서 인터럽트 처리가 예상보다 오래 걸리고
그 동안 더 많은 인터럽트가 발생하는 경우 카운터가 그에 따라 증가하기 때문에
손실되지 않기 때문에 유용합니다.
반면에 플래그가 신호 메커니즘으로 사용되는 경우 계속 true로 설정되고
메인 루프가 추가 이벤트가 발생했다고 가정하기 때문에 인터럽트가 손실됩니다.
평소와 같이 이 카운터 변수는
메인 루프와 ISR 간에 공유 되므로 컴파일러 최적화로 인해 제거되지 않도록
volatile 키워드 로 선언해야 합니다 .
volatile int interruptCounter;
프로그램 시작 이후 이미 발생한 인터럽트 수를 추적하는 추가 카운터가 있습니다.
이것은 메인 루프에서만 사용되므로 volatile 으로 선언할 필요가 없습니다.
int totalInterruptCounter;
타이머를 구성하려면 나중에 Arduino 설정 기능에서 사용할 hw_timer_t 유형의 변수에 대한 포인터가 필요 합니다.
hw_timer_t * timer = NULL;
마지막으로 공유 변수를 수정할 때 메인 루프와 ISR 간의 동기화를 처리하는 데 사용할
portMUX_TYPE 유형의 변수를 선언해야 합니다.
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
Setup function
평소와 같이 직렬 연결을 열어 setup 함수를 시작합니다. 나중에 Arduino IDE 직렬 모니터에서
프로그램 결과를 출력할 수 있습니다.
Serial.begin(115200);
다음으로,
이전 섹션에서 선언한 타이머 전역 변수 중 하나인
hw_timer_t 유형의 구조에 대한 포인터를 반환하는
timerBegin 함수를 호출하여 타이머를 초기화 합니다.
입력으로 이 함수는 우리가 사용하려는 타이머의 번호(4개의 하드웨어 타이머, 0에서 3까지),
프리스케일러의 값 및 카운터가 카운트 업(true) 또는 다운( 거짓)인지 가리키는 플래그를
반환 받습니다. 이 예에서는 첫 번째 타이머를 사용하고
마지막 매개변수에 true 를 전달 하므로 카운터가 카운트업됩니다.
프리스케일러는 ESP32 카운터에 의해 사용되는 기준 신호의 주파수로
80메가 헤르츠 입니다(이것은 FireBeetle 보드 마찬가지이다). 이 값은 80,000,000Hz와 같으며,
이는 신호가 타이머 카운터를 초당 80,000,000번 증가 시킨다는 것을 의미합니다 .
인터럽트 생성을 위한 카운터 번호를 설정하기 위해 이 값으로 계산을 수행할 수 있지만
단순화하기 위해 프리스케일러를 활용할 것입니다.
따라서 이 값을 80으로 나누면 (80을 프리스케일러 값으로 사용)
타이머 카운터를 초당 1,000,000번 증가시키는 1MHz 주파수의 신호를 얻게 됩니다 .
이전 값에서 반전하면 카운터가 마이크로초마다 증가한다는 것을 알 수 있습니다 .
따라서 80 의 프리스케일러를 사용하여 인터럽트를 생성하기 위한
카운터 값을 설정하는 함수를 호출할 때 해당 값을 마이크로초 단위로 지정합니다.
timer = timerBegin(0, 80, true);
그러나 타이머를 활성화하기 전에
인터럽트가 생성될 때 실행될 핸들링 함수에 타이머를 바인딩해야 합니다.
이것은 timerAttachInterrupt 함수에 대한 호출로 수행됩니다 .
이 함수는 전역 변수에 저장한 초기화된 타이머에 대한 포인터,
인터럽트를 처리할 함수의 주소 및 생성될 인터럽트가 edge(true)인지 level(false)인지 나타내는
플래그를 입력으로 받습니다.
여기 에서 edge 인터럽트와 level 인터럽트의 차이점에 대해 자세히 알아볼 수 있습니다 .
따라서 언급한대로
전역 타이머 변수를 첫 번째 입력으로,
두 번째로 나중에 지정할 onTimer 라는 함수의 주소 ,
세 번째로 true 값을 전달 하므로 생성된 인터럽트는 edge 유형입니다.
timerAttachInterrupt(timer, &onTimer, true);
다음으로 timerAlarmWrite 함수를 사용하여
타이머 인터럽트가 생성될 카운터 값을 지정합니다.
따라서 이 함수는 타이머에 대한 포인터를 첫 번째 입력으로 수신하고,
두 번째로 인터럽트가 생성되어야 하는 카운터 값을,
세 번째로 타이머가 인터럽트 생성시 자동으로 다시 로드해야 하는지 여부를 나타내는
플래그를 수신합니다.
따라서 첫 번째 인수로 타이머 전역 변수를 다시 전달하고 세 번째 인수로 true를 전달 하므로
카운터가 다시 로드되어 인터럽트가 주기적으로 생성됩니다.
두 번째 인수와 관련하여 인터럽트가 발생해야 하는 마이크로초 수를 의미하도록
프리스케일러를 설정했음을 기억하십시오. 따라서 이 예에서는 1초마다 인터럽트를 생성하고
1 초에 해당하는 1 000 000 마이크로초 값을 전달한다고 가정합니다 .
중요 : 이 값은 프리스케일러에 대해 값 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);
}
The main loop
앞에서 언급했듯이 메인 루프는
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를 반환하고 인수를 받지 않는 함수여야 합니다.
우리의 기능은 메인 루프에 인터럽트가 발생했다는 신호를 보내는
인터럽트 카운터를 증가시키는 것만큼 간단할 것입니다.
이것은 우리가 일찍 선언한
portMUX_TYPE 전역 변수의 주소를 입력 매개변수로 수신하는 portENTER_CRITICAL_ISR 및 portEXIT_CRITICAL_ISR 매크로로 선언된 임계 섹션 내에서 수행됩니다.
업데이트: 컴파일러가 코드를 IRAM에 배치하려면
인터럽트 처리 루틴에 IRAM_ATTR 속성이 있어야 합니다.
또한, 인터럽트 처리 루틴은, 또한 IRAM에 배치 함수를 호출해야 합니다.
여기 IDF의 설명서에 볼 수 있듯이. 이 점에 대해 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 - 타이머 인터럽트 프로그램의 출력.