카테고리 보관물: 8비트 마이크로 컨트롤러

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