ADC와 아날로그 입력) 실제 세상의 신호를 디지털로

안녕하세요! 지금까지 디지털 입력(버튼)과 아날로그 출력(PWM)을 배웠습니다. 이번에는 마지막 퍼즐 조각인 아날로그 입력을 다뤄보겠습니다. ADC(Analog to Digital Converter)를 사용해서 센서 값, 전압, 온도 등 실제 세상의 연속적인 신호를 디지털 데이터로 변환해보겠습니다!

이번 편의 목표

  • ADC의 개념과 동작 원리 이해
  • 가변저항으로 LED 밝기 실시간 제어
  • 온도 센서와 광센서 활용
  • ADC 레지스터 설정과 샘플링 기법
  • 아날로그 신호 처리와 필터링

ADC란 무엇인가?

아날로그 vs 디지털

실제 세상은 아날로그입니다:

온도: 23.7°C, 24.1°C, 24.5°C... (연속적)
밝기: 어둠 ←────────────→ 밝음 (무한 단계)
소리: 작음 ←────────────→ 큼 (연속적)
전압: 0V ←───────────→ 5V (무한 값)

마이크로컨트롤러는 디지털만 이해합니다:

디지털: 0 또는 1, HIGH 또는 LOW
숫자: 0, 1, 2, 3, ... 255 (불연속적)

ADC의 역할:

아날로그 전압 (0~5V) → ADC → 디지털 숫자 (0~1023)

ATmega328P의 ADC 사양

  • 분해능: 10비트 (0~1023, 총 1024단계)
  • 입력 범위: 0V ~ VCC (보통 5V)
  • 정확도: ±2 LSB (충분히 정밀함)
  • 변환 시간: 13~260µs (매우 빠름)
  • 채널 수: 8개 (PC0~PC5, 내부 온도센서, 1.1V 기준전압)

ADC 분해능 이해하기

c

// 10비트 ADC: 0~1023 (2^10 = 1024단계)
전압 범위: 0V ~ 5V
분해능: 5V ÷ 1024 = 4.88mV/step

예시:
ADC = 0    → 0V
ADC = 512  → 2.5V  
ADC = 1023 → 5V

⚙️ ADC 설정과 기본 사용법

ADC 레지스터 설정

ADMUX (ADC Multiplexer Selection Register)

c

ADMUX = (1 << REFS0);  // AVcc를 기준전압으로 사용

REFS 비트 설정:

REFS1REFS0기준전압설명
00AREF외부 기준전압
01AVcc5V 기준 (일반적)
10예약됨사용 안함
11내부 1.1V정밀 측정용

채널 선택 (MUX 비트):

c

ADMUX |= 0;  // ADC0 (PC0) 선택
ADMUX |= 1;  // ADC1 (PC1) 선택  
ADMUX |= 2;  // ADC2 (PC2) 선택
// ...

ADCSRA (ADC Control and Status Register A)

c

ADCSRA = (1 << ADEN) |   // ADC 활성화
         (1 << ADPS2) |  // 분주비 설정
         (1 << ADPS1) |  // 128분주 (125kHz)  
         (1 << ADPS0);   // → 적당한 변환 속도

분주비 설정:

ADPS2ADPS1ADPS0분주비ADC 클럭변환시간
00028MHz너무 빠름
00128MHz너무 빠름
01044MHz너무 빠름
01182MHz너무 빠름
100161MHz빠름
10132500kHz적당함
11064250kHz적당함
111128125kHz권장

첫 번째 ADC 프로그램

c

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

void adc_init(void) {
    // 기준전압: AVcc (5V)
    ADMUX = (1 << REFS0);
    
    // ADC 활성화, 128 분주
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

uint16_t adc_read(uint8_t channel) {
    // 채널 선택 (하위 4비트만 사용)
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    
    // 변환 시작
    ADCSRA |= (1 << ADSC);
    
    // 변환 완료까지 대기
    while (ADCSRA & (1 << ADSC));
    
    // 결과 반환 (ADCL을 먼저 읽어야 함)
    return ADC;
}

int main(void) {
    adc_init();
    
    while(1) {
        uint16_t adc_value = adc_read(0);  // PC0에서 읽기
        
        // 전압 계산
        float voltage = (adc_value * 5.0) / 1024.0;
        
        printf("ADC: %d, Voltage: %.2fV\n", adc_value, voltage);
        
        _delay_ms(500);
    }
    
    return 0;
}

가변저항으로 LED 밝기 제어

회로 구성

VCC (5V) ──┬── 가변저항 ──┬── GND
           │              │
           │              ├── PC0 (ADC0)
           │
         LED + 저항 + PB1 (PWM)

실시간 밝기 제어 프로그램

c

#include <avr/io.h>
#include <util/delay.h>

void adc_init(void) {
    ADMUX = (1 << REFS0);    // AVcc 기준
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

void pwm_init(void) {
    // PB1을 출력으로 설정
    DDRB |= (1 << PB1);
    
    // Fast PWM, 8비트 모드
    TCCR1A = (1 << COM1A1) | (1 << WGM10);
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);  // 64 분주
}

uint16_t adc_read(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    return ADC;
}

int main(void) {
    adc_init();
    pwm_init();
    
    while(1) {
        // 가변저항 값 읽기 (0~1023)
        uint16_t pot_value = adc_read(0);
        
        // PWM 범위로 변환 (0~255)
        uint8_t brightness = pot_value >> 2;  // 1024→256 변환 (÷4)
        
        // LED 밝기 설정
        OCR1A = brightness;
        
        _delay_ms(10);  // 부드러운 반응을 위한 짧은 지연
    }
    
    return 0;
}

개선된 버전: 스무딩 필터 적용

c

#include <avr/io.h>
#include <util/delay.h>

#define FILTER_SIZE 8

void adc_init(void) {
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

void pwm_init(void) {
    DDRB |= (1 << PB1);
    TCCR1A = (1 << COM1A1) | (1 << WGM10);
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
}

uint16_t adc_read_filtered(uint8_t channel) {
    uint32_t sum = 0;
    
    // 여러 번 읽어서 평균 계산
    for (uint8_t i = 0; i < FILTER_SIZE; i++) {
        ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
        ADCSRA |= (1 << ADSC);
        while (ADCSRA & (1 << ADSC));
        sum += ADC;
        _delay_us(100);  // 샘플 간격
    }
    
    return sum / FILTER_SIZE;
}

int main(void) {
    adc_init();
    pwm_init();
    
    uint16_t filtered_value = 0;
    uint16_t target_value;
    
    while(1) {
        // 목표값 읽기
        target_value = adc_read_filtered(0);
        
        // 지수 이동 평균 필터 (부드러운 변화)
        filtered_value = (filtered_value * 7 + target_value) / 8;
        
        // PWM 설정
        OCR1A = filtered_value >> 2;
        
        _delay_ms(20);
    }
    
    return 0;
}

온도 센서 활용

LM35 온도센서 사용

LM35 특성:

  • 출력: 10mV/°C (선형적)
  • 0°C = 0V, 25°C = 250mV, 100°C = 1V
  • 전원: 4V~30V

온도 측정 프로그램

c

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

void adc_init(void) {
    ADMUX = (1 << REFS0);  // AVcc 기준 (5V)
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

uint16_t adc_read_avg(uint8_t channel, uint8_t samples) {
    uint32_t sum = 0;
    
    for (uint8_t i = 0; i < samples; i++) {
        ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
        ADCSRA |= (1 << ADSC);
        while (ADCSRA & (1 << ADSC));
        sum += ADC;
        _delay_ms(2);  // 안정화 시간
    }
    
    return sum / samples;
}

float adc_to_temperature(uint16_t adc_value) {
    // ADC 값을 전압으로 변환
    float voltage = (adc_value * 5.0) / 1024.0;
    
    // LM35: 10mV/°C
    float temperature = voltage * 100.0;  // V → mV → °C
    
    return temperature;
}

int main(void) {
    adc_init();
    
    while(1) {
        // 16번 평균으로 정확도 향상
        uint16_t adc_value = adc_read_avg(0, 16);
        float temperature = adc_to_temperature(adc_value);
        
        printf("ADC: %d, Temp: %.1f°C\n", adc_value, temperature);
        
        // 온도에 따른 경고 LED
        if (temperature > 30.0) {
            PORTB |= (1 << PB5);   // 고온 경고
        } else if (temperature < 20.0) {
            PORTB &= ~(1 << PB5);  // 정상 온도
        }
        
        _delay_ms(1000);
    }
    
    return 0;
}

내부 온도센서 사용

c

float read_internal_temperature(void) {
    // 내부 온도센서 선택 (채널 8)
    ADMUX = (1 << REFS1) | (1 << REFS0) | (1 << MUX3);  // 1.1V 기준, 내부 온도센서
    
    _delay_ms(2);  // 기준전압 안정화
    
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    
    uint16_t adc_raw = ADC;
    
    // 내부 온도센서 공식 (데이터시트 참조)
    float temperature = (adc_raw - 324.31) / 1.22;
    
    return temperature;
}

광센서(포토레지스터) 활용

자동 밝기 조절 시스템

c

#include <avr/io.h>
#include <util/delay.h>

void adc_init(void) {
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

void pwm_init(void) {
    DDRB |= (1 << PB1);  // LED 출력
    TCCR1A = (1 << COM1A1) | (1 << WGM10);
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
}

uint16_t adc_read(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    return ADC;
}

int main(void) {
    adc_init();
    pwm_init();
    
    while(1) {
        // 광센서 값 읽기 (0~1023)
        uint16_t light_level = adc_read(0);
        
        // 역방향 제어: 어두우면 LED 밝게, 밝으면 LED 어둡게
        uint8_t led_brightness = 255 - (light_level >> 2);
        
        // 임계값 설정으로 스위칭 동작
        if (light_level < 300) {
            // 어둠: LED 켜기
            OCR1A = 200;  // 적당한 밝기
        } else if (light_level > 700) {
            // 밝음: LED 끄기
            OCR1A = 0;
        }
        // 중간 값에서는 변화 없음 (히스테리시스)
        
        _delay_ms(100);
    }
    
    return 0;
}

고급 광센서 처리: 로그 스케일 적용

c

#include <math.h>

uint8_t light_to_brightness(uint16_t light_adc) {
    // 인간의 시각은 로그 스케일로 밝기를 인식
    // y = a * log(x + 1) + b 형태로 변환
    
    float normalized = (float)light_adc / 1023.0;  // 0~1 정규화
    float log_scale = log(normalized * 9.0 + 1.0) / log(10.0);  // 0~1 로그 스케일
    
    return (uint8_t)(255 - log_scale * 255);  // 역방향 + PWM 범위
}

int main(void) {
    adc_init();
    pwm_init();
    
    while(1) {
        uint16_t light_level = adc_read(0);
        uint8_t brightness = light_to_brightness(light_level);
        
        OCR1A = brightness;
        
        _delay_ms(50);
    }
    
    return 0;
}

ADC 인터럽트와 고급 기법

인터럽트 기반 ADC

c

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

volatile uint16_t adc_result = 0;
volatile uint8_t adc_ready = 0;
volatile uint8_t current_channel = 0;

// ADC 변환 완료 인터럽트
ISR(ADC_vect) {
    adc_result = ADC;
    adc_ready = 1;
}

void adc_init(void) {
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) |    // ADC 활성화
             (1 << ADIE) |    // 인터럽트 활성화
             (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

void adc_start_conversion(uint8_t channel) {
    current_channel = channel;
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    adc_ready = 0;
    ADCSRA |= (1 << ADSC);  // 변환 시작
}

int main(void) {
    adc_init();
    sei();
    
    adc_start_conversion(0);  // 첫 번째 변환 시작
    
    while(1) {
        if (adc_ready) {
            // ADC 결과 처리
            uint16_t value = adc_result;
            
            // 다음 채널 준비
            current_channel = (current_channel + 1) % 4;  // 4개 채널 순환
            adc_start_conversion(current_channel);
            
            // 값에 따른 처리
            switch(current_channel) {
                case 0: handle_channel0(value); break;
                case 1: handle_channel1(value); break;
                case 2: handle_channel2(value); break;
                case 3: handle_channel3(value); break;
            }
        }
        
        // 메인 루프에서 다른 작업 가능
        other_tasks();
    }
    
    return 0;
}

자동 트리거 모드

c

void adc_auto_trigger_init(void) {
    ADMUX = (1 << REFS0);
    
    ADCSRA = (1 << ADEN) |     // ADC 활성화
             (1 << ADATE) |    // 자동 트리거 활성화
             (1 << ADIE) |     // 인터럽트 활성화
             (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
    
    // Timer0 오버플로우로 트리거 설정
    ADCSRB = (1 << ADTS2);
    
    // Timer0 설정
    TCCR0B = (1 << CS02) | (1 << CS00);  // 1024 분주
    
    ADCSRA |= (1 << ADSC);  // 첫 번째 변환 시작
}

// 이제 Timer0가 오버플로우될 때마다 자동으로 ADC 변환 실행

다중 센서 모니터링 시스템

종합 센서 모니터링

c

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

typedef struct {
    uint16_t raw_value;
    float processed_value;
    uint8_t status;  // 0: OK, 1: Warning, 2: Critical
} sensor_data_t;

sensor_data_t sensors[4];
volatile uint8_t current_sensor = 0;
volatile uint8_t measurement_complete = 0;

ISR(ADC_vect) {
    sensors[current_sensor].raw_value = ADC;
    
    current_sensor = (current_sensor + 1) % 4;
    
    if (current_sensor == 0) {
        measurement_complete = 1;  // 모든 센서 측정 완료
    }
    
    // 다음 센서로 전환
    ADMUX = (ADMUX & 0xF0) | current_sensor;
    ADCSRA |= (1 << ADSC);
}

void process_sensor_data(void) {
    // 센서 0: 온도 (LM35)
    float voltage0 = (sensors[0].raw_value * 5.0) / 1024.0;
    sensors[0].processed_value = voltage0 * 100.0;  // °C
    sensors[0].status = (sensors[0].processed_value > 35.0) ? 2 : 
                       (sensors[0].processed_value > 30.0) ? 1 : 0;
    
    // 센서 1: 광센서
    sensors[1].processed_value = (sensors[1].raw_value * 100.0) / 1023.0;  // %
    sensors[1].status = (sensors[1].processed_value < 10.0) ? 1 : 0;
    
    // 센서 2: 가변저항 (사용자 설정값)
    sensors[2].processed_value = (sensors[2].raw_value * 100.0) / 1023.0;  // %
    sensors[2].status = 0;  // 항상 정상
    
    // 센서 3: 배터리 전압 모니터링
    float voltage3 = (sensors[3].raw_value * 5.0) / 1024.0;
    sensors[3].processed_value = voltage3;  // V
    sensors[3].status = (voltage3 < 3.0) ? 2 :
                       (voltage3 < 3.5) ? 1 : 0;
}

void display_status(void) {
    printf("=== Sensor Status ===\n");
    printf("Temperature: %.1f°C [%s]\n", 
           sensors[0].processed_value,
           (sensors[0].status == 0) ? "OK" : 
           (sensors[0].status == 1) ? "WARN" : "CRIT");
    
    printf("Light: %.0f%% [%s]\n",
           sensors[1].processed_value,
           (sensors[1].status == 0) ? "OK" : "DARK");
    
    printf("User Setting: %.0f%%\n", sensors[2].processed_value);
    
    printf("Battery: %.2fV [%s]\n",
           sensors[3].processed_value,
           (sensors[3].status == 0) ? "OK" :
           (sensors[3].status == 1) ? "LOW" : "CRIT");
    
    printf("\n");
}

void update_outputs(void) {
    // 온도에 따른 쿨링팬 제어 (PWM)
    if (sensors[0].status >= 1) {
        OCR1A = 200;  // 팬 가동
    } else {
        OCR1A = 0;    // 팬 정지
    }
    
    // 배터리 상태 LED
    if (sensors[3].status == 2) {
        PORTB |= (1 << PB5);   // 위험: LED 켜기
    } else {
        PORTB &= ~(1 << PB5);  // 정상: LED 끄기
    }
    
    // 사용자 설정에 따른 밝기 조절
    uint8_t brightness = (uint8_t)((sensors[2].processed_value / 100.0) * 255);
    OCR1B = brightness;
}

int main(void) {
    // 하드웨어 초기화
    DDRB |= (1 << PB1) | (1 << PB2) | (1 << PB5);  // PWM + LED 출력
    
    // PWM 초기화
    TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM10);
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);
    
    // ADC 초기화
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
    
    sei();
    
    // 첫 번째 변환 시작
    ADCSRA |= (1 << ADSC);
    
    while(1) {
        if (measurement_complete) {
            measurement_complete = 0;
            
            process_sensor_data();
            display_status();
            update_outputs();
        }
        
        _delay_ms(1000);  // 1초마다 출력
    }
    
    return 0;
}

ADC 최적화와 고급 기법

노이즈 제거 기법

1. 하드웨어적 노이즈 제거

c

// 아날로그 전원 분리
AVCC ─── 페라이트 비드 ─── 디커플링 커패시터 ─── ADC 회로

// 기준전압 안정화
void adc_init_precise(void) {
    // 외부 정밀 기준전압 사용
    ADMUX = 0;  // AREF 사용
    
    // 느린 변환으로 정확도 향상
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

2. 소프트웨어적 노이즈 제거

c

// 중앙값 필터 (스파이크 노이즈 제거)
uint16_t median_filter(uint16_t *samples, uint8_t count) {
    // 간단한 버블 정렬
    for (uint8_t i = 0; i < count - 1; i++) {
        for (uint8_t j = 0; j < count - i - 1; j++) {
            if (samples[j] > samples[j + 1]) {
                uint16_t temp = samples[j];
                samples[j] = samples[j + 1];
                samples[j + 1] = temp;
            }
        }
    }
    
    return samples[count / 2];  // 중앙값 반환
}

// 이동평균 필터 (연속적 노이즈 제거)
uint16_t moving_average(uint16_t new_value) {
    static uint16_t buffer[16];
    static uint8_t index = 0;
    static uint32_t sum = 0;
    static uint8_t count = 0;
    
    // 이전 값 제거
    if (count == 16) {
        sum -= buffer[index];
    } else {
        count++;
    }
    
    // 새 값 추가
    buffer[index] = new_value;
    sum += new_value;
    
    index = (index + 1) % 16;
    
    return sum / count;
}

고속 샘플링

c

// 빠른 ADC 설정 (분주비 최소화)
void adc_fast_init(void) {
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) | (1 << ADPS2);  // 16분주 (1MHz ADC 클럭)
}

// 연속 샘플링
void fast_sampling(uint16_t *buffer, uint16_t count) {
    for (uint16_t i = 0; i < count; i++) {
        ADCSRA |= (1 << ADSC);
        while (ADCSRA & (1 << ADSC));  // 13 클럭 사이클 대기
        buffer[i] = ADC;
    }
}

응용 과제

과제 1: 데이터 로거

  • 여러 센서 값을 시간과 함께 기록
  • 최대/최소/평균값 계산
  • 이상 상황 감지 및 알람

과제 2: 스마트 온실 제어

  • 온도, 습도, 광량 모니터링
  • 자동 환기팬, 물 공급, 조명 제어
  • 일일/주간 패턴 학습

과제 3: 간단한 오실로스코프

  • 고속 ADC 샘플링
  • 파형 표시 (LED 배열 또는 시리얼)
  • 트리거 기능 구현

다음 편 예고

다음 편에서는 인터럽트 심화를 배워보겠습니다:

  • 외부 인터럽트와 핀 변화 인터럽트
  • 인터럽트 우선순위와 네스팅
  • 실시간 시스템 설계 기법
  • 인터럽트 기반 멀티태스킹
  • 안전한 인터럽트 프로그래밍 패턴

이제 아날로그와 디지털을 모두 다룰 수 있게 되었습니다!


ADC를 마스터하면 실제 세상과 마이크로컨트롤러 사이의 완벽한 다리가 완성됩니다. 이제 센서의 세계로 무한히 확장할 수 있어요!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.