# 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) 참고.