고급 임베디드 프로토콜

고급 임베디드 프로토콜

개요

이 장에서는 임베디드 시스템에서 자주 사용되는 통신 프로토콜과 하드웨어 제어 기법을 학습합니다. PWM, I2C, SPI, ADC 등 실제 센서와 액추에이터를 제어하는 데 필요한 핵심 기술을 다룹니다.

난이도: ⭐⭐⭐⭐

선수 지식: 임베디드 기초, 비트 연산, 포인터


목차

  1. PWM (Pulse Width Modulation)
  2. 타이머와 인터럽트
  3. I2C 통신
  4. SPI 통신
  5. ADC (Analog to Digital Converter)
  6. 실전 프로젝트

PWM (Pulse Width Modulation)

PWM이란?

PWM은 디지털 신호의 ON/OFF 비율을 조절하여 아날로그와 유사한 효과를 내는 기술입니다.

듀티 사이클 25%:
████________________████________________
|<-- Period -->|

듀티 사이클 50%:
████████________████████________
|<-- Period -->|

듀티 사이클 75%:
████████████____████████████____
|<-- Period -->|

PWM 용어

용어 설명
주기 (Period) 한 사이클의 전체 시간
듀티 사이클 HIGH 상태의 비율 (%)
주파수 초당 사이클 수 (Hz)

Arduino PWM

// Arduino PWM 기본
const int LED_PIN = 9;  // PWM 지원 핀

void setup() {
    pinMode(LED_PIN, OUTPUT);
}

void loop() {
    // 0-255 값으로 밝기 조절
    for (int brightness = 0; brightness <= 255; brightness++) {
        analogWrite(LED_PIN, brightness);
        delay(10);
    }

    for (int brightness = 255; brightness >= 0; brightness--) {
        analogWrite(LED_PIN, brightness);
        delay(10);
    }
}

서보 모터 제어

#include <Servo.h>

Servo myServo;
const int SERVO_PIN = 9;

void setup() {
    myServo.attach(SERVO_PIN);
}

void loop() {
    // 0도에서 180도로 이동
    for (int angle = 0; angle <= 180; angle++) {
        myServo.write(angle);
        delay(15);
    }

    // 180도에서 0도로 이동
    for (int angle = 180; angle >= 0; angle--) {
        myServo.write(angle);
        delay(15);
    }
}

DC 모터 속도 제어

const int MOTOR_PIN = 9;     // PWM 핀
const int DIR_PIN = 8;       // 방향 제어 핀
const int POT_PIN = A0;      // 가변저항

void setup() {
    pinMode(MOTOR_PIN, OUTPUT);
    pinMode(DIR_PIN, OUTPUT);
}

void loop() {
    int potValue = analogRead(POT_PIN);  // 0-1023
    int speed = map(potValue, 0, 1023, 0, 255);

    digitalWrite(DIR_PIN, HIGH);  // 정방향
    analogWrite(MOTOR_PIN, speed);

    delay(100);
}

소프트웨어 PWM

// 하드웨어 PWM이 없는 핀에서 사용
#include <avr/io.h>
#include <util/delay.h>

void software_pwm(volatile uint8_t *port, uint8_t pin,
                  uint8_t duty, uint16_t period_us) {
    uint16_t on_time = (period_us * duty) / 100;
    uint16_t off_time = period_us - on_time;

    *port |= (1 << pin);   // HIGH
    _delay_us(on_time);
    *port &= ~(1 << pin);  // LOW
    _delay_us(off_time);
}

int main(void) {
    DDRB |= (1 << PB0);  // 출력 설정

    while (1) {
        software_pwm(&PORTB, PB0, 30, 1000);  // 30% 듀티
    }
    return 0;
}

타이머와 인터럽트

타이머 개요

┌─────────────┐     클럭     ┌─────────────┐
│  클럭 소스   │────────────▶│   타이머     │
└─────────────┘              │  카운터      │
                             └──────┬──────┘
                                    │
                             ┌──────▼──────┐
                             │   비교기     │──▶ 인터럽트/PWM
                             └─────────────┘

AVR 타이머 설정

#include <avr/io.h>
#include <avr/interrupt.h>

// Timer1 CTC 모드로 1초마다 인터럽트
void timer1_init(void) {
    // CTC 모드 설정
    TCCR1B |= (1 << WGM12);

    // 비교값 설정 (16MHz / 256 / 62500 = 1Hz)
    OCR1A = 62500 - 1;

    // 프리스케일러 256
    TCCR1B |= (1 << CS12);

    // 비교 일치 인터럽트 활성화
    TIMSK1 |= (1 << OCIE1A);

    // 전역 인터럽트 활성화
    sei();
}

// 인터럽트 서비스 루틴
ISR(TIMER1_COMPA_vect) {
    PORTB ^= (1 << PB0);  // LED 토글
}

int main(void) {
    DDRB |= (1 << PB0);
    timer1_init();

    while (1) {
        // 메인 루프
    }
    return 0;
}

Arduino 타이머 인터럽트

// TimerOne 라이브러리 사용
#include <TimerOne.h>

const int LED_PIN = 13;
volatile bool ledState = false;

void timerISR() {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
}

void setup() {
    pinMode(LED_PIN, OUTPUT);

    Timer1.initialize(500000);  // 500ms (마이크로초 단위)
    Timer1.attachInterrupt(timerISR);
}

void loop() {
    // 다른 작업 수행
}

외부 인터럽트

const int BUTTON_PIN = 2;  // INT0
const int LED_PIN = 13;
volatile int buttonCount = 0;

void buttonISR() {
    buttonCount++;
    digitalWrite(LED_PIN, buttonCount % 2);
}

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);

    // FALLING: HIGH→LOW 전환 시 인터럽트
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN),
                    buttonISR, FALLING);
}

void loop() {
    Serial.println(buttonCount);
    delay(500);
}

I2C 통신

I2C 개요

I2C는 2선식 동기 통신 프로토콜입니다.

        VCC
         │
        ┌┴┐
        │R│ 풀업 저항
        └┬┘
         │
Master ──┼─────────────────── SDA (데이터)
         │
        ┌┴┐
        │R│ 풀업 저항
        └┬┘
         │
Master ──┼─────────────────── SCL (클럭)
         │
      ┌──┴──┐   ┌──┴──┐
      │Slave│   │Slave│
      │ 0x48│   │ 0x50│
      └─────┘   └─────┘

I2C 특징

특징 설명
선 수 2개 (SDA, SCL)
속도 100kHz (표준), 400kHz (고속)
주소 7비트 (128개 장치)
마스터/슬레이브 다중 마스터 지원

Arduino Wire 라이브러리

#include <Wire.h>

// I2C 마스터로 데이터 전송
void sendToSlave(uint8_t address, uint8_t *data, size_t len) {
    Wire.beginTransmission(address);
    Wire.write(data, len);
    Wire.endTransmission();
}

// I2C 마스터로 데이터 수신
void readFromSlave(uint8_t address, uint8_t *buffer, size_t len) {
    Wire.requestFrom(address, len);

    size_t i = 0;
    while (Wire.available() && i < len) {
        buffer[i++] = Wire.read();
    }
}

void setup() {
    Wire.begin();  // 마스터로 초기화
    Serial.begin(9600);
}

void loop() {
    uint8_t data[] = {0x01, 0x02, 0x03};
    sendToSlave(0x48, data, sizeof(data));

    uint8_t buffer[4];
    readFromSlave(0x48, buffer, 4);

    for (int i = 0; i < 4; i++) {
        Serial.print(buffer[i], HEX);
        Serial.print(" ");
    }
    Serial.println();

    delay(1000);
}

I2C 온도 센서 (LM75)

#include <Wire.h>

const uint8_t LM75_ADDR = 0x48;
const uint8_t TEMP_REG = 0x00;

float readTemperature() {
    Wire.beginTransmission(LM75_ADDR);
    Wire.write(TEMP_REG);
    Wire.endTransmission();

    Wire.requestFrom(LM75_ADDR, (uint8_t)2);

    if (Wire.available() >= 2) {
        int16_t temp = Wire.read() << 8;
        temp |= Wire.read();
        temp >>= 5;  // 11비트 데이터

        return temp * 0.125;  // 0.125°C 단위
    }

    return 0.0;
}

void setup() {
    Wire.begin();
    Serial.begin(9600);
}

void loop() {
    float temp = readTemperature();
    Serial.print("Temperature: ");
    Serial.print(temp);
    Serial.println(" C");
    delay(1000);
}

I2C OLED 디스플레이

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,
                          &Wire, OLED_RESET);

void setup() {
    if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println("SSD1306 allocation failed");
        for (;;);
    }

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.println("Hello, World!");
    display.display();
}

void loop() {
    static int count = 0;

    display.clearDisplay();
    display.setCursor(0, 0);
    display.print("Count: ");
    display.println(count++);
    display.display();

    delay(1000);
}

SPI 통신

SPI 개요

SPI는 4선식 고속 동기 통신 프로토콜입니다.

Master              Slave
  │                   │
  ├───── MOSI ───────▶│  (Master Out, Slave In)
  │                   │
  │◀───── MISO ───────┤  (Master In, Slave Out)
  │                   │
  ├───── SCK ────────▶│  (클럭)
  │                   │
  ├───── SS ─────────▶│  (슬레이브 선택)
  │                   │

SPI vs I2C

특징 SPI I2C
선 수 4개 2개
속도 더 빠름 (수 MHz) 느림 (400kHz)
거리 짧음 중간
장치 수 SS 핀 수에 따라 주소로 127개

Arduino SPI 사용

#include <SPI.h>

const int SS_PIN = 10;

void setup() {
    pinMode(SS_PIN, OUTPUT);
    digitalWrite(SS_PIN, HIGH);  // 비활성화

    SPI.begin();
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
}

uint8_t spiTransfer(uint8_t data) {
    digitalWrite(SS_PIN, LOW);   // 슬레이브 선택
    uint8_t result = SPI.transfer(data);
    digitalWrite(SS_PIN, HIGH);  // 슬레이브 해제
    return result;
}

void loop() {
    uint8_t response = spiTransfer(0x42);
    Serial.println(response, HEX);
    delay(100);
}

SPI SD 카드

#include <SPI.h>
#include <SD.h>

const int CS_PIN = 4;

void setup() {
    Serial.begin(9600);

    if (!SD.begin(CS_PIN)) {
        Serial.println("SD card initialization failed!");
        return;
    }
    Serial.println("SD card initialized.");

    // 파일 쓰기
    File dataFile = SD.open("data.txt", FILE_WRITE);
    if (dataFile) {
        dataFile.println("Hello, SD Card!");
        dataFile.close();
        Serial.println("Data written.");
    }

    // 파일 읽기
    dataFile = SD.open("data.txt");
    if (dataFile) {
        while (dataFile.available()) {
            Serial.write(dataFile.read());
        }
        dataFile.close();
    }
}

void loop() {
    // 센서 데이터 로깅
    File dataFile = SD.open("log.csv", FILE_WRITE);
    if (dataFile) {
        int sensorValue = analogRead(A0);
        dataFile.print(millis());
        dataFile.print(",");
        dataFile.println(sensorValue);
        dataFile.close();
    }
    delay(1000);
}

ADC (Analog to Digital Converter)

ADC 개요

아날로그 입력    ADC     디지털 출력
  0V ~ 5V  ──▶ ┌───┐ ──▶ 0 ~ 1023 (10비트)
               │ADC│
               └───┘

Arduino ADC

const int SENSOR_PIN = A0;

void setup() {
    Serial.begin(9600);
    // analogReference(DEFAULT);  // 5V 기준
    // analogReference(INTERNAL); // 1.1V 기준 (더 정밀)
}

void loop() {
    int rawValue = analogRead(SENSOR_PIN);  // 0-1023

    // 전압으로 변환
    float voltage = rawValue * (5.0 / 1023.0);

    Serial.print("Raw: ");
    Serial.print(rawValue);
    Serial.print(" Voltage: ");
    Serial.println(voltage);

    delay(100);
}

온도 센서 (TMP36)

const int TEMP_PIN = A0;

float readTemperature() {
    int rawValue = analogRead(TEMP_PIN);

    // TMP36: 10mV/°C, 500mV at 0°C
    float voltage = rawValue * (5.0 / 1023.0);
    float tempC = (voltage - 0.5) * 100.0;

    return tempC;
}

void setup() {
    Serial.begin(9600);
}

void loop() {
    float temp = readTemperature();
    Serial.print("Temperature: ");
    Serial.print(temp);
    Serial.println(" C");
    delay(1000);
}

조도 센서 (LDR)

const int LDR_PIN = A0;
const int LED_PIN = 9;

void setup() {
    pinMode(LED_PIN, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    int lightLevel = analogRead(LDR_PIN);

    // 밝으면 LED 어둡게, 어두우면 LED 밝게
    int brightness = map(lightLevel, 0, 1023, 255, 0);
    analogWrite(LED_PIN, brightness);

    Serial.print("Light: ");
    Serial.print(lightLevel);
    Serial.print(" LED: ");
    Serial.println(brightness);

    delay(100);
}

다중 ADC 채널 읽기

const int NUM_CHANNELS = 4;
const int ADC_PINS[] = {A0, A1, A2, A3};

void setup() {
    Serial.begin(9600);
}

void loop() {
    for (int i = 0; i < NUM_CHANNELS; i++) {
        int value = analogRead(ADC_PINS[i]);
        Serial.print("CH");
        Serial.print(i);
        Serial.print(": ");
        Serial.print(value);
        Serial.print("\t");
    }
    Serial.println();
    delay(500);
}

ADC 노이즈 필터링

const int SENSOR_PIN = A0;
const int NUM_SAMPLES = 10;

// 이동 평균 필터
int readFiltered() {
    long sum = 0;
    for (int i = 0; i < NUM_SAMPLES; i++) {
        sum += analogRead(SENSOR_PIN);
        delay(1);
    }
    return sum / NUM_SAMPLES;
}

// 중앙값 필터
int readMedian() {
    int samples[NUM_SAMPLES];

    for (int i = 0; i < NUM_SAMPLES; i++) {
        samples[i] = analogRead(SENSOR_PIN);
        delay(1);
    }

    // 정렬
    for (int i = 0; i < NUM_SAMPLES - 1; i++) {
        for (int j = i + 1; j < NUM_SAMPLES; j++) {
            if (samples[i] > samples[j]) {
                int temp = samples[i];
                samples[i] = samples[j];
                samples[j] = temp;
            }
        }
    }

    return samples[NUM_SAMPLES / 2];
}

void setup() {
    Serial.begin(9600);
}

void loop() {
    int raw = analogRead(SENSOR_PIN);
    int filtered = readFiltered();
    int median = readMedian();

    Serial.print("Raw: ");
    Serial.print(raw);
    Serial.print(" Filtered: ");
    Serial.print(filtered);
    Serial.print(" Median: ");
    Serial.println(median);

    delay(100);
}

실전 프로젝트

온습도 데이터 로거

I2C, SPI, ADC를 종합 활용하는 프로젝트입니다.

#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <DHT.h>

// 핀 설정
const int DHT_PIN = 2;
const int SD_CS_PIN = 4;
const int LDR_PIN = A0;

// DHT 센서
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

// 로깅 간격 (밀리초)
const unsigned long LOG_INTERVAL = 5000;
unsigned long lastLogTime = 0;

void setup() {
    Serial.begin(9600);

    // DHT 초기화
    dht.begin();

    // SD 카드 초기화
    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD card failed!");
        while (1);
    }
    Serial.println("SD card initialized.");

    // CSV 헤더 작성
    File dataFile = SD.open("log.csv", FILE_WRITE);
    if (dataFile) {
        dataFile.println("timestamp,temperature,humidity,light");
        dataFile.close();
    }
}

void loop() {
    unsigned long currentTime = millis();

    if (currentTime - lastLogTime >= LOG_INTERVAL) {
        lastLogTime = currentTime;

        // 센서 읽기
        float temp = dht.readTemperature();
        float humidity = dht.readHumidity();
        int light = analogRead(LDR_PIN);

        // 유효성 검사
        if (isnan(temp) || isnan(humidity)) {
            Serial.println("DHT read failed!");
            return;
        }

        // 시리얼 출력
        Serial.print("Temp: ");
        Serial.print(temp);
        Serial.print("C, Humidity: ");
        Serial.print(humidity);
        Serial.print("%, Light: ");
        Serial.println(light);

        // SD 카드에 기록
        File dataFile = SD.open("log.csv", FILE_WRITE);
        if (dataFile) {
            dataFile.print(currentTime);
            dataFile.print(",");
            dataFile.print(temp);
            dataFile.print(",");
            dataFile.print(humidity);
            dataFile.print(",");
            dataFile.println(light);
            dataFile.close();
        }
    }
}

회로 연결도

Arduino Uno
    │
    ├── D2  ────── DHT22 DATA
    ├── D4  ────── SD CS
    ├── D11 ────── SD MOSI
    ├── D12 ────── SD MISO
    ├── D13 ────── SD SCK
    ├── A0  ────── LDR (with voltage divider)
    ├── 5V  ────── VCC (sensors)
    └── GND ────── GND (sensors)

연습 문제

문제 1: PWM LED 제어

버튼을 누를 때마다 LED 밝기가 25%씩 증가하는 프로그램을 작성하세요. 100%에서 다시 0%로 돌아갑니다.

정답 보기
const int LED_PIN = 9;
const int BUTTON_PIN = 2;

int brightness = 0;
bool lastButtonState = HIGH;

void setup() {
    pinMode(LED_PIN, OUTPUT);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
    bool buttonState = digitalRead(BUTTON_PIN);

    if (lastButtonState == HIGH && buttonState == LOW) {
        brightness = (brightness + 64) % 256;  // 25% = 64
        analogWrite(LED_PIN, brightness);
        delay(50);  // 디바운싱
    }

    lastButtonState = buttonState;
}

문제 2: I2C 스캐너

연결된 모든 I2C 장치의 주소를 찾는 스캐너를 작성하세요.

정답 보기
#include <Wire.h>

void setup() {
    Wire.begin();
    Serial.begin(9600);
    Serial.println("I2C Scanner");
}

void loop() {
    int deviceCount = 0;

    for (uint8_t addr = 1; addr < 127; addr++) {
        Wire.beginTransmission(addr);
        uint8_t error = Wire.endTransmission();

        if (error == 0) {
            Serial.print("Found device at 0x");
            Serial.println(addr, HEX);
            deviceCount++;
        }
    }

    Serial.print("Found ");
    Serial.print(deviceCount);
    Serial.println(" device(s)");
    Serial.println();

    delay(5000);
}

참고 자료

to navigate between lessons