ADC 값이 몇 카운트씩 흔들릴 때 최근 샘플의 평균을 내면 표시값을 안정시킬 수 있습니다. 하지만 배열을 0으로 채운 뒤 처음부터 16으로 나누면 시작값이 실제보다 작아지고, 누산기를 너무 작게 잡으면 정상 입력에서도 평균이 깨집니다. 이 글은 16개 샘플을 보관하는 이동평균을 C로 구현하고 초기화·정수 범위·응답 지연을 재현합니다.
대상은 MCU에서 온도·조도·전압 등 비교적 천천히 변하는 센서의 ADC 값을 처리하는 펌웨어 개발자입니다. 아래 예제는 Windows 호스트의 GCC에서 실행한 순수 계산 코드이며, 특정 MCU의 ADC 드라이버나 실제 센서 잡음 성능을 시험한 결과는 아닙니다.
1. 블록 평균과 이동평균의 차이
블록 평균은 새 샘플 16개가 모일 때마다 결과 한 개를 만듭니다. 이동평균은 처음 창을 채운 뒤 새 샘플이 들어올 때 가장 오래된 샘플을 빼고, 새 샘플을 더해 결과를 갱신합니다. 1 kHz로 샘플링하는 이동평균이라면 결과도 매 1 ms마다 갱신할 수 있습니다. 갱신 주기가 유지된다고 입력 변화에 즉시 반응하는 것은 아닙니다.
Microchip AN2801은 ADC의 누산·평균·버스트 평균·저역통과 모드를 구분합니다. 사용하는 MCU의 하드웨어 평균 기능이 아래의 겹치는 이동 창과 동일하게 동작한다고 가정하면 안 됩니다. 해당 장치의 누산 초기화 시점과 결과 갱신 조건을 확인해야 합니다. 공식 AN2801, 1~2장
2. 초기 샘플 수와 누산 범위를 먼저 정하기
이 예제는 아직 창이 차지 않았을 때 실제로 받은 샘플 수로 나눕니다. 첫 입력이 1000이면 출력도 1000이고, 다음 입력이 1200이면 1100입니다. 이 기간에는 필터 길이가 변하므로 정상 상태와 잡음 감소량·주파수 응답이 다릅니다. 제어 로직에 일정한 창이 필요하면 count == WINDOW일 때만 결과를 사용하세요.
입력은 부호 없는 16비트입니다. 16개 합의 최댓값은 65535 × 16 = 1,048,560이므로 uint32_t 누산기를 사용합니다. 12비트 ADC만 고려해 16비트 누산기를 고르면 창 길이나 ADC 설정을 바꾸는 순간 문제가 생길 수 있습니다. 입력이 음수인 보정값이면 이 함수를 그대로 쓰지 말고 부호 있는 범위와 나눗셈 규칙을 다시 설계해야 합니다.
정수 나눗셈은 소수 부분을 버립니다. 예를 들어 1000과 1001의 평균은 1000으로 반환됩니다. 이 글은 반올림이나 추가 분해능 비트 보존을 구현하지 않습니다. 누산한 원본 합을 유지하고 출력할 때만 나누어야, 매 단계의 반올림 오차가 다음 상태에 누적되지 않습니다.
3. 복사해 실행할 수 있는 전체 C 예제
아래 내용을 adc_average.c로 저장하세요. GCC가 설치된 터미널에서 gcc -std=c11 -Wall -Wextra -Werror adc_average.c -o adc_average로 빌드하고 Windows PowerShell에서는 .\adc_average.exe, Linux에서는 ./adc_average로 실행합니다. 검증용 assert가 켜지도록 -DNDEBUG는 붙이지 마세요.
확인한 실행 결과는 다음과 같습니다.
순환 배열과 별도로 최근 샘플을 이동시키고 64비트로 합하는 기준 계산을 만들어 10,000개 입력에서 비교했습니다. 최댓값 1,000회, 0 입력, 초기 샘플 두 개, 단계 입력도 검사합니다. 이 코드는 average_push() 호출당 창 크기와 무관한 일정한 수의 연산을 수행합니다. 테스트 부분의 기준 계산은 검증을 위해 일부러 다른 방식으로 작성했습니다.
4. 매번 출력해도 지연은 남는다
창이 1000으로 가득 찬 상태에서 입력이 2600으로 바뀌었다고 합시다. 새 값이 들어온 횟수를 k라 하면, k가 1~16일 때 출력은 1000 + 100 × k입니다. 첫 출력은 1100, 여덟 번째는 1800, 열여섯 번째는 2600입니다. 1 ms 주기로 읽는다면 첫 변화 샘플과 완전히 바뀐 출력 사이에는 15 ms가 있습니다. 아날로그 입력이 샘플 사이에 변하면 다음 샘플까지의 대기시간도 더해집니다.
가득 찬 N개 창은 모든 계수가 1/N인 FIR 필터입니다. 샘플들의 시간 위치 0부터 N−1까지의 중심을 계산하면 그룹 지연은 (N−1)/2 샘플입니다. N=16, 1 kHz에서는 7.5 ms입니다. 이는 단계 응답이 완전히 정착하는 15 ms와 다른 지표입니다. 이 계산은 일정한 샘플 간격과 창이 가득 찬 정상 상태를 전제로 합니다. FIR의 계수·이전 입력·상태 버퍼 구조는 Arm CMSIS-DSP 공식 FIR 문서에서 확인할 수 있습니다. 이 예제는 CMSIS 함수를 호출하지 않는 자체 구현입니다.
5. 펌웨어에 넣을 때 확인할 사항
- 채널마다 별도
Average상태를 둡니다. 여러 센서 값을 한 창에 섞지 않습니다. - 필터 상태는 한 태스크 또는 한 실행 문맥이 소유하게 합니다. ISR과 태스크가 같은 구조체를 동시에 갱신하는 코드는 아닙니다.
volatile만 추가해도 동시 접근이 안전해지는 것은 아닙니다. - 타이머 트리거 등으로 샘플 간격을 관리합니다. 누락이나 지연이 있으면 최근 16개가 나타내는 실제 시간 구간도 달라집니다.
- 필터는 매 샘플 갱신하되, 화면 전송은 더 느리게 할 수 있습니다. 처리 주기와 표시 주기를 별도로 기록합니다.
- 채널·게인·기준전압·샘플 주기를 바꾼 뒤에는 기존 창을 계속 사용할지 초기화할지 명시합니다. 초기화는 구조체 전체를 0 상태로 되돌리는 방식입니다.
- 필터값만 보고 과전류 같은 빠른 보호 동작을 설계하지 않습니다. 필요한 반응 시간에 맞춰 원시값 비교나 하드웨어 보호 경로를 별도로 검토합니다.
6. 평균으로 해결되지 않는 문제
평균은 오프셋·기준전압 오차·클리핑을 교정하지 않습니다. 샘플링 전에 필요한 아날로그 대역 제한을 대신하지도 못합니다. 정적인 잘못된 입력을 여러 번 읽으면 잘못된 값의 평균이 나옵니다. 평균값과 원시값을 함께 기록해 실제 신호 변화까지 줄어드는지 확인하세요.
테스트의 다음 단계는 목표 보드에서 원시 ADC 값과 필터값에 같은 타임스탬프를 붙여, 센서 변화 후 필요한 시간 안에 임계값에 도달하는지 확인하는 것입니다. 이 글에서 검증한 것은 정수 연산과 상태 전이이며, 보드의 정확도·샘플링 지터·노이즈 감소량은 측정하지 않았습니다.
공식 문서 확인일: 2026-09-23. 예제 코드: JDevCompany 작성, CC0-1.0.
댓글
첫 댓글을 남겨 보세요.