Arduino millis()와 STM32 HAL_GetTick()을 이용해 LED를 제어하고 블로킹, 상태 머신, 타이밍 한계, 인터럽트와 DMA의 역할을 이해합니다.
LED를 기다리는 동안 버튼도 기다린다
LED를 켜고 delay(500), 끄고 delay(500)를 실행하면 눈에는 깜빡이는 LED가 보인다. 하지만 그 루프에서 버튼 읽기나 통신 처리를 함께 한다면 기다리는 시간이 기능의 응답을 늦춘다.
delay() 중에도 플랫폼의 인터럽트는 동작할 수 있다. 따라서 ‘CPU의 모든 기능이 정지한다’는 설명은 정확하지 않다. 핵심은 현재 실행 흐름이 다음 애플리케이션 작업으로 진행하지 못한다는 점이다.
여러 일을 해야 한다면 ‘500 ms를 기다린다’를 ‘500 ms가 지났는지 확인한다’로 바꿔보자. Arduino의 Blink Without Delay 공식 예제도 같은 원리를 설명한다.
Arduino UNO에서 시간 차이를 계산하기
아래 스케치는 Arduino UNO Rev3의 내장 LED를 500 ms마다 토글한다. ON·OFF 한 주기는 약 1초다.
#include
const uint32_t ledToggleIntervalMs = 500;
uint32_t lastLedToggleMs = 0;
bool ledIsOn = false;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
lastLedToggleMs = millis();
}
void loop() {
const uint32_t currentTimeMs = millis();
if (static_cast(currentTimeMs - lastLedToggleMs)
>= ledToggleIntervalMs) {
lastLedToggleMs = currentTimeMs;
ledIsOn = !ledIsOn;
digitalWrite(LED_BUILTIN, ledIsOn ? HIGH : LOW);
}
// 여기서 버튼 확인 등 짧은 작업을 수행한다.
}
부호 없는 정수의 차이를 사용하는 이유는 타이머 카운터가 최댓값을 지나 0으로 돌아오는 상황을 처리하기 위해서다.
이 방식은 시간 차이가 표현 가능한 범위 안에 있고 충분히 자주 확인한다는 전제에서 사용한다. 영원히 호출하지 않아도 정확하다는 뜻은 아니다.
이 예제는 실제 처리 시각을 다음 기준으로 삼으므로 루프의 지연이 누적되면 주기가 조금씩 밀릴 수 있다. 엄격한 일정이 필요하다면 절대 마감 시각을 관리하거나 하드웨어 타이머를 검토한다. 지연 후 밀린 작업을 모두 따라잡을지, 오래된 작업을 건너뛸지도 정책으로 정해야 한다.
STM32에서도 원리는 같다
다음은 STM32Cube HAL 프로젝트의 코드 조각이다. NUCLEO-F401RE의 LD2가 연결된 PA5를 출력으로 초기화한 조건을 가정한다. HAL_Init(), SystemClock_Config(), MX_GPIO_Init() 등 생성된 초기화 흐름은 유지한다. 전체 프로젝트를 대신하는 파일은 아니다.
/* 생성된 GPIO 초기화 이후, main()의 while(1) 앞 */
uint32_t lastLedToggleMs = HAL_GetTick();
const uint32_t ledToggleIntervalMs = 500U;
while (1)
{
const uint32_t currentTimeMs = HAL_GetTick();
if ((uint32_t)(currentTimeMs - lastLedToggleMs)
>= ledToggleIntervalMs)
{
lastLedToggleMs = currentTimeMs;
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
/* 짧은 버튼 확인, 수신 데이터 처리 등을 배치 */
}
HAL_GetTick()의 기본 시간 기준은 보통 1 ms지만 프로젝트에서 시간 기준을 변경할 수 있다. 저전력 모드에서 tick을 중지하는 경우도 있다. 따라서 시간 함수의 숫자가 실제 경과 시간과 어떻게 대응하는지 확인해야 한다.
두 코드를 비교하면 시간 읽기와 GPIO 접근 함수만 바뀌고 애플리케이션의 판단 구조는 거의 같다. 이 부분이 하드웨어에 의존하는 코드와 제품 동작을 분리하는 출발점이다.

delay()만 지웠다고 전부 논블로킹은 아니다
센서 읽기 함수가 응답을 기다리거나 직렬 출력 버퍼가 가득 찬다면 여전히 오래 걸릴 수 있다. 통신 함수의 timeout이 1초라면 LED 코드가 논블로킹이어도 버튼 반응이 늦어질 수 있다.
확인할 것은 각 작업의 최악 실행 시간이다. 센서 변환에 시간이 필요하면 ‘변환 시작 → 다른 작업 수행 → 완료 확인 → 결과 읽기’로 나눌 수 있다.
긴 데이터 전송은 인터럽트나 DMA 기반 처리로 옮기고, 완료 콜백에서는 상태를 기록한 뒤 메인 흐름에서 후속 처리를 수행한다.
인터럽트는 짧게 끝내는 것이 좋다.
긴 로그 출력과 복잡한 연산을 인터럽트 안에 모으면 다른 이벤트의 지연이 커진다.
또한 volatile은 공유 데이터의 원자성이나 상호 배제를 보장하지 않는다.
ISR과 메인 루프가 같은 데이터를 다룰 때에는 읽기·쓰기 크기, 경쟁 상태, 필요한 임계구역을 따로 검토한다.
상태 머신은 기다림을 상태로 바꾸는 도구다
센서를 읽는 동작을 IDLE, WAITING, READY, ERROR로 나눠보자. IDLE에서는 변환을 시작하고 시작 시간을 기록한다. WAITING에서는 완료 여부와 timeout을 확인한다. READY에서는 결과를 처리하고, ERROR에서는 재시도나 오류 보고 정책을 실행한다.
이렇게 하면 ‘응답이 올 때까지 붙잡고 있기’보다 각 단계가 언제 끝났는지 관찰하기 쉽다. 통신 장치의 재연결, 모터의 준비·가동·정지, 사용자 인터페이스에도 같은 방식이 적용된다.
다만 상태 머신을 만들었다고 자동으로 실시간성이 보장되지는 않는다. 각 상태 안의 처리 시간이 길면 루프 전체가 느려진다. 실제 요구 응답 시간과 측정 결과를 기준으로 평가한다.
동작하는 코드에서 검증 가능한 제품으로

버튼 응답 시간이 요구사항에 맞는지, 통신이 끊겼을 때 timeout과 복구가 동작하는지, tick 카운터의 래핑을 처리하는지, 센서 오류가 다른 기능까지 멈추게 하는지 확인해 보자.
긴 구간의 실행 시간을 GPIO 펄스나 시간 기록으로 측정하면 개선할 위치를 찾을 수 있다.
코드를 바꾸는 이유는 ‘고급스럽게 보이기’가 아니다. 장치가 기다리는 동안 다른 일을 하고, 실패했을 때도 다음 행동을 결정하기 위해서다.
'산업 IoT & 스마트 팩토리 디지털트윈 > 임베디드&하드웨어 개발' 카테고리의 다른 글
| LED 미용 거울 회로 설계 공개 — KiCad·Gerber·BOM·펌웨어 파일 설명 (0) | 2026.10.08 |
|---|---|
| LED는 켜지는데 제품은 왜 멈출까? 전원·GPIO·회로의 기본 (0) | 2026.10.08 |
| ATmega와 STM32, 무엇이 다를까? 제품 개발자가 MCU를 고르는 기준 (0) | 2026.10.07 |


























