# 04. 소프트웨어 제어 구조 (`xbox_motor_control.cpp`) 출처: `xbox_motor_control.cpp` 전체 (1382줄), `Makefile`, `run_4wd.sh` ## 4.1 빌드 & 실행 ```bash make # g++ -O3 -std=c++17 -Wall -Wextra ... -lSDL2 -lpthread -llivox_lidar_sdk_shared ./run_4wd.sh [PORT] # USB latency_timer 1ms 튜닝 + 빌드(필요시) + 실행 ``` `run_4wd.sh`는 실행 전 `set_low_latency.sh`로 `/sys/bus/usb-serial/devices//latency_timer`를 16ms(리눅스 기본값) → 1ms로 낮춰, 100Hz 제어 루프에서 시리얼 read 지연이 병목이 되지 않게 한다. ## 4.2 클래스 구조 ``` SerialPort — 포트 open/close, Modbus RTU read/write (writeReg/writeRegs/readRegs) MotorDriver — SerialPort + slave_id를 감싸는 드라이버 1대(좌우 2채널) 추상화 initDriver / setBrakes / setRPMs / readFeedback / readCurrentLimits / setMaxCurrent LidarObstacleDetector — Livox Mid-360S SDK 콜백 기반 장애물 거리 추적 (전/후/좌/우 4방향) ``` 드라이버 2대(`driver_front`, `driver_rear`)가 각각 `MotorDriver` 인스턴스로 생성되며, 브로드캐스트 모드가 아니면 후방 드라이버 포인터(`driver_rear_ptr`)가 유효해 개별 제어/피드백이 가능하다. ## 4.3 100Hz 메인 루프 개요 ``` 매 틱 (10ms): 1. SDL 이벤트 처리 (버튼: A=트립리셋, X=라이다 토글, B/Back=종료) 2. 조이스틱 입력 읽기 + 데드존 적용 3. 라이다 장애물 회피 로직 (속도 스케일링 + 조향 오프셋) 4. 스키드 스티어 운동학으로 target_fl/fr/rl/rr(RPM) 계산 5. airborne(들뜸) 판정된 바퀴는 target를 0으로 강제 6. 상태머신(STOPPED/RUNNING/STOPPING) + 저크 제한 S-curve로 cmd_* 갱신 7. setRPMs()로 드라이버에 지령 전송 8. readFeedback()으로 드라이버에서 피드백 수신 9. 드라이버 알람(에러코드) 감지 및 콘솔 경고 10. 휠 상태(정상/들뜸/걸림) 판정, 주행거리 갱신 11. (옵션) CSV 로그 기록 12. HUD 한 줄 출력 (\r로 갱신) ``` ## 4.4 상태 머신 | 상태 | 전이 조건 | 동작 | | :--- | :--- | :--- | | `STOPPED` | 초기 상태 | `target_*` 절대값이 0.1 RPM 초과하면 브레이크 해제(`setBrakes(false)`) 후 50ms 대기, `RUNNING`으로 전이 | | `RUNNING` | `STOPPED`에서 전이 | 저크 제한 프로파일로 `cmd_*` → `target_*` 추종. `target_*`와 `cmd_*`가 모두 임계값 이하면 `STOPPING`으로 전이 | | `STOPPING` | `RUNNING`에서 전이 | 감속 프로파일로 0으로 수렴. `cmd_fl/fr`이 0.1 RPM 미만이면 즉시 정지 지령(`setRPMs(0,0)`) 후 50ms 뒤 브레이크 잠금, `STOPPED`로 복귀 | 브레이크 해제/잠금 시 50ms 대기를 두는 이유는 전자식 브레이크(솔레노이드)의 기계적 응답 시간을 확보하기 위함이다. ## 4.5 저크 제한(Jerk-Limited) S-curve 프로파일 ```cpp jerkLimitedStep(current_vel, target_vel, current_accel&, max_accel, max_decel, jerk_limit) ``` * 목표 속도까지 **가속도 자체를 매 틱 `jerk_limit × dt`만큼만 변화**시켜 부드럽게 목표 가속도에 도달시킨다. 정지 상태에서 출발할 때 첫 틱부터 풀가속도가 걸려 정지마찰이 급격히 파괴되는 문제(예: 저마찰 노면에서 순간 슬립)를 막기 위한 구조. * 오버슈트 방지: 다음 틱 속도가 목표를 넘어서면 그 자리에서 목표값으로 스냅하고 가속도를 0으로 리셋한다. * 가속/감속/저크 한계값은 CLI 인자로 조정 가능 (§4.8). * 제자리 회전은 `spin_jerk_rate`(기본 220 RPM/s²)로 직진/커브 선회(`jerk_rate`, 기본 600 RPM/s²) 보다 훨씬 완만하게 적용된다 — 고마찰 노면에서 제자리 선회 시 정지마찰이 급격히 깨지지 않도록 가속에 도달하는 시간을 더 길게 늘린 것. ## 4.6 착지(Landing) 스냅 처리 들뜸(airborne) 상태에서 해제된(착지) 직후 한 틱은, 저크 제한 프로파일을 건너뛰고 `cmd_*`를 `target_*`로 즉시 스냅한다(`just_landed_*` 플래그). 차체는 이미 그 속도로 이동 중인데 지령이 정지출발용 완만한 가속 램프를 다시 타면, 그 사이 바퀴가 지면 이동 속도를 못 따라가며 끌리는 현상을 없애기 위함. ## 4.7 라이다(Livox Mid-360S) 장애물 회피 `LidarObstacleDetector`가 포인트클라우드 콜백에서 좌표를 17° 하향 피치 보정 후 4개 구역 (전방/후방/좌측/우측)의 최소 거리를 EMA(지수이동평균, 접근 시 즉각 반응·회복 시 완만)로 추적한다. | 파라미터 | 기본값 | 의미 | | :--- | :-: | :--- | | `front_stop_dist` | 0.35 m | 전방 완전 정지 | | `front_warn_dist` | 0.50 m | 전방 감속 시작 (선형 스케일) | | `side_dodge_dist` | 0.50 m | 측면 회피 조향 시작 | | `side_stop_dist` | 0.20 m | 측면 회피 강도 100% 도달 거리 | | `max_dodge_omega` | 0.35 rad/s | 자동 회피 최대 조향 각속도 | * 전진 중 전방 장애물 → 속도 스케일링(0~1) 또는 완전 정지. * 후진 중 후방 장애물 → 완전 정지만 적용(감속 스케일링 없음). * 전진 중이면서 사용자가 좌우 조향(rx) 입력이 없을 때만 측면 회피 조향 개입 (요청 사양: 후진 시 측면 회피 미적용). * 라이다 데이터 800ms 이상 미수신 시 `connected=false`로 판단, HUD에 `LIDAR:WAITING` 표시. * X 버튼으로 라이다 회피 기능 자체를 런타임에 토글 가능. ## 4.8 휠 이상(들뜸/걸림) 진단 속도 폐루프 특성상(부하 무관하게 지령 RPM을 추종하려 함) "지령 대비 실제속도" 하나만으로는 무부하(들뜸)를 판정할 수 없다. 두 개의 독립된 판정 축을 사용한다: ### 걸림/과부하 판정 (`classifyStall`) ```cpp vel_ratio = |실제RPM| / |지령RPM| stall = (vel_ratio < stall_vel_ratio) && (|전류| >= stall_current_a) ``` 지령속도가 `min_active_rpm`(기본 3 RPM) 미만이면 판정 보류. 기본값: `stall_vel_ratio=0.5`, `stall_current_a=12.0A`(정격 15A 근접). ### 무부하/들뜸 판정 (`classifyAirPair`) 같은 지령속도를 받는 **같은쪽 앞뒤 페어**(FL↔RL, FR↔RR)의 전류를 비교한다. 좌/우를 통째로 비교하면 정상 커브 선회에서 좌우 지령속도가 원래 다르기 때문에 오탐이 발생하므로, 반드시 `target_fl==target_rl`, `target_fr==target_rr`가 항상 성립하는 앞뒤 페어끼리만 비교한다. ```cpp hi = max(|amp_a|, |amp_b|) if (hi < 0.5A) return; // 둘 다 무전류(관성 주행)면 판정 보류 if (a < hi * airborne_current_ratio) air_a = true; // 기본 0.35 if (b < hi * airborne_current_ratio) air_b = true; ``` **디바운스**: 노이즈성 순간 전류 편차로 즉시 개입하지 않도록, `airborne_debounce_ticks` (기본 5틱=50ms) 연속으로 조건이 유지될 때만 실제 `airborne_*` 상태가 확정되고 해당 바퀴 `target`이 0으로 강제된다. 접지력이 회복돼 전류가 정상화되면 다음 판정 틱에서 자동 해제되고, §4.6의 착지 스냅 로직으로 즉시 원래 지령을 재개한다. HUD의 `WHL(FL/FR/RL/RR)` 4글자는 각 바퀴 상태를 `O`(정상)/`A`(들뜸,Airborne)/`S`(걸림,Stall)로 표시. ## 4.9 드라이버 알람 감지 `readFeedback()`로 받은 `err_f_l/err_f_r/err_r_l/err_r_r` 중 하나라도 0이 아니면 알람 발생으로 간주, **상태 전이 시(0→알람) 1회만** 콘솔에 `decodeDriverError()` 결과와 함께 큰 경고를 출력한다 (매 틱 갱신되는 HUD 줄과 별개 라인). 알람 발생 시 드라이버가 명령을 무시하는 잠금 상태가 되어 소프트웨어 재시작만으로는 해제되지 않는 경우가 많음을 코드 주석이 명시 — 하드웨어 알람 클리어 또는 전원 재투입이 필요할 수 있다. 상세 대응은 [05-error-troubleshooting.md](05-error-troubleshooting.md) 참고. ## 4.10 CSV 로깅 (`--log <경로>`) 매 틱 아래 컬럼을 기록한다: ``` t_ms,state,ly,lx,rx, cmd_fl,cmd_fr,cmd_rl,cmd_rr, fb_fl,fb_fr,fb_rl,fb_rr, amp_fl,amp_fr,amp_rl,amp_rr, stat_fl,stat_fr,stat_rl,stat_rr, err_fl,err_fr,err_rl,err_rr, dist_axle,comm_ms ``` 실외에서 로봇을 조종하며 화면을 동시에 읽기 어려우므로, 문제가 된 구간(턱 넘는 지점 등)을 나중에 잘라서 분석하기 위한 용도. ## 4.11 CLI 인자 전체 목록 | 인자 | 기본값 | 설명 | | :--- | :-- | :--- | | `--port`, `--port1` | `/dev/ttyUSB0` | 전방 드라이버(및 기본 후방) 시리얼 포트 | | `--port2` | (없음, port1 재사용) | 후방 드라이버 전용 포트 지정 시 | | `--id1` | 1 | 전방 드라이버 Modbus 슬레이브 주소 | | `--id2` | 2 | 후방 드라이버 Modbus 슬레이브 주소 | | `--bcast` / `--broadcast` | off | 두 드라이버 모두 주소 0(브로드캐스트)으로 동시 제어 | | `--track_width` | 0.576 m | 좌우 바퀴 중심거리 | | `--wheelbase` | 0.3684 m | 전후 바퀴 중심거리 | | `--radius` | 0.131517 m | 휠 유효 반경 | | `--bumper` | 0.045 m | 휠 축→전면 범퍼 거리 | | `--accel` | 120 RPM/s | 최대 가속도 | | `--decel` | 180 RPM/s | 최대 감속도 | | `--jerk` | 600 RPM/s² | 직진/커브 저크 한계 | | `--spin_jerk` | 220 RPM/s² | 제자리 회전 저크 한계 | | `--spin_arc_bias` | 0.12 | 제자리 회전 시 혼합할 미세 전진 비율 (0~1) | | `--min_active_rpm` | 3.0 RPM | 이상판정 보류 임계 지령속도 | | `--airborne_current_ratio` | 0.35 | 무부하(들뜸) 판정 전류비 | | `--stall_vel_ratio` | 0.5 | 걸림 판정 속도비 | | `--stall_current_a` | 12.0 A | 걸림 판정 전류 임계값 | | `--airborne_debounce_ticks` | 5 (50ms) | 들뜸 판정 디바운스 | | `--log ` | (비활성) | CSV 로그 파일 경로 | | `--max_current_a` | 20.0 A | 4바퀴 최대전류 일괄 설정 (음수=미변경) | | `--no_lidar` | (라이다 사용) | 라이다 비활성화 | | `--lidar_config` | `mid360s_config.json` | Livox SDK 설정 파일 | | `--front_stop` | 0.35 m | 전방 정지거리 | | `--front_warn` | 0.50 m | 전방 경고(감속)거리 | | `--side_dodge` | 0.50 m | 측면 회피 감지거리 | | `--robot_width` | 0.410 m | 라이다 회피 폭 계산용 로봇 전폭 | | `--lidar_height` | 0.460 m | 라이다 설치 높이 | | `--lidar_pitch` | 17.0 deg | 라이다 하향 피치각 | | `--lidar_x_offset` | 0.250 m | 라이다 전방 오프셋 | | `--min_z` / `--max_z` | -0.40 / 0.80 m | 라이다 필터링 Z축 범위 |