# 03. 로봇/드라이버 시스템 구성

출처: `README.md`, `xbox_motor_control.cpp` (main 함수 상단 파라미터), 실측값

## 3.1 로봇 물리 제원

| 항목 | 값 | 비고 |
| :--- | :-- | :--- |
| 구동 방식 | 스키드 스티어(Skid-Steer) 4WD | 좌/우 각 2륜씩 기계적으로 독립 구동, 조향 없음 |
| 전폭 (Track Width, W) | 576.0 mm | 좌우 바퀴 중심 간 거리 |
| 전장 축간거리 (Wheelbase, L) | 368.4 mm | 전후 바퀴 중심 간 거리 |
| 휠 유효 반경 (R) | 131.517 mm | 버니어 실측값, 10.35인치 타이어 |
| 전면 범퍼 오프셋 | 45.0 mm | 휠 축 → 로봇 최전방 범퍼 |
| 로봇 전폭(차체) | 410 mm | 라이다 회피 폭 계산 기준 |
| 로봇 전장(차체) | 631 mm | |
| 로봇 전고 | 286 mm | |

## 3.2 모터 & 드라이버 매핑

| 위치 | 모터 | 소속 드라이버 | Station ID | 시리얼 포트 | 드라이버 채널 |
| :--- | :--- | :-: | :-: | :--- | :-: |
| 전방 좌 (FL) | ZLLG80ASM250-4096-B V2.12 | 드라이버 #1 (전방) | 1 | `/dev/ttyUSB0` | Left |
| 전방 우 (FR) | ZLLG80ASM250-4096-B V2.12 | 드라이버 #1 (전방) | 1 | `/dev/ttyUSB0` | Right |
| 후방 좌 (RL) | ZLLG80ASM250-4096-B V2.12 | 드라이버 #2 (후방) | 2 | `/dev/ttyUSB0` (동일 포트, 데이지체인) 또는 `--port2` | Left |
| 후방 우 (RR) | ZLLG80ASM250-4096-B V2.12 | 드라이버 #2 (후방) | 2 | 〃 | Right |

* 드라이버 2대는 **동일 RS485 버스(같은 USB-RS485 포트)를 공유**하는 것이 기본 동작이며,
  Modbus 슬레이브 주소(1, 2)로 구분한다. `--port2`를 지정하면 별도의 물리 포트를 사용하도록
  전환 가능(`sp2_ptr`가 `&sp1` 대신 `&sp2`를 가리키게 됨).
* `--bcast`/`--broadcast` 옵션 사용 시 두 드라이버 모두 주소 `0`(브로드캐스트)으로 동시에
  명령을 받는다 — 이 경우 개별 피드백 조회/전류 조회는 생략된다(`driver_rear_ptr = nullptr`
  로 유지, 앞뒤 드라이버가 응답을 구분해 회신할 수 없기 때문).
* 좌/우 부호가 반대인 이유: `MotorDriver::setRPMs(l_rpm, r_rpm)`에 넘기는 지령에서
  `target_fr = -v_r * rpm_per_ms`, `target_rr = -v_r * rpm_per_ms`처럼 **우측 바퀴는 항상
  부호 반전**해서 넘긴다. 좌/우 모터가 로봇 상에서 물리적으로 반대 방향으로 장착되어 있어
  (양쪽 모터가 "전진"으로 회전해야 하는 U/V/W 위상이 거울 대칭) 이렇게 보정하지 않으면
  우측 바퀴가 반대로 회전한다.

## 3.3 엔코더 해상도

* 모터 1회전당 **16,384 ticks** (4체배 카운팅 기준)
* `meters_per_tick = (2π × wheel_radius) / 16384`
* 주행거리(Odometry)는 4륜 엔코더 델타의 **중앙값(4개 중 중간 2개 평균)**으로 계산해,
  웅덩이 등으로 한 바퀴가 들떠 헛도는 경우의 이상치를 자동으로 배제한다 (`dist_axle`).
  `dist_bumper = dist_axle + bumper_offset`(전진 시)으로 범퍼 기준 거리도 함께 산출.

## 3.4 스키드 스티어 운동학

### 유효 선회 반경 계수

```cpp
effective_w = sqrt(track_width² + wheelbase²)   // 대각선 유효 폭
k_skid = (track_width + wheelbase² / track_width) / 2
```

`k_skid`는 순수 사각 스키드 스티어(회전축이 로봇 중심)보다 실제 마찰 중심을 반영해 보정한
계수로, 직진+커브 선회 시 좌/우 바퀴 속도 차를 계산하는 데 사용된다.

### 모드 1 — 직진/커브 선회 (`is_spin_turn == false`)

좌측 스틱 전후(ly)로 전진 속도 `v_x = ly × max_v`를 만들고, 좌측 스틱 좌우(lx)로 곡선 조향을
만든다. 저속에서 커브 반경이 스로틀과 무관하게 일정하도록, 각속도 `omega`는 **현재 전진 속도
`v_x`에 비례**하도록 계산된다(상수 `max_v`가 아님 — 저속에서 안쪽 바퀴가 역회전해버리는 버그를
막기 위한 수정):

```cpp
omega += -lx * (abs(v_x) / (effective_w / 2))
v_l = v_x - omega * k_skid
v_r = v_x + omega * k_skid
target_fl = target_rl =  v_l * rpm_per_ms
target_fr = target_rr = -v_r * rpm_per_ms   // 우측 부호 반전
```

### 모드 2 — 제자리 회전 (`is_spin_turn == true`, ly==0 && rx!=0)

우측 스틱 좌우(rx)만으로 제자리 선회한다. 순수 제자리 회전(ICR이 로봇 중심)은 고마찰 노면에서
정지마찰이 급격히 파괴돼 바퀴가 긁히므로, **미세한 전진 성분(`spin_arc_bias`, 기본 12%)**을
섞어 ICR을 로봇 중심에서 살짝 벗어나게 해 스크럽 마찰을 줄인다:

```cpp
omega = -rx * (max_spin_v / (effective_w / 2))
v_bias = abs(rx) * max_spin_v * spin_arc_bias
v_l = v_bias - omega * (effective_w / 2)
v_r = v_bias + omega * (effective_w / 2)
```

제자리 회전 진입 조건은 `ly == 0.0f`의 **완전 일치 비교**다. `applyDeadzone()`이 데드존
이하 입력을 정확히 `0.0f`로 반환하기 때문에 안전하며, 부동소수점 근사 오차로 인한 오탐을
막기 위해 별도 임계값을 두지 않고 정확히 이 특성에 의존한다.

### 라이다 자동 회피 조향 (측면 회피 시, 조이스틱 좌우 입력이 없을 때)

```cpp
v_l = v_x - omega * k_skid
v_r = v_x + omega * k_skid
```
(제자리 회전이 아니면서 `lx==rx==0`이고 `omega!=0`인 경우 — [04-software-control.md](04-software-control.md)
의 라이다 회피 절 참고)

## 3.5 속도 → 드라이버 RPM 지령 변환

```cpp
rpm_per_ms = 60 / (2π × wheel_radius)   // m/s → RPM 변환 계수
```

계산된 `target_*`(RPM)는 곧바로 드라이버에 보내지 않고, 저크 제한(Jerk-Limited) S-curve
프로파일(`jerkLimitedStep()`)을 거쳐 `cmd_*`로 서서히 수렴한 뒤 `setRPMs()`로 전달된다.
소프트웨어 측 가감속/저크 프로파일은 드라이버 자체의 가감속 레지스터(`0x2080`~`0x2083`,
150ms)보다 상위 레벨에서 한 번 더 부드럽게 다듬는 이중 구조다. 상세는
[04-software-control.md](04-software-control.md) 참고.
