# 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()`의
> 연속 읽기 범위를 확장하거나 별도 저빈도 폴링을 추가하는 것을 검토할 수 있다.
