
HC-SR04는 초음파 펄스의 왕복 시간을 측정하여 거리를 계산하는 센서입니다. STM32에서는 Trigger 신호 생성에 GPIO + 타이머 지연을 사용하고, Echo 신호 측정에 타이머 입력 캡처를 활용합니다. 이번 강의에서는 센서 인터페이스 전체 흐름을 구현하고, 실제 측정값의 정확도를 높이기 위한 필터링 기법까지 다룹니다.
HC-SR04 사양
동작 전압 : 5V DC
동작 전류 : 15mA
측정 주파수: 40kHz 초음파
측정 범위 : 2cm ~ 400cm
측정 각도 : 15도 이내
Trigger 입력 펄스 폭: 최소 10us HIGH
Echo 출력: 펄스 폭이 왕복 시간에 비례 (최대 38ms = 타임아웃)
핀 구성
VCC → 5V 전원
GND → GND
TRIG → Trigger 입력 (MCU GPIO 출력)
ECHO → Echo 출력 (MCU GPIO 입력 / 타이머 입력 캡처)
주의: ECHO 핀 출력 전압은 5V
STM32 GPIO는 3.3V 허용 → 전압 분배 회로 또는 레벨 시프터 필요
전압 분배 예시 (5V → 3.3V):
측정 원리
음속: 약 340m/s (20도 기준)
= 0.034cm/us = 1cm당 29.4us
거리 계산:
Echo 펄스 폭(us) = 초음파 왕복 시간
거리(cm) = Echo 펄스 폭(us) / 58
= Echo 펄스 폭(us) × 0.017
(왕복이므로 편도 시간 = 펄스 폭 / 2, 거리 = (펄스 폭 / 2) / 29.4)
신호 타이밍 다이어그램
타이밍 규격:
- TRIG HIGH: 최소 10us
- TRIG → ECHO 상승 엣지: 약 450us ~ 600us (센서 내부 처리)
- Echo 펄스 폭: 거리에 비례 (2cm = 116us, 400cm = 23,200us)
- 최소 측정 주기: 60ms 이상 권장 (이전 초음파 반향 소거)
권장 핀 배치 (STM32F411 기준)
HC-SR04 TRIG → PA1 (GPIO 출력, Push-Pull)
HC-SR04 ECHO → PA6 (TIM3_CH1, 입력 캡처 AF)
전압 분배 회로를 ECHO와 PA6 사이에 삽입
TIM2: Trigger 지연 생성용 (10us 펄스 타이밍)
TIM3: Echo 입력 캡처용 (CH1: 상승 엣지, CH2: 하강 엣지)
CubeMX 타이머 설정
TIM3 설정 (입력 캡처):
- Clock Source : Internal Clock
- Prescaler : 83 (84MHz / (83+1) = 1MHz → 1us 분해능)
- Counter Period: 65535 (16비트 최대)
- CH1 : Input Capture direct mode, Rising Edge
- CH2 : Input Capture indirect mode (CH1으로부터), Falling Edge
- NVIC : TIM3 global interrupt 활성화
TIM2 설정 (Trigger 지연):
- Clock Source : Internal Clock
- Prescaler : 83
- Counter Period: 9 (10us 카운트 후 UIF 발생)
- One Pulse Mode: 활성화 (1회 카운트 후 자동 정지)
- NVIC : TIM2 global interrupt 활성화
간접 캡처(Indirect Capture) 구조
TIM3_CH1 (PA6): 상승 엣지 캡처 (Echo HIGH 시작)
TIM3_CH2 : 하강 엣지 캡처 (Echo LOW 종료, CH1 핀을 공유)
CubeMX에서 CH2를 "Input Capture indirect mode"로 설정하면
CH1 핀(PA6)의 신호를 반전하여 CH2가 하강 엣지를 캡처함
측정 흐름:
1. Echo 상승 엣지 → TIM3 CH1 캡처 레지스터에 CNT 저장
2. Echo 하강 엣지 → TIM3 CH2 캡처 레지스터에 CNT 저장
3. 펄스 폭 = CH2 캡처값 - CH1 캡처값 (us 단위)
project/
├── Core/
│ ├── Inc/
│ │ ├── hcsr04.h
│ │ └── main.h
│ └── Src/
│ ├── hcsr04.c
│ └── main.c
#ifndef HCSR04_H
#define HCSR04_H
#include "stm32f4xx_hal.h"
/* 센서 측정 상태 */
typedef enum
{
HCSR04_IDLE = 0,
HCSR04_TRIGGERED,
HCSR04_CAPTURING,
HCSR04_DONE,
HCSR04_TIMEOUT,
HCSR04_ERROR,
} HCSR04_State_t;
/* 센서 핸들 구조체 */
typedef struct
{
TIM_HandleTypeDef *htim_capture; /* 입력 캡처 타이머 (TIM3) */
TIM_HandleTypeDef *htim_trigger; /* Trigger 지연 타이머 (TIM2) */
GPIO_TypeDef *trig_port; /* TRIG GPIO 포트 */
uint16_t trig_pin; /* TRIG GPIO 핀 */
uint32_t capture_rise; /* 상승 엣지 캡처값 */
uint32_t capture_fall; /* 하강 엣지 캡처값 */
volatile HCSR04_State_t state; /* 현재 측정 상태 */
float distance_cm; /* 최종 거리 결과 */
} HCSR04_Handle_t;
/* 측정 유효 범위 */
#define HCSR04_ECHO_MIN_US 116U /* 2cm = 116us */
#define HCSR04_ECHO_MAX_US 23200U /* 400cm = 23200us */
#define HCSR04_TIMEOUT_MS 50U /* 타임아웃 50ms */
/* API */
void HCSR04_Init(HCSR04_Handle_t *dev,
TIM_HandleTypeDef *htim_cap,
TIM_HandleTypeDef *htim_trig,
GPIO_TypeDef *trig_port,
uint16_t trig_pin);
void HCSR04_Trigger(HCSR04_Handle_t *dev);
float HCSR04_GetDistance(HCSR04_Handle_t *dev);
/* ISR 콜백 내에서 호출 */
void HCSR04_CaptureCallback(HCSR04_Handle_t *dev, TIM_HandleTypeDef *htim);
void HCSR04_TriggerDoneCallback(HCSR04_Handle_t *dev, TIM_HandleTypeDef *htim);
#endif /* HCSR04_H */
#include "hcsr04.h"
/* 음속 기반 변환 상수 (1us당 거리, 단위: cm) */
/* 왕복이므로 편도 = 1/2, 음속 340m/s = 0.034cm/us → 0.034/2 = 0.017 */
#define US_TO_CM (0.017f)
/* 초기화 */
void HCSR04_Init(HCSR04_Handle_t *dev,
TIM_HandleTypeDef *htim_cap,
TIM_HandleTypeDef *htim_trig,
GPIO_TypeDef *trig_port,
uint16_t trig_pin)
{
dev->htim_capture = htim_cap;
dev->htim_trigger = htim_trig;
dev->trig_port = trig_port;
dev->trig_pin = trig_pin;
dev->capture_rise = 0;
dev->capture_fall = 0;
dev->state = HCSR04_IDLE;
dev->distance_cm = -1.0f;
/* TRIG 핀 초기 LOW */
HAL_GPIO_WritePin(trig_port, trig_pin, GPIO_PIN_RESET);
/* 입력 캡처 시작 */
HAL_TIM_IC_Start_IT(htim_cap, TIM_CHANNEL_1); /* 상승 엣지 */
HAL_TIM_IC_Start_IT(htim_cap, TIM_CHANNEL_2); /* 하강 엣지 */
}
/* Trigger 펄스 시작 */
void HCSR04_Trigger(HCSR04_Handle_t *dev)
{
if (dev->state != HCSR04_IDLE) return; /* 이전 측정 미완료 시 무시 */
dev->state = HCSR04_TRIGGERED;
/* TRIG HIGH */
HAL_GPIO_WritePin(dev->trig_port, dev->trig_pin, GPIO_PIN_SET);
/* TIM2 시작: 10us 후 UIF 발생 → ISR에서 TRIG LOW */
__HAL_TIM_SET_COUNTER(dev->htim_trigger, 0);
HAL_TIM_Base_Start_IT(dev->htim_trigger);
}
/* TIM2 UIF ISR에서 호출: TRIG LOW 처리 */
void HCSR04_TriggerDoneCallback(HCSR04_Handle_t *dev, TIM_HandleTypeDef *htim)
{
if (htim->Instance != dev->htim_trigger->Instance) return;
HAL_TIM_Base_Stop_IT(dev->htim_trigger);
HAL_GPIO_WritePin(dev->trig_port, dev->trig_pin, GPIO_PIN_RESET);
dev->state = HCSR04_CAPTURING;
/* TIM3 카운터 초기화 (캡처 기준점 설정) */
__HAL_TIM_SET_COUNTER(dev->htim_capture, 0);
}
/* TIM3 입력 캡처 ISR에서 호출 */
void HCSR04_CaptureCallback(HCSR04_Handle_t *dev, TIM_HandleTypeDef *htim)
{
if (htim->Instance != dev->htim_capture->Instance) return;
if (dev->state != HCSR04_CAPTURING) return;
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
/* 상승 엣지: Echo 시작 */
dev->capture_rise = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
}
else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
/* 하강 엣지: Echo 종료 */
dev->capture_fall = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
dev->state = HCSR04_DONE;
}
}
/* 거리 계산 및 반환 */
float HCSR04_GetDistance(HCSR04_Handle_t *dev)
{
if (dev->state != HCSR04_DONE) return -1.0f;
uint32_t echo_us;
/* 오버플로우 처리 */
if (dev->capture_fall >= dev->capture_rise)
{
echo_us = dev->capture_fall - dev->capture_rise;
}
else
{
/* 16비트 카운터 오버플로우 발생 시 */
echo_us = (0xFFFF - dev->capture_rise) + dev->capture_fall + 1U;
}
/* 유효 범위 검사 */
if (echo_us < HCSR04_ECHO_MIN_US || echo_us > HCSR04_ECHO_MAX_US)
{
dev->state = HCSR04_ERROR;
dev->distance_cm = -1.0f;
return -1.0f;
}
dev->distance_cm = (float)echo_us * US_TO_CM;
dev->state = HCSR04_IDLE;
return dev->distance_cm;
}
#include "main.h"
#include "hcsr04.h"
#include <stdio.h>
/* HAL 타이머 핸들 (CubeMX 생성) */
extern TIM_HandleTypeDef htim2;
extern TIM_HandleTypeDef htim3;
/* 센서 핸들 */
HCSR04_Handle_t hcsr04;
/* 입력 캡처 콜백 → HCSR04 드라이버로 위임 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
HCSR04_CaptureCallback(&hcsr04, htim);
}
/* 타이머 주기 만료 콜백 (TIM2: Trigger 10us 완료) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
HCSR04_TriggerDoneCallback(&hcsr04, htim);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_USART2_UART_Init();
/* 센서 초기화: TIM3(캡처), TIM2(트리거), PA1(TRIG 핀) */
HCSR04_Init(&hcsr04, &htim3, &htim2, GPIOA, GPIO_PIN_1);
uint32_t last_trigger = 0;
while (1)
{
/* 60ms 간격으로 측정 (이전 초음파 반향 소거) */
if (HAL_GetTick() - last_trigger >= 60U)
{
last_trigger = HAL_GetTick();
HCSR04_Trigger(&hcsr04);
}
/* 측정 완료 시 결과 출력 */
if (hcsr04.state == HCSR04_DONE)
{
float dist = HCSR04_GetDistance(&hcsr04);
if (dist >= 0.0f)
{
printf("Distance: %.1f cm\r\n", dist);
}
else
{
printf("Distance: out of range\r\n");
}
}
}
}
이동 평균 필터 원리
단일 측정값은 반사면 각도, 노이즈, 다중 반사 등으로 튐 현상 발생
N회 측정값의 평균을 사용하면 노이즈 감소
N이 클수록 안정적이나 응답 지연 증가
실용 범위: N = 4 ~ 16
이동 평균 필터 구현
#define FILTER_SIZE 8U
typedef struct
{
float buf[FILTER_SIZE];
uint8_t idx;
uint8_t count;
float sum;
} MovAvgFilter_t;
void filter_init(MovAvgFilter_t *f)
{
f->idx = 0;
f->count = 0;
f->sum = 0.0f;
for (uint8_t i = 0; i < FILTER_SIZE; i++) f->buf[i] = 0.0f;
}
float filter_update(MovAvgFilter_t *f, float new_val)
{
/* 유효하지 않은 값은 필터에 반영하지 않음 */
if (new_val < 0.0f) return (f->count > 0) ? (f->sum / f->count) : -1.0f;
f->sum -= f->buf[f->idx];
f->buf[f->idx] = new_val;
f->sum += new_val;
f->idx = (f->idx + 1) % FILTER_SIZE;
if (f->count < FILTER_SIZE) f->count++;
return f->sum / f->count;
}
필터 적용 예시
MovAvgFilter_t dist_filter;
int main(void)
{
// ...초기화...
filter_init(&dist_filter);
while (1)
{
if (HAL_GetTick() - last_trigger >= 60U)
{
last_trigger = HAL_GetTick();
HCSR04_Trigger(&hcsr04);
}
if (hcsr04.state == HCSR04_DONE)
{
float raw = HCSR04_GetDistance(&hcsr04);
float filt = filter_update(&dist_filter, raw);
if (filt >= 0.0f)
{
printf("Raw: %.1f cm | Filtered: %.1f cm\r\n", raw, filt);
}
}
}
}
음속의 온도 의존성
음속(m/s) = 331.5 + 0.6 × T(도)
온도별 음속 및 오차:
T = 0도 → 331.5m/s
T = 20도 → 343.5m/s (기본값으로 340 사용 시 약 1% 오차)
T = 40도 → 355.5m/s
100cm 측정 기준:
T = 0도 기준 340m/s로 계산 시: 오차 약 ±8mm
T = 40도 기준 340m/s로 계산 시: 오차 약 ±15mm
온도 보정이 필요한 경우: 측정 정밀도 5mm 이하 요구 시
온도 보정이 불필요한 경우: 일반적인 장애물 감지(수 cm 허용 오차)
온도 보정 적용 코드
/* 온도 센서(LM35 등)로부터 온도 읽기 후 적용 */
float HCSR04_GetDistanceWithTempComp(uint32_t echo_us, float temp_c)
{
float sound_speed_cm_us = (331.5f + 0.6f * temp_c) * 0.0001f;
/* m/s → cm/us 변환: × 100 / 1,000,000 = × 0.0001 */
return ((float)echo_us * sound_speed_cm_us) / 2.0f;
}
중간값 필터 원리
N회 측정 후 중앙값을 선택
이상치(Outlier) 제거에 효과적
반사 노이즈, 전기적 노이즈에 의한 단발성 오류 제거
N = 3 (최소 구성), N = 5 권장
중간값 필터 구현 (N=5)
#define MEDIAN_SIZE 5U
float median_filter(float *buf, uint8_t size)
{
/* 버블 정렬 후 중앙값 반환 */
float sorted[MEDIAN_SIZE];
for (uint8_t i = 0; i < size; i++) sorted[i] = buf[i];
for (uint8_t i = 0; i < size - 1; i++)
{
for (uint8_t j = 0; j < size - 1 - i; j++)
{
if (sorted[j] > sorted[j + 1])
{
float tmp = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = tmp;
}
}
}
return sorted[size / 2]; /* 중앙값 */
}
타임아웃이 발생하는 상황
1. 측정 범위 초과 (400cm 이상, Echo 펄스 폭 > 23ms)
2. 반사면 없음 (개방 공간, 흡음 재질 등)
3. 센서 연결 불량
Echo 미수신 시 상태 머신이 CAPTURING에 고착
→ 다음 HCSR04_Trigger() 호출이 무시됨
→ 주기적인 타임아웃 해제 처리 필요
타임아웃 처리 구현
/* HCSR04_Handle_t 에 타임아웃 필드 추가 */
typedef struct
{
// ...기존 필드...
uint32_t trigger_tick; /* Trigger 시점의 HAL_GetTick() 값 */
} HCSR04_Handle_t;
/* Trigger 시 타임아웃 시작 시각 기록 */
void HCSR04_Trigger(HCSR04_Handle_t *dev)
{
if (dev->state != HCSR04_IDLE) return;
dev->trigger_tick = HAL_GetTick();
dev->state = HCSR04_TRIGGERED;
HAL_GPIO_WritePin(dev->trig_port, dev->trig_pin, GPIO_PIN_SET);
__HAL_TIM_SET_COUNTER(dev->htim_trigger, 0);
HAL_TIM_Base_Start_IT(dev->htim_trigger);
}
/* 메인 루프에서 주기적으로 호출 */
void HCSR04_Poll(HCSR04_Handle_t *dev)
{
if (dev->state == HCSR04_CAPTURING || dev->state == HCSR04_TRIGGERED)
{
if (HAL_GetTick() - dev->trigger_tick > HCSR04_TIMEOUT_MS)
{
dev->state = HCSR04_TIMEOUT;
dev->distance_cm = -1.0f;
/* 다음 측정을 위해 IDLE로 복귀 */
dev->state = HCSR04_IDLE;
}
}
}
/* main 루프 */
while (1)
{
HCSR04_Poll(&hcsr04); /* 타임아웃 감시 */
if (HAL_GetTick() - last_trigger >= 60U)
{
last_trigger = HAL_GetTick();
HCSR04_Trigger(&hcsr04);
}
if (hcsr04.state == HCSR04_DONE)
{
float dist = HCSR04_GetDistance(&hcsr04);
printf("Distance: %.1f cm\r\n", (dist >= 0.0f) ? dist : -1.0f);
}
}
증상별 원인 분석
증상 1: 거리가 항상 -1.0f (측정 실패)
원인 1-1: ECHO 신호가 MCU에 도달하지 않음
→ 전압 분배 회로 및 배선 확인
→ 오실로스코프로 PA6 핀 파형 확인
원인 1-2: 입력 캡처 초기화 누락
→ HAL_TIM_IC_Start_IT() 호출 여부 확인
원인 1-3: TRIG 핀이 HIGH가 되지 않음
→ CubeMX GPIO 출력 설정 확인
증상 2: 측정값이 심하게 요동침 (±수 cm)
원인: 다중 반사, 센서 주변 장애물, 전원 노이즈
해결: 이동 평균 필터 또는 중간값 필터 적용
센서 전용 디커플링 커패시터 추가 (100nF + 10uF)
증상 3: 측정값이 실제보다 항상 크거나 작음
원인: 음속 상수 오류 또는 타이머 분해능 불일치
확인: Prescaler 계산 재검토 (1MHz = 1us 분해능 여부)
US_TO_CM 상수 (0.017) 재확인
증상 4: 첫 번째 측정만 성공하고 이후 실패
원인: state가 DONE 또는 ERROR에서 IDLE로 복귀하지 않음
해결: HCSR04_GetDistance() 호출 후 state = IDLE 확인
타임아웃 처리 로직 점검
Trigger 및 Echo 타이밍 검증
/* TRIG 펄스 폭 검증 */
uint32_t t_start, t_end;
t_start = DWT->CYCCNT;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
/* TIM2 10us 대기 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
t_end = DWT->CYCCNT;
uint32_t trig_us = (t_end - t_start) / 84U;
printf("TRIG pulse width: %lu us\r\n", trig_us);
/* 기대값: 10us ± 1us */
문제 1: ECHO 5V → 3.3V 변환 누락
STM32 GPIO 최대 허용 입력: VDD + 0.3V ≈ 3.6V
5V 직결 시 ESD 다이오드를 통해 전류 유입 → GPIO 손상 또는 오동작
반드시 전압 분배 회로 또는 레벨 시프터 삽입
비용 절감 시: 1kΩ + 2kΩ 분압 (5V → 3.33V)
정밀 보호 필요 시: 74LVC1T45 또는 BSS138 레벨 시프터
문제 2: 측정 주기가 너무 짧아 이전 초음파 간섭
이전 측정의 초음파가 먼 거리에서 반사되어 돌아오는 동안
다음 TRIG 펄스가 발사되면 잘못된 Echo 감지 발생
최소 측정 주기: 60ms 이상 (데이터시트 권장)
400cm 왕복 시간: 약 23ms + 안전 여유 = 60ms
문제 3: TIM3 Prescaler 계산 오류
목표: 1us 분해능 (1MHz 카운트 클럭)
84MHz 기준: Prescaler = 84 - 1 = 83
잘못된 설정:
Prescaler = 84 → 클럭 = 84MHz / 85 ≈ 988kHz (1us보다 느림)
Prescaler = 83 → 클럭 = 84MHz / 84 = 1MHz (정확)
CubeMX Prescaler 입력값 = 실제값 - 1 임에 주의
HC-SR04 센서 인터페이스
상태 머신 기반 드라이버 구조
측정 정확도 향상
1. 거리 계산 공식
거리(cm) = Echo 펄스 폭(us) × 0.017
= Echo 펄스 폭(us) / 58
음속 340m/s 기준, 왕복 편도 계산
1cm 왕복 = 약 58us
2. 타이머 설정 핵심
TIM3 Prescaler: 84 - 1 = 83 (1MHz, 1us 분해능)
TIM3 CH1: 상승 엣지 캡처 (direct)
TIM3 CH2: 하강 엣지 캡처 (indirect, CH1 핀 공유)
TIM2 ARR: 10 - 1 = 9 (10us 후 UIF → TRIG LOW)
3. 입력 캡처 오버플로우 처리
if (fall >= rise)
echo_us = fall - rise;
else
echo_us = (0xFFFF - rise) + fall + 1U;
4. 측정 주기 및 유효 범위
최소 측정 주기 : 60ms
유효 Echo 범위 : 116us (2cm) ~ 23200us (400cm)
타임아웃 기준 : 50ms 내 DONE 미달성 시 IDLE 복귀
| 항목 | 확인 사항 | 상태 |
|---|---|---|
| 전압 분배 | ECHO 5V → 3.3V 변환 회로 적용 여부 | |
| Prescaler | TIM3 1us 분해능 설정 확인 (83) | |
| 입력 캡처 | CH1 상승/CH2 하강 엣지 설정 여부 | |
| TRIG 펄스 | 10us 이상 HIGH 유지 확인 | |
| 측정 주기 | 60ms 이상 간격 유지 여부 | |
| 타임아웃 | 50ms 이내 미완료 시 IDLE 복귀 처리 | |
| 유효 범위 | 116us ~ 23200us 외 값 필터링 여부 | |
| 필터 적용 | 이동 평균 또는 중간값 필터 적용 여부 |