ESP32-S3 WiFi/BLE 공존 시스템 설계

IoTBLEWiFiEmbeddedFreeRTOS

ESP32-S3는 WiFi와 BLE가 하나의 2.4GHz 라디오를 공유한다. 아무 설정 없이 둘을 동시에 켜면, WiFi가 라디오를 점유하는 동안 BLE 연결이 끊기고, BLE가 응답하는 동안 WiFi 패킷이 유실된다. 로봇 펌웨어에서 BLE로 사용자 명령을 받으면서 WiFi로 클라우드에 데이터를 올리는 구조를 안정적으로 운용하기 위해 구현한 것들을 정리한다.


문제 상황

로봇 디바이스(Ceily, Wally)의 통신 구조:

[모바일 앱] ←── BLE ──→ [ESP32-S3] ←── WiFi ──→ [AWS IoT]

                         [모터 RS485]
  • BLE: 사용자 명령 수신, 상태 알림, WiFi 설정 변경
  • WiFi: MQTT를 통한 텔레메트리 업로드, OTA 다운로드, 디바이스 섀도우
  • RS485(Modbus): 서보모터 제어 (100ms 주기)

세 가지가 동시에 동작해야 한다. WiFi OTA 중에도 BLE 연결이 살아있어야 하고, 모터 제어 중에도 WiFi beacon timeout이 발생하면 안 된다.


1. ESP-IDF 소프트웨어 공존 활성화

ESP32-S3의 소프트웨어 공존(software coexistence)은 라디오 시분할을 자동으로 관리한다.

# sdkconfig.coex
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y

이것만으로 기본적인 시분할은 동작한다. 하지만 기본값만으로는 부족하다. 추가 최적화 없이는 WiFi AMPDU 전송 중 BLE connection event가 밀리거나, BLE advertising 중 WiFi beacon 수신을 놓친다.


2. Coexistence Manager

공존 관련 설정을 한 곳에서 관리하는 전용 컴포넌트를 만들었다.

/* coex_manager.h */
typedef enum {
    COEX_MODE_BALANCED,        /* 평시: 균등 분배 */
    COEX_MODE_WIFI_PRIORITY,   /* OTA, 데이터 동기화 */
    COEX_MODE_BLE_PRIORITY,    /* 사용자 명령 처리 */
} coex_mode_t;

초기화

/* coex_manager.c */
esp_err_t coex_manager_init(void) {
    coex_mutex = xSemaphoreCreateMutex();

    /* 균형 모드로 시작 */
    esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);

    /* WiFi/BLE 상태 비트 설정 — 공존 스케줄러에 양쪽 활성 알림 */
    esp_coex_status_bit_set(ESP_COEX_ST_TYPE_WIFI, ESP_COEX_ST_TYPE_BLE);

    apply_ble_optimizations();
    apply_wifi_optimizations();
}

동적 우선순위 전환

상황에 따라 라디오 시간 배분을 바꾼다.

esp_err_t coex_manager_set_priority(coex_priority_t priority) {
    xSemaphoreTake(coex_mutex, portMAX_DELAY);

    switch (priority) {
        case COEX_PRIORITY_BLE:
            esp_coex_preference_set(ESP_COEX_PREFER_BT);
            break;
        case COEX_PRIORITY_WIFI:
            esp_coex_preference_set(ESP_COEX_PREFER_WIFI);
            break;
        case COEX_PRIORITY_BALANCED:
            esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);
            break;
    }

    xSemaphoreGive(coex_mutex);
}

실제 사용:

/* OTA 시작 시 */
coex_manager_set_priority(COEX_PRIORITY_WIFI);
/* OTA 완료 후 */
coex_manager_set_priority(COEX_PRIORITY_BALANCED);

3. WiFi 최적화

20MHz 대역폭 강제

가장 효과가 큰 설정이다. 40MHz(HT40)를 쓰면 WiFi가 2.4GHz 대역의 절반을 점유해서 BLE 채널과 심각하게 충돌한다.

esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT20);

HT40 → HT20으로 바꾸자 BLE 연결 끊김 빈도가 현저히 줄었다. WiFi throughput은 절반으로 떨어지지만, OTA 다운로드와 MQTT 텔레메트리에는 충분하다.

AMPDU 비활성화

CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n

AMPDU(Aggregated MAC Protocol Data Unit)는 여러 패킷을 묶어 전송한다. throughput은 좋지만 라디오를 길게 점유하므로 BLE connection event를 놓칠 확률이 높아진다. 비활성화하면 WiFi가 짧은 단위로 라디오를 쓰고 반납한다.

버퍼 축소

CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4     # 기본 10
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=8    # 기본 32

WiFi 버퍼를 줄이면 WiFi 드라이버가 한 번에 처리하는 패킷 수가 줄어들고, 라디오 점유 시간이 짧아진다. RAM 절약 효과도 있다.

Power Save 모드

/* WiFi 초기화 시 */
esp_wifi_set_ps(WIFI_PS_MAX_MODEM);

/* 공존 매니저에서 추가 최적화 시 */
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);

MIN_MODEM은 DTIM beacon 때만 깨어나므로 라디오를 가장 많이 비운다. BLE에 시간을 더 많이 줄 수 있지만 WiFi 응답 지연이 늘어난다.

TX Power 제한, Listen Interval 증가

int8_t power = 15;  /* 15dBm (최대 20dBm에서 줄임) */
esp_wifi_set_max_tx_power(power);

wifi_config.sta.listen_interval = 3;  /* 3 beacon마다 수신 */

TX power를 낮추면 인접 채널 간섭이 줄어든다. Listen interval을 늘리면 beacon 수신 빈도가 줄어 BLE에 시간을 더 준다.


4. BLE 최적화

연결 파라미터

BLE 연결 파라미터 설정의 핵심:

struct ble_gap_upd_params conn_params = {
    .itvl_min = 24,              /* 30ms */
    .itvl_max = 40,              /* 50ms */
    .latency = 0,
    .supervision_timeout = 400,  /* 4초 */
};
  • 30~50ms interval: 7.5ms로 줄이면 WiFi 간섭이 심해지고, 100ms로 늘리면 모터 제어 응답이 느려진다
  • latency 0: 전원 연결 디바이스이므로 전력 절약 불필요. 모든 interval에 응답
  • supervision timeout 4초: WiFi OTA 중 BLE가 잠시 밀려도 연결 유지

Modem Sleep, TX Power

/* BLE idle 시 라디오 해제 → WiFi가 사용 가능 */
esp_bt_sleep_enable();

/* TX Power P6 (14dBm) — WiFi 15dBm과 균형 */
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P6);

단일 연결, 버퍼 최소화

CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=8
CONFIG_BT_NIMBLE_SLEEP_ENABLE=y

연결 수를 1로 제한하면 BLE 스케줄링이 단순해지고 라디오 점유 시간이 예측 가능해진다.

NimBLE 태스크 우선순위

xTaskCreate(nimble_host_task, "NimBLE Host", 8 * 1024, NULL, 5, &handle);

우선순위를 5로 설정했다. 처음에 10으로 설정했을 때 WiFi 태스크가 굶주려서 beacon timeout이 발생했다. 5로 낮추자 WiFi와 BLE 양쪽 태스크가 균등하게 실행됐다.


5. Modbus 프레임 간 딜레이

모터 제어는 RS485(Modbus RTU)로 100ms마다 수행한다. Modbus 트랜잭션 중 UART가 블로킹으로 동작하므로, 연속 프레임 전송 시 CPU가 Modbus에 붙잡혀 WiFi/BLE 처리가 밀린다.

/* main.c */
modbus_lite_set_frame_delay(15);  /* 프레임 사이 15ms 딜레이 */
/* modbus_lite.c — 프레임 전송 전후에 딜레이 삽입 */
if (s_modbus.frame_delay_ms > 0) {
    vTaskDelay(pdMS_TO_TICKS(s_modbus.frame_delay_ms));
}

15ms 딜레이 동안 FreeRTOS 스케줄러가 WiFi/BLE 태스크를 실행할 수 있다. 이 딜레이 없이는 4개 모터에 연속 Modbus 요청을 보내는 동안 WiFi beacon timeout이 발생했다.


6. 초기화 순서

WiFi와 BLE의 초기화 순서가 중요하다.

/* network_manager.c */
connectivity_init_wifi();              /* 1. WiFi 먼저 */
connectivity_configure_coexistence();  /* 2. 공존 설정 (WiFi 안정화 100ms 대기 포함) */
connectivity_init_ble(name, profile);  /* 3. BLE 나중에 */

WiFi를 먼저 초기화하는 이유: WiFi 드라이버가 라디오를 점유한 상태에서 공존 설정을 적용해야 BLE 초기화 시 충돌이 없다. 순서를 바꾸면 BLE advertising 중에 WiFi가 초기화되면서 첫 연결이 불안정해진다.

공존 설정 후 100ms 대기:

/* connectivity.c */
coex_manager_configure_wifi();
vTaskDelay(pdMS_TO_TICKS(100));  /* WiFi 설정 안정화 */
coex_manager_set_mode(COEX_MODE_WIFI_PRIORITY);

7. WiFi 재연결 — Exponential Backoff

WiFi가 끊기면 즉시 재연결을 시도하되, 실패할 때마다 대기 시간을 2배로 늘린다.

static uint32_t s_backoff_ms = 1000;
#define MAX_BACKOFF_MS 60000

/* 연결 끊김 이벤트 */
vTaskDelay(pdMS_TO_TICKS(s_backoff_ms));
esp_wifi_connect();
s_backoff_ms = (s_backoff_ms * 2 > MAX_BACKOFF_MS)
    ? MAX_BACKOFF_MS : s_backoff_ms * 2;

/* 연결 성공 시 리셋 */
s_backoff_ms = 1000;

재연결 시도 자체가 라디오를 점유한다. backoff 없이 즉시 재시도하면 BLE 연결까지 같이 끊어진다. 1초 → 2초 → 4초 → … → 60초로 간격을 벌리면 BLE에 충분한 라디오 시간을 확보할 수 있다.

인증 실패(잘못된 비밀번호)는 재시도해도 소용없으므로 즉시 중단하고 BLE로 사용자에게 알린다:

if (reason == WIFI_REASON_AUTH_FAIL ||
    reason == WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT) {
    s_auth_failed = true;
    return;  /* 재시도 중단, BLE 알림 전송 */
}

8. PHY 자동 전환

RSSI 기반 PHY 전환으로 거리에 따라 BLE 1M ↔ Coded S8을 자동 전환한다. 직접적인 공존 기능은 아니지만, BLE 연결 안정성이 높아지면 supervision timeout으로 인한 재연결이 줄어들고, 재연결 과정에서의 라디오 경쟁이 감소한다.


효과 요약

구현효과비용
SW 공존 활성화라디오 시분할 자동 관리없음 (필수)
20MHz 대역폭 강제BLE 끊김 빈도 현저히 감소WiFi throughput 절반
AMPDU 비활성화BLE connection event 유실 감소WiFi burst throughput 감소
동적 우선순위 전환OTA 시 WiFi 속도 확보, 조작 시 BLE 반응성 확보전환 로직 필요
BLE interval 30~50ms모터 제어 응답 확보 + WiFi 간섭 최소화최저 응답 30ms
Supervision timeout 4초WiFi 부하 중 BLE 유지실제 끊김 감지 4초 지연
Modbus 프레임 딜레이 15ms모터 제어 중 WiFi beacon timeout 방지모터 응답 15ms 지연
NimBLE 우선순위 5WiFi/BLE 태스크 균등 실행
WiFi exponential backoff재연결 중 BLE 보호재연결 최대 60초 지연

메모

  • 40MHz → 20MHz가 가장 효과적이었다. 다른 모든 최적화를 적용해도 40MHz를 쓰면 BLE 끊김이 발생했다. 20MHz로 바꾸는 것만으로 대부분의 문제가 해결됐다
  • AMPDU 비활성화의 trade-off: OTA 다운로드 속도가 떨어진다. OTA 시에만 AMPDU를 켜는 방법도 가능하지만, 런타임에 AMPDU를 토글하면 WiFi 재연결이 필요해서 적용하지 않았다
  • WiFi Power Save 모드 선택: MIN_MODEM이 공존에 가장 유리하지만, MQTT keepalive 패킷이 지연될 수 있다. MAX_MODEM으로 시작하고, 공존 매니저에서 필요 시 MIN_MODEM으로 전환하는 방식을 사용했다
  • 안테나 분리로는 해결 안 된다: ESP32-S3는 단일 라디오이므로 안테나를 분리해도 시분할 문제는 동일하다. 하드웨어로 해결하려면 nRF52 같은 BLE 전용 칩을 추가해야 한다
  • BLE와 WiFi를 시간적으로 분리하는 방식(WiFi on/off)은 검토 후 기각했다. WiFi 재연결에 수 초, MQTT 재연결까지 포함하면 ~5초가 걸려서 주기적 텔레메트리 업로드가 불가능하다