CS/마이크로프로세서

[마이크로프로세서] ADC(Analog-Digital Converter)

nowkoes 2024. 1. 15. 16:18

ADC

개요

 

 많은 센서들은 아날로그 신호를 출력한다. 예를 들어, 온도, 습도, 광도, 압력 등을 측정하는 센서들이 그러하다. 이러한 아날로그 신호를 디지털 신호로 변환해야 마이크로컨트롤러에서 이를 처리하여 사용할 수 있다. 일반적으로 아날로그 신호를 디지털 신호로 변환하는 소자ADC(Analog-Digital Converter)라고 부르며, 표본화(Sampling), 양자화(Quantization), 부호화(coding)의 과정을 거친다.

  • 표본화: 연속 신호를 시간 축 방향에서 일정 간격으로 표본을 추출하여 이산 신호로 바꾸는 과정
  • 양자화: 샘플링된 진폭의 값을 한정된 개수의 대표 값으로 바꾸는 과정
  • 부호화: 신호 처리가 용이한 디지털 코드인 0과 1로 변환하는 과정

 

 

 아날로그 신호가 입력되는 방법에는 크게 두 가지가 있다. 먼저 싱글엔드(Single-ended) 방법입력 핀이 1개로 접지에 대한 입력 신호의 크기를 측정한다. 차동(Differential) 입력입력 핀이 2개로 두 선의 전압 차를 측정한다. 이때 전자의 방식은 단순하지만 노이즈에 취약하고, 후자는 잡음에 강한 특성을 가진다.


본문

ATmega128의 ADC

 

 ATmega 128의 ADC는 10비트 분해능(양자화)을 가지며, 최대 분해능에서 65~260us의 변환 시간과 초당 15,000 샘플링이 가능하다. 또한 싱글엔드 입력 채널이 8개, 차동 입력 채널이 7개로 둘 중 하나를 선택할 수 있으며, 기준 전압으로 참조 전압 AREF, 시스템 전력 AVCC, 내부 전압을 고를 수 있다. 그리고 ADC 변환이 완료되면 인터럽트를 발생시킨다.

  • AREF: 외부에서 제공되는 아날로그 참조 전압. 사용자가 특정한 전압 값을 참조 전압으로 사용하고 싶을 때, 이 전압을 AREF 핀에 공급하여 ADC 변환의 기준으로 사용할 수 있음
  • AVCC: 아날로그 시스템에 전력을 공급하는 전압 핀. 일반적으로 마이크로 컨트롤러의 전원과 같은 전압

 

 

 또한 변환된 데이터를 메모리에 쓸 때 데이터를 어떻게 정렬하느냐에 따라 값을 처리하는 방식이 달라진다. ATmega 128에서는 ADC 결과가 10비트 값으로 제공된다고 하였다. 따라서 이 10비트 데이터는 두 개의 8비트 레지스터 ADCH(ADC High Byte)와 ADCL(ADC Low Byte)에 분할되어 저장된다. 예를 들어, ADC 변환 결과가 0x03FF라고 했을 때, 데이터를 우측으로 정렬하면 ADCH 0x03, ADCL에 0xFF가 저장된다. 

 

 

 반면 데이터를 좌측으로 정렬하면 상위 8비트 11111111가 ADCH에, 나머지 하위 2비트 11000000가 ADCL에 저장되므로 각각 0xFF, 0xC0이 저장된다. 

 

 

 어떤 정렬 방식을 사용할지는 응용 프로그램의 요구 사항과 처리해야 할 데이터의 특성에 따라 결정된다. 기본적으로 싱글 엔드 모드에서는 아날로그-디지털 변환기(ADC)는 항상 양의 전압만을 측정한다. 이는 결괏값이 항상 양수라는 것을 의미하며, 따라서 부호 비트가 필요 없다. 이러한 상황에서 우측 정렬 방식을 사용하는 것이 일반적이다. 우측 정렬은 결괏값의 최하위 비트(LSB)가 레지스터의 가장 낮은 주소에 위치하게 하여, 표준 데이터 타입과 일치하는 메모리 구조를 제공한다.

 그러나 좌측 정렬을 사용할 경우, 상위 비트가 부호 비트로 잘못 해석될 수 있는 문제가 발생할 수 있다. 특히 10비트 ADC 결과를 16비트나 8비트 signed 자료형으로 변환할 때 음수로 인식하는 문제가 발생한다. 따라서 싱글 엔드 모드에서는 우측 정렬을 일반적으로 선택한다.

 

 

 반면 차동 입력 모드를 사용할 땐 결괏값이 음수가 나올 수 있으므로 반드시 signed 자료형을 사용해야 한다. 이때 우측 정렬을 사용하면 음수여도 부호 비트가 0이므로 양수로 인식하는 오류가 발생할 수 있다. 따라서 좌측 정렬을 사용하여 부호 정보를 저장하며, 부호에 상관없이 결과 값을 변환 값의 64 배가 되게 할 수 있다. 여기서 64배는 10비트를 사용하기 위해 ADC 변환 결과를 2^6으로 나눈 것을 의미한다.

 

 변환 데이터를 얻을 땐 위와 같이 워드 단위로도 읽을 수 있고, 바이트 단위로도 읽을 수 있다. 만약 바이트 단위로 읽을 땐 AVR의 16비트 레지스터 액세스 방법에 따라 반드시 하위 바이트인 ADCL을 먼저 읽고, 상위 바이트인 ADCH를 읽어야 한다(싱글 엔드, 우측 정렬 기준).


ADC 제어(1) - ADMUX 레지스터

 

 먼저 입력 핀의 방향을 설정하고, 입력 채널을 설정해야 한다. 이때 사용하는 레지스터는 ADMUX(ADC Multiplexer Selection) 레지스터다. 해당 레지스터는 전압을 선택, 받아온 데이터를 정렬, 핀을 설정하는 기능을 갖고 있다.

 

 

1. REFS: ADC를 위한 전압을 고르는 비트다. 필자가 사용한 J-KIT-128-1에서는 참조 전압만 사용하므로 REFS1 비트와 REFS0 비트를 각각 0으로 설정해 둔다. 여기서 AREF에 전압이 걸리면 AVCC, 내부 기준 전압은 충돌이 일어나므로 사용이 불가능하다. 또한 싱글엔드일 경우 0부터 2^n - 1까지 표현하므로 ADC 값은 Vin/Vref x (2^n - 1)이 되고, 차동입력일 경우 -2^n-1부터 2^n-1 - 1까지 표현이 가능하므로, (V+ - V-)/Vref x 2^n-1이 ADC 값이 된다.

 

2. ADLAR: 받아온 데이터를 좌측 정렬을 할지, 우측 정렬을 할지 선택하는 비트다. 기본적으로 우측 정렬이 되어 있지만, 해당 비트에 1을 쓴다면 좌측 정렬이 된다. 

 

 

3. MUX: ADC에서 사용할 입력 채널을 선택하는 데 사용되는 비트다. 이를 통해 싱글 엔드 모드와 차동 입력 모드 중 하나를 선택하고, 구체적으로 어떤 핀을 사용할지 결정할 수 있다. 이때 차동 입력 채널의 경우 특정한 이득(Gain) 값을 갖고 있어, 측정 범위와 정밀도를 조절할 수 있다. 


ADC 제어(2) - ADCSRA 레지스터

 

 ADCSRA(ADC Control and Status Register)분주비, 인터럽트, 모드, 변환의 시작, 동작 허가를 설정하는 레지스터다. 

 

1. ADEN: ADC를 허가하는 비트다. 1로 설정하면 ADC 동작을 허용하고, 0으로 설정하면 ADC 동작을 금지시킨다. 만약 A/D 변환 중 0으로 설정되면 현재 수행 중인 작업을 완료한 후 동작을 중지시킨다.

 

2. ADSC: 해당 비트를 1로 설정함으로써 ADC 변환을 시작할 수 있다. 이때 변환이 진행 중인 동안 ADSC 비트는 1로 읽히는데, 이는 ADC가 현재 활성화된 상태이며, 데이터 변환 중임을 나타낸다. 그 후 변환이 완료되면 ADSC 비트는 자동으로 0으로 바뀐다. 

 

3. ADFR: 해당 비트가 1로 설정되어 있으면 ADC는 프리 러닝(Free Running) 모드로 설정되어 연속으로 A/D를 변환한다. 즉, 변환이 끝나면 자동으로 다음 변환을 시작한다. 만약 비트가 0으로 설정되어 있을 때 단일 변환(Single Conversion) 모드가 되어 사용자가 ADSC 비트를 1로 설정할 때마다 A/D 변환이 수행된다.

 

4. ADIF: A/D 변환이 종료되고 폴링 방식일 경우 데이터 레지스터가 갱신되면 1로 바뀌고, 인터럽트 서비스 루틴이 실행되면 자동으로 클리어된다. 이때 1을 쓰면 소프트웨어적으로 클리어된다.

 

5. ADIE: 이 비트가 1로 설정되고 상태 레지스터의 I 비트가 1로 설정되면 A/D 변환 완료 인터럽트가 활성화된다.

 

 

6.ADPS: ATmega 128에서 첫 번째 샘플의 A/D 변환 시, 약 25 클럭 사이클이 소요된다. 이는 ADC가 초기화되고, 변환 준비 과정을 거치는 데 필요한 시간을 포함한다. 이후의 샘플들은 약 13 클럭 사이클 만에 변환된다. 이러한 평균 클럭 소요 시간과 최대 분해능에서의 변환 시간 65us~260us을 나누면 필요 동작 주파수가 50kHz ~ 200kHz라는 결과를 얻을 수 있다. 따라서 내부 클럭 주파수는 16MHz이므로 최대 해상도로 변환하기 위한 분주비는 128 밖에 없다(16Mhz/128 = 125kHz). 


실습

 위에서 배운 내용들을 토대로 3초간 강한 빛에 노출되면 LED 8개를 켜 경고등을 활성화하는 실습을 진행해 보도록 하겠다. 이때 강한 빛의 기준은 휴대폰 플래시로 광센서를 비췄을 때 나오는 값인 1010으로 설정하였다.

 

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

#define ON 0xFF
#define OFF 0x00
#define ADC_THRESHOLD 1010
#define WARNING_TIME 3000

unsigned char segmentTable[16] = 
{0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
unsigned char selectTable[4] = {0x01, 0x02, 0x04, 0x08};
unsigned char fndNumber[4] = {0,};

 필요한 헤더파일을 받아오고, 코드의 가독성을 위해 상수를 정의해 주었다. LED 8개가 ON이 되려면 LED 포트에 0xFF가 들어가야 하므로 ON을 0xFF로, LED 8개가 OFF가 되려면 LED 포트에 0x00이 들어가야 하므로 OFF를 0x00으로 정의해 주었다. 또한 강한 빛의 기준인 ADC_THRESHOLD를 1010으로 설정해 주었고, 3초를 의미하는 WARNING_TIME을 3000으로 정의하였다.

 

ISR(ADC_vect)
{
	unsigned int ADC_Data = ADCW;
	
	for (int i_digit = 0; i_digit < 4; i_digit++)
	{
		fndNumber[i_digit] = ADC_Data % 10;
		ADC_Data /= 10;
	}
}

 ADC 변환 인터럽트 서비스 루틴이다. ADC 값을 워드 단위로 읽어와서 FND에 출력하도록 설정하였다.

 

ISR(TIMER0_COMP_vect)
{
	static unsigned int exposureTime = 0;
	ADCSRA |= (1 << ADSC);
	
	if (ADCW >= ADC_THRESHOLD)
	{
		exposureTime++;
		
		if (exposureTime >= WARNING_TIME)
			PORTA = ON;
	}
	
	else
	{
		exposureTime = 0;
		PORTA = OFF;
	}
}

 3초를 카운트하기 위해 타이머 인터럽트 서비스 루틴을 정의하였다. 노출되고 있는 시간을 static 키워드로 설정해 주고, ADSC 비트를 1로 설정하여 ADC 변환과 타이머의 시작을 동시에 설정하였다. 그리고 만약 강한 빛보다 큰 값이 들어오면 노출된 시간을 증가시켜, 총시간이 3초가 되면 LED 8개를 ON 되게 설정하였다.

 

int main(void)
{
	DDRA = 0xFF;
	DDRC = 0xFF;
	DDRG = 0x0F;
	
	ADMUX = 0x00;
	ADCSRA = ADCSRA | 0b10001111;
	ADCSRA = ADCSRA | (1 << ADSC);
	
	TCCR0 = TCCR0 | (1 << WGM01) | (1 << CS02);
	TIMSK |= (1 << OCIE0);
	sei();
	
	OCR0 = 249;

	while (1)
	{
		for (int i_4ms = 0; i_4ms < 25; i_4ms++)
		{
			PORTG = selectTable[i_4ms % 4];
			PORTC = segmentTable[fndNumber[i_4ms % 4]];
			_delay_ms(1);
		}
	}

	return 0;
}

 메인 함수에서는 LED, FND를 설정하기 위한 DDR 레지스터를 설정하고, ADC 변환을 위한 ADMUX, ADCSRA 레지스터를 설정하였다. 또한 TCCR0 레지스터를 CTC 모드로 설정하고 분주비를 64로 설정하였고, 비교 일치 인터럽트를 활성화시켰다. 이때 OCR0는 1ms/4us - 1 = 249이므로 OCR0 값을 249로 설정하였다.

 

 또한 FND에 출력되는 값은 프로그램이 돌아가는 동안 반복되어야 하기 때문에 무한 반복문으로 구현하였다.

 

 

총합본

더보기
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define ON 0xFF
#define OFF 0x00
#define ADC_THRESHOLD 1010
#define WARNING_TIME 3000

unsigned char segmentTable[16] = 
{0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
unsigned char selectTable[4] = {0x01, 0x02, 0x04, 0x08};
unsigned char fndNumber[4] = {0,};

ISR(ADC_vect)
{
	unsigned int ADC_Data = ADCW;
	
	for (int i_digit = 0; i_digit < 4; i_digit++)
	{
		fndNumber[i_digit] = ADC_Data % 10;
		ADC_Data /= 10;
	}
}

ISR(TIMER0_COMP_vect)
{
	static unsigned int exposureTime = 0;
	ADCSRA |= (1 << ADSC);
	
	if (ADCW >= ADC_THRESHOLD)
	{
		exposureTime++;
		
		if (exposureTime >= WARNING_TIME)
			PORTA = ON;
	}
	
	else
	{
		exposureTime = 0;
		PORTA = OFF;
	}
}

int main(void)
{
	DDRA = 0xFF;
	DDRC = 0xFF;
	DDRG = 0x0F;
	
	ADMUX = 0x00;
	ADCSRA = ADCSRA | 0b10001111;
	ADCSRA = ADCSRA | (1 << ADSC);
	
	TCCR0 = TCCR0 | (1 << WGM01) | (1 << CS02);
	TIMSK |= (1 << OCIE0);
	sei();
	
	OCR0 = 249;

	while (1)
	{
		for (int i_4ms = 0; i_4ms < 25; i_4ms++)
		{
			PORTG = selectTable[i_4ms % 4];
			PORTC = segmentTable[fndNumber[i_4ms % 4]];
			_delay_ms(1);
		}
	}

	return 0;
}

요약

ATmega 128의 ADC
1. 정의: 아날로그 데이터를 디지털 데이터로 변환
2. 입력 방법
 a. 싱글 엔드: 입력 핀 1개, 노이즈에 취약
 b. 차동 입력: 입력 핀 2개의 전압차 측정, 노이즈에 강함
3. 특징
 a. 10비트 분해능
 b. 싱글 엔드와 차동 입력모드 지원
 c. ADC 변환 완료 시 인터럽트 발생
 d. 최대 분해능에서 65us ~ 260us 변환 시간 및 초당 13 클럭
  -> 필요 동작 주파수: 13/65us ~ 13/260us = 50kHz ~ 200kHz
4. 데이터 정렬
 a. 좌측 정렬: 저해상도 변환 유리. 음수 부호 유지(차동 입력 모드)
 b. 우측 정렬: 직관적(싱글 엔드)
5. 레지스터
 a. ADMUX: 전압 선택, 정렬 설정, 핀 설정
 b. ADCSR: 분주비, 인터럽트 활성화, 단일/프리 모드, 변환 시작, 동작 허가 설정
반응형