ESP32 BLE 스택을 OTA 시 해제하고 복원하기

BLEIoTEmbeddedOTA

ESP32-S3에서 HTTPS OTA는 TLS 핸드셰이크만으로 약 40KB 힙을 소모한다. BLE 스택(NimBLE)이 43~53KB를 점유하고 있으므로, OTA 전에 BLE를 완전히 해제하면 충분한 메모리를 확보할 수 있다.

구조

OTA 메모리 매니저에 BLE를 모듈로 등록한다. OTA가 시작되면 매니저가 등록된 모듈을 순서대로 해제한다.

ota_memory_module_t ble_module = {
    .name = "BLE",
    .get_memory_usage = ble_get_memory_usage,  // 약 43KB 반환
    .deinit = ble_deinit,
    .reinit = ble_reinit,
    .can_recover = true
};
ota_memory_register_module(&ble_module);

can_recover = true이므로 OTA 완료(성공/실패 모두) 후 reinit이 호출된다.

해제 순서

BLE 스택 해제에는 엄격한 순서 제약이 있다.

esp_err_t DeinitBleComs(void) {
  // 1. 광고 중지 + 연결 해제
  ble_gap_adv_stop();
  ble_gap_conn_cancel();

  // 2. NimBLE 호스트 루프 종료
  nimble_port_stop();

  // 3. 호스트 태스크 종료 대기 (최대 1초)
  int wait_count = 0;
  while (g_ble_state.host_task_handle != NULL && wait_count < 20) {
    vTaskDelay(pdMS_TO_TICKS(50));
    wait_count++;
  }
  if (g_ble_state.host_task_handle != NULL) {
    vTaskDelete(g_ble_state.host_task_handle);  // 강제 종료
  }

  // 4. NimBLE 포트 해제 (메모리 풀 반환)
  nimble_port_deinit();

  // 5. BT 컨트롤러 해제
  esp_bt_controller_disable();
  esp_bt_controller_deinit();
  esp_bt_mem_release(ESP_BT_MODE_BLE);

  return ESP_OK;
}

3번(태스크 종료 대기)을 건너뛰면 4번에서 크래시가 발생한다. nimble_port_run()이 아직 실행 중인 상태에서 메모리 풀을 해제하기 때문이다. 강제 종료(vTaskDelete)는 정상 종료 실패 시 fallback이다.

복원

저장해둔 설정으로 역순 초기화한다.

esp_err_t ReinitBleComs(void) {
  esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
  esp_bt_controller_init(&bt_cfg);
  esp_bt_controller_enable(ESP_BT_MODE_BLE);

  // 최초 초기화와 동일한 함수 재호출
  InitBleComs(g_ble_state.device_name, g_ble_state.profile);
  return ESP_OK;
}

g_ble_state에 device_name과 GATT profile 포인터를 보관해두므로 복원 시 별도 인자가 필요 없다.

메모리 내역

항목크기
NimBLE 호스트 태스크 스택8KB
NimBLE 버퍼/구조체약 15KB
BT 컨트롤러약 20KB
합계약 43KB

esp_get_free_heap_size()로 해제 전후를 비교해 실제 회수량을 로그로 확인한다.

장단점

장점:

  • 추가 하드웨어 없이 약 43KB 힙 확보
  • OTA 성공률이 높아진다. HTTPS OTA에서 메모리 부족으로 TLS 핸드셰이크 실패하는 문제 해결
  • 모듈 등록 패턴이므로 BLE 외 다른 모듈(태스크 매니저 등)도 같은 방식으로 추가 가능

단점:

  • OTA 중 BLE 통신 불가. 모바일 앱에서 OTA 진행 상태를 BLE로 받을 수 없다
  • 복원 시 기존 bonding 정보가 유지되지만, 모바일 앱은 재연결이 필요하다
  • 해제/복원 과정에 약 500ms 소요

대안 검토

BLE 유지하면서 OTA:

  • WiFi만으로 OTA를 수행하고 BLE는 유지하는 방식
  • 메모리가 충분한 칩(PSRAM 탑재)에서는 가능하지만, 내부 SRAM만 사용하는 구성에서는 힙 부족 발생
  • BLE로 OTA 진행률을 전송할 수 있다는 장점이 있으나, 이 프로젝트에서는 PSRAM 없이 동작해야 하므로 채택하지 않았다

HTTP(비암호화) OTA:

  • TLS 없이 HTTP로 OTA하면 약 40KB를 절약할 수 있어 BLE 해제가 불필요
  • 보안 문제로 프로덕션에서는 사용하지 않는다. 개발 단계에서는 HTTP OTA + BLE 유지 조합으로 디버깅 편의성을 확보했다

NimBLE 버퍼 축소:

  • CONFIG_BT_NIMBLE_ACL_BUF_COUNT, CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT 등을 줄여 상주 메모리를 낮추는 방식
  • 이미 최소 수준으로 설정했으나(ACL_BUF_COUNT=8, MAX_CONNECTIONS=1), 그래도 OTA에 충분하지 않았다