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
프로덕션 배포에는 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에 적합한 용도: - 환경 모니터링 (온도, 습도, 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
Xylolabs 대시보드에서 SSE 제어 채널을 통해 OTA를 트리거한다.
전력 관리¶
틱 사이 라이트 슬립¶
// Rust (Embassy) - 권장
// Embassy는 비동기 태스크 폴링 사이에 자동으로 라이트 슬립에 진입한다.
// WiFi 연결 유지; 타이머 또는 인터럽트로 CPU가 깨어난다.
loop {
client.tick().await;
Timer::after_millis(10).await; // 틱 사이에 CPU 슬립
}
Legacy C equivalent
센서 전용용 딥 슬립 (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
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();
}
}