Hardware | ESP32 의 Dual core 확인해 보기

|

ESP32 대한 지금까지 글은 아래 포스팅들을 참고해 주세요.


* Hardware | ESP32 스펙 확인해 보기
    - https://chocoball.tistory.com/entry/Hardware-ESP32-spec-check

* Hardware | ESP32 간단 사용기
    - https://chocoball.tistory.com/entry/Hardware-simple-review-ESP32




1. Dual core 활용에 앞서


ESP32 의 최장점 중 하나인, dual core 활용에 앞서 개념적인 이야기들이 조금 필요하다고 생각되었다.



그 이유는, 제가 dual core 활용을 이해하고 실행해 보기까지 삽질한 흔적들을 공유하기 위해서 이다.

아직 100% 맞는 이야기 인지는 확신이 들지 않지만, 대략 문제는 없어 보이니, 서두를 조금 길게 하겠다.




2. Arduino IDE 와 ESP-IDF


우선 ESP32 에 프로그램을 올리기 위해서는 coding 하는 환경이 필요한데, 이 개발환경이 ESP-IDF 이다.

보통 ESP-IDF framework 라 부른다.



다만, 우리가 친숙한 Arduino IDE 환경에서도 개발할 수 있도록, Arduino IDE 의 Arduino Core 가 ESP-IDF 를 포함하고 있다.

말하자면, ESP-IDF 의 간편한 interface 인 셈이다.


참고로, Arduino IDE 에서 컴파일한 결과물 보다, ESP-IDF 환경에서 컴파일한 결과물이 대략 65% 정도의 더 빠르다고 하니, ESP32 개발은 가능하면, ESP-IDF 프레임워크에서 개발하는 것이 좋다 하겠다.


살짝 다른 이야기 이지만, 위의 글에서도 설명 되어 있 듯, ESP32 세계에서 이야기 하는 firmware 는, 흔히 OS 기저에 존재하는 HW/OS 사이를 interfacing 해주는 미들웨어가 아니라, 구동 프로그램 자체를 이야기 한다. 헷갈리지 말자.


이 용어의 혼란 때문에, ESP32 의 firmware 를 업데이트 하고자 답도 없는 웹페이지들을 읽어댓다.




3. ESP-IDF FreeRTOS


그럼 자주 등장하는 FreeRTOS 는 도대체 뭔가?


* Quality RTOS & Embedded Software
    - https://www.freertos.org/a00106.html


이름의 약자 부분이 "RTOS - Real Time OS" 인 실시간 처리를 위한 OS이고, 그 중에 가장 유명한 open source 인 RTOS 를 뜻한다.

결국, 아래 발췌한 문구에서 볼 수 있 듯, ESP-IDF framework 는 이 FreeRTOS 실시간 운영 체제를 기반으로 하고 있다.


The ESP-IDF FreeRTOS is a modified version of vanilla FreeRTOS which supports symmetric multiprocessing (SMP).

이 FreeRTOS 덕에, 이 글의 목적인 dual core 를 활용할 수 있게 된 것이다.

처음엔 ESP32Arduino + WiFi 정도로 생각하고 덤볐는데, 완전 deep 하게 들어온 듯 하다.

상하 관계를 정리하면, FreeRTOS + Components > ESP-IDF > Arduino Core > Arduino IDE 되겠다.



AWS 에서도 MQTT 를 위해 FreeRTOS 서비스를 지원하고 있지만, 아래 그림에서 보면 살짤 달라 보인다.

ESP32 에서는 ESP-IDF 하위 layer 에 FreeRTOS 가 위치하지만, AWS FreeRTOS 는 ESP-IDF 상위에 자리하고 있다.





4. SMP


ESP32 에서 dual core 는 CPU 0 - Protocol CPU (PRO_CPU) CPU 1 - Application CPU (APP_CPU)두 가지로 정의되어 사용된다.

보통, PRO_CPU 는 백그라운드 작업이나 WiFi 인터페이싱 쪽을, APP_CPU 는 어플리케이션 쪽에 우선 순위를 둬서 동작하게 된다.

그러다 보니, APP_CPU 는 항상 바쁘고, PRO_CPU 는 많은 시간 놀게 되는, 좋지 않은 효율을 보일 수 있다.


Arduino IDE 에서 코딩하게 되면, setup() / loop() 는 CPU 1 에서 실행되고, 무선관련 동작은 CPU 0 에서 실행된다고 한다.



효율이 좋지 않다고 하더라도, 엄연한 dual core architecture 이며, memory 와 cache 및 외부기기 접근을 공유한다.



Core 이름에서도 알 수 있듯, 통신에 관한 background 처리는 CPU 0 에서 처리하고, 어플리케이션은 CPU 1 에서 하려고 하기 때문에, 효율 좋게 동작할 수 있도록 아래와 같이 CPU core 지정 명령어를 구비해 놓았습니다.


|------------------------------------------------------------------|
| instruction               | meaning                              |
|------------------------------------------------------------------|
| xTaskCreate()             | for single core tasks                |
| xTaskCreatePinnedToCore() | for assigning tasks to specific core |
| xTaskCreateUniversal()    | for making cores used evenly         |
|------------------------------------------------------------------|


Single core 에는 "xTaskCreate()" 를 사용하고, 특정 core 에 할당하고 싶을 때에는 "xTaskCreatePinnedToCore()" 을 사용합니다.

Core 수를 확인하여, core 갯수에 알맞게 task 를 할당하는 것이 "xTaskCreateUniversal()" 입니다.


우리는 core 를 편중되지 않고, 조금 강제적으로 균등하게 사용하기 위해 "xTaskCreatePinnedToCore()" 을 사용할 껍니다.

참고로, 함수의 첫번째 부분에 표시되는 "x..." 는 FreeRTOS 네이티브 명령어를 지정하는 것이라 하네요.




5. Source 기본 구조


기존 Arduino 처럼 코딩하는 것이 아닌, Dual core 만의 규칙이 있습니다. 사실 FreeRTOS 의 규칙이겠죠.

void task1(void *pvParameters) {
	while (1) {
		...
		...
		delay(10);
	}
}

void task2(void *pvParameters) {
	while (1) {
		...
		...
		delay(10);
	}
}


우선 각 core 에 시킬 일들을 미리 정의해 놓습니다. Function 정의와 비슷합니다.

다만, "while(1)" 구문 안에 작업 루틴이 들어가고, 필히 "delay(10)" 가 필요합니다.

Delay 는 10 microseconds 미만이면, 균등하게 core 끼리 일을 나눠 갖지 못합니다. 꼭 10 microseconds 이상으로 설정해야 합니다.


참고로 Arduino IDE 에서 제공하는 "delay(10)" 함수를 사용해도 되나, FreeRTOS 용으로 표현해 보면 다음과 같습니다.
ESP32 를 활용함에 있어, FreeRTOS 용 함수를 사용하는 것이 좀더 빠를 듯 해서입니다.

portTickType delay_10 = 10 / portTICK_RATE_MS;

...
		vTaskDelay(delay_10);
...

setup()loop() 는 다음과 같은 형식 입니다.

void setup() {
	Serial.begin(115200);
	delay(50); // wait for initialization
	
	xTaskCreatePinnedToCore(
		task1,
		"task1",
		8192,
		NULL,
		1,
		NULL,
		PRO_CPU_NUM
	);
	
	xTaskCreatePinnedToCore(
		task2,
		"task2",
		8192,
		NULL,
		0,
		NULL,
		APP_CPU_NUM
	);
}

void loop() {
}


위에서 언급 했던, core 할당 방법 - xTaskCreate() / xTaskCreatePinnedToCore() / xTaskCreateUniversal() - 과, 어느 CPU - PRO_CPU_NUM / APP_CPU_NUM - 를 사용할 것인지를 정의해 줍니다.


위의 예시는, Core 0 / 1 에 강제적으로 task 를 할당한 예 입니다.

자세한 설명은 아래 FreeRTOS 문서를 참고해 보세요.


* FreeRTOS

- https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html




6. Single Core 활용


Core 한개만을 이용하여, 2승을 계산하는 간단한 코드를 만들어 봤습니다.


portTickType delay_10 = 10 / portTICK_RATE_MS;
int countX = 0;

void task1(void *pvParameters) {
	while (1) {
		for ( int i = 0 ; i < 1000 ; i++ ) {
			pow(2, i);
		}
		countX++;
		vTaskDelay(delay_10);
	}
}
 
void setup() {
	Serial.begin(115200);
	delay(50); // wait for initialization
	
	xTaskCreatePinnedToCore(
		task1,
		"task1",
		8192,
		NULL,
		1,
		NULL,
		PRO_CPU_NUM
	);
}
 
void loop() {
	Serial.println(countX);
	delay(100);
}


2의 1000 승이 완료되면 countX 에 1을 증가하는 방식으로, 일정한 시간 동안 Single 과 Dual 의 계산량 차이를 보기 위한 것입니다.


int countX = 0;

...

		for ( int i = 0 ; i < 1000 ; i++ ) {
			pow(2, i);
		}
		countX++;
...

void loop() {
	Serial.println(countX);
	delay(100);
}


setup() 에서 xTaskCreatePinnedToCore 를 이용하여 task1 하나만 정의하여 돌려 봤습니다.


...

	xTaskCreatePinnedToCore(
		task1,
		"task1",
		8192,
		NULL,
		1,
		NULL,
		PRO_CPU_NUM
	);

...


위에서 CPU core 지정 부분을 PRO_CPU_NUMAPP_CPU_NUM 을 바꿔 해봤지만, 동일한 결과를 보여 줬습니다.



약 50 초 동안, 2의 1000 승을 1600 회 정도 계산했습니다.




7. Dual Core 활용


xTaskCreatePinnedToCorePRO_CPU_NUM / APP_CPU_NUM 를 모두 사용하는 코드 입니다.


portTickType delay_10 = 10 / portTICK_RATE_MS;
int countX = 0;

void task1(void *pvParameters) {
	while (1) {
		for ( int i = 0 ; i < 1000 ; i++ ) {
			pow(2, i);
		}
		countX++;
		vTaskDelay(delay_10);
	}
}

void task2(void *pvParameters) {
	while (1) {
		for ( int i = 0 ; i < 1000 ; i++ ) {
			pow(2, i);
		}
		countX++;
		vTaskDelay(delay_10);
	}
}
 
void setup() {
	Serial.begin(115200);
	delay(50); // wait for initialization
	
	xTaskCreatePinnedToCore(
		task1,
		"task1",
		8192,
		NULL,
		1,
		NULL,
		PRO_CPU_NUM
	);
 
	xTaskCreatePinnedToCore(
		task2,
		"task2",
		8192,
		NULL,
		1,
		NULL,
		APP_CPU_NUM
	);
}
 
void loop() {
	Serial.println(countX);
	delay(100);
}


약 3200 개 정도를 계산해 냈네요. Single core 사용할 때보다, 딱 2배의 숫자 입니다. 이로써, dual core 가 사용되었다는 것을 알 수 있습니다.



Single core 의 계산 처리값과 한 그래프에서 비교하고 싶어서 index 를 추가하고 EXCEL 에서 그래프를 그려 보았습니다.


int x = 0;

...
 
void loop() {
  x++;
  Serial.print(x);
  Serial.print("\t");
  Serial.println(countX);
  delay(100);
}

왼쪽이 index, 오른쪽이 100 마이크로초 동안 2 의 1000 승을 계산한 횟수를 보여줍니다.


생각 외로 처리량이 꽤 좋습니다. Arduino Nano 와는 넘사벽인 성능입니다.


ESP32_dual_core_calc.xlsx



그래프에서 선이 겹쳐 보여 잘 보이지 않지만, PRO_CPU_NUM 이 조금 낮게 나올 것이라고 예상 했지만, WiFi 등을 사용하고 있지 않기 때문에, APP_CPU_NUM 과 동일하게 나왔습니다.


Dual core 를 활용하면서, 정확하게 2배의 계산 처리량을 보여 줬습니다.




FIN


CPU 도 Dual core 에다가 내장 센서, 그리고 Arduino IDE 에서 코딩할 수 있다는 점은 엄청나게 큰 장점일 듯 합니다.

거의 왠만한 프로젝트는 ESP32 하나로 커버 가능할 것 같습니다. EPS32 에 대해 다른 기능들도 차차 알아 보겠습니다.




참고 사이트


- https://www.hackster.io/rayburne/esp32-in-love-with-both-cores-8dd948

- https://www.mgo-tec.com/blog-entry-ledc-pwm-arduino-esp32.html/2

- https://qiita.com/makotaka/items/dd035f4b2db94f87b63c

- https://lang-ship.com/blog/work/esp32-freertos-l03-multitask/

- https://techtutorialsx.com/2017/05/16/esp32-dual-core-execution-speedup/



And