누리로봇 RS485 프로토콜 메뉴얼

온라인 메뉴얼 URL: https://manuals.nurirobot.info/rs485/index.html

적용대상: 누리로봇 RS485 기반 제어기 / 액추에이터 전 제품군

1. 문서개요

1.1 목적

RS485 기반 누리로봇 제품 제어를 위한 통신 프로토콜을 명확히 이해하고 적용할 수 있도록 구조, 명령 체계, 피드백 구조를 정의한다.

1.2 적용 범위

본 문서는 아래 제품에 공통적으로 적용된다.

  • BLDC 모터 컨트롤러(RS485 통신)

    • BCA3508

    • BCA3516

    • BCA3516E

    • BCA5008

    • BCA5016

    • BCA5016E

  • DC 모터 컨트롤러(RS485 통신)

    • DCA3508

    • DCA5008

  • 스마트모터(RS485 통신)

    • SA 모터군

      • SA59

      • SA70

      • SA84

      • SA112

    • SB 모터군

      • SB60

    • SC 모터군

      • SC70(SM70)


2. 시스템 구성(System Architecture)

2.1 통신 개요

  • RS485 Half-Duplex

  • Data bit: 8bit / Stop bit: 1bit / Parity: None

  • Baudrate: 1,200 ~ 1,000,000bps (출고 기본값: 9600bps)

  • 통신 구조: 1:N Master–Slave

2.2 네트워크 구성 예시

구성 예시

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%EB%B0%B0%EC%84%A0%EB%8F%84.png

구성 권장

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%EB%B0%B0%EC%84%A0%EB%8F%84_%EA%B6%8C%EC%9E%A5.png
  • Daisy-chain 방식 권장

  • 종단저항 반드시 양 끝단 120Ω 적용

  • Shielded Cable 권장

  • 모든 모듈의 GND 공통 연결

※ 제품(모터제어기, 스마트모터) 구성 상 전원부와 통신부가 2개씩으로 구성되어 있으나 두개 모두 사용하는 것을 권장하지 않음.

테스트

2.3 Broadcast 사용 주의사항

  • ID 0xFF는 Broadcast 전용

  • Broadcast 사용 시 피드백 요청 금지

  • 단, 응답시간 차등 설정 시 동시 Broadcast 피드백 가능

  • 예) 통신속도가 115200bps 일 때, ID 1번 응답시간(us) 1(100us) 설정, ID 2번 응답시간(us) 21 (2100us = 2.1ms), ID 3번 응답시간(us) 42 (4200us = 4.2ms), ID 4번 응답시간 63(6300us = 6.3ms)으로 설정할 경우 위치피드백이나 속도피드백을 Broadcast 방식으로 1회 호출하여, 4대의 장비의 피드백 수신이 가능하다.

일반적인 다중 피드백

../_images/Pasted_image_20251113095312.png

Broadcast 피드백

../_images/Pasted_image_20251113100951.png

2.4 DC 전원 배선

  • 연결방식

  • 병렬

  • 버스형

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-DC_%EC%A0%84%EC%9B%90_%EB%B6%84%EA%B8%B0.png ../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-DC_%EC%A0%84%EC%9B%90_%EB%B3%91%EB%A0%AC.png
  • 반드시 모든 노드의 전원 GND를 공통으로 연결

  • 전원 제어를 LowSide Switch 제어를 할 경우 통신선·신호선을 통해 역전류 유입 통신 IC 손상.

  • 전원 제어는 HighSide Switch 제어로 구성하여야 한다.

../_images/Pasted_image_20251113151335.png
  • Low Side Switch는 모든 그라운드 공유된 제품이 동시에 전원이 꺼질 경우, 외부의 별도 전원을 쓰는 장치와 연결이 되지 않을 경우에만 안전하다.

  • Low Side Switch가 비용적 이득이 있음.

  • Low Side Switch를 릴레이 스위치 제어용으로 사용하고 릴레이 스위치를 전원 제어로 사용하면 전류 이슈 등에서 자유로울 수 있다.

3. 데이터 프레임 구조 (Packet Structure)

필드명

크기(Byte)

Index

설명

Header

2

[0-1]

Header (0xFFFE)

ID

1

[2]

ID (0x00~0xFE, 0xFF=Broadcast)

Data Size

1

[3]

Data Size (N)

Check Sum

1

[4]

Checksum = ~((ID + Size + Mode + Data…) & 0xFF)

Mode

1

[5]

명령 구분 코드

Values

n

[6..]

명령에 따른 데이터 영역

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%ED%8E%98%EC%9D%B4%EC%A7%80-5.png

3.1 Checksum 계산식

CheckSum = ~(([ID + DataSize + Mode + Data...]) & 0xFF)

3.2 송수신 예시

TX : 0xFF 0xFE 0x00 0x02 0x5C 0xA1
RX : 0xFF 0xFE 0x00 0x08 0x26 0xD1 0x00 0x00 0x00 0x00 0x00 0x00

4. 명령 체계 (Command Set Overview)

4.1 명령 분류

구분

Mode 범위

설명

제어 명령

0x01~0x03

위치, 속도, 가감속 제어

설정 명령

0x04~0x0F

제어기 설정, 통신 설정, ID, 감속비 등

특수 명령

0x10~0x11

공장 초기화, Open-loop 제어

요청 명령

0xA0~0xAA, 0xCD

피드백 요청

응답 명령

0xD0~0xFD

장치 피드백 및 상태 보고

PC(MCU) → 컨트롤러

구분

Mode(hex)

BCA, DCA

SA, SB, SC

SM(구형 SC)

1

0x01

위치.속도 제어

위치.속도 제어

지원안함

2

0x02

가감속 위치 제어

가감속 위치 제어

지원안함

3

0x03

가감속 속도 제어

가감속 속도 제어

가감속 속도 제어

4

0x04

위치제어기 설정

위치제어기 설정

지원안함

5

0x05

속도제어기 설정

속도제어기 설정

속도제어기 설정

6

0x06

ID 설정

ID 설정

외부 감속비 설정

7

0x07

통신속도 설정

통신속도 설정

제어 On/Off설정

8

0x08

통신 응답시간 설정

통신 응답시간 설정

위치 초기화

9

0x09

모터 정격속도 설정

외부 감속비 설정

공장 초기화

10

0x0A

분해능 설정

제어 On/Off설정

지원안함

11

0x0B

감속비 설정

위치제어 모드 설정

지원안함

12

0x0C

제어 On/Off 설정

위치초기화

지원안함

13

0x0D

위치제어 모드 설정

공장 초기화

지원안함

14

0x0E

제어방향 설정

Echo 모드 설정

지원안함

15

0x0F

위치 초기화

지원안함

지원안함

16

0x10

공장 초기화

지원안함

지원안함

17

0x11

Open-Loop 구동

Open-Loop 구동

지원안함

구분

Mode(hex)

BCA, DCA

SA, SB, SC

SM(구형 SC)

18

0xA0

Ping 요청

Ping 요청

Ping 요청

19

0xA1

위치 피드백 요청

위치 피드백 요청

속도 피드백 요청

20

0xA2

속도 피드백 요청

속도 피드백 요청

속도제어기 피드백

21

0xA3

위치제어기 피드백 요청

위치제어기 피드백 요청

통신 응답시간 피드백

22

0xA4

속도제어기 피드백 요청

속도제어기 피드백 요청

외부 감속비 피드백

23

0xA5

통신 응답시간 피드백 요청

통신 응답시간 피드백 요청

제어 On/Off 피드백

24

0xA6

모터 정격속도 피드백

외부 감속비 피드백

펌웨어 버전 피드백

25

0xA7

분해능 피드백

제어 On/Off 피드백

지원안함

26

0xA8

감속비 피드백

위치제어 모드 피드백

지원안함

27

0xA9

제어 On/Off 피드백

절대엔코더 피드백

지원안함

28

0xAA

위치제어 모드 피드백

Echo 모드 피드백

지원안함

29

0xAB

제어방향 피드백

지원안함

지원안함

30

0xCD

펌웨어 버전

펌웨어 버전

펌웨어 버전

컨트롤러 → PC(MCU)

구분

Mode(hex)

BCA, DCA

SA, SB, SC

SM(구형 SC)

31

0xD0

Ping 응답

Ping 응답

Ping 응답

32

0xD1

위치 피드백 응답

위치 피드백 응답

속도 피드백 응답

33

0xD2

속도 피드백 응답

속도 피드백 응답

속도제어기 피드백 응답

34

0xD3

위치제어기 피드백 응답

위치제어기 피드백 응답

통신 응답시간 피드백 응답

35

0xD4

속도제어기 피드백 응답

속도제어기 피드백 응답

외부 감속비 피드백 응답

36

0xD5

통신 응답시간 피드백 응답

통신 응답시간 피드백 응답

제어 On/Off 피드백 응답

37

0xD6

모터 정격속도 피드백 응답

외부 감속비 피드백 응답

펌웨어 버전 피드백 응답

38

0xD7

분해능 피드백 응답

제어 On/Off 피드백 응답

지원안함

39

0xD8

감속비 피드백 응답

위치제어 모드 피드백 응답

지원안함

40

0xD9

제어 On/Off 피드백 응답

절대엔코더 피드백 응답

지원안함

41

0xDA

위치제어 모드 피드백 응답

Echo 모드 피드백 응답

지원안함

42

0xDB

제어방향 피드백 응답

지원안함

지원안함

43

0xFD

펌웨어 버전 응답

펌웨어 버전 응답

펌웨어 버전 응답

설정 명령은 기기별로 모드 구분자가 다르다.

※ BCA와 DCA는 동일하지만, SA, SB, SC는 설정 명령이 다름.

4.2 제품별 지원 명령 요약

제품

지원 범위

특이 사항

BCA, DCA

0x01~0x11

분해능, 감속비, 제어방향 포함, Open-loop PWM 제어 포함

SA, SB, SC

0x01~0x0E, 0x11

Open-loop PWM 제어 포함, Echo 모드 설정

펌웨어 버전에 따라 일부 지원되지 않는 기능이 존재한다.

5. 제어 명령 (Control Commands)

../_images/Pasted_image_20251112172611.png ../_images/Pasted_image_20251112172651.png ../_images/Pasted_image_20251112172754.png ../_images/Pasted_image_20251112172837.png

Mode

명령명

데이터 구조

단위

설명

0x01

위치·속도제어

[방향, 위치, 속도]

deg(0.01), RPM(0.1)

위치 및 속도를 동시에 지정

0x02

가감속 위치제어

[방향, 위치, 도달시간]

sec(0.1)

도달 시간 기반 위치 이동

0x03

가감속 속도제어

[방향, 속도, 도달시간]

sec(0.1)

도달 시간 기반 속도 변경

0x11

Open-loop 구동

[방향, 듀티비]

0.01%

PWM 방식의 개방 루프 제어

방향: 1 bytes, 위치: 2 bytes, 속도: 2 bytes, 듀티비: 2 bytes

5.1 PID 게인 튜닝

  • PID 게인 튜닝은 목표 도달 방식에 따라 결정되어야 한다.

  • 한번에 목표에 도달하는 것이 목적일 경우 PID 순으로 튜닝하는 것이 유리하다.

  • 위치 제어

  • 목표에 빠르게 도달하는 것이 목적일 경우 PDI 순으로 튜닝하는 것이 유리하다.

  • 속도 제어

  • Kp, Ki, Kd: 1byte 정규화 값(0~255)

  • 실제 제어식은 제품 내부 알고리즘에 의해 스케일링됨

  • PID 튜닝 그래프는 개념 참고용이며 절대 scale은 아님

5.1.1 P I D 순차 튜닝

  • 목표에 부드럽게 도달할 수 있다.

P 게인 튜닝
../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-P_%EA%B2%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D_1.png
P+I 게인 튜닝
../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-I%EA%B2%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D.png
P+I+D 게인 튜닝
../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-D%EA%B2%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D_1.png

5.1.2 P D I 순차 튜닝

  • P 게인이 크면 목표 도달 시간이 짧다.

../_images/Pasted_image_20251113175709.png

5.2 위치 제어

  • 위치 제어는 절대 위치제어와 상대 위치제어로 구분된다.

  • 절대 위치제어

  • 0~65533까지 제어가 가능(0도 ~ 655.33 도)

  • 기준점(0도)을 기준으로 목표 위치로 이동

  • 예: 목표값 100도 → 항상 절대 100도 위치로 이동

  • 허용 범위보다 크게 이동하기 위해서는 감속비를 설정하여서 우회할 수 있다.

  • 상대 위치제어

  • 현재 위치를 기준으로 지정한 크기만큼 이동

  • 예: 현재 300도에서 +80도 이동이면 최종 위치 = 380도 → 360도 Overflow → 20도 위치

  • 위치제어 모드는 위치제어 모드 설정을 통해서 변경된다.

  • 위치 제어는 위치 이동 후 지정 위치를 유지하기 위해서 계속 제어 동작을 한다.

  • 외력에 의해서 위치가 이동될 경우 지정 위치로 이동한다.

  • 위치 제어는 위치제어기 설정을 통해서 PID 게인을 설정한다.

  • PID는 개별 혹은 조합하여 사용이 가능하다.

  • 위치제어 PID 게인 튜닝은 기구적인 외부 요인의 영향을 받으므로 항상 성능 좋은 게인 값을 찾기는 어렵다.

5.2.1 위치 속도 제어

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%EC%9C%84%EC%B9%98%EC%86%8D%EB%8F%84_%EA%B7%B8%EB%9E%98%ED%94%84.png
  • 현재 위치에서 목표 위치까지 지정한 속도로 이동한다.

  • 방향은 0점으로 부터의 회전 방향이다.

  • 절대 위치제어

  • 고정된 0점을 기준으로 방향(CW/CCW)으로 위치값으로 이동한다.

  • 상대 위치제어

  • 현재 위치에서 지정한 방향(CW/CCW)으로 위치값만큼 이동한다.

5.2.2 가감속 위치제어

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%EA%B0%80%EA%B0%90%EC%86%8D%EC%9C%84%EC%B9%98%EC%A0%9C%EC%96%B4.png
  • 현재 위치에서 목표 위치까지 지정된 시간동안 이동한다.

  • 지정된 시간동안 속도는 리니어하게 가속과 감속한다.

  • 도달 시간을 2초로 설정하면, 가속 1초, 감속 1초로 동작한다.

5.3 속도 제어

  • 속도 제어는 지정한 속도로 회전하도록 하는 명령이다.

  • 누리로봇의 제어기는 가감속 속도 제어(accelerated speed control)만 제공한다.

  • 가감속 속도제어모터가 목표 속도에 부드럽게 도달하도록 가속과 감속을 자동으로 제어하는 기능이다.

5.3.1 가감속 속도제어기

../_images/%EB%AA%A8%ED%84%B0%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC_20251112-%EC%86%8D%EB%8F%84_%EC%A0%9C%EC%96%B4_1.png
  • 지정된 속도로 지정된 시간 동안 가속한다.

  • 현재 속도가 지정된 속도보다 크면 지정된 시간동안 감속한다.

5.4 Open-loop 구동

  • Closed-Loop의 다른 제어 방식과 달리 Open-Loop 구동은 피드백을 통한 보정이 되지 않는다.

  • 위치 유지 불가

  • 지정된 PWM 듀티 출력을 유지한다.

  • 부하 증가 시 Stall 위험

  • 과전류 발생 가능

  • 피드백에 의한 보정이 없기 때문에 상태 유지를 위한 동작이 없다.

  • 각 모터별 동작하기 위한 최소 입력 전압이 다르다.

  • 같은 듀티라도 모터별로 동작 유무가 다를 수 있다.

6. 설정 명령 (Configuration Commands)

구분

Mode(hex)

BCA, DCA

SA, SB, SC

SM(구형 SC)

4

0x04

위치제어기 설정

위치제어기 설정

지원안함

5

0x05

속도제어기 설정

속도제어기 설정

속도제어기 설정

6

0x06

ID 설정

ID 설정

외부 감속비 설정

7

0x07

통신속도 설정

통신속도 설정

제어 On/Off설정

8

0x08

통신 응답시간 설정

통신 응답시간 설정

위치 초기화

9

0x09

모터 정격속도 설정

외부 감속비 설정

공장 초기화

10

0x0A

분해능 설정

제어 On/Off설정

지원안함

11

0x0B

감속비 설정

위치제어 모드 설정

지원안함

12

0x0C

제어 On/Off 설정

위치초기화

지원안함

13

0x0D

위치제어 모드 설정

공장 초기화

지원안함

14

0x0E

제어방향 설정

Echo 모드 설정

지원안함

15

0x0F

위치 초기화

지원안함

지원안함

16

0x10

공장 초기화

지원안함

지원안함

명령명

데이터 구조

기본값

비고

위치제어기 설정

[Kp, Ki, Kd, 전류]

제품별 상이

내부 제어기 게인 설정

속도제어기 설정

[Kp, Ki, Kd, 전류]

제품별 상이

ID 설정

[New ID]

0x00

중복 금지

통신속도 설정

[Baudrate 코드]

0x06 (9,600bps)

부록 참고

통신 응답시간 설정

[시간 코드]

0x01

100µs 단위

감속비 / 외부 감속비 설정

[ratio(2B)]

10:1

제품별 차이

제어 On/Off, 위치제어 모드, 초기화

[1B]

0x00(On)

공장 초기화

모든 설정 초기화

6.1 설정 명령 공통 유의사항

항목

설명

설정 명령 처리 시간

내부 EEPROM에 기록되므로 약 50~300ms 지연이 발생한다. 연속 명령 전송 시 최소 300ms 이상 간격이 필요하다.

피드백 확인 필요

설정 후 반드시 피드백 요청으로 변경값을 검증해야 한다.

전원/통신 안정 상태에서만 수행

설정 도중 통신 끊김 또는 전원 차단 시 데이터 손상(EEPROM Error)이 발생할 수 있다.

제어 중 설정 변경 금지

제어 루프가 활성화된 상태에서 제어기, 분해능, 감속비 등의 설정을 변경하면 오작동 또는 과전류가 발생할 수 있다.

6.2 설정 명령 설명

../_images/Pasted_image_20251113181203.png ../_images/Pasted_image_20251113181229.png ../_images/Pasted_image_20251113181257.png ../_images/Pasted_image_20251113181348.png ../_images/Pasted_image_20251113181423.png ../_images/Pasted_image_20251113192148.png ../_images/Pasted_image_20251113192215.png ../_images/Pasted_image_20251113192234.png ../_images/Pasted_image_20251113192304.png ../_images/Pasted_image_20251113192326.png ../_images/Pasted_image_20251113192600.png ../_images/Pasted_image_20251113192616.png ../_images/Pasted_image_20251113192704.png ../_images/Pasted_image_20260507200411.png

6.2.1 위치제어기 설정

  • 기능설명

  • 위치 제어기의 PID 게인(Kp, Ki, Kd) 및 정격전류(Max Current)를 설정한다.

  • 제어기의 응답성, 위치 정밀도, 오버슈트 등에 직접적인 영향을 미친다.

  • 유의사항

  • 잘못된 게인 설정 시 진동 및 발진 발생 가능

  • 전류 값은 모터 정격전류 이하로 설정

  • 정격전류 이상 설정 시 발열 또는 과전류 보호 작동

  • 설정 후 반드시 피드백으로 값 확인 필요

6.2.2 속도제어기 설정

  • 기능설명

  • 속도 제어기의 PID 게인과 속도 제어용 정격전류를 설정한다.

  • 모터의 회전 속도 안정성 및 응답 특성을 조정할 때 사용된다.

  • 유의사항

  • 잘못된 게인 설정 시 진동 및 발진 발생 가능

  • 전류 값은 모터 정격전류 이하로 설정

  • 정격전류 이상 설정 시 발열 또는 과전류 보호 작동

  • 설정 후 반드시 피드백으로 값 확인 필요

6.2.3 ID 설정

  • 기능설명

  • 모듈의 고유 ID를 변경한다.

  • 각 RS485 네트워크에서 장치를 구분하는 식별 번호로 사용된다.

  • 유의사항

  • ID는 0x00~0xFE 범위 내에서 중복 금지

  • Broadcast ID(0xFF)는 제어용이 아닌 공통 명령용

  • Broadcast로 명령 전달 시 동일 ID로 설정된다.

  • 시스템에서 예외처리하지 않는다.

6.2.4 통신속도 설정

  • 기능설명

  • 통신 Baudrate를 설정한다.

  • 유의사항

  • 제어기와 슬레이브의 Baudrate 반드시 일치해야 함

  • 설정 직후 재통신 시도 필요

  • 잘못된 설정 시 통신 불가 상태 발생 가능

6.2.5 통신 응답시간 설정

  • 기능설명

  • 피드백 응답 지연시간(100µs 단위)을 설정한다.

  • 여러 모듈이 동시에 응답하는 환경에서 충돌을 방지하기 위한 지연을 부여한다.

  • 유의사항

  • 응답시간이 짧으면 다중모듈 환경에서 충돌 가능

  • 응답시간이 너무 길면 통신 지연 발생

  • ID별로 적절한 간격 설정 필요

  • [[모터제어기/누리로봇 제어 프로토콜 매뉴얼#2.3 Broadcast 사용 주의사항|누리로봇 제어 프로토콜 매뉴얼]] 참고

6.2.6 모터 정격속도 설정

  • 기능설명

  • 모터의 정격속도(Rated Speed, 단위: RPM)를 설정한다.

  • 내부 제어기의 기준 속도 한계를 지정한다.

  • 유의사항

  • 실제 모터 사양에 맞게 설정 필요

  • 과도한 정격속도 설정 시 보호 동작 또는 손상 위험

  • 변경 후 속도 피드백으로 확인 권장

6.2.7 분해능 설정

  • 기능설명

  • 엔코더 또는 홀센서의 분해능(해상도)을 설정한다.

  • Encoder 지원 제품은 엔코더 펄스를 입력한다.(PPR)

  • CPR(Counts per Revolution)과 PPR(Pulses per Revolution) 차이를 구분하라.

  • CPR : CPR / 4 입력

  • PPR: PPR 입력

  • 기본 제품은 홀센서 극수를 입력한다.(Number of Poles)

  • Pols과 Pole Pair 구분하라.

  • Pole : 그대로 입력

  • Pole Pair : Pole Pair × 2

  • 유의사항

  • 현재 하드웨어 사양과 일치해야 정확한 제어 가능

  • 잘못된 설정 시 위치 제어 오차 또는 속도 이상 발생

  • 모터 모델 변경 시 반드시 재설정

6.2.8 감속비 설정

  • 기능설명

  • 내부 또는 외부 감속기(Gear Ratio)를 설정한다.

  • 모터 회전량과 출력축의 각도/속도를 변환하는 비율을 지정한다.

  • 유의사항

  • 감속비 계산식: 출력속도 = 모터속도 ÷ 감속비

  • 실제 감속비와 다르면 위치 오차 누적 발생

  • 외부감속비 설정은 RSA, SA 제품에서 주로 사용

6.2.9 제어 On/Off 설정

  • 기능설명

  • 모터 드라이버의 제어 상태를 켜거나 끈다. (0x00=On, 0x01=Off)

  • 유의사항

  • 제어 Off 시 PWM 출력 및 제어 루프 정지

  • On 상태에서 제어기 파라미터 변경 금지

6.2.10 위치제어 모드 설정

  • 기능설명

  • 절대 위치제어(0x00) 또는 상대 위치제어(0x01) 모드를 선택한다.

  • 유의사항

  • 절대모드: 지정된 절대 좌표로 이동

  • 상대모드: 현재 위치를 기준으로 이동

  • 잘못 설정 시 위치 이동 방향 오류 발생 가능

6.2.11 제어방향 설정

  • 기능설명

  • 모터의 회전 방향 기준을 설정한다.

  • 유의사항

  • 실제 배선 방향과 불일치 시 동작 반대 발생

  • 초기 구동 시 반드시 확인 필요

  • 잘못된 방향 설정은 모터 발산의 원인

6.2.12 위치 초기화

  • 기능설명

  • 현재 위치를 0도로 초기화한다.

  • 홈 포지션 지정 시 사용된다.

  • 유의사항

  • 제어 Off 상태에서 수행 권장

  • 초기화 후 피드백 기준점 변경

  • 홈센서 등 외부 기준 신호와 병행 시 주의 필요

6.2.13 공장 초기화

  • 기능설명

  • 모든 설정값을 출하 초기값으로 복원한다.

  • EEPROM에 저장된 설정이 모두 재설정된다.

  • 유의사항

  • 모든 사용자 설정값 삭제됨

  • 실행 전 반드시 백업 필요 (캡쳐 등)

  • Broadcast(0xFF) ID로 전송 시 모든 모듈 초기화됨

  • 실행 후 재부팅 필수

6.2.14 Echo 모드 설정

  • 기능설명

  • 송신 명령에 대해서 에코(반송) 설정을 하면 전송된 메시지를 확인해서 통신 무결성을 확보할 수 있음.

  • 유의사항

  • 일부 제품만 제공

  • 반송 데이터에 의한 다중 제어 시 충돌 발생할 수 있음.

  • 다중 모터 제어 시 반송 데이터 수신 시간을 감안하여야 함.

7. 피드백 응답 (Feedback Response)

구분

Mode(hex)

BCA, DCA

SA, SB, SC

SM(구형 SC)

31

0xD0

Ping 응답

Ping 응답

Ping 응답

32

0xD1

위치 피드백 응답

위치 피드백 응답

속도 피드백 응답

33

0xD2

속도 피드백 응답

속도 피드백 응답

속도제어기 피드백 응답

34

0xD3

위치제어기 피드백 응답

위치제어기 피드백 응답

통신 응답시간 피드백 응답

35

0xD4

속도제어기 피드백 응답

속도제어기 피드백 응답

외부 감속비 피드백 응답

36

0xD5

통신 응답시간 피드백 응답

통신 응답시간 피드백 응답

제어 On/Off 피드백 응답

37

0xD6

모터 정격속도 피드백 응답

외부 감속비 피드백 응답

펌웨어 버전 피드백 응답

38

0xD7

분해능 피드백 응답

제어 On/Off 피드백 응답

지원안함

39

0xD8

감속비 피드백 응답

위치제어 모드 피드백 응답

지원안함

40

0xD9

제어 On/Off 피드백 응답

절대엔코더 피드백 응답

지원안함

41

0xDA

위치제어 모드 피드백 응답

Echo 모드 피드백 응답

지원안함

42

0xDB

제어방향 피드백 응답

지원안함

지원안함

43

0xFD

펌웨어 버전 응답

펌웨어 버전 응답

펌웨어 버전 응답

7.1 공통 유의사항

항목

설명

요청/응답 구조

피드백 응답(0xD0~0xDB)은 반드시 요청 명령(0xA0~0xAB) 이후에만 전송됩니다.

응답 주기

10ms~50ms 간격 이내에서 요청해야 안정적이며, 폴링 속도가 과도하면 통신 과부하가 발생할 수 있습니다.

통신 충돌 방지

여러 노드가 동시에 피드백하지 않도록 각 ID의 응답시간을 차등 설정해야 합니다.

Checksum 검증

수신 데이터는 반드시 Checksum을 계산하여 검증해야 합니다. (NOT(Sum of bytes))

피드백 오류 발생 시

데이터 깨짐 또는 지연 발생 시 RS485 선로(쉴드, GND, 종단저항)를 확인해야 합니다.

Broadcast 제한

Broadcast(0xFF)로 요청하면 모든 노드가 응답하여 충돌이 발생하므로 개별 ID로 요청해야 합니다. Broadcast를 사용하려는 경우에는 통신 응답시간을 설정하여 충돌을 회피해야 합니다.

  • 위치: 0.01도 단위

  • 속도 피드백 상의 위치는 0.1도 단위이다.

  • 속도: 0.1RPM 단위

  • 전류: 100mA 단위

  • 방향: 0x00(CCW), 0x01(CW)

7.2.1 Ping 응답

  • 기능설명

    • 장치가 통신 가능 상태인지 확인하는 기본 응답이다.

    • 요청에 대한 회신으로, ID 및 연결 상태를 검증할 때 사용한다.

  • 유의사항

    • 통신 정상 여부 확인용으로 사용

    • 응답이 없을 경우 배선, GND, Baudrate 확인

    • Broadcast ID(0xFF)로 Ping 요청하여 응답이 충돌하면 응답을 못 받을 수 있음

7.2.2 위치 피드백 응답

  • 기능설명

    • 현재 회전 방향, 위치, 속도, 전류 정보를 반환한다.

  • 유의사항

    • 위치 데이터 단위는 0.01°

    • 속도 단위 0.1RPM, 전류 단위 100mA

    • 회전 방향 (0x00 = CCW, 0x01 = CW) 확인

    • 연속 요청 시 10ms 이상 간격 유지

7.2.3 속도 피드백 응답

  • 기능설명

    • 현재 속도, 위치, 전류를 피드백한다.

    • 위치제어 모드가 아닌 상태에서 실시간 속도 모니터링에 사용된다.

  • 유의사항

    • 단위: 속도 0.1RPM, 위치 0.1°

    • 제어 Off 상태에서도 응답 가능

    • 고속 폴링 시 CPU 부하 증가 주의

7.2.4 위치제어기 피드백 응답

  • 기능설명

    • 위치제어기의 내부 PID 값(Kp, Ki, Kd) 및 정격전류를 반환한다.

  • 유의사항

    • 단위: 1바이트 = 정규화된 제어 게인 값

    • 변경 후 값 확인용으로 사용

    • 제어기 설정 후 200ms 이상 대기 후 요청

7.2.5 속도제어기 피드백 응답

  • 기능설명

    • 속도제어기의 PID 값(Kp, Ki, Kd)과 정격전류를 반환한다.

  • 유의사항

    • 설정 값이 EEPROM에 저장되어야 반영

    • 속도제어 루프의 안정성 점검용

    • 실시간 조정 시 발산 주의

7.2.6 통신 응답시간 피드백 응답

  • 기능설명

    • 장치의 통신 응답 지연시간(100µs 단위)을 반환한다.

  • 유의사항

    • 통신지연 보정용

    • 여러 장치가 있을 경우 ID별 시간차 부여 확인

    • 값이 0이거나 불규칙하면 버스 충돌 가능성

7.2.7 모터 정격속도 피드백 응답

  • 기능설명

    • 설정된 모터 정격속도(RPM 단위)를 반환한다.

  • 유의사항

    • 단위: 1RPM

    • 실제 모터 스펙과 일치 확인

    • 잘못된 설정 시 제어기 계산 오류 발생

      • 속도 프로파일에 도달하지 못할 경우 처리되는 동안 다른 처리가 제한될 수 있음

7.2.8 분해능 피드백 응답

  • 기능설명

    • 엔코더/홀센서의 분해능(pulse 또는 pole)을 반환한다.

  • 유의사항

    • 단위: pulse 또는 pole 수

    • 실제 하드웨어와 불일치 시 위치제어 불안정

    • 모터 교체 시 반드시 확인 필요

7.2.9 감속비 피드백 응답

  • 기능설명

    • 설정된 감속비(0.1 단위)를 반환한다.

  • 유의사항

    • 단위: 0.1 ratio

    • 예: 0x0064 = 10:1 감속비

    • 외부 감속기 존재 시 반드시 설정 확인

7.2.10 제어 On/Off 피드백 응답

  • 기능설명

    • 현재 제어기 On/Off 상태를 반환한다 (0x00 = On, 0x01 = Off).

  • 유의사항

    • 제어 Off 상태에서는 제어 명령이 무시됨

    • On/Off 전환 후 100ms 대기 필요

    • EEPROM 기록 중에는 On 금지

7.2.11 위치제어 모드 피드백 응답

  • 기능설명

    • 현재 위치제어 모드가 절대(0x00) 인지 상대(0x01) 인지를 반환한다.

  • 유의사항

    • 절대모드: 고정 기준좌표 이동

    • 상대모드: 현재 위치 기준 이동

    • 응용프로그램에서 모드 일치 여부 반드시 확인

7.2.12 제어방향 피드백 응답

  • 기능설명

    • 모터의 제어 기준 방향을 반환한다 (0x00 = CCW, 0x01 = CW).

  • 유의사항

    • 제어방향 설정과 피드백 일치 여부 확인

    • 잘못된 방향 설정 시 위치/속도 반전

    • 배선 변경 시 반드시 확인

7.2.13 펌웨어 버전 응답

  • 기능설명

    • 제어기의 펌웨어 버전 정보를 반환한다.

  • 유의사항

    • 버전별 기능 차이 가능

    • 펌웨어 업데이트 전/후 비교용

    • 버전 불일치 시 명령어 호환성 주의

7.2.14 절대엔코더 피드백 응답

  • 기능설명

    • 절대엔코더가 추가된 제품의 경우 엔코더를 이용하여 모터의 절대 위치를 반환한다.

  • 유의사항

    • 절대엔코더가 포함된 제품에서만 제공

      • 엔코더 옵션이 A 인 제품에서만 지원

7.2.15 Echo 모드 피드백 응답

  • 기능설명

    • Echo(반송) 설정값을 반환한다.

  • 유의사항

    • 0x00 : Echo 사용 안 함

    • 0x01 : Echo 사용

8. 통신 처리 로직 (Communication Logic)

8.1 송신 구조

  • 제어기 → 모듈 방향으로 데이터 전송

  • 파라미터를 구성하여 패킷 전송

  • Checksum 계산 및 Header 포함 필수

8.2 수신 파싱 알고리즘

  1. Header (0xFFFE) 탐색

  2. Data Size 기준으로 프레임 완성

  3. Checksum 검증

  4. Mode 값 해석 후 명령별 데이터 파싱

8.3 다중 데이터 처리 시 주의사항

  • 수신 중 새로운 Header가 검출될 경우 이전 패킷 폐기

  • Broadcast 명령은 응답하지 않음

  • 설정 명령 수행 후 피드백 요청 시 충분한 지연 시간 확보 필요

9. 예제 (Examples)

9.1 Python 시리얼 제어 예제

예제 소스는 아래 저장소에서 확인할 수 있다.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Nurirobot RS-485 control demo for MC/SA series
- Read current settings via feedback requests, then re-apply the same settings via "set" commands
- Run: Pos&Speed (0x01), AccPos (0x02), AccSpeed (0x03), and (if SA) Open-Loop (0x11) for 1s each, then stop
Spec refs:
  * MC-RS485 V1.0.1: control (0x01~0x03), setters (0x04~0x0F/0x10), feedback (0xA0.. -> 0xD0..FD)
  * SA-RS485 V1.0.2: Open-Loop drive (0x11)
"""

import argparse
import time
from dataclasses import dataclass
from typing import List, Dict, Optional

import serial
try:
    from serial.rs485 import RS485Settings
except Exception:
    RS485Settings = None

프로토콜 상수와 프레임 파서:

# -----------------------------
# Common protocol constants
# -----------------------------
HEADER = bytes((0xFF, 0xFE))

# Mode values (MC shared)
MODE_POS_SPEED         = 0x01  # 위치, 속도제어 (direction[1], pos[2](0.01°), spd[2](0.1RPM))
MODE_ACC_POS           = 0x02  # 가감속 위치제어 (direction[1], pos[2], arrive[1](0.1s))
MODE_ACC_SPEED         = 0x03  # 가감속 속도제어 (direction[1], spd[2], arrive[1](0.1s))
MODE_SET_POS_CTRL      = 0x04  # 위치제어기 설정 (Kp,Ki,Kd, rated[0.1A])
MODE_SET_SPD_CTRL      = 0x05  # 속도제어기 설정 (Kp,Ki,Kd, rated[0.1A])
MODE_SET_ID            = 0x06  # ID 설정 (주의: 실장비 변경 위험)
MODE_SET_BAUD          = 0x07  # 통신속도 설정 (주의: 즉시 변경)
MODE_SET_RESPTIME      = 0x08  # 통신 응답시간 설정 (value[100us])
MODE_SET_RATED_SPD     = 0x09  # 모터 정격속도 설정 (RPM, 2B)
MODE_SET_RESOLUTION    = 0x0A  # 분해능 설정 (2B)
MODE_SET_RATIO         = 0x0B  # 감속비 설정 (0.1ratio, 2B)
MODE_SET_ONOFF         = 0x0C  # 제어 On/Off (0x00 On, 0x01 Off)
MODE_SET_POS_MODE      = 0x0D  # 위치제어 모드 (0x00 절대, 0x01 상대)
MODE_SET_CTRL_DIR      = 0x0E  # 제어 방향 (0x00 CCW, 0x01 CW)
MODE_RESET_POS         = 0x0F  # 위치 초기화
MODE_RESET_FACTORY     = 0x10  # 공장 초기화 (주의)

# SA series only
MODE_OPEN_LOOP         = 0x11  # Open-Loop 구동 (direction[1], duty[2] in 0.01%)

# Feedback requests (송신)
REQ = {
    "ping":           0xA0,
    "pos":            0xA1,
    "spd":            0xA2,
    "pos_ctrl":       0xA3,
    "spd_ctrl":       0xA4,
    "resp_time":      0xA5,
    "rated_speed":    0xA6,
    "resolution":     0xA7,
    "ratio":          0xA8,
    "ctrl_onoff":     0xA9,
    "pos_ctrl_mode":  0xAA,
    "ctrl_dir":       0xAB,
    "firmware":       0xCD,
}
# Feedback responses (수신)
FEED = {
    "ping":           0xD0,
    "pos":            0xD1,
    "spd":            0xD2,
    "pos_ctrl":       0xD3,
    "spd_ctrl":       0xD4,
    "resp_time":      0xD5,
    "rated_speed":    0xD6,
    "resolution":     0xD7,
    "ratio":          0xD8,
    "ctrl_onoff":     0xD9,
    "pos_ctrl_mode":  0xDA,
    "ctrl_dir":       0xDB,
    "firmware":       0xFD,
}


# -----------------------------
# Bytes / checksum helpers
# -----------------------------
def be16(msb: int, lsb: int) -> int:
    return ((msb & 0xFF) << 8) | (lsb & 0xFF)

def to_be16(v: int) -> bytes:
    return bytes(((v >> 8) & 0xFF, v & 0xFF))

def calc_checksum(frame_with_placeholder: bytes) -> int:
    # ~(sum of all bytes except header[0:2] and checksum[4]) & 0xFF
    s = 0
    for i, b in enumerate(frame_with_placeholder):
        if i in (0, 1, 4):
            continue
        s = (s + (b & 0xFF)) & 0xFF
    return (~s) & 0xFF

def build_frame(dev_id: int, mode: int, data: bytes = b'') -> bytes:
    # Frame: FF FE | ID | LEN | CHK | MODE | DATA...
    # LEN includes CHK + MODE + DATA
    size = 1 + 1 + len(data)
    buf = bytearray(6 + len(data))
    buf[0:2] = HEADER
    buf[2] = dev_id & 0xFF
    buf[3] = size & 0xFF
    buf[5] = mode & 0xFF
    if data:
        buf[6:] = data
    buf[4] = calc_checksum(buf)
    return bytes(buf)


# -----------------------------
# Stream parser (robust)
# -----------------------------
class StreamParser:
    def __init__(self):
        self.buf = bytearray()

    def feed(self, chunk: bytes) -> List[bytes]:
        frames: List[bytes] = []
        if not chunk:
            return frames
        self.buf.extend(chunk)

        while True:
            start = self.buf.find(HEADER)
            if start < 0:
                self.buf.clear()
                break
            if start > 0:
                del self.buf[:start]
            if len(self.buf) < 6:
                break

            length = self.buf[3]
            total = length + 4
            if total < 6:
                del self.buf[:2]
                continue

            if len(self.buf) < total:
                nxt = self.buf.find(HEADER, 2)
                if 0 <= nxt < len(self.buf):
                    del self.buf[:nxt]
                    continue
                break

            frame = bytes(self.buf[:total])
            try:
                print("Recv Frame:", ' '.join(f"{b:02X}" for b in frame))
            except Exception:
                # 출력 도중 문제가 생겨도 파싱에는 영향이 없도록 무시합니다.
                pass

            if calc_checksum(frame) != frame[4]:
                del self.buf[:2]
                continue
            frames.append(frame)
            del self.buf[:total]
        return frames

시리얼 링크와 피드백 파싱:

# -----------------------------
# Serial link
# -----------------------------
@dataclass
class LinkConfig:
    port: str
    baud: int = 9600
    timeout: float = 0.2
    rs485_rts_enable: bool = False

class Link:
    def __init__(self, cfg: LinkConfig):
        self.cfg = cfg
        self.ser: Optional[serial.Serial] = None
        self.parser = StreamParser()

    def open(self):
        self.ser = serial.Serial()
        self.ser.port = self.cfg.port
        self.ser.baudrate = self.cfg.baud
        self.ser.bytesize = serial.EIGHTBITS
        self.ser.parity = serial.PARITY_NONE
        self.ser.stopbits = serial.STOPBITS_ONE
        self.ser.timeout = self.cfg.timeout
        self.ser.xonxoff = False
        self.ser.rtscts = False
        self.ser.dsrdtr = False
        if self.cfg.rs485_rts_enable and RS485Settings is not None:
            self.ser.rs485_mode = RS485Settings(
                rts_level_for_tx=True, rts_level_for_rx=False,
                delay_before_tx=0.0, delay_before_rx=0.0
            )
        self.ser.open()

    def close(self):
        if self.ser and self.ser.is_open:
            self.ser.close()

    def write(self, frame: bytes):
        self.ser.write(frame)
        self.ser.flush()
        try:
            print("Send Frame:", ' '.join(f"{b:02X}" for b in frame))
        except Exception:
            # 출력 도중 문제가 생겨도 파싱에는 영향이 없도록 무시합니다.
            pass

    def read_frames(self) -> List[bytes]:
        chunk = self.ser.read(1024)
        return self.parser.feed(chunk) if chunk else []

피드백 요청 대기:

# -----------------------------
# Feedback: read current settings (MC spec)
# -----------------------------
def request_and_wait(link: Link, dev_id: int, req_mode: int, expect_feed_mode: int, wait_s=0.35):
    frm = build_frame(dev_id, req_mode, b'')
    link.write(frm)
    end = time.monotonic() + wait_s
    while time.monotonic() < end:
        for f in link.read_frames():
            if len(f) >= 6 and f[2] == (dev_id & 0xFF) and f[5] == (expect_feed_mode & 0xFF):
                return f
        time.sleep(0.005)
    return None

피드백 응답 파싱:

def parse_feed_pos_ctrl(f: bytes) -> Dict:
    d = f[6:]
    return {"kp": d[0], "ki": d[1], "kd": d[2], "rated_current_a": d[3] * 0.1}

def parse_feed_spd_ctrl(f: bytes) -> Dict:
    d = f[6:]
    return {"kp": d[0], "ki": d[1], "kd": d[2], "rated_current_a": d[3] * 0.1}

현재 설정 읽기:

def parse_feed_settings(link: Link, dev_id: int) -> Dict:
    """
    읽을 수 있는 현재 설정/상태를 모두 피드백 요청으로 수집.
    (D3/D4/D5/D6/D7/D8/D9/DA/DB/FD 등)
    """
    out: Dict = {}
    q = [
        ("pos_ctrl",      REQ["pos_ctrl"],      FEED["pos_ctrl"],      parse_feed_pos_ctrl),
        ("spd_ctrl",      REQ["spd_ctrl"],      FEED["spd_ctrl"],      parse_feed_spd_ctrl),
        ("resp_time",     REQ["resp_time"],     FEED["resp_time"],     lambda f: {"resptime_100us": f[6]}),
        ("rated_speed",   REQ["rated_speed"],   FEED["rated_speed"],   lambda f: {"rated_rpm": be16(f[6], f[7])}),
        ("resolution",    REQ["resolution"],    FEED["resolution"],    lambda f: {"resolution": be16(f[6], f[7])}),
        ("ratio",         REQ["ratio"],         FEED["ratio"],         lambda f: {"ratio": be16(f[6], f[7]) / 10.0}),
        ("ctrl_onoff",    REQ["ctrl_onoff"],    FEED["ctrl_onoff"],    lambda f: {"on": (f[6] == 0x00)}),
        ("pos_ctrl_mode", REQ["pos_ctrl_mode"], FEED["pos_ctrl_mode"], lambda f: {"pos_mode": "absolute" if f[6]==0x00 else "relative"}),
        ("ctrl_dir",      REQ["ctrl_dir"],      FEED["ctrl_dir"],      lambda f: {"ctrl_dir": "CCW" if f[6]==0x00 else "CW"}),
        ("firmware",      REQ["firmware"],      FEED["firmware"],      lambda f: {"fw_version": f[6]}),
    ]
    for key, req_m, feed_m, parser in q:
        fr = request_and_wait(link, dev_id, req_m, feed_m)
        if fr:
            out[key] = parser(fr)
        else:
            out[key] = None
    return out

설정 및 제어 명령:

# -----------------------------
# Setters (MC spec + SA Open-Loop)
# -----------------------------
def set_pos_ctrl(link: Link, dev_id: int, kp: int, ki: int, kd: int, rated_current_a: float):
    data = bytes((kp & 0xFF, ki & 0xFF, kd & 0xFF, int(round(rated_current_a * 10)) & 0xFF))
    link.write(build_frame(dev_id, MODE_SET_POS_CTRL, data))

def set_spd_ctrl(link: Link, dev_id: int, kp: int, ki: int, kd: int, rated_current_a: float):
    data = bytes((kp & 0xFF, ki & 0xFF, kd & 0xFF, int(round(rated_current_a * 10)) & 0xFF))
    link.write(build_frame(dev_id, MODE_SET_SPD_CTRL, data))

def set_resp_time(link: Link, dev_id: int, hundred_us: int):
    link.write(build_frame(dev_id, MODE_SET_RESPTIME, bytes((hundred_us & 0xFF,))))

def set_rated_speed(link: Link, dev_id: int, rpm: int):
    link.write(build_frame(dev_id, MODE_SET_RATED_SPD, to_be16(max(1, min(65533, int(rpm))))))

def set_resolution(link: Link, dev_id: int, res: int):
    link.write(build_frame(dev_id, MODE_SET_RESOLUTION, to_be16(max(1, min(65533, int(res))))))

def set_ratio(link: Link, dev_id: int, ratio: float):
    raw = max(1, min(65533, int(round(ratio * 10.0))))
    link.write(build_frame(dev_id, MODE_SET_RATIO, to_be16(raw)))

def set_onoff(link: Link, dev_id: int, on: bool):
    link.write(build_frame(dev_id, MODE_SET_ONOFF, bytes((0x00 if on else 0x01,))))

def set_pos_mode(link: Link, dev_id: int, absolute: bool):
    link.write(build_frame(dev_id, MODE_SET_POS_MODE, bytes((0x00 if absolute else 0x01,))))

def set_ctrl_dir(link: Link, dev_id: int, cw: bool):
    link.write(build_frame(dev_id, MODE_SET_CTRL_DIR, bytes((0x01 if cw else 0x00,))))

def reset_pos(link: Link, dev_id: int):
    link.write(build_frame(dev_id, MODE_RESET_POS, b''))


# -----------------------------
# Controllers (run then stop)
# -----------------------------
def cmd_pos_speed(link: Link, dev_id: int, direction_cw: bool, pos_deg: float, spd_rpm: float):
    pos_raw = max(0, min(65533, int(round(pos_deg * 100.0))))     # 0.01°
    spd_raw = max(0, min(65533, int(round(spd_rpm * 10.0))))      # 0.1RPM
    data = bytes((0x01 if direction_cw else 0x00,)) + to_be16(pos_raw) + to_be16(spd_raw)
    link.write(build_frame(dev_id, MODE_POS_SPEED, data))

def cmd_acc_pos(link: Link, dev_id: int, direction_cw: bool, pos_deg: float, arrive_s: float):
    pos_raw = max(0, min(65533, int(round(pos_deg * 100.0))))     # 0.01°
    arr_raw = max(1, min(255, int(round(arrive_s * 10.0))))       # 0.1s
    data = bytes((0x01 if direction_cw else 0x00,)) + to_be16(pos_raw) + bytes((arr_raw,))
    link.write(build_frame(dev_id, MODE_ACC_POS, data))

def cmd_acc_speed(link: Link, dev_id: int, direction_cw: bool, spd_rpm: float, arrive_s: float):
    spd_raw = max(0, min(65533, int(round(spd_rpm * 10.0))))      # 0.1RPM
    arr_raw = max(1, min(255, int(round(arrive_s * 10.0))))       # 0.1s
    data = bytes((0x01 if direction_cw else 0x00,)) + to_be16(spd_raw) + bytes((arr_raw,))
    link.write(build_frame(dev_id, MODE_ACC_SPEED, data))

def cmd_open_loop(link: Link, dev_id: int, direction_cw: bool, duty_percent: float):
    # SA 전용: duty in 0.01% (0..100.00%)
    duty_raw = max(0, min(10000, int(round(duty_percent * 100.0))))
    data = bytes((0x01 if direction_cw else 0x00,)) + to_be16(duty_raw)
    link.write(build_frame(dev_id, MODE_OPEN_LOOP, data))

def safe_stop(link: Link, dev_id: int):
    # 위치제어, 가감속 위치제어에서 사용하면 위치가 초기화됨
    # 스톨 등의 위험상황에서 사용하는 코드
    # 속도를 0RPM으로 짧게 감속 후 제어 Off
    cmd_acc_speed(link, dev_id, direction_cw=False, spd_rpm=0.0, arrive_s=0.2)
    time.sleep(0.25)
    set_onoff(link, dev_id, on=False)  # 0x01 = Off
    time.sleep(0.05)

데모 실행 흐름:

# -----------------------------
# Demo flow
# -----------------------------
def demo(series: str, port: str, dev_id: int, baud: int, rs485_rts_enable: bool):
    link = Link(LinkConfig(port=port, baud=baud, rs485_rts_enable=rs485_rts_enable))
    link.open()
    try:
        print(f"[OPEN] {port} @{baud}bps, id=0x{dev_id:02X}, series={series}")

        # 1) 현재 설정값 읽기 (feedback)
        cur = parse_feed_settings(link, dev_id)
        print("\n[CURRENT SETTINGS]")
        for k, v in cur.items():
            print(f"- {k}: {v}")

        # 2) 읽은 설정을 그대로 설정(송신)으로 반영 (안전 데모)
        if cur.get("pos_ctrl"):
            set_pos_ctrl(link, dev_id, **cur["pos_ctrl"])
        if cur.get("spd_ctrl"):
            set_spd_ctrl(link, dev_id, **cur["spd_ctrl"])
        if cur.get("resp_time"):
            set_resp_time(link, dev_id, cur["resp_time"]["resptime_100us"])
        if cur.get("rated_speed"):
            set_rated_speed(link, dev_id, cur["rated_speed"]["rated_rpm"])
        if cur.get("resolution"):
            set_resolution(link, dev_id, cur["resolution"]["resolution"])
        if cur.get("ratio"):
            set_ratio(link, dev_id, cur["ratio"]["ratio"])
        if cur.get("pos_ctrl_mode"):
            set_pos_mode(link, dev_id, cur["pos_ctrl_mode"]["pos_mode"] == "absolute")
        if cur.get("ctrl_dir"):
            set_ctrl_dir(link, dev_id, cw=(cur["ctrl_dir"]["ctrl_dir"] == "CW"))
        if cur.get("ctrl_onoff"):
            set_onoff(link, dev_id, on=cur["ctrl_onoff"]["on"])

        time.sleep(0.05)

        # 3) 제어 데모: 각 1초 구동 후 정지
        print("\n[DEMO] 제어 On")
        set_onoff(link, dev_id, on=True)           # 제어 On (0x00)
        time.sleep(0.05)

        # (a) 위치, 속도제어 1s
        print("[RUN] 위치, 속도제어 (0x01) -> 1s")
        # 예: CCW로 180.00도, 5.0RPM
        cmd_pos_speed(link, dev_id, direction_cw=False, pos_deg=180.00, spd_rpm=5.0)  # MC 스펙: dir/pos(0.01°)/spd(0.1RPM) :contentReference[oaicite:3]{index=3}
        time.sleep(1.0)
        safe_stop(link, dev_id)
        print("[STOP] done\n")
        time.sleep(0.2)
        set_onoff(link, dev_id, on=True); time.sleep(0.05)

        # (b) 가감속 위치제어 1s
        print("[RUN] 가감속 위치제어 (0x02) -> 1s")
        # 예: CW로 360.00도, 도달시간 1.0s
        cmd_acc_pos(link, dev_id, direction_cw=True, pos_deg=360.00, arrive_s=1.0)    # MC 스펙: dir/pos/arrive(0.1s) :contentReference[oaicite:4]{index=4}
        time.sleep(1.0)
        safe_stop(link, dev_id)
        print("[STOP] done\n")
        time.sleep(0.2)
        set_onoff(link, dev_id, on=True); time.sleep(0.05)

        # (c) 가감속 속도제어 1s
        print("[RUN] 가감속 속도제어 (0x03) -> 1s")
        # 예: CCW로 100RPM, 도달시간 0.5s
        cmd_acc_speed(link, dev_id, direction_cw=False, spd_rpm=100.0, arrive_s=0.5)  # MC 스펙: dir/spd/arrive(0.1s) :contentReference[oaicite:5]{index=5}
        time.sleep(1.0)
        safe_stop(link, dev_id)
        print("[STOP] done\n")

        set_onoff(link, dev_id, on=True); time.sleep(0.05)
        print("[RUN] Open-Loop (0x11) -> 1s")
        # 예: CCW 50% duty
        cmd_open_loop(link, dev_id, direction_cw=False, duty_percent=50.0)        # SA 스펙: dir/duty(0.01%) :contentReference[oaicite:6]{index=6}
        time.sleep(1.0)
        safe_stop(link, dev_id)
        print("[STOP] done\n")

        print("[DEMO] 완료")

    finally:
        link.close()

진입점:

def main():
    ap = argparse.ArgumentParser(description="Nurirobot RS-485 MC/SA control demo")
    ap.add_argument("--series", choices=["MC", "SA"], default="MC", help="제품 시리즈 (MC 기본, SA는 Open-Loop 지원)")
    ap.add_argument("--port", default="COM10",help="시리얼 포트 (예: /dev/ttyUSB0, COM5)")
    ap.add_argument("--id", type=lambda x: int(x, 0), default=0x00, help="장치 ID (기본 0x00)")
    ap.add_argument("--baud", type=int, default=115200, help="Baudrate (기본 9600)")
    ap.add_argument("--rts485", action="store_true", help="어댑터가 RTS 기반 DE/RE 전환 필요 시 사용")
    args = ap.parse_args()

    demo(args.series, args.port, args.id, args.baud, args.rts485)


if __name__ == "__main__":
    main()

9.2 C 시리얼 제어 예제

예제 소스는 아래 저장소에서 확인할 수 있다.

#include <windows.h>
#include <iostream>
#include <vector>
#include <string>
#include <iomanip>
#include <chrono>
#include <thread>

using namespace std;

// -----------------------------
// Protocol constants
// -----------------------------
static const uint8_t HEADER[2] = { 0xFF, 0xFE };

// MC-control
#define MODE_POS_SPEED      0x01
#define MODE_ACC_POS        0x02
#define MODE_ACC_SPEED      0x03
#define MODE_OPEN_LOOP      0x11   // SA 전용

// Settings
#define MODE_SET_POS_CTRL   0x04
#define MODE_SET_SPD_CTRL   0x05
#define MODE_SET_ID         0x06
#define MODE_SET_BAUD       0x07
#define MODE_SET_RESPTIME   0x08
#define MODE_SET_RATEDSPD   0x09
#define MODE_SET_RESOLUTION 0x0A
#define MODE_SET_RATIO      0x0B
#define MODE_SET_ONOFF      0x0C
#define MODE_SET_POSMODE    0x0D
#define MODE_SET_CTRLDIR    0x0E
#define MODE_RESET_POS      0x0F
#define MODE_RESET_FACTORY  0x10

// Feedback Requests
#define REQ_PING           0xA0
#define REQ_POS            0xA1
#define REQ_SPD            0xA2
#define REQ_POS_CTRL       0xA3
#define REQ_SPD_CTRL       0xA4
#define REQ_RESP_TIME      0xA5
#define REQ_RATEDSPD       0xA6
#define REQ_RESOLUTION     0xA7
#define REQ_RATIO          0xA8
#define REQ_ONOFF          0xA9
#define REQ_POSMODE        0xAA
#define REQ_CTRLDIR        0xAB
#define REQ_FIRMWARE       0xCD

유틸리티 함수:

// =====================================================
// Utility
// =====================================================
static uint16_t be16(uint8_t hi, uint8_t lo)
{
    return ((uint16_t)hi << 8) | lo;
}

// 체크섬: 헤더(2바이트) & checksum 바이트 제외한 모든 바이트 합의 NOT
uint8_t calcChecksum(const vector<uint8_t>& frame)
{
    uint32_t sum = 0;
    for (size_t i = 0; i < frame.size(); i++)
    {
        if (i == 0 || i == 1 || i == 4) continue;
        sum += frame[i];
    }
    return (uint8_t)(~(sum & 0xFF));
}

// 프레임 생성
vector<uint8_t> buildFrame(uint8_t id, uint8_t mode, const vector<uint8_t>& data)
{
    size_t size = 1 + 1 + data.size();
    vector<uint8_t> frame(6 + data.size());

    frame[0] = 0xFF;
    frame[1] = 0xFE;
    frame[2] = id;
    frame[3] = (uint8_t)size;
    frame[5] = mode;

    if (!data.empty())
        memcpy(&frame[6], &data[0], data.size());

    frame[4] = calcChecksum(frame);
    return frame;
}

// 프레임 출력
void printFrame(const string& tag, const vector<uint8_t>& f)
{
    cout << tag;
    for (auto b : f)
        cout << " " << hex << setw(2) << setfill('0') << (int)b;
    cout << dec << endl;
}

시리얼 클래스와 스트림 파서:

// =====================================================
// Windows Serial Class
// =====================================================
class SerialPort
{
private:
    HANDLE hSerial = INVALID_HANDLE_VALUE;

public:
    bool openPort(const string& port, uint32_t baud)
    {
        hSerial = CreateFileA(
            port.c_str(),
            GENERIC_READ | GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            0,
            NULL
        );

        if (hSerial == INVALID_HANDLE_VALUE)
        {
            cout << "Failed to open port" << endl;
            return false;
        }

        DCB dcbSerialParams = { 0 };
        dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

        if (!GetCommState(hSerial, &dcbSerialParams))
            return false;

        dcbSerialParams.BaudRate = baud;
        dcbSerialParams.ByteSize = 8;
        dcbSerialParams.StopBits = ONESTOPBIT;
        dcbSerialParams.Parity = NOPARITY;

        SetCommState(hSerial, &dcbSerialParams);
        return true;
    }

    void closePort()
    {
        if (hSerial != INVALID_HANDLE_VALUE)
            CloseHandle(hSerial);
    }

    bool writeBytes(const vector<uint8_t>& data)
    {
        DWORD written;
        BOOL ok = WriteFile(hSerial, data.data(), data.size(), &written, NULL);

        return ok && written == data.size();
    }

    vector<uint8_t> readBytes()
    {
        uint8_t buf[1024];
        DWORD dwRead = 0;
        ReadFile(hSerial, buf, 1024, &dwRead, NULL);

        vector<uint8_t> out;
        for (size_t i = 0; i < dwRead; i++)
            out.push_back(buf[i]);

        return out;
    }
};


// ================================================
// StreamParser (FF FE + LEN+4 기준 파싱)  :contentReference[oaicite:2]{index=2}
// ================================================
class StreamParser
{
private:
    vector<uint8_t> buf;

public:
    vector<vector<uint8_t>> feed(const vector<uint8_t>& chunk)
    {
        vector<vector<uint8_t>> frames;
        buf.insert(buf.end(), chunk.begin(), chunk.end());

        while (true)
        {
            // FF FE 검색
            size_t pos = string::npos;
            for (size_t i = 0; i + 1 < buf.size(); i++)
            {
                if (buf[i] == 0xFF && buf[i + 1] == 0xFE)
                {
                    pos = i;
                    break;
                }
            }

            if (pos == string::npos)
            {
                if (buf.size() > 1)
                    buf.erase(buf.begin(), buf.end() - 1);
                break;
            }

            if (pos > 0)
                buf.erase(buf.begin(), buf.begin() + pos);

            if (buf.size() < 6)
                break;

            uint8_t len = buf[3];
            size_t total = len + 4;
            if (buf.size() < total)
                break;

            vector<uint8_t> frame(buf.begin(), buf.begin() + total);

            // 체크섬 확인
            if (calcChecksum(frame) == frame[4])
                frames.push_back(frame);

            buf.erase(buf.begin(), buf.begin() + total);
        }

        return frames;
    }
};

제어 명령 생성:

// ================================================
// Build control commands
// ================================================
vector<uint8_t> cmd_pos_speed(uint8_t id, bool cw, float pos_deg, float spd)
{
    uint16_t pos_raw = (uint16_t)(pos_deg * 100.0f);   // 0.01°
    uint16_t spd_raw = (uint16_t)(spd * 10.0f);        // 0.1RPM

    vector<uint8_t> d;
    d.push_back(cw ? 0x01 : 0x00);
    d.push_back((pos_raw >> 8) & 0xFF);
    d.push_back(pos_raw & 0xFF);
    d.push_back((spd_raw >> 8) & 0xFF);
    d.push_back(spd_raw & 0xFF);

    return buildFrame(id, MODE_POS_SPEED, d);
}

vector<uint8_t> cmd_acc_pos(uint8_t id, bool cw, float pos_deg, float arrive_s)
{
    uint16_t pos_raw = (uint16_t)(pos_deg * 100.0f);
    uint8_t arr_raw = (uint8_t)(arrive_s * 10.0f);

    vector<uint8_t> d;
    d.push_back(cw ? 1 : 0);
    d.push_back((pos_raw >> 8) & 0xFF);
    d.push_back(pos_raw & 0xFF);
    d.push_back(arr_raw);

    return buildFrame(id, MODE_ACC_POS, d);
}

vector<uint8_t> cmd_acc_speed(uint8_t id, bool cw, float spd, float arrive_s)
{
    uint16_t spd_raw = (uint16_t)(spd * 10.0f);
    uint8_t arr_raw = (uint8_t)(arrive_s * 10.0f);

    vector<uint8_t> d;
    d.push_back(cw ? 1 : 0);
    d.push_back((spd_raw >> 8) & 0xFF);
    d.push_back(spd_raw & 0xFF);
    d.push_back(arr_raw);

    return buildFrame(id, MODE_ACC_SPEED, d);
}

vector<uint8_t> cmd_open_loop(uint8_t id, bool cw, float duty_percent)
{
    uint16_t duty = (uint16_t)(duty_percent * 100);

    vector<uint8_t> d;
    d.push_back(cw ? 1 : 0);
    d.push_back((duty >> 8) & 0xFF);
    d.push_back(duty & 0xFF);

    return buildFrame(id, MODE_OPEN_LOOP, d);
}

// 위치제어, 가감속 위치제어에서 사용하면 위치가 초기화됨
// 스톨 등의 위험상황에서 사용하는 코드
// 안전 정지
void safe_stop(SerialPort& sp, uint8_t id)
{
    auto f = cmd_acc_speed(id, false, 0.0f, 0.2f);
    sp.writeBytes(f);
    this_thread::sleep_for(chrono::milliseconds(250));

    vector<uint8_t> off = buildFrame(id, MODE_SET_ONOFF, { 0x01 }); // Off
    sp.writeBytes(off);
}

메인 데모:

// =====================================================
// Main Demo (파이썬 demo() 동일 로직)
// =====================================================
int main()
{
    SerialPort serial;
    if (!serial.openPort("\\\\.\\COM10", 115200))
    {
        cout << "Port open failed" << endl;
        return 0;
    }

    cout << "[OPEN] COM10 115200bps" << endl;

    uint8_t id = 0x00;

    // 제어 ON
    serial.writeBytes(buildFrame(id, MODE_SET_ONOFF, { 0x00 }));
    this_thread::sleep_for(chrono::milliseconds(50));

    // ---- 위치 속도제어 ----
    cout << "[RUN] PosSpeed 1s" << endl;
    serial.writeBytes(cmd_pos_speed(id, false, 180.0f, 5.0f));
    this_thread::sleep_for(chrono::seconds(1));
    safe_stop(serial, id);

    // ---- 가감속 위치제어 ----
    serial.writeBytes(buildFrame(id, MODE_SET_ONOFF, { 0x00 }));
    cout << "[RUN] AccPos 1s" << endl;
    serial.writeBytes(cmd_acc_pos(id, true, 360.0f, 1.0f));
    this_thread::sleep_for(chrono::seconds(1));
    safe_stop(serial, id);

    // ---- 가감속 속도제어 ----
    serial.writeBytes(buildFrame(id, MODE_SET_ONOFF, { 0x00 }));
    cout << "[RUN] AccSpeed 1s" << endl;
    serial.writeBytes(cmd_acc_speed(id, false, 100.0f, 0.5f));
    this_thread::sleep_for(chrono::seconds(1));
    safe_stop(serial, id);

    // ---- Open-loop ----
    serial.writeBytes(buildFrame(id, MODE_SET_ONOFF, { 0x00 }));
    cout << "[RUN] OpenLoop 1s" << endl;
    serial.writeBytes(cmd_open_loop(id, false, 50.0f));
    this_thread::sleep_for(chrono::seconds(1));
    safe_stop(serial, id);

    serial.closePort();
    cout << "[DONE]" << endl;

    return 0;
}

10. 통신 오류 처리 (Error Handling)

  • 헤더 불일치 → 버퍼 초기화

  • Checksum 불일치 → 프레임 폐기

  • 수신 타임아웃 → 재요청

  • 응답 지연 발생 시 → 200~500µs 대기 후 재시도

11. 부록 (Appendix)

모터제어기 프로토콜

../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_MC-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-1.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_MC-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-2.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_MC-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-3.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_MC-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-4.png

스마트모터 프로토콜

../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_SA-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-1.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_SA-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-2.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_SA-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-3.png ../_images/%ED%86%B5%EC%8B%A0%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C_SA-RS485_V1.0.2_%EB%88%84%EB%A6%AC%EB%A1%9C%EB%B4%87-4.png