로봇 · 기본
좌표·센서·구동기의 기초
구동기 기초 - DC 모터·PWM·서보
토크·속도 곡선, PWM 듀티비, 모터 모델 시뮬레이션
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서는 엔코더나 IMU 값에 섞인 잡음을 이동평균과 저역통과 필터로 줄이는 방법을 다뤘다. 이번 장은 반대 방향이다. 로봇이 판단한 값을 실제 바퀴 움직임으로 바꾸는 쪽, 즉 DC 모터와 서보모터를 다룬다. 모터는 전압을 주면 그대로 원하는 속도가 나오는 부품이 아니다. 토크와 속도는 서로를 제한하고, PWM 신호의 듀티비는 전압 그 자체가 아니라 평균 전압을 의미한다. 이 관계를 모르면 "듀티비를 절반으로 줄였는데 로봇이 안 움직인다" 같은 문제를 만났을 때 원인을 찾기 어렵다.
- DC 모터의 토크-속도 곡선을 읽고 동작점을 구할 수 있다
- PWM 듀티비와 평균 전압의 관계를 설명할 수 있다
- 부하가 걸렸을 때 모터가 멈추는 조건을 계산할 수 있다
- 서보모터의 펄스 폭 제어가 DC 모터의 PWM 구동과 다른 이유를 설명할 수 있다
- 간단한 모터 모델을 오일러 적분으로 시뮬레이션할 수 있다
문제 상황
2바퀴 차동 구동 로봇을 만들면서 왼쪽 바퀴 모터에 듀티비 20%를 줬다. 데이터시트의 무부하 속도를 기준으로 계산하면 분명히 천천히라도 돌아야 하는데, 바퀴는 꿈쩍도 하지 않는다. 듀티비를 80%까지 올리고 나서야 겨우 움직였다. 반대로 서보모터 쪽에서는 DC 모터를 제어하던 코드를 그대로 재사용해 듀티비 값을 넘겼더니 서보 축이 부르르 떨기만 하고 원하는 각도로 가지 않았다.
두 문제 모두 원인은 같다. PWM 듀티비, 모터 토크, 부하 사이의 관계를 숫자로 다루지 않고 "듀티비가 크면 빠르게 돈다" 정도로만 이해했기 때문이다. 이 장에서는 이 관계를 수식과 코드로 정리해서, 로봇이 왜 안 움직이는지, 얼마나 듀티비를 줘야 움직이는지를 미리 계산할 수 있게 한다.
DC 모터의 토크-속도 곡선
DC 모터는 전압을 걸면 전류가 흐르고, 전류는 자기장 속에서 토크를 만든다. 동시에 모터가 회전하면 역기전력(back-EMF)이 발생해 전류를 줄인다. 이 두 효과를 합치면 모터가 낼 수 있는 토크 T는 공급 전압 V와 현재 각속도 ω에 대해 다음과 같은 직선 관계를 가진다.
T = (Kt / R) * V - (Kt^2 / R) * ω
Kt는 토크 상수, R은 전기자 저항이다. 이 식은 ω=0일 때 가장 큰 토크(정지 토크, stall torque)를 내고, 토크가 0이 되는 속도(무부하 속도, no-load speed)에서 최대 속도를 낸다는 뜻이다. 즉 모터는 토크와 속도를 동시에 최대로 낼 수 없고, 하나를 늘리면 하나는 줄어든다.
로봇 바퀴에는 저항하는 부하 토크가 항상 걸린다. 바닥과의 마찰, 로봇 무게, 경사 등이 모두 부하다. 모터가 실제로 도달하는 속도는 모터의 토크-속도 곡선과 부하가 요구하는 토크가 같아지는 지점, 즉 동작점(operating point)이다. 부하가 커질수록 동작점은 곡선을 따라 왼쪽 위로, 즉 더 낮은 속도 쪽으로 이동한다.
부하가 정지 토크보다 크면 모터는 아예 움직이지 못한다. 문제 상황에서 듀티비 20%로 바퀴가 안 돌았던 이유도 여기에 있다. 그 전압으로 낼 수 있는 최대 토크가 바퀴를 돌리는 데 필요한 최소 토크보다 작았던 것이다.
PWM 듀티비와 평균 전압
PWM(pulse width modulation)은 전압을 켰다 껐다 빠르게 반복해서, 평균적으로 원하는 전압을 낸 것과 같은 효과를 낸다. 한 주기 안에서 켜져 있는 비율을 듀티비(duty ratio)라고 부르며, 공급 전압이 Vcc일 때 평균 전압은 다음과 같다.
평균 전압 = 듀티비 * Vcc
모터가 이 평균 전압에 반응하는 이유는 모터의 회전 관성과 전기적 시정수가 PWM 스위칭 주기보다 훨씬 느리기 때문이다. 앞 장에서 다룬 저역통과 필터와 같은 원리로, 모터 자체가 빠른 On/Off 신호를 걸러내고 평균값에만 반응하는 저역통과 필터 역할을 한다. 그래서 스위칭 주파수가 충분히 높으면(보통 수 kHz 이상) 모터는 듀티비에 비례하는 부드러운 전압을 받은 것처럼 움직인다.
반대로 스위칭 주파수가 너무 낮으면 모터의 관성이 한 주기 동안의 전압 변화를 다 걸러내지 못한다. 이 경우 모터 코일이 켜질 때마다 미세하게 진동하면서 가청 대역의 소음을 낸다. PWM 주파수를 정할 때 "얼마나 빨라야 하는가"는 결국 모터가 평균 전압만 보게 만들 수 있는지의 문제다.
서보모터: 펄스 폭으로 각도를 지정한다
서보모터는 DC 모터와 신호를 해석하는 방식이 다르다. 듀티비가 아니라 한 주기(보통 20ms, 50Hz) 안에서 펄스가 켜져 있는 시간, 즉 펄스 폭(pulse width)이 목표 각도를 의미한다. 흔히 쓰이는 규격은 펄스 폭 1.0ms가 한쪽 끝, 2.0ms가 반대쪽 끝에 대응하고 그 사이를 선형으로 나눈다.
목표 각도 = (펄스 폭 - 1.0ms) / (2.0ms - 1.0ms) * 최대 각도
서보 내부에는 자체 되먹임 회로(대개 포텐쇼미터)와 제어기가 들어 있어서, 펄스 폭으로 지정한 각도에 도달할 때까지 알아서 모터를 구동한다. 즉 서보는 속도가 아니라 위치를 지정하는 장치다. DC 모터용 PWM 구동 코드를 그대로 서보에 적용하면, 듀티비를 펄스 폭으로 잘못 해석해 서보가 엉뚱한 각도를 목표로 삼거나 신호 주기가 맞지 않아 떨리는 문제가 생긴다.
| 이름 | 기호 | 값 | 의미 |
|---|---|---|---|
| 토크 상수 | Kt | 0.02 N·m/A | 전류를 토크로 바꾸는 비율 |
| 전기자 저항 | R | 2.0 Ω | 모터 코일의 전기 저항 |
| 회전 관성 | J | 0.0004 kg·m² | 회전자와 바퀴를 합친 관성 |
| 점성 마찰 | B | 0.00005 N·m·s/rad | 속도에 비례하는 저항 토크 |
완성 코드
motor_pwm_sim.py
"""DC 모터 토크-속도 곡선과 PWM 구동을 시뮬레이션한다."""
import numpy as np
# 모터 파라미터 (가상의 소형 DC 기어드 모터)
KT = 0.02 # 토크 상수 [N*m/A]
R = 2.0 # 전기자 저항 [ohm]
J = 0.0004 # 회전자 + 바퀴 환산 관성 [kg*m^2]
B = 0.00005 # 점성 마찰 계수 [N*m*s/rad]
VCC = 6.0 # 공급 전압 [V]
K1 = KT / R # 전압을 토크로 바꾸는 이득
K2 = KT * KT / R # 역기전력에 의한 속도 감쇠
DT = 0.001 # 적분 시간 간격 [s]
T_END = 20.0 # 시뮬레이션 길이 [s]
def motor_torque(duty, omega, vcc=VCC):
"""PWM 듀티비와 현재 각속도로 모터 토크를 계산한다."""
voltage = duty * vcc
return K1 * voltage - K2 * omega
def simulate(duty, load_torque, dt=DT, t_end=T_END):
"""오일러 적분으로 각속도 시계열을 구한다."""
n = int(t_end / dt)
omega = np.zeros(n)
for i in range(1, n):
torque = motor_torque(duty, omega[i - 1]) - load_torque - B * omega[i - 1]
domega = torque / J * dt
omega[i] = max(0.0, omega[i - 1] + domega)
return omega
def rise_time_90(omega, dt=DT):
"""정상상태 값의 90%에 처음 도달하는 시각을 구한다."""
steady = omega[-1]
if steady <= 0:
return None
target = 0.9 * steady
idx = np.argmax(omega >= target)
if omega[idx] < target:
return None
return idx * dt
def pulse_to_angle(pulse_ms, min_ms=1.0, max_ms=2.0, max_deg=180.0):
"""서보 펄스 폭[ms]을 목표 각도[deg]로 변환한다."""
ratio = (pulse_ms - min_ms) / (max_ms - min_ms)
ratio = min(1.0, max(0.0, ratio))
return ratio * max_deg
def main():
print("=== 듀티비별 정상상태 속도 (무부하) ===")
for duty in [0.2, 0.5, 0.8, 1.0]:
omega = simulate(duty, load_torque=0.0)
t_rise = rise_time_90(omega)
print(f"duty={duty:.1f} steady={omega[-1]:7.2f} rad/s t90={t_rise:.1f} s")
print("\n=== 부하 토크 0.02 N*m 적용 ===")
for duty in [0.2, 0.5, 0.8, 1.0]:
omega = simulate(duty, load_torque=0.02)
t_rise = rise_time_90(omega)
rise_str = f"{t_rise:.1f} s" if t_rise is not None else "정지(토크 부족)"
print(f"duty={duty:.1f} steady={omega[-1]:7.2f} rad/s t90={rise_str}")
print("\n=== 토크-속도 곡선 표본 (V=6.0V) ===")
for w in [0, 60, 120, 180, 240]:
torque = motor_torque(1.0, w)
print(f"omega={w:4d} rad/s torque={torque:6.4f} N*m")
print("\n=== 서보 펄스 폭 → 각도 ===")
for pulse in [1.0, 1.25, 1.5, 1.75, 2.0]:
angle = pulse_to_angle(pulse)
print(f"pulse={pulse:.2f} ms angle={angle:6.1f} deg")
if __name__ == "__main__":
main()
줄별 해설
KT, R, J, B, VCC는 가상의 소형 기어드 DC 모터 값이다. K1은 전압 1V가 만드는 토크, K2는 속도 1rad/s가 역기전력으로 깎아먹는 토크를 나타낸다. 이 두 값이 motor_torque의 직선 관계 T = K1*V - K2*ω를 이룬다.
motor_torque는 듀티비를 평균 전압(duty * vcc)으로 바꾼 뒤 토크-속도 직선에 대입한다. 이 함수 하나가 앞서 설명한 "PWM 듀티비 → 평균 전압 → 토크"의 흐름을 그대로 코드로 옮긴 것이다.
simulate는 뉴턴의 회전 운동 방정식 J*dω/dt = T_motor - T_load - B*ω를 오일러 적분으로 한 스텝씩 계산한다. max(0.0, ...)로 감싼 이유는 부하 토크가 모터 토크보다 크면 각속도가 음수로 내려가려 하는데, 바퀴가 거꾸로 돌지 않고 그 자리에 멈춰 있어야 하기 때문이다.
rise_time_90은 배열 마지막 값을 정상상태로 보고, 그 90%에 처음 도달한 인덱스를 찾아 시간으로 바꾼다. 정상상태가 0 이하면(모터가 못 움직이면) None을 돌려준다.
pulse_to_angle은 서보 펄스 폭을 각도로 바꾸는 선형 변환이다. min/max로 비율을 0~1로 잘라서, 규격을 벗어난 펄스 폭이 들어와도 범위 밖 각도가 나오지 않게 한다.
main은 무부하/부하 상태에서 듀티비별 정상상태 속도와 도달 시간을 출력하고, 토크-속도 직선의 표본값과 서보 펄스-각도 변환표를 이어서 출력한다.
실행 결과
$ python3 motor_pwm_sim.py
=== 듀티비별 정상상태 속도 (무부하) ===
duty=0.2 steady= 48.00 rad/s t90=3.7 s
duty=0.5 steady= 120.00 rad/s t90=3.7 s
duty=0.8 steady= 192.00 rad/s t90=3.7 s
duty=1.0 steady= 240.00 rad/s t90=3.7 s
=== 부하 토크 0.02 N*m 적용 ===
duty=0.2 steady= 0.00 rad/s t90=정지(토크 부족)
duty=0.5 steady= 40.00 rad/s t90=3.7 s
duty=0.8 steady= 112.00 rad/s t90=3.7 s
duty=1.0 steady= 160.00 rad/s t90=3.7 s
=== 토크-속도 곡선 표본 (V=6.0V) ===
omega= 0 rad/s torque=0.0600 N*m
omega= 60 rad/s torque=0.0480 N*m
omega= 120 rad/s torque=0.0360 N*m
omega= 180 rad/s torque=0.0240 N*m
omega= 240 rad/s torque=0.0120 N*m
=== 서보 펄스 폭 → 각도 ===
pulse=1.00 ms angle= 0.0 deg
pulse=1.25 ms angle= 45.0 deg
pulse=1.50 ms angle= 90.0 deg
pulse=1.75 ms angle= 135.0 deg
pulse=2.00 ms angle= 180.0 deg
무부하 표와 부하 표를 비교하면 흥미로운 점이 보인다. 도달 시간(t90)은 부하가 있든 없든, 듀티비가 얼마든 항상 3.7초로 같다. 시정수는 관성과 마찰로만 정해지고 목표 속도의 크기와는 무관하기 때문이다. 반면 duty=0.2에 부하를 걸었을 때는 모터가 아예 움직이지 않는데, 이는 그 듀티비가 낼 수 있는 토크(0.012 N·m)가 부하 토크(0.02 N·m)보다 작기 때문이다.
실무에서 자주 틀리는 것
듀티비와 속도를 무조건 비례로 가정한다
부하나 정지 마찰을 무시하고 듀티비만으로 속도를 계산하면 저속 구간에서 크게 틀린다.
# 틀린 코드
target_speed = duty * MAX_SPEED
# 고친 코드
available_torque = K1 * duty * VCC
if available_torque <= load_torque:
target_speed = 0.0
else:
target_speed = (available_torque - load_torque) / (K2 + B)
PWM 주파수를 너무 낮게 잡는다
스위칭 주파수가 낮으면 모터가 평균 전압이 아니라 On/Off 자체에 반응해 떨림과 소음이 생긴다.
# 틀린 코드
set_pwm_frequency(50) # 가청 주파수대, 코일이 진동하며 소음 발생
# 고친 코드
set_pwm_frequency(20000) # 가청 대역보다 높아 모터가 평균 전압만 본다
서보모터를 듀티비 API로 제어한다
서보는 평균 전압이 아니라 펄스 폭 자체를 각도 지령으로 읽는다. 듀티비를 넘기면 펄스 폭이 의도와 다르게 해석된다.
# 틀린 코드
servo.set_duty(0.5) # 서보는 듀티비를 모른다
# 고친 코드
servo.set_pulse_width_ms(1.5) # 1.5ms는 가동 범위의 중앙 각도
무부하 속도만 보고 모터를 고른다
데이터시트의 무부하 속도는 부하가 0일 때의 값이다. 실제 동작점의 토크 여유를 확인하지 않으면 부하 아래에서 멈추는 모터를 고르게 된다.
# 틀린 코드
choose_motor(no_load_speed=300) # 부하 조건을 안 봄
# 고친 코드
choose_motor(no_load_speed=300, stall_torque=0.08,
required_load_torque=0.05) # 정지 토크가 부하보다 충분히 큰지 확인
한눈에 보기
| 개념 | 핵심 관계식 | 의미 |
|---|---|---|
| 토크-속도 곡선 | T = K1·V - K2·ω | 토크와 속도는 반비례 직선 관계 |
| 동작점 | 모터 곡선 ∩ 부하선 | 부하가 크면 동작점 속도가 낮아진다 |
| PWM 평균 전압 | V_avg = duty × Vcc | 모터는 스위칭이 아니라 평균 전압에 반응 |
| 서보 각도 매핑 | angle = (pulse-1.0)/(2.0-1.0)×180 | 서보는 펄스 폭으로 위치를 지정 |
연습 문제
- 공급 전압 12V, K1=0.02, K2=0.0004인 모터가 있다. 점성 마찰을 무시할 때(B=0) 정지 토크와 무부하 속도를 구하라.
- 이 장 코드의 상수를 그대로 쓸 때, duty=0.3, 부하 토크=0.05 N·m인 상황에서 모터가 움직이는지 판단하고 근거를 계산으로 보여라.
- PWM 주파수를 200Hz로 낮췄더니 모터에서 미세한 진동음이 들렸다. 원인과 해결 방향을 서술하라.
- 서보모터의 펄스 폭이 1.6ms일 때(범위 1.0~2.0ms, 최대 각도 180도) 목표 각도를 구하라.
정답과 해설
- 정지 토크는 ω=0일 때 값이므로 T = K1·V = 0.02 × 12 = 0.24 N·m다. 무부하 속도는 B=0이므로 토크가 0이 되는 지점, ω = K1·V / K2 = 0.24 / 0.0004 = 600 rad/s다.
- duty=0.3에서 낼 수 있는 토크는 K1 × duty × VCC = 0.01 × 0.3 × 6 = 0.018 N·m다. 이는 부하 토크 0.05 N·m보다 작으므로 모터는 움직이지 못하고 그 자리에 멈춘다.
- 모터의 회전 관성은 수백 Hz 이하의 스위칭 신호를 완전히 걸러내지 못한다. 이 경우 모터 코일이 On/Off 주기에 맞춰 실제로 진동하며 가청 대역의 소음을 낸다. 해결책은 PWM 주파수를 모터가 평균 전압만 보게 되는 수 kHz~수십 kHz 이상으로 올리는 것이다.
- ratio = (1.6-1.0)/(2.0-1.0) = 0.6, 각도 = 0.6 × 180 = 108도다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.