Skip to content

ESP32 플랫폼 가이드

ESP32 마이크로컨트롤러용 Xylolabs SDK 통합 가이드이다.


지원 대상

MCU 코어 클록 RAM 연결 오디오 지원
ESP32-S3 2x Xtensa LX7 240 MHz 512 KB + 8 MB PSRAM WiFi 802.11b/g/n + BLE 5.0 4ch XAP @96kHz
ESP32-C3 RISC-V RV32IMC 160 MHz 400 KB WiFi 802.11b/g/n + BLE 5.0 센서 전용

하드웨어 설정

I2S 마이크 연결 (ESP32-S3)

SDK는 두 개의 스테레오 I2S MEMS 마이크(예: 공유 버스에서 좌/우 채널을 제공하는 INMP441 또는 ICS-43434 두 쌍)로부터 인터리브된 PCM을 입력받는다.

ESP32-S3 GPIO           MEMS 마이크 (예: INMP441)
──────────────────      ───────────────────────
GPIO14 (I2S_BCK) ─────  BCK
GPIO15 (I2S_WS)  ─────  WS / LRCK
GPIO16 (I2S_DIN) ─────  SD (데이터 입력)
3V3              ─────  VDD
GND              ─────  GND
GND              ─────  L/R (좌측 마이크)
3V3              ─────  L/R (우측 마이크)

두 스테레오 마이크 쌍으로 4채널 오디오를 구성하려면, 두 개의 I2S 주변장치(I2S0, I2S1)를 사용하거나 L/R 선택과 함께 단일 버스를 사용해 펌웨어에서 인터리브한다.

WiFi 설정

SDK는 TCP/TLS로 연결한다. WiFi 프로비저닝은 SDK 코어 외부에서 처리한다:

// Rust (Embassy) - 권장
use esp_wifi::wifi::{WifiController, WifiStaDevice};
use embassy_net::{Stack, Config};

// xylolabs 초기화 전에 WiFi STA 모드 초기화
let wifi = WifiController::new(&init, wifi, WifiStaDevice).unwrap();
wifi.set_configuration(&wifi::Configuration::Client(wifi::ClientConfiguration {
    ssid: "your_ssid".try_into().unwrap(),
    password: "your_password".try_into().unwrap(),
    ..Default::default()
})).unwrap();
wifi.start().await.unwrap();
wifi.connect().await.unwrap();
// DHCP 대기 후 진행
Legacy C equivalent
// xylolabs_init() 호출 전에 WiFi 초기화
wifi_init_sta("your_ssid", "your_password");
// IP 할당 대기 후 진행

프로덕션 배포에는 ESP-IDF의 esp_wifi 프로비저닝 컴포넌트 또는 BLE를 통한 WiFiProvisioning을 사용한다.

UART를 통한 LTE-M1 모뎀 (선택적)

WiFi 커버리지가 없는 배포 환경에서는 UART LTE 모뎀을 연결:

ESP32-S3 GPIO           LTE 모뎀 (BG770A)
──────────────────      ──────────────────────────
GPIO17 (UART1_TX) ───── RXD
GPIO18 (UART1_RX) ───── TXD
GPIO19 (GPIO)     ───── PWRKEY
GPIO20 (GPIO)     ───── RESET_N
5V / VBAT         ───── VCC
GND               ───── GND

센서 버스

버스 GPIO (S3 예시) 주요 장치
I2C0 GPIO8 (SDA), GPIO9 (SCL) BME280, LIS3DH, MPU6050
SPI2 GPIO11/12/13 + CS MAX31865, ADS1256
ADC1 GPIO1–GPIO10 아날로그 센서

Rust 빌드 (권장)

# ESP Rust 툴체인 설치
cargo install espup
espup install

# ESP32-S3 예제 빌드
cd sdk/examples/esp32s3-wifi
cargo build --release --target xtensa-esp32s3-none-elf

# 플래시
cargo espflash flash --release

모든 ESP32 예제는 sdk/examples/를 참고한다.


레거시 C 빌드 시스템

옵션 A: ESP-IDF (idf.py)

# ESP-IDF 설치 (S3 USB 지원을 위해 v5.2+ 권장)
. $IDF_PATH/export.sh

# 프로젝트 생성
idf.py create-project my_xylolabs_device
cd my_xylolabs_device

# SDK를 컴포넌트로 추가
mkdir -p components
ln -s /path/to/sdk/c/esp32 components/xylolabs_esp32
ln -s /path/to/sdk/c/common components/xylolabs_common

CMakeLists.txt (프로젝트 루트):

cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_xylolabs_device)

main/CMakeLists.txt:

idf_component_register(
    SRCS "main.c" "platform_esp32.c"
    INCLUDE_DIRS "."
    REQUIRES xylolabs_esp32 xylolabs_common esp_wifi esp_netif nvs_flash
)

옵션 B: PlatformIO

platformio.ini:

[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = espidf
build_flags =
    -DXYLOLABS_PLATFORM_ESP32
    -DXYLOLABS_PLATFORM_ESP32S3
lib_deps =
    symlink://path/to/sdk/c/esp32
    symlink://path/to/sdk/c/common


ESP32-S3의 장점

XAP 및 XMBP는 Xylolabs Inc.의 특허 출원 중인 기술이다.

ESP32-S3는 풀 오디오 + 센서 배포에 권장되는 ESP32 변형이다:

  • PSRAM (8 MB): 내부 SRAM 한계를 넘어서는 대용량 오디오 링 버퍼를 지원한다. XYLOLABS_AUDIO_RING_SIZE를 PSRAM에 매핑한다.
  • 듀얼 코어 아키텍처: FreeRTOS 태스크 고정을 통해 Core 0에서 오디오 인코딩을, Core 1에서 네트워크 I/O를 실행하여 TCP 재전송 중 오디오 글리치를 방지한다.
  • USB OTG: UART 어댑터 없이 시리얼 콘솔 및 펌웨어 플래시 지원.
  • 벡터 확장: XAP 스펙트럼 연산을 위한 가속 DSP.

PSRAM 오디오 링 버퍼

// Rust (Embassy) - 권장
use esp_hal::psram;

// 설정에서 PSRAM 활성화 (sdkconfig 또는 cargo features)
// esp-alloc을 통해 PSRAM에 오디오 링 버퍼 할당
const AUDIO_RING_SIZE: usize = 256 * 1024; // PSRAM의 256 KB

#[link_section = ".ext_ram.bss"]
static mut AUDIO_RING_MEM: [u8; AUDIO_RING_SIZE] = [0u8; AUDIO_RING_SIZE];
Legacy C equivalent
// sdkconfig 또는 idf.py menuconfig에서:
// Component config → ESP PSRAM → Support for external, SPI-connected RAM → Enable

// PSRAM을 사용하도록 링 버퍼 크기 오버라이드
#define XYLOLABS_AUDIO_RING_SIZE  (256 * 1024)  // PSRAM의 256 KB

// PSRAM에 할당 (ESP-IDF 속성)
static uint8_t s_audio_ring_mem[XYLOLABS_AUDIO_RING_SIZE]
    __attribute__((section(".ext_ram.bss")));

ESP32-C3: 센서 전용 노드

ESP32-C3 RISC-V 코어는 유용한 오디오 레이트에서 실시간 XAP 인코딩에 적합한 하드웨어 FPU가 없다. C3는 센서 전용 배포에 사용한다:

// Rust (Embassy) - 권장
// C3용 설정 오버라이드 — build.rs 또는 Cargo.toml features에서 설정
// C3에서는 오디오 없음 (RISC-V, FPU 없음)
const AUDIO_CHANNELS: usize = 0;
const XMBP_BUF_SIZE: usize = 4096;
const SENSOR_CHANNELS: usize = 4;
const MOTOR_CHANNELS: usize = 8;
Legacy C equivalent
// C3용 설정 오버라이드
#define XYLOLABS_AUDIO_CHANNELS    0   // 오디오 없음
#define XYLOLABS_XMBP_BUF_SIZE    4096
#define XYLOLABS_SENSOR_CHANNELS   4
#define XYLOLABS_MOTOR_CHANNELS    8

C3에 적합한 용도: - 환경 모니터링 (온도, 습도, CO2) - 진동 임계값 알림 (원시 ADC, XAP 인코딩 아님) - 딥 슬립을 사용하는 저전력 WiFi 센서 노드


FreeRTOS 태스크 아키텍처 (ESP32-S3)

네트워크 지터로 인한 오디오 캡처 손상을 방지하기 위해 오디오와 네트워크 태스크를 별도 코어에 고정한다:

// Rust (Embassy) - 권장
use esp_hal::i2s::I2s;
use embassy_executor::Spawner;

// 오디오 캡처 태스크 (Core 0에 고정)
#[embassy_executor::task]
async fn audio_task(i2s: I2s<'static>) {
    let mut buf = [0i16; 256 * 4];
    loop {
        let samples = i2s.read_async(&mut buf).await.unwrap();
        xylolabs_audio_feed(&buf[..samples]);
    }
}

// 네트워크 + SDK 틱 태스크 (Core 1에 고정)
#[embassy_executor::task]
async fn network_task(stack: Stack<'static>) {
    let mut client = XylolabsClient::new(stack);
    client.set_api_key("xk_your_key");
    client.set_device_id(1);
    client.session_create("esp32_node_1", &streams).await;

    loop {
        client.tick().await;
        Timer::after_millis(10).await;
    }
}
Legacy C equivalent
// 오디오 캡처 + 인코딩 태스크 (Core 0)
void audio_task(void *arg) {
    while (1) {
        // DMA 콜백이 s_i2s_buf를 채움
        i2s_read(I2S_NUM_0, s_i2s_buf, sizeof(s_i2s_buf), &bytes_read, portMAX_DELAY);
        xylolabs_audio_feed((int16_t *)s_i2s_buf, bytes_read / sizeof(int16_t));
    }
}

// 네트워크 + SDK 틱 태스크 (Core 1)
void network_task(void *arg) {
    xylolabs_init(&g_platform);
    xylolabs_set_api_key("xk_your_key");
    xylolabs_set_device_id(1);
    xylolabs_session_create("esp32_node_1", streams, stream_count);

    while (1) {
        xylolabs_tick();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void app_main(void) {
    // ...WiFi 초기화...
    xTaskCreatePinnedToCore(audio_task,   "audio",   4096, NULL, 5, NULL, 0);
    xTaskCreatePinnedToCore(network_task, "network", 8192, NULL, 4, NULL, 1);
}

WiFi vs LTE-M1

특성 WiFi (네이티브) LTE-M1 (UART 모뎀)
범위 실내 ~50–150 m 전국/전세계
지연 ~5–20 ms ~50–200 ms
소비 전력 활성 시 ~80–160 mA 활성 시 ~10–200 mA
설정 esp_wifi API UART AT 명령
사용 사례 WiFi AP가 있는 공장 현장 원격/야외 현장

두 연결 옵션 모두 동일한 SDK 전송 콜백을 사용한다. 플랫폼 구현만 다릅니다.


OTA 펌웨어 업데이트

ESP-IDF의 esp_https_ota 컴포넌트를 사용한 무선 업데이트:

// Rust (Embassy) - 권장
use esp_hal::reset::software_reset;
use esp_ota::OtaUpdate;

async fn ota_update(stack: Stack<'static>) {
    let url = "https://update.yourserver.com/firmware/esp32s3/latest.bin";
    let mut ota = OtaUpdate::begin().unwrap();
    // HTTPS에서 펌웨어를 스트리밍하여 OTA 파티션에 기록
    let mut client = HttpClient::new(stack);
    let response = client.get(url).await.unwrap();
    ota.write_all(response.body()).await.unwrap();
    ota.finalize().unwrap();
    software_reset();
}
Legacy C equivalent
#include "esp_https_ota.h"

void ota_task(void *arg) {
    esp_http_client_config_t config = {
        .url = "https://update.yourserver.com/firmware/esp32s3/latest.bin",
        .cert_pem = server_cert_pem_start,
    };
    esp_err_t ret = esp_https_ota(&config);
    if (ret == ESP_OK) {
        esp_restart();
    }
    vTaskDelete(NULL);
}

Xylolabs 대시보드에서 SSE 제어 채널을 통해 OTA를 트리거한다.


전력 관리

틱 사이 라이트 슬립

// Rust (Embassy) - 권장
// Embassy는 비동기 태스크 폴링 사이에 자동으로 라이트 슬립에 진입한다.
// WiFi 연결 유지; 타이머 또는 인터럽트로 CPU가 깨어난다.
loop {
    client.tick().await;
    Timer::after_millis(10).await; // 틱 사이에 CPU 슬립
}
Legacy C equivalent
// 자동 라이트 슬립 설정 (WiFi 연결 유지)
esp_pm_config_t pm_config = {
    .max_freq_mhz = 240,
    .min_freq_mhz = 40,
    .light_sleep_enable = true,
};
esp_pm_configure(&pm_config);

// SDK 틱은 10ms 간격으로 실행; CPU는 틱 사이에 자동으로 슬립

센서 전용용 딥 슬립 (C3)

// Rust (Embassy) - 권장
use esp_hal::sleep::{TimerWakeupSource, enter_deep_sleep};
use fugit::ExtU64;

// 60초마다 깨어나 센서 배치 전송 후 다시 슬립
let wakeup = TimerWakeupSource::new(60u64.secs());
// ... 센서 수집, client.tick().await, 플러시 ...
enter_deep_sleep(&[&wakeup]);
Legacy C equivalent
// 60초마다 깨어나 센서 배치 전송 후 다시 슬립
esp_sleep_enable_timer_wakeup(60 * 1000000ULL);  // 마이크로초 단위 60초
// ... 센서 수집, xylolabs_tick(), 플러시 ...
esp_deep_sleep_start();

ULP 코프로세서

C3 또는 S3 초저전력 시나리오에서는 ULP(Ultra-Low-Power) 코프로세서를 사용해 메인 CPU가 딥 슬립 상태에서 느린 센서(온도, 습도)를 샘플링한다. 임계값이 초과되거나 배치 간격이 만료될 때만 메인 CPU를 깨웁니다.


Rust (ESP32-S3에 권장)

Rust는 새로운 ESP32 프로젝트의 권장 언어이다. Embassy 기반 Rust 예제는 비동기 WiFi, I2S 오디오 캡처, 센서 스트리밍을 컴파일 타임 타입 안전성과 함께 구현한다.

cd sdk/examples/esp32s3-audio  # 또는 esp32c3-sensor
cargo build --release
# espflash를 통해 플래시: espflash flash target/xtensa-esp32s3-none-elf/release/esp32s3-audio

전체 아키텍처 개요와 대상별 빌드 지침은 sdk/examples/README.md를 참고한다.

주의: ESP32-C3 센서 전용 배포의 경우, Rust와 C 모두 잘 지원된다. 팀의 친숙도에 따라 선택한다.


C 예제 (대안) ### 예제: ESP32-S3 풀 오디오 + 센서 ### 플랫폼 구현
// platform_esp32s3.c
#include "xylolabs/client.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "lwip/sockets.h"

static int s_sock = -1;

static xylolabs_err_t esp32_tcp_connect(const char *host, uint16_t port) {
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(port),
    };
    inet_pton(AF_INET, host, &addr.sin_addr);

    s_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (s_sock < 0) return XYLOLABS_ERR_NETWORK;

    if (connect(s_sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
        close(s_sock);
        s_sock = -1;
        return XYLOLABS_ERR_NETWORK;
    }
    return XYLOLABS_OK;
}

static uint64_t esp32_get_time_us(void) {
    return (uint64_t)esp_timer_get_time();
}

static void esp32_sleep_ms(uint32_t ms) {
    vTaskDelay(pdMS_TO_TICKS(ms));
}

const xylolabs_platform_t g_platform = {
    .tcp_connect      = esp32_tcp_connect,
    .tcp_send         = esp32_tcp_send,
    .tcp_recv         = esp32_tcp_recv,
    .tcp_close        = esp32_tcp_close,
    .tcp_is_connected = esp32_tcp_is_connected,
    .get_time_us      = esp32_get_time_us,
    .sleep_ms         = esp32_sleep_ms,
    .watchdog_feed    = esp32_watchdog_feed,
};
### 메인 애플리케이션
// Rust (Embassy) - 권장
use xylolabs_sdk::{XylolabsClient, Config, StreamDef};
use xylolabs_hal_esp::EspPlatform;

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let peripherals = esp_hal::init(esp_hal::Config::default());
    let platform = EspPlatform::new(/* WiFi 설정 */);
    let mut client = XylolabsClient::<_, 65536, 16384>::new(platform, Config::default());

    client.set_api_key("xk_your_key_here");
    client.set_device_id(2);

    let streams = [
        StreamDef::new(0, "vibration_x", ValueType::F32, "g", 100.0),
        StreamDef::new(1, "temperature", ValueType::F32, "celsius", 1.0),
        StreamDef::new(2, "humidity", ValueType::F32, "percent", 1.0),
    ];
    client.session_create("esp32_s3_node", &streams).unwrap();

    loop {
        // I2S 캡처 + XAP 인코딩은 HAL에서 처리
        client.meta_feed_f32(0, read_accel_x().await).unwrap();
        client.meta_feed_f32(1, read_temp().await).unwrap();
        client.meta_feed_f32(2, read_humidity().await).unwrap();
        client.tick().unwrap();
    }
}
레거시 C 코드
// main.c
#include "xylolabs/client.h"
#include "platform_esp32s3.h"
#include "driver/i2s_std.h"

static int16_t s_i2s_buf[256 * 4];  // 256 샘플 x 4ch 인터리브
static i2s_chan_handle_t s_i2s_rx;

void app_main(void) {
    // 1. WiFi 초기화
    wifi_init_sta("my_ssid", "my_password");

    // 2. 4ch 마이크 캡처를 위한 I2S 설정
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
    i2s_new_channel(&chan_cfg, NULL, &s_i2s_rx);

    i2s_std_config_t std_cfg = {
        .clk_cfg  = I2S_STD_CLK_DEFAULT_CONFIG(96000),
        .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
        .gpio_cfg = { .bclk = GPIO_NUM_14, .ws = GPIO_NUM_15, .dout = I2S_GPIO_UNUSED, .din = GPIO_NUM_16 },
    };
    i2s_channel_init_std_mode(s_i2s_rx, &std_cfg);
    i2s_channel_enable(s_i2s_rx);

    // 3. SDK 초기화
    xylolabs_init(&g_platform);
    xylolabs_set_api_key("xk_your_key_here");
    xylolabs_set_device_id(2);

    xylolabs_stream_def_t streams[] = {
        { 0, "vibration_x", XMBP_VT_F32, "g", 100.0f },
        { 1, "temperature",  XMBP_VT_F32, "celsius", 1.0f },
        { 2, "humidity",     XMBP_VT_F32, "percent",  1.0f },
    };
    xylolabs_session_create("esp32_s3_node", streams, 3);

    // 4. 메인 루프 (태스크 고정을 통해 Core 1에서 실행)
    size_t bytes_read;
    while (1) {
        i2s_channel_read(s_i2s_rx, s_i2s_buf, sizeof(s_i2s_buf), &bytes_read, portMAX_DELAY);
        xylolabs_audio_feed(s_i2s_buf, bytes_read / sizeof(int16_t));

        xylolabs_meta_feed_f32(0, read_lis3dh_x());
        xylolabs_meta_feed_f32(1, read_bme280_temp());
        xylolabs_meta_feed_f32(2, read_bme280_humidity());

        xylolabs_tick();
    }
}
--- ## 문제 해결 | 증상 | 가능한 원인 | 해결 방법 | |------|-----------|----------| | 오디오 글리치 / 끊김 | WiFi TX가 I2S DMA 방해 | 오디오 태스크를 Core 0에, 네트워크를 Core 1에 고정 | | S3에서 힙 고갈 | 내부 RAM의 대용량 버퍼 | `__attribute__((section(".ext_ram.bss")))` 로 오디오 링을 PSRAM으로 이동 | | TCP 연결 타임아웃 | STA 인터페이스에서 DNS 해석 | IP 주소 사용 또는 `tcp_connect` 전에 `getaddrinfo()` 호출 | | C3에서 오디오 hardfault | RISC-V의 FPU 명령어 | `XYLOLABS_AUDIO_CHANNELS=0` 설정 (C3는 센서 전용) | | OTA 업데이트 중단 | 플래시 중 전원 손실 | `sdkconfig`에서 롤백 파티션 활성화; `esp_ota_mark_app_valid_cancel_rollback()` 테스트 | | 슬립 중 높은 전류 | WiFi 모뎀 미정지 | `esp_light_sleep_start()` 전에 `esp_wifi_stop()` 호출 |