본문 바로가기

자동차 속도 기반 IoT 장식 만들기 09 - FreeRTOS 구조 상세 설명

@밀양박씨!2026. 6. 1. 05:27

자동차에 장착하는 카피바라 대시보드, Carpybara를 만들면서 가장 고민했던 부분 중 하나가 동시성 처리였습니다.

ESP32 하나로 이 네 가지를 동시에 해야 했습니다:

 

 

  • GPS에서 실시간 속도 받기
  • IMU로 브레이크 / 과속방지턱 감지
  • 2.8인치 LCD에 카피바라 애니메이션 렌더링
  • WiFi AP 열고 폰에 JSON API 응답하기

 

 

단순히 loop() 하나에 다 우겨넣으면 WiFi 응답 처리 중 화면이 멈추고, GPS 파싱 중 HTTP가 늦어집니다. 이걸 해결한 게 FreeRTOS입니다.

ESP32의 듀얼코어 구조

ESP32-S3는 Xtensa LX7 코어가 물리적으로 2개입니다.

  Core 0 (PRO_CPU) Core 1 (APP_CPU)
기본 용도 WiFi/BT 스택 사용자 앱
Arduino loop() 실행 위치
WiFi 드라이버 인터럽트 여기서 처리 영향 없음

WiFi 스택이 Core 0을 간헐적으로 선점하기 때문에, 레이턴시에 민감한 렌더링 작업은 Core 1에 두는 게 핵심입니다.

 

 

3태스크 아키텍처

Carpybara 펌웨어는 FreeRTOS 태스크를 3개로 분리했습니다.

20 Hz                           ~25 fps
  │     GPS NMEA 파싱                   ILI9341 SPI 렌더링
  │     IMU 가속도 읽기                 카피바라 애니메이션
  │     brake/bump 이벤트 감지          HUD 속도 표시
  │
  └── NetworkTask (Priority 1)
        WiFi AP 유지
        HTTP 클라이언트 처리
        /api/state JSON 응답

DisplayTask를 Core 1에 단독 배치한 이유가 중요합니다. HTTP 요청 처리로 Core 0이 잠깐 블로킹되어도 화면 렌더링은 독립적으로 계속 돌아가야 하기 때문입니다.

 

 

 

 

// setup() 안에서 태스크 3개 생성
  xTaskCreatePinnedToCore(
      petSensorTaskFn, "Sensor",
      4096,     // 스택 4KB
      nullptr, 2, nullptr,
      0         // Core 0
  );

  xTaskCreatePinnedToCore(
      petNetworkTaskFn, "Network",
      8192,     // HTTP 버퍼 필요 → 스택 8KB
      nullptr, 1, nullptr,
      0         // Core 0 (WiFi 친화)
  );

  xTaskCreatePinnedToCore(
      petDisplayTaskFn, "Display",
      4096,
      nullptr, 2, nullptr,
      1         // Core 1 (렌더링 전용)
  );

  // Arduino loop()는 완전히 비움
  vTaskDelete(nullptr);

setup() 끝에서 vTaskDelete(nullptr)로 Arduino의 loopTask를 삭제합니다. 이후 loop() 함수는 빈 상태로 두고, 모든 실행 흐름은 FreeRTOS 스케줄러가 전담합니다.

 

 

 

공유 데이터와 Mutex

세 태스크가 공통으로 읽고 쓰는 데이터가 있습니다.

struct SensorState {
      float    speed_mph;
      double   lat, lon;
      uint8_t  satellites;
      bool     gps_valid;
      float    accel_x, accel_y, accel_z;
      bool     brake_active;
      bool     bump_active;
      uint32_t last_brake_ms;
      uint32_t last_bump_ms;
  };

SensorTask는 쓰고, NetworkTaskDisplayTask는 읽습니다. 멀티코어에서 아무 보호 없이 이러면 어떤 일이 생길까요?

 

 

Mutex 없는 경우의 문제

SensorTask:  speed_mph = 42.7f  ← float 4바이트 쓰기 시작
                   [1바이트 씀]
  NetworkTask:     speed_mph 읽기  ← 절반만 쓰인 쓰레기값!
                   → JSON에 0.00 또는 이상한 값이 나감
  SensorTask:  [나머지 3바이트 씀]
float 하나도 CPU는 여러 명령어로 씁니다. 코어가 다르면 정확히 그 사이에 다른 태스크가 끼어들 수 있습니다.

 

 

Mutex로 해결

// 초기화
  static SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

  // SensorTask — 쓰기 전 잠금
  xSemaphoreTake(mutex, portMAX_DELAY);
  state.speed_mph = (float)gps.speed.mph();
  state.gps_valid = gps.location.isValid();
  xSemaphoreGive(mutex);

  // NetworkTask — 읽기 전 잠금
  inline void petSensorRead(SensorState* out) {
      xSemaphoreTake(mutex, portMAX_DELAY);
      *out = state;   // 전체 구조체 스냅샷 복사
      xSemaphoreGive(mutex);
  }

Take로 잠그면 다른 태스크는 Give가 불릴 때까지 대기합니다. 임계 구역이 짧아서 체감 지연은 없습니다.

 

 

 

GPS + IMU 센서 태스크 (20 Hz)

static void task(void*) {
      for (;;) {
          // 1) GPS NMEA 문장 파싱
          while (Serial2.available())
              gps.encode(Serial2.read());

          if (gps.speed.isUpdated()) {
              xSemaphoreTake(mutex, portMAX_DELAY);
              state.speed_mph  = (float)gps.speed.mph();
              state.satellites = (uint8_t)gps.satellites.value();
              xSemaphoreGive(mutex);
          }

          // 2) IMU 가속도 읽기 + 이벤트 감지
          if (mpuOk) {
              sensors_event_t a, g_ev, temp;
              mpu.getEvent(&a, &g_ev, &temp);
              float ax = a.acceleration.x / 9.81f;
              float az = a.acceleration.z / 9.81f;

              checkBrake(ax);  // 전후 가속도로 브레이크 판정
              checkBump(az);   // 수직 가속도로 과속방지턱 판정
          }

          vTaskDelay(pdMS_TO_TICKS(50));  // 50ms = 20 Hz
      }
  }

vTaskDelay가 중요합니다. delay(50)은 CPU를 점유하며 기다리지만, vTaskDelay는 해당 태스크를 블로킹 상태로 전환해 스케줄러가 다른 태스크에 CPU를 줍니다.

 

 

 

 

브레이크 감지 로직

단순히 "가속도가 임계값을 넘었다"로 판정하면 노이즈에 민감합니다. 80ms 지속 확인 후 래치 방식을 씁니다.

static void checkBrake(float ax_g) {
      uint32_t now = millis();
      if (ax_g < -IMU_BRAKE_G) {        // -0.35g 미만 (전방 감속)
          if (!brakeCandidate) {
              brakeCandidate = true;
              brakeStartMs   = now;
          } else if (now - brakeStartMs >= IMU_BRAKE_MS) {  // 80ms 지속
              xSemaphoreTake(mutex, portMAX_DELAY);
              state.brake_active  = true;
              state.last_brake_ms = now;
              xSemaphoreGive(mutex);
          }
      } else {
          brakeCandidate = false;       // 임계값 벗어나면 리셋
      }
      // 2초 후 자동 해제
      xSemaphoreTake(mutex, portMAX_DELAY);
      if (state.brake_active && millis() - state.last_brake_ms > EVENT_LATCH_MS)
          state.brake_active = false;
      xSemaphoreGive(mutex);
  }

 

임계값은 pet_config.h에서 조정 가능합니다:

constexpr float    IMU_BRAKE_G    = 0.35f;  // g 단위, 낮추면 민감
  constexpr float    IMU_BUMP_G     = 1.8f;   // 과속방지턱 높이에 따라
  constexpr uint32_t IMU_BRAKE_MS   = 80;     // debounce 시간 (ms)
  constexpr uint32_t EVENT_LATCH_MS = 2000;   // 이벤트 유지 시간 (ms)

 

 

 

속도 → 캐릭터 상태 매핑

const char* state =
      s.brake_active          ? "brake"   // IMU 이벤트 최우선
    : (spd <= 0)              ? "sleep"
    : (spd < SPEED_RUN_MIN)   ? "walk"    // 1~29 mph
    : (spd < SPEED_TURBO_MIN) ? "run"     // 30~54 mph
                              : "turbo";  // 55+ mph

이 상태값이 /api/state JSON에 포함되어 폰 앱으로 전송되면, 앱에서 해당 애니메이션(mp4/webp)을 재생합니다.

 

 

 

전체 데이터 흐름


  GPS (UART2, 115200)
    └─ SensorTask ─┬─ speed, lat, lon, sats
  IMU (I2C)        │   brake_active, bump_active
    └─ SensorTask ─┘
                    │  [Mutex 보호]
                    ├──► NetworkTask ─► GET /api/state ─► 폰 앱 (1Hz 폴링)
                    └──► DisplayTask ─► ILI9341 LCD HUD + 카피바라 애니메이션

마치며

FreeRTOS를 쓰기 전에는 loop() 안에 GPS 읽기, 화면 그리기, HTTP 처리를 순서대로 넣었습니다. WiFi 응답이 늦으면 화면이 버벅이고, 화면 렌더링이 길면 HTTP 타임아웃이 났습니다.

태스크로 분리하고 나서 세 가지가 해결됐습니다:

  • DisplayTask는 WiFi 처리 중에도 끊김 없이 렌더링
  • SensorTask는 20Hz 고정 주기로 안정적인 IMU 샘플링
  • NetworkTask는 HTTP 블로킹이 다른 태스크에 영향 없음

핵심은 두 가지입니다:

  1. 어느 코어에 배치할지 — 렌더링은 Core 1, 네트워크는 WiFi와 같은 Core 0
  2. 공유 데이터는 반드시 Mutex — 멀티코어에서 보호 없는 공유 구조체는 반드시 문제가 됩니다

 

 

 

ESP32 FreeRTOS 임베디드 멀티태스킹 Mutex GPS MPU6050 Arduino PlatformIO Carpybara
밀양박씨!
@밀양박씨! :: 박씨의 개발블로그

lovebotw049 님의 블로그 입니다.

목차