Limo 구동 모드

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,
};

이 부분이 전체 주행 모드의 기준입니다.

값모드실제 의미
0x00MODE_FOUR_DIFF4륜 차동 + 무한궤도
0x01MODE_ACKERMANN전륜 스티어링
0x02MODE_MCNAMU메카넘 휠
0xFFMODE_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의 설계 방식입니다.

Leave a Comment