4 Commits

Author SHA1 Message Date
robin f75570b66b update 2026-08-19 11:32:40 +09:00
robin 45a888664b Merge main: snap wheel command to target on landing after airborne
Brings in the airborne-landing fix from main. The previously
committed ARM binary was built against the pre-fix source, so it's
dropped again here rather than carried forward stale — same
build-on-target convention as before: run_4wd.sh will compile a
fresh aarch64 binary on next launch on the Jetson. Build and commit
that binary back to this branch afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 13:22:54 +09:00
robin bb5a205df1 Fix curve-steering wheel-reversal at partial throttle
Steering angular velocity (omega) was scaled by the constant max
speed (max_v) instead of the actual current forward speed (v_x).
This decoupled turn tightness from throttle position: at low/medium
throttle, a moderate left-stick steering input already produced an
omega large enough to flip the inner wheel's commanded velocity
negative, causing the robot to spin/reverse/lurch forward instead of
smoothly curving. Scaling by abs(v_x) keeps turn radius proportional
to speed as intended, matching prior full-throttle behavior while
fixing the reversal at partial throttle.

Also commits the ARM binary rebuilt on-device with this fix, per the
build-on-target convention for this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:54:52 +09:00
robin f2b0968979 jetson-arm: drop x86 build artifact, build fresh on target
The committed xbox_motor_control_cpp binary is x86-64 and won't run
on Jetson's aarch64. run_4wd.sh only rebuilds when the binary is
missing, so removing it here lets the first run on the Jetson
compile a native ARM binary via `make`. Commit that binary back to
this branch after building on-device to keep an ARM build tracked
here, same as main tracks the x86 one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:15:23 +09:00
31 changed files with 201 additions and 2987 deletions
Vendored
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
logs/
+1 -1
View File
@@ -1,6 +1,6 @@
CXX = g++
CXXFLAGS = -O3 -std=c++17 -Wall -Wextra -I/usr/local/include
LIBS = -lpthread -llivox_lidar_sdk_shared
LIBS = -lSDL2 -lpthread -llivox_lidar_sdk_shared
TARGET = xbox_motor_control_cpp
SRC = xbox_motor_control.cpp
-187
View File
@@ -1,187 +0,0 @@
## A (q1.md에 대한 답변)
`doc/06-imu-integration-plan.md`에 대한 4가지 질문에 순서대로 답한다. 결론을 먼저 요약하면:
**개루프(조이스틱)와 폐루프(IMU 트림)는 상충하지 않으며 실무에서도 계층 구조로 함께 쓰인다.
ICR은 실제로 동적이며 9축 IMU로 온라인 추정이 가능하다. 헤딩 홀드는 상대 헤딩 기준 + 최소한의
필터링으로 충분하다. HWT905는 RS485/Modbus를 지원해 기존 통신 스택 재사용이 가능하다.**
---
### Q1. 조이스틱 폐루프 제어가 실무에서 실제로 쓰이는가?
**결론: 쓰인다. 다만 사용자가 걱정하는 "조이스틱 값이 바뀌면 폐루프 계산을 처음부터 다시 해야
한다"는 문제는 애초에 발생하지 않는다.** 그 걱정 자체가 폐루프 제어에 대한 흔한 오해를 짚고
있어서, 왜 문제가 안 되는지부터 설명한다.
#### 왜 "재계산 문제"가 없는가
폐루프 제어의 기본 형태는 다음과 같다.
```
e(t) = r(t) y(t) // r(t): 기준값(레퍼런스), y(t): 센서 측정값
u(t) = Controller(e(t)) // 그 순간의 오차만 보고 그 순간의 보정량을 계산
```
여기서 `r(t)`(조이스틱이 매 순간 주는 목표값)가 틱마다 바뀌는 것은 폐루프 제어의 정상적인
동작 방식이지, 예외 상황이 아니다. 제어기는 "이전 계산이 끝날 때까지 기다렸다가 다음 값을
반영"하는 게 아니라, **매 사이클(본 프로젝트는 100Hz=10ms)마다 그 순간의 `r(t)``y(t)`만으로
그 순간의 보정량 하나를 계산**한다. 지금 이미 코드에 있는 `jerkLimitedStep()`도 정확히 이 구조다
`target_*`(조이스틱에서 매 틱 갱신되는 레퍼런스)가 바뀌어도 다음 틱에서 그냥 새 `target_*`
기준으로 다시 한 스텝 계산할 뿐, "이전 목표를 향한 계산이 안 끝났으니 못 바꾼다"는 일이 없다.
헤딩 홀드도 동일한 구조로 넣을 수 있다.
#### 이미 이 프로젝트에도 폐루프가 존재한다
"조이스틱 제어 = 개루프"라는 인식은 절반만 맞다. 계층을 나눠보면:
| 계층 | 신호 흐름 | 개/폐루프 |
| :--- | :--- | :-: |
| 상위 (조이스틱 → 목표 RPM) | `ly/lx/rx` → 스키드 스티어 킨매틱스 → `target_fl/fr/rl/rr` | **개루프** (피드포워드) |
| 중위 (목표 RPM → 지령 RPM) | 저크 제한 S-curve (`jerkLimitedStep`) | 개루프 프로파일 생성 (폐루프 아님, 단 매 틱 최신 목표 반영) |
| **하위 (지령 RPM → 실제 RPM)** | **드라이버 내장 Velocity Loop** (`0x203C`/`0x203D`/`0x203E` L, `0x206C`/`0x206D`/`0x206E` R — Kp/Ki/Kf) | **이미 폐루프.** 부하가 걸려도 드라이버가 실제 RPM을 지령 RPM에 맞추려 함 |
즉 지금도 "조이스틱 값 → 목표 속도"까지는 개루프지만, "목표 속도 → 실제 바퀴 속도"는 이미
드라이버가 폐루프로 하고 있다. IMU 헤딩 홀드(계획서 Phase 3)는 이 구조에 **한 계층을 더
얹는 것**뿐이다: "조향 입력이 중립일 때, 목표 요레이트(=0)와 실측 요레이트(IMU)의 오차를 보고
좌/우 목표 RPM에 작은 트림을 더한다." 조이스틱이 다시 움직이면(조향 입력 재개) 그 즉시 헤딩
락을 풀고 트림을 0으로 되돌리면 되므로, "조이스틱 값 변경 시 재계산" 문제는 설계상 발생하지
않는다.
#### 실무 사례 (동일 패턴)
* **자동차 ESC/TCS(전자식 주행안정장치)**: 운전자의 스티어링·가속페달이 항상 최우선 레퍼런스지만,
요레이트 센서가 실측한 값이 스티어링 각도로부터 기대되는 값과 어긋나면 개별 바퀴 브레이크/토크를
트림한다. 운전자가 스티어링을 계속 바꿔도 문제없이 동작 — 매 사이클 그 순간의 편차만 본다.
* **쿼드콥터**: 스틱은 각속도/자세에 대한 레퍼런스이고, 내부 자세제어 루프가 IMU로 안정화한다.
스틱을 중립으로 놓으면 자세/포지션 홀드로 전환되어 바람 등 외란을 자동 보정 — Phase 3와 정확히
동일한 "중립 시에만 홀드" 패턴.
* **선박 오토파일럿**: 조타핸들이 중립일 때만 자이로컴퍼스 기준 헤딩홀드가 개입하고, 조타를
움직이면 즉시 새 헤딩으로 재설정된다.
**요약**: 실무의 표준 패턴은 "조이스틱 = 상위 레퍼런스(운전자 의도, 매 틱 갱신 가능) + 하위
폐루프(그 레퍼런스를 추종·안정화)"의 계층 구조다. 계획서 Phase 3가 제안하는 헤딩 홀드도 이
패턴을 그대로 따른다.
---
### Q2. ICR이 동적이라면, IMU로 이를 감지해 더 고차원의 제어가 가능한가?
**결론: 가능하다. 그리고 지적한 대로 현재 로직은 "동적으로 변하는 현상을 정적 스칼라로 땜빵"한
것이 맞다.**
#### 현재 로직이 "땜빵"인 이유 (재확인)
`k_skid`, `effective_w`는 로봇 형상(트랙폭, 축거)만으로 계산된 기하학적 근사치이고,
`spin_arc_bias=0.12`는 실측 없이 경험으로 넣은 상수다. 이 값들은 지면 마찰계수, 무게중심,
경사, 타이어 트레드 마모, 속도에 따라 실제로는 계속 바뀌는 "유효 ICR"을 대표할 수 없다
— 특정 노면/속도 조건에서 튜닝된 값일 뿐이다.
#### 9축 IMU가 추가하는 정보 차원
| IMU 축 | 얻는 정보 | 현재(전류+엔코더)로는 불가능한 이유 |
| :--- | :--- | :--- |
| 자이로 요레이트 | 로봇이 **실제로** 얼마나 회전하고 있는가 (슬립 무관) | 전류는 부하/토크 정보, 엔코더는 개별 바퀴 회전량 정보일 뿐 — 차체가 실제로 얼마나 돌았는지는 아무도 안 알려준다 |
| 가속도계 (중력보정 후) | 급조향/급가감속 시 차체가 실제로 그만큼 가속했는가 | 저크 리미터가 만드는 "기대 가속도"와 비교해 "명령은 갔는데 차체가 안 따라옴" 상태(전신 슬립)를 검출 가능 |
| Roll/Pitch(경사각) | 현재 지형이 경사면인지 | 경사에서는 좌우 바퀴 접지압이 비대칭이 되어 ICR이 더 크게 틀어짐 — 지금은 이 정보 자체가 전무 |
| 지자기 | 절대 방위 참조 (제한적) | 다만 차체가 모터/드라이버/배터리 등 금속 덩어리라 지자기 왜곡이 클 가능성이 높음 — 절대 헤딩보다는 참고용으로만 신중히 사용 권장 |
#### "정적 캘리브레이션"보다 한 단계 더 — 온라인 ICR 추정
계획서 Phase 5(§6.4)는 원래 "몇 차례 실측 후 한 번 피팅해서 상수를 교체"하는 **정적**
캘리브레이션으로 설계했다. 하지만 지적한 대로 ICR이 지형에 따라 계속 바뀐다면, 매 틱
```
effective_ICR_gain = 실측_요레이트(IMU) / 지령_차동속도
```
를 계속 갱신하는 **온라인 추정**(Pentzer 2014가 제안한 방식)이 이론적으로는 더 맞는 방향이다.
다만 이건 Phase 5를 정적 캘리브레이션 → 상시 온라인 추정으로 확장하는 것이므로, 구현 난이도와
실외 검증 부담이 커진다. 이번 답변을 반영해 계획서 §6.4 Phase 5에 "1차: 정적 캘리브레이션으로
먼저 검증 → 2차: 온라인 추정으로 확장 가능"이라는 단계를 추가해 두었다(아래 "계획서 반영"
참고).
#### 우선순위: 조이스틱 제어가 먼저라는 원칙과의 정합성
이 답변에서 제안하는 내용은 계획서의 Phase 순서를 바꾸지 않는다. Phase 1~2(§6.4)는 IMU 값을
"관측/로깅"만 할 뿐 제어에 전혀 개입하지 않으므로, 조이스틱 제어 자체의 완성도에는 영향을 주지
않는다. ICR 온라인 추정 같은 고차원 활용은 Phase 5 이후, 즉 조이스틱 경로가 충분히 검증된
다음에야 의미가 있는 단계로 이미 순서가 잡혀 있다.
---
### Q3. 헤딩 기준을 IMU로 얻어 직진 편향 보정이 가능한가? 필터는?
**결론: 맞다. 그리고 이 용도에서는 "정밀한 절대 헤딩"이 아니라 "짧은 구간 동안의 상대 헤딩"만
있으면 되기 때문에, 필터링 요구 수준이 생각보다 무겁지 않다.**
#### 왜 절대 헤딩이 필요 없는가
Phase 3(§6.4)의 헤딩 홀드는 "조향 입력이 중립인 동안 로봇이 옆으로 틀어지지 않게" 하는 용도다.
조향 입력이 다시 들어오면(사용자가 스틱을 움직이면) 헤딩 락을 풀고, 다시 중립이 되는 시점에
**그 순간의 방향을 새 기준으로 재설정(reset)**하면 된다. 즉 각 "직진 구간"은 보통 수 초~수십 초
단위이므로, 자이로 바이어스 드리프트가 누적될 시간 자체가 짧아 실사용상 큰 문제가 되지 않는다.
(장기 절대 헤딩이 필요해지는 건 계획서 Phase 6의 자율주행/맵핑 단계부터다.)
#### 그래도 필요한 최소한의 처리
1. **정적 바이어스 캘리브레이션**: 전원 인가 직후 정지 상태에서 수 초간 자이로 값을 평균 내
영점(zero-rate offset)을 구해 빼준다. 이거 없이는 정지 중에도 헤딩이 서서히 흘러간다.
2. **노이즈 필터링**: 실외 오프로드 특성상 진동이 크다. 저역통과 또는 이동평균 정도로 고주파
진동 성분을 제거해야 트림 제어기가 노이즈에 반응해 떨지 않는다.
3. **HWT905류 온보드 AHRS 모듈을 쓰면 위 부담이 상당 부분 줄어든다**: 이 모듈은 가속도+자이로
+지자기를 내부적으로 칼만필터 등으로 융합해 이미 안정화된 roll/pitch/yaw를 직접 출력한다.
순수 MEMS 칩(raw 자이로 값만 주는 IC)을 쓰는 것보다 애플리케이션 쪽 필터링 구현 부담이
훨씬 적다 — Q4에서 이어서 설명.
---
### Q4. IMU 하드웨어: HWT905 확정
WitMotion 공식 자료 기준으로 확인한 HWT905-RS485 스펙 (정식 PDF 매뉴얼은 이미지 기반이라
텍스트 자동 추출이 안 돼, 아래는 제조사 웹페이지 기준 요약이다 — 레지스터 맵 등 세부 사항은
§실행 전 확인 필요 참고):
| 항목 | 내용 |
| :--- | :--- |
| 측정 축 | 9축 — 가속도 3축 + 자이로 3축 + 지자기 3축, 내장 자세융합 알고리즘으로 roll/pitch/yaw 직접 출력 |
| 통신 | RS485, **Modbus 프로토콜 지원**(레지스터 리스트 기반) |
| 보드레이트 | 기본 9600bps, 2400~921600bps 가변 |
| 출력 레이트 | 최대 200Hz |
| 정확도 | 제조사 공식 스펙 "0.05°" (동적/정적 조건은 정식 PDF 확보 후 재확인 필요) |
| 케이스 | 55×36.8×24mm 알루미늄, **IP67 방수/방진** |
| 케이블 | RS485 최대 30m |
#### 이 프로젝트에 잘 맞는 이유
1. **RS485 + Modbus 프로토콜**`xbox_motor_control.cpp``SerialPort` 클래스(Modbus RTU,
CRC16, `readReg`/`writeReg`/`readRegs` 패턴)를 상당 부분 재사용할 수 있다. IMU만을 위한
완전히 새로운 통신 스택을 만들 필요가 없다. (단, ZLAC8015D 2대가 이미 쓰고 있는 버스에
같이 물릴지, 별도 물리 포트로 분리할지는 §실행 전 확인 필요에서 결정해야 한다 — 100Hz
루프에서 통신 3개 슬레이브를 한 버스로 돌리면 틱당 통신 시간이 늘어날 수 있다.)
2. **IP67 방수/방진** — 실외 로봇이라는 조건에 부합.
3. **온보드 AHRS 융합** — Q3에서 설명한 필터링 부담을 하드웨어가 상당 부분 대신 처리해준다.
저가 MEMS 칩 대비 확실한 이점.
#### 실행 전 확인 필요 (아직 확정 못한 것)
이번 조사에서 확보한 HWT905 정식 매뉴얼 PDF는 이미지 스캔본이라 텍스트 추출이 되지 않아,
**정확한 레지스터 주소(각속도/각도/가속도/지자기 레지스터, 출력레이트 설정 레지스터, 슬레이브
주소 설정 방법 등)는 이번 문서에 포함하지 못했다.** ZLAC8015D 때와 동일한 방식으로 진행하려면:
1. HWT905 정식 매뉴얼(텍스트 검색 가능한 PDF)을 `resource/`에 추가.
2. `doc/07-imu-hwt905-spec.md`(가칭)로 레지스터 맵을 ZLAC8015D 문서와 동일한 정밀도로 문서화.
3. 그 다음에야 Phase 1(하드웨어 연동) 코드 작업 착수.
---
## 계획서(`doc/06-imu-integration-plan.md`) 반영 사항
이 답변 내용을 반영해 계획서를 다음과 같이 갱신했다:
* §6.7 "결정이 필요한 사항" 중 **IMU 하드웨어 항목을 HWT905로 확정** 표시.
* Phase 5에 "정적 캘리브레이션 → 온라인 ICR 추정으로 확장 가능" 단계 구분을 추가.
* Phase 1 앞에 "IMU 정식 매뉴얼 확보 및 레지스터 맵 문서화"를 선행 작업으로 명시.
남은 결정 사항: **IMU를 ZLAC8015D와 같은 RS485 버스에 물릴지, 별도 포트로 분리할지** — 이건
실측(버스 하나에 3개 슬레이브 얹었을 때 100Hz 루프의 `comm_ms` 여유가 있는지) 후 결정하는 것을
제안한다.
-11
View File
@@ -1,11 +0,0 @@
## Q
06-imu-integration-plan.md 문서를 읽어보았습니다.
현재 코드를 분석한 결과 제가 알 수 있는 것은 다음과 같았습니다.
- 현재 개루프 텔레옵이다. 하지만 조이스틱으로의 제어를 먼저 완성하려는 입장에서 스키드 스티어 차량의 경우 개루프일 수 밖에 없는 것이 아닌가. 가령 폐루프를 그린다고 하면 폐루프 계산을 진행하고 다음 제어를 할 때 조이스틱의 값이 변경되면 다시 계산해야 할텐데, 나도 이런 제어방식이 있어야 완벽한 조이스틱 제어가 완성될 것이라고 생각한다. 하지만 실무에서 실제로 조이스틱 제어가 폐루프로 이루어지는 지 알고 싶다.
- ICR은 로봇이 있는 곳의 경사 그리고 무게중심, 지형 특성, 바퀴의 트레드 등 여러 요소에 의존되어 다이나믹하게 변할 것이다. 현재 로직은 분석한 문서를 보면, 변화무쌍한 ICR을 고정된 스칼라값을 입력하여 땜빵식으로의 보정을 한 것이라고 이해하였다. 그렇다면 IMU를 추가하여 ICR을 다이나믹하게 감지하여 전류와 현재 회전량 뿐만아니라 9축 IMU를 통해 고차원의 제어가 가능한 것인가. 일단 조이스틱 제어가 우선이다.
- 헤딩 기준이 없다고 했는데 헤딩 기준 또한 IMU를 통해 얻을 수 있으므로, 직직시 편향 또한 보정이 가능한 것으로 보인다. 물론 IMU는 필터가 필요할 것이다.
- IMU는 MCU 내장 등 초 저가형은 사용하지 않을 것이며, HWT905 같은 IMU를 사용할 것이다.
-207
View File
@@ -1,207 +0,0 @@
# 01. ZLAC8015D V4 하드웨어 스펙
출처: `resource/ZLAC8015D V4 Series Manual Version 1.03-20251111.pdf`
## 1.1 개요
* 제조사: Shenzhen Zhongling Technology (ZLTECH)
* 허브 서보 모터 전용 디지털 AC 서보 드라이버로, **1대가 좌/우 모터 2개(듀얼 채널)를 동시 구동**한다.
* RS485(Modbus RTU) 및 CANopen(CiA301/CiA402) 버스 통신을 내장.
* 위치/속도/토크 제어 모드를 지원.
* 용도: AGV, 배송 로봇, 서비스 로봇, 자동화 이송 장비 등 — 본 프로젝트에서는 실외 스키드 스티어
4WD 로봇의 좌우 휠 페어 구동에 사용된다.
## 1.2 전기 사양
| 파라미터 | 최소 | 표준 | 최대 | 단위 |
| :--- | :---: | :---: | :---: | :--- |
| 입력 전압 | 20 | 36 | 48 | VDC |
| 출력 전류 (피크) | 0 | 15 | 30 | A |
| 제어 신호 입력 전류 | 7 | 10 | 16 | mA |
| 과전압 보호 | - | 75 | - | VDC |
| 저전압 보호 | - | 16 | - | VDC |
| 입력 신호 전압 | - | 5 | - | VDC |
| 절연 저항 | 18 | 20 | - | MΩ |
> 채널당 정격전류 150(=15.0A), 최대전류 300(=30.0A)이 레지스터 `0x2033/0x2034`(좌),
> `0x2063/0x2064`(우)의 공장 기본값이며, 위 표의 15A/30A와 일치한다.
> 자세한 내용은 [05-error-troubleshooting.md](05-error-troubleshooting.md)의 전류 한계 절 참고.
## 1.3 환경 사양
| 항목 | 내용 |
| :--- | :--- |
| 냉각 방식 | 자연 냉각 또는 강제 냉각 |
| 사용 장소 | 먼지, 유분(oil mist), 부식성 가스 회피 |
| 동작 온도 | 0 ~ 50 ℃ |
| 최대 습도 | 90% RH (결로 없음) |
| 보관 온도 | -10 ~ 70 ℃ |
| 진동 | 10~55Hz / 0.15mm |
실외 로봇에 탑재되므로 방진/방수 인클로저 내부에 설치하고, 방열이 충분한 방향(narrow side,
방열판 면)으로 장착할 것을 매뉴얼이 권장한다. 주변 온도가 60℃를 넘는 밀폐/금속 분진 환경은
피해야 한다.
## 1.4 외형/설치 치수
* 크기: 150 × 97 × 31mm 내외 (방열판 폭 제외 케이스 기준, `resource/ZLAC8015D V4 Series.pdf` 도면 참고)
* M3 나사로 4모서리(와이드 사이드) 또는 양측면(네로우 사이드) 고정. 방열을 위해 네로우 사이드
설치 권장.
* 장착 시 진동/충격 보호 필요 (차체에 완충 마운트 권장).
## 1.5 커넥터 핀맵
### J1/J2 — 좌측 모터 전원 (5핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1 | DC | 전원 입력 24-48V |
| 2 | GND | 전원 GND |
| 3 | U | 좌모터 U상 |
| 4 | V | 좌모터 V상 |
| 5 | W | 좌모터 W상 |
### 우측 모터 전원 (5핀, 핀 순서 반대)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 5 | GND | 전원 GND |
| 4 | DC | 전원 입력 24-48V |
| 3 | W | 우모터 W상 |
| 2 | V | 우모터 V상 |
| 1 | U | 우모터 U상 |
전원은 좌/우 커넥터 중 한쪽 또는 양쪽 동시 입력이 가능하다(퀵스타트 가이드 확인).
### J2/J6 — 좌/우 모터 인크리멘탈 엔코더 & 홀 (12핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1-4 | iA+/iA-/iB+/iB- | 인크리멘탈 엔코더 A/B상 |
| 5-6 | RTC+/RTC- | 온도 센서 |
| 7-8 | V/W (Hall) | 홀 센서 |
| 9 | U (Hall) | 홀 센서 |
| 10 | GND | 전원 GND |
| 11 | VCC | 엔코더/홀 전원 출력 |
| 12 | GND | 전원 GND |
### J3 — 모터 제어 신호 포트 (8핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1 | BGND-L | 좌 브레이크 전원- |
| 2 | -BR-L | 좌 브레이크- |
| 3 | BDC-L | 좌 브레이크 전원+/브레이크+ |
| 4 | BGND-R | 우 브레이크 전원- |
| 5 | -BR-R | 우 브레이크- |
| 6 | BDC-R | 우 브레이크 전원+/브레이크+ |
| 7 | OUTPUT1 | 내부 풀업 5V 출력 (CAN/RS485로 기능 설정 가능) |
| 8 | OUTPUT2 | 내부 풀업 5V 출력 (CAN/RS485로 기능 설정 가능) |
전자식 브레이크는 레지스터 `0x201A`(B0, 좌)/`0x201B`(B1, 우)로 제어한다
(`0`=해제, `1`=잠금). 코드의 `MotorDriver::setBrakes()`가 이 두 레지스터에 동시에 쓴다.
### J4 — 모터 제어 신호 포트 (8핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1 | AOUT-R | 우모터 엔코더 A 출력 |
| 2 | BOUT-R | 우모터 엔코더 B 출력 |
| 3 | AOUT-L | 좌모터 엔코더 A 출력 |
| 4 | BOUT-L | 좌모터 엔코더 B 출력 |
| 5 | +5V | 엔코더 5V 출력 (<100mA) |
| 6 | GND | 엔코더 GND |
| 7 | INPUT1 | 프로그래머블 입력 (기본 비상정지 등으로 설정 가능) |
| 8 | INPUT2 | 프로그래머블 입력 |
INPUT1/INPUT2 펄스폭은 10ms 이상 유지해야 드라이버가 정상 인식한다. 입력 레벨은 기본 5V이며
12V 사용 시 1kΩ/0.5W, 24V 사용 시 2kΩ/0.5W 전류제한 저항을 외부에 추가해야 한다.
### J5 — 통신 포트 (5핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1 | CANH | CAN |
| 2 | CANL | CAN |
| 3 | SGND | 통신 GND |
| 4 | A | RS485 A |
| 5 | B | RS485 B |
**본 프로젝트는 이 중 SGND/A/B 3선을 사용해 RS485 Modbus RTU 통신을 한다.** 통신선은
차폐 트위스트페어 사용 및 접지 연결을 매뉴얼이 권장한다.
### eRS-R/eRS-L — 절대값 엔코더 인터페이스 (4핀, 미사용)
RS485 방식 외장 절대값 엔코더용 인터페이스. 본 로봇은 인크리멘탈 엔코더 기반 속도제어만
사용하므로 미배선.
### SW — DIP 스위치 (2핀)
| SW | 기능 |
| :-: | :--- |
| SW1 | CAN 종단저항 선택 |
| SW2 | RS485 종단저항 선택 |
데이지체인 마지막 드라이버에서만 종단저항을 ON 하는 것이 일반적인 RS485 배선 원칙이다.
### RES — 회생 저항 입력 (2핀)
| Pin | 표기 | 기능 |
| :-: | :--- | :--- |
| 1 | R- | 브레이크 저항 |
| 2 | R+ | 브레이크 저항 |
100RPM 이상 속도 또는 급정지/비상정지 기능을 사용할 경우, 급감속 시 발생하는 역기전력으로부터
드라이버를 보호하기 위해 외부 회생 저항(권장 5~10Ω/100W) 연결을 권장한다. 본 로봇은 최고속도
0.4m/s(둘레 반경 131.5mm 기준 약 29RPM)로 저속 운용이라 리스크는 낮지만, 급제동(decel 150ms)
프로파일을 쓰므로 실외 장시간 운용 시 회생 저항 장착을 검토할 가치가 있다.
## 1.6 상태 LED
* **녹색 LED**: 전원 표시. 전원 인가 시 항상 점등.
* **적색 LED**: 알람 표시. 알람 발생 시 코드 횟수만큼 점멸 후 일시정지, 반복.
| 상태 | 점멸 횟수 | 원인 |
| :-: | :-: | :--- |
| Over-Voltage | 1 | 공급 전압이 최대 정격 초과 |
| Under-Voltage | 2 | 공급 전압이 최소 동작 전압 미만 |
| Over-Current | 3 | 상간 단락 등으로 모터 상전류 초과 |
| Over-Load | 4 | 상전류가 설정 과부하 전류 초과 |
| Current out-of-tolerance | 5 | (예약) |
| Position out-of-tolerance | 6 | 목표 위치와 출력 위치 오차 초과 |
| Speed out-of-tolerance | 7 | 목표 속도와 출력 속도 오차 초과 |
| Internal reference error | 8 | 드라이버 내부 오류 |
| Parameter reading error | 9 | EEPROM 파라미터 읽기 오류 |
| Hall fault | 10 | 홀 케이블 미접속 또는 신호 오류 |
| High motor temperature | 11 | 모터 과열 |
| Encoder error | 12 | 엔코더 케이블 헐거움/핀 순서 오류 |
| Driver overtemperature | 13 | 드라이버 과열 |
| Speed setting error | 14 | 지령 속도가 정격 속도 초과 |
| Mixed error | 15 | 2개 이상 알람 동시 발생 |
RS485로는 이 상태를 레지스터 `0x20A5`(좌)/`0x20A6`(우)에서 비트마스크로 읽을 수 있으며,
`xbox_motor_control.cpp``decodeDriverError()`가 이를 한글 문자열로 변환한다. 상세는
[05-error-troubleshooting.md](05-error-troubleshooting.md) 참고.
## 1.7 배선 요약도 (본 프로젝트 기준)
```
┌─────────────────────────┐
배터리 24-48V ─┤ DC/GND (좌 또는 우 or 양쪽) │
│ │
좌측 허브모터 ─┤ U/V/W (좌) │ 드라이버 #1 (Station ID 1)
│ │ → 전방 좌/우 모터 구동
우측 허브모터 ─┤ U/V/W (우) │
│ │
좌 엔코더/홀 ─┤ J2 │
우 엔코더/홀 ─┤ J6 │
│ │
RS485 A/B/GND ┤ J5 ── (데이지체인) ──────┼──→ 드라이버 #2 (Station ID 2)
│ │ → 후방 좌/우 모터 구동
좌/우 브레이크 ─┤ J3 │
└─────────────────────────┘
/dev/ttyUSB0 (RS485-USB 컨버터)
제어 PC (xbox_motor_control_cpp)
```
-274
View File
@@ -1,274 +0,0 @@
# 02. RS485 통신 프로토콜 & 레지스터 맵
출처: `resource/ZLAC8015D V4 Series RS485 Communication Version 1.06-20251111.pdf`,
`xbox_motor_control.cpp`
## 2.1 시리얼 포트 설정
| 항목 | 값 |
| :--- | :--- |
| 프로토콜 | Modbus RTU |
| 보드레이트 | 115200 bps (기본값, 코드에서 사용) — 9600/19200/38400/57600/128000 선택 가능 |
| 데이터 비트 | 8 |
| 패리티 | None |
| 스톱 비트 | 1 |
| 드라이버 주소 범위 | 1 ~ 127 (본 프로젝트: 드라이버#1=1, 드라이버#2=2) |
`xbox_motor_control.cpp``SerialPort::openPort()`가 termios로 위 설정(8N1, RAW 모드,
VTIME=1 → 0.1s read timeout)을 그대로 구성한다.
## 2.2 Modbus 프레임 포맷
```
[슬레이브 주소 1B][기능코드 1B][데이터 N B][CRC16 2B (Low,High 순)]
```
CRC는 표준 Modbus CRC16(다항식 0xA001, 초기값 0xFFFF) — `calcCRC16()`이 이를 그대로 구현.
### 지원 기능 코드
| 기능 | 코드 | 에러 응답 코드 |
| :--- | :-: | :-: |
| 다중 레지스터 읽기 | `0x03` | `0x83` |
| 단일 레지스터 쓰기 | `0x06` | `0x86` |
| 다중 레지스터 쓰기 | `0x10` | `0x90` |
| 에러 코드 | 이름 | 의미 |
| :-: | :--- | :--- |
| `0x01` | Illegal function code | 지원하지 않는 기능 코드 |
| `0x02` | Illegal data address | 잘못된 레지스터 주소 |
| `0x03` | Illegal data value | 잘못된 데이터 값 |
### 코드 구현 매핑
| 클래스 함수 | 기능 코드 | 용도 |
| :--- | :-: | :--- |
| `SerialPort::readRegs()` | 0x03 | 피드백/상태/전류한계 조회 |
| `SerialPort::writeReg()` | 0x06 | 제어워드/모드/브레이크 등 단일 레지스터 쓰기 |
| `SerialPort::writeRegs()` | 0x10 | 가감속시간, 목표속도(L/R 동시) 쓰기 |
응답 검증은 CRC16 재계산 + 슬레이브 주소 일치 여부로 이루어지며(`writeReg`/`writeRegs`/`readRegs`
모두 동일), 브로드캐스트(`slave==0`)일 때는 응답을 기다리지 않는다(`--bcast` 옵션).
읽기 타임아웃은 25ms(100Hz=10ms 주기 루프 안에서 여유를 둔 값), 쓰기 응답 타임아웃은 50ms로
설정되어 있다.
## 2.3 코드가 실제로 사용하는 레지스터 (초기화/제어/피드백 경로)
| 주소 | 이름 | 방향 | 코드 사용처 |
| :-- | :--- | :-: | :--- |
| `0x200D` | Control mode | W | `initDriver()`: `3` = 속도 제어 모드 고정 |
| `0x200E` | Control word | W | `initDriver()`: `0x0006` 알람 클리어 → `0x0007` Disable → (가감속 설정 후) `0x0008` Enable |
| `0x2080` / `0x2081` | 가속시간 좌/우 | W | `initDriver(acl_ms)``writeRegs(0x2080,{acl,acl})`로 2레지스터 동시 기록 (기본 150ms) |
| `0x2082` / `0x2083` | 감속시간 좌/우 | W | `initDriver(dcl_ms)``writeRegs(0x2082,{dcl,dcl})`로 동시 기록 (기본 150ms) |
| `0x2088` / `0x2089` | 목표속도 좌/우 (RPM, I16, ±3000) | W | `setRPMs()`: 매 틱 `writeRegs(0x2088,{l,r})`로 좌/우 동시 지령 |
| `0x201A` | 출력단자 B0 (좌 브레이크) | W | `setBrakes()`: `0`=해제, `1`=잠금 |
| `0x201B` | 출력단자 B1 (우 브레이크) | W | `setBrakes()`: 좌와 동일 값 동시 기록 |
| `0x2033` / `0x2034` | 좌 정격전류 / 좌 최대전류 (0.1A) | R/W | `readCurrentLimits()`로 조회, `setMaxCurrent()``0x2034`만 기록 |
| `0x2063` / `0x2064` | 우 정격전류 / 우 최대전류 (0.1A) | R/W | 좌와 동일 패턴 (우 채널) |
| `0x20A5` ~ `0x20AE` | 에러코드~토크 (아래 표 참고) | R | `readFeedback()`**10레지스터 연속 읽기**(0x03, count=10)로 한 번에 확보 |
### `readFeedback()`의 10레지스터 연속 읽기 상세 (`0x20A5` 시작, count=10)
| offset | 주소 | 이름 | 코드 내 변수 | 스케일 |
| :-: | :-- | :--- | :--- | :--- |
| 0 | `0x20A5` | 에러코드(좌) | `err_l` | 비트마스크 (그대로) |
| 1 | `0x20A6` | 에러코드(우) | `err_r` | 비트마스크 (그대로) |
| 2 | `0x20A7` | 실제위치 상위16비트(좌) | `val_l` 상위 | `int32_t` 합성 → `l_tick` |
| 3 | `0x20A8` | 실제위치 하위16비트(좌) | `val_l` 하위 | 〃 |
| 4 | `0x20A9` | 실제위치 상위16비트(우) | `val_r` 상위 | `int32_t` 합성 → `r_tick` |
| 5 | `0x20AA` | 실제위치 하위16비트(우) | `val_r` 하위 | 〃 |
| 6 | `0x20AB` | 실제속도(좌), 단위 0.1r/min | `vl` | `× 0.1f``l_fb` (RPM) |
| 7 | `0x20AC` | 실제속도(우), 단위 0.1r/min | `vr` | `× 0.1f``r_fb` (RPM) |
| 8 | `0x20AD` | 실제토크/전류(좌), 단위 0.1A | `tl` | `× 0.1f``l_torque_a` (A) |
| 9 | `0x20AE` | 실제토크/전류(우), 단위 0.1A | `tr` | `× 0.1f``r_torque_a` (A) |
이 한 번의 통신으로 에러코드·엔코더 위치(주행거리 계산용)·속도 피드백·전류(부하 진단용)를
모두 얻기 때문에, 100Hz 루프에서 드라이버 1대당 통신 1회(읽기)+1회(쓰기)만 필요하다.
2대 드라이버 × 2회 = 틱당 최대 4회 왕복, `comm_ms`로 HUD에 실측 소요시간을 표시한다.
> 위치(`0x20A7`~`0x20AA`)는 엔코더 라인수 1024 기준 4체배(quadrature)로 회전당
> `1024 × 4 = 4096`이 아니라, README 기준 실측 `16,384 ticks/rev`를 사용한다
> (ZLLG80ASM250-**4096**-B 모터명의 4096은 엔코더 라인수이며, 4체배 카운팅 시
> `4096 × 4 = 16384`가 된다. `0x2030`/`0x2060` "Encoder line" 파라미터 공장 기본값 1024와는
> 별개로, 실제 장착된 ZLLG80ASM250-4096-B 모터는 라인수 4096 사양이므로 드라이버의
> Encoder line 파라미터가 모터 사양(4096)에 맞춰 설정되어 있어야 `meters_per_tick` 계산이
> 정확하다. 드라이버 실측정격전류가 15A인 것도 이 모터의 정격과 일치.)
## 2.4 에러코드 (`0x20A5` / `0x20A6`)
| 값 | 의미 | 코드 내 한글 표기 |
| :-- | :--- | :--- |
| `0x0000` | 정상 (No error) | (없음) |
| `0x0001` | 과전압 (Over voltage) | 과전압 |
| `0x0002` | 저전압 (Under voltage) | 저전압 |
| `0x0004` | 과전류 (Over current) | 과전류 |
| `0x0008` | 과부하 (Over load) | 과부하 |
| `0x0010` | 전류 이상 (예약) | 전류이상(예약) |
| `0x0020` | 엔코더 오차 (Encoder out of tolerance) | 엔코더오차 |
| `0x0040` | 속도 이상 (예약) | 속도이상(예약) |
| `0x0080` | 기준전압 오류 (Reference voltage error) | 기준전압오류 |
| `0x0100` | EEPROM 오류 | EEPROM오류 |
| `0x0200` | 홀센서 오류 | 홀센서오류 |
| `0x0400` | 모터 과열 | 모터과열 |
| `0x0800` | 엔코더 오류 | 엔코더오류 |
| `0x2000` | 속도설정 오류 (지령속도가 정격속도 초과) | 속도설정오류 |
여러 비트가 동시에 서면 코드가 `"+"`로 이어붙여 표시한다 (`decodeDriverError()`).
드라이버 알람 상세 진단과 대응은 [05-error-troubleshooting.md](05-error-troubleshooting.md) 참고.
## 2.5 제어워드(`0x200E`)와 초기화 시퀀스
| 값 | 의미 |
| :-- | :--- |
| `0x05` | 비상 정지 (Emergency stop) |
| `0x06` | 알람 클리어 (Clear fault) |
| `0x07` | 정지 (Stop / Disable) |
| `0x08` | 활성화 (Enable) |
| `0x10` | 동기 시작 (Start Synchronous, 포지션 모드 전용) |
| `0x11` | 좌측 단독 시작 (Start Left, 포지션 모드) |
| `0x12` | 우측 단독 시작 (Start Right, 포지션 모드) |
`MotorDriver::initDriver()`의 실제 시퀀스 (각 단계 사이 30ms 대기):
1. `0x200E``0x0006` (알람 클리어)
2. `0x200E``0x0007` (Disable)
3. `0x200D``3` (속도 제어 모드)
4. `0x2080`/`0x2081` ← 가속시간 (좌우 동시, 기본 150ms)
5. `0x2082`/`0x2083` ← 감속시간 (좌우 동시, 기본 150ms)
6. `0x200E``0x0008` (Enable)
7. `setBrakes(true)` — 초기 상태는 브레이크 잠금
이 프로젝트는 속도 제어 모드(Profile Velocity Mode)만 사용하며, 포지션/토크 모드 관련 레지스터
(`0x208A`~`0x208F`, `0x2090`/`0x2091` 등)는 사용하지 않는다. 참고용으로 §2.7에 전체 표를 남긴다.
## 2.6 전류 한계 레지스터
| 주소 | 이름 | 단위 | 기본값 | 범위 |
| :-- | :--- | :-: | :-: | :--- |
| `0x2033` | 좌 정격전류 | 0.1A | 150 (15.0A) | 0-150 |
| `0x2034` | 좌 최대전류 | 0.1A | 300 (30.0A) | 0-300 |
| `0x2063` | 우 정격전류 | 0.1A | 150 (15.0A) | 0-150 |
| `0x2064` | 우 최대전류 | 0.1A | 300 (30.0A) | 0-300 |
`readCurrentLimits()`가 시작 시 1회 조회해 콘솔에 출력하고, `--max_current_a` 인자
(기본 20.0A)로 최대전류를 4바퀴 동일하게 낮춘다. 정격전류(150=15A)는 쓰지 않고 최대전류만
변경한다 — 근거와 튜닝 배경은 [05-error-troubleshooting.md](05-error-troubleshooting.md) 참고.
## 2.7 전체 레지스터 주소록 (Address Directory, 매뉴얼 §4 원문 기준)
### 공통 상수 (Left/Right 공통)
| 주소 | 이름 | 접근 | 기본값 | 설명 |
| :-- | :--- | :-: | :-: | :--- |
| `2000h` | Communication offline time | RW/S | 0 | ms, 0-32000. 시간 내 통신 없으면 정지 |
| `2001h` | RS485 Node ID | RW/S | 1 | 1-127 |
| `2002h` | RS485 Baud Rate | RW/S | 2 | 1:128000 2:115200 3:57600 4:38400 5:19200 6:9600 |
| `2003h` | Input signal status | RO | 0 | bit0-1: X0-X1 입력 레벨 |
| `2004h` | Output signal status | RO | 0 | bit0-1: Y0-Y1 출력 레벨 |
| `2005h` | Clear feedback position | RW | 0 | 0 무효, 1 좌, 2 우, 3 좌우 (미저장) |
| `2006h` | Reset absolute zero point | RW | 0 | 0 무효, 1 좌, 2 우, 3 좌우 (미저장) |
| `2007h` | Shaft state after power on | RW/S | 0 | 0 Enable 안 됨/락 안 됨, 1 Enable 안 됨/락 |
| `2008h` | Maximum motor speed | RW/S | 1000 | r/min, 1-1000 |
| `2009h` | Register parameter settings | RW | 0 | 0 무효, 1 공장 초기화 |
| `200Ah` | CAN Node ID | RW/S | 1 | 1-127 |
| `200Bh` | CAN Baud rate | RW/S | 1 | 0:1000 1:500 2:250 3:125 4:100 Kbit/s |
| `200Ch` | Parking mode | RW/S | 0 | 0 Close, 1 Open |
| `200Dh` | Control mode | RW/S | 0 | 0 undefined, 1 위치(상대), 2 위치(절대), 3 속도, 4 토크 |
| `200Eh` | Control word | RW | 0 | §2.5 참고 |
| `200Fh` | Sync/Async control status | RW/S | 0 | 0 동기, 1 비동기 |
| `2010h` | RW 레지스터 EEPROM 저장 여부 | RW | 0 | 0 무효, 1 저장 |
| `2011h` | Quick stop control | RW/S | 5 | 5 정지, 6 감속시간 적용 급정지, 7 감속시간 미적용 급정지 |
| `2012h` | Close operation control | RW/S | 1 | 0 무효, 1 정상정지 |
| `2013h` | Disable control | RW/S | 1 | 0 무효, 1 정지(switch on 상태로) |
| `2014h` | Halt control | RW/S | 1 | 1 정지, 2 감속 급정지, 3 무감속 급정지 |
| `2016h` | Input effective level | RW/S | 0 | bit0 X0, bit1 X1 (0 기본 High, 1 반전 Low) |
| `2017h` | Input X0 function | RW/S | 9 | 0 none, 1-8 NC, 9 비상정지 |
| `2018h` | Input X1 function | RW/S | 0 | 〃 |
| `2019h` | Output effective level | RW/S | 0 | bit0 Y0, bit1 Y1, bit2 B0, bit3 B1 |
| `201Ah` | **출력 B0 (좌 브레이크)** | RW/S | 0 | 0 Open, 1 Close — **코드 사용** |
| `201Bh` | **출력 B1 (우 브레이크)** | RW/S | 0 | 0 Open, 1 Close — **코드 사용** |
| `201Ch` | Output Y0 function | RW/S | 0 | 0 undefined, 1 알람신호, 2 상태신호, 3 목표위치도달(예약) |
| `201Dh` | Output Y1 function | RW/S | 0 | 〃 |
| `201Eh` | 드라이버 온도 보호 임계값 | RW/S | 800 | 0.1℃, 0-1200 |
| `201Fh` | Alarm PWM 처리 방식 | RW/S | 0 | 0 close, 1 open |
| `2020h` | Overload 처리 방식 | RW/S | 0 | 0 close, 1 open |
| `2021h` | I/O 비상정지 처리 모드 | RW/S | 0 | 0 축고정(우선I), 1 댐핑(우선II), 2 축해제(우선III); low8=INPUT1 high8=INPUT2 |
| `2022h` | Given speed resolution | RW/S | 1 | 1:1RPM ... A:0.1RPM |
| `2023h` | Velocity overshoot | RW/S | 1 | 0 close, 1 open |
| `2024h` | Regen resistance value | RW/S | 50 | 0.1Ω, 0-1000 |
| `2025h` | Regen resistance power | RW/S | 100 | W, 0-1000 |
| `2026h` | Regen opening voltage | RW/S | 650 | 0.1V, 360-750 |
| `2027h` | Regen close voltage | RW/S | 600 | 0.1V, 310-700 |
| `2028h` | Regen function control | RW/S | 1 | 0 close, 1 open |
| `2029h` | Default running direction | RW/S | 0 | 0 CW, 1 CCW |
### 좌모터 파라미터 (`2030h`~`204Ah`) / 우모터 파라미터 (`2060h`~`207Ah`, 좌+0x30 오프셋)
| 좌 주소 | 우 주소 | 이름 | 접근 | 기본값 | 설명 |
| :-- | :-- | :--- | :-: | :-: | :--- |
| `2030h` | `2060h` | Encoder line | RW/S | 1024 | 0-4096 |
| `2031h` | `2061h` | Hall offset angle | RW/S | 0 | 1°, -360~360 |
| `2032h` | `2062h` | Overload factor | RW/S | 200 | %, 0-300 |
| `2033h` | `2063h` | **정격전류** | RW/S | 150 | 0.1A, 0-150 — **코드 사용** |
| `2034h` | `2064h` | **최대전류** | RW/S | 300 | 0.1A, 0-300 — **코드 사용** |
| `2035h` | `2065h` | Overload protection time | RW/S | 300 | 10ms, 0-6553 |
| `2036h` | `2066h` | Position following error threshold | RW/S | 409 | 10counts, 1-6553 |
| `2037h` | `2067h` | Velocity smoothing factor | RW/S | 1000 | 0-30000 |
| `2038h` | `2068h` | Current Loop Kp | RW/S | 600 | 0-30000 |
| `2039h` | `2069h` | Current Loop Ki | RW/S | 300 | 0-30000 |
| `203Ah` | `206Ah` | Feedforward output smoothing | RW/S | 100 | 0-30000 |
| `203Bh` | `206Bh` | Torque output smoothing | RW/S | 100 | 0-30000 |
| `203Ch` | `206Ch` | Velocity Loop Kp | RW/S | 500 | 0-30000 |
| `203Dh` | `206Dh` | Velocity Loop Ki | RW/S | 100 | 0-30000 |
| `203Eh` | `206Eh` | Velocity Loop Kf | RW/S | 500 | 0-30000 |
| `203Fh` | `206Fh` | Position Loop Kp | RW/S | 100 | 0-30000 |
| `2040h` | `2070h` | Position Loop Kf | RW/S | 50 | 0-30000 |
| `2043h` | `2073h` | Initial velocity (속도모드) | RW/S | 1 | r/min, 1-250 |
| `2044h` | `2074h` | Initial velocity (위치모드) | RW/S | 1 | r/min, 1-250 |
| `2045h` | `2075h` | Motor poles | RW/S | 15 | 4-64 |
| `2046h` | `2076h` | Over temperature threshold | RW/S | 800 | 0.1℃, 0-1200 |
| `2047h`-`204Ah` | `2077h`-`207Ah` | Velocity observer coefficients 1-4 | RW/S | 1000/750/350/1000 | 0-30000 |
### 제어 파라미터 (`2080h`~`2091h`)
| 주소 | 이름 | 접근 | 기본값 | 설명 |
| :-- | :--- | :-: | :-: | :--- |
| `2080h` | **S자 가속시간(좌)** | RW/S | 500ms | 0-32767ms — **코드 사용 (150ms)** |
| `2081h` | **S자 가속시간(우)** | RW/S | 500ms | 0-32767ms — **코드 사용 (150ms)** |
| `2082h` | **S자 감속시간(좌)** | RW/S | 500ms | 0-32767ms — **코드 사용 (150ms)** |
| `2083h` | **S자 감속시간(우)** | RW/S | 500ms | 0-32767ms — **코드 사용 (150ms)** |
| `2084h` | 급정지 감속시간(좌) | RW/S | 10ms | 0-32767ms |
| `2085h` | 급정지 감속시간(우) | RW/S | 10ms | 0-32767ms |
| `2086h` | 토크 슬로프(좌) | RW/S | 300ms | mA/s(1/1000초 단위) |
| `2087h` | 토크 슬로프(우) | RW/S | 300ms | 〃 |
| `2088h` | **목표속도(좌)** | RW | 0 | I16, r/min, ±3000 — **코드 사용, 매 틱 기록** |
| `2089h` | **목표속도(우)** | RW | 0 | I16, r/min, ±3000 — **코드 사용, 매 틱 기록** |
| `208Ah`/`208Bh` | 목표위치 상/하위(좌) | RW | 0 | 포지션 모드 전용 (미사용) |
| `208Ch`/`208Dh` | 목표위치 상/하위(우) | RW | 0 | 포지션 모드 전용 (미사용) |
| `208Eh`/`208Fh` | 최대속도(좌/우, 포지션모드) | RW/S | 120 r/min | 포지션 모드 전용 (미사용) |
| `2090h`/`2091h` | 목표토크(좌/우) | RW | 0 | mA, ±30000 — 토크 모드 전용 (미사용) |
### 읽기 전용 상태 (`20A0h`~`20B0h`)
| 주소 | 이름 | 단위/범위 | 코드 사용 여부 |
| :-- | :--- | :--- | :--- |
| `20A0h` | Software version | - | 미사용 |
| `20A1h` | Bus voltage | 0.01V | 미사용 (배터리 전압 모니터링에 활용 가능) |
| `20A2h` | Status word | L:bit7-6, R:bit15-14 → 00 해제/40 축고정/80 비상정지/C0 알람; bit0(L)/bit8(R) 0정지 1구동 | 미사용 |
| `20A3h` | Hall input state | 0-7 (0,7이면 홀 오류), high8=좌 low8=우 | 미사용 |
| `20A4h` | Motor temperature | 1℃, -55~120, high8=좌 low8=우 | 미사용 (과열 사전감지에 활용 가능) |
| `20A5h` | **에러코드(좌)** | 비트마스크 (§2.4) | **코드 사용** |
| `20A6h` | **에러코드(우)** | 비트마스크 (§2.4) | **코드 사용** |
| `20A7h`/`20A8h` | **실제위치 상/하위(좌)** | counts, I16×2 | **코드 사용**`l_tick` |
| `20A9h`/`20AAh` | **실제위치 상/하위(우)** | counts, I16×2 | **코드 사용**`r_tick` |
| `20ABh` | **실제속도(좌)** | 0.1r/min | **코드 사용**`l_fb` |
| `20ACh` | **실제속도(우)** | 0.1r/min | **코드 사용**`r_fb` |
| `20ADh` | **실제토크/전류(좌)** | 0.1A, ±300 | **코드 사용**`l_torque_a` |
| `20AEh` | **실제토크/전류(우)** | 0.1A, ±300 | **코드 사용**`r_torque_a` |
| `20AFh` | Software connected status | `01` | 미사용 |
| `20B0h` | Driver temperature | 0.1℃, -550~1200 | 미사용 (과열 사전감지에 활용 가능) |
> **미사용이지만 유용한 레지스터**: `20A1h`(버스 전압), `20A4h`/`20B0h`(모터/드라이버 온도)는
> 현재 코드가 읽지 않는다. 배터리 저전압 경보나 열 관리 기능을 추가하려면 `readFeedback()`의
> 연속 읽기 범위를 확장하거나 별도 저빈도 폴링을 추가하는 것을 검토할 수 있다.
-110
View File
@@ -1,110 +0,0 @@
# 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) 참고.
-194
View File
@@ -1,194 +0,0 @@
# 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/<dev>/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 <path>` | (비활성) | 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축 범위 |
-91
View File
@@ -1,91 +0,0 @@
# 05. 알람 진단 & 트러블슈팅
출처: `xbox_motor_control.cpp` 주석/구현, `resource/ZLAC8015D V4 Series RS485 Communication*.pdf`,
`resource/ZLAC8015D V4 Series Manual*.pdf`
## 5.1 드라이버 알람(에러코드) 대응표
콘솔에 `[!!! 드라이버 알람 발생 !!!]`이 출력되면 `0x20A5`(좌)/`0x20A6`(우) 값을 근거로
`decodeDriverError()`가 원인을 표시한다. HUD의 `[!!알람!!]` 태그도 `fault_active` 동안 계속
표시된다.
| 비트 | 한글 표기 | 가능한 원인 | 권장 대응 |
| :-- | :--- | :--- | :--- |
| `0x0001` 과전압 | 과전압 | 배터리 전압이 75V 근접/초과, 급감속 시 회생 전류 유입 | 배터리 전압 확인, 회생 저항(`RES` 단자) 장착 검토, `2011h` 급정지 설정을 감속시간 포함(6) 방식으로 |
| `0x0002` 저전압 | 저전압 | 배터리 방전, 배선 전압강하, 순간 대전류로 전압 새그 | 배터리 충전 상태·배선 굵기 확인, `--max_current_a`로 최대전류를 낮춰 순간 전압강하 완화 |
| `0x0004` 과전류 | 과전류 | 상간 단락, 급격한 부하 변화, 배선 손상 | 배선/커넥터 점검, 모터 상간 저항 측정. 지속 시 드라이버 교체 검토 |
| `0x0008` 과부하 | 과부하 | 지형 저항/걸림 상태가 `2035h`(과부하 보호시간, 기본 300×10ms=3s) 이상 지속 | §5.3 전류 한계 튜닝 참고. 걸림(Stall) HUD 표시와 함께 발생하면 지형/장애물 확인 |
| `0x0020` 엔코더오차 | 엔코더오차 | 위치 폐루프 추종 오차가 `2036h`(기본 4090 counts) 초과 | 급격한 외력(충돌, 헛도는 지면)으로 순간 발생 가능. 반복 시 엔코더 배선 확인 |
| `0x0080` 기준전압오류 | 기준전압오류 | 드라이버 내부 기준전압 회로 이상 | 드라이버 하드웨어 이상 — 제조사 문의 |
| `0x0100` EEPROM오류 | EEPROM오류 | 파라미터 저장영역 읽기 실패 | `2009h=1`로 공장 초기화 후 필요한 파라미터(전류한계, 가감속 등) 재설정 |
| `0x0200` 홀센서오류 | 홀센서오류 | 홀 케이블 미접속/단선, 핀 순서 오류 | J2/J6 커넥터 및 홀 배선 점검 |
| `0x0400` 모터과열 | 모터과열 | 모터 온도가 임계치 초과(과부하 지속 주행 등) | 잠시 정지해 냉각, 반복 시 `--max_current_a` 하향 또는 부하 조건 재검토 |
| `0x0800` 엔코더오류 | 엔코더오류 | 엔코더 케이블 헐거움/핀 순서 오류(매뉴얼 명시) | J2/J6 커넥터 재체결, 케이블 단선 여부 확인 |
| `0x2000` 속도설정오류 | 속도설정오류 | 지령속도가 정격 속도(`2008h`, 기본 1000rpm) 초과 | 코드의 `setRPMs()` 클램프(±3000)가 드라이버 정격보다 커서 이론상 발생 가능 — `2008h` 값과 실제 사용 RPM 범위 확인 |
> `0x0010`(전류이상), `0x0040`(속도이상)은 매뉴얼상 예약(Reserved) 비트로 정상 동작에서는
> 발생하지 않는다.
**공통 대응 원칙**: 알람 발생 시 드라이버가 명령을 무시하는 잠금 상태가 될 수 있어 소프트웨어
재시작만으로는 풀리지 않는 경우가 많다. RS485로 `0x200E ← 0x0006`(알람 클리어) 후
`0x200E ← 0x0008`(Enable) 순으로 재초기화가 필요하며, `initDriver()`가 프로그램 재시작 시
이 시퀀스를 자동 수행한다. 그래도 반복되면 전원 재투입(Power Cycle)을 시도한다.
## 5.2 휠 이상(들뜸/걸림) 진단 — 알람이 아닌 소프트웨어 레벨 진단
드라이버 알람과 별개로, 애플리케이션 레벨에서 4륜 각각의 상태를 실시간 분류한다
(원리는 [04-software-control.md](04-software-control.md) §4.8 참고). HUD `WHL` 표시:
| 표시 | 의미 | 조치 |
| :-: | :--- | :--- |
| `O` | 정상(Ok) | - |
| `A` | 들뜸(Airborne) — 동료 바퀴 대비 전류가 비정상적으로 낮음 (무부하 헛돎) | 자동으로 해당 바퀴 지령을 0으로 낮춤. 지속되면 지형(웅덩이, 턱) 확인 |
| `S` | 걸림(Stall) — 지령 대비 실제속도가 크게 못 따라가며 고전류 지속 | 장애물 끼임, 지형 저항 확인. 반복 시 `0x0008` 과부하 알람으로 이어질 수 있음 |
이 진단은 어디까지나 소프트웨어 추정치이며, 드라이버 알람(§5.1)과는 독립적으로 동작한다.
`--log`로 CSV를 남기면 `stat_*`/`amp_*` 컬럼으로 사후 분석이 가능하다.
## 5.3 최대전류(`0x2034`/`0x2064`) 튜닝 배경
코드 기본값은 `--max_current_a 20.0`(공장 기본 최대전류 30A보다 낮춤). 코드 주석에 남겨진
실측 배경:
> 실측 결과 정격 15A / 최대 30A(공장 기본값) 그대로 사용 시, 걸림/과부하 상황에서 드라이버가
> 30A까지 전류를 밀어붙이며 3초(과부하 보호시간 `2035h` 기본값)씩 버티다가 과부하 알람이
> 발생하는 문제가 있었다. 정격(15A) 위로 5A 정도의 여유는 남기되, 상한을 20A로 낮춰 과도한
> 전류로 장시간 버티는 상황 자체를 줄인 것.
* `--max_current_a`에 음수를 주면 드라이버에 저장된 기존/공장 설정을 그대로 둔다(미변경).
* 정격전류(`0x2033`/`0x2063`)는 코드가 변경하지 않는다 — 최대전류만 낮춘다.
* 실외 험지 주행에서 순간 고부하가 잦다면 이 값을 상황에 맞게 재조정할 것. 너무 낮추면
정상 등판/가속 상황에서도 전류 제한에 걸려 토크 부족이 발생할 수 있다.
* 시작 시 콘솔에 출력되는 "전방/후방 드라이버 전류설정" 로그로 **실제 드라이버에 저장된 값**을
항상 확인할 수 있다(문서 기본값이 아니라 하드웨어에 실제로 쓰여있는 값).
## 5.4 RS485 통신 트러블슈팅
| 증상 | 가능한 원인 | 확인/조치 |
| :--- | :--- | :--- |
| 시작 시 "포트 열기 실패" | 잘못된 `/dev/ttyUSB*` 경로, 권한 부족 | `ls /dev/ttyUSB*`로 실제 장치명 확인, `dialout` 그룹 권한 확인 |
| "전류설정 조회 실패" 경고 | 배선 불량, 종단저항 미설정, 슬레이브 주소 불일치 | J5 커넥터 A/B/SGND 배선, 데이지체인 마지막 드라이버의 SW2 종단저항, `--id1`/`--id2`가 실제 드라이버 주소와 일치하는지 확인 |
| 명령은 가는데 응답이 자주 누락(HUD `comm_ms` 급등, 피드백 값이 갱신 안 됨) | 100Hz 루프 대비 응답 지연/노이즈, USB latency_timer 미적용 | `set_low_latency.sh` 적용 여부 확인(`cat /sys/bus/usb-serial/devices/<dev>/latency_timer`가 1이어야 함), 차폐 트위스트페어 케이블 및 접지 확인 |
| 알람은 없는데 바퀴가 반대로 회전 | 좌/우 모터 배선 반대 장착 오인, `2029h` 기본 회전방향 설정 문제 | [03-system-architecture.md](03-system-architecture.md) §3.2의 우측 부호 반전 로직 확인. 하드웨어적으로 U/V/W가 뒤바뀐 경우 `2029h`(Default running direction)로도 보정 가능 |
| CRC 불일치로 통신 실패 반환 (`readRegs`/`writeReg``false`) | 전기 노이즈, 접지 불량, 데이지체인 배선 길이 과다 | 코드는 CRC16을 재계산해 손상된 응답을 자동으로 폐기하므로 오탐 지령은 방지되지만, 값 자체가 갱신되지 않아 HUD가 멈춘 것처럼 보일 수 있다. 배선 점검 필요 |
> 코드는 통신 실패 시 **재시도(retry)를 하지 않는다** — 실패한 틱은 그냥 다음 100Hz 틱으로
> 넘어간다. 이는 저지연(latency)을 우선한 설계 선택이며, 지속적인 통신 불량은 알람이 아니라
> "피드백이 갱신되지 않음"으로만 나타날 수 있으므로 HUD의 `comm_ms` 값과 위치/전류 피드백이
> 실제로 변하는지 함께 관찰해야 한다.
## 5.5 운용 전 체크리스트
1. 배선: 좌/우 U/V/W, 엔코더/홀(J2·J6), RS485 A/B/SGND(J5), 브레이크(J3) 재확인.
2. RS485 종단저항: 데이지체인 마지막 드라이버에서만 SW2 ON.
3. `run_4wd.sh` 실행 후 콘솔에서 "전방/후방 드라이버 전류설정" 값이 기대값(정격15A 근방,
최대 20~30A)인지 확인.
4. 알람 없이 브레이크가 해제/잠금되는지(휠 수동 회전 저항으로 체감) 확인.
5. 저속으로 A버튼(트립리셋) → 직진 → 정지 → 후진 테스트로 좌/우 회전 방향이 의도대로인지
확인.
6. 라이다 연결 시 HUD에 `LIDAR:OFF`가 아닌 실측 거리(`F:x.xxm|L:...|R:...`)가 표시되는지 확인.
7. 실외 주행 전 `--log`로 CSV 기록을 켜서 최초 주행 구간을 남겨두면 사후 튜닝(§5.3, §4.11의
`--stall_*`/`--airborne_*` 인자)에 유용하다.
-162
View File
@@ -1,162 +0,0 @@
# 06. IMU 도입을 통한 제어 고도화 — 조사 및 계획 (실행 전 검토용)
> **이 문서는 계획안이다. 코드는 아직 수정하지 않았다.** 아래 내용을 검토하고 방향에 동의하면
> 그때 `xbox_motor_control.cpp`에 반영을 시작한다.
>
> **갱신 이력**: 이 문서에 대한 1차 Q&A가 [`QnA/q1.md`](../QnA/q1.md) / [`QnA/a1.md`](../QnA/a1.md)에
> 정리되어 있다. 개루프/폐루프 계층 구조에 대한 실무 관행, 온라인 ICR 추정 가능성, 헤딩 홀드에
> 필요한 필터링 수준, IMU 하드웨어(HWT905) 확정 내용이 아래 §6.4, §6.7에 반영되어 있다.
출처: `resource/husky.md`(Clearpath Husky User Manual 링크), Clearpath `robot_localization`
연동 자료, Yi, J. et al., *"Kinematic Modeling and Analysis of Skid-Steered Mobile Robots With
Applications to Low-Cost Inertial-Measurement-Unit-Based Motion Estimation,"* IEEE Transactions
on Robotics, 25(5), 2009 (이하 "Yi 2009"), Pentzer, J. et al., *"Model-based Prediction of
Skid-steer Robot Kinematics Using Online Estimation of Track Instantaneous Centers of Rotation,"*
Journal of Field Robotics, 2014 (이하 "Pentzer 2014"), 그리고 본 저장소
`xbox_motor_control.cpp` 현재 구현.
## 6.1 왜 이 조사를 하는가
Husky(Clearpath Robotics)는 본 로봇과 동일한 **스키드 스티어 4WD 실외 플랫폼**이다. Husky의
표준 소프트웨어 스택은 휠 오도메트리만 쓰지 않고, `robot_localization`
`ekf_localization_node`로 **휠 오도메트리 + IMU**를 융합해 `/odometry/filtered`를 만들고, 이를
헤딩 유지·자율주행의 기준으로 삼는다. 이 구조와 그 이론적 근거(Yi 2009)를 우리 시스템과
비교하면, 현재 우리 제어가 왜 "투박"한지, 그리고 IMU 추가가 실제로 어떤 문제를 해결하는지가
명확해진다.
## 6.2 현재 구현의 한계 (코드 근거)
| # | 한계 | 코드 근거 |
| :-: | :--- | :--- |
| 1 | **완전 개루프(open-loop) 텔레옵.** 조이스틱 입력 → 킨매틱스 수식 → 저크제한 램프 → RPM 지령까지, 로봇이 실제로 그 지령대로 움직였는지 확인하는 피드백이 전무하다. | `main()` 루프 전체 — `target_*` 계산에 어떤 실측 모션 피드백도 관여하지 않음 |
| 2 | **헤딩/자세(x, y, θ) 추정 자체가 없다.** 주행거리(trip meter)는 4륜 엔코더 델타의 중앙값이라는 스칼라 값 하나뿐이고, 회전 방향/누적 헤딩은 어디에도 저장되지 않는다. | `dist_axle` 계산부 (엔코더 tick → 거리만 산출, 각속도 적분 없음) |
| 3 | **고정 트랙폭 공식이 ICR을 로봇 중심으로 가정.** `k_skid = (track_width + wheelbase²/track_width) / 2`는 Yi 2009가 지적하는 "단순화된 근사"이며, 실제 좌/우/로봇 ICR이 중심에서 벗어나는 지면 마찰 효과를 반영하지 않는다. | `k_skid`, `effective_w` 계산부 |
| 4 | **매직넘버로 근사 오차를 손땜빵.** `spin_arc_bias=0.12`(제자리 회전 시 스크럽 마찰 완화용 임의 전진 비율), `spin_jerk_rate=220`(제자리 회전만 저크를 더 낮춤) 등은 모두 "ICR이 실제로는 중심이 아니다"라는 동일 현상을 실측 없이 경험적으로 조정한 값들이다. | 코드 상단 파라미터 선언부 주석 |
| 5 | **슬립 진단이 전류만으로 이루어지는 간접 추정.** `classifyStall`/`classifyAirPair`는 지령 대비 실제 RPM과 전류만 보고 개별 바퀴 상태를 추정한다. 전류는 부하·경사·배터리 전압 새그에도 영향받으므로, "로봇 전체가 실제로 회전/가속하고 있는가"라는 직접적 진단은 불가능하다. | `classifyStall()`, `classifyAirPair()` |
| 6 | **직진 시 편향(drift) 자동 보정 수단이 없다.** 좌/우 지면 마찰이 다르면 동일 RPM을 줘도 로봇이 서서히 한쪽으로 틀어지는데, 이를 감지할 헤딩 기준 자체가 없어 순전히 운전자가 조이스틱으로 수동 보정해야 한다. | 없음 (기능 자체 부재) |
## 6.3 Yi 2009 논문이 제시하는 제어/추정 방식 (요약)
1. **3-ICR 운동학 모델**: 좌측 휠(트랙), 우측 휠(트랙), 로봇 몸체 각각에 별도의 순간회전중심
(ICR_l, ICR_r, ICR_b) 좌표를 부여한다. 기존 "트랙폭 하나로 결정되는" 모델을 일반화한 것으로,
`(v_l, v_r) → (v_x, ω)` 관계가 이 3개 ICR 좌표를 파라미터로 하는 선형식이 된다.
2. **오프라인/온라인 캘리브레이션**: ICR 좌표는 고정 상수로 가정하지 않고, 실측 모션(외부
기준 또는 IMU)에 대해 최소자승으로 피팅하거나 EKF 상태의 일부로 추정한다. Pentzer 2014는
이를 한 발 더 발전시켜 **속도·지형에 따라 ICR이 실시간으로 변한다고 보고 온라인으로 계속
추정**하는 방식을 제안한다.
3. **IMU-EKF 융합**: 자이로 요레이트는 슬립과 무관하게 로봇의 실제 회전각속도를 직접 측정한다.
이를 ICR 모델의 휠-기반 예측치와 EKF로 결합하면, 저가 스트랩다운 IMU + 휠 인코더만으로도
위치/헤딩 추정 정확도가 크게 향상됨을 실험으로 보인다.
4. **부가 효과 — 잔차 기반 슬립 검출**: 휠 기반 ω 예측치와 자이로 실측치의 차이(잔차) 자체가
정량적 슬립 지표가 된다. 이는 §6.2-5에서 지적한 "전류만으로 하는 간접 추정"보다 훨씬 직접적인
신호다.
## 6.4 단계별 개선 계획 (Phase 1 → 5, 각 단계는 독립적으로 가치가 있고 이전 단계에 얹는 구조)
> 설계 원칙: 각 Phase는 **먼저 관측/로깅으로 검증**한 뒤에만 제어 루프에 개입하도록 순서를
> 잡았다. 검증 없이 곧바로 폐루프 보정을 넣으면, IMU 노이즈나 마운트 문제를 슬립으로 오판해
> 오히려 제어가 나빠질 위험이 있기 때문이다(§6.6 리스크 참고).
### Phase 0 — IMU(HWT905) 정식 매뉴얼 확보 및 레지스터 맵 문서화 (선행 작업)
* IMU 하드웨어는 **WitMotion HWT905-RS485**로 확정 (근거: `QnA/a1.md` §Q4 — 9축 AHRS 융합
출력, RS485/Modbus 프로토콜 지원, IP67, 최대 200Hz).
* RS485+Modbus라는 점에서 `xbox_motor_control.cpp``SerialPort`(Modbus RTU, CRC16,
`readReg`/`writeReg`/`readRegs`)를 상당 부분 재사용 가능할 것으로 예상되나, **정확한
레지스터 주소(각속도/각도/가속도/지자기, 출력레이트, 슬레이브 주소 설정)는 아직 확정하지
못했다** — 확보한 정식 PDF가 이미지 스캔본이라 텍스트 추출이 안 됐기 때문.
* 실행 순서: (1) 텍스트 검색 가능한 정식 매뉴얼을 `resource/`에 추가 → (2) ZLAC8015D 문서와
동일한 정밀도로 `doc/07-imu-hwt905-spec.md`(가칭) 작성 → (3) 그 다음에야 Phase 1 코드 작업
착수.
* **버스 분리 여부 결정 필요**: ZLAC8015D 드라이버 2대가 쓰는 RS485 버스에 IMU를 슬레이브로
얹을지, 별도 USB-RS485 포트로 분리할지는 실측(3개 슬레이브를 한 버스에 얹었을 때 100Hz
루프의 `comm_ms` 여유)으로 결정한다(§6.7 참고).
### Phase 1 — IMU 연동 + 순수 관측 (제어 루프 미개입)
* Phase 0에서 확정한 레지스터로 HWT905에서 요레이트(Z축 자이로) 최소, 가능하면 roll/pitch도
함께 읽는다. 장착 위치는 차체 무게중심에 근접하고 진동이 적은 곳.
* 별도 스레드에서 100Hz 이상으로 폴링해 값을 전역/원자적으로 공유(기존
`LidarObstacleDetector`가 이미 별도 스레드+뮤텍스 패턴을 쓰고 있으므로 동일 패턴 재사용
가능).
* **제어 로직은 건드리지 않는다.** 요레이트 값을 HUD 한 줄에 추가 표시하고, `--log` CSV에
`imu_gyro_z` 컬럼만 추가.
* 목적: 실제 하드웨어 노이즈 특성, 진동 영향, 드리프트/바이어스 크기를 먼저 눈으로 확인.
HWT905는 온보드 AHRS 필터를 내장하므로(`QnA/a1.md` §Q3), 애플리케이션 레벨에서 별도
저역통과/상보필터를 얼마나 더 추가해야 하는지는 이 단계의 실측으로 판단한다.
### Phase 2 — 헤딩(θ) 적분 + x,y 오도메트리 노출
* 자이로 요레이트를 단순 적분해 헤딩 θ를 추정하고, 기존 `dist_axle`(휠 기반 선속도 크기)과
결합해 x, y 위치도 함께 적분.
* 휠 기반 ω(현재 `k_skid` 공식으로부터 나오는 값)와 IMU 기반 ω를 **나란히** HUD/CSV에 출력해
두 추정치가 실제로 얼마나 차이 나는지 정량적으로 확인. 이 차이가 바로 §6.3-4의 슬립 잔차다.
* 아직 제어에는 반영하지 않음 — Phase 1과 마찬가지로 "계측 단계".
### Phase 3 — 직진 헤딩 홀드 (첫 폐루프 개입)
* 조향 입력(`lx`, `rx`)이 0이고 전진/후진 중일 때, θ를 "락(lock)"하고 IMU 실측 요레이트가
0에서 벗어나면 작은 PI 트림을 좌/우 `cmd_*`에 더해 편향을 보정.
* 개입 폭에 상한을 두어(예: 최대 트림 RPM 제한) 사용자의 의도적 조향을 절대 압도하지 않게 함.
* 이 단계부터 "운전자가 매번 손으로 직진 편향을 보정하는" 현재의 투박함이 실질적으로 개선됨.
### Phase 4 — IMU 기반 전신(whole-body) 슬립 감지 (기존 전류 진단 보완)
* 지령 ω 대비 IMU 실측 ω의 오차가 임계값을 넘으면 "전체 슬립"(예: 젖은 잔디, 빙판, 4륜 동시
헛돎)으로 판정 — 기존 `classifyStall`/`classifyAirPair`(개별 휠·전류 기반)와는 독립적인
신호이므로 서로 교차검증하는 2중 진단 체계가 됨.
* 전체 슬립 판정 시 속도를 일시적으로 낮추는 등, 기존 라이다 회피의 `speed_scale` 메커니즘과
유사한 패턴으로 개입 가능.
### Phase 5 — ICR 실측 캘리브레이션으로 매직넘버 제거 (정적 → 온라인 확장)
* **5a. 정적 캘리브레이션 (먼저 검증)**: 다양한 속도·회전반경 조합으로 주행하며 지령 대비 IMU
실측 ω를 기록, 최소자승으로 유효 트랙폭/ICR 오프셋을 피팅. `k_skid`, `effective_w`, 그리고
경험적으로 넣은 `spin_arc_bias=0.12` 같은 값을 이 실측 피팅값으로 교체하거나 근거를 마련.
* **5b. 온라인 ICR 추정 (선택적 확장, `QnA/a1.md` §Q2)**: ICR은 실제로는 경사·무게중심·지형
마찰·트레드 마모에 따라 계속 변하는 값이므로(Pentzer 2014), 5a의 정적 상수 대신
`effective_ICR_gain = 실측_요레이트(IMU) / 지령_차동속도`를 매 틱 갱신하는 방식으로 확장할
수 있다. 구현/검증 부담이 커지므로 5a가 실외에서 충분히 검증된 뒤에만 착수.
* 커밋 이력(`19fb70d Fix curve-steering wheel-reversal at partial throttle`,
`ae3f92f Improve outdoor traction control`)에서 반복적으로 나타난 "저속/부분 스로틀에서
커브가 이상하게 도는" 류의 버그들은 상당수가 이 근사식의 부정확성에서 기인했을 가능성이
높다 — 근본 원인 제거 시도.
### Phase 6 (장기, 선택) — 자율주행 확장 가능성
* Phase 2에서 만든 x,y,θ 추정을 Livox Mid-360S(이미 장착, 현재는 반응형 회피에만 사용)와
결합하면 SLAM/웨이포인트 주행으로 자연스럽게 확장 가능. 이번 계획의 범위 밖이지만, IMU
도입이 갖는 장기적 가치로 기록해 둔다.
## 6.5 Husky 실제 소프트웨어 스택과의 대응 관계
| Husky (`robot_localization` 기반) | 본 계획의 대응 Phase |
| :--- | :--- |
| 휠 인코더 오도메트리 (`/husky_velocity_controller/odom`) | 이미 존재 (`readFeedback()` 기반 `l_fb`/`r_fb`, `l_tick`/`r_tick`) |
| IMU (`/imu/data`) | Phase 1 |
| `ekf_localization_node` (휠+IMU 융합 → `/odometry/filtered`) | Phase 2, 장기적으로 정식 EKF로 고도화 가능 |
| 헤딩 기반 주행 안정성 | Phase 3 |
| Nav Stack 기반 자율주행 | Phase 6 (장기) |
## 6.6 리스크 / 트레이드오프
* **진동 노이즈**: 실외 오프로드 특성상 차체 진동이 크다. 저역통과/상보필터 없이 자이로 값을
바로 쓰면 오히려 노이즈를 슬립으로 오판할 수 있다 — Phase 1의 "순수 관측" 단계를 반드시
거쳐 실제 노이즈 특성을 먼저 파악해야 한다.
* **마운트 위치**: 무게중심에서 멀거나 진동이 심한 곳에 달면 가속도계 값이 왜곡된다. 요레이트는
위치에 덜 민감하지만 완전히 자유롭지는 않다.
* **추가 통신/스레드 부하**: 100Hz 제어 루프에 영향을 주지 않도록 IMU 폴링은 별도 스레드로
분리(라이다와 동일 패턴)하고, 메인 루프는 원자적 읽기만 수행해야 한다.
* **개발/검증 비용**: Phase 3 이후의 폐루프 개입은 실외 실주행 테스트로만 튜닝 가능하다 —
단계별로 나눠 각 단계를 충분히 검증한 뒤 다음 단계로 넘어가는 접근이 필요하다.
## 6.7 결정이 필요한 사항
| # | 항목 | 상태 |
| :-: | :--- | :--- |
| 1 | **목표 범위**: Phase 3(헤딩 홀드)까지만 목표로 할지, Phase 6(자율주행 확장)까지 염두에 두고 설계할지 — Phase 2 오도메트리 구조(단순 적분 vs 정식 EKF)에 영향 | 미결정 |
| 2 | **IMU 하드웨어** | **결정됨 — WitMotion HWT905-RS485** (`QnA/a1.md` §Q4) |
| 3 | **RS485 버스 분리 여부**: IMU를 ZLAC8015D와 같은 버스에 얹을지, 별도 포트로 분리할지 | 미결정 — Phase 1 실측 후 결정 |
| 4 | **시작 지점**: Phase 0(매뉴얼 확보/문서화)부터 순차 진행할지, 특정 Phase만 우선 원하는지 | 미결정 |
이 문서에 대한 피드백을 반영해 계획을 확정한 뒤 코드 작업을 시작한다.
-39
View File
@@ -1,39 +0,0 @@
# 드라이버 문서화 (ZLAC8015D V4)
이 문서 세트는 `resource/` 폴더의 ZLTECH ZLAC8015D V4 시리즈 서보 드라이버 매뉴얼을 분석하고,
본 저장소의 실제 제어 코드(`xbox_motor_control.cpp`)가 그 드라이버를 어떻게 사용하고 있는지
정리한 것이다. 매뉴얼 원문(영문 PDF)과 실제 구현 사이의 매핑을 명확히 하는 데 초점을 둔다.
## 로봇/드라이버 구성 요약
* **로봇 형식**: 실외 스키드 스티어(Skid-Steer) 4WD 로봇
* **모터**: ZLLG80ASM250-4096-B V2.12 허브 서보 모터 × 4 (전좌/전우/후좌/후우)
* **드라이버**: ZLAC8015D V4 (듀얼 채널 서보 드라이버) × 2대
* 드라이버 1대당 좌/우 모터 2개를 동시에 구동 (Left/Right 채널)
* 드라이버 #1 (Station ID 1) → 전방 좌/우 모터
* 드라이버 #2 (Station ID 2) → 후방 좌/우 모터
* **통신**: RS485 Modbus RTU, 115200bps 8N1, 두 드라이버가 `/dev/ttyUSB0` 한 포트에 데이지체인
* **제어 PC 소프트웨어**: `xbox_motor_control.cpp` (C++17, 100Hz 제어 루프, SDL2 Xbox 컨트롤러 입력,
Livox Mid-360S 라이다 장애물 회피 통합)
## 문서 목차
| 문서 | 내용 |
| :--- | :--- |
| [01-hardware-spec.md](01-hardware-spec.md) | ZLAC8015D 하드웨어 스펙, 커넥터 핀맵, 배선, 설치, LED 진단 |
| [02-communication-protocol.md](02-communication-protocol.md) | RS485 Modbus RTU 프로토콜과 실제 코드가 사용하는 레지스터 맵 |
| [03-system-architecture.md](03-system-architecture.md) | 로봇 물리 제원, 2드라이버×4모터 매핑, 스키드 스티어 운동학 |
| [04-software-control.md](04-software-control.md) | `xbox_motor_control.cpp` 제어 루프/클래스 구조 분석 |
| [05-error-troubleshooting.md](05-error-troubleshooting.md) | 알람 에러코드 표, 휠 이상(들뜸/걸림) 진단, 전류 한계 튜닝, 트러블슈팅 |
| [06-imu-integration-plan.md](06-imu-integration-plan.md) | **(계획안, 미구현)** IMU 도입을 통한 제어 고도화 조사 및 단계별 계획 — Husky/논문 비교 |
06번 문서에 대한 Q&A 기록은 [`../QnA/`](../QnA/) 폴더에 별도로 누적된다 (예: [`q1.md`](../QnA/q1.md) / [`a1.md`](../QnA/a1.md)).
## 원본 자료
* `resource/ZLAC8015D V4 Series Manual Version 1.03-20251111.pdf` — 하드웨어 매뉴얼
* `resource/ZLAC8015D V4 Series RS485 Communication Version 1.06-20251111.pdf` — RS485 통신/레지스터 상세
* `resource/ZLAC8015D V4 Series RS485 Communication Quick Start Guide Version 1.00-20251111.pdf` — RS485 빠른 시작 가이드
* `resource/ZLAC8015D V4 Series CANopen Communication *.pdf` — CANopen 통신 (본 프로젝트는 RS485만 사용, 참고용)
* `resource/ZLAC8015D V4 Series.pdf` — 외형 치수도
* `resource/husky.md` — Clearpath Husky User Manual 링크 (스키드 스티어 IMU 융합 제어 비교 참고자료, [06](06-imu-integration-plan.md) 참고)
-67
View File
@@ -1,67 +0,0 @@
# [기능 명세서] RadioMaster Pocket & XR1 V1.0 기반 조종 제어 전환
## 1. 프로젝트 개요
* **목적:** 기존 Xbox 게임패드 기반의 원격 제어 모듈을 **RadioMaster Pocket RC 조종기****XR1 V1.0 수신기(USB-to-TTL 시리얼 통신)** 기반으로 교체/마이그레이션.
* **통신 환경:**
* 하드웨어: USB to TTL 컨버터 (`/dev/ttyUSB*` 또는 COM 포트)
* 수신기: XR1 V1.0
* 조종기: RadioMaster Pocket
* **실측 확인된 시리얼 프로토콜: CRSF(Crossfire), 420000bps, 8N1.**
프레임 구조 `[Sync 0xC8][Length][FrameType][Payload][CRC8]`, FrameType
`0x16`(RC_CHANNELS_PACKED)의 22바이트 payload에 16채널이 11비트씩
리틀엔디안으로 패킹되어 raw `172~1811` 값이 `1000~2000us`로 선형
변환된다. (CRC8은 이 송신기에서 표준 DVB-S2 값과 안 맞아 검증 없이
Sync+Length 프레이밍만 신뢰함.)
---
## 2. 하드웨어 및 채널 매핑 사양
### 2.1 채널별 입력 범위 및 기능 정의
| 채널 | 입력 장치 | Raw Value 범위 | 할당 기능 | 동작 설명 |
| :--- | :--- | :--- | :--- | :--- |
| **CH1** | 롤/조향 스틱 | • `1005`: 좌측 끝<br>• `1481 ~ 1503`: 중앙 (데드존)<br>• `2000`: 우측 끝 | **좌우 조향 (Steering / Angular Z)** | • 중앙 데드존(`1481~1503`) 적용<br>• 좌회전(-) / 우회전(+) 정규화 |
| **CH2** | 피치/스로틀 스틱 | • `1005`: 최대 후진<br>• `1500`: 중앙 (정지)<br>• `2000`: 최대 전진 | **전후진 (Throttle / Linear X)** | • 값이 높을수록 전진, 낮을수록 후진 (CH1과 반대 방향)<br>• CH6에서 결정된 최대 속도 기준으로 선형 매핑 |
| **CH5** | 2단 스위치 | • `1012`: 미작동 (OFF)<br>• `1988`: 눌림 (ON) | **전자/비상 브레이크 (Brake)** | • `1988` 수신 시 모든 주행 속도 즉시 0 (`Stop`)<br>• `1012` 수신 시 일반 주행 허용 |
| **CH6** | 2단 스위치 | • `1832`: 저속 위치<br>• `1012~1090`: 고속 위치 | **속도 모드 선택 (Speed Mode)** | • `1832`(≥1500 부근): 저속 모드 (최대 **0.3 m/s**)<br>• `1012~1090`(<1500): 고속 모드 (최대 **1.5 m/s**) |
---
## 3. 제어 변환 알고리즘 및 로직
### 3.1 브레이크 최우선 로직 (CH5)
* $\text{CH5} \ge 1500$ (누름 / `1988` 부근):
$$\text{Linear Velocity} = 0.0, \quad \text{Angular Velocity} = 0.0$$
* $\text{CH5} < 1500$ (안누름 / `1012` 부근): 정상 주행 로직 실행
### 3.2 속도 제한 설정 (CH6)
* $\text{CH6} \ge 1500$ (`1832` 부근): $V_{\max} = 0.3 \text{ m/s}$ (저속 모드)
* $\text{CH6} < 1500$ (`1012~1090` 부근): $V_{\max} = 1.5 \text{ m/s}$ (고속 모드)
### 3.3 전후진 속도 계산 (CH2)
* **데드존(Deadband):** $1480 \le \text{CH2} \le 1520 \rightarrow V_x = 0.0$
* **후진 구간 ($1005 \le \text{CH2} < 1480$):**
$$V_x = -\left( \frac{1480 - \text{CH2}}{1480 - 1005} \right) \times V_{\max}$$
* **전진 구간 ($1520 < \text{CH2} \le 2000$):**
$$V_x = \left( \frac{\text{CH2} - 1520}{2000 - 1520} \right) \times V_{\max}$$
### 3.4 좌우 조향 각속도 계산 (CH1)
* **데드존(Deadband):** $1481 \le \text{CH1} \le 1503 \rightarrow \omega_z = 0.0$
* **좌회전 구간 ($1005 \le \text{CH1} < 1481$):**
$$\omega_z = -\left( \frac{1481 - \text{CH1}}{1481 - 1005} \right) \times \Omega_{\max}$$
* **우회전 구간 ($1503 < \text{CH1} \le 2000$):**
$$\omega_z = +\left( \frac{\text{CH1} - 1503}{2000 - 1503} \right) \times \Omega_{\max}$$
---
## 4. 구현 요구사항 (To Claude Code)
1. **기존 Xbox 입력 모듈 분리 및 교체:**
- 기존의 조이스틱 라이브러리(pygame, evdev, joy 노드 등) 종속성 제거
- 시리얼 통신(PySerial 등) 기반의 RC 수신기 데이터 리더 클래스 구현
2. **시리얼 패킷 디코딩 & Fail-safe 처리:**
- USB-to-TTL 시리얼 포트 오픈 및 예외 처리(재연결 로직)
- 패킷 타임아웃(예: 200ms 이상 신호 미수신 시) 발생 시 자동 감속 및 긴급 정지
3. **설정값 파라미터화 (Config):**
- Serial Port, Baudrate, 데드존 범위, 채널별 Max/Min/Center 값을 외부 설정(YAML, JSON, ROS 파라미터 등)으로 관리 가능하도록 모듈화
-236
View File
@@ -1,236 +0,0 @@
// HWT905-RS232 (WitMotion) IMU reader — Phase 1: pure observation, no control loop.
// Reads the sensor's active-push protocol (0x55-prefixed packets) over a plain
// serial port (RS232-over-USB, e.g. /dev/ttyUSB0) and prints accel/gyro/angle to
// stdout, optionally logging to CSV. Not Modbus — the RS485 variant uses Modbus,
// this RS232 variant does not.
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <string>
#include <termios.h>
#include <unistd.h>
namespace {
constexpr uint8_t kFrameHeader = 0x55;
constexpr uint8_t kTypeAccel = 0x51;
constexpr uint8_t kTypeGyro = 0x52;
constexpr uint8_t kTypeAngle = 0x53;
constexpr uint8_t kTypeMag = 0x54;
struct ImuState {
double accel[3] = {0, 0, 0}; // g
double gyro[3] = {0, 0, 0}; // deg/s
double angle[3] = {0, 0, 0}; // deg (roll, pitch, yaw)
double mag[3] = {0, 0, 0}; // raw counts
bool has_accel = false, has_gyro = false, has_angle = false, has_mag = false;
};
int16_t toInt16(uint8_t lo, uint8_t hi) {
return static_cast<int16_t>(static_cast<uint16_t>(lo) | (static_cast<uint16_t>(hi) << 8));
}
speed_t baudToSpeed(int baud) {
switch (baud) {
case 4800: return B4800;
case 9600: return B9600;
case 19200: return B19200;
case 38400: return B38400;
case 57600: return B57600;
case 115200: return B115200;
case 230400: return B230400;
default:
std::cerr << "[경고] 지원하지 않는 baud " << baud << ", 115200으로 대체\n";
return B115200;
}
}
int openSerialPort(const std::string &port, int baud) {
int fd = open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
std::cerr << "[오류] 포트 열기 실패: " << port << " (" << std::strerror(errno) << ")\n";
return -1;
}
fcntl(fd, F_SETFL, 0); // switch back to blocking reads
struct termios options;
if (tcgetattr(fd, &options) != 0) {
std::cerr << "[오류] tcgetattr 실패\n";
close(fd);
return -1;
}
speed_t speed = baudToSpeed(baud);
cfsetispeed(&options, speed);
cfsetospeed(&options, speed);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_iflag &= ~(IXON | IXOFF | IXANY);
options.c_iflag &= ~(INLCR | ICRNL);
options.c_oflag &= ~OPOST;
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 5; // 0.5s inter-byte timeout
tcflush(fd, TCIFLUSH);
if (tcsetattr(fd, TCSANOW, &options) != 0) {
std::cerr << "[오류] tcsetattr 실패\n";
close(fd);
return -1;
}
return fd;
}
// Parses one validated 11-byte WitMotion frame (frame[0]==0x55, checksum ok)
// into the running ImuState. Returns true if this frame completed an
// accel+gyro+angle group worth printing (i.e. it was an angle frame).
bool applyFrame(const uint8_t *frame, ImuState &state) {
const uint8_t type = frame[1];
switch (type) {
case kTypeAccel:
for (int i = 0; i < 3; ++i) {
int16_t raw = toInt16(frame[2 + 2 * i], frame[3 + 2 * i]);
state.accel[i] = raw / 32768.0 * 16.0; // g
}
state.has_accel = true;
return false;
case kTypeGyro:
for (int i = 0; i < 3; ++i) {
int16_t raw = toInt16(frame[2 + 2 * i], frame[3 + 2 * i]);
state.gyro[i] = raw / 32768.0 * 2000.0; // deg/s
}
state.has_gyro = true;
return false;
case kTypeAngle:
for (int i = 0; i < 3; ++i) {
int16_t raw = toInt16(frame[2 + 2 * i], frame[3 + 2 * i]);
state.angle[i] = raw / 32768.0 * 180.0; // deg
}
state.has_angle = true;
return true;
case kTypeMag:
for (int i = 0; i < 3; ++i) {
int16_t raw = toInt16(frame[2 + 2 * i], frame[3 + 2 * i]);
state.mag[i] = raw;
}
state.has_mag = true;
return false;
default:
return false; // time/quaternion/GPS frames etc. — ignored in Phase 1
}
}
} // namespace
int main(int argc, char **argv) {
std::setvbuf(stdout, nullptr, _IOLBF, 4096); // line-buffer even when piped
std::string port = "/dev/ttyUSB0";
int baud = 9600; // HWT905-232 factory default (confirmed against this unit)
std::string log_path;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--port" && i + 1 < argc) {
port = argv[++i];
} else if (arg == "--baud" && i + 1 < argc) {
baud = std::stoi(argv[++i]);
} else if (arg == "--log" && i + 1 < argc) {
log_path = argv[++i];
} else if (arg == "--help") {
std::cout << "사용법: " << argv[0]
<< " [--port /dev/ttyUSB0] [--baud 115200] [--log out.csv]\n";
return 0;
}
}
int fd = openSerialPort(port, baud);
if (fd < 0) return 1;
std::ofstream log_file;
if (!log_path.empty()) {
log_file.open(log_path);
if (!log_file) {
std::cerr << "[오류] 로그 파일 열기 실패: " << log_path << "\n";
return 1;
}
log_file << "t_s,ax_g,ay_g,az_g,gx_dps,gy_dps,gz_dps,roll_deg,pitch_deg,yaw_deg\n";
}
std::cout << "포트 " << port << " @ " << baud << "bps 에서 IMU 데이터 수신 시작 "
<< "(Ctrl+C로 종료)\n";
std::cout.flush();
const auto t_start = std::chrono::steady_clock::now();
ImuState state;
uint8_t buf[11];
size_t buf_len = 0;
while (true) {
uint8_t byte;
ssize_t n = read(fd, &byte, 1);
if (n <= 0) {
if (n < 0 && errno != EAGAIN && errno != EINTR) {
std::cerr << "[오류] 시리얼 읽기 실패: " << std::strerror(errno) << "\n";
break;
}
continue;
}
if (buf_len == 0) {
if (byte != kFrameHeader) continue; // resync: wait for header
buf[buf_len++] = byte;
continue;
}
buf[buf_len++] = byte;
if (buf_len < 11) continue;
// Full 11-byte candidate frame collected — validate checksum.
uint8_t sum = 0;
for (int i = 0; i < 10; ++i) sum += buf[i];
if (sum != buf[10]) {
// Checksum mismatch: resync by sliding one byte and rescanning for 0x55.
std::cerr << "[경고] 체크섬 불일치, 프레임 폐기\n";
buf_len = 0;
continue;
}
bool print_now = applyFrame(buf, state);
buf_len = 0;
if (print_now && state.has_accel && state.has_gyro && state.has_angle) {
double t_s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t_start).count();
std::printf(
"t=%7.3fs accel[g]=(%+.3f,%+.3f,%+.3f) gyro[dps]=(%+7.2f,%+7.2f,%+7.2f) "
"angle[deg]=(roll=%+7.2f,pitch=%+7.2f,yaw=%+7.2f)\n",
t_s, state.accel[0], state.accel[1], state.accel[2], state.gyro[0], state.gyro[1],
state.gyro[2], state.angle[0], state.angle[1], state.angle[2]);
if (log_file) {
log_file << t_s << ',' << state.accel[0] << ',' << state.accel[1] << ','
<< state.accel[2] << ',' << state.gyro[0] << ',' << state.gyro[1] << ','
<< state.gyro[2] << ',' << state.angle[0] << ',' << state.angle[1] << ','
<< state.angle[2] << '\n';
log_file.flush();
}
}
}
close(fd);
return 0;
}
-79
View File
@@ -1,79 +0,0 @@
import serial
import time
# 확인된 포트 경로 설정
SERIAL_PORT = "/dev/cu.usbserial-0001"
BAUD_RATE = 420000
CRSF_SYNC = 0xC8
CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16
def parse_channels(payload):
if len(payload) < 22:
return []
# 22바이트의 채널 데이터를 정수로 변환 후 11비트씩 언패킹
data = int.from_bytes(payload[:22], byteorder='little')
channels = []
for _ in range(16):
raw_val = data & 0x07FF # 11-bit 마스크
# CRSF 원본 값(172~1811)을 RC 마이크로초(1000~2000µs) 단위로 변환
us_val = int((raw_val - 172) * (2000 - 1000) / (1811 - 172) + 1000)
channels.append(us_val)
data >>= 11
return channels
def main():
try:
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=0.1)
print(f"Connected to {SERIAL_PORT} at {BAUD_RATE} baud.")
print("스틱과 스위치를 움직여 채널 값을 확인하세요. (종료: Ctrl+C)\n")
except Exception as e:
print(f"포트 연결 오류: {e}")
return
buf = bytearray()
while True:
try:
if ser.in_waiting:
buf.extend(ser.read(ser.in_waiting))
while len(buf) > 3:
if buf[0] != CRSF_SYNC:
buf.pop(0)
continue
length = buf[1]
total_packet_len = length + 2
if len(buf) < total_packet_len:
break # 전체 패킷 대기
packet = buf[:total_packet_len]
buf = buf[total_packet_len:]
frame_type = packet[2]
if frame_type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED:
payload = packet[3:-1] # CRC 제외 payload
channels = parse_channels(payload)
# read_crsf.py 출력 부분 수정
print(
f"\rCH1: {channels[0]:4d} | CH2: {channels[1]:4d} | "
f"CH3: {channels[2]:4d} | CH4: {channels[3]:4d} | "
f"CH5(SA/SC): {channels[4]:4d} | CH6(SD): {channels[5]:4d} | "
f"CH7: {channels[6]:4d} | CH8: {channels[7]:4d}",
end="", flush=True
)
except KeyboardInterrupt:
print("\n모니터링 종료")
break
except Exception as e:
print(f"\n데이터 수신 오류: {e}")
break
ser.close()
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,55 +0,0 @@
RELEASE NOTES
=============
WITMOTION Turotial File
Package version: 2023.06.26
Release date: 2023-06-26
This package contains all necessary files for set-up of the sensor.
Unzip the .zip file and you will have a directory containing the files.
-----------
Website:
https://www.wit-motion.com/
Document download link:
Google Drive:
https://drive.google.com/open?id=1s-7NIhHXFyLhF8TmCFdrVPz80SwS3C1i
-----------
Youtube Channel:
https://www.youtube.com/c/WITMOTION
HWT905-RS232 Playlist:
https://youtube.com/playlist?list=PL43tdDrVL_VC49nv2WW4NphnUTxSkQ_cZ
-----------
After-sale Service& Technical Support:
If you have queries about the sensors' set-up, software installation, APP, drivers
or any other questions, suggestions, please feel free to contact us.
Our engineering team is committed to providing the required support necessary
to ensure that you are satisfied with WITMOTION sensor and the service.
1. Please find the below link of detailed contact method (support@wit-motion.com)
https://www.wit-motion.com/contacts/
2. Double-confirmed which platform you bought and
contact the corresponding team by the provided way
3. Sending a message including the problem and necessary description
would be highly helpful to give a workable solution with details
4. Leave a note indicating the platform you placed the order,
the order ID, so it is intuitive to check the model you purchase
and accessory you may buy
5. For a complicated issue like calibration, PIN connection, configurations,
it is recommended to provide necessary files including screenshots,
taking several pictures, or shooting videos. A short video including the PIN connection,
software setup, operation details would be preferred.
It would be further help for remote diagnostics.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
https://docs.clearpathrobotics.com/docs_robots/legacy/ros1_robots/outdoor_robots/husky/user_manual_husky
+9 -18
View File
@@ -1,11 +1,7 @@
#!/usr/bin/env bash
# 1-Click Launch Script for C++ 4WD Motor Control with Mid-360S LiDAR Avoidance
# udev 규칙(/etc/udev/rules.d/99-fori-robot-serial.rules)으로 고정된 심볼릭
# 링크 사용 — ttyUSB 번호는 꽂는 순서에 따라 바뀌지만 이 이름들은 고정이다.
PORT="${1:-/dev/ttyMOTOR}"
RC_PORT="${RC_PORT:-/dev/ttyRC}"
IMU_PORT="${IMU_PORT:-/dev/ttyIMU}"
PORT="${1:-/dev/ttyUSB0}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" || exit 1
@@ -14,12 +10,10 @@ echo "=================================================================="
echo " 🚀 ZLAC8015D 4WD + Livox Mid-360S 라이다 원클릭 런치 시스템"
echo "=================================================================="
# 1. USB Latency 1ms 단축 (모터/RC/IMU 포트 전부)
# 1. USB Latency 1ms 단축
if [ -f "./set_low_latency.sh" ]; then
echo "[1/2] USB 시리얼 포트 지연 시간 1ms 최적화 적용 중... (모터: $PORT / RC: $RC_PORT / IMU: $IMU_PORT)"
echo "[1/2] USB 시리얼 포트($PORT) 지연 시간 1ms 최적화 적용 중..."
./set_low_latency.sh "$PORT"
./set_low_latency.sh "$RC_PORT"
./set_low_latency.sh "$IMU_PORT"
fi
# 2. C++ 바이너리 존재 여부 확인 및 컴파일
@@ -29,17 +23,14 @@ if [ ! -f "./xbox_motor_control_cpp" ]; then
fi
echo "=================================================================="
echo " [시작] C++ 100Hz 초저지연 4WD RC(RadioMaster Pocket + XR1) + IMU(HWT905) + 라이다 장애물 회피 시작"
echo " - 모터 포트: $PORT / RC 수신기 포트: $RC_PORT / IMU 포트: $IMU_PORT"
echo " - 라이다 기능 비활성화: --no_lidar 옵션 / IMU 비활성화: --no_imu 옵션"
echo " - 매 주행마다 logs/에 CSV 로그 자동 저장 (끄려면 --no_log)"
echo " - 비상 정지: CH5 브레이크 스위치 또는 Ctrl+C"
echo " [시작] C++ 100Hz 초저지연 4WD 조이스틱 + 라이다 장애물 회피 시작 ($PORT)"
echo " - 라이다 기능 토글: Xbox 컨트롤러 X 버튼"
echo " - 정지/종료: B 버튼 또는 Ctrl+C"
echo "=================================================================="
# 3. C++ 4WD 프로그램 실행 (추가 인자 전달 가능. --rc_port/--imu_port를 다시
# 넘기면 아래 기본값을 덮어쓸 수 있다 — 인자 파싱은 뒤에 온 값이 우선 적용됨)
# 3. C++ 4WD 프로그램 실행 (추가 인자 전달 가능)
if [ $# -gt 1 ]; then
./xbox_motor_control_cpp --port "$PORT" --rc_port "$RC_PORT" --imu_port "$IMU_PORT" "${@:2}"
./xbox_motor_control_cpp --port "$PORT" "${@:2}"
else
./xbox_motor_control_cpp --port "$PORT" --rc_port "$RC_PORT" --imu_port "$IMU_PORT"
./xbox_motor_control_cpp --port "$PORT"
fi
+1 -4
View File
@@ -8,10 +8,7 @@ if [ ! -e "$PORT" ]; then
exit 1
fi
# /dev/ttyMOTOR 같은 udev 고정 심볼릭 링크로 넘어올 수 있으므로, sysfs 조회에
# 필요한 실제 장치명(ttyUSB0 등)으로 반드시 풀어준다 — 안 그러면
# /sys/bus/usb-serial/devices/ttyMOTOR 경로가 존재하지 않아 항상 폴백으로 샌다.
DEV_NAME=$(basename "$(readlink -f "$PORT")")
DEV_NAME=$(basename "$PORT")
LATENCY_PATH="/sys/bus/usb-serial/devices/$DEV_NAME/latency_timer"
if [ -f "$LATENCY_PATH" ]; then
+172 -1232
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.