TIKI-MINI로 시작하는 ROS2 실무
목차

2. 시뮬레이션

2.6cmd_vel과 차동구동

1.3장에서 /cmd_vel로 Twist를 발행해 로봇을 움직여 봤습니다. 이 장은 그 숫자 두 개가 바퀴 두 개의 속도로 바뀌는 과정을 계산으로 따라갑니다. 근거는 TIKI-MINI 패키지(tiki_control, tiki_base, tiki_description)의 실제 값과 코드입니다.

Twist에서 쓰는 필드는 두 개

geometry_msgs/msg/Twist에는 linear(x, y, z)와 angular(x, y, z) 여섯 값이 있습니다. 바닥 위를 구르는 바퀴 두 개짜리 로봇은 옆으로 미끄러지거나 뜰 수 없으므로 두 값만 의미가 있습니다.

필드의미단위
linear.x로봇 앞(+)·뒤(-) 속도m/s
angular.z위에서 볼 때 반시계(+) 회전 속도rad/s

나머지 네 필드(linear.y, linear.z, angular.x, angular.y)는 차동구동 컨트롤러가 읽지 않습니다. 값을 넣어도 무시됩니다. 좌표 방향은 1.9장의 REP-103 규칙(x 앞, y 왼쪽, z 위)을 따르고, 속도는 로봇 몸체 기준입니다.

운동학: 바퀴 속도에서 로봇 속도로

바퀴 반지름을 r, 좌우 바퀴 사이 거리를 L, 바퀴 각속도를 wL, wR(rad/s)이라 하겠습니다. 바퀴가 미끄러지지 않고 구르면 접지점 속도, 즉 바퀴 선속도는 이렇습니다.

vL = r * wL        vR = r * wR

로봇의 앞쪽 속도는 두 바퀴의 평균입니다. 한쪽이 더 빠르면 빠른 쪽이 바깥이 되어 로봇이 돌고, 회전 속도는 속도 차이를 바퀴 간격으로 나눈 값입니다. 두 바퀴가 같은 속도로 반대 방향으로 돌면 평균은 0이므로 제자리에서 돕니다.

v = (vR + vL) / 2 = r (wR + wL) / 2
ω = (vR - vL) / L = r (wR - wL) / L

명령을 바퀴로 바꾸려면 위 두 식을 vR, vL에 대해 풉니다. 더하면 vR + vL = 2v, 빼면 vR - vL = ω L이므로 다음과 같습니다.

vR = v + ω L / 2        wR = (v + ω L / 2) / r
vL = v - ω L / 2        wL = (v - ω L / 2) / r

ros2_controllers의 diff_drive_controller는 humble 브랜치 소스(diff_drive_controller.cpp)에서 같은 식을 씁니다. 왼쪽은 (linear - angular * wheel_separation / 2.0) / wheel_radius, 오른쪽은 부호만 반대입니다.

TIKI-MINI 숫자로 계산하기

상수값출처
바퀴 반지름 r0.0325 mtiki_controllers.yaml wheel_radius (URDF도 같은 값)
바퀴 간격 L0.1924 mtiki_controllers.yaml wheel_separation (URDF 조인트 y = ±0.0962의 차)
엔코더1170 틱/회전diffdrive_tiki.cpp의 rad_per_tick = 2π/1170
제어 주기50 Hztiki_controllers.yaml update_rate

L/2 = 0.0962 m이고, 1 rad/s는 1170 / 2π ≈ 186.2 틱/초입니다.

직진 최고 속도, v = 0.2, ω = 0:

wL = wR = 0.2 / 0.0325 ≈ 6.154 rad/s
틱/초 = 6.154 * 186.2 ≈ 1146        (제어 주기 한 번, 20 ms에 약 23틱)

제자리 회전 최고 속도, v = 0, ω = 0.85:

바퀴 선속도 = ±0.85 * 0.0962 ≈ ±0.0818 m/s
wR = +2.516 rad/s, wL = -2.516 rad/s
틱/초 = ±2.516 * 186.2 ≈ ±468.5
한 바퀴 도는 시간 = 2π / 0.85 ≈ 7.4 초

두 한계를 동시에 쓰면, v = 0.2, ω = 0.85:

wL = (0.2 - 0.0818) / 0.0325 ≈ 3.638 rad/s  (약 677 틱/초)
wR = (0.2 + 0.0818) / 0.0325 ≈ 8.670 rad/s  (약 1614 틱/초)
회전 반지름 = v / ω = 0.2 / 0.85 ≈ 0.235 m

회전 반지름 v/ω는 직선 속도가 0이 아닐 때만 정의됩니다. 완만한 호는 v = 0.1, ω = 0.2로 반지름 0.5 m입니다. 이때 바깥쪽(오른쪽) 바퀴는 (0.1 + 0.01924) / 0.0325 ≈ 3.669, 안쪽은 ≈ 2.485 rad/s입니다.

직접 바꿔 보기

아래 계산기에서 linear.x와 angular.z를 움직여 보세요. 슬라이더 범위는 일부러 제한값(0.2, 0.85)보다 넓습니다. 한계를 넘으면 제한 후 값, 좌우 바퀴 속도, 틱/초, 회전 반지름이 함께 바뀌고, 4초 동안의 경로에 제한 전(점선)과 제한 후(실선)가 같이 그려집니다.

위에서 본 4초 경로 (1 m = 90px)실선 = 제한 후, 점선 = 제한 전 요청
/cmd_vel 요청 (Twist)
항목왼쪽 바퀴오른쪽 바퀴
각속도 (rad/s)2.846.39
바퀴 선속도 (m/s)0.0920.208
엔코더 틱/초5291190

제한 후 명령: v = 0.15 m/s, w = 0.60 rad/s
회전 반지름 v/w = 0.25 m

바퀴 각속도가 URDF의 ros2_control 한계 ±5 rad/s를 넘습니다. 플러그인은 이 값으로 제한하지 않습니다(본문 참고).

상수값출처
바퀴 반지름 r0.0325 mtiki_control/config/tiki_controllers.yaml wheel_radius
바퀴 간격 L0.1924 mtiki_controllers.yaml wheel_separation
linear.x 한계±0.2 m/stiki_controllers.yaml linear.x.max_velocity
angular.z 한계±0.85 rad/stiki_controllers.yaml angular.z.max_velocity
엔코더1170 틱/회전tiki_base/src/diffdrive_tiki.cpp rad_per_tick = 2π/1170
URDF 바퀴 명령 한계±5 rad/stiki_description/urdf/tiki.urdf.xacro ros2_control

시험해 볼 것은 세 가지입니다. linear.x = 0.4, angular.z = 0.4로 놓고 제한 전과 후의 경로를 비교해 보세요. 반지름이 1 m에서 0.5 m로 줄어듭니다. 두 값을 따로 자르기 때문입니다(아래 "제한은 어디서 걸리나" 참고). 다음으로 linear.x = 0에서 angular.z를 올려 보세요. 로봇이 제자리에서 돌고 두 바퀴 속도의 부호가 반대가 됩니다. 마지막으로 0.2와 0.85를 함께 주면 오른쪽 바퀴가 8.67 rad/s로 가장 빠르게 돕니다.

/cmd_vel이 모터에 닿기까지

제품에서 명령이 지나가는 길은 다음 순서입니다. 앞의 세 단계는 소프트웨어, 뒤는 하드웨어입니다.

단계하는 일근거
1. /cmd_velTwist를 받는다. 컨트롤러의 ~/cmd_vel_unstamped를 /cmd_vel로 리매핑한 것이다tiki_control.launch.py remappings
2. diff_controller제한을 적용하고 위의 역기구학으로 좌우 바퀴 각속도를 계산한다tiki_controllers.yaml, diff_drive_controller.cpp
3. DiffDriveTiki::write()바퀴 각속도(rad/s)를 틱/초 정수로 바꿔 시리얼로 보낸다diffdrive_tiki.cpp
4. 시리얼AT+M=<좌>,<우> 명령 한 줄writeMotorSpeed()
5. 모터 컨트롤러엔코더를 보며 PID로 바퀴를 돌린다AT+MODE=2, AT+PID=...

write()의 핵심은 두 줄입니다.

double rad_per_tick = (2 * M_PI) / 1170;
const int left_value_target = static_cast<int>(left_wheel_cmd / rad_per_tick);

0.2 m/s에서 6.154 / (2π/1170) = 1145.9이고 static_cast<int>는 소수점 이하를 버리므로 모터 컨트롤러에는 AT+M=1145,1145가 갑니다. 모터 컨트롤러 펌웨어는 reference에 없으므로 이 정수를 모터 컨트롤러 안에서 틱/초로 해석하는지는 패키지 코드에서 확인할 수 없습니다.

돌아오는 길도 있습니다. read()는 AT+EL?과 AT+ER?로 엔코더 값을 읽어 rad_per_tick을 곱해 바퀴 위치로 삼고, 직전 위치와의 차를 주기로 나눠 바퀴 속도로 씁니다. joint_state_broadcaster가 이 값을 /joint_states로 내보내고, diff_controller는 같은 값으로 오도메트리를 적분해 /odom과 TF odom → base_footprint를 발행합니다. 두 발행 모두 publish_rate 50 Hz입니다.

제한은 어디서 걸리나

tiki_controllers.yaml은 linear.x를 ±0.2 m/s, angular.z를 ±0.85 rad/s로 제한하고 가속도와 저크 제한은 꺼 두었습니다(has_acceleration_limits: false). 제한은 두 값을 독립적으로 자릅니다. 결과는 두 가지입니다.

  • 0.4 m/s를 요청하면 0.2 m/s로 줄어듭니다. 한 번에 속도가 바뀌고 가속 구간은 컨트롤러에 없습니다. 바퀴가 부드럽게 가속하는 것은 모터 컨트롤러의 PID 몫입니다.
  • v와 ω를 같은 비율로 줄이지 않으므로 호의 모양이 바뀝니다. v = 0.4, ω = 0.4는 반지름 1 m를 요청한 것이지만 v만 잘려 실제 반지름은 0.5 m가 됩니다.

해보기 1: 완만한 호

이제 /cmd_vel을 발행해 봅니다. 노드가 v = 0.1, ω = 0.2를 0.1초마다 발행하고, 시작할 때 바퀴 속도를 직접 계산해 로그로 남깁니다. 위에서 손으로 구한 2.485와 3.669가 나오는지 확인하세요.

LIVE

1) 반지름 0.5 m 호 그리기

실행한 뒤 결과 화면의 3D (URDF) 탭을 누르면 실제 URDF로 그린 TIKI-MINI가 호를 그리며 달립니다. 바깥쪽 바퀴가 안쪽 바퀴보다 빨리 도는 것을 확인하세요.

직접 바꿔 보기

  • self.w를 -0.2로 바꾸면 반대 방향으로 돕니다. 왼쪽과 오른쪽 바퀴 속도의 로그가 어떻게 바뀔까요?
  • 반지름 0.25 m 호를 그리려면 v와 w를 어떻게 정할까요? v / w가 0.25가 되는 쌍이면 됩니다. 로그의 바퀴 속도로 계산을 확인해 보세요.
  • self.v = 0.0으로 바꾸면 제자리 회전이 되고 로그의 반지름은 0이 됩니다.

해보기 2: 제자리 반 바퀴

두 번째 노드는 일부러 ω = 1.5를 요청하고, 컨트롤러가 하듯 ±0.85로 자른 뒤 반 바퀴에 걸리는 시간만큼 발행합니다. 시간은 π / 0.85 ≈ 3.70초입니다.

LIVE

2) 제한을 걸고 제자리 반 바퀴

직접 바꿔 보기

  • 코드의 clamp(...) 줄을 지우고 self.w = self.w_wanted로 바꿔 보세요. 에뮬레이터는 TIKI-MINI의 한계를 모르므로 1.5 rad/s 그대로 더 빠르게 돕니다. 실제 TIKI-MINI에서는 컨트롤러가 0.85로 줄입니다.
  • 한 바퀴(2π)를 돌려면 turn_time을 어떻게 바꿔야 할까요?

실제 로봇에서 확인하기

다음 명령은 TIKI-MINI 실기기 또는 같은 패키지를 실행 중인 환경에서 씁니다. 바퀴가 실제로 돌므로 로봇을 들어 올리거나 넓은 바닥에 놓고, 바로 멈출 수 있는 상태에서만 실행하세요.

# 실기기(TIKI-MINI): 로봇이 움직입니다
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}, angular: {z: 0.0}}"

# 다른 터미널에서 오도메트리와 컨트롤러 상태 확인
ros2 topic echo /odom --field twist.twist
ros2 control list_controllers

-r 10을 빼고 --once로 한 번만 보내면 타임아웃이 마지막 명령을 0으로 만들므로 로봇은 0.5초 안에 멈춥니다. 멈출 때는 Ctrl+C로 발행을 끊으면 0.5초 뒤에 정지하고, 바로 멈추려면 0을 보냅니다.

한눈에 정리

  1. 차동구동 컨트롤러는 Twist의 linear.x와 angular.z만 쓰고 나머지 필드는 무시한다.
  2. v = r(wR + wL)/2, ω = r(wR − wL)/L이고, 역으로 wR,L = (v ± ω L/2) / r이다.
  3. TIKI-MINI는 r = 0.0325 m, L = 0.1924 m이므로 0.2 m/s 직진은 바퀴 6.154 rad/s, 약 1146 틱/초다.
  4. 경로는 /cmd_vel → diff_controller(제한, 역기구학) → DiffDriveTiki::write() → AT+M=좌,우 → 모터 컨트롤러다. 되돌아오는 길은 AT+EL?/AT+ER? → /joint_states → /odom과 TF다.
  5. 제한은 linear.x ±0.2, angular.z ±0.85를 따로 자르므로 궤적 반지름이 바뀔 수 있다.
  6. cmd_vel_timeout 기본 0.5초라서 명령은 10 Hz 정도로 계속 보내야 한다.

확인 문제

Q1. v = 0.1 m/s, ω = 0.5 rad/s를 요청했습니다. 왼쪽과 오른쪽 중 어느 바퀴가 더 빠르고, 회전 반지름은 얼마인가요?

오른쪽 바퀴가 더 빠릅니다. ω가 양수(반시계)이면 오른쪽이 바깥이 되기 때문입니다. 오른쪽은 (0.1 + 0.5 × 0.0962) / 0.0325 ≈ 4.56 rad/s, 왼쪽은 (0.1 − 0.0481) / 0.0325 ≈ 1.60 rad/s입니다. 반지름은 v / ω = 0.1 / 0.5 = 0.2 m입니다.

Q2. linear.x = 0.6, angular.z = 0.3을 계속 발행했습니다. 요청한 반지름과 TIKI-MINI가 실제로 그리는 반지름은?

요청 반지름은 0.6 / 0.3 = 2 m입니다. 컨트롤러는 linear.x를 0.2로 자르고 angular.z 0.3은 한계 안이라 그대로 두므로 실제 반지름은 0.2 / 0.3 ≈ 0.67 m입니다. 두 값을 따로 제한하기 때문에 호가 더 급해집니다.

Q3. ros2 topic pub --once로 속도를 한 번 보냈더니 로봇이 잠깐 움직이다 멈췄습니다. 이유는?

diff_drive_controller의 cmd_vel_timeout 기본값이 0.5초이고 TIKI-MINI 설정이 바꾸지 않았기 때문입니다. 마지막 명령이 0.5초 넘게 갱신되지 않으면 컨트롤러가 명령을 0으로 만듭니다. 계속 움직이려면 -r 10처럼 주기적으로 발행합니다.

다음 장

2.7장에서는 이 계산을 바탕으로 한 변 길이를 정하고 Twist를 직접 발행해 정사각형을 그리며 달립니다. 직진 시간과 제자리 회전 시간을 속도에서 거꾸로 구하는 데 이 장의 식을 씁니다.