개요
로봇 제어 코드를 작성하다 보면 **"일정 시간 대기"**가 반드시 필요합니다. Arduino에서는 delay(), ROS2에서는 sleep()을 사용하는데, 이름은 비슷해도 동작 방식이 완전히 다릅니다. 이 글에서는 두 함수의 차이점과, 제어 루프의 핵심 개념인 **Hz(주파수)**와 **ms(주기)**의 관계를 정리합니다.
sleep vs delay — 블로킹과 양보의 차이
delay()(Arduino): CPU를 점유한 채 대기 (블로킹)sleep()(ROS2/Linux): OS에 CPU를 반납하고 대기 (논블로킹)
한 줄 요약:
delay는 CPU를 멈추고,sleep은 CPU를 양보합니다.
| 항목 | delay() (Arduino) | sleep() (ROS2/Linux) |
|---|---|---|
| CPU 사용 | 붙잡고 있음 (독점) | 반납 (다른 일 가능) |
| 멀티태스킹 | 불가 | 가능 |
| 콜백 처리 | 멈춤 (인터럽트 제외) | 계속 동작 |
| OS 존재 | 없음 (베어메탈) | 있음 (Linux) |
| 코어 | 싱글 코어 | 멀티 코어 가능 |
sequenceDiagram
participant A as Arduino (delay)
participant CPU_A as CPU
A->>CPU_A: delay(33) 호출
Note over CPU_A: 33ms 동안 아무것도 못함
Note over CPU_A: 인터럽트 외 모든 처리 중단
CPU_A-->>A: 33ms 후 다음 줄 실행
participant R as ROS2 Node (sleep)
participant CPU_R as CPU
participant OS as Linux OS
R->>OS: sleep(0.033) 호출
OS->>CPU_R: CPU 반납, 다른 콜백 처리
Note over CPU_R: joint_states 콜백 등 실행
OS-->>R: 33ms 후 깨움// Arduino: delay() 사용 — CPU가 완전히 멈춤
void loop() {
digitalWrite(LED_PIN, HIGH); // LED 켜기
delay(1000); // 1초 대기 (이 동안 아무것도 못함)
digitalWrite(LED_PIN, LOW); // LED 끄기
delay(1000); // 1초 대기
// delay 중에는 센서 읽기, 통신 등 모두 불가!
}import rclpy
from rclpy.node import Node
import time
class ControlNode(Node):
def __init__(self):
super().__init__('control_node')
# 30Hz 제어 루프용 타이머
self.timer = self.create_timer(1/30, self.control_loop)
# joint_states 구독 — sleep 중에도 콜백 동작
self.sub = self.create_subscription(
JointState, '/joint_states', self.joint_callback, 10
)
def control_loop(self):
# 제어 명령 전송
self.publish_command()
# sleep 중에도 joint_callback은 계속 호출됨
def joint_callback(self, msg):
# sleep과 무관하게 새 데이터가 들어올 때마다 실행
self.current_joints = msg.position실전 팁: ROS2에서 루프를 만들 때
while + sleep패턴보다create_timer()를 사용하는 것이 더 안전합니다. 타이머는 executor가 관리하므로 콜백 충돌 위험이 줄어듭니다.
Hz와 ms — 주파수와 주기의 관계
- Hz (헤르츠): 1초에 몇 번 실행하는가 → 주파수
- ms (밀리초): 한 번 실행하는 데 몇 밀리초가 걸리는가 → 주기
Hz와 ms는 같은 것을 반대 방향에서 표현한 것입니다. 역수 관계!
| Hz | ms | 의미 | 사용 예시 |
|---|---|---|---|
| 1 Hz | 1000 ms | 1초에 1번 | 상태 모니터링 |
| 10 Hz | 100 ms | 1초에 10번 | 센서 폴링 |
| 30 Hz | 33.3 ms | 1초에 30번 | 로봇 제어 루프 |
| 50 Hz | 20 ms | 1초에 50번 | 서보 모터 PWM |
| 100 Hz | 10 ms | 1초에 100번 | IMU 데이터 수집 |
| 1000 Hz | 1 ms | 1초에 1000번 | 고속 제어 시스템 |
# Hz → 초 변환하여 sleep에 사용
rate_hz = 30
sleep_sec = 1 / rate_hz # 0.0333초
sleep_ms = 1000 / rate_hz # 33.3ms
time.sleep(1/30) # = sleep(0.0333초) = sleep(33.3ms) = 30Hz 루프# ROS2에서 Rate 객체를 사용하는 방법
import rclpy
rclpy.init()
node = rclpy.create_node('example')
rate = node.create_rate(30) # 30Hz Rate 생성
while rclpy.ok():
# 제어 로직 실행
do_something()
rate.sleep() # 남은 시간만큼만 대기하여 정확한 30Hz 유지주의:
time.sleep(1/30)은 실행 시간을 고려하지 않기 때문에 실제로는 30Hz보다 느려집니다. ROS2의Rate.sleep()은 실행 시간을 빼고 남은 시간만 대기하므로 더 정확합니다.
flowchart LR
Hz["주파수 (Hz)<br/>1초에 몇 번?"]
ms["주기 (ms)<br/>한 번에 몇 ms?"]
Hz -- "ms = 1000 / Hz" --> ms
ms -- "Hz = 1000 / ms" --> Hz
subgraph 예시: 30Hz 제어 루프
H30["30 Hz"] --- M33["33.3 ms"]
endArduino vs ROS2 — 왜 다를 수밖에 없는가
두 환경의 차이는 운영체제의 유무에서 비롯됩니다.
flowchart TB
subgraph Arduino ["Arduino (베어메탈)"]
direction TB
A1[사용자 코드] --> A2[하드웨어 직접 제어]
A2 --> A3["delay() = CPU 정지"]
style A3 fill:#f96,stroke:#333
end
subgraph ROS2 ["ROS2 (Linux 위)"]
direction TB
R1[ROS2 노드] --> R2[Linux 커널]
R2 --> R3[스케줄러]
R3 --> R4["sleep() = 다른 프로세스 실행"]
R3 --> R5[콜백 처리]
R3 --> R6[토픽 수신]
style R4 fill:#6c6,stroke:#333
end| 항목 | Arduino | ROS2 (Linux) |
|---|---|---|
| OS | 없음 | Linux |
| 스케줄러 | 없음 (단일 루프) | 있음 (선점형 멀티태스킹) |
| 대기 방식 | 바쁜 대기 (busy wait) | 커널 타이머 |
| 적합한 작업 | 단순 센서/액추에이터 | 복잡한 로봇 시스템 |
정리
| 개념 | 핵심 |
|---|---|
delay() | CPU 독점, 모든 것 멈춤 (Arduino) |
sleep() | CPU 양보, 다른 작업 계속 가능 (ROS2) |
| Hz | 1초당 실행 횟수 (주파수) |
| ms | 1회 실행 소요 시간 (주기) |
| 변환 공식 |
- Arduino의
delay()와 ROS2의sleep()은 "대기"라는 목적은 같지만, 블로킹 여부가 완전히 다릅니다 - 제어 루프 설계 시 목표 Hz를 먼저 정하고, 이를 ms로 변환하여 타이머/sleep에 적용하세요
- ROS2에서는
time.sleep()대신 **create_timer()또는Rate.sleep()**을 사용하는 것이 권장됩니다