서론: 왜 임베디드/IoT인가?
2025년 현재 전 세계 IoT 디바이스 수는 약 300억 개를 돌파했습니다. 스마트 홈, 산업용 센서, 웨어러블, 자율주행차, 스마트 팩토리에 이르기까지 임베디드 시스템은 현대 인프라의 핵심 구성요소입니다. 일반 애플리케이션 개발과 임베디드 개발은 근본적으로 다른 사고방식을 요구합니다. 메모리는 KB 단위로 제한되고, CPU는 MHz 수준이며, 배터리로 수년간 동작해야 하고, 실시간 제약을 만족해야 합니다.
이 가이드에서는 임베디드 시스템의 기초부터 IoT 프로덕션 시스템까지 전체 스택을 다룹니다. 마이크로컨트롤러 선택, 개발 환경 설정, RTOS 기반 멀티태스킹, 하드웨어 인터페이스, 센서 통합, IoT 프로토콜, OTA 업데이트, 전력 관리, Edge AI, 보안까지 실무자가 알아야 할 모든 내용을 포괄합니다.
1. 임베디드 시스템 vs 일반 컴퓨팅
1.1 핵심 차이점
| 항목 | 일반 컴퓨팅 | 임베디드 시스템 |
|---|---|---|
| CPU 속도 | GHz | MHz (수십~수백) |
| RAM | GB 단위 | KB~수 MB |
| 저장장치 | SSD/HDD TB | Flash KB~MB |
| OS | Linux/Windows/macOS | 베어메탈, RTOS, 임베디드 Linux |
| 전력 소비 | 수십~수백 W | μA~mW |
| 실시간 제약 | 없음 또는 약함 | 강함 (마이크로초 단위) |
| 사용 수명 | 수년 | 10-20년 |
| 업데이트 | 자주 | OTA, 드물게 |
1.2 자원 제약 사고방식
임베디드 개발자는 항상 자원 제약을 의식해야 합니다.
- 메모리 할당: malloc/free 사용을 최소화. 정적 할당 선호
- 스택 크기: 함수 재귀 제한, 로컬 배열 크기 주의
- CPU 사이클: 부동소수점 연산 대신 정수 연산, 불필요한 복사 회피
- 전력: 대부분 시간을 슬립 모드로, 인터럽트로 깨어나기
1.3 실시간 요구사항
실시간 시스템은 결과의 정확성뿐만 아니라 시간 제약도 만족해야 합니다.
- Hard Real-Time: 데드라인 미스 = 시스템 실패 (항공 제어, 의료기기)
- Firm Real-Time: 데드라인 미스 = 결과 무효 (실시간 비디오 스트리밍)
- Soft Real-Time: 데드라인 미스 = 품질 저하 (멀티미디어)
2. 마이크로컨트롤러(MCU) 랜드스케이프
2.1 주요 MCU 플랫폼 비교
| MCU | CPU | RAM | Flash | 가격 | 특징 |
|---|---|---|---|---|---|
| Arduino Uno (ATmega328P) | 8-bit AVR 16MHz | 2KB | 32KB | $5-25 | 학습용, 단순 |
| ESP32 | Xtensa dual-core 240MHz | 520KB | 4MB+ | $3-10 | Wi-Fi + BLE 내장, IoT 최적 |
| STM32F4 (Cortex-M4) | ARM 168MHz FPU | 192KB | 1MB | $5-15 | 산업용, 고성능 |
| STM32L0 (Cortex-M0+) | ARM 32MHz | 8KB | 64KB | $1-5 | 저전력 |
| Raspberry Pi Pico (RP2040) | Dual Cortex-M0+ 133MHz | 264KB | 2MB | $4 | 저가, PIO |
| nRF52840 | Cortex-M4F 64MHz | 256KB | 1MB | $5-15 | BLE/Thread/Zigbee |
| ESP32-S3 | Xtensa dual-core 240MHz | 512KB | 8MB+ | $5-12 | Wi-Fi 6, AI 가속기 |
2.2 MCU 선택 가이드
- IoT + Wi-Fi: ESP32 또는 ESP32-S3
- 저전력 배터리: nRF52840, STM32L 시리즈
- 고성능 제어: STM32F4/F7, Teensy 4
- 학습/프로토타입: Arduino Uno, Raspberry Pi Pico
- 산업 제어: STM32F 시리즈, NXP iMX RT
2.3 SoC vs MCU vs SBC
- MCU (마이크로컨트롤러): 단일 칩, KB-MB 메모리, 베어메탈/RTOS, mW 전력
- SoC (System on Chip): Linux 구동 가능, Raspberry Pi, 더 높은 전력
- SBC (싱글보드 컴퓨터): Raspberry Pi, BeagleBone, 완전한 Linux 시스템
3. 개발 환경 구축
3.1 주요 IDE/툴체인
Arduino IDE (초보자 친화)
// blink.ino - 가장 간단한 임베디드 프로그램
const int LED_PIN = 13;
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
Serial.println("Arduino started");
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
PlatformIO (멀티 플랫폼)
; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
bblanchon/ArduinoJson @ ^7.0.0
knolleary/PubSubClient @ ^2.8
build_flags =
-DCORE_DEBUG_LEVEL=3
ESP-IDF (ESP32 네이티브 SDK)
// main.c - ESP-IDF Hello World
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "main";
void app_main(void) {
ESP_LOGI(TAG, "Hello from ESP32!");
while (1) {
ESP_LOGI(TAG, "Tick %lld", esp_timer_get_time() / 1000000);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
STM32CubeIDE (STM32 공식)
STM32CubeMX로 GUI 기반 핀 설정, HAL 코드 생성, STM32CubeIDE에서 개발 및 디버깅.
3.2 디버깅 도구
- JTAG/SWD: 하드웨어 디버거 (ST-Link, J-Link, Black Magic Probe)
- GDB + OpenOCD: 오픈소스 디버깅 체인
- Serial Monitor: UART 기반 로그 출력
- Logic Analyzer: 디지털 신호 분석 (Saleae, PulseView)
- Oscilloscope: 아날로그 신호 측정
4. RTOS(실시간 운영체제) 기초
4.1 RTOS vs 베어메탈 vs 임베디드 Linux
| 항목 | 베어메탈 | RTOS | 임베디드 Linux |
|---|---|---|---|
| 복잡도 | 낮음 | 중간 | 높음 |
| 메모리 요구 | 최소 | 수 KB~수백 KB | 수 MB+ |
| 멀티태스킹 | Super Loop | 태스크 기반 | 프로세스/스레드 |
| 결정론 | 높음 | 높음 | 중간 |
| 부팅 시간 | 즉시 | 밀리초 | 초 단위 |
| 예시 | Arduino, 간단 센서 | ESP32, 산업 제어 | Raspberry Pi |
4.2 RTOS 핵심 개념
- Task (Thread): 독립적 실행 단위, 우선순위 보유
- Scheduler: 태스크 선택 (Preemptive Priority-based)
- Context Switching: 태스크 전환 시 레지스터 저장/복원
- Semaphore: 동기화 기본 원시(binary, counting)
- Mutex: 상호 배제(우선순위 상속 지원)
- Queue: 태스크 간 메시지 전달(thread-safe)
- Event Flag: 여러 이벤트 대기
- Software Timer: 주기적/일회성 타이머 콜백
4.3 우선순위 역전 (Priority Inversion)
낮은 우선순위 태스크가 고우선순위 태스크를 블록하는 현상. 해결책:
- Priority Inheritance: Mutex 보유 태스크의 우선순위를 일시적으로 상승
- Priority Ceiling: Mutex에 최대 우선순위 지정
1997년 Mars Pathfinder에서 실제 발생한 유명한 버그입니다.
5. FreeRTOS 심화
5.1 태스크 생성과 스케줄링
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 센서 읽기 태스크
void sensor_task(void *pvParameters) {
while (1) {
float temp = read_temperature();
printf("Temperature: %.2f C\n", temp);
vTaskDelay(pdMS_TO_TICKS(1000)); // 1초 대기
}
}
// LED 제어 태스크
void led_task(void *pvParameters) {
int pin = (int)pvParameters;
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
while (1) {
gpio_set_level(pin, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(pin, 0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main(void) {
xTaskCreate(
sensor_task, // 함수
"sensor", // 이름
4096, // 스택 크기 (바이트)
NULL, // 파라미터
5, // 우선순위
NULL // 핸들
);
xTaskCreate(led_task, "led", 2048, (void*)2, 3, NULL);
}
5.2 큐(Queue) 사용 예
QueueHandle_t xSensorQueue;
typedef struct {
float temperature;
float humidity;
uint32_t timestamp;
} sensor_data_t;
void producer_task(void *pv) {
sensor_data_t data;
while (1) {
data.temperature = read_temp();
data.humidity = read_humid();
data.timestamp = xTaskGetTickCount();
if (xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(100)) != pdPASS) {
printf("Queue full!\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void consumer_task(void *pv) {
sensor_data_t data;
while (1) {
if (xQueueReceive(xSensorQueue, &data, portMAX_DELAY) == pdPASS) {
printf("Received: T=%.1f H=%.1f at %lu\n",
data.temperature, data.humidity, data.timestamp);
// MQTT publish 등
}
}
}
void app_main(void) {
xSensorQueue = xQueueCreate(10, sizeof(sensor_data_t));
xTaskCreate(producer_task, "prod", 4096, NULL, 5, NULL);
xTaskCreate(consumer_task, "cons", 4096, NULL, 4, NULL);
}
5.3 세마포어와 뮤텍스
SemaphoreHandle_t xMutex;
SemaphoreHandle_t xBinarySemaphore;
int shared_counter = 0;
void task_a(void *pv) {
while (1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
shared_counter++;
printf("Task A: %d\n", shared_counter);
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// ISR에서 태스크 깨우기
void IRAM_ATTR gpio_isr_handler(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void button_task(void *pv) {
while (1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
printf("Button pressed!\n");
}
}
}
5.4 메모리 관리
FreeRTOS는 다양한 힙 구현을 제공합니다.
- heap_1: 할당만, 해제 불가 (가장 단순)
- heap_2: 할당/해제, 단편화 발생
- heap_3: 표준 malloc/free 래퍼
- heap_4: 인접 블록 병합 (권장)
- heap_5: 비연속 메모리 영역 지원
// heap_4 사용 예
void *ptr = pvPortMalloc(256);
if (ptr != NULL) {
// 사용
vPortFree(ptr);
}
// 현재 힙 상태 확인
printf("Free heap: %u\n", xPortGetFreeHeapSize());
printf("Min ever: %u\n", xPortGetMinimumEverFreeHeapSize());
6. Zephyr RTOS
6.1 Zephyr의 특징
Zephyr는 Linux Foundation이 관리하는 오픈소스 RTOS로, 다음 특징을 가집니다.
- 450+ 보드 지원: ARM, RISC-V, x86, ARC, Xtensa
- 디바이스 트리: 하드웨어 추상화를 YAML로 선언
- Kconfig: 기능 모듈화 (Linux 커널과 유사)
- POSIX 호환 레이어
- Bluetooth LE, 802.15.4, LoRa, Thread 내장
- West: 메타 빌드 도구
6.2 Zephyr Hello World
// src/main.c
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
int main(void) {
int ret;
if (!gpio_is_ready_dt(&led)) {
return 0;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return 0;
}
while (1) {
gpio_pin_toggle_dt(&led);
k_msleep(1000);
}
return 0;
}
6.3 디바이스 트리 오버레이
/* boards/esp32.overlay */
/ {
aliases {
led0 = &my_led;
};
leds {
compatible = "gpio-leds";
my_led: led_0 {
gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
label = "Green LED";
};
};
};
7. 하드웨어 인터페이스 심화
7.1 GPIO (General Purpose I/O)
// ESP-IDF GPIO 예
#include "driver/gpio.h"
void configure_gpio(void) {
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_POSEDGE, // 상승 에지 인터럽트
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << 4),
.pull_down_en = 0,
.pull_up_en = 1,
};
gpio_config(&io_conf);
gpio_install_isr_service(0);
gpio_isr_handler_add(4, gpio_isr_handler, (void*) 4);
}
7.2 I2C (Inter-Integrated Circuit)
2선 통신(SDA, SCL)으로 여러 디바이스 연결 가능. 100kHz/400kHz/1MHz/3.4MHz 속도.
#include "driver/i2c.h"
#define I2C_MASTER_NUM 0
#define I2C_MASTER_SDA_IO 21
#define I2C_MASTER_SCL_IO 22
#define I2C_MASTER_FREQ_HZ 400000
#define SENSOR_ADDR 0x76 // BME280
esp_err_t i2c_master_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(I2C_MASTER_NUM, &conf);
return i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
esp_err_t read_bme280_temp(float *temperature) {
uint8_t data[3];
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (SENSOR_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0xFA, true); // temp register
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (SENSOR_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read(cmd, data, 3, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000/portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
int32_t raw = ((int32_t)data[0] << 12) | ((int32_t)data[1] << 4) | (data[2] >> 4);
*temperature = raw / 5120.0; // 간소화된 변환
return ret;
}
7.3 SPI (Serial Peripheral Interface)
4선(MOSI/MISO/SCK/CS), 전이중, 고속(수십 MHz). Flash, SD카드, 디스플레이 등에 사용.
#include "driver/spi_master.h"
spi_device_handle_t spi;
void spi_init(void) {
spi_bus_config_t buscfg = {
.miso_io_num = 19,
.mosi_io_num = 23,
.sclk_io_num = 18,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096,
};
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 10 * 1000 * 1000, // 10 MHz
.mode = 0,
.spics_io_num = 5,
.queue_size = 7,
};
spi_bus_initialize(HSPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
spi_bus_add_device(HSPI_HOST, &devcfg, &spi);
}
7.4 UART (Universal Asynchronous Receiver/Transmitter)
비동기 직렬 통신, 시작/정지 비트, 9600~115200+ bps.
#include "driver/uart.h"
void uart_init(void) {
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(UART_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, 17, 16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_1, 1024, 0, 0, NULL, 0);
}
void uart_task(void *pv) {
uint8_t data[128];
while (1) {
int len = uart_read_bytes(UART_NUM_1, data, sizeof(data), pdMS_TO_TICKS(20));
if (len > 0) {
uart_write_bytes(UART_NUM_1, (const char*)data, len); // echo
}
}
}
7.5 ADC (Analog to Digital Converter)
#include "esp_adc/adc_oneshot.h"
adc_oneshot_unit_handle_t adc1_handle;
void adc_init(void) {
adc_oneshot_unit_init_cfg_t init = { .unit_id = ADC_UNIT_1 };
adc_oneshot_new_unit(&init, &adc1_handle);
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_12,
.atten = ADC_ATTEN_DB_12,
};
adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_0, &config);
}
int read_voltage_mv(void) {
int raw;
adc_oneshot_read(adc1_handle, ADC_CHANNEL_0, &raw);
return raw * 3300 / 4095;
}
7.6 PWM (Pulse Width Modulation)
#include "driver/ledc.h"
void pwm_init(void) {
ledc_timer_config_t timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_13_BIT,
.timer_num = LEDC_TIMER_0,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer);
ledc_channel_config_t channel = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = 18,
.duty = 4096, // 50% of 8192
};
ledc_channel_config(&channel);
}
8. 센서 통합 실전
8.1 온습도 센서 (DHT22, BME280, SHT31)
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(115200);
dht.begin();
}
void loop() {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
Serial.printf("T: %.1f C, H: %.1f %%\n", t, h);
}
delay(2000);
}
8.2 가속도계/자이로스코프 (MPU6050)
#include <Wire.h>
#include <MPU6050.h>
MPU6050 mpu;
void setup() {
Wire.begin();
mpu.initialize();
if (mpu.testConnection()) {
Serial.println("MPU6050 OK");
}
}
void loop() {
int16_t ax, ay, az, gx, gy, gz;
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
Serial.printf("Accel: %d %d %d\n", ax, ay, az);
delay(100);
}
8.3 GPS (NEO-6M, u-blox)
#include <TinyGPS++.h>
TinyGPSPlus gps;
void setup() {
Serial.begin(115200);
Serial2.begin(9600); // GPS UART
}
void loop() {
while (Serial2.available() > 0) {
if (gps.encode(Serial2.read())) {
if (gps.location.isValid()) {
Serial.printf("Lat: %.6f, Lng: %.6f\n",
gps.location.lat(), gps.location.lng());
}
}
}
}
9. IoT 통신 프로토콜 비교
9.1 프로토콜 매트릭스
| 프로토콜 | 계층 | 범위 | 대역폭 | 전력 | 토폴로지 | 주요 용도 |
|---|---|---|---|---|---|---|
| Wi-Fi | L1-L2 | 50-100m | 높음 | 높음 | Star | 홈 IoT |
| Bluetooth LE | L1-L2 | 10-50m | 중간 | 저 | Star/Mesh | 웨어러블 |
| Zigbee | L1-L2 | 10-100m | 낮음 | 저 | Mesh | 스마트 홈 |
| Thread | L1-L3 | 10-100m | 낮음 | 저 | Mesh | Matter 기반 |
| LoRa/LoRaWAN | L1-L2 | 2-15km | 매우 낮음 | 매우 저 | Star | 스마트 시티 |
| NB-IoT | L1-L2 | 셀룰러 | 낮음 | 저 | Cellular | 원격 자산 |
| MQTT | L7 | Any | - | - | Pub/Sub | 범용 IoT |
| CoAP | L7 | Any | - | - | REST | 제약된 디바이스 |
| AMQP | L7 | Any | - | - | Queue | 기업 IoT |
| HTTP/REST | L7 | Any | - | - | Req/Res | 범용 |
9.2 Wi-Fi 연결 (ESP32)
#include "esp_wifi.h"
#include "esp_event.h"
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
printf("Got IP: " IPSTR "\n", IP2STR(&event->ip_info.ip));
}
}
void wifi_init_sta(void) {
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
wifi_event_handler, NULL, NULL);
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
wifi_event_handler, NULL, NULL);
wifi_config_t wifi_config = {
.sta = {
.ssid = "MyWiFi",
.password = "MyPassword",
},
};
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
esp_wifi_start();
}
10. MQTT 심화
10.1 MQTT 핵심 개념
- Broker: 메시지 중개자 (Mosquitto, EMQX, HiveMQ)
- Client: Publisher 또는 Subscriber
- Topic: 계층적 문자열 (sensor/temperature/room1)
- QoS: 0 (최대 1회), 1 (최소 1회), 2 (정확히 1회)
- Retained Message: 브로커가 저장, 새 구독자에 즉시 전달
- Last Will Testament (LWT): 비정상 연결 종료 시 자동 발행
10.2 ESP32 MQTT 클라이언트
#include "mqtt_client.h"
esp_mqtt_client_handle_t client;
static void mqtt_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data) {
esp_mqtt_event_handle_t event = event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
printf("MQTT connected\n");
esp_mqtt_client_subscribe(client, "device/cmd", 1);
break;
case MQTT_EVENT_DATA:
printf("Topic: %.*s, Data: %.*s\n",
event->topic_len, event->topic,
event->data_len, event->data);
break;
case MQTT_EVENT_DISCONNECTED:
printf("MQTT disconnected\n");
break;
default:
break;
}
}
void mqtt_start(void) {
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = "mqtts://mqtt.example.com:8883",
.credentials.username = "device01",
.credentials.authentication.password = "secret",
.session.last_will.topic = "device/status",
.session.last_will.msg = "offline",
.session.last_will.qos = 1,
.session.last_will.retain = 1,
};
client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}
void publish_sensor_data(float temp, float humid) {
char payload[128];
snprintf(payload, sizeof(payload),
"{\"t\":%.2f,\"h\":%.2f,\"ts\":%lld}",
temp, humid, esp_timer_get_time() / 1000000);
esp_mqtt_client_publish(client, "sensor/data", payload, 0, 1, 0);
}
10.3 Topic 설계 베스트 프랙티스
- 계층적이고 직관적으로: tenant/site/device/metric
- 와일드카드 활용:
+(단일 레벨),#(멀티 레벨) - 예:
home/+/temperature,factory/line1/# - PII 포함 금지
- 읽기/쓰기 경로 분리:
device/+/cmdvsdevice/+/state
11. CoAP, LoRaWAN, BLE
11.1 CoAP (Constrained Application Protocol)
RFC 7252, REST 스타일, UDP 기반, 매우 가벼움.
#include "coap3/coap.h"
void coap_client_example(void) {
coap_context_t *ctx = coap_new_context(NULL);
coap_address_t dst;
coap_address_init(&dst);
dst.size = sizeof(dst.addr.sin);
dst.addr.sin.sin_family = AF_INET;
dst.addr.sin.sin_port = htons(5683);
inet_pton(AF_INET, "192.168.1.100", &dst.addr.sin.sin_addr);
coap_session_t *session = coap_new_client_session(ctx, NULL, &dst, COAP_PROTO_UDP);
coap_pdu_t *pdu = coap_pdu_init(COAP_MESSAGE_CON, COAP_REQUEST_CODE_GET,
coap_new_message_id(session), coap_session_max_pdu_size(session));
coap_add_option(pdu, COAP_OPTION_URI_PATH, 4, (const uint8_t*)"temp");
coap_send(session, pdu);
}
11.2 LoRaWAN
장거리(수 km), 저대역폭(수 kbps), 저전력(수년 배터리). 농업, 환경 모니터링에 적합.
// Heltec ESP32 LoRa 예
#include "LoRaWan_APP.h"
uint8_t devEui[] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 };
uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t appKey[] = { 0x00, /* 16 bytes */ };
void setup() {
Mcu.begin();
LoRaWAN.init(loraWanClass, loraWanRegion);
}
void loop() {
if (deviceState == DEVICE_STATE_SEND) {
prepareTxFrame(appPort);
LoRaWAN.send();
deviceState = DEVICE_STATE_CYCLE;
}
LoRaWAN.cycle(txDutyCycleTime);
}
11.3 BLE (Bluetooth Low Energy)
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLECharacteristic *pCharacteristic;
void setup() {
BLEDevice::init("ESP32_Sensor");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
pService->start();
pServer->getAdvertising()->start();
}
void loop() {
float temp = readTemp();
pCharacteristic->setValue((uint8_t*)&temp, sizeof(temp));
pCharacteristic->notify();
delay(1000);
}
12. OTA (Over-the-Air) 펌웨어 업데이트
12.1 OTA 기본 원리
- A/B 파티션: 현재 실행 중인 펌웨어(A)는 그대로, 새 펌웨어를 B에 쓰고 부팅 시 전환
- Rollback: 새 펌웨어가 부팅 실패 또는 기본 검사 실패 시 이전 펌웨어로 롤백
- 보안: HTTPS + 서명 검증 필수
- 중단 내성: 다운로드 중 전원 손실에도 복구 가능해야 함
12.2 ESP-IDF OTA 예
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
void ota_task(void *pv) {
esp_http_client_config_t config = {
.url = "https://ota.example.com/firmware.bin",
.cert_pem = (char *)server_cert_pem_start,
.timeout_ms = 5000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
printf("OTA successful. Restarting...\n");
esp_restart();
} else {
printf("OTA failed: %s\n", esp_err_to_name(ret));
}
vTaskDelete(NULL);
}
// 새 부팅 후 자체 검사
void mark_valid_after_tests(void) {
esp_ota_img_states_t state;
const esp_partition_t *running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
if (self_tests_passed()) {
esp_ota_mark_app_valid_cancel_rollback();
} else {
esp_ota_mark_app_invalid_rollback_and_reboot();
}
}
}
}
12.3 Secure Boot + Flash Encryption
- Secure Boot: 부트로더, 펌웨어에 RSA/ECDSA 서명. 검증 후 실행
- Flash Encryption: AES-256으로 외부 플래시 암호화
- eFuse: 칩 내부에 키 저장, 읽기 불가 설정
13. 전력 관리
13.1 ESP32 슬립 모드
| 모드 | 전류 소비 | RAM 보존 | 복귀 시간 | 웨이크업 소스 |
|---|---|---|---|---|
| Active | 100-240 mA | Yes | - | - |
| Modem-Sleep | 3-20 mA | Yes | μs | 자동 |
| Light-Sleep | 0.8 mA | Yes | ms | Timer, GPIO, UART |
| Deep-Sleep | 10 μA | RTC만 | 수백 ms | Timer, EXT0/1, Touch, ULP |
| Hibernation | 5 μA | No | 부팅부터 | RTC Timer, EXT1 |
13.2 Deep Sleep 예
#include "esp_sleep.h"
RTC_DATA_ATTR int boot_count = 0; // 슬립 후에도 유지
void app_main(void) {
boot_count++;
printf("Boot count: %d\n", boot_count);
// 센서 데이터 수집 및 전송
collect_and_send();
// 다음 웨이크업 설정: 30분 후
esp_sleep_enable_timer_wakeup(30 * 60 * 1000000ULL);
// GPIO 25에서 LOW 감지 시 웨이크업
esp_sleep_enable_ext0_wakeup(GPIO_NUM_25, 0);
printf("Entering deep sleep\n");
esp_deep_sleep_start();
}
13.3 배터리 수명 계산
배터리 용량: 2000 mAh
평균 전류 = (Active_mA × Active_time + Sleep_mA × Sleep_time) / Total_time
예: 5초 active(100 mA) + 30분 sleep(0.01 mA)
평균 = (100 × 5 + 0.01 × 1800) / 1805 ≈ 0.29 mA
수명 = 2000 / 0.29 / 24 ≈ 287일
14. Edge AI on MCU
14.1 TensorFlow Lite Micro
KB 단위 메모리에서 동작하는 경량 ML 런타임. 음성 인식, 이미지 분류, 이상 탐지 등에 사용.
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "model_data.h"
constexpr int kTensorArenaSize = 100 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
void setup() {
const tflite::Model* model = tflite::GetModel(g_model);
static tflite::MicroMutableOpResolver<5> resolver;
resolver.AddConv2D();
resolver.AddMaxPool2D();
resolver.AddFullyConnected();
resolver.AddSoftmax();
resolver.AddRelu();
static tflite::MicroInterpreter interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
interpreter.AllocateTensors();
TfLiteTensor* input = interpreter.input(0);
// 입력 데이터 채우기
for (int i = 0; i < input->bytes; i++) {
input->data.uint8[i] = sensor_reading[i];
}
interpreter.Invoke();
TfLiteTensor* output = interpreter.output(0);
int predicted_class = argmax(output);
}
14.2 Edge Impulse
노코드 ML 파이프라인 툴. 데이터 수집 → 전처리 → 모델 학습 → MCU 배포 자동화.
15. 보안
15.1 임베디드 보안 체크리스트
- Secure Boot 활성화: 서명된 펌웨어만 실행
- Flash Encryption: 민감 데이터 보호
- TLS 1.2+: 모든 네트워크 통신
- TLS 인증서 검증: 서버 인증서 체인 검증
- 하드웨어 암호화 가속기: AES, SHA, RSA/ECC
- 디버그 포트 비활성화: 프로덕션 전 JTAG/SWD 잠금
- 펌웨어 서명: ECDSA로 서명, 부트로더에서 검증
- Random Number: TRNG 사용 (HRNG)
- 패스워드/키 로테이션: OTA로 갱신 가능해야 함
- 최소 권한: 불필요한 서비스/포트 비활성화
15.2 Mutual TLS 예
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = "mqtts://broker.example.com:8883",
.broker.verification.certificate = (const char*)server_ca_pem,
.credentials.authentication.certificate = (const char*)client_cert_pem,
.credentials.authentication.key = (const char*)client_key_pem,
};
16. 프로덕션 고려사항
16.1 로깅과 모니터링
- 로컬 로그 레벨: DEBUG/INFO/WARN/ERROR
- 원격 로그 전송: 이상 시만, 대역폭 절약
- 텔레메트리: CPU/메모리/배터리/신호 강도
- 크래시 덤프: ESP32 Core Dump, Flash에 저장 후 전송
16.2 디바이스 수명주기 관리
- Provisioning: 공장에서 기본 설정
- Onboarding: 사용자 Wi-Fi/계정 연결 (BLE, SoftAP, WPS)
- Operation: 정상 동작, OTA 업데이트
- Decommissioning: 키 삭제, 팩토리 리셋
16.3 제조와 양산
- 파티션 테이블: 부트로더, 앱, NVS, OTA, 사용자 데이터
- MAC 주소 기반 고유 ID
- 자동 테스트 픽스처: JTAG + 테스트 펌웨어로 자동 검증
- 인증: FCC, CE, IC, KC 등 지역별 전파 인증
17. 실습 프로젝트 아이디어
- 환경 모니터: ESP32 + BME280 + MQTT → Grafana 대시보드
- 스마트 도어락: ESP32 + RFID/지문 + BLE 모바일 앱
- 농장 센서 네트워크: LoRaWAN + 토양 수분 + 게이트웨이
- 웨어러블 심박계: nRF52 + MAX30102 + BLE
- 음성 제어 허브: ESP32-S3 + 마이크 + TFLite Micro
- 공장 자산 추적: LoRaWAN + GPS + NB-IoT 백업
18. 퀴즈
Q1. FreeRTOS에서 우선순위 역전 문제의 해결책 두 가지는?
A: Priority Inheritance(우선순위 상속)와 Priority Ceiling(우선순위 상한)입니다. 전자는 뮤텍스를 잡고 있는 저우선순위 태스크의 우선순위를 대기 중인 고우선순위 태스크와 같게 일시적으로 올리고, 후자는 뮤텍스 생성 시 최대 우선순위를 미리 지정합니다.
Q2. MQTT QoS 레벨 0/1/2의 차이는?
A: QoS 0은 "최대 1회"(fire and forget, 손실 가능), QoS 1은 "최소 1회"(PUBACK 필요, 중복 가능), QoS 2는 "정확히 1회"(4-way handshake, 가장 느림). 센서 텔레메트리는 보통 QoS 0 또는 1, 중요 커맨드는 QoS 2를 사용합니다.
Q3. OTA 업데이트에서 A/B 파티션이 필요한 이유는?
A: 새 펌웨어를 별도 파티션(B)에 쓰면서 현재 실행 중인 펌웨어(A)가 동작을 유지할 수 있고, 업데이트 실패 시 자동으로 이전 펌웨어로 롤백할 수 있기 때문입니다. 다운로드 중 전원 손실에도 디바이스가 벽돌이 되지 않습니다.
Q4. I2C와 SPI의 주요 차이점은?
A: I2C는 2선(SDA/SCL)으로 여러 디바이스를 주소로 구분, 속도는 100kHz-3.4MHz, 비교적 느림. SPI는 4선(MOSI/MISO/SCK/CS)으로 CS 선으로 디바이스 선택, 전이중, 수십 MHz 가능. 단순한 멀티 센서는 I2C, 고속 디스플레이/Flash는 SPI가 적합합니다.
Q5. ESP32 Deep Sleep에서 일어난 후 이전 데이터를 유지하는 방법은?
A: RTC_DATA_ATTR 매크로로 RTC 메모리에 변수를 배치하면 Deep Sleep 중에도 유지됩니다(8KB 제한). 예: RTC_DATA_ATTR int boot_count = 0;. 더 큰 데이터는 NVS(Non-Volatile Storage) 또는 플래시에 저장해야 합니다.
19. 참고 자료
- FreeRTOS 공식 문서 - freertos.org
- Zephyr Project Documentation - docs.zephyrproject.org
- ESP-IDF Programming Guide - docs.espressif.com
- STM32 HAL and Low-Layer Driver - st.com
- MQTT 5.0 Specification - docs.oasis-open.org
- CoAP RFC 7252 - datatracker.ietf.org
- LoRa Alliance 기술 문서 - lora-alliance.org
- TensorFlow Lite Micro - tensorflow.org/lite/microcontrollers
- Edge Impulse Documentation - docs.edgeimpulse.com
- Arm Mbed OS Documentation - os.mbed.com
- Making Embedded Systems (Elecia White, O'Reilly)
- Mastering STM32 (Carmine Noviello)
- The Hardware Hacker (Andrew Huang)
결론
임베디드/IoT 개발은 하드웨어, 실시간, 제약된 환경, 네트워킹, 보안이 한데 모이는 매력적인 분야입니다. Arduino로 시작해 ESP32/STM32/Zephyr로 진화하고, 프로덕션 수준의 OTA, 보안, 전력 관리를 갖춘 시스템을 만들 수 있습니다. 2025년 Matter, Thread, AI at the Edge 표준이 성숙해지며 임베디드 개발자의 기회는 더 확대되고 있습니다. 작은 LED 깜빡임에서 시작해 도시 전체를 네트워킹하는 시스템까지, 여정은 무한합니다.
현재 단락 (1/703)
2025년 현재 전 세계 IoT 디바이스 수는 약 300억 개를 돌파했습니다. 스마트 홈, 산업용 센서, 웨어러블, 자율주행차, 스마트 팩토리에 이르기까지 임베디드 시스템은 현대 ...