BLE 연결 파라미터 제어와 WiFi 공존 설정
ESP32-S3는 BLE와 WiFi가 하나의 2.4GHz 라디오를 공유한다. BLE 연결 파라미터를 잘못 설정하면 WiFi 처리량이 떨어지고, WiFi가 바쁘면 BLE 연결이 끊어진다. 연결 파라미터 양방향 제어와 소프트웨어 공존 설정으로 해결했다.
Client 연결 과정
상태 머신
BLE 연결은 4단계 상태를 거친다.
NONE → ADVERTISING → CONNECTING → CONNECTED
| 상태 | 의미 | 전이 조건 |
|---|---|---|
NONE | 스택 미초기화 | NimBLE 초기화 완료 후 → ADVERTISING |
ADVERTISING | 연결 대기 중 | Central이 연결 요청 → CONNECTING |
CONNECTING | GAP 연결 수립됨, GATT 미완료 | Client가 characteristic에 subscribe → CONNECTED |
CONNECTED | 양방향 통신 가능 | 연결 끊김 시 → ADVERTISING |
CONNECTING과 CONNECTED의 구분이 중요하다. GAP 연결만으로는 데이터를 주고받을 수 없다. Client가 notify characteristic에 subscribe해야 비로소 CONNECTED 상태가 된다.
Advertising 설정
NimBLE 호스트가 BT 컨트롤러와 동기화되면 on_stack_sync 콜백이 호출되고, advertising이 시작된다.
static void start_advertising(void) {
struct ble_gap_adv_params adv_params = {
.conn_mode = BLE_GAP_CONN_MODE_UND, /* undirected connectable */
.disc_mode = BLE_GAP_DISC_MODE_GEN, /* general discoverable */
.itvl_min = 80, /* 100ms (80 × 1.25ms) */
.itvl_max = 120, /* 150ms (120 × 1.25ms) */
};
/* Advertising Data */
struct ble_hs_adv_fields fields = {
.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP,
.name = (uint8_t *)device_name,
.name_len = strlen(device_name),
.name_is_complete = 1,
.tx_pwr_lvl_is_present = 1,
.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO,
.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG,
};
ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, gap_event_handler, NULL);
}
BLE_GAP_CONN_MODE_UND: 어떤 Central이든 연결 가능BLE_HS_FOREVER: 타임아웃 없이 계속 advertising- advertising interval 100~150ms: 검색 속도와 라디오 점유의 균형. 짧으면 검색이 빠르지만 WiFi 시간을 뺏는다
연결 수립 (BLE_GAP_EVENT_CONNECT)
Client가 연결을 요청하면 BLE_GAP_EVENT_CONNECT 이벤트가 발생한다.
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
/* 연결 성공 */
ble_gap_conn_find(event->connect.conn_handle, &desc);
ChangeStatus(BLE_COMS_STATUS_CONNECTING);
/* 즉시 연결 파라미터 업데이트 요청 */
struct ble_gap_upd_params conn_params = {
.itvl_min = 24, /* 30ms */
.itvl_max = 40, /* 50ms */
.latency = 0,
.supervision_timeout = 400, /* 4초 */
.min_ce_len = 0,
.max_ce_len = 0,
};
ble_gap_update_params(event->connect.conn_handle, &conn_params);
/* Central의 GATT discovery 완료 대기 */
vTaskDelay(pdMS_TO_TICKS(300));
} else {
/* 연결 실패 → 재광고 */
start_advertising();
}
연결 직후의 처리:
- 상태를
CONNECTING으로 변경: 아직CONNECTED가 아니다. GATT subscribe가 오지 않았다 - 연결 파라미터를 즉시 요청: Central의 기본 파라미터(보통 iOS 30ms, Android 7.5~50ms)를 디바이스가 원하는 값으로 변경
- 300ms 대기: Central이 GATT service discovery를 수행하는 시간. 이 딜레이 없이 바로 데이터를 보내면 discovery와 충돌할 수 있다
GATT Subscribe (BLE_GAP_EVENT_SUBSCRIBE)
Client가 notify characteristic에 subscribe하면 CONNECTED 상태가 된다.
case BLE_GAP_EVENT_SUBSCRIBE:
ChangeStatus(BLE_COMS_STATUS_CONNECTED);
SetBleConnHandle(event->subscribe.conn_handle);
/* Device Status notify 활성화 확인 */
if (attr_handle == device_status_chr_val_handle) {
device_status_set_subscribed(event->subscribe.cur_notify);
}
이 시점부터:
- connection handle이 저장되어 다른 태스크에서 notify를 보낼 수 있다
- Device Status service가 10Hz(100ms)로 상태 알림을 시작한다
- BLE Logger가 이벤트 로그를 전송할 수 있다
연결 끊김 (BLE_GAP_EVENT_DISCONNECT)
case BLE_GAP_EVENT_DISCONNECT:
ResetBleConnHandle(); /* handle 초기화 */
device_status_set_subscribed(false); /* notify 중단 */
start_advertising(); /* 즉시 재광고 시작 */
연결이 끊기면 즉시 advertising을 재개한다. Client가 다시 연결할 수 있도록 대기하는 것이 재연결 지연을 최소화하는 방법이다.
MTU 교환
case BLE_GAP_EVENT_MTU:
/* 로깅만 수행. 별도 처리 없음 */
/* NimBLE가 MTU 교환을 자동 처리한다 */
기본 MTU는 23바이트이고, MTU 교환으로 최대 512바이트까지 늘릴 수 있다. NimBLE 스택이 자동으로 처리하므로 애플리케이션에서 별도 작업은 불필요하다.
전체 연결 시퀀스
[Mobile App] [ESP32-S3]
│ │
│ ← ADV packet (100~150ms) ── │ ADVERTISING
│ │
│ ── Connection Request ──→ │
│ │ CONNECTING
│ ← Connection Response ──── │
│ │ conn_params 요청 (30~50ms interval)
│ │ 300ms 대기
│ │
│ ── Service Discovery (GATT) ──→ │
│ ← Service List ──── │
│ │
│ ── MTU Exchange Request ──→ │
│ ← MTU Exchange Response ── │
│ │
│ ── Enable Notifications (Subscribe) → │ CONNECTED
│ │ device_status notify 시작 (10Hz)
│ │
│ ← Notify (status data) ── │
│ ── Write (motion command) ──→ │
│ ← Notify (log event) ── │
연결 파라미터 제어
접속 즉시 파라미터 요청
위 연결 과정에서 봤듯이, BLE_GAP_EVENT_CONNECT 시점에 즉시 파라미터 업데이트를 요청한다.
struct ble_gap_upd_params conn_params = {
.itvl_min = 24, /* 30ms (24 × 1.25ms) */
.itvl_max = 40, /* 50ms (40 × 1.25ms) */
.latency = 0, /* 모든 interval에 응답 */
.supervision_timeout = 400, /* 4초 (400 × 10ms) */
.min_ce_len = 0, /* connection event 길이 제한 없음 */
.max_ce_len = 0,
};
ble_gap_update_params(conn_handle, &conn_params);
BLE 연결 파라미터의 단위는 직관적이지 않다. itvl은 1.25ms 단위, supervision_timeout은 10ms 단위이다. 값 24는 24 × 1.25ms = 30ms를 의미한다.
Connection Interval
Central과 Peripheral이 데이터를 교환하는 주기이다. interval마다 한 번씩 connection event가 발생하고, 그 안에서 양쪽이 패킷을 주고받는다. BLE 스펙상 7.5ms ~ 4초 범위에서 설정할 수 있다.
interval = 50ms 일 때:
시간 ──→
0ms 50ms 100ms 150ms 200ms
├──CE──┤ ├──CE──┤ ├──CE──┤
│ tx/rx│ (idle) │ tx/rx│ (idle) │ tx/rx│
└──────┘ └──────┘ └──────┘
CE = Connection Event
interval이 짧을수록:
- 데이터 교환이 빈번해져 응답 지연이 줄어든다
- 라디오 점유 시간이 늘어나 WiFi에 줄 시간이 줄어든다
- 전력 소비가 증가한다
interval이 길수록:
- idle 구간이 길어져 WiFi나 다른 작업에 라디오를 양보할 수 있다
- 앱에서 write 요청을 보내면 다음 connection event까지 대기해야 한다. interval 100ms이면 최악의 경우 100ms를 기다린다
이 프로젝트에서 30~50ms를 선택한 이유: 모바일 앱에서 모터 제어 명령(activate, deactivate)을 보내면 최대 50ms 안에 디바이스가 수신한다. 사용자가 버튼을 누른 후 50ms 이내에 모터가 반응하므로 체감 지연이 없다. 7.5ms로 줄이면 1초에 133회 connection event가 발생해 WiFi가 굶주리고, 100ms로 늘리면 버튼 반응이 느려진다.
Slave Latency
Peripheral이 연속으로 건너뛸 수 있는 connection event 수이다. Central은 매 interval마다 폴링하지만, Peripheral은 보낼 데이터가 없으면 latency 횟수만큼 응답하지 않아도 된다.
interval = 50ms, latency = 0 일 때:
├──CE──┤ ├──CE──┤ ├──CE──┤ ├──CE──┤
│ 응답 │ │ 응답 │ │ 응답 │ │ 응답 │
매 interval 응답. 실질 응답 주기 = 50ms
interval = 50ms, latency = 2 일 때:
├──CE──┤ ├─skip─┤ ├─skip─┤ ├──CE──┤
│ 응답 │ │ │ │ │ │ 응답 │
3번 중 1번 응답. 실질 응답 주기 = 50ms × (2+1) = 150ms
latency가 높을수록:
- Peripheral이 라디오를 켜는 횟수가 줄어 전력 소비가 감소한다
- 보낼 데이터가 생겨도 다음 응답 차례까지 기다려야 한다. latency=2, interval=50ms이면 최악 150ms 지연
- WiFi 공존 관점에서는 BLE 라디오 점유가 줄어들어 유리하다
latency=0을 선택한 이유: 이 디바이스는 전원에 연결되어 있으므로 전력 절약이 불필요하다. Device Status를 10Hz(100ms)로 notify하고 있어서 매 connection event마다 보낼 데이터가 있다. latency를 올려도 실제로 skip되는 event가 거의 없다.
파라미터 값 요약
| 파라미터 | 값 | 이유 |
|---|---|---|
| interval | 30~50ms | 모터 제어 응답 시간 확보. 7.5ms는 WiFi 간섭, 100ms는 체감 지연 |
| latency | 0 | 전원 연결 + 10Hz notify로 skip할 event가 없다 |
| supervision timeout | 4초 | WiFi OTA 중 BLE가 밀려도 연결 유지 |
| ce_len | 0 | 컨트롤러 자동 결정. WiFi 공존 스케줄러가 동적 조절 |
Peripheral이 ble_gap_update_params()를 호출하면 L2CAP Connection Parameter Update Request가 Central로 전송된다. Central이 수락하면 양쪽 모두 새 파라미터로 전환된다. 거부하면 Central의 기본 파라미터가 유지된다.
상대방 파라미터 검증
Central(모바일 기기)이 파라미터 변경을 요청하면 검증 후 수락한다.
case BLE_GAP_EVENT_CONN_UPDATE_REQ: {
struct ble_gap_upd_params *peer = event->conn_update_req.peer_params;
struct ble_gap_upd_params *self = event->conn_update_req.self_params;
*self = *peer; /* 기본적으로 수락 */
/* supervision timeout 최소 2초 보장 */
if (self->supervision_timeout < 200) {
self->supervision_timeout = 200;
}
return 0;
}
iOS는 연결 후 자체적으로 파라미터를 변경한다. supervision timeout을 짧게 설정하는 경우가 있어 최소 2초를 강제한다. Apple의 BLE 가이드라인은 peripheral이 파라미터 업데이트를 수락할 것을 권장한다. 3회 연속 거부하면 iOS가 연결을 끊는 경우가 있다.
파라미터 업데이트 완료 확인
case BLE_GAP_EVENT_CONN_UPDATE:
ble_gap_conn_find(event->conn_update.conn_handle, &desc);
print_conn_desc(&desc); /* 변경된 파라미터 로깅 */
파라미터 업데이트가 완료되면 BLE_GAP_EVENT_CONN_UPDATE 이벤트가 발생한다. 실제로 적용된 파라미터를 ble_gap_conn_find()로 확인할 수 있다. Central이 요청을 수정해서 적용하는 경우도 있으므로 로깅으로 확인이 필요하다.
WiFi 공존
소프트웨어 공존 활성화
ESP-IDF의 소프트웨어 공존 매니저가 BLE와 WiFi의 라디오 사용을 조율한다.
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
동적 우선순위 전환
상황에 따라 BLE/WiFi 우선순위를 전환한다.
// OTA 다운로드 시: WiFi 우선
coex_manager_set_priority(COEX_PRIORITY_WIFI);
// 사용자 BLE 조작 시: BLE 우선
coex_manager_set_priority(COEX_PRIORITY_BLE);
// 평시: 균형 모드
coex_manager_set_priority(COEX_PRIORITY_BALANCED);
WiFi 최적화 설정
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4 # 기본 10 → 4
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=8 # 기본 32 → 8
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n # AMPDU 비활성화
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
AMPDU(패킷 묶음 전송)를 비활성화하면 WiFi가 라디오를 길게 점유하는 것을 방지한다. 처리량은 감소하지만 BLE와의 시간 분배가 개선된다.
BLE 최적화 설정
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 # 단일 연결만 허용
CONFIG_BT_NIMBLE_SLEEP_ENABLE=y # BLE 슬립 활성화
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=8 # ACL 버퍼 최소화
장단점
장점:
- BLE와 WiFi를 동시 사용하면서 양쪽 안정성 유지
- OTA 시 WiFi 우선으로 전환해 다운로드 속도 확보
- supervision timeout 검증으로 모바일 OS별 차이 흡수
단점:
- 소프트웨어 공존은 완벽하지 않다. 두 프로토콜이 동시에 무선 자원을 요구하면 한쪽은 지연된다
- AMPDU 비활성화로 WiFi 최대 처리량이 감소한다
- 동적 우선순위 전환을 호출하는 코드가 여러 곳에 분산된다
대안 검토
BLE 연결 파라미터를 고정으로 두기:
BLE_GAP_EVENT_CONN_UPDATE_REQ에서 무조건 거부- 장점: 예측 가능한 동작
- 단점: iOS가 파라미터 변경을 3회 거부당하면 연결을 끊는 경우가 있다. Apple의 BLE 가이드라인에서 peripheral이 파라미터 업데이트를 수락할 것을 권장한다
연결 파라미터를 넓게 잡기 (interval 100ms+):
- WiFi와의 충돌이 줄어들고 전력 소모도 감소한다
- 단점: 모터 제어 명령의 응답 지연이 100ms 이상으로 늘어난다. 사용자가 버튼을 누른 후 0.1초 이상 기다리는 것은 체감된다
BLE와 WiFi를 시간적으로 분리:
- BLE로 커맨드를 받을 때는 WiFi를 끄고, WiFi가 필요할 때만 켜는 방식
- 장점: 공존 문제가 원천적으로 없다
- 단점: WiFi 연결/해제에 수 초가 걸린다. MQTT 서버 재연결까지 포함하면 약 5초. 주기적 데이터 업로드가 불가능해진다
외부 안테나 분리:
- BLE와 WiFi에 각각 별도 안테나를 사용
- ESP32-S3는 단일 라디오이므로 안테나를 분리해도 시간 분할 문제는 동일하다. 하드웨어적으로 해결하려면 BLE 전용 칩(nRF52 등)을 추가해야 한다