# 08. 로봇 제어 시스템 개선 보고서

> 이 문서는 `feat/radiomaster-rc-control` 브랜치에서 진행한 개선 작업을 정리한 보고서다.
> 모든 항목은 실제 로봇 실주행 로그로 검증을 거쳤으며, 정량적 결과는 실측 CSV 로그 분석
> 기반이다. 관련 상세 계획/근거는 [06-imu-integration-plan.md](06-imu-integration-plan.md),
> [joystick.md](joystick.md)에 별도로 정리되어 있다.

## 개요

기존 Xbox 컨트롤러(SDL2) 기반 조종을 RadioMaster Pocket + XR1 RC 수신기로 교체하면서 시작해,
IMU(WitMotion HWT905) 도입을 통해 로봇이 "지령한 대로 실제로 움직이고 있는지"를 처음으로
확인·보정할 수 있게 만들었다. 그 결과 제자리 회전 성능이 지령 대비 실측 기준 **44~58% →
93.6%**로 크게 개선되었고, 직진 헤딩 자동 유지, 전신 슬립 감지, 모터 과열 조기 감지 등의
안전/편의 기능이 추가되었다.

## 1. RC 조종 시스템 전환

* Xbox/SDL2 조이스틱을 RadioMaster Pocket + XR1 수신기(CRSF 프로토콜, 420000bps)로 교체.
* 실측 하드웨어 캘리브레이션(CH1 조향/CH2 전후진/CH5 브레이크/CH6 속도모드) 및 Fail-safe
  타임아웃 적용.

**문제**: 실측 중 채널 값이 간헐적으로 엉뚱한 값으로 튀는 현상 발견(예: 스틱을 살짝만
움직였는데 최대 후진값이 순간적으로 찍힘). CRSF 프로토콜은 표준상 CRC-8 체크섬으로 손상된
프레임을 걸러낼 수 있는데, 이 특정 수신기 하드웨어에서는 표준 CRC-8(DVB-S2) 및 가능한
변형(다항식/초기값/반전 조합 전수조사, 보율 오차 가설 등)을 모두 실측 데이터로 대조해봐도
일치율이 최대 33%에 그쳐 **이 하드웨어에서는 CRC 검증 자체가 신뢰할 수 없다**는 결론을 실험적으로
확정.

**적용 방식**: CRC 대신 3단계 방어를 조합했다.
1. **범위 검증**: 캘리브레이션으로 확인된 정상 범위를 벗어나는 값은 무조건 거부.
2. **슬루레이트(변화율) 제한**: 한 틱 사이에 물리적으로 불가능할 만큼 큰 변화는 손상 프레임으로
   간주해 거부하되, 진짜 빠른 스틱 조작까지 막지 않도록 N틱 연속 거부되면 강제로 재동기화하는
   예외 처리를 둠(그렇지 않으면 채널이 영구히 멈추는 결함이 있었음 — 발견 후 수정).
3. **중앙값(median-of-3) 필터**: 최근 3개 값의 중앙값을 취해 단발성 튐값의 영향을 한 번 더
   줄임.
* udev 규칙으로 `/dev/ttyMOTOR`/`/dev/ttyRC`/`/dev/ttyIMU` 고정 심볼릭 링크 적용 — USB 재연결
  시 포트 번호가 매번 바뀌던 문제 해결.

## 2. 주행 제어 안정성 개선

* **브레이크**: 기존엔 스로틀을 중립으로 되돌려 자연 정지할 때도 전자식 브레이크가 자동으로
  잠겼는데, 이를 CH5 스위치를 눌렀을 때만 잠기도록 변경.

* **가속/감속 — 저크 제한(jerk-limited) S-curve란**: 속도(RPM)를 목표까지 바로 바꾸는 게 아니라,
  "가속도가 변하는 속도(저크)" 자체에 상한을 둬서 가속도 곡선을 매끄러운 S자로 만드는 방식이다.
  급격한 가속도 변화는 큰 전류 스파이크(역기전력)로 이어질 수 있어, 감속에는 계속 이 방식을
  유지한다. 다만 하드웨어 백EMF 보호회로 구성이 완료되면서 **가속은 더 이상 소프트웨어로 완만하게
  램프업할 필요가 없어져, 조이스틱 목표값에 1:1로 즉시 추종하도록 변경**했다(감속은 기존과 동일).

* **정지 직전 반전 방지**: 위 "즉시가속" 로직은 "목표값이 현재값보다 커지면 무조건 즉시 반영"
  하는 단순 조건이었는데, 정지 막바지 저속 구간에서 RC 노이즈로 목표값이 순간적으로 반대 부호로
  흔들리면 이 조건이 그대로 성립해버려 "전진→후진→전진"처럼 튀는 현상이 있었다. **적용 방식**:
  조건을 "같은 방향으로 더 빨라지는 경우"로 한정하고, 부호가 바뀌는 경우(RC 노이즈든 실제
  방향 전환 지시든)는 전부 저크 제한 감속 경로로 보내 0을 관통할 때도 부드럽게 처리하도록 수정.

## 3. IMU 통합 — 오도메트리 (Phase 1~2)

* WitMotion HWT905-RS232 IMU를 별도 USB 포트로 연동, 전원 인가 시점의 방향을 정면(0°) 기준으로
  저장.

**적용 방식 — 데드 레커닝(dead reckoning)**: GPS 없이 "지금까지 어느 방향으로 얼마나 움직였는지"를
누적 적분해서 현재 위치(x, y)를 추정하는 고전적 방식이다. 매 제어 주기(틱)마다 (1) 바퀴 인코더
피드백으로 계산한 이동 속도와 (2) 그 순간의 헤딩(진행 방향)을 곱해 이동한 x, y 변위를 누적한다.
정확도는 전적으로 **헤딩을 얼마나 정확히 아는가**에 달려 있다 — 헤딩이 조금만 틀려도 매 틱
누적되며 오차가 기하급수적으로 벌어지기 때문에, 이번 작업에서 가장 공들인 부분이 헤딩 소스
선정이었다.

* 바퀴 엔코더 기반 위치 적분(x, y)과 회전율(wheel_omega) 대 IMU 실측 회전율(imu_omega) 비교
  로직 추가.
* **헤딩 소스 결정 — 실측 검증으로 설계를 뒤집은 사례**: 처음엔 상식적으로 "IMU 자체 내장
  융합 yaw(가속도+자이로를 자체적으로 결합해 계산한 값)가 원시 자이로를 그냥 적분하는 것보다
  안정적일 것"이라 예상하고 그렇게 구현했다. 그런데 **검증 방법으로 "제자리로 돌아오는
  루프백 주행"을 설계**해서(직선/원 등 정해진 경로로 나간 뒤 출발점으로 복귀시켜, 오차가
  없다면 최종 위치가 (0,0)에 가까워야 한다는 원리) 실측해보니 시작점 대비 **20.8m나 이탈**하는
  심각한 오차가 나왔다. 원인을 더 파보니 IMU가 자체적으로 계산해 내놓는 융합 yaw가 **자기
  자신의 원시 자이로 방향과도 1초 평균 기준 72%밖에 일치하지 않음**을 발견했다(저가 IMU가
  모터 전류로 인한 자기장 간섭 등으로 내부 지자기 보정이 흔들리는 것으로 추정). **이후 헤딩
  소스를 바퀴 인코더로 추정한 회전율과 원시 자이로 값의 평균(단순 상보 융합)으로 교체** —
  이 두 신호는 서로 독립적인 센서(엔코더 vs IMU)인데도 부호/크기 일치율이 99.3%로 매우
  높아 신뢰할 수 있음을 먼저 확인한 뒤 채택했다.
* **재검증**: 동일한 루프백 테스트를 다시 수행해 이탈 거리가 **20.8m → 0.55m**로 개선됨을
  확인(22m 주행 기준 약 2.5% 오차) — 같은 검증 방법을 "전/후 비교"에도 그대로 재사용해
  설계 변경의 효과를 정량적으로 입증했다.

## 4. 직진 헤딩 자동 유지 (Phase 3)

* 조향이 중립이고 실제로 주행 중이며 라이다 회피가 개입하지 않을 때, 진입 시점의 헤딩을
  목표로 고정하고 **PI 제어**로 좌우 편향을 자동 보정.

**적용 방식 — PI 제어**: 목표 헤딩과 현재 헤딩의 차이(오차)를 계속 관찰하면서, (1) **P(비례)항**
— 오차가 클수록 더 크게 보정하고, (2) **I(적분)항** — 작은 오차가 오래 지속되면(예: 좌우 노면
마찰 차이로 인한 지속적 편향) 그것까지 누적해서 보정을 강화하는 두 요소를 더해 좌우 바퀴 속도에
미세한 트림(보정값)을 얹는 방식이다. 트림 값에는 상한을 둬서 사용자의 의도적 조향 입력을 절대
압도하지 않도록 제한했고, 적분항에도 별도 상한(anti-windup)을 둬 트림이 무한정 커지는 것을 방지.

* 실주행 검증(다양한 노면, 총 20분 이상): 평균 오차 0.48~1.24°, 최대 오차 10° 이내, 트림
  값은 상한(±0.15 rad/s)의 최대 38%까지만 사용 — 포화·진동 없이 안정적으로 수렴.

## 5. 전신 슬립 감지 (Phase 4)

**적용 방식 — 독립 신호 교차검증**: 기존 걸림/들뜸 판정은 "지령 RPM 대비 실제 RPM"과
"전류"만으로 개별 바퀴 상태를 추정하는데, 이 두 값 모두 바퀴 자체(엔코더/전류 센서)에서
나오는 신호라 만약 4바퀴가 동시에(예: 빙판·젖은 잔디) 헛돌면 "다들 정상적으로 지령을
따라가는 것처럼" 보여 잡아내지 못하는 사각지대가 있다. 그래서 **바퀴 시스템과 완전히
독립적인 IMU(관성 센서)로 실제 차체가 얼마나 돌고 있는지를 직접 재서 지령값과 비교**하는
이중 안전망을 추가했다: 지령 회전율과 IMU 실측 회전율의 비율이 임계값 밑으로 일정 시간
지속되면 "전신 슬립"으로 판정해 라이다 회피의 감속 패턴과 동일한 방식으로 속도를 일시적으로
낮춘다. 제자리 회전은 원래도 지령보다 덜 도는 게 정상(스크럽 마찰)이라, 오탐을 피하기 위해
"거의 안 도는" 극단적인 경우만 잡히도록 문턱값을 보수적으로 설정.

* 18분 실주행 중 11회, 0.4%의 틱에서만 발동 — 오탐 거의 없음. 가장 긴 발동 사례(1.68초)를
  분석한 결과, 고속 주행 중 급선회 시 관성으로 인한 언더스티어(로봇이 거의 안 도는 상황)를
  정확히 감지해 속도를 절반으로 낮췄고, 그 직후 실제로 회전이 정상적으로 회복되는 것을 확인
  — 설계 의도대로 작동.

## 6. 킨매틱스 실측 캘리브레이션 — 제자리 회전 성능 대폭 개선 (Phase 5a)

### 6.1 원인 진단

제자리 회전이 지령한 만큼 돌지 않는 문제의 원인을 로그로 추적한 결과, 두 가지가 확인됨:

1. 회전 시 반력 토크로 인한 대각선 방향 하중 이동(FL+RR 또는 FR+RL 바퀴 쌍이 동시에
   가벼워짐)이 정상적인 물리 현상인데, 기존 "들뜸(바퀴가 지면에서 떨어짐)" 판정 로직이 이를
   오탐하여 목표 속도를 0으로 꺼버려 4륜 중 사실상 2륜만 구동되는 상황이 전체 회전 틱의
   **38%**에서 발생.
2. 회전 반경 계산에 쓰이는 트랙폭 계수(`k_skid`, `effective_w`)가 기하학적 공식값으로,
   실제 스킷스티어 특유의 스크럽 마찰(바퀴가 옆으로 긁히며 도는 저항)을 반영하지 못해
   지령보다 작은 차동 속도를 걸고 있었음.

### 6.2 조치 및 결과

* 제자리 회전 중에는 "들뜸" 판정으로 목표 속도를 꺾는 개입을 비활성화(판정 표시는 유지).

**적용 방식 — 실측 역산(파라미터 피팅)**: 기존 `k_skid`/`effective_w`는 로봇의 트랙폭·휠베이스
치수만으로 계산한 순수 기하학적 공식값이었는데, 실제 지면에서는 바퀴가 옆으로 긁히는 저항
때문에 이 공식이 가정하는 것보다 더 큰 차동을 걸어줘야 같은 회전 속도가 나온다. 이를 정량화하기
위해 매 틱 로그에 이미 기록되고 있던 "바퀴 인코더로 계산한 회전율"과 "IMU로 실측한 실제
회전율"을 이용해, 공식을 거꾸로 풀어서(예: `유효계수 = 바퀴 기반 회전율 추정치 × 기존계수 ÷
IMU 실측 회전율`) **틱마다 "실제로 맞았어야 할 계수값"을 역산**했다. 이렇게 나온 수천 개의
값 중 이상치 영향을 줄이기 위해 평균이 아닌 **중앙값**을 대표값으로 채택했고, 서로 다른 날
수집한 **3차례의 독립적인 실주행 로그**에서 반복 계산해도 값이 거의 같게 수렴하는 것을
확인해 신뢰도를 높였다. 회전 반경별로 값을 나눠 보면 완만한 커브(0.44)와 급한 커브(0.53)
사이에서 계수가 변하는 경향도 확인했는데, 정적 상수 하나로는 이 편차를 완전히 없앨 수
없어 실용적인 절충값(중앙값)을 채택 — 이걸 실시간으로 자동 보정하는 방식(Phase 5b)도
검토했으나, 슬립이 발생하는 바로 그 순간에 추정 신호 자체가 같이 왜곡되는 구조적 문제가
있어 득보다 실이 크다고 판단해 보류했다. 아래 값으로 최종 수렴:

| 파라미터 | 기존(기하학적 공식) | 실측 캘리브레이션 |
| :--- | :--- | :--- |
| `k_skid` (커브 선회) | 0.406 | **0.51** |
| `effective_w` (제자리 회전) | 0.684 | **0.87** |

* **최종 성능**: 제자리 회전 시 지령 대비 실제 회전율(IMU 실측 기준)이 **44~58% → 93.6%**로
  개선. 완전히 100%에 도달하지 못하는 잔여 오차는 스킷스티어 구조 특유의 물리적 한계(4륜
  전부가 옆으로 긁히며 돌아야 하는 구조상 슬립 없이는 방향 전환이 불가능)로 판단되며, 현재
  수준이 이 하드웨어에서 실용적으로 기대 가능한 한계에 근접한 것으로 평가.
* 실시간 온라인 적응형 보정(Phase 5b)은 추가 정확도 이득 대비 구현/검증 리스크가 크다고
  판단해 보류 결정(근거는 06번 문서 참고).

## 7. 모터/드라이버 온도 모니터링

* 기존엔 사용하지 않던 드라이버 레지스터(모터 온도 `0x20A4`, 드라이버 온도 `0x20B0`)를 읽어
  HUD/CSV에 실시간 노출.

**적용 방식**: 드라이버와의 통신은 매 제어 주기(약 27Hz)마다 이루어지는데, 통신 1회를 늘릴
때마다 제어 루프 지연이 커진다. 모터 온도 레지스터(`0x20A4`)는 마침 기존에 읽고 있던
레지스터 범위(`0x20A5`~) 바로 앞이라, **읽는 레지스터 개수만 하나 늘려서 통신 횟수 추가 없이**
얻어냈다. 반면 드라이버 자체 온도(`0x20B0`)는 기존 읽기 범위와 떨어져 있어 합치면 통신이
늘어나는데, 온도는 물리적으로 초 단위로 천천히 변하는 값이라 매 틱 갱신할 필요가 없다는 점을
이용해 **약 1.1초에 한 번만 별도로 폴링**하는 저빈도 방식을 택했다 — 정확도 손실 없이 통신
비용을 최소화한 설계.
* 60℃ 이상 시 HUD에 경고 표시.

## 8. 진단만 완료, 조치는 보류 중인 이슈

* **급정지 시 미세한 요잉(회전) 현상**: 처음엔 "조이스틱을 놓을 때 튕기는 물리적 반동" 가설을
  세우고 로그의 RC 원시값 변화를 직접 확인했으나, 해당 구간에서 스틱 값은 매끄럽게 중립으로
  복귀했을 뿐 반대 방향으로 튀는 값은 전혀 없어 이 가설은 기각했다. 대신 정지 이벤트마다
  전후 바퀴 명령/피드백/상태 플래그를 시간순으로 대조해본 결과 두 가지 원인을 확인:
  (1) 거친 노면에서 정지 직전 특정 바퀴가 순간적으로 "들뜸" 판정되어 다른 바퀴와 감속 타이밍이
  어긋나는 것, (2) 드라이버 자체의 속도 제어 루프가 급정지 시 0을 살짝 지나쳤다 돌아오는
  오버슈트(4바퀴가 0을 지나치는 타이밍이 서로 달라 그 사이 짧게 회전 모멘트가 생김). 감속
  속도를 낮추면 완화될 여지가 있으나 "즉각 정지" 체감이 희생되는 트레이드오프가 있어 적용
  여부는 보류 중.

## 9. 자동화 및 진단 인프라

* 매 주행마다 `logs/` 폴더에 타임스탬프 CSV 자동 저장(수동으로 `--log` 지정할 필요 없음) —
  RC/IMU/모터 피드백/오도메트리/헤딩홀드/슬립감지/온도까지 전 항목 기록, 이번 보고서의 모든
  정량적 검증이 이 로그를 근거로 이루어짐.

## 10. 향후 계획

라이다(Livox Mid-360S) 기반 SLAM/자율주행 확장 계획은
[07-lidar-slam-plan.md](07-lidar-slam-plan.md)에 별도 정리되어 있음(아직 미착수).

## 종합 결과 요약

| 항목 | 개선 전 | 개선 후 |
| :--- | :--- | :--- |
| 조종기 | Xbox/SDL2 (유선) | RadioMaster Pocket + XR1 (무선 RC) |
| 브레이크 | 자연 정지 시에도 자동 잠김 | CH5로만 제어 |
| 헤딩 인지 | 없음 | 오도메트리(x,y,θ) + 자동 직진 유지 |
| 제자리 회전 정확도(지령 대비) | 44~58% | 93.6% |
| 슬립 감지 | 개별 바퀴 전류 기반만 존재 | + IMU 기반 전신 슬립 이중 감지 |
| 모터 상태 가시성 | 전류만 | + 온도(모터/드라이버) 실시간 감시 |
| 주행 기록 | 수동 `--log` 필요 | 매 주행 자동 저장 |
