ADC — 아날로그를 숫자로
온도·빛·소리·전압은 연속적인 값이다. MCU 는 숫자만 다룬다. ADC 가 그 사이를 잇는데, 어떻게 변환하느냐가 곧 값의 신뢰도다.
"센서 값이 튄다" 는 문제의 대부분이 ADC 를 잘못 쓴 것이지 센서가 나쁜 게 아니다.
세 가지 수가 값을 정한다
셋 중에서 기준 전압이 값 전체를 좌우한다 — ADC 가 내놓는 숫자는 언제나 기준 전압에 대한 비율이기 때문이다. 기준이 1% 흔들리면 측정값도 그만큼 흔들린다.
분해능(bits) 몇 단계로 나누나 12비트 → 0~4095
기준 전압(Vref) 최대값이 몇 볼트냐 3.3V
샘플링 시간 한 번 재는 데 걸리는 시간
측정 전압 = (ADC 값 / (2^분해능 - 1)) × Vref
12비트, Vref 3.3V 에서 ADC 값이 2048 이면
2048 / 4095 × 3.3 = 1.65V
uint16_t raw = ADC1->DR; // 0~4095
float mv = raw * 3300.0f / 4095.0f; // 밀리볼트
한 단계가 얼마인가 — LSB
12비트, Vref 3.3V → 3.3V / 4096 = 0.806mV 가 1단계
10비트, Vref 3.3V → 3.3V / 1024 = 3.22mV
LSB 보다 작은 변화는 아예 못 읽는다. 0.5mV 차이를 재야 한다면 12비트로는 불가능하고, 신호를 증폭하거나 더 높은 분해능이 필요하다.
"12비트를 산다"와 "12비트를 쓴다"는 다르다 — 오차 예산
분해능이 12비트라고 12비트만큼 믿을 수 있는 것은 아니다. 오차 요인을 LSB 단위로 환산하면 현실이 드러난다 (Vref 3.3V, 1 LSB = 0.806mV).
| 오차 요인 | 전압 | LSB 환산 | 의미 |
|---|---|---|---|
| Vref 편차 1% | 33 mV | 약 41 LSB | 하위 5~6비트가 통째로 무의미 |
| 전원 노이즈 20mVpp | 20 mV | 약 25 LSB | 하위 4~5비트가 흔들린다 |
| ADC 자체 오프셋 2 LSB | 1.6 mV | 2 LSB | 이건 작은 편 |
Vref 1% 하나가 41 LSB 다. 즉 12비트로 읽어도 실질은 6~7비트밖에 안 된다. 그래서 "분해능을 16비트로 올리자"보다 Vref 를 정밀 기준전압 IC 로 바꾸는 것이 먼저다 — 분해능을 올려도 기준이 흔들리면 흔들리는 값을 더 잘게 재는 것뿐이다.
비율 측정(ratiometric)이 답이 되는 경우 — 저항 분압으로 만든 센서(포텐쇼미터·서미스터)는 센서도 Vref 도 같은 전원을 쓰면 Vref 가 흔들릴 때 센서 출력도 같은 비율로 흔들려 비율이 유지된다. 이때는 Vref 정밀도가 거의 무의미해진다. 반대로 절대 전압(배터리 전압, 4-20mA 루프)을 재려면 Vref 정밀도가 곧 정확도다. 무엇을 재는가가 Vref 를 얼마나 좋게 쓸지 결정한다.
샘플링 시간은 소스 임피던스가 정한다
ADC 는 내부 커패시터에 입력 전압을 복사해 담은 뒤 변환한다. 이 충전이 덜 되면 값이 낮게 나온다. 12비트에서 0.5 LSB 이내로 정착하려면 약 9 시정수(τ = R × C) 가 필요하다.
| 소스 임피던스 | 필요 정착 시간 (C ≈ 4pF 근사) | 대응 |
|---|---|---|
| 1 kΩ | 약 36 ns | 최소 샘플링 시간으로 충분 |
| 10 kΩ | 약 360 ns | 샘플링 사이클을 늘려야 한다 |
| 100 kΩ | 약 3.6 µs | 버퍼(OP앰프)를 넣는 것이 정석 |
함정 — "값이 실제보다 조금 낮게, 그리고 채널을 바꾼 직후에만 이상하다"면 거의 이 문제다. 이전 채널의 전압이 커패시터에 남아 있는 상태에서 충전이 덜 끝난 것이라 직전 채널 값 쪽으로 끌린 값이 나온다(채널 간 크로스토크처럼 보인다). 고임피던스 센서를 멀티채널로 스캔할 때 자주 겪는다.
Vref 가 값을 통째로 흔든다
// ✗ Vref 를 3.3V 로 가정
float v = raw * 3.3f / 4095.0f;
ADC 는 "Vref 에 대한 비율" 만 알려 준다. 실제 Vref 가 3.25V 인데 3.3V 로 계산하면 모든 값이 1.5% 틀린다. 배터리 전원이라 전압이 떨어지면 오차가 계속 커진다.
// ✓ 내부 기준 전압으로 실제 Vref 를 역산한다
// STM32 는 1.21V 짜리 내부 기준(VREFINT)을 채널 17 에 갖고 있다
uint16_t vrefint_raw = read_adc(ADC_CHANNEL_VREFINT);
float actual_vref = 1.21f * 4095.0f / vrefint_raw;
float v = raw * actual_vref / 4095.0f; // 이제 배터리가 떨어져도 맞다
전원 전압이 변하는 제품에서는 이 보정이 필수다. 안 하면 배터리가 줄어들 때 센서 값이 같이 흘러간다.
샘플링 시간 — 짧으면 값이 틀린다
ADC 는 내부 커패시터에 전압을 담아 그 값을 읽는다. 그 커패시터가 충전될 시간이 필요하다.
MCU 내부
센서 ──[ Rs ]──── 핀 ────┬── S/H 커패시터 (약 4pF)
센서 출력 저항 │
ADC
// 샘플링 시간이 짧으면 커패시터가 다 안 차서 값이 낮게 나온다
ADC1->SMPR2 |= (0b000 << 0); // 3 사이클 — 저항이 낮은 소스만
ADC1->SMPR2 |= (0b111 << 0); // 480 사이클 — 저항이 높은 소스(서미스터 등)
"센서 값이 실제보다 낮게 나온다" 의 흔한 원인이 이것이다. 서미스터나 분압 저항처럼 출력 저항이 큰 소스는 충전이 느리므로 샘플링 시간을 늘려야 한다.
필요한 샘플링 시간 ∝ (센서 출력 저항 + 내부 저항) × 커패시터
데이터시트에 최대 허용 소스 저항 표가 있다. 그것을 넘으면 샘플링 시간을 아무리 늘려도 정확도가 안 나오므로, 오피앰프 버퍼를 앞에 둔다.
값이 튀는 이유와 대응
// ✗ 한 번 읽고 그대로 쓴다
uint16_t v = read_adc(); // 잡음이 그대로 들어온다
① 여러 번 읽어 평균
uint16_t read_avg(int n) {
uint32_t sum = 0;
for (int i = 0; i < n; i++) sum += read_adc();
return sum / n;
}
무작위 잡음은 평균으로 줄어든다. N 번 평균하면 잡음이 √N 배 줄어든다 — 16번 평균이면 4배 개선이다. 다만 시간이 N 배 걸린다.
② 중앙값 — 튀는 값 하나를 버린다
uint16_t median3(uint16_t a, uint16_t b, uint16_t c) {
if (a > b) { uint16_t t=a; a=b; b=t; }
if (b > c) { uint16_t t=b; b=c; c=t; }
if (a > b) { uint16_t t=a; a=b; b=t; }
return b;
}
평균은 이상값 하나에 끌려가지만 중앙값은 안 끌려간다. 스파이크성 잡음(모터 스위칭 등)에는 중앙값이 낫다.
③ 이동 평균 필터 — 정수만으로
// 지수 이동 평균: y += (x - y) / 16 — 나눗셈 없이 시프트로
static int32_t filtered;
filtered += (raw - filtered) >> 4; // 시정수 약 16 샘플
곱셈도 나눗셈도 없이 필터가 된다. 메모리는 변수 하나뿐이라 임베디드에서 가장 많이 쓰인다. >> 4 를 >> 6 으로 바꾸면 더 부드럽고 더 느리게 따라온다.
④ 하드웨어로 — 회로가 먼저다
센서 ──[ R ]──┬── 핀
│
[ C ] RC 저역 통과 필터
│
GND
차단 주파수 = 1 / (2π × R × C)
R=1kΩ, C=100nF → 약 1.6kHz 이상을 걸러낸다
소프트웨어 필터로 못 살리는 잡음이 있다. 샘플링 주파수의 절반(나이퀴스트)보다 높은 성분은 낮은 주파수로 위장해 들어오므로(에일리어싱) 디지털로는 구분이 불가능하다. 그건 회로에서 걸러야 한다.
DMA — CPU 없이 연속 변환
// ADC 를 계속 돌려 결과를 배열에 자동으로 채운다
ADC1->CR2 |= ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_DDS;
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; // 출발지: ADC 데이터 레지스터
DMA2_Stream0->M0AR = (uint32_t)adc_buf; // 목적지: 우리 배열
DMA2_Stream0->NDTR = ADC_BUF_LEN;
DMA2_Stream0->CR |= DMA_SxCR_CIRC // 순환 — 끝나면 처음으로
| DMA_SxCR_MINC // 목적지 주소 증가
| (0b01 << DMA_SxCR_PSIZE_Pos) // 16비트
| (0b01 << DMA_SxCR_MSIZE_Pos)
| DMA_SxCR_EN;
이제 adc_buf 에 값이 계속 갱신된다. CPU 는 아무것도 안 하고, 읽고 싶을 때 배열을 보면 된다.
// 여러 채널을 순서대로 — 스캔 모드
ADC1->SQR1 = (3 - 1) << 20; // 3개 채널
ADC1->SQR3 = (0) | (1 << 5) | (4 << 10); // CH0, CH1, CH4 순서
ADC1->CR1 |= ADC_CR1_SCAN;
// adc_buf[0]=CH0, adc_buf[1]=CH1, adc_buf[2]=CH4 로 채워진다
DMA 로 채운 버퍼는
volatile로 선언한다. 하드웨어가 바꾸므로 컴파일러가 캐시하면 옛 값을 읽는다. D-Cache 가 있는 칩(M7)이라면 캐시 무효화까지 필요하다.
정확도를 갉아먹는 것들
| 원인 | 대응 |
|---|---|
| 전원 잡음 | VDDA·VREF+ 에 디커플링 커패시터(100nF + 1µF) |
| 접지 루프 | 아날로그 접지를 분리하고 한 점에서 합친다 |
| 디지털 스위칭 간섭 | ADC 변환 중에는 PWM·통신을 피하거나 타이머로 동기 |
| 채널 간 누화 | 샘플링 시간을 늘리고 채널 사이를 띄운다 |
| 온도 드리프트 | 내부 기준으로 주기적 재보정 |
PWM 이 돌아가는 동안 ADC 를 읽으면 값이 튄다. 스위칭 순간의 전원 흔들림이 그대로 들어오기 때문이다. 타이머 트리거로 PWM 이 꺼져 있는 구간에 변환을 맞추는 것이 모터 제어에서 쓰는 정석이다.
// 타이머 이벤트로 ADC 를 시작 — 소프트웨어 타이밍보다 정확하다
ADC1->CR2 |= (0b0110 << ADC_CR2_EXTSEL_Pos) // TIM2 CC2 이벤트
| (0b01 << ADC_CR2_EXTEN_Pos); // 상승 엣지
DAC — 반대 방향
DAC->CR |= DAC_CR_EN1;
DAC->DHR12R1 = 2048; // 12비트 → 약 1.65V 출력
DAC 는 진짜 아날로그 전압을 만든다(PWM 과 다르다). 다만 출력 전류가 아주 작아 버퍼 없이 부하를 물리면 전압이 떨어진다. 오피앰프로 받아 쓰는 것이 기본이다.
| PWM | DAC | |
|---|---|---|
| 출력 | 디지털 펄스 | 진짜 아날로그 |
| 필터 | 필요(RC) | 불필요 |
| 채널 수 | 타이머 수만큼 많다 | 보통 1~2개 |
| 전류 | 핀 전류만큼 | 아주 약하다 |
실무 순서
// ① 원시값을 그대로 로그로 본다 — 가공 전에 무엇이 오는지 확인
printf("raw=%u\r\n", ADC1->DR);
// ② 흔들림의 폭을 센다
uint16_t mn = 0xFFFF, mx = 0;
for (int i = 0; i < 1000; i++) { uint16_t v = read_adc(); if(v<mn)mn=v; if(v>mx)mx=v; }
printf("min=%u max=%u spread=%u\r\n", mn, mx, mx - mn);
흔들림 폭(spread)이 몇 LSB 인지 먼저 재는 것이 순서다. 12비트에서 ±2 LSB 면 실용적으로 충분하고, ±50 LSB 면 회로나 샘플링 시간에 문제가 있다는 뜻이다. 이 숫자를 모르고 필터를 넣으면 무엇이 개선됐는지 알 수 없다.
한눈에 정리
- ADC 는 "Vref 에 대한 비율" 만 알려 준다 — 전원이 변하는 제품은 내부 기준(VREFINT)으로 실제 Vref 를 역산한다
- LSB 보다 작은 변화는 못 읽는다 — 12비트 3.3V 에서 0.806mV
- 샘플링 시간이 짧으면 값이 낮게 나온다 — 출력 저항이 큰 센서는 시간을 늘리고, 한계를 넘으면 버퍼를 넣는다
- 평균은 잡음을 √N 배 줄이고, 중앙값은 스파이크에 강하다. 지수 이동 평균은 시프트만으로 된다
- 에일리어싱은 소프트웨어로 못 잡는다 — 나이퀴스트 위 성분은 RC 필터로 미리 걸러야 한다
- DMA + 순환 모드로 CPU 없이 배열을 계속 채운다. 그 버퍼는
volatile - PWM 이 돌 때 ADC 를 읽으면 튄다 — 타이머 트리거로 스위칭 구간을 피한다
- DAC 는 진짜 아날로그지만 전류가 약하다 — 버퍼가 필요하다
- 필터를 넣기 전에 흔들림 폭이 몇 LSB 인지 먼저 잰다
꼬리질문 대비
- "12비트 ADC 인데 실효 분해능이 낮은 이유는?" → Vref 1% 편차만으로 약 41 LSB 가 흔들린다 — 분해능보다 기준 전압 정밀도가 먼저
- "Vref 정밀도가 거의 무의미해지는 경우는?" → 비율 측정 — 센서와 Vref 가 같은 전원을 쓰면 비율이 유지된다
- "채널을 바꾼 직후에만 값이 이상하다면?" → 샘플링 시간 부족. 소스 임피던스가 높아 내부 커패시터 충전이 덜 끝난 것
- "값이 튀는 것을 소프트웨어로 어떻게 다루나?" → 평균·중앙값·이동평균. 다만 회로(디커플링·필터)가 먼저다
출처 — STMicroelectronics, RM0090 STM32F4 Reference Manual §13 ADC(SMPR·SQR·CR2·EXTSEL)·§14 DAC · STMicroelectronics, DS8626 STM32F407 Datasheet — ADC 특성·최대 소스 저항 · STMicroelectronics, AN2834 STM32 ADC 정확도 향상 기법