ROS2 드라이버 소스에서 구분하는 주행 모드는 3개입니다.
- MODE_FOUR_DIFF : 4륜 차동 구동
- MODE_ACKERMANN : 스티어링/Ackermann 구동
- MODE_MCNAMU : 메카넘 휠 구동
- 무한궤도(Track)는 별도의 모드가 아니라
MODE_FOUR_DIFF와 동일한 차동 구동 방식으로 처리됩니다.
AgileX 공식 LIMO Pro 매뉴얼에서도 Track 모드는 four-wheel differential과 동일한 방식의 slip/skid steering을 사용한다고 설명합니다. 즉 ROS 측에서는 4륜 차동과 무한궤도를 구분할 필요가 없습니다.
1. 가장 먼저 볼 곳: 주행 모드 정의
파일:
limo_ros2/limo_base/include/limo_base/limo_protocol.h
97~102행:
enum {
MODE_FOUR_DIFF = 0x00,
MODE_ACKERMANN = 0x01,
MODE_MCNAMU = 0x02,
MODE_UNKNOWN = 0xff,
};
이 부분이 전체 주행 모드의 기준입니다.
| 값 | 모드 | 실제 의미 |
|---|---|---|
0x00 | MODE_FOUR_DIFF | 4륜 차동 + 무한궤도 |
0x01 | MODE_ACKERMANN | 전륜 스티어링 |
0x02 | MODE_MCNAMU | 메카넘 휠 |
0xFF | MODE_UNKNOWN | 알 수 없음 |
특히 중요한 점은 MODE_TRACK이 존재하지 않는다는 것입니다.
2. 가장 중요한 부분: /cmd_vel을 모드별로 처리하는 곳
실질적으로 가장 참고해야 할 함수입니다.
파일:
limo_ros2/limo_base/src/limo_driver.cpp
421~464행:
void LimoDriver::twistCmdCallback(
const geometry_msgs::msg::Twist::SharedPtr msg)
{
switch (motion_mode_) {
case MODE_FOUR_DIFF: {
setMotionCommand(
msg->linear.x,
msg->angular.z,
0,
0);
break;
}
case MODE_ACKERMANN: {
double r =
msg->linear.x /
msg->angular.z;
...
double central_angle =
std::atan(wheelbase_ / r);
double inner_angle =
convertCentralAngleToInner(
central_angle);
...
setMotionCommand(
msg->linear.x,
0,
0,
steering_angle);
break;
}
case MODE_MCNAMU: {
setMotionCommand(
msg->linear.x,
msg->angular.z,
msg->linear.y,
0);
break;
}
default:
break;
}
}
이 함수가 사실상 LIMO의 주행 모드별 ROS2 운동학 인터페이스의 핵심입니다.

3. 4륜 차동 구동
위치는:
limo_driver.cpp
421~427행
핵심:
case MODE_FOUR_DIFF: {
setMotionCommand(
msg->linear.x,
msg->angular.z,
0,
0);
break;
}
즉 ROS2에서는
/cmd_vel
linear.x → 전진/후진
angular.z → 좌/우 회전
만 사용합니다.
개념적으로:
+X
↑
FL FR
O O
O O
RL RR
linear.x > 0
↑ ↑
↑ ↑
angular.z > 0
↓ ↑
↓ ↑
따라서 차동/스키드 스티어링입니다.
4. 무한궤도 구동은 어디인가?
여기가 중요한 부분입니다.
소스에는:
MODE_TRACK
이 없습니다.
무한궤도는 다음 코드를 그대로 사용합니다.
case MODE_FOUR_DIFF:
왜냐하면 Track의 운동학 자체가 4륜 차동과 동일하기 때문입니다.
Track motion mode는 좌우 트랙의 상대 속도로 방향을 제어하며, four-wheel differential과 동일하게 slide steering을 사용합니다.
따라서:
4륜 차동
│
├── MODE_FOUR_DIFF
│
└── linear.x + angular.z
▲
│
무한궤도 ──────┘
입니다.
즉 무한궤도를 장착해도 ROS 코드는:
setMotionCommand(
msg->linear.x,
msg->angular.z,
0,
0);
그대로입니다.
5. Ackermann / 스티어링 구동
파일:
limo_ros2/limo_base/src/limo_driver.cpp
428~455행:
case MODE_ACKERMANN: {
double r =
msg->linear.x /
msg->angular.z;
if(fabs(r) < track_/2.0)
{
...
}
double central_angle =
std::atan(wheelbase_ / r);
double inner_angle =
convertCentralAngleToInner(
central_angle);
if (inner_angle > max_inner_angle_)
inner_angle = max_inner_angle_;
if (inner_angle < -max_inner_angle_)
inner_angle = -max_inner_angle_;
double steering_angle;
if (inner_angle > 0) {
steering_angle =
inner_angle /
right_angle_scale_;
}
else {
steering_angle =
inner_angle /
right_angle_scale_;
}
setMotionCommand(
msg->linear.x,
0,
0,
steering_angle);
break;
}
여기서는 /cmd_vel의
linear.x
angular.z
를 바로 보내지 않습니다.
먼저 회전 반경:
r = linear.x / angular.z;
를 계산합니다.
그리고 Ackermann steering angle을 계산합니다.
cmd_vel
linear.x ─────────┐
│
angular.z ──> 회전반경 r │
│ │
↓ │
central_angle │
│ │
↓ │
inner_angle │
│ │
↓ │
steering_angle │
│ │
▼ ▼
LIMO 하부 컨트롤러
6. Ackermann 각도 변환 함수

central_angle= 차량 중심축 기준의 가상 조향각inner_angle= 실제 회전 시 안쪽 바퀴의 조향각- Ackermann 구조에서는 회전할 때 안쪽 바퀴가 바깥쪽 바퀴보다 더 많이 꺾입니다.
- 따라서 일반적으로
|inner_angle| > |central_angle| > |outer_angle|
입니다.
같은 파일의 아래쪽도 반드시 참고하셔야 합니다.
Inner → Central
실제 안쪽 앞바퀴의 조향각으로부터 차량의 bicycle-model 기준 중앙 조향각을 계산하는 함수입니다.
limo_driver.cpp
696~705행
double LimoDriver::convertInnerAngleToCentral(
double inner_angle)
{
double r = wheelbase_ / std::tan(fabs(inner_angle)) + track_ / 2;
double central_angle = std::atan(wheelbase_ / r);
if (inner_angle < 0) {
central_angle = -central_angle;
}
return central_angle;
}
Central → Inner
ROS/차량 모델에서 사용되는 중앙 조향각을 실제 Ackermann 기구에서 필요한 안쪽 바퀴 조향각으로 변환하는 함수입니다.
limo_driver.cpp
707~722행
double LimoDriver::convertCentralAngleToInner(
double central_angle)
{
double inner_angle =
std::atan(
2 * wheelbase_ *
std::sin(fabs(central_angle))
/
(
2 * wheelbase_ *
std::cos(fabs(central_angle))
-
track_ *
std::sin(fabs(central_angle))
)
);
if (central_angle < 0) {
inner_angle =
-inner_angle;
}
return inner_angle;
}
Ackermann 로봇을 직접 구현하실 계획이라면 이 두 함수는 꽤 중요한 참고 부분입니다.

7. Ackermann 차량 파라미터
파일:
limo_ros2/limo_base/include/limo_base/limo_driver.h
127~131행 부근:
static constexpr double max_inner_angle_
= 0.48869; // 28 degree
static constexpr double track_
= 0.172; // 좌우 휠 간 거리
static constexpr double wheelbase_
= 0.2; // 앞뒤 휠 간 거리
즉:
wheelbase = 0.2 m
<-------------------->
FL FR
O----------O
| |
| |
| | track
| | 0.172 m
| |
O----------O
RL RR

Ackermann 계산을 다른 로봇에 이식할 경우 이 값은 반드시 실제 차량 치수에 맞춰 변경해야 합니다.
8. 메카넘 휠
메카넘 모드는 아주 명확합니다.
limo_driver.cpp
456~459행
case MODE_MCNAMU: {
setMotionCommand(
msg->linear.x,
msg->angular.z,
msg->linear.y,
0);
break;
}
즉:
linear.x → 전진/후진
linear.y → 좌/우 횡이동
angular.z → 회전
입니다.
그래서 메카넘은 ROS 표준 Twist를 거의 그대로 사용합니다.
geometry_msgs/Twist
│
├── linear.x → VX
│
├── linear.y → VY
│
└── angular.z → WZ
예를 들어:
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.0, y: 0.3, z: 0.0},
angular: {x: 0.0, y: 0.0, z: 0.0}}"
이면 메카넘 모드에서 옆으로 이동할 수 있는 구조입니다.
9. 최종 명령을 LIMO MCU에 보내는 곳
모드별 계산이 끝나면 모두 이 함수로 모입니다.
파일:
limo_ros2/limo_base/src/limo_driver.cpp
385~404행:
void LimoDriver::setMotionCommand(
double linear_vel,
double angular_vel,
double lateral_velocity,
double steering_angle)
{
LimoFrame frame;
frame.id =
MSG_MOTION_COMMAND_ID;
int16_t linear_cmd =
linear_vel * 1000;
int16_t angular_cmd =
angular_vel * 1000;
int16_t lateral_cmd =
lateral_velocity * 1000;
int16_t steering_cmd =
steering_angle * 1000;
frame.data[0] =
static_cast<uint8_t>(
linear_cmd >> 8);
frame.data[1] =
static_cast<uint8_t>(
linear_cmd & 0x00ff);
frame.data[2] =
static_cast<uint8_t>(
angular_cmd >> 8);
frame.data[3] =
static_cast<uint8_t>(
angular_cmd & 0x00ff);
frame.data[4] =
static_cast<uint8_t>(
lateral_cmd >> 8);
frame.data[5] =
static_cast<uint8_t>(
lateral_cmd & 0x00ff);
frame.data[6] =
static_cast<uint8_t>(
steering_cmd >> 8);
frame.data[7] =
static_cast<uint8_t>(
steering_cmd & 0x00ff);
sendFrame(frame);
}
8바이트 구조를 보면 더 명확합니다.
MSG_MOTION_COMMAND_ID = 0x111
+--------+--------+----------------------+
| Byte | 값 | 의미 |
+--------+--------+----------------------+
| 0~1 | VX | 전진 속도 |
| 2~3 | WZ | 회전 각속도 |
| 4~5 | VY | 횡방향 속도 |
| 6~7 | STEER | 스티어링 각도 |
+--------+--------+----------------------+
즉 LIMO ROS2 드라이버는 하부 MCU에 개별 휠 RPM을 직접 보내지 않습니다.
이 부분이 중요합니다.
ROS2
/cmd_vel
↓
twistCmdCallback()
↓
주행모드별 운동학 변환
↓
setMotionCommand()
↓
VX / WZ / VY / steering
↓
Serial 460800 bps
↓
LIMO 하부 MCU
↓
각 모터 속도 계산
↓
4개 허브모터
따라서 4개의 휠 RPM을 계산하는 코드는 이 ROS2 소스 안에 없습니다.
그 부분은 LIMO 본체 하부 MCU firmware 내부에 있습니다.
10. 현재 주행 모드를 어디서 알아오는가
이 부분도 상당히 중요합니다.
limo_driver.cpp
254~269행
case MSG_SYSTEM_STATE_ID: {
uint8_t vehicle_state =
frame.data[0];
uint8_t control_mode =
frame.data[1];
...
if (!use_mcnamu_) {
motion_mode_ =
frame.data[6];
}
publishLimoState(
frame.stamp,
vehicle_state,
control_mode,
battervyoltage,
error_code,
motion_mode_);
break;
}
즉 하부 제어기로부터:
MSG_SYSTEM_STATE_ID = 0x211
메시지가 들어오고,
frame.data[6]
가 현재 motion mode입니다.
구조는 대략:
LIMO MCU
│
│ MSG_SYSTEM_STATE_ID
▼
serial
│
▼
parseFrame()
│
└── frame.data[6]
│
▼
motion_mode_
│
▼
twistCmdCallback()
│
┌──────┼─────────┐
▼ ▼ ▼
4DIFF ACK MCNAMU
입니다.
11. /limo_status에서도 확인 가능
메시지:
limo_ros2/limo_msgs/msg/LimoStatus.msg
내용:
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
float64 battery_voltage
uint16 error_code
uint8 motion_mode
그리고:
status_msg.motion_mode =
motion_mode_;
로 발행합니다.
따라서 실제 LIMO에서:
ros2 topic echo /limo_status
를 보면 현재 모드를 확인할 수 있습니다.
일반적으로:
motion_mode: 0
→ 4륜 차동 또는 무한궤도
motion_mode: 1
→ Ackermann
motion_mode: 2
→ Mecanum
으로 보면 됩니다.
12. 메카넘 모드를 강제로 지정하는 부분
생성자도 보셔야 합니다.
limo_driver.cpp
36~64행
use_mcnamu_ =
this->declare_parameter<bool>(
"use_mcnamu",
false);
...
if(use_mcnamu_) {
motion_mode_ =
MODE_MCNAMU;
}
그리고 상태를 받을 때:
if (!use_mcnamu_) {
motion_mode_ =
frame.data[6];
}
즉:
use_mcnamu = false
│
└─ MCU가 알려주는 mode 사용
use_mcnamu = true
│
└─ 강제로 MODE_MCNAMU 사용
구조입니다.
limo_ros2/limo_base/launch/limo_base_mecanum.launch.py
에는 'use_mcnamu‘: True로 설정되어 있습니다.
이 launch 파일을 사용할 수 있습니다.
13. Odometry도 모드별로 따로 계산
명령 쪽만 볼 것이 아니라 Odometry 쪽도 참고할 가치가 큽니다.
limo_driver.cpp
530~626행
핵심:
switch (motion_mode_) {
case MODE_FOUR_DIFF:
vx = linear_velocity;
vy = 0;
wz = angular_velocity;
break;
case MODE_ACKERMANN:
...
wz = linear_velocity / r;
vx =
linear_velocity *
std::cos(central_angle);
vy =
linear_velocity *
std::sin(central_angle);
break;
case MODE_MCNAMU:
vx = linear_velocity;
vy = lateral_velocity;
wz = angular_velocity;
break;
}

따라서 각 모드별 참고할 곳은 크게 두 군데입니다.
명령 운동학
twistCmdCallback()
↓
/cmd_vel → 차량 명령
Odometry 운동학
publishOdometry()
↓
차량 피드백 → /odom
둘을 같이 보셔야 합니다.
14. URDF에서 참고할 곳
물리 구조를 참고하려면 다음 파일도 보시면 됩니다.
4륜 차동
limo_ros2/limo_description/urdf/
limo_four_diff.xacro
65~79행:
front_left
front_right
rear_left
rear_right

4개 휠 배치가 있습니다.
Ackermann
limo_ros2/limo_description/urdf/
limo_steering_hinge.xacro
4~122행:
<xacro:limo_left_steering_hinge ... />
<xacro:limo_right_steering_hinge ... />
전륜 조향 구조입니다.
26~34행:
<joint
name="${wheel_prefix}_wheel"
type="revolute">
<axis xyz="0 0 1" />
<limit
lower="-0.523598767"
upper="0.523598767"
effort="5"
velocity="0.5" />
</joint>

실제 조향 joint 범위를 볼 수 있습니다.
15. Gazebo의 4륜 차동 구조
limo_description/urdf/limo_four_diff.gazebo
58~76행에 아주 좋은 참고 코드가 있습니다.
현재는 주석 처리되어 있지만:
<plugin
name="four_diff_controller"
filename="libgazebo_ros_skid_steer_drive.so">
<leftFrontJoint>
front_left_wheel
</leftFrontJoint>
<rightFrontJoint>
front_right_wheel
</rightFrontJoint>
<leftRearJoint>
rear_left_wheel
</leftRearJoint>
<rightRearJoint>
rear_right_wheel
</rightRearJoint>
<wheelSeparation>
0.172
</wheelSeparation>
<wheelDiameter>
0.09
</wheelDiameter>
<commandTopic>
cmd_vel
</commandTopic>
</plugin>

이 부분은 4륜 차동/무한궤도 구현을 직접 만들 때 매우 좋은 참고 자료입니다.
특히:
libgazebo_ros_skid_steer_drive.so
라는 이름 자체가 LIMO의 four-diff와 track이 skid steering 계열이라는 것을 보여줍니다.
16. Ackermann Gazebo도 참고 가치가 높음
파일:
limo_description/urdf/limo_ackerman.gazebo
32~52행:
<plugin
name="four_diff_controller"
filename="libgazebo_ros_ackerman.so">
<leftFrontJoint>
front_left_wheel
</leftFrontJoint>
<rightFrontJoint>
front_right_wheel
</rightFrontJoint>
<leftRearJoint>
rear_left_wheel
</leftRearJoint>
<rightRearJoint>
rear_right_wheel
</rightRearJoint>
<leftHingeJoint>
left_steering_hinge_wheel
</leftHingeJoint>
<rightHingeJoint>
right_steering_hinge_wheel
</rightHingeJoint>
</plugin>

직접 4WS 또는 Ackermann 로봇을 개발하신다면 이 URDF 구조가 상당히 유용합니다.
17. 정리
구동모드의 구조를 한 장으로 보면 다음과 같습니다.
ROS2 /cmd_vel
│
▼
twistCmdCallback()
│
motion_mode_ switch
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
MODE_FOUR_DIFF MODE_ACKERMANN MODE_MCNAMU
│ │ │
4륜 차동 Ackermann 계산 Mecanum
무한궤도 steering_angle VX,VY,WZ
│ │ │
└─────────────────┼─────────────────┘
▼
setMotionCommand()
│
▼
MSG_MOTION_COMMAND_ID
0x111
│
┌─────────┬───────┼─────────┐
▼ ▼ ▼ ▼
VX WZ VY STEER
data0~1 data2~3 data4~5 data6~7
│
▼
Serial 460800
│
▼
LIMO 하부 MCU
│
▼
4개 Hub Motor
따라서 다른 4륜/무한궤도/메카넘/Ackermann 로봇을 직접 개발하기 위해 LIMO 코드를 참고하시는 목적이라면, 가장 먼저 twistCmdCallback() → setMotionCommand() → publishOdometry() 세 부분을 보시면 됩니다. 특히 무한궤도는 별도 모드로 만들 필요 없이 MODE_FOUR_DIFF 운동학을 그대로 재사용하는 것이 LIMO의 설계 방식입니다.