중국 사이트에서 발췌한 자료다. 영어보다 보기가 힘들고 번역도 정확한지 알 수 없다. 하지만 글에 사용한 설명 이미지가 마음에 들어 옮긴다. 세부 사항들은 검토하거나 다른 블로그 글을 참고한다.
ESP32 FreeRTOS 구현 살펴보기
ESP32-WROOM-32

아두이노 IDE 환경 설정
함수 라이브러리 설치
먼저, esp32 라이브러리를 가져와야 합니다.
스케치 -> 라이브러리 포함 -> 라이브러리 관리
ESP32 라이브러리를 찾아 설치합니다.
존재하다
파일 -> 기본 설정 -> 추가 보드 관리자 URL
https://dl.espressif.com/dl/package_esp32_index.json 이 추가되었습니다 .
보드 선택
이 시점에서 모든 ESP32 보드를 선택할 수 있는 추가 옵션이 있습니다. 제가 사용하는 보드가 ESP32이므로 ESP32-WROOM-32개발 보드를 선택했습니다 DOIT ESP32 DIVKIT V1. 디버깅 중 출력 레벨을 선택하는 데 도움이 되는 설정 등 다른 설정은 core debug level일반적으로 높은 레벨을 선택하면 낮은 디버그 레벨이 표시되지 않도록 지정합니다.
파일 위치
ESP32 무료 RTOS 라이브러리 아카이브 전체 위치
$ cd ~/.arduino15/packages/esp32/hardware/esp32/1.0.6/tools/sdk/include/freertos/freertos
그 안에서 FreeRTOSConfig.h다음과 같은 몇 가지 관련 설정을 볼 수 있습니다.
/*-----------------------------------------------------------
* Application specific definitions.
*
* These definitions should be adjusted for your particular hardware and
* application requirements.
*
* Note that the default heap size is deliberately kept small so that
* the build is more likely to succeed for configurations with limited
* memory.
*
* THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
* FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
*----------------------------------------------------------*/
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 1
#define configUSE_TICK_HOOK 1
#define configTICK_RATE_HZ ( CONFIG_FREERTOS_HZ )
/* Default clock rate for simulator */
//#define configCPU_CLOCK_HZ 80000000
/* This has impact on speed of search for highest priority */
#ifdef SMALL_TEST
#define configMAX_PRIORITIES ( 7 )
#else
#define configMAX_PRIORITIES ( 25 )
#endif
#ifndef CONFIG_ESP32_APPTRACE_ENABLE
#define configMINIMAL_STACK_SIZE 768
#else
/* apptrace module requires at least 2KB of stack per task */
#define configMINIMAL_STACK_SIZE 2048
#endif
중요 매개변수:
Priority level: 25
최소 작업 크기: 768 바이트
여러 개의 정의가 있는 매개변수에 대해 확신이 없다면 Serial 문자열에서 직접 인쇄하여 확인할 수 있습니다.
문제 해결
업로드 중 오류 메시지가 나타나는 경우
A fatal error occurred: Failed to connect to ESP32: Invalid head of packet(0x0D)
즉, 보드에 데이터를 쓸 수 없습니다(온에어 업로드). 이 경우 다음과 같은 조치를 취할 수 있습니다.
Boot 버튼을 길게 눌러 업로드하세요
더 나은 전송 케이블로 교체하세요.
포트를 읽는 데 문제가 있는지 확인하세요.
재부팅 후 다시 업로드하세요
초보자 연습
초기 구현은 단일 코어에 초점을 맞추고 병렬 처리를 포함하지 않습니다. FreeRTOS를 개발하기 위한 기본 프레임워크로 Arduino의 내장 설정 및 루프 기능을 활용합니다.
Blink 예제 Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const int led_pin = LED_BUILTIN;
void toggleLED(void *parameter) {
while(1) {
digitalWrite(led_pin, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(led_pin, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
void setup() {
pinMode(led_pin, OUTPUT);
xTaskCreatePinnedToCore(toggleLED, "Toggle LED", 1024, NULL, 1, NULL, app_cpu);
}
void loop() {
}
컴파일하고 업로드하면 플래싱 보드가 표시됩니다.
- vTaskDelay(500 / portTICK_PERIOD_MS):
INCLUDE_vTaskDelay먼저 1로 정의해야 합니다. 해상도는 단위 틱이며, 전달된 매개변수의 변수 유형은 입니다 const TickType_t. portTICK_PERIOD_MS는 틱 하나가 차지하는 시간이며, 여기서는 500ms 간격을 의미합니다. 여기서 지연은 일반 아두이노의 지연과 다르다는 점을 기억하세요. 여기서 지연은 비차단 지연입니다.
- xTaskCreatePinnedToCore:
freertos/include/freertos/ task.h에 정의됨
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char *constpcName,
const uint32_t usStackDepth,
void *constpvParameters,
UBaseType_t uxPriority,
TaskHandle_t *constpvCreatedTask,
const BaseType_t xCoreID
);
지정된 선호도를 가진 새 작업을 생성합니다.
- pvTaskCode이는 새 스레드를 생성하는 것과 유사하지만, 포스트백을 수행하지 않으며 기본 구조는 루프입니다.
- pcName작업 이름은 문자열입니다.
- ulStackDepth작업 스택 크기는 표준 FreeRTOS와 다릅니다.
- pvParameter생성되는 작업의 매개변수로 사용될 포인터입니다.
- uxPriority작업이 실행될 우선순위입니다.
- pxStackBuffer생성된 작업을 참조할 수 있는 핸들을 전달합니다.
- pxTaskBuffer값 0 또는 1은 작업을 고정할 CPU의 인덱스 번호를 나타냅니다.
작업 스케줄링
이 실험은 우선순위와 작업 간의 관계를 단순히 감추는 데 그칩니다. 우선순위가 가장 높은 작업인 작업 2가 우선순위가 낮은 작업들을 선점할 수 있음을 알 수 있습니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
# else
static const BaseType_t app_cpu = 1;
# endif
// string to print
const char msg[] = "Barkadeer brig Arr booty rum.";
//Task handles
static TaskHandle_t task_1 = NULL;
static TaskHandle_t task_2 = NULL;
//*************************************************
void startTask1 (void *parameter) {
int len = strlen(msg);
while (1){
Serial.println();
for (int i=0; i < len; i++) {
Serial.print(msg[i]);
}
Serial.println();
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void startTask2 (void *parameter) {
while (1) {
Serial.print('*');
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(300);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Task Demo---");
Serial.print("Setup and loop task running on core ");
Serial.print(xPortGetCoreID());
Serial.print(" with priority ");
Serial.println(uxTaskPriorityGet(NULL));
xTaskCreatePinnedToCore(startTask1,"Task1", 1024, NULL, 1, &task_1, app_cpu);
xTaskCreatePinnedToCore(startTask2,"Task2", 1024, NULL, 2, &task_1, app_cpu);
}
void loop() {
for (int i = 0; i < 3; i++) {
vTaskSuspend(task_2);
vTaskDelay(2000 / portTICK_PERIOD_MS);
vTaskResume(task_2);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (task_1 != NULL) {
vTaskDelete(task_1);
task_1 = NULL;
}
}
컨텍스트 전환 개념

vTaskSuspend(): 우리를 주목하세요
실제에서의 도전
LED의 깜박임 주파수를 제어하기 위해 직렬 읽기/쓰기 인터페이스를 구현합니다.
Code
#include <stdlib.h>
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
# else
static const BaseType_t app_cpu = 1;
# endif
static TaskHandle_t task_1 = NULL;
static TaskHandle_t task_2 = NULL;
//setting
static const uint8_t buf_len = 20;
static int blinking_rate = 500; //ms
static const int led_pin = LED_BUILTIN;
void startTask1 (void *parameter) {
char c;
char buf[buf_len];
uint8_t idx = 0;
memset(buf, 0, buf_len);
while (1) {
if (Serial.available() > 0) {
c = Serial.read();
if (c == '\n') {
blinking_rate = atoi(buf);
Serial.print("Updated LED delay to: ");
Serial.println(blinking_rate);
memset(buf, 0, buf_len);
idx = 0;
//vTaskDelay(2000 / portTICK_PERIOD_MS);
} else {
if (idx < buf_len - 1) {
buf[idx] = c;
idx++;
}
}
}
}
}
void startTask2 (void *parameter) {
while (1) {
vTaskDelay(blinking_rate / portTICK_PERIOD_MS);
digitalWrite(led_pin, HIGH);
vTaskDelay(blinking_rate / portTICK_PERIOD_MS);
digitalWrite(led_pin, LOW);
}
}
void setup() {
Serial.begin(9600);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("----LED Brinking Test with UI---");
Serial.println("Please Enter a number in miliseconds to change LED blinking rate");
pinMode(led_pin, OUTPUT);
xTaskCreatePinnedToCore(startTask1,"Read Serial", 1024, NULL, 1, &task_1, app_cpu);
xTaskCreatePinnedToCore(startTask2,"Toggle LED", 1024, NULL, 1, &task_1, app_cpu);
vTaskDelete(NULL);
}
void loop() {
}
두 작업 모두 우선순위가 같으므로 차례로 수행됩니다. task_2는 대부분의 시간을 지연 구간에서 보내기 때문에 직렬 데이터를 읽을 때 선점될 가능성이 적습니다.
다른 테스트를 수행해 보면, 예를 들어 우선순위를 변경하면
xTaskCreatePinnedToCore(startTask1,"Read Serial", 1024, NULL, 2, &task_1, app_cpu);task_1이 계속 바쁘게 루프를 돌 때 task_2가 CPU를 선점하는 것을 방지하여 task_2가 실행하는 LED가 전혀 변하지 않도록 할 수 있습니다. 하지만 앞서 vTaskDelay이 함수가 비차단형이라고 언급했기 때문에, 읽기 작업 후 함수를 추가하면 vTaskDelay(2000 / portTICK_PERIOD_MS);LED가 2초 동안 깜빡인 후 다시 차단됩니다.
메모리 관리
커널 -> 개발자 문서 -> 힙 메모리 관리
메모리 할당 섹션에서는 heap1~5의 할당 방법을 선택할 수 있으며, 각 할당 방법에는 해당 할당 제한이 있음을 알 수 있습니다.
Code
#include <stdlib.h>
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
# else
static const BaseType_t app_cpu = 1;
# endif
void testTask(void *parameter) {
while (1) {
int a = 1;
int b[100];
for (int i = 0; i < 100; i++) {
b[i] = a + 1;
}
Serial.println(b[0]);
}
}
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("freeRTOS Memory Demo");
xTaskCreatePinnedToCore(testTask,"Test Task", 1024, NULL, 1, NULL, app_cpu);
vTaskDelete(NULL);
}
void loop() {
}
컴파일 후 오류 메시지가 나타날 것으로 예상됩니다.
Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception)
Debug exception reason: Stack canary watchpoint triggered (Test Task)
Core 1 register dump:
PC : 0x40083774 PS : 0x00060c36 A0 : 0x3ffb8520 A1 : 0x3ffb8460
A2 : 0x3ffbdbb4 A3 : 0x3ffb85aa A4 : 0x3ffb85ab A5 : 0x00000000
A6 : 0x00000000 A7 : 0x00000000 A8 : 0x0000007f A9 : 0x3ff40000
A10 : 0x0000007f A11 : 0x00000000 A12 : 0x3ffb8218 A13 : 0x00000000
A14 : 0x00000000 A15 : 0x00000000 SAR : 0x00000000 EXCCAUSE: 0x00000001
EXCVADDR: 0x00000000 LBEG : 0x400d0bed LEND : 0x400d0bf5 LCOUNT : 0x00000000
ELF file SHA256: 0000000000000000
Backtrace: 0x40083774:0x3ffb8460 0x3ffb851d:0x3ffb8540 0x400d0ed2:0x3ffb8560 0x400d0f4e:0x3ffb8580 0x400d0f8d:0x3ffb85d0 0x400d0fa0:0x3ffb85f0 0x400d0bfd:0x3ffb8610 0x400860ed:0x3ffb87c0
Rebooting...
ets Jun 8 2016 00:22:57
rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0018,len:4
load:0x3fff001c,len:1044
load:0x40078000,len:10124
load:0x40080400,len:5856
entry 0x400806a8
여기서는 이 작업에 1024바이트를 할당했기 때문에 스택 오버플로가 발생하는 것을 볼 수 있지만, 전체 작업에 약 1200바이트의 공간이 필요하므로 1500바이트의 공간을 할당했습니다.
남은 스택 공간을 쿼리하는 데 사용되며 UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask), 단어 단위로 반환합니다. 매개변수로 NULL을 전달하면 현재 스택을 쿼리합니다.
이를 사용하여 xPortGetFreeHeapSize()남은 힙 공간을 쿼리합니다.
이를 사용하여 vPortFree(ptr)지표를 발표합니다.
또는 vTaskList()를 사용하여 작업과 스택 상태를 동시에 확인할 수 있습니다.
실제에서의 도전
구현에는 두 가지 작업을 생성할 수 있는 기능이 필요합니다. 하나는 직렬 모니터에 메시지를 기록하고, 다른 하나는 해당 메시지를 다른 작업에 전송하여 표시할 수 있습니다.
Code
include <stdlib.h>
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
# else
static const BaseType_t app_cpu = 1;
# endif
static const int buf_len = 225;
static char *msg_ptr = NULL;
static volatile uint8_t msg_flag = 0;
//***************************************************
void serialProducer(void *parameter) {
char c;
char buf[buf_len];
uint8_t idx = 0;
memset(buf, 0, buf_len);
while (1) {
if (Serial.available() > 0) {
c = Serial.read();
// Store received character to buffer if not over buffer limit
if (idx < buf_len - 1) {
buf[idx] = c;
idx++;
}
// Create a message buffer for print task
if (c == '\n') {
buf[idx - 1] = '\0';
if (msg_flag == 0) {
msg_ptr = (char *)pvPortMalloc(idx * sizeof(char));
// If malloc returns 0 (out of memory), throw an error and reset
configASSERT(msg_ptr);
memcpy(msg_ptr, buf, idx);
msg_flag = 1;
}
memset(buf, 0, buf_len);
idx = 0;
}
}
}
}
void serialConsumer(void *parameter) {
while (1) {
if (msg_flag == 1) {
Serial.println(msg_ptr);
// Give amount of free heap memory (uncomment if you'd like to see it)
Serial.print("Free heap (bytes): ");
Serial.println(xPortGetFreeHeapSize());
// Free buffer, set pointer to null, and clear flag
vPortFree(msg_ptr);
msg_ptr = NULL;
msg_flag = 0;
}
}
}
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Heap Demo---");
Serial.println("Enter a string");
xTaskCreatePinnedToCore(serialProducer,"Serial Producer",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(serialConsumer,"Serial Consumer",1024,NULL,1,NULL,app_cpu);
vTaskDelete(NULL);
}
void loop() {
}
구현은 serialProducer225자 미만의 문자열을 읽고, 줄바꿈 문자가 수신되면 buf위의 문자열을 다음 msg_ptr빈 msg_flag위치로 이동합니다. 동시에 serialConsumer변경 플래그가 수신되면 msg_ptr문자열을 출력하고, 남은 힙 공간을 확인한 후, 읽기가 완료되면 힙에 할당된 공간을 비웁니다.
configASSERTC의 assert 함수와 비슷하게 0으로 설정하면 프로그램 전체가 중지됩니다.
여기서 volatile을 사용하는 이유 msg_flag는 플래그가 변경되는 순간 플래그를 메모리에 다시 쓸 수 있도록 하여 데이터 일관성을 보장하기 위함입니다.
Queue 대기열
이 실험은 쓰기 보호 문제를 해결하는 것을 목표로 합니다. 두 개의 서로 다른 스레드(태스크)에 의해 메모리가 비동기적으로 수정될 때, 수정 시점 때문에 경쟁 조건이 발생할 수 있다는 것을 알고 있습니다 . 여러 가지 해결책이 있습니다. 하나는 읽기 및 쓰기 작업을 원자적 동작으로 수행하거나, 뮤텍스 또는 세마포어를 사용하여 임계 영역을 확보하는 것입니다. 여기서는 읽기-쓰기 큐를 사용합니다.
- 큐 공간은 힙에 저장됩니다.
- ISR 내부의 큐에 읽거나 쓰지 마세요. 인터럽트는 틱 타이머를 따르지 않습니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const uint8_t msg_queue_len = 5;
static QueueHandle_t msg_queue;
//*****************************************************************************
// Tasks
void printMessages(void *parameters) {
int item;
while (1) {
if (xQueueReceive(msg_queue, (void *)&item, 0) == pdTRUE) {
//Serial.println(item);
}
Serial.println(item);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Queue Demo---");
msg_queue = xQueueCreate(msg_queue_len, sizeof(int));
xTaskCreatePinnedToCore(printMessages,
"Print Messages",
1024,
NULL,
1,
NULL,
app_cpu);
}
void loop() {
static int num = 0;
if (xQueueSend(msg_queue, (void *)&num, 10) != pdTRUE) {
Serial.println("Queue full");
}
num++;
vTaskDelay(500 / portTICK_PERIOD_MS);
}
xQueueReceive : 입력 QueueHandle_t과 void * pvItemToQueue타임아웃 틱 카운트. 수신이 성공하면 pdTRUE콜백이 전송되고, 그렇지 않으면 콜백이 전송됩니다.pdFALSE
xQueueSend : 값 QueueHandle_t과 void * pvItemToQueue타임아웃 틱 번호를 전달합니다. 반환 값은 위와 같습니다.
코드에서 queueSend와 queueRead의 지연 속도를 조정하면 읽기와 쓰기 속도가 다를 때 큐의 변화를 관찰할 수 있습니다.
실제에서의 도전
다음을 달성하기 위해 두 개의 작업과 대기열로 구성된 비동기 메시지 전송 구조를 구현합니다.

TaskA는 주로 Serial 메시지를 수신하여 q1에 전달하는 역할을 담당하고, taskB는 q1에서 해당 명령어를 검색하여 깜박임 빈도를 변경하고 깜박임 정보를 q2에 전송합니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const uint8_t buf_len = 255; // Size of buffer to look for command
static const char command[] = "delay "; // Note the space!
static const int delay_queue_len = 5; // Size of delay_queue
static const int msg_queue_len = 5; // Size of msg_queue
static const uint8_t blink_max = 100; // Num times to blink before message
// Pins (change this if your Arduino board does not have LED_BUILTIN defined)
static const int led_pin = LED_BUILTIN;
typedef struct Message {
char body[20];
int count;
} Message;
static QueueHandle_t delay_queue;
static QueueHandle_t msg_queue;
//*****************************************************************************
// Tasks
// Task: command line interface (CLI)
void doCLI(void *parameters) {
Message rcv_msg;
char c;
char buf[buf_len];
uint8_t idx = 0;
uint8_t cmd_len = strlen(command);
int led_delay;
memset(buf, 0, buf_len);
while (1) {
if (xQueueReceive(msg_queue, (void *)&rcv_msg, 0) == pdTRUE) {
Serial.print(rcv_msg.body);
Serial.println(rcv_msg.count);
}
if (Serial.available() > 0) {
c = Serial.read();
if (idx < buf_len - 1) {
buf[idx] = c;
idx++;
}
if ((c == '\n') || (c == '\r')) {
Serial.print("\r\n");
if (memcmp(buf, command, cmd_len) == 0) {
char* tail = buf + cmd_len;
led_delay = atoi(tail);
led_delay = abs(led_delay);
if (xQueueSend(delay_queue, (void *)&led_delay, 10) != pdTRUE) {
Serial.println("ERROR: Could not put item on delay queue.");
}
}
memset(buf, 0, buf_len);
idx = 0;
} else {
Serial.print(c);
}
}
}
}
// Task: flash LED based on delay provided, notify other task every 100 blinks
void blinkLED(void *parameters) {
Message msg;
int led_delay = 500;
uint8_t counter = 0;
pinMode(LED_BUILTIN, OUTPUT);
while (1) {
if (xQueueReceive(delay_queue, (void *)&led_delay, 0) == pdTRUE) {
strcpy(msg.body, "Message received ");
msg.count = 1;
xQueueSend(msg_queue, (void *)&msg, 10);
}
digitalWrite(led_pin, HIGH);
vTaskDelay(led_delay / portTICK_PERIOD_MS);
digitalWrite(led_pin, LOW);
vTaskDelay(led_delay / portTICK_PERIOD_MS);
counter++;
if (counter >= blink_max) {
strcpy(msg.body, "Blinked: ");
msg.count = counter;
xQueueSend(msg_queue, (void *)&msg, 10);
counter = 0;
}
}
}
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Queue Solution---");
Serial.println("Enter the command 'delay xxx' where xxx is your desired ");
Serial.println("LED blink delay time in milliseconds");
delay_queue = xQueueCreate(delay_queue_len, sizeof(int));
msg_queue = xQueueCreate(msg_queue_len, sizeof(Message));
xTaskCreatePinnedToCore(doCLI,
"CLI",
1024,
NULL,
1,
NULL,
app_cpu);
xTaskCreatePinnedToCore(blinkLED,
"Blink LED",
1024,
NULL,
1,
NULL,
app_cpu);
vTaskDelete(NULL);
}
void loop() {
}
두 개의 대기열은 각각 지연과 메시지를 기록합니다.
전송할 메시지 구조를 정의합니다.
typedef struct Message {
char body[20];
int count;
} Message;
가운데 부분인 몸통은 LED가 깜박이는 횟수를 기록하는 문자열로 사용됩니다.
Mutex 뮤텍스
freeRTOS에서 뮤텍스는 실제로 세마포어와 같은 .h 파일에 패키징되어 있습니다. 기본적으로 freeRTOS에서 뮤텍스와 세마포어는 같은 것입니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
// Globals
static int shared_var = 0;
static SemaphoreHandle_t mutex;
//*****************************************************************************
// Tasks
void incTask(void *parameters) {
int local_var;
while (1) {
if (xSemaphoreTake(mutex, 0) == pdTRUE) {
local_var = shared_var;
local_var++;
vTaskDelay(random(100, 500) / portTICK_PERIOD_MS);
shared_var = local_var;
Serial.println(shared_var);
xSemaphoreGive(mutex);
} else {
// Do something else
}
}
}
//*****************************************************************************
// Main (runs as its own task with priority 1 on core 1)
void setup() {
randomSeed(analogRead(0));
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Race Condition Demo---");
mutex = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(incTask,"Increment Task 1",1024,NULL,1,NULL,app_cpu);
xTaskCreatePinnedToCore(incTask,"Increment Task 2",1024,NULL,1,NULL,app_cpu);
// Delete "setup and loop" task
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}
커널 -> API 참조 -> 세마포어/뮤텍스
xSemaphoreCreateMutex()뮤텍스/세마포어 초기화
xSemaphoreTake : mutex_lock과 동일한 뮤텍스/세마포어를 검색합니다.
xSemaphoreGive : mutex_unlock과 동일하게 뮤텍스/세마포어를 해제합니다.
여기서는 무작위 방법을 사용하여 incTask각 증가 후 지연 시간을 결정하고 경쟁 조건을 시뮬레이션했습니다. 그러나 임계 구역을 보호한 후 최종 출력은 서로 다른 속도로 업데이트되지만, shared_var증가는 순차적입니다.
실제에서의 도전
들어오는 데이터를 처리할 때 void *parameter로컬 동기화 문제가 발생할 수 있으며, 이로 인해 delay_arg쓰기 중에는 값이 변경되지만 입력 매개변수가 전달될 때는 여전히 0이 되는 경우가 있습니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const int led_pin = LED_BUILTIN;
//*****************************************************************************
// Tasks
void blinkLED(void *parameters) {
// Copy the parameter into a local variable
int num = *(int *)parameters;
// Print the parameter
Serial.print("Received: ");
Serial.println(num);
// Configure the LED pin
pinMode(led_pin, OUTPUT);
while (1) {
digitalWrite(led_pin, HIGH);
vTaskDelay(num / portTICK_PERIOD_MS);
digitalWrite(led_pin, LOW);
vTaskDelay(num / portTICK_PERIOD_MS);
}
}
void setup() {
long int delay_arg;
// Configure Serial
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Mutex Challenge---");
Serial.println("Enter a number for delay (milliseconds)");
// Wait for input from Serial
while (Serial.available() <= 0);
delay_arg = Serial.parseInt();
Serial.print("Sending: ");
Serial.println(delay_arg);
// Start task 1
xTaskCreatePinnedToCore(blinkLED,
"Blink LED",
1024,
(void *)&delay_arg,
1,
NULL,
app_cpu);
// Show that we accomplished our task of passing the stack-based argument
Serial.println("Done!");
}
void loop() {
// Do nothing but allow yielding to lower-priority tasks
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
예상 출력
---FreeRTOS Mutex Challenge---
Enter a number for delay (milliseconds)
Sending: 2323
Done!
Received: 0
변경 작업에는 두 단계가 포함됩니다. 첫째, delay_arg전역 변수로 변환하고, 둘째, 쓰기 작업을 뮤텍스로 래핑합니다.
while(1){
if (xSemaphoreTake(mutex, 0) == pdTRUE) {
delay_arg = Serial.parseInt();
Serial.print("Sending: ");
Serial.println(delay_arg);
xSemaphoreGive(mutex);
break;
}
}
예상 출력은 입력 후 깜박임 주파수를 변경할 수 있습니다.
Semaphore 세마포어
- xSemaphoreCreateBinary초기화하는 동안 기본 세마포어로 선언할 수 있는데, 이것이 같은 .h 파일 내에서 가장 큰 차이점입니다.
- xSemaphoreCreateCounting(uxMaxCount, uxInitialCount) : 최대 크기가 uxMaxCount이고 초기 값이 uxInitialCount인 세마포어를 생성합니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const int num_tasks = 5;
typedef struct Message {
char body[20];
uint8_t len;
} Message;
static SemaphoreHandle_t sem_params;
void myTask(void *parameters) {
Message msg = *(Message *)parameters;
xSemaphoreGive(sem_params);
xSemaphoreTake(sem_params);
Serial.print("Received: ");
Serial.print(msg.body);
Serial.print(" | len: ");
Serial.println(msg.len);
xSemaphoreGive(sem_params);
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
void setup() {
char task_name[12];
Message msg;
char text[20] = "All your base";
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Counting Semaphore Demo---");
// Create semaphores (initialize at 0)
sem_params = xSemaphoreCreateCounting(num_tasks, 0);
// Create message to use as argument common to all tasks
strcpy(msg.body, text);
msg.len = strlen(text);
// Start tasks
for (int i = 0; i < num_tasks; i++) {
// Generate unique name string for task
sprintf(task_name, "Task %i", i);
// Start task and pass argument (common Message struct)
xTaskCreatePinnedToCore(myTask,
task_name,
1024,
(void *)&msg,
1,
NULL,
app_cpu);
}
for (int i = 0; i < num_tasks; i++) {
xSemaphoreTake(sem_params, portMAX_DELAY);
}
Serial.println("All tasks created");
}
void loop() {
// Do nothing but allow yielding to lower-priority tasks
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
원래 버전에서는 Task.Serial.print 명령문 내에 세마포어를 지정하지 않고 세마포어 보호 메커니즘을 추가했습니다. 즉, "Received: All your base | len: 13"이라는 텍스트를 출력하는 작업이 다른 작업에 의해 중단될 수 있다는 것을 의미했습니다. print 명령문 앞뒤에 세마포어 획득 및 해제를 추가함으로써 이러한 print 명령문을 성공적으로 보호할 수 있습니다.
xSemaphoreTake(sem_params);
Serial.print("Received: ");
Serial.print(msg.body);
Serial.print(" | len: ");
Serial.println(msg.len);
xSemaphoreGive(sem_params);
하지만 한 가지 의문이 남습니다. 왜 이 세마포어는 내부 메시지가 순서대로 출력되도록 보장하는 걸까요? 제가 이해하기로는 이 세마포어는 임계 구역에 진입하는 작업의 제한만 보장할 뿐, 임계 구역에 진입하는 작업의 실행 순서를 보장해서는 안 됩니다.
실제에서의 도전
그림에서 보는 바와 같이,

프로듀서 다섯 명과 컨슈머 두 명을 생성하겠습니다. 각 프로듀서는 자신에게 할당된 번호를 순환 버퍼에 세 번 쓰고, 컨슈머는 버퍼에서 데이터를 읽습니다.
Code
//#include semphr.h
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
enum {BUF_SIZE = 5};
static const int num_prod_tasks = 5;
static const int num_cons_tasks = 2;
static const int num_writes = 3;
static int buf[BUF_SIZE];
static int head = 0; // Writing index to buffer
static int tail = 0; // Reading index to buffer
static SemaphoreHandle_t bin_sem; // Waits for parameter to be read
void producer(void *parameters) {
int num = *(int *)parameters;
xSemaphoreGive(bin_sem);
for (int i = 0; i < num_writes; i++) {
buf[head] = num;
head = (head + 1) % BUF_SIZE;
}
vTaskDelete(NULL);
}
void consumer(void *parameters) {
int val;
while (1) {
val = buf[tail];
tail = (tail + 1) % BUF_SIZE;
Serial.println(val);
}
}
void setup() {
char task_name[12];
// Configure Serial
Serial.begin(115200);
// Wait a moment to start (so we don't miss Serial output)
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Semaphore Alternate Solution---");
bin_sem = xSemaphoreCreateBinary();
for (int i = 0; i < num_prod_tasks; i++) {
sprintf(task_name, "Producer %i", i);
xTaskCreatePinnedToCore(producer,
task_name,
1024,
(void *)&i,
1,
NULL,
app_cpu);
xSemaphoreTake(bin_sem, portMAX_DELAY);
}
for (int i = 0; i < num_cons_tasks; i++) {
sprintf(task_name, "Consumer %i", i);
xTaskCreatePinnedToCore(consumer,
task_name,
1024,
NULL,
1,
NULL,
app_cpu);
}
Serial.println("All tasks created");
}
void loop() {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
소프트웨어 타이머
주기적인 작업을 만들려면 다음과 같이 하면 됩니다.
- 일정 기간마다 새로 추가된 작업을 사용하는 것은 가능하지만, 오버헤드가 너무 크기 때문에 권장하지는 않습니다.vTaskDelay
- xTaskGetTickCount우리는 얼마나 많은 작업이 완료되지 않았는지 주기적으로 확인합니다. 하지만 정확도는 1밀리초 수준입니다.
- 하드웨어 타이머는 휴대성이 좋지 않으며 ESP에서 사용할 수 있는 유형이 많지 않습니다.
- 소프트웨어 타이머:
두 개의 작업을 만듭니다. 하나는 시작 후 2초마다 한 번씩 실행되는 작업이고, 다른 하나는 1초마다 루프로 실행되는 작업입니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static TimerHandle_t one_shot_timer = NULL;
static TimerHandle_t auto_reload_timer = NULL;
//*****************************************************************************
// Callback function
// Called when one of the timers expires
void myTimerCallback(TimerHandle_t xTimer) {
// Print message if timer 0 expired
if ((uint32_t)pvTimerGetTimerID(xTimer) == 0) {
Serial.println("One-shot timer expired");
}
// Print message if timer 1 expired
if ((uint32_t)pvTimerGetTimerID(xTimer) == 1) {
Serial.println("Auto-reload timer expired");
}
}
//*****************************************************************************
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Timer Demo---");
// Create a one-shot timer
one_shot_timer = xTimerCreate(
"One-shot timer", // Name of timer
2000 / portTICK_PERIOD_MS, // Period of timer (in ticks)
pdFALSE, // Auto-reload
(void *)0, // Timer ID
myTimerCallback); // Callback function
// Create an auto-reload timer
auto_reload_timer = xTimerCreate(
"Auto-reload timer", // Name of timer
1000 / portTICK_PERIOD_MS, // Period of timer (in ticks)
pdTRUE, // Auto-reload
(void *)1, // Timer ID
myTimerCallback); // Callback function
if (one_shot_timer == NULL || auto_reload_timer == NULL) {
Serial.println("Could not create one of the timers");
} else {
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Starting timers...");
xTimerStart(one_shot_timer, portMAX_DELAY);
xTimerStart(auto_reload_timer, portMAX_DELAY);
}
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}
- xTimerCreate : 정확도는 1ms로 유지됩니다.
- xTimerStart: 두 개의 매개변수를 사용합니다. 첫 번째는 제어할 타이머이고, 두 번째는 블록 시간입니다.
실제에서의 도전
구현에는 문자 입력을 받을 수 있는 타이머가 포함되어 있으며, 이 타이머는 5초 동안 LED를 켜둔 다음 문자가 입력될 때마다 꺼집니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
static const TickType_t dim_delay = 5000 / portTICK_PERIOD_MS;
static const int led_pin = LED_BUILTIN;
static TimerHandle_t one_shot_timer = NULL;
// Turn Off LED after 5 seconds
void autoDimmerCallback(TimerHandle_t xTimer) {
digitalWrite(led_pin, LOW);
}
void serialin (void *parameter) {
char c;
pinMode(led_pin, OUTPUT);
while(1) {
if (Serial.available() > 0) {
c = Serial.read();
Serial.print(c);
// Turn on the LED
digitalWrite(led_pin, HIGH);
// Start timer (if timer is already running, this will act as
// xTimerReset() instead)
xTimerStart(one_shot_timer, portMAX_DELAY);
}
}
}
void setup() {
Serial.begin(115200);
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS LED Dimmer Demo---");
one_shot_timer = xTimerCreate(
"One-shot timer", // Name of timer
dim_delay, // Period of timer (in ticks)
pdFALSE, // Auto-reload
(void *)0, // Timer ID
autoDimmerCallback); // Callback function
xTaskCreatePinnedToCore(serialin,
"Do CLI",
1024,
NULL,
1,
NULL,
app_cpu);
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}
하드웨어 인터럽트
많은 실시간 시스템, 특히 FreeRTOS에서는 하드웨어 인터럽트가 소프트웨어 제어 인터럽트보다 우선순위가 가장 높습니다.
기본 타이머 클럭은 80MHz입니다. timer_divider매개변수를 80으로 설정하는 목적은 점멸 주파수를 1Hz로, 즉 1초에 한 번씩 전환하도록 하는 것입니다.
하드웨어 인터럽트 때문에 ESP에서 제공하는 API를 사용하므로 FreeRTOS와는 거의 관련이 없습니다.
함수가 하드웨어 인터럽트 이벤트를 처리하도록 설계된 경우, IRAM_ATTR
속성 파일에 추가 사양을 추가해야 합니다.
ESP_INTR_FLAG_IRAM을 사용하여 인터럽트 핸들러를 등록하는 경우, 인터럽트 핸들러는 IRAM에 저장되어야 합니다. 이 경우, ISR은 IRAM에 저장된 함수나 ROM에 있는 함수만 호출할 수 있습니다.
ISR 핸들러는 IRAM에 직접 배치해야 합니다. 또한 다른 FreeRTOS API도 IRAM에 배치됩니다. 플래시에서 코드를 로드하는 데 소요되는 시간을 줄이기 위해 특정 시간에 민감한 코드는 IRAM에 배치해야 합니다. ESP32는 32KB 캐시를 통해 코드와 데이터를 읽습니다. 경우에 따라 함수를 IRAM에 배치하면 캐시 미스로 인한 지연 시간을 줄일 수 있습니다.
timerBeginESP-IDF에서 관련 기록을 찾을 수 없으므로 Arduino ESP 에서 사용하는 함수 로 제한되는 것 같습니다 . (타이머 생성 및 시작...)
timerAttachInterrupt타이머에 ISR을 제공합니다.
timerAlarmWrite타이머는 언제 작동하나요? 반복되나요?
timerAlarmEnable: ISR 트리거
Code
static const uint16_t timer_divider = 80;
static const uint64_t timer_max_count = 1000000;
// Pins (change this if your Arduino board does not have LED_BUILTIN defined)
static const int led_pin = LED_BUILTIN;
// Globals
static hw_timer_t *timer = NULL;
//*****************************************************************************
// Interrupt Service Routines (ISRs)
// This function executes when timer reaches max (and resets)
void IRAM_ATTR onTimer() {
// Toggle LED
int pin_state = digitalRead(led_pin);
digitalWrite(led_pin, !pin_state);
}
//*****************************************************************************
// Main (runs as its own task with priority 1 on core 1)
void setup() {
// Configure LED pin
pinMode(led_pin, OUTPUT);
// Create and start timer (num, divider, countUp)
timer = timerBegin(0, timer_divider, true);
// Provide ISR to timer (timer, function, edge)
timerAttachInterrupt(timer, &onTimer, true);
// At what count should ISR trigger (timer, count, autoreload)
timerAlarmWrite(timer, timer_max_count, true);
// Allow ISR to trigger
timerAlarmEnable(timer);
}
void loop() {
// Do nothing
}
이는 세마포어를 사용하여 1초마다 한 번씩 트리거됩니다. xSemaphoreTakeISR은 작업으로 간주될 수 없으므로 차단될 수 없으므로 ISR에는 적합하지 않습니다.
- xSemaphoreGiveFromISR : 먼저 사용 xSemaphoreCreateBinary()하거나 xSemaphoreCreateCounting()생성해야 합니다. ISR 계열 인터럽트는 차단될 수 없으므로 뮤텍스와 함께 사용할 수 없습니다. 따라서 뮤텍스나 세마포어는 당연히 존재하지 않습니다.
- portYIELD_FROM_ISR()그렇다면 task_woken == pdTRUE해당 작업이 세마포어를 성공적으로 수신했으며 포트를 성공적으로 전송할 수 있다는 의미입니다.
이 접근 방식은 while 루프를 사용하여 스스로를 잠그고(교착 상태), 하드웨어 인터럽트를 통해 강제로 잠금을 해제하는 것과 다소 유사합니다. 그러나 이 접근 방식은 뮤텍스 잠금의 특성상 다음과 같은 위험을 수반합니다.
스레드가 이미 잠근 뮤텍스를 다시 잠그려고 시도하는 경우 pthread_mutex_lock()은 다음 표의 Relock 열에 설명된 대로 동작합니다.
- 유형의 뮤텍스는 PTHREAD_MUTEX_NORMAL교착 상태에 빠질 수 있습니다.
- 유형의 뮤텍스는 PTHREAD_MUTEX_ERRORCHECK오류를 반환해야 합니다.
- 유형의 뮤텍스는 PTHREAD_MUTEX_RECURSIVE재귀적 잠금으로 작동하므로 잠근 횟수만큼 잠금을 해제해야 합니다.
- 유형의 뮤텍스는 PTHREAD_MUTEX_DEFAULT정의되지 않은 동작을 합니다. 즉, 해당 플랫폼에서 기본 잠금이 이전 3가지 유형 중 하나라면 위의 열에 나와 있는 것과 같은 특성을 가지고 동작하고, 다른 유형이라면 동작이 정의되지 않습니다.
이 설명에서는 pthread를 사용하지만, 프로젝트가 커지면 잠금이 걸린 위치에 대한 혼란이 쉽게 발생할 수 있습니다. 또한, 사용된 뮤텍스가 일반 뮤텍스를 제대로 표현하는지 여부도 불확실하므로 이 방법은 권장되지 않습니다.
Code
//*****************************************************************************
// Interrupt Service Routines (ISRs)
// This function executes when timer reaches max (and resets)
void IRAM_ATTR onTimer() {
BaseType_t task_woken = pdFALSE;
// Perform action (read from ADC)
val = analogRead(adc_pin);
// Give semaphore to tell task that new value is ready
xSemaphoreGiveFromISR(bin_sem, &task_woken);
// Exit from ISR (Vanilla FreeRTOS)
//portYIELD_FROM_ISR(task_woken);
// Exit from ISR (ESP-IDF)
if (task_woken) {
portYIELD_FROM_ISR();
}
}
//*****************************************************************************
// Tasks
// Wait for semaphore and print out ADC value when received
void printValues(void *parameters) {
// Loop forever, wait for semaphore, and print value
while (1) {
xSemaphoreTake(bin_sem, portMAX_DELAY);
Serial.println(val);
}
}
- portENTER_CRITICAL_ISR(&spinlock)차단하는 뮤텍스는 사용할 수 없지만 항상 활성화된 스핀락은 사용할 수 있습니다.
- portEXIT_CRITICAL_ISR(&spinlock)스핀락으로 돌아갑니다. 가운데에 있는 섹션이 임계 섹션입니다.
- portMUX_TYPE spinlock = portMUX_INITIALIZER_UNLOCKED스핀록 초기화.
각 출력이 19에서 0까지 카운트다운되는 것을 볼 수 있습니다. 이 코드는 2초마다 카운터를 0으로 줄이는 역할을 합니다. 이 시간 동안 프로그램은 0.1초마다 한 번씩 중단되고 카운터는 1씩 증가합니다.
Code
void IRAM_ATTR onTimer() {
// ESP-IDF version of a critical section (in an ISR)
portENTER_CRITICAL_ISR(&spinlock);
isr_counter++;
portEXIT_CRITICAL_ISR(&spinlock);
}
void printValues(void *parameters) {
// Loop forever
while (1) {
// Count down and print out counter value
while (isr_counter > 0) {
// Print value of counter
Serial.println(isr_counter);
// ESP-IDF version of a critical section (in a task)
portENTER_CRITICAL(&spinlock);
isr_counter--;
portEXIT_CRITICAL(&spinlock);
}
// Wait 2 seconds while ISR increments counter a few times
vTaskDelay(task_delay);
}
}
Deadlock 교착상태
우선순위가 높은 작업은 우선순위가 낮은 작업의 자원 고갈을 초래할 수 있습니다. 이 문제를 해결하는 방법은 두 가지입니다. 첫째, 우선순위가 높은 작업에 더 많은 유휴 시간을 허용하여 우선순위가 낮은 작업이 충분한 실행 시간을 확보하도록 합니다. 둘째, 우선순위가 낮은 작업에 대해 에이징 메커니즘을 구현하여 시간이 지남에 따라 우선순위가 높아지도록 합니다. 우선순위가 높아지는 정도는 설정에 따라 다릅니다. 충분한 실행이 완료된 것으로 간주되면 우선순위가 원래 수준으로 낮아진 후 다시 점진적으로 높아집니다.
이와 대조적으로, 또 다른 시나리오는 라이브락(livelock)입니다. 라이브락은 한 자원이 다른 자원을 획득하려고 시도하다가 포기되어 또 다른 교착 상태가 발생하는 상황입니다. 이러한 동적 교착 상태를 라이브락이라고 합니다.
Code
if (xSemaphoreTake(mutex_2, mutex_timeout) == pdTRUE) {
// Say we took mutex 2 and wait (to force deadlock)
Serial.println("Task B took mutex 2");
vTaskDelay(1 / portTICK_PERIOD_MS);
// Take mutex 1
if (xSemaphoreTake(mutex_1, mutex_timeout) == pdTRUE) {
// Say we took mutex 1
Serial.println("Task B took mutex 1");
// Critical section protected by 2 mutexes
Serial.println("Task B doing some work");
vTaskDelay(500 / portTICK_PERIOD_MS);
} else {
Serial.println("Task B timed out waiting for mutex 1");
}
} else {
Serial.println("Task B timed out waiting for mutex 2");
}
// Give back mutexes
xSemaphoreGive(mutex_1);
xSemaphoreGive(mutex_2);
// Wait to let the other task execute
Serial.println("Task B going to sleep");
vTaskDelay(500 / portTICK_PERIOD_MS);
교착 상태 문제에 대한 한 가지 해결책은 타임아웃입니다. 타임아웃
역시 임계 구역에 진입하기 위해 두 개의 서로 다른 리소스가 필요하지만, 각 작업은 1초의 타임아웃 기간을 갖습니다.
Code
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
// Globals
static SemaphoreHandle_t mutex_1;
static SemaphoreHandle_t mutex_2;
//*****************************************************************************
// Tasks
// Task A (high priority)
void doTaskA(void *parameters) {
// Loop forever
while (1) {
// Take mutex 1 (introduce wait to force deadlock)
xSemaphoreTake(mutex_1, portMAX_DELAY);
Serial.println("Task A took mutex 1");
vTaskDelay(1 / portTICK_PERIOD_MS);
// Take mutex 2
xSemaphoreTake(mutex_2, portMAX_DELAY);
Serial.println("Task A took mutex 2");
// Critical section protected by 2 mutexes
Serial.println("Task A doing some work");
vTaskDelay(500 / portTICK_PERIOD_MS);
// Give back mutexes (in reverse order that we took them)
xSemaphoreGive(mutex_2);
xSemaphoreGive(mutex_1);
// Wait to let the other task execute
Serial.println("Task A going to sleep");
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// Task B (low priority)
void doTaskB(void *parameters) {
// Loop forever
while (1) {
// Take mutex 1 (introduce wait to force deadlock)
xSemaphoreTake(mutex_1, portMAX_DELAY);
Serial.println("Task B took mutex 1");
vTaskDelay(1 / portTICK_PERIOD_MS);
// Take mutex 2
xSemaphoreTake(mutex_2, portMAX_DELAY);
Serial.println("Task B took mutex 2");
// Critical section protected by 2 mutexes
Serial.println("Task B doing some work");
vTaskDelay(500 / portTICK_PERIOD_MS);
// Give back mutexes (in reverse order that we took them)
xSemaphoreGive(mutex_2);
xSemaphoreGive(mutex_1);
// Wait to let the other task execute
Serial.println("Task A going to sleep");
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
//*****************************************************************************
// Main (runs as its own task with priority 1 on core 1)
void setup() {
// Configure Serial
Serial.begin(115200);
// Wait a moment to start (so we don't miss Serial output)
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Deadlock Demo---");
// Create mutexes before starting tasks
mutex_1 = xSemaphoreCreateMutex();
mutex_2 = xSemaphoreCreateMutex();
// Start Task A (high priority)
xTaskCreatePinnedToCore(doTaskA,
"Task A",
1024,
NULL,
2,
NULL,
app_cpu);
// Start Task B (low priority)
xTaskCreatePinnedToCore(doTaskB,
"Task B",
1024,
NULL,
1,
NULL,
app_cpu);
// Delete "setup and loop" task
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}
우선순위 반전

우선순위 대체의 개념은 우선순위가 낮은 작업이 임계 구역에 진입하면 우선순위가 높은 작업이 이를 선점한다는 것입니다. 그러나 어느 시점에서 우선순위가 높은 작업이 잠금을 획득하려고 하는데 해당 잠금이 이미 우선순위가 낮은 작업에 의해 점유되어 있는 경우, 우선순위가 높은 작업은 먼저 CPU를 차단하고 반환해야 합니다. 반대로, 우선순위가 낮은 작업은 잠금이 해제될 때까지 실행 권한을 획득할 수 있으며, 잠금이 해제된 후에는 우선순위가 높은 작업이 해당 잠금을 사용할 수 있습니다. 하드웨어 인터럽트는 이 경우 적용되지 않습니다.
이를 방지하려면 여러 개의 코어를 활용하거나 중요 섹션을 전혀 사용하지 않는 것이 좋습니다.

문제 중 하나는 우선순위가 낮은 작업이 성공적으로 잠금을 선점하더라도, 잠금이 반환되기 전에 우선순위가 중간인 작업이 선점될 수 있다는 것입니다. 우선순위가 중간인 작업의 실행 시간이 매우 긴 경우, 우선순위가 중간인 작업이 실행되어 우선순위가 높은 작업이 선점되는 것을 방지할 수 있습니다.
한 가지 해결책은 우선순위 상속입니다. 이 접근법은 우선순위가 낮은 작업이 임계 영역을 장시간 점유하는 것을 방지하고 임계 영역 실행 오류를 방지합니다. 이 방법은 우선순위가 높은 작업이 선점하려고 할 때, 우선순위가 낮은 작업이 먼저 작업을 완료한 후에 우선순위가 높은 작업이 임계 영역에 진입하도록 허용하는 방식을 사용합니다.

멀티코어 프로세서
AMP 대 SMP : 주요 차이점은 비대칭 아키텍처에서는 스케줄링과 작업 배포를 담당하는 메인 코어가 있는 반면, 대칭 아키텍처에서는 각 코어가 다른 코어를 확인하고 균형을 맞춘다는 것입니다.


디버그 도구를 사용하여 내부 작업 동작을 관찰합니다.
참조
참고한 문서의 링크를 따라가려면 아래를 참고하세요.
FreeRTOS on ESP32
https://hackmd.io/@RainbowEye0486/S1zxtaQ9_
FreeRTOS on ESP32 - HackMD
# FreeRTOS on ESP32 contributed by<`RainbowEye0486`> ###### tags: `Side Project` --- ## 硬體規格 ###
hackmd.io
'ESP32' 카테고리의 다른 글
| ESP32 FreeRTOS 튜토리얼 가이드 6부작 6 (0) | 2025.11.21 |
|---|---|
| FreeRTOS를 사용한 ESP32: 소프트웨어 타이머/타이머 인터럽트 (0) | 2025.11.21 |
| ESP32 FreeRTOS 튜토리얼 가이드 6부작 5 (1) | 2025.11.21 |
| ESP32 ESP-NOW 양방향 커뮤니케이션 (0) | 2025.11.20 |
| esp32 및 esp8266 Dallas ds18b20 네트워크 연결 (0) | 2025.11.19 |
| ESP32 FreeRTOS 튜토리얼 가이드 6부작 4 (1) | 2025.11.19 |
| ESP32 FreeRTOS 튜토리얼 가이드 6부작 3 (0) | 2025.11.19 |
| ESP32 ADXL345 Accelerometer Interfacing (0) | 2025.11.18 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
지난 30년 여정, 캐어랩이 얻은 모든 것을 함께 나누고 싶습니다.
귀사가 성공하기까지의 긴 고난의 시간을 캐어랩과 함께 하세요.
캐어랩