Skip to content

Split View: 임베디드 시스템 & IoT 완전 가이드: MCU부터 엣지 AI까지

|

임베디드 시스템 & IoT 완전 가이드: MCU부터 엣지 AI까지

1. 임베디드 시스템 개요

임베디드 시스템(Embedded System)은 특정 기능을 수행하기 위해 하드웨어와 소프트웨어가 결합된 전용 컴퓨팅 시스템입니다. 세탁기, 자동차 엔진 제어 유닛(ECU), 스마트 온도조절기, 의료 기기 등 우리 주변의 수많은 기기에 내장되어 있습니다.

마이크로컨트롤러(MCU) vs 마이크로프로세서(MPU)

항목MCUMPU
CPU단순, 저전력고성능, 복잡
메모리내장 Flash/RAM외부 DRAM 필요
운영체제없거나 RTOSLinux, Android 등
소비 전력수 mW수 W
예시STM32, AVR, ESP32Raspberry Pi, i.MX8

주요 MCU 플랫폼

  • STM32 (ST Microelectronics): ARM Cortex-M 기반, 산업용으로 널리 사용. HAL 라이브러리와 CubeMX 개발 도구 제공.
  • AVR (Microchip/Atmel): Arduino UNO의 기반 MCU. 입문자에게 친숙한 플랫폼.
  • ESP32 (Espressif): 듀얼코어 Xtensa LX6, WiFi+BLE 내장. IoT 프로젝트에 최적.
  • RP2040 (Raspberry Pi): 듀얼코어 ARM Cortex-M0+, PIO(Programmable I/O) 기능이 독특.
  • PIC (Microchip): 산업 현장에서 오랜 역사를 가진 MCU.

개발 환경

  • IDE: STM32CubeIDE, MPLAB X, Arduino IDE, PlatformIO
  • 컴파일러: GCC (arm-none-eabi-gcc), LLVM/Clang
  • 디버거: JTAG, SWD (Serial Wire Debug), OpenOCD, J-Link

2. C 언어로 하드웨어 제어

임베디드 C 프로그래밍의 핵심은 하드웨어 레지스터를 직접 제어하거나 HAL(Hardware Abstraction Layer) 라이브러리를 활용하는 것입니다.

GPIO 제어

GPIO(General Purpose Input/Output)는 디지털 입출력을 제어하는 가장 기본적인 인터페이스입니다.

// STM32 HAL 라이브러리를 사용한 GPIO 제어
#include "stm32f4xx_hal.h"

// LED 토글 (핀 PA5)
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);

// 버튼 읽기 (핀 PC13, 활성 LOW)
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) {
    // 버튼이 눌린 경우 처리
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}

레지스터 직접 조작

HAL 없이 레지스터를 직접 제어하는 방법은 더 빠르고 코드 크기가 작습니다.

// 레지스터 직접 조작 (STM32F4)
// GPIOA 클록 활성화
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// PA5를 출력으로 설정 (MODER 레지스터)
GPIOA->MODER &= ~(0x3 << (5 * 2));   // 비트 클리어
GPIOA->MODER |=  (0x1 << (5 * 2));   // 출력 모드 설정

// PA5 HIGH 출력
GPIOA->BSRR = (1 << 5);

// PA5 LOW 출력
GPIOA->BSRR = (1 << (5 + 16));

volatile 키워드의 중요성

컴파일러 최적화로 인해 하드웨어 레지스터 접근이 무시될 수 있습니다. volatile로 이를 방지합니다.

// 잘못된 예: 컴파일러가 최적화로 제거할 수 있음
uint32_t *reg = (uint32_t *)0x40020000;
*reg = 0x01;

// 올바른 예: volatile 사용
volatile uint32_t *reg = (volatile uint32_t *)0x40020000;
*reg = 0x01;

// ISR에서 사용하는 전역 변수도 volatile 필요
volatile uint8_t button_pressed = 0;

3. 인터럽트와 타이머

인터럽트 서비스 루틴 (ISR)

인터럽트는 하드웨어 이벤트(버튼 입력, 타이머 오버플로우, 통신 수신 등)가 발생할 때 CPU가 현재 작업을 중단하고 ISR을 실행하는 메커니즘입니다.

// EXTI 인터럽트 콜백 (HAL 방식)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_13) {
        button_pressed = 1;
        // ISR 내에서는 최소한의 작업만 수행
        // HAL_Delay() 등 블로킹 함수 사용 금지
    }
}

// 타이머 인터럽트 콜백 (1ms마다 호출)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        system_tick++;
    }
}

PWM 출력

타이머의 PWM(Pulse Width Modulation) 기능으로 서보모터, LED 밝기 조절, 모터 속도 제어를 구현합니다.

// PWM 시작 (TIM3 채널 1)
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

// 듀티 사이클 50% 설정 (ARR = 999이면 CCR = 500)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);

// 서보모터 제어 함수 (0~180도)
void servo_set_angle(uint8_t angle) {
    // 1ms~2ms 펄스폭을 타이머 카운터값으로 변환
    uint32_t pulse = 500 + (angle * 1000 / 180);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
}

4. 직렬 통신 프로토콜

UART/USART

비동기 직렬 통신. 디버그 출력, GPS 모듈, 블루투스 모듈 연결에 주로 사용됩니다.

// UART로 문자열 전송
char msg[] = "Hello, UART!\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100);

// UART 수신 (인터럽트 방식)
uint8_t rx_byte;
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        rx_buffer[rx_index++] = rx_byte;
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
    }
}

SPI 통신

마스터-슬레이브 동기 통신. 고속 데이터 전송이 필요한 디스플레이, SD카드, ADC에 사용됩니다.

// SPI로 데이터 송수신
uint8_t tx_data[] = {0x9F, 0x00};  // Flash ID 읽기 명령
uint8_t rx_data[2];

HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);  // CS LOW
HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);    // CS HIGH

I2C 통신

2선식(SDA, SCL) 버스 통신. 여러 기기를 같은 버스에 연결 가능. 센서류에 주로 사용됩니다.

// I2C로 MPU6050 가속도계 읽기
#define MPU6050_ADDR  0xD0  // 7비트 주소 0x68, 쓰기 0xD0
#define ACCEL_XOUT_H  0x3B

uint8_t data[6];
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, ACCEL_XOUT_H,
                  I2C_MEMADD_SIZE_8BIT, data, 6, 100);

int16_t accel_x = (data[0] << 8) | data[1];
int16_t accel_y = (data[2] << 8) | data[3];
int16_t accel_z = (data[4] << 8) | data[5];

// 실제 가속도 (g단위) = raw / 16384.0 (±2g 설정시)
float ax = accel_x / 16384.0f;

CAN Bus

자동차와 산업용 장비에서 사용하는 강인한 차동 직렬 통신 프로토콜입니다.

// CAN 메시지 전송 (STM32 HAL)
CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t tx_mailbox;

tx_header.StdId = 0x123;          // CAN ID
tx_header.IDE = CAN_ID_STD;       // 표준 프레임
tx_header.RTR = CAN_RTR_DATA;     // 데이터 프레임
tx_header.DLC = 8;                // 데이터 길이

HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox);

5. RTOS (Real-Time Operating System)

RTOS는 결정론적(Deterministic) 타이밍을 보장하는 운영체제입니다. 여러 태스크를 우선순위에 따라 스케줄링하며, 실시간 응답이 요구되는 시스템에 필수적입니다.

FreeRTOS 기초

FreeRTOS는 가장 널리 사용되는 오픈소스 RTOS입니다. Amazon이 관리하며 STM32, ESP32, Arduino 등 다양한 플랫폼을 지원합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"

// 태스크 함수 정의
void vTaskLED(void *pvParameters) {
    while(1) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));  // 500ms 대기 (CPU 양보)
    }
}

void vTaskSensor(void *pvParameters) {
    QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
    float temperature;

    while(1) {
        temperature = read_temperature_sensor();
        xQueueSend(xQueue, &temperature, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();

    QueueHandle_t xTempQueue = xQueueCreate(10, sizeof(float));

    // 태스크 생성 (함수명, 태스크명, 스택크기, 파라미터, 우선순위, 핸들)
    xTaskCreate(vTaskLED,    "LED",    128, NULL,        1, NULL);
    xTaskCreate(vTaskSensor, "Sensor", 256, xTempQueue,  2, NULL);

    vTaskStartScheduler();  // RTOS 스케줄러 시작 (이 줄 이후로 돌아오지 않음)
    while(1);
}

세마포어와 뮤텍스

SemaphoreHandle_t xMutex;
SemaphoreHandle_t xSemaphore;

// 뮤텍스로 공유 자원 보호
xMutex = xSemaphoreCreateMutex();

void vTaskA(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // 임계 구역: 공유 자원 접근
            shared_data++;
            xSemaphoreGive(xMutex);
        }
    }
}

// 바이너리 세마포어로 ISR-태스크 동기화
xSemaphore = xSemaphoreCreateBinary();

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

6. Arduino 생태계

Arduino는 임베디드 입문자를 위한 오픈소스 플랫폼입니다. 간단한 API와 방대한 라이브러리 생태계가 특징입니다.

Arduino 보드 비교

보드MCU클록FlashRAM특징
UNO R3ATmega328P16MHz32KB2KB기본 입문용
NanoATmega328P16MHz32KB2KB소형 폼팩터
Mega 2560ATmega256016MHz256KB8KB많은 핀 수
DueSAM3X8E84MHz512KB96KB32비트 ARM
UNO R4RA4M148MHz256KB32KB최신 버전

DHT22 + I2C LCD 예제

#include <DHT.h>
#include <LiquidCrystal_I2C.h>

#define DHTPIN 2
#define DHTTYPE DHT22

DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2);

void setup() {
    Serial.begin(9600);
    dht.begin();
    lcd.init();
    lcd.backlight();
    lcd.print("IoT Weather Box");
    delay(2000);
    lcd.clear();
}

void loop() {
    float temp = dht.readTemperature();
    float hum  = dht.readHumidity();

    if (isnan(temp) || isnan(hum)) {
        Serial.println("DHT read error!");
        return;
    }

    lcd.setCursor(0, 0);
    lcd.print("Temp: ");
    lcd.print(temp, 1);
    lcd.print(" C  ");

    lcd.setCursor(0, 1);
    lcd.print("Hum:  ");
    lcd.print(hum, 1);
    lcd.print(" %  ");

    Serial.print("T="); Serial.print(temp);
    Serial.print(" H="); Serial.println(hum);

    delay(2000);
}

7. ESP32 & WiFi/BLE IoT

ESP32는 Espressif Systems의 강력한 IoT MCU입니다. 듀얼코어 240MHz, WiFi 802.11 b/g/n, BLE 4.2/5.0을 내장하며 가격 대비 성능이 탁월합니다.

MicroPython으로 MQTT IoT 구현

# MicroPython ESP32 MQTT 센서 데이터 전송
import time
import network
from umqtt.simple import MQTTClient
from machine import Pin, ADC

# WiFi 연결
def connect_wifi(ssid, password):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(ssid, password)
        while not wlan.isconnected():
            time.sleep(0.5)
    print('WiFi connected:', wlan.ifconfig())

# MQTT 메시지 수신 콜백
def on_message(topic, msg):
    print('Topic:', topic, 'Message:', msg)
    if topic == b'device/led' and msg == b'on':
        led.value(1)

connect_wifi('MySSID', 'MyPassword')

# ADC로 온도 센서 읽기 (NTC 서미스터)
adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)

client = MQTTClient('esp32_sensor', 'broker.hivemq.com', port=1883)
client.set_callback(on_message)
client.connect()
client.subscribe(b'device/led')

while True:
    raw = adc.read()
    voltage = raw * 3.3 / 4095
    # 간단한 온도 변환 (실제는 NTC 특성 곡선 필요)
    temp_approx = (voltage - 0.5) * 100
    payload = '{:.1f}'.format(temp_approx)
    client.publish(b'sensor/temperature', payload.encode())
    client.check_msg()
    time.sleep(5)

ESP-IDF (C/C++ 기반)

// ESP-IDF WiFi 연결 예제
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

#define WIFI_SSID "MyNetwork"
#define WIFI_PASS "MyPassword"

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;
        ESP_LOGI("WiFi", "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
    }
}

void wifi_init_sta(void) {
    nvs_flash_init();
    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_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL);
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    esp_wifi_start();
}

8. Raspberry Pi & Linux 임베디드

Raspberry Pi는 완전한 Linux 운영체제를 실행하는 싱글보드 컴퓨터(SBC)입니다. 임베디드와 일반 컴퓨팅의 경계에 있는 강력한 플랫폼입니다.

Raspberry Pi 시리즈 비교

모델CPURAM특징
Pi Zero 2WARM Cortex-A53 quad 1GHz512MB초소형, WiFi/BLE
Pi 4 Model BARM Cortex-A72 quad 1.8GHz1~8GB범용 고성능
Pi 5ARM Cortex-A76 quad 2.4GHz4~8GB최신 고성능
Pi Pico 2RP2350 dual Cortex-M33520KB마이크로컨트롤러

GPIO 제어 및 서보모터

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)      # BCM 핀 번호 사용
GPIO.setup(18, GPIO.OUT)    # GPIO18을 출력으로 설정

# 50Hz PWM 서보모터 제어
pwm = GPIO.PWM(18, 50)
pwm.start(7.5)  # 중립 위치 (90도)

def set_servo_angle(angle):
    """서보모터 각도 설정 (0~180도)"""
    duty = 2.5 + (angle / 180.0) * 10.0
    pwm.ChangeDutyCycle(duty)
    time.sleep(0.3)

try:
    for angle in [0, 45, 90, 135, 180]:
        set_servo_angle(angle)
        print(f"Angle: {angle} degrees")
        time.sleep(1)
finally:
    pwm.stop()
    GPIO.cleanup()

gpiozero 라이브러리

from gpiozero import LED, Button, Servo, DistanceSensor
from signal import pause
import time

# LED와 버튼 연결
led = LED(17)
button = Button(4)

# 버튼 이벤트 핸들러
button.when_pressed = led.on
button.when_released = led.off

# 초음파 거리 센서 (HC-SR04)
sensor = DistanceSensor(echo=24, trigger=23)
while True:
    dist = sensor.distance * 100  # cm 단위
    print(f"Distance: {dist:.1f} cm")
    if dist < 20:
        led.blink(on_time=0.1, off_time=0.1)
    time.sleep(0.5)

Docker 컨테이너 활용

# Raspberry Pi용 Python IoT 앱 Dockerfile
FROM python:3.11-slim-bullseye

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "sensor_server.py"]
# docker-compose.yml
version: '3'
services:
  sensor-app:
    build: .
    privileged: true # GPIO 접근을 위해 필요
    volumes:
      - /dev:/dev
    restart: unless-stopped

  influxdb:
    image: influxdb:2.7
    ports:
      - '8086:8086'
    volumes:
      - influx_data:/var/lib/influxdb2

  grafana:
    image: grafana/grafana:latest
    ports:
      - '3000:3000'
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  influx_data:
  grafana_data:

9. 엣지 AI (Edge AI)

엣지 AI는 클라우드가 아닌 로컬 기기에서 AI 추론을 실행하는 기술입니다. 낮은 지연 시간, 오프라인 동작, 프라이버시 보호, 대역폭 절감이 주요 장점입니다.

TensorFlow Lite (TFLite)

TFLite는 모바일 및 임베디드 기기용으로 최적화된 경량 ML 프레임워크입니다.

# TFLite 이미지 분류 추론 (Raspberry Pi)
import tflite_runtime.interpreter as tflite
import numpy as np
from PIL import Image
import time

# 모델 로드
interpreter = tflite.Interpreter(
    model_path='mobilenet_v2_1.0_224_quant.tflite',
    num_threads=4  # 멀티스레드 추론
)
interpreter.allocate_tensors()

input_details  = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# 입력 정보 출력
print("Input shape:", input_details[0]['shape'])   # [1, 224, 224, 3]
print("Input dtype:", input_details[0]['dtype'])   # uint8 (양자화 모델)

def classify_image(image_path, labels):
    img = Image.open(image_path).convert('RGB').resize((224, 224))
    input_data = np.expand_dims(np.array(img), axis=0)

    # uint8 양자화 모델의 경우 정규화 불필요
    interpreter.set_tensor(input_details[0]['index'], input_data)

    start = time.time()
    interpreter.invoke()
    elapsed = (time.time() - start) * 1000

    output = interpreter.get_tensor(output_details[0]['index'])
    top_idx = np.argmax(output[0])

    print(f"Label: {labels[top_idx]}, Score: {output[0][top_idx]}")
    print(f"Inference time: {elapsed:.1f}ms")

# 레이블 파일 로드
with open('labels.txt') as f:
    labels = [line.strip() for line in f.readlines()]

classify_image('test.jpg', labels)

ONNX Runtime

# ONNX Runtime으로 커스텀 모델 실행
import onnxruntime as ort
import numpy as np
from PIL import Image

# 세션 생성 (CPU/GPU 선택 가능)
session = ort.InferenceSession(
    'model.onnx',
    providers=['CPUExecutionProvider']
)

# 입출력 정보 확인
input_name  = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
input_shape = session.get_inputs()[0].shape
print(f"Input: {input_name}, Shape: {input_shape}")

# 이미지 전처리
img = Image.open('test.jpg').resize((224, 224))
img_array = np.array(img).astype(np.float32) / 255.0
img_array = np.transpose(img_array, (2, 0, 1))  # HWC -> CHW
img_array = np.expand_dims(img_array, axis=0)   # 배치 차원 추가

# 추론
result = session.run([output_name], {input_name: img_array})
print("Output shape:", result[0].shape)

Edge AI 플랫폼 비교

플랫폼제조사AI 성능소비전력특징
Jetson Orin NanoNVIDIA40 TOPS7~15WCUDA GPU 내장
Coral Dev BoardGoogle4 TOPS~2WEdge TPU 전용
RK3588Rockchip6 TOPS NPU~8W가성비 우수
Hailo-8 M.2Hailo26 TOPS2.5WPCIe 확장 모듈
Pi 5 + AI HAT+RPi Foundation26 TOPS~5W라즈베리파이 에코계

10. IoT 통신 프로토콜 & 클라우드

MQTT

MQTT(Message Queuing Telemetry Transport)는 IoT에 최적화된 경량 pub/sub 메시지 프로토콜입니다. 브로커를 통해 발행자(Publisher)와 구독자(Subscriber)가 통신합니다.

# Python Paho-MQTT 클라이언트 예제
import paho.mqtt.client as mqtt
import json
import time

BROKER = "broker.hivemq.com"
PORT   = 1883
TOPIC_PUB = "home/sensor/living_room"
TOPIC_SUB = "home/control/#"

def on_connect(client, userdata, flags, rc):
    print(f"Connected with code: {rc}")
    client.subscribe(TOPIC_SUB)

def on_message(client, userdata, msg):
    payload = json.loads(msg.payload.decode())
    print(f"Topic: {msg.topic}, Payload: {payload}")

client = mqtt.Client("python_sensor_01")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()

while True:
    data = {
        "temperature": 23.5,
        "humidity": 60.2,
        "timestamp": int(time.time())
    }
    client.publish(TOPIC_PUB, json.dumps(data))
    time.sleep(10)

InfluxDB + Grafana 데이터 시각화

# InfluxDB에 센서 데이터 저장
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS

INFLUX_URL   = "http://localhost:8086"
INFLUX_TOKEN = "your_token_here"
INFLUX_ORG   = "myorg"
INFLUX_BUCKET = "iot_sensors"

client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = client.write_api(write_options=SYNCHRONOUS)

# 데이터 포인트 작성
point = (
    Point("environment")
    .tag("location", "living_room")
    .tag("device", "esp32_01")
    .field("temperature", 23.5)
    .field("humidity", 60.2)
    .field("co2_ppm", 650)
)
write_api.write(bucket=INFLUX_BUCKET, record=point)

AWS IoT Core

AWS IoT Core는 수십억 개의 IoT 기기를 안전하게 연결하고 관리할 수 있는 완전관리형 클라우드 서비스입니다. X.509 인증서 기반 mTLS로 보안을 보장하며, AWS Lambda, DynamoDB, S3 등 AWS 서비스와 통합됩니다.


11. 임베디드 보안

OTA (Over-the-Air) 펌웨어 업데이트

보안 OTA의 핵심은 서명 검증입니다. 서명되지 않은 펌웨어를 거부하여 악성 코드 설치를 방지합니다.

// ESP-IDF OTA 업데이트 예제
#include "esp_ota_ops.h"
#include "esp_https_ota.h"

void ota_task(void *pvParameter) {
    esp_https_ota_config_t ota_config = {
        .http_config = &(esp_http_client_config_t){
            .url = "https://myserver.com/firmware.bin",
            .cert_pem = server_cert_pem,  // 서버 인증서 검증
        },
    };

    esp_err_t ret = esp_https_ota(&ota_config);
    if (ret == ESP_OK) {
        esp_restart();  // 성공시 재부팅
    } else {
        ESP_LOGE("OTA", "Update failed: %s", esp_err_to_name(ret));
    }
    vTaskDelete(NULL);
}

MCU에서 AES 암호화

// mbedTLS AES-128 CBC 암호화 (STM32/ESP32)
#include "mbedtls/aes.h"

void aes_encrypt_data(uint8_t *plaintext, uint8_t *ciphertext,
                       uint8_t *key, uint8_t *iv, size_t len) {
    mbedtls_aes_context ctx;
    mbedtls_aes_init(&ctx);
    mbedtls_aes_setkey_enc(&ctx, key, 128);  // 128비트 키

    uint8_t iv_copy[16];
    memcpy(iv_copy, iv, 16);  // IV는 암호화 과정에서 변경됨

    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, len,
                           iv_copy, plaintext, ciphertext);
    mbedtls_aes_free(&ctx);
}

보안 설계 원칙

  1. 최소 권한 원칙: 필요한 기능만 활성화, 미사용 인터페이스 비활성화
  2. 보안 부트(Secure Boot): 부팅 시 펌웨어 서명 검증
  3. 플래시 암호화: 플래시 메모리에 저장된 코드/데이터 암호화
  4. RDP(Read-Out Protection): JTAG/SWD를 통한 펌웨어 덤프 방지
  5. 난수 생성기(TRNG): 암호화 키 생성에 하드웨어 RNG 사용
  6. 타임아웃 및 워치독: 응답 없는 상태 자동 복구

12. 퀴즈

Q1. MCU와 MPU의 가장 큰 차이점은 무엇인가요?

정답: MCU(마이크로컨트롤러)는 CPU, 메모리(Flash, RAM), 주변장치(UART, SPI, I2C, ADC 등)가 하나의 칩에 통합되어 있습니다. 반면 MPU(마이크로프로세서)는 CPU 코어에 집중하며, 외부 메모리와 주변장치가 별도로 필요합니다.

설명: MCU는 저전력, 소형, 저비용이 요구되는 임베디드 애플리케이션에 적합하고, MPU는 Linux 같은 복잡한 OS를 실행해야 하는 고성능 애플리케이션에 적합합니다.

Q2. RTOS에서 세마포어와 뮤텍스의 차이점은?

정답: 뮤텍스(Mutex)는 **소유권(Ownership)**이 있어 잠근 태스크만 열 수 있으며, 우선순위 역전(Priority Inversion) 방지를 위한 우선순위 상속 메커니즘이 있습니다. 세마포어는 소유권이 없어 다른 태스크나 ISR도 신호를 줄 수 있습니다.

설명: 뮤텍스는 공유 자원의 상호 배제(Mutual Exclusion)에, 세마포어는 태스크 간 동기화나 자원 카운팅에 사용합니다. ISR에서 태스크로 이벤트를 알릴 때는 바이너리 세마포어를 사용합니다.

Q3. I2C와 SPI 통신 방식의 장단점을 비교하세요.

정답:

  • I2C 장점: 2선(SDA, SCL)만 필요, 여러 기기를 같은 버스에 연결 가능 (주소로 구분), 배선이 간단
  • I2C 단점: SPI보다 느림(최대 3.4Mbps), 풀업 저항 필요, 오픈 드레인 방식으로 고속 불리
  • SPI 장점: 매우 빠름(수십 Mbps), 풀 듀플렉스 통신, 하드웨어 구현 단순
  • SPI 단점: 기기마다 CS 핀 필요(핀 수 증가), 주소 개념 없음

설명: 저속 센서(MPU6050, BMP280 등)에는 I2C, 고속 전송이 필요한 디스플레이, SD카드, SPI Flash에는 SPI를 주로 사용합니다.

Q4. 임베디드 시스템에서 volatile 키워드가 중요한 이유는?

정답: 컴파일러는 최적화 과정에서 변수 값이 코드 내에서만 변경된다고 가정합니다. 하지만 하드웨어 레지스터, ISR에서 변경되는 변수, DMA 버퍼 등은 프로그램 흐름 외부에서 값이 바뀔 수 있습니다. volatile 키워드는 컴파일러에게 이 변수를 최적화하지 말고 매번 메모리에서 읽으라고 지시합니다.

설명: volatile 없이 ISR에서 플래그를 설정해도 메인 루프가 캐시된 값을 읽어 인식하지 못하는 버그가 발생할 수 있습니다. 이는 디버거 없이 발견하기 어려운 클래식한 임베디드 버그입니다.

Q5. 엣지 AI에서 모델 양자화(Quantization)를 사용하는 이유는?

정답: 양자화는 모델의 가중치와 활성화값을 32비트 부동소수점(FP32)에서 8비트 정수(INT8)로 변환하는 기법입니다. 이를 통해 모델 크기를 약 4배 줄이고, 추론 속도를 2~4배 향상시키며, 메모리 사용량과 전력 소비를 크게 줄일 수 있습니다.

설명: 임베디드 기기는 메모리(수 MB)와 연산 능력이 제한적입니다. TFLite의 INT8 양자화 모델은 Raspberry Pi나 Cortex-M MCU 같은 저사양 기기에서도 실시간 추론이 가능합니다. 단, 정확도가 약간 감소할 수 있습니다 (일반적으로 1~2% 이내).


참고 자료

Embedded Systems & IoT Complete Guide: From MCU to Edge AI

1. Introduction to Embedded Systems

An embedded system is a dedicated computing system that combines hardware and software to perform specific functions. They are found everywhere — in washing machines, automotive engine control units (ECUs), smart thermostats, medical devices, and billions of other products worldwide.

Microcontroller (MCU) vs Microprocessor (MPU)

FeatureMCUMPU
CPUSimple, low-powerHigh-performance, complex
MemoryIntegrated Flash/RAMRequires external DRAM
OSNone or RTOSLinux, Android, etc.
PowerA few mWSeveral W
ExamplesSTM32, AVR, ESP32Raspberry Pi, i.MX8

Major MCU Platforms

  • STM32 (ST Microelectronics): ARM Cortex-M based, widely used in industrial applications. Backed by HAL libraries and the CubeMX GUI configuration tool.
  • AVR (Microchip/Atmel): The foundation of Arduino UNO. Familiar and beginner-friendly.
  • ESP32 (Espressif): Dual-core Xtensa LX6 with integrated WiFi and BLE. Ideal for IoT projects.
  • RP2040 (Raspberry Pi): Dual-core ARM Cortex-M0+ with unique PIO (Programmable I/O) subsystem.
  • PIC (Microchip): A long-established MCU family with a strong presence in industrial settings.

Development Environment

  • IDEs: STM32CubeIDE, MPLAB X, Arduino IDE, PlatformIO (VS Code extension)
  • Compilers: GCC (arm-none-eabi-gcc), LLVM/Clang
  • Debuggers: JTAG, SWD (Serial Wire Debug), OpenOCD, J-Link, ST-Link

2. Hardware Control in C

The core of embedded C programming is either directly manipulating hardware registers or leveraging a HAL (Hardware Abstraction Layer) library provided by the manufacturer.

GPIO Control

GPIO (General Purpose Input/Output) is the most fundamental interface for digital I/O control.

// GPIO control using STM32 HAL library
#include "stm32f4xx_hal.h"

// Toggle LED on pin PA5
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);

// Read button on pin PC13 (active LOW)
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) {
    // Button is pressed — turn LED on
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}

Direct Register Manipulation

Bypassing the HAL and writing to registers directly yields faster, more compact code.

// Direct register access (STM32F4)
// Enable GPIOA peripheral clock
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// Set PA5 as output (MODER register)
GPIOA->MODER &= ~(0x3 << (5 * 2));  // Clear bits
GPIOA->MODER |=  (0x1 << (5 * 2)); // Output mode

// Set PA5 HIGH
GPIOA->BSRR = (1 << 5);

// Set PA5 LOW
GPIOA->BSRR = (1 << (5 + 16));

The Importance of the volatile Keyword

The compiler may optimize away hardware register accesses, assuming values only change within the program itself. The volatile qualifier prevents this.

// WRONG: compiler may optimize this out entirely
uint32_t *reg = (uint32_t *)0x40020000;
*reg = 0x01;

// CORRECT: declare as volatile
volatile uint32_t *reg = (volatile uint32_t *)0x40020000;
*reg = 0x01;

// Global variables modified in ISRs must also be volatile
volatile uint8_t button_pressed = 0;

3. Interrupts and Timers

Interrupt Service Routines (ISR)

An interrupt is a mechanism that allows a hardware event (button press, timer overflow, data received, etc.) to pause the CPU's current task and execute a dedicated ISR immediately.

// External interrupt callback (HAL style)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_13) {
        button_pressed = 1;
        // Keep ISRs short — never call blocking functions like HAL_Delay()
    }
}

// Timer period elapsed callback (called every 1ms)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        system_tick++;
    }
}

PWM Output

Timer PWM (Pulse Width Modulation) is used for servo motor control, LED brightness dimming, and DC motor speed regulation.

// Start PWM on TIM3 Channel 1
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

// Set 50% duty cycle (when ARR = 999, set CCR = 500)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);

// Servo motor control function (0 to 180 degrees)
void servo_set_angle(uint8_t angle) {
    // Map angle to timer compare value for 1ms~2ms pulse width
    uint32_t pulse = 500 + (angle * 1000 / 180);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
}

4. Serial Communication Protocols

UART/USART

Asynchronous serial communication. Primarily used for debug output, GPS modules, and Bluetooth module connectivity.

// Transmit a string over UART
char msg[] = "Hello, UART!\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100);

// Receive a byte using interrupt mode
uint8_t rx_byte;
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        rx_buffer[rx_index++] = rx_byte;
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
    }
}

SPI Communication

Master-slave synchronous serial communication. Used for high-speed peripherals such as displays, SD cards, and ADCs.

// SPI transmit/receive (reading Flash ID)
uint8_t tx_data[] = {0x9F, 0x00};
uint8_t rx_data[2];

HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);  // CS LOW
HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);    // CS HIGH

I2C Communication

Two-wire (SDA, SCL) bus that supports multiple devices on the same lines. The standard choice for sensor communication.

// Read MPU6050 accelerometer over I2C
#define MPU6050_ADDR  0xD0  // 7-bit address 0x68, write = 0xD0
#define ACCEL_XOUT_H  0x3B

uint8_t data[6];
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, ACCEL_XOUT_H,
                  I2C_MEMADD_SIZE_8BIT, data, 6, 100);

int16_t accel_x = (data[0] << 8) | data[1];
int16_t accel_y = (data[2] << 8) | data[3];
int16_t accel_z = (data[4] << 8) | data[5];

// Convert to real acceleration in g (at ±2g setting)
float ax = accel_x / 16384.0f;

CAN Bus

A robust differential serial communication protocol used in automotive and industrial equipment.

// Send a CAN message (STM32 HAL)
CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t tx_mailbox;

tx_header.StdId = 0x123;
tx_header.IDE   = CAN_ID_STD;
tx_header.RTR   = CAN_RTR_DATA;
tx_header.DLC   = 8;

HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox);

5. RTOS (Real-Time Operating System)

An RTOS guarantees deterministic timing — it ensures that tasks respond to events within a bounded, predictable time. Multiple tasks are scheduled by priority, which is essential for safety-critical and real-time systems.

FreeRTOS Fundamentals

FreeRTOS is the most widely adopted open-source RTOS in the world. Maintained by Amazon, it supports STM32, ESP32, Arduino, and many other platforms.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"

// Task function — blinks an LED
void vTaskLED(void *pvParameters) {
    while(1) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));  // Yield CPU for 500ms
    }
}

// Task function — reads a sensor and posts to a queue
void vTaskSensor(void *pvParameters) {
    QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
    float temperature;

    while(1) {
        temperature = read_temperature_sensor();
        xQueueSend(xQueue, &temperature, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();

    QueueHandle_t xTempQueue = xQueueCreate(10, sizeof(float));

    // Create tasks (function, name, stack depth, param, priority, handle)
    xTaskCreate(vTaskLED,    "LED",    128, NULL,        1, NULL);
    xTaskCreate(vTaskSensor, "Sensor", 256, xTempQueue,  2, NULL);

    vTaskStartScheduler();  // Hand control to the RTOS — never returns
    while(1);
}

Semaphores and Mutexes

SemaphoreHandle_t xMutex;
SemaphoreHandle_t xSemaphore;

// Mutex — protects a shared resource
xMutex = xSemaphoreCreateMutex();

void vTaskA(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // Critical section
            shared_data++;
            xSemaphoreGive(xMutex);
        }
    }
}

// Binary semaphore — synchronizes ISR to task
xSemaphore = xSemaphoreCreateBinary();

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

6. The Arduino Ecosystem

Arduino is an open-source platform designed to lower the barrier to embedded development. It offers a simple API and an enormous library ecosystem.

Arduino Board Comparison

BoardMCUClockFlashRAMNotes
UNO R3ATmega328P16MHz32KB2KBClassic beginner board
NanoATmega328P16MHz32KB2KBSmall form factor
Mega 2560ATmega256016MHz256KB8KBMany I/O pins
DueSAM3X8E84MHz512KB96KB32-bit ARM
UNO R4RA4M148MHz256KB32KBLatest generation

DHT22 + I2C LCD Example

#include <DHT.h>
#include <LiquidCrystal_I2C.h>

#define DHTPIN  2
#define DHTTYPE DHT22

DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2);

void setup() {
    Serial.begin(9600);
    dht.begin();
    lcd.init();
    lcd.backlight();
    lcd.print("IoT Weather Box");
    delay(2000);
    lcd.clear();
}

void loop() {
    float temp = dht.readTemperature();
    float hum  = dht.readHumidity();

    if (isnan(temp) || isnan(hum)) {
        Serial.println("DHT read error!");
        return;
    }

    lcd.setCursor(0, 0);
    lcd.print("Temp: ");
    lcd.print(temp, 1);
    lcd.print(" C  ");

    lcd.setCursor(0, 1);
    lcd.print("Hum:  ");
    lcd.print(hum, 1);
    lcd.print(" %  ");

    Serial.print("T="); Serial.print(temp);
    Serial.print(" H="); Serial.println(hum);

    delay(2000);
}

7. ESP32 & WiFi/BLE IoT

The ESP32 is a highly capable IoT MCU from Espressif. It features a dual-core 240MHz processor, integrated WiFi 802.11 b/g/n, and BLE 4.2/5.0 — all at an exceptionally low price point.

MQTT IoT with MicroPython

# MicroPython ESP32 — publish sensor data over MQTT
import time
import network
from umqtt.simple import MQTTClient
from machine import Pin, ADC

def connect_wifi(ssid, password):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(ssid, password)
        while not wlan.isconnected():
            time.sleep(0.5)
    print('WiFi connected:', wlan.ifconfig())

def on_message(topic, msg):
    print('Topic:', topic, 'Message:', msg)

connect_wifi('MySSID', 'MyPassword')

adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)

client = MQTTClient('esp32_sensor', 'broker.hivemq.com', port=1883)
client.set_callback(on_message)
client.connect()
client.subscribe(b'device/control')

while True:
    raw = adc.read()
    voltage = raw * 3.3 / 4095
    temp_approx = (voltage - 0.5) * 100
    payload = '{:.1f}'.format(temp_approx)
    client.publish(b'sensor/temperature', payload.encode())
    client.check_msg()
    time.sleep(5)

ESP-IDF WiFi (C/C++)

// ESP-IDF WiFi station initialization
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

#define WIFI_SSID "MyNetwork"
#define WIFI_PASS "MyPassword"

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;
        ESP_LOGI("WiFi", "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
    }
}

void wifi_init_sta(void) {
    nvs_flash_init();
    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_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL);
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL);

    wifi_config_t wifi_config = {
        .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    esp_wifi_start();
}

8. Raspberry Pi & Linux Embedded

The Raspberry Pi is a single-board computer (SBC) that runs a full Linux operating system. It sits at the intersection of embedded development and general computing, making it a uniquely powerful and flexible platform.

Raspberry Pi Model Comparison

ModelCPURAMHighlights
Pi Zero 2WARM Cortex-A53 quad 1GHz512MBUltra-compact, WiFi/BLE
Pi 4 Model BARM Cortex-A72 quad 1.8GHz1–8GBGeneral-purpose powerhouse
Pi 5ARM Cortex-A76 quad 2.4GHz4–8GBLatest generation
Pi Pico 2RP2350 dual Cortex-M33520KBMicrocontroller

GPIO and Servo Motor Control

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)

pwm = GPIO.PWM(18, 50)  # 50Hz for servo
pwm.start(7.5)          # Neutral position (90 degrees)

def set_servo_angle(angle):
    """Set servo angle from 0 to 180 degrees."""
    duty = 2.5 + (angle / 180.0) * 10.0
    pwm.ChangeDutyCycle(duty)
    time.sleep(0.3)

try:
    for angle in [0, 45, 90, 135, 180]:
        set_servo_angle(angle)
        print(f"Angle: {angle} degrees")
        time.sleep(1)
finally:
    pwm.stop()
    GPIO.cleanup()

gpiozero Library

from gpiozero import LED, Button, DistanceSensor
import time

led    = LED(17)
button = Button(4)

button.when_pressed  = led.on
button.when_released = led.off

# HC-SR04 ultrasonic distance sensor
sensor = DistanceSensor(echo=24, trigger=23)
while True:
    dist = sensor.distance * 100  # convert to cm
    print(f"Distance: {dist:.1f} cm")
    if dist < 20:
        led.blink(on_time=0.1, off_time=0.1)
    time.sleep(0.5)

Deploying with Docker

# Dockerfile for a Raspberry Pi IoT app
FROM python:3.11-slim-bullseye

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "sensor_server.py"]
# docker-compose.yml — full IoT stack
version: '3'
services:
  sensor-app:
    build: .
    privileged: true # Required for GPIO access
    volumes:
      - /dev:/dev
    restart: unless-stopped

  influxdb:
    image: influxdb:2.7
    ports:
      - '8086:8086'
    volumes:
      - influx_data:/var/lib/influxdb2

  grafana:
    image: grafana/grafana:latest
    ports:
      - '3000:3000'
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  influx_data:
  grafana_data:

9. Edge AI

Edge AI refers to running AI inference directly on local devices rather than in the cloud. The key benefits are ultra-low latency, offline operation, enhanced privacy, and reduced bandwidth consumption.

TensorFlow Lite (TFLite)

TFLite is a lightweight ML framework optimized for mobile and embedded devices.

# TFLite image classification on Raspberry Pi
import tflite_runtime.interpreter as tflite
import numpy as np
from PIL import Image
import time

# Load and initialize the interpreter
interpreter = tflite.Interpreter(
    model_path='mobilenet_v2_1.0_224_quant.tflite',
    num_threads=4
)
interpreter.allocate_tensors()

input_details  = interpreter.get_input_details()
output_details = interpreter.get_output_details()

print("Input shape:", input_details[0]['shape'])   # [1, 224, 224, 3]
print("Input dtype:", input_details[0]['dtype'])   # uint8 for quantized model

def classify_image(image_path, labels):
    img = Image.open(image_path).convert('RGB').resize((224, 224))
    input_data = np.expand_dims(np.array(img), axis=0)

    interpreter.set_tensor(input_details[0]['index'], input_data)

    start = time.time()
    interpreter.invoke()
    elapsed = (time.time() - start) * 1000

    output  = interpreter.get_tensor(output_details[0]['index'])
    top_idx = np.argmax(output[0])

    print(f"Label: {labels[top_idx]}, Score: {output[0][top_idx]}")
    print(f"Inference time: {elapsed:.1f}ms")

with open('labels.txt') as f:
    labels = [line.strip() for line in f.readlines()]

classify_image('test.jpg', labels)

ONNX Runtime

# Running a custom model with ONNX Runtime
import onnxruntime as ort
import numpy as np
from PIL import Image

session = ort.InferenceSession(
    'model.onnx',
    providers=['CPUExecutionProvider']
)

input_name  = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
input_shape = session.get_inputs()[0].shape
print(f"Input: {input_name}, Shape: {input_shape}")

# Preprocess image
img       = Image.open('test.jpg').resize((224, 224))
img_array = np.array(img).astype(np.float32) / 255.0
img_array = np.transpose(img_array, (2, 0, 1))  # HWC to CHW
img_array = np.expand_dims(img_array, axis=0)   # Add batch dimension

result = session.run([output_name], {input_name: img_array})
print("Output shape:", result[0].shape)

Edge AI Platform Comparison

PlatformVendorAI PerformancePowerNotes
Jetson Orin NanoNVIDIA40 TOPS7–15WCUDA GPU on-chip
Coral Dev BoardGoogle4 TOPS~2WDedicated Edge TPU
RK3588 boardRockchip6 TOPS NPU~8WExcellent value
Hailo-8 M.2Hailo26 TOPS2.5WPCIe add-in module
Pi 5 + AI HAT+RPi Foundation26 TOPS~5WPi ecosystem native

10. IoT Communication Protocols & Cloud

MQTT

MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe protocol optimized for IoT. A broker decouples publishers from subscribers, enabling scalable and efficient messaging.

# Python Paho-MQTT client
import paho.mqtt.client as mqtt
import json
import time

BROKER    = "broker.hivemq.com"
PORT      = 1883
TOPIC_PUB = "home/sensor/living_room"
TOPIC_SUB = "home/control/#"

def on_connect(client, userdata, flags, rc):
    print(f"Connected with code: {rc}")
    client.subscribe(TOPIC_SUB)

def on_message(client, userdata, msg):
    payload = json.loads(msg.payload.decode())
    print(f"Topic: {msg.topic}, Payload: {payload}")

client = mqtt.Client("python_sensor_01")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()

while True:
    data = {
        "temperature": 23.5,
        "humidity": 60.2,
        "timestamp": int(time.time())
    }
    client.publish(TOPIC_PUB, json.dumps(data))
    time.sleep(10)

InfluxDB + Grafana

# Write sensor data to InfluxDB 2.x
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS

INFLUX_URL    = "http://localhost:8086"
INFLUX_TOKEN  = "your_token_here"
INFLUX_ORG    = "myorg"
INFLUX_BUCKET = "iot_sensors"

client    = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = client.write_api(write_options=SYNCHRONOUS)

point = (
    Point("environment")
    .tag("location", "living_room")
    .tag("device", "esp32_01")
    .field("temperature", 23.5)
    .field("humidity", 60.2)
    .field("co2_ppm", 650)
)
write_api.write(bucket=INFLUX_BUCKET, record=point)

Cloud IoT Services

  • AWS IoT Core: Fully managed, supports billions of devices. Uses X.509 certificate-based mTLS. Integrates natively with Lambda, DynamoDB, S3, and the broader AWS ecosystem.
  • Azure IoT Hub: Enterprise-grade device management with built-in device twins and direct method invocation.
  • Google Cloud IoT: Integrates with Pub/Sub, BigQuery, and Vertex AI for end-to-end ML pipelines.

11. Embedded Security

OTA (Over-the-Air) Firmware Updates

Secure OTA is centered on signature verification — rejecting unsigned or tampered firmware to prevent malicious code installation.

// ESP-IDF HTTPS OTA update
#include "esp_ota_ops.h"
#include "esp_https_ota.h"

void ota_task(void *pvParameter) {
    esp_https_ota_config_t ota_config = {
        .http_config = &(esp_http_client_config_t){
            .url      = "https://myserver.com/firmware.bin",
            .cert_pem = server_cert_pem,  // Server certificate verification
        },
    };

    esp_err_t ret = esp_https_ota(&ota_config);
    if (ret == ESP_OK) {
        esp_restart();
    } else {
        ESP_LOGE("OTA", "Update failed: %s", esp_err_to_name(ret));
    }
    vTaskDelete(NULL);
}

AES Encryption on MCU

// mbedTLS AES-128 CBC encryption (STM32/ESP32)
#include "mbedtls/aes.h"

void aes_encrypt_data(uint8_t *plaintext, uint8_t *ciphertext,
                       uint8_t *key, uint8_t *iv, size_t len) {
    mbedtls_aes_context ctx;
    mbedtls_aes_init(&ctx);
    mbedtls_aes_setkey_enc(&ctx, key, 128);  // 128-bit key

    uint8_t iv_copy[16];
    memcpy(iv_copy, iv, 16);  // IV is mutated during encryption

    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, len,
                           iv_copy, plaintext, ciphertext);
    mbedtls_aes_free(&ctx);
}

Security Design Principles

  1. Principle of Least Privilege: Enable only required features; disable unused interfaces.
  2. Secure Boot: Verify firmware signature at every boot.
  3. Flash Encryption: Encrypt all code and data stored in flash memory.
  4. Read-Out Protection (RDP): Block JTAG/SWD firmware extraction.
  5. Hardware RNG (TRNG): Use the on-chip true random number generator for cryptographic keys.
  6. Watchdog Timer: Automatically recover from hangs and unresponsive states.

12. Quiz

Q1. What is the fundamental difference between an MCU and an MPU?

Answer: An MCU (Microcontroller Unit) integrates a CPU, memory (Flash and RAM), and peripherals (UART, SPI, I2C, ADC, etc.) into a single chip. An MPU (Microprocessor Unit), by contrast, focuses on the CPU core and requires external memory and peripheral chips.

Explanation: MCUs are ideal for low-power, compact, cost-sensitive embedded applications. MPUs are suited for applications that need to run complex operating systems like Linux, such as gateways, HMI panels, or media devices.

Q2. What is the difference between a semaphore and a mutex in an RTOS?

Answer: A mutex has ownership — only the task that locked it can unlock it, and it supports priority inheritance to prevent priority inversion. A binary semaphore has no ownership — any task or ISR can signal it.

Explanation: Use a mutex to protect shared resources (mutual exclusion). Use a binary semaphore to synchronize tasks with events, especially for signaling from an ISR to a task. Using a mutex from within an ISR is incorrect and can cause deadlocks.

Q3. Compare the trade-offs between I2C and SPI communication.

Answer:

  • I2C pros: Only 2 wires (SDA + SCL), multiple devices on one bus (addressed by 7-bit ID), simple wiring.
  • I2C cons: Slower than SPI (max ~3.4Mbps), requires pull-up resistors, open-drain bus.
  • SPI pros: Very fast (tens of Mbps), full-duplex, electrically simple.
  • SPI cons: Requires a dedicated Chip Select (CS) pin per device (more pins), no addressing scheme.

Explanation: I2C is preferred for low-speed sensors (MPU6050, BMP280, etc.) where wiring simplicity matters. SPI is used for high-speed peripherals like TFT displays, SD cards, and SPI flash memory.

Q4. Why is the volatile keyword critical in embedded C programming?

Answer: The compiler assumes a variable's value only changes through code in the current execution path. It may therefore cache values in registers and skip re-reading from memory. However, hardware registers, variables modified by ISRs, and DMA buffers can change at any time outside the normal program flow. The volatile qualifier instructs the compiler never to optimize these reads — always re-fetch from memory.

Explanation: Without volatile, a flag set inside an ISR may never be seen by the main loop, because the compiler has optimized the load into a register read that only happens once. This is one of the most notorious and hard-to-debug bugs in embedded systems.

Q5. Why is model quantization used for Edge AI deployment?

Answer: Quantization converts model weights and activations from 32-bit floating-point (FP32) to 8-bit integers (INT8). This reduces model size by approximately 4x, improves inference speed by 2–4x, and significantly cuts memory usage and power consumption.

Explanation: Embedded devices have severely constrained memory (a few MB) and compute resources. A TFLite INT8 quantized model can run in real time on a Raspberry Pi or even a Cortex-M MCU. The accuracy trade-off is usually small (within 1–2% of the original FP32 model) and acceptable for most production use cases.


References