LabHub

블로그

임베디드 시스템 & IoT 프로그래밍 완전 가이드 2025: 펌웨어, RTOS, 통신 프로토콜

한국어English日本語

서론: 왜 임베디드/IoT인가?

2025년 현재 전 세계 IoT 디바이스 수는 약 300억 개를 돌파했습니다. 스마트 홈, 산업용 센서, 웨어러블, 자율주행차, 스마트 팩토리에 이르기까지 임베디드 시스템은 현대 인프라의 핵심 구성요소입니다. 일반 애플리케이션 개발과 임베디드 개발은 근본적으로 다른 사고방식을 요구합니다. 메모리는 KB 단위로 제한되고, CPU는 MHz 수준이며, 배터리로 수년간 동작해야 하고, 실시간 제약을 만족해야 합니다.

이 가이드에서는 임베디드 시스템의 기초부터 IoT 프로덕션 시스템까지 전체 스택을 다룹니다. 마이크로컨트롤러 선택, 개발 환경 설정, RTOS 기반 멀티태스킹, 하드웨어 인터페이스, 센서 통합, IoT 프로토콜, OTA 업데이트, 전력 관리, Edge AI, 보안까지 실무자가 알아야 할 모든 내용을 포괄합니다.

1. 임베디드 시스템 vs 일반 컴퓨팅

1.1 핵심 차이점

항목일반 컴퓨팅임베디드 시스템
CPU 속도GHzMHz (수십~수백)
RAMGB 단위KB~수 MB
저장장치SSD/HDD TBFlash KB~MB
OSLinux/Windows/macOS베어메탈, RTOS, 임베디드 Linux
전력 소비수십~수백 WμA~mW
실시간 제약없음 또는 약함강함 (마이크로초 단위)
사용 수명수년10-20년
업데이트자주OTA, 드물게

1.2 자원 제약 사고방식

임베디드 개발자는 항상 자원 제약을 의식해야 합니다.

1.3 실시간 요구사항

실시간 시스템은 결과의 정확성뿐만 아니라 시간 제약도 만족해야 합니다.

2. 마이크로컨트롤러(MCU) 랜드스케이프

2.1 주요 MCU 플랫폼 비교

MCUCPURAMFlash가격특징
Arduino Uno (ATmega328P)8-bit AVR 16MHz2KB32KB$5-25학습용, 단순
ESP32Xtensa dual-core 240MHz520KB4MB+$3-10Wi-Fi + BLE 내장, IoT 최적
STM32F4 (Cortex-M4)ARM 168MHz FPU192KB1MB$5-15산업용, 고성능
STM32L0 (Cortex-M0+)ARM 32MHz8KB64KB$1-5저전력
Raspberry Pi Pico (RP2040)Dual Cortex-M0+ 133MHz264KB2MB$4저가, PIO
nRF52840Cortex-M4F 64MHz256KB1MB$5-15BLE/Thread/Zigbee
ESP32-S3Xtensa dual-core 240MHz512KB8MB+$5-12Wi-Fi 6, AI 가속기

2.2 MCU 선택 가이드

2.3 SoC vs MCU vs SBC

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 디버깅 도구

4. RTOS(실시간 운영체제) 기초

4.1 RTOS vs 베어메탈 vs 임베디드 Linux

항목베어메탈RTOS임베디드 Linux
복잡도낮음중간높음
메모리 요구최소수 KB~수백 KB수 MB+
멀티태스킹Super Loop태스크 기반프로세스/스레드
결정론높음높음중간
부팅 시간즉시밀리초초 단위
예시Arduino, 간단 센서ESP32, 산업 제어Raspberry Pi

4.2 RTOS 핵심 개념

4.3 우선순위 역전 (Priority Inversion)

낮은 우선순위 태스크가 고우선순위 태스크를 블록하는 현상. 해결책:

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_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로, 다음 특징을 가집니다.

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-FiL1-L250-100m높음높음Star홈 IoT
Bluetooth LEL1-L210-50m중간Star/Mesh웨어러블
ZigbeeL1-L210-100m낮음Mesh스마트 홈
ThreadL1-L310-100m낮음MeshMatter 기반
LoRa/LoRaWANL1-L22-15km매우 낮음매우 저Star스마트 시티
NB-IoTL1-L2셀룰러낮음Cellular원격 자산
MQTTL7Any--Pub/Sub범용 IoT
CoAPL7Any--REST제약된 디바이스
AMQPL7Any--Queue기업 IoT
HTTP/RESTL7Any--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 핵심 개념

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 설계 베스트 프랙티스

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 기본 원리

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

13. 전력 관리

13.1 ESP32 슬립 모드

모드전류 소비RAM 보존복귀 시간웨이크업 소스
Active100-240 mAYes--
Modem-Sleep3-20 mAYesμs자동
Light-Sleep0.8 mAYesmsTimer, GPIO, UART
Deep-Sleep10 μARTC만수백 msTimer, EXT0/1, Touch, ULP
Hibernation5 μANo부팅부터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 임베디드 보안 체크리스트

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 로깅과 모니터링

16.2 디바이스 수명주기 관리

  1. Provisioning: 공장에서 기본 설정
  2. Onboarding: 사용자 Wi-Fi/계정 연결 (BLE, SoftAP, WPS)
  3. Operation: 정상 동작, OTA 업데이트
  4. Decommissioning: 키 삭제, 팩토리 리셋

16.3 제조와 양산

17. 실습 프로젝트 아이디어

  1. 환경 모니터: ESP32 + BME280 + MQTT → Grafana 대시보드
  2. 스마트 도어락: ESP32 + RFID/지문 + BLE 모바일 앱
  3. 농장 센서 네트워크: LoRaWAN + 토양 수분 + 게이트웨이
  4. 웨어러블 심박계: nRF52 + MAX30102 + BLE
  5. 음성 제어 허브: ESP32-S3 + 마이크 + TFLite Micro
  6. 공장 자산 추적: 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. 참고 자료

  1. FreeRTOS 공식 문서 - freertos.org
  2. Zephyr Project Documentation - docs.zephyrproject.org
  3. ESP-IDF Programming Guide - docs.espressif.com
  4. STM32 HAL and Low-Layer Driver - st.com
  5. MQTT 5.0 Specification - docs.oasis-open.org
  6. CoAP RFC 7252 - datatracker.ietf.org
  7. LoRa Alliance 기술 문서 - lora-alliance.org
  8. TensorFlow Lite Micro - tensorflow.org/lite/microcontrollers
  9. Edge Impulse Documentation - docs.edgeimpulse.com
  10. Arm Mbed OS Documentation - os.mbed.com
  11. Making Embedded Systems (Elecia White, O'Reilly)
  12. Mastering STM32 (Carmine Noviello)
  13. The Hardware Hacker (Andrew Huang)

결론

임베디드/IoT 개발은 하드웨어, 실시간, 제약된 환경, 네트워킹, 보안이 한데 모이는 매력적인 분야입니다. Arduino로 시작해 ESP32/STM32/Zephyr로 진화하고, 프로덕션 수준의 OTA, 보안, 전력 관리를 갖춘 시스템을 만들 수 있습니다. 2025년 Matter, Thread, AI at the Edge 표준이 성숙해지며 임베디드 개발자의 기회는 더 확대되고 있습니다. 작은 LED 깜빡임에서 시작해 도시 전체를 네트워킹하는 시스템까지, 여정은 무한합니다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다