안녕하세요! 지금까지 디지털 입력(버튼)과 아날로그 출력(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 비트 설정:
REFS1 | REFS0 | 기준전압 | 설명 |
---|---|---|---|
0 | 0 | AREF | 외부 기준전압 |
0 | 1 | AVcc | 5V 기준 (일반적) |
1 | 0 | 예약됨 | 사용 안함 |
1 | 1 | 내부 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); // → 적당한 변환 속도
분주비 설정:
ADPS2 | ADPS1 | ADPS0 | 분주비 | ADC 클럭 | 변환시간 |
---|---|---|---|---|---|
0 | 0 | 0 | 2 | 8MHz | 너무 빠름 |
0 | 0 | 1 | 2 | 8MHz | 너무 빠름 |
0 | 1 | 0 | 4 | 4MHz | 너무 빠름 |
0 | 1 | 1 | 8 | 2MHz | 너무 빠름 |
1 | 0 | 0 | 16 | 1MHz | 빠름 |
1 | 0 | 1 | 32 | 500kHz | 적당함 |
1 | 1 | 0 | 64 | 250kHz | 적당함 |
1 | 1 | 1 | 128 | 125kHz | 권장 |
첫 번째 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를 마스터하면 실제 세상과 마이크로컨트롤러 사이의 완벽한 다리가 완성됩니다. 이제 센서의 세계로 무한히 확장할 수 있어요!