[CS300 #219] 임베디드와 IoT 기초 — 작은 기계가 현실을 읽고 움직이는 법
컴퓨터공학 300 주제 시리즈의 219번째 글이다. 전체 지도는 여기.
한 줄 요약
임베디드 시스템은 특정 기능을 위해 기기 안에 들어간 컴퓨터이고, IoT 는 그런 기기가 네트워크로 연결되어 데이터를 보내고 원격으로 제어되는 체계다. 핵심은 제한된 자원(메모리·전력) 안에서 센서를 읽고(입력), 판단하고, 액추에이터를 움직이는(출력) 일을 정해진 시간 안에 오래 안정적으로 하는 것이다.
왜 필요한가
웹·모바일 개발자에게도 임베디드는 가깝다. 스마트 플러그, 온습도계, 출입 카드 리더기, 공장 설비 센서가 보내는 데이터를 받는 쪽은 결국 웹 서버와 앱이다. 그리고 그 데이터는 기기 쪽 사정을 모르면 해석할 수 없다.
- 센서 값이 튀는 것은 서버 버그인가, 센서 잡음인가.
- 기기가 하루에 몇 번씩 재부팅되는 것은 네트워크 문제인가, 워치독 리셋인가.
- 배터리 기기에 “1초마다 상태 전송”을 요구하면 배터리가 얼마나 버티는가.
일반 서버와 비교하면 차이가 크다. 메모리는 기가바이트가 아니라 킬로바이트 단위일 수 있고, 운영체제가 없거나 아주 작다. 디스크가 없을 수 있다. 장애가 나도 누가 가서 재부팅해 주지 않는다. 그래서 설계 습관이 다르다.
핵심 개념
마이크로컨트롤러와 마이크로프로세서
| 구분 | 마이크로컨트롤러(MCU) | 애플리케이션 프로세서(SoC) |
|---|---|---|
| 예 | Arduino 보드의 AVR, ESP32, RP2040 | 라즈베리 파이의 ARM SoC, 스마트폰 칩 |
| 메모리 | 칩 안에 작은 RAM·플래시 | 외부 DRAM, 저장 장치 |
| 운영체제 | 없음(베어메탈) 또는 RTOS | 리눅스 같은 범용 OS |
| 부팅 | 전원 켜면 바로 | 수 초 이상 |
| 전력 | 아주 적음, 깊은 절전 모드 | 상대적으로 많음 |
| 적합 | 센서 읽기, 모터 제어, 배터리 기기 | 카메라 처리, 게이트웨이, 복잡한 앱 |
실제 IoT 시스템은 둘을 섞는다. 배터리로 도는 MCU 센서 여러 개가 무선으로 리눅스 게이트웨이에 데이터를 보내고, 게이트웨이가 클라우드나 서버로 전달하는 식이다.
입출력: 현실 세계와 만나는 핀
| 인터페이스 | 무엇 | 예 |
|---|---|---|
| GPIO | 디지털 입력·출력 (켜짐/꺼짐) | 버튼, LED, 릴레이 |
| ADC | 아날로그 전압을 숫자로 | 온도·조도 센서, 배터리 전압 |
| PWM | 켜짐 비율로 평균 전력 조절 | LED 밝기, 모터 속도, 서보 |
| UART | 두 선 비동기 직렬 통신 | GPS 모듈, 디버그 콘솔 |
| I2C | 두 선(데이터·클록) 버스, 주소로 여러 장치 | 센서, 작은 디스플레이 |
| SPI | 클록·데이터 여러 선, 빠른 전송 | SD 카드, 디스플레이 |
ADC 의 분해능은 비트 수로 말한다. 12비트 ADC 는 0~4095 의 값을 낸다. 같은 전압 범위를 더 많은 단계로 나눌수록 미세한 변화를 구분한다. 하지만 실제 값에는 언제나 잡음이 섞인다.
실행 모델: 슈퍼루프, 인터럽트, RTOS
가장 단순한 펌웨어 구조는 무한 반복하는 슈퍼루프다. Arduino 의 setup() 과 loop() 가 이 구조다.
void setup() { /* 핀 설정, 센서 초기화 */ }
void loop() {
read_sensors();
update_control();
send_if_due();
}
버튼 누름처럼 놓치면 안 되는 사건은 인터럽트로 처리한다. 하드웨어가 신호를 감지하면 CPU 가 하던 일을 멈추고 인터럽트 처리 함수를 실행한다. 이 함수는 아주 짧아야 한다. 플래그만 세우고 실제 처리는 메인 루프에서 하는 것이 원칙이다.
할 일이 많아지면 RTOS(실시간 운영체제)를 쓴다. FreeRTOS 같은 RTOS 는 태스크, 우선순위 기반 스케줄링, 큐·세마포어를 제공한다. 여기서 “실시간”은 빠르다는 뜻이 아니라 마감 시간 안에 반응한다는 보장이 핵심이다. 6부 운영체제에서 본 스케줄링·동기화 개념이 아주 작은 규모로 그대로 나타난다.
잡음과 떨림을 다루는 습관
- 필터링: 센서 값은 이동 평균이나 중앙값 필터로 다듬는다.
- 디바운싱: 기계식 버튼은 눌리는 순간 수 밀리초 동안 켜짐/꺼짐이 여러 번 튄다. 일정 시간 안정된 뒤에만 상태를 바꾼다.
- 히스테리시스: “28도 넘으면 팬 켜기” 하나로 제어하면, 온도가 28도 근처에서 오르내릴 때 릴레이가 계속 딸깍거린다. 켜는 기준(28.5도)과 끄는 기준(27.5도)을 다르게 둔다. 가정용 온도 조절기도 같은 원리다.
오래 혼자 버티기
- 워치독 타이머: 정해진 시간 안에 프로그램이 “살아 있다” 신호(feed)를 주지 않으면 하드웨어가 기기를 리셋한다. 무한 루프·교착에 빠진 기기를 스스로 되살린다.
- 전력 관리: 배터리 기기는 대부분의 시간을 깊은 절전 상태로 보내고, 잠깐 깨어 측정·전송한 뒤 다시 잔다. 무선 전송이 가장 비싼 동작인 경우가 많아, 전송 빈도가 배터리 수명을 좌우한다.
- OTA 업데이트: 현장에 깔린 기기를 원격으로 업데이트하는 기능이다. 업데이트 도중 전원이 끊겨도 벽돌이 되지 않도록 두 개의 펌웨어 영역(A/B)을 두고, 새 펌웨어가 정상 부팅을 확인한 뒤에만 전환하는 방식을 쓴다.
보안
IoT 기기는 오래, 사람의 관리 없이 네트워크에 붙어 있다. NIST IR 8259 는 IoT 기기 제조사가 출시 전후에 해야 할 기본 보안 활동을 정리하고, 짝이 되는 NIST IR 8259A 는 기기가 기본으로 갖춰야 할 보안 기능의 기준선(core baseline)을 정의한다. 실무에서는 다음이 출발점이다.
- 모든 기기에 같은 기본 비밀번호를 쓰지 않는다.
- 통신은 TLS 로 암호화하고, 기기별 인증 정보를 쓴다.
- 서명된 펌웨어만 설치되게 한다.
- 기기를 일반 업무망과 분리된 네트워크 구간에 둔다.
직접 해 보기
실제 보드 없이, 슈퍼루프 안에서 잡음 낀 12비트 ADC 온도 값을 읽어 팬 릴레이를 제어하는 상황을 흉내 낸다. 순진한 단일 기준 제어와 “이동 평균 + 히스테리시스” 제어의 릴레이 전환 횟수를 비교한다.
import math, random
from collections import deque
random.seed(3)
def read_adc(t):
"""12비트 ADC(0~4095) 로 읽은 온도 센서 값이라고 가정: 실제 온도 + 잡음."""
true_c = 27 + 2.5 * math.sin(t / 40) # 천천히 오르내리는 실제 온도
raw = int((true_c / 50) * 4095 + random.gauss(0, 60)) # 0~50도를 0~4095 로
return max(0, min(4095, raw)), true_c
def to_celsius(raw):
return raw / 4095 * 50
window = deque(maxlen=8) # 이동 평균 창
fan_naive = fan_hyst = False
toggles_naive = toggles_hyst = 0
ON, OFF = 28.5, 27.5 # 히스테리시스: 켜는 기준과 끄는 기준을 다르게
for tick in range(400): # 슈퍼루프: 100ms 마다 한 바퀴라고 가정
raw, true_c = read_adc(tick)
c = to_celsius(raw)
window.append(c)
avg = sum(window) / len(window)
# 순진한 제어: 잡음 낀 값으로 28도 하나만 비교
want = c > 28
if want != fan_naive: fan_naive, toggles_naive = want, toggles_naive + 1
# 개선한 제어: 평균값 + 히스테리시스
if not fan_hyst and avg > ON: fan_hyst, toggles_hyst = True, toggles_hyst + 1
elif fan_hyst and avg < OFF: fan_hyst, toggles_hyst = False, toggles_hyst + 1
if tick % 80 == 0:
print(f"t={tick/10:5.1f}s raw={raw:4d} 측정={c:5.2f}℃ 평균={avg:5.2f}℃ 실제={true_c:5.2f}℃")
print(f"팬 릴레이 전환 횟수: 순진한 제어 {toggles_naive}회, 평균+히스테리시스 {toggles_hyst}회")
실행 결과(난수 시드를 고정해 재현 가능):
t= 0.0s raw=2216 측정=27.06℃ 평균=27.06℃ 실제=27.00℃
t= 8.0s raw=2520 측정=30.77℃ 평균=29.55℃ 실제=29.27℃
t= 16.0s raw=2105 측정=25.70℃ 평균=25.40℃ 실제=25.11℃
t= 24.0s raw=2093 측정=25.56℃ 평균=25.92℃ 실제=26.30℃
t= 32.0s raw=2421 측정=29.56℃ 평균=29.45℃ 실제=29.47℃
팬 릴레이 전환 횟수: 순진한 제어 76회, 평균+히스테리시스 4회
같은 온도 변화인데 순진한 제어는 40초 동안 릴레이를 76번 껐다 켰고, 개선한 제어는 4번만 바꿨다. 실제 릴레이는 전환 횟수만큼 수명이 줄고, 모터는 잦은 기동에 열이 난다. 측정값 한 개는 실제 온도와 1도 넘게 차이 날 때도 있지만, 8개 평균은 실제 값에 훨씬 가깝다.
실제 보드에서는 MicroPython 으로 거의 같은 코드를 쓸 수 있다. 아래는 MicroPython 문서의 machine 모듈 API 를 쓴 형태이며, 핀 번호는 보드마다 다르다(여기서는 실행하지 않았다).
from machine import ADC, Pin, WDT
import time
adc = ADC(Pin(26)) # read_u16() 은 0~65535 로 스케일된 값을 준다
fan = Pin(15, Pin.OUT)
wdt = WDT(timeout=2000) # 2초 안에 feed() 가 없으면 리셋
while True:
raw = adc.read_u16()
# ... 평균·히스테리시스 계산 후 fan.value(1) 또는 fan.value(0)
wdt.feed()
time.sleep_ms(100)
현업에서는
- “데이터가 이상해요”의 절반은 기기 쪽: 서버 대시보드에 튀는 값이 보이면, 기기에서 필터링을 했는지, 시계가 맞는지(RTC·NTP), 재부팅 직후 첫 값인지부터 확인한다. 기기가 보내는 메시지에 펌웨어 버전과 부팅 후 경과 시간을 넣어 두면 원인 추적이 쉬워진다.
- 현장 기기 업데이트: OTA 는 단계적으로 배포한다. 일부 기기에 먼저 올려 재부팅·오류율을 본 뒤 넓힌다. 웹 서비스의 카나리 배포와 같은 생각이다.
- 홈랩의 IoT 게이트웨이: 집 안 센서들을 무선으로 모으는 게이트웨이와, 그 데이터를 받는 메시지 브로커·시계열 DB 를 홈랩 k3s 클러스터에 두는 구성이 흔하다. 이때 센서 기기들은 일반 PC·서버와 다른 네트워크 구간에 두고, 브로커 접근은 인증으로 제한한다. 기기 쪽 통신 프로토콜로는 다음 글의 MQTT 가 가장 널리 쓰인다.
- 시간이 곧 사양: “버튼을 누르면 200ms 안에 문이 열린다” 같은 요구는 평균이 아니라 최악의 경우로 검증한다. 임베디드에서 지연은 기능의 일부다.
확인 문제
- 마이크로컨트롤러와 리눅스가 도는 애플리케이션 프로세서의 차이를 메모리·운영체제·전력 관점에서 설명하라.
- 인터럽트 처리 함수를 짧게 유지해야 하는 이유는?
- RTOS 의 “실시간”은 무엇을 보장한다는 뜻인가.
- 온도 제어에서 히스테리시스를 두는 이유는?
- 워치독 타이머는 어떤 상황에서 기기를 살리는가.
풀이
- MCU 는 칩 안의 작은 RAM·플래시로 OS 없이(또는 RTOS 로) 돌고 전력 소모가 매우 적다. 애플리케이션 프로세서는 외부 DRAM·저장 장치와 리눅스 같은 범용 OS 를 쓰며 전력을 더 쓴다.
- 인터럽트 처리 중에는 메인 흐름과 다른 인터럽트가 지연되므로 길어지면 다른 사건을 놓치거나 타이밍이 깨진다. 플래그만 세우고 처리는 메인 루프에서 한다.
- 빠르다는 뜻이 아니라, 정해진 마감 시간 안에 반응하는 것을 예측 가능하게 보장한다는 뜻이다.
- 기준값 근처에서 값이 오르내릴 때 출력이 계속 켜졌다 꺼지는 떨림(채터링)을 막기 위해서다.
- 프로그램이 무한 루프·교착·멈춤으로 정해진 시간 안에 신호를 주지 못할 때, 하드웨어가 자동으로 리셋해 사람 개입 없이 복구한다.
더 읽을거리 (References)
- FreeRTOS, The FreeRTOS Kernel: https://www.freertos.org/Documentation/02-Kernel/01-About-the-FreeRTOS-kernel/01-FreeRTOS-kernel
- MicroPython, class WDT – watchdog timer: https://docs.micropython.org/en/latest/library/machine.WDT.html
- Arduino, loop(): https://docs.arduino.cc/language-reference/en/structure/sketch/loop/
- NIST IR 8259, Foundational Cybersecurity Activities for IoT Device Manufacturers: https://csrc.nist.gov/pubs/ir/8259/final